diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1444ca5d..f875ddf6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,10 +1,15 @@ name: CI on: - pull_request: push: branches: - - master - tags: '*' + - main + tags: "*" + pull_request: +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} @@ -13,13 +18,11 @@ jobs: fail-fast: false matrix: version: - - '1.0' - - '1' - - 'nightly' + - "1" + - "1.6" + - "nightly" os: - ubuntu-latest - - macOS-latest - - windows-latest arch: - x64 steps: @@ -28,19 +31,29 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v2 + with: + files: lcov.info + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 with: - file: lcov.info + version: "1" + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-docdeploy@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + - run: | + julia --project=docs -e ' + using Documenter: DocMeta, doctest + using GeoInterface + DocMeta.setdocmeta!(GeoInterface, :DocTestSetup, :(using GeoInterface); recursive=true) + doctest(GeoInterface)' diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index d94b38df..cba9134c 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -7,26 +7,10 @@ jobs: CompatHelper: runs-on: ubuntu-latest steps: - - name: "Add the General registry via Git" - run: | - import Pkg - ENV["JULIA_PKG_SERVER"] = "" - Pkg.Registry.add("General") - shell: julia --color=yes {0} - - name: "Install CompatHelper" - run: | - import Pkg - name = "CompatHelper" - uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" - version = "3" - Pkg.add(; name, uuid, version) - shell: julia --color=yes {0} - - name: "Run CompatHelper" - run: | - import CompatHelper - CompatHelper.main() - shell: julia --color=yes {0} + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.gitignore b/.gitignore index 42f07ead..e5a16dee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.jl.cov *.jl.mem +/Manifest.toml +/docs/build/ +.DS_Store +docs/src/reference/integrations.md diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md new file mode 100644 index 00000000..d528450e --- /dev/null +++ b/INTEGRATIONS.md @@ -0,0 +1,17 @@ +# Packages +Packages currently integrating with GeoInterface.jl: +* [AlgebraOfGraphics](https://github.com/JuliaPlots/AlgebraOfGraphics.jl.git) +* [ArchGDAL](https://github.com/yeesian/ArchGDAL.jl.git) +* [GADM](https://github.com/JuliaGeo/GADM.jl.git) +* [GeoData](https://github.com/rafaqz/GeoData.jl.git) +* [GeoDatasets](https://github.com/JuliaGeo/GeoDatasets.jl.git) +* [GeoJSON](https://github.com/JuliaGeo/GeoJSON.jl.git) +* [GeoMakie](https://github.com/JuliaPlots/GeoMakie.jl.git) +* [GeoTables](https://github.com/JuliaEarth/GeoTables.jl.git) +* [LibGEOS](https://github.com/JuliaGeo/LibGEOS.jl.git) +* [Mangal](https://github.com/EcoJulia/Mangal.jl.git) +* [OmniSci](https://github.com/omnisci/OmniSci.jl.git) +* [Rasters](https://github.com/rafaqz/Rasters.jl.git) +* [Shapefile](https://github.com/JuliaGeo/Shapefile.jl.git) +* [SpatialDependence](https://github.com/javierbarbero/SpatialDependence.jl.git) +* [Turf](https://github.com/philoez98/Turf.jl.git) diff --git a/LICENSE.md b/LICENSE.md index 2bbf8074..5509ad8e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The GeoInterface.jl package is licensed under the MIT "Expat" License: -> Copyright (c) 2015: Yeesian Ng. +> Copyright (c) 2015: Yeesian Ng, Julia Computing > > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the diff --git a/Project.toml b/Project.toml index f0bfd776..fff74913 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,17 @@ name = "GeoInterface" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -license = "MIT" -version = "0.5.7" +authors = ["JuliaGeo and contributors"] +version = "1.0.0" [deps] -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" [compat] -RecipesBase = "0.6, 0.7, 0.8, 1.0" julia = "1" [extras] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test", "Documenter"] diff --git a/README.md b/README.md index a57dd88e..c51e7a95 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,18 @@ -# GeoInterface.jl +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliageo.github.io/GeoInterface.jl/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliageo.github.io/GeoInterface.jl/dev) +[![Build Status](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml?query=branch%3Amain) -A Julia Protocol for Geospatial Data +# GeoInterface +An interface for geospatial vector data in [Julia](https://julialang.org/). -## Motivation -To support operations or visualization of multiple (but similar) implementations of vector data (across `GeoJSON.jl`, `LibGEOS.jl`, etc). As a starting point, it will follow the [GEO interface](https://gist.github.com/sgillies/2217756) [1] in Python (which in turn borrows its design from the [GeoJSON specification](http://geojson.org/) [2]). +This Package describe a set of traits based on the [Simple Features standard +(SF)](https://www.opengeospatial.org/standards/sfa) for geospatial vector data, including +the SQL/MM extension with support for circular geometry. Using these traits, it should be +easy to parse, serialize and use different geometries in the Julia ecosystem, without +knowing the specifics of each individual package. In that regard it is similar to +[Tables.jl](https://github.com/JuliaData/Tables.jl), but for geometries instead of tables. -## GEO Interface +Packages which support the GeoInterface.jl interface can be found in +[INTEGRATIONS.md](INTEGRATIONS.md). -### AbstractPosition -A position can be thought of as a tuple of numbers. There must be at least two elements, and may be more. The order of elements must follow `x`, `y`, `z` order (e.g. easting, northing, altitude for coordinates in a projected coordinate reference system, or longitude, latitude, altitude for coordinates in a geographic coordinate reference system). It requires the following methods: - -- `xcoord(::AbstractPosition)::Float64` -- `ycoord(::AbstractPosition)::Float64` -- `zcoord(::AbstractPosition)::Float64` -- `hasz(::AbstractPosition)::Bool` (`false` by default) - -Remark: Although the specification allows the representation of up to 3 dimensions, not all algorithms support require all 3 dimensions. Also, if you are working with an arbitrary `obj::AbstractPosition`, you should call `hasz(obj)` before calling `zcoord(obj)`. - -### AbstractGeometry -Represents vector geometry, and encompasses the following abstract types: `AbstractPoint, AbstractMultiPoint, AbstractLineString, AbstractMultiLineString, AbstractMultiPolygon, AbstractPolygon`. It requires the `coordinates` method, where - -- `coordinates(::AbstractPoint)` returns a single position. -- `coordinates(::AbstractMultiPoint)` returns a vector of positions. -- `coordinates(::AbstractLineString)` returns a vector of positions. -- `coordinates(::AbstractMultiLineString)` returns a vector of linestrings. -- `coordinates(::AbstractPolygon)` returns a vector of linestrings. -- `coordinates(::AbstractMultiPolygon)` returns a vector of polygons. - -### AbstractGeometryCollection -Represents a collection of geometries, and requires the `geometries` method, which returns a vector of geometries. Is also a subtype of `AbstractGeometry`. - -### AbstractFeature -Represents a geometry with additional attributes, and requires the following methods - -- `geometry(::AbstractFeature)::AbstractGeometry` returns the corresponding geometry -- `properties(::AbstractFeature)::Dict{AbstractString,Any}` returns a dictionary of the properties - -Optionally, you can also provide the following methods - -- `bbox(::AbstractFeature)::AbstractGeometry` returns the bounding box for that feature -- `crs(::AbstractFeature)::Dict{AbstractString,Any}` returns the coordinate reference system - -## Geospatial Geometries -If you don't need to provide your own user types, GeoInterface also provides a set of geometries (below), which implements the GEO Interface: - -- `CRS` -- `Position` -- `Geometry <: AbstractGeometry` - - `Point <: AbstractPoint <: AbstractGeometry` - - `MultiPoint <: AbstractMultiPoint <: AbstractGeometry` - - `LineString <: AbstractLineString <: AbstractGeometry` - - `MultiLineString <: AbstractMultiLineString <: AbstractGeometry` - - `Polygon <: AbstractPolygon <: AbstractGeometry` - - `MultiPolygon <: AbstractMultiPolygon <: AbstractGeometry` - - `GeometryCollection <: AbstractGeometryCollection <: AbstractGeometry` -- `Feature <: AbstractFeature` -- `FeatureCollection <: AbstractFeatureCollection` - -## Remarks - -Conceptually, - -- an `::AbstractGeometryCollection` maps to a `DataArray{::AbstractGeometry}`, and -- an `::AbstractFeatureCollection` maps to a `DataFrame`, where each row is an `AbstractFeature` - -The design of the types in GeoInterface differs from the GeoJSON specification in the following ways: - -- Julia Geometries do not provide a `bbox` and `crs` method. If you wish to provide a `bbox` or `crs` attribute, wrap the geometry into a `Feature` or `FeatureCollection`. -- Features do not have special fields for `id`, `bbox`, and `crs`. These are to be provided (or found) in the `properties` field, under the keys `featureid`, `bbox`, and `crs` respectively (if they exist). - -## References - -[1]: A Python Protocol for Geospatial Data ([gist](https://gist.github.com/sgillies/2217756)) - -[2]: GeoJSON Specification ([website](http://geojson.org/)) +We thank Julia Computing for supporting contributions to this package. diff --git a/docs/Manifest.toml b/docs/Manifest.toml new file mode 100644 index 00000000..a51f6f5d --- /dev/null +++ b/docs/Manifest.toml @@ -0,0 +1,111 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.7.2" +manifest_format = "2.0" + +[[deps.ANSIColoredPrinters]] +git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" +uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" +version = "0.0.1" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.8.6" + +[[deps.Documenter]] +deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "7d9a46421aef53cbd6b8ecc40c3dcbacbceaf40e" +uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +version = "0.27.15" + +[[deps.Extents]] +git-tree-sha1 = "a087a23129ac079d43ba6b534c6350325fcd41c9" +uuid = "411431e0-e8b7-467b-b5e0-f676ba4f2910" +version = "0.1.0" + +[[deps.GeoInterface]] +deps = ["Extents", "RecipesBase"] +path = ".." +uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" +version = "1.0.0" + +[[deps.IOCapture]] +deps = ["Logging", "Random"] +git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" +uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +version = "0.2.2" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[deps.JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.3" + +[[deps.LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[deps.Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[deps.Parsers]] +deps = ["Dates"] +git-tree-sha1 = "621f4f3b4977325b9128d5fae7a8b4829a0c2222" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.2.4" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.RecipesBase]] +git-tree-sha1 = "6bf3f380ff52ce0832ddd3a2a7b9538ed1bcca7d" +uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +version = "1.2.1" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 00000000..e4d2aa00 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 00000000..c9cb5a7a --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,42 @@ +using GeoInterface +using Documenter + +DocMeta.setdocmeta!(GeoInterface, :DocTestSetup, :(using GeoInterface); recursive=true) +cp(joinpath(@__DIR__, "../INTEGRATIONS.md"), joinpath(@__DIR__, "src/reference/integrations.md"); force=true) + +makedocs(; + modules=[GeoInterface], + authors="JuliaGeo and contributors", + repo="https://github.com/JuliaGeo/GeoInterface.jl/blob/{commit}{path}#{line}", + sitename="GeoInterface.jl", + format=Documenter.HTML(; + prettyurls=get(ENV, "CI", "false") == "true", + canonical="https://juliageo.github.io/GeoInterface.jl", + assets=String[] + ), + pages=[ + "Home" => "index.md", + "Background" => Any[ + "Simple Features"=>"background/sf.md", + "History"=>"background/history.md", + ], + "Tutorials" => Any[ + "Installation"=>"tutorials/installation.md", + "Usage"=>"tutorials/usage.md", + ], + "Guides" => Any[ + "For developers"=>"guides/developer.md", + "Defaults"=>"guides/defaults.md", + ], + "Reference" => Any[ + "API" => "reference/api.md" + "Implementations" => "reference/integrations.md" + ], + ], + doctest=true +) + +deploydocs(; + repo="github.com/JuliaGeo/GeoInterface.jl", + devbranch="main" +) diff --git a/docs/src/background/history.md b/docs/src/background/history.md new file mode 100644 index 00000000..cbb1d384 --- /dev/null +++ b/docs/src/background/history.md @@ -0,0 +1,21 @@ +# History +The previous pre-1.0 releases of GeoInterface.jl were smaller in scope, aligned to geointerface in Python [^sgillies] +which builds on GeoJSON [^geojson]. It provided abstract types and expected other geometries to be implemented as a subtype. +Recent Julia developments have shown that subtyping is difficult--you can only choose one supertype--and many packages moved to trait-based interfaces. Tables.jl is an excellent example of traits-based interface. + +[^sgillies]: https://gist.github.com/sgillies/2217756 +[^geojson]: https://geojson.org/ + +## Backwards compatibility +To keep function compatibility with pre-v1 releases--even while switching to traits--we keep the following methods. +```julia +# for Features +isfeature # new +geometry +properties + +# for Geometries +coordinates +``` + +However, the `position` type is gone and merged with `PointTrait`. diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md new file mode 100644 index 00000000..5df83ca5 --- /dev/null +++ b/docs/src/background/sf.md @@ -0,0 +1,60 @@ +```@meta +CurrentModule = GeoInterface +``` + +# Simple Features +Simple Features ([SF](https://en.wikipedia.org/wiki/Simple_Features)) are OGC standards describing two dimensional geographic features, such as Points and Polygons and the relations between them. +The standards describe a hierarchy of types (Part 1), a functional interface with SQL (Part II) and an SQL/MM extension with support for circular geometry types, such as `Circularstring`. + +## Type hierarchy +All types used here come from the SF. We added `Trait` to all geometry types here to distinguish them from actual geometry structs. + +![SF Type hierarchy. From the Simple Feature standard by OGC.](types.png) +`The SF Type hierarchy. From OpenGIS® Implementation Standard for Geographic information - Simple feature access - Part 1: Common architecture at http://www.opengis.net/doc/is/sfa/1.2.1.` + + +## Changes with respect to SF +While we try to adhere to SF, there are changes and extensions to make it more Julian. + +### Function names +All function names are without the `ST_` prefix and are lowercased. In some cases the names have changed as well, to be inline with common Julia functions. `NumX` becomes `nx` and `geomN` becomes `getgeom`: +```julia +GeometryType -> geomtype +NumGeometries -> ngeom +GeometryN -> getgeom +NumPatches -> npatch +# etc +``` + +We generalized [`ngeom`](@ref) and [`getgeom`](@ref) to apply to +all geometries, not just a [`AbstractGeometryCollectionTrait`](@ref)s. + +We also simplified the dimension functions. From the three original (`dimension`, `coordinateDimension`, `spatialDimension`) there's now only the coordinate dimension, by using `ncoords`, which represent coordinate dimensions like `X`, `Y`, `Z` and `M`. Topological dimensions (a point is 0-dimensional), and the functions related to it, are not used in this interface to prevent confusion. Similarly, we do not overload the Julia `ndims`, to prevent confusion and possible conflict with custom vector based geometries. + +```julia +coordinateDimension -> ncoords # x, y, z, m +dimension -> unused +spatialDimension -> unused +``` + +We've generalized the naming of some functions: +```julia +SRID -> crs +envelope -> extent # also aliased to bbox +``` + +And added a helper method to clarify the naming of coordinates. +```julia +coordnames = (:X, :Y, :Z, :M) +``` + +### Coverage +Not all SF functions are implemented, either as a possibly slower fallback or empty descriptor or not at all. The following SF functions are not (yet) available. + +```julia +dimension # topological dimensions +spatialDimension + +locateAlong +locateBetween +``` diff --git a/docs/src/background/types.png b/docs/src/background/types.png new file mode 100644 index 00000000..fa50ce9a Binary files /dev/null and b/docs/src/background/types.png differ diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md new file mode 100644 index 00000000..7157b7fa --- /dev/null +++ b/docs/src/guides/defaults.md @@ -0,0 +1,47 @@ +```@meta +CurrentModule = GeoInterface +``` + +# Defaults +There are *many* function in SF, but most only apply to a single geometry type. +Of note here are the `ngeom` and `getgeom` for each geometry type, which translate to the following function for each type automatically: + +| | ngeom | getgeom | +|----------------------------|-------------|---------------| +| [`AbstractPointTrait`](@ref) | - | - | +| [`AbstractCurveTrait`](@ref), [`MultiPointTrait`](@ref) | [`npoint(geom)`](@ref) | [`getpoint(geom)`](@ref) | +| [`AbstractPolygonTrait`](@ref) | [`nring(geom)`](@ref) | [`getring(geom)`](@ref) | +| [`AbstractMultiLineStringTrait`](@ref) | [`nlinestring(geom)`](@ref) | [`getlinestring(geom)`](@ref) | +| [`AbstractMultiPolygonTrait`](@ref) | [`npolygon(geom)`](@ref) | [`getpolygon(geom)`](@ref) | +| [`AbstractPolyhedralSurfaceTrait`](@ref) | [`npatch(geom)`](@ref) | [`getpatch(geom)`](@ref) | +| [`AbstractGeometryCollectionTrait`](@ref) | [`ngeom(geom)`](@ref) | [`getgeom(geom)`](@ref) | + +## Polygons +Of note are `PolygonTrait`s, which can have holes, for which we automatically add the following +functions based on the `ngeom` implemented by package authors. In some cases, the assumptions here +are not correct (most notably Shapefile), where the second ring is not necessarily a hole, but could +be another exterior. + +```julia +getexterior(p::AbstractPolygonTrait, geom) = getring(p, geom, 1) +nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1 +gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) +``` +## LineStrings +Similarly for `LineStringTrait`s, we have the following +```julia +startpoint(geom) = getpoint(geom, 1) +endpoint(geom) = getpoint(geom, length(geom)) +``` + +## Fallbacks +In some cases, we know the return value of a function for a specific geometry (sub)type beforehand and have implemented them. + +```julia +npoint(::LineTrait, geom) = 2 +npoint(::TriangleTrait, geom) = 3 +npoint(::RectangleTrait, geom) = 4 +npoint(::QuadTrait, geom) = 4 +npoint(::PentagonTrait, geom) = 5 +npoint(::HexagonTrait, geom) = 6 +``` diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md new file mode 100644 index 00000000..f020953b --- /dev/null +++ b/docs/src/guides/developer.md @@ -0,0 +1,182 @@ +```@meta +CurrentModule = GeoInterface +``` + +# Implementing GeoInterface +GeoInterface requires six functions to be defined for a custom geometry. On top of that +it could be useful to also implement some optional methods if they apply or are faster than the [Fallbacks](@ref). + +If your package also supports geospatial operations on geometries--such as intersections--, please +also implement those interfaces where applicable. + +Last but not least, we also provide an interface for features--geometries with properties--if applicable. + +## Required for Geometry + +```julia +GeoInterface.isgeometry(geom::customgeom)::Bool = true +GeoInterface.geomtype(geom::customgeom)::DataType = XTrait() # <: AbstractGeometryTrait +# for PointTraits +GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer +GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real +# for non PointTraits +GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer +GeoInterface.getgeom(geomtype(geom), geom::customgeom, i) +``` +Where the `getgeom` and `getcoord` could be an iterator (without the `i`) as well. It will return a new geom with the correct `geomtype`. +This means that a call to `getgeom` on a geometry that has a `LineStringTrait` should return something that implements the `PointTrait`. This hierarchy can be checked programmatically with [`subtrait`](@ref). You read more about the `geomtype` in the [Type hierarchy](@ref). + +The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for `LineStringTrait`s. + + +## Optional for Geometry + +There are also optional generic methods that could help with locating this geometry. +```julia +GeoInterface.crs(geomtype(geom), geom::customgeom)::GeoFormatTypes.GeoFormat} +GeoInterface.extent(geomtype(geom), geom::customgeom)::Extents.Extent +``` + +And lastly, there are many other optional functions for each specific geometry. GeoInterface provides fallback implementations based on the generic functions above, but these are not optimized. These are detailed in [Fallbacks](@ref). + +### Conversion +It is useful if others can convert any custom geometry into your +geometry type, if their custom geometry supports GeoInterface as well. +This requires the following three methods, and the last one requires more code to generate `T` with `ngeom`, `getgeom` or just `coordinates` calls. + +```julia +Base.convert(::Type{T}, geom) where T<:AbstractPackageType = Base.convert(T, geomtype(geom), geom) +Base.convert(::Type{T}, ::LineStringTrait, geom::T) = geom # fast fallthrough without conversion +Base.convert(::Type{T}, ::LineStringTrait, geom) = ... # slow custom conversion based on ngeom and getgeom +``` + +## Required for Feature +```julia +GeoInterface.isfeature(feat::customfeat)::Bool = true +GeoInterface.properties(feat::customfeat) +GeoInterface.geometry(feat::customfeat) +``` + +## GeoSpatial Operations +```julia +distance(geomtype(a), geomtype(b), a, b) +buffer(geomtype(geom), geom, distance) +convexhull(geomtype(geom), geom) +``` + +## GeoSpatial Relations +These functions are used to describe the relations between geometries as defined in the Dimensionally Extended 9-Intersection Model ([DE-9IM](https://en.wikipedia.org/wiki/DE-9IM)). + +```julia +equals(geomtype(a), geomtype(b), a, b) +disjoint(geomtype(a), geomtype(b), a, b) +intersects(geomtype(a), geomtype(b), a, b) +touches(geomtype(a), geomtype(b), a, b) +within(geomtype(a), geomtype(b), a, b) +contains(geomtype(a), geomtype(b), a, b) +overlaps(geomtype(a), geomtype(b), a, b) +crosses(geomtype(a), geomtype(b), a, b) +relate(geomtype(a), geomtype(b), a, b, relationmatrix) +``` + +## Geospatial Sets +```julia +symdifference(geomtype(a), geomtype(b), a, b) +difference(geomtype(a), geomtype(b), a, b) +intersection(geomtype(a), geomtype(b), a, b) +union(geomtype(a), geomtype(b), a, b) +``` + +## Testing the interface +GeoInterface provides a Testsuite for a geom type to check whether the required functions that have been correctly implemented and work as expected. + +```julia +GeoInterface.testgeometry(geom) +GeoInterface.testfeature(geom) +``` + +## Examples + +All custom geometries implement +```julia +GeoInterface.isgeometry(geom::customgeom)::Bool = true +``` + +A `geom::customgeom` with "Point"-like traits implements +```julia +GeoInterface.geomtype(geom::customgeom)::DataType = PointTrait() +GeoInterface.ncoord(::PointTrait, geom::customgeom)::Integer +GeoInterface.getcoord(::PointTrait, geom::customgeom, i)::Real + +# Defaults +GeoInterface.ngeom(::PointTrait, geom)::Integer = 0 +GeoInterface.getgeom(::PointTrait, geom::customgeom, i) = nothing +``` + +A `geom::customgeom` with "LineString"-like traits implements the following methods: +```julia +GeoInterface.geomtype(geom::customgeom)::DataType = LineStringTrait() +GeoInterface.ncoord(::LineStringTrait, geom::customgeom)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::LineStringTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::LineStringTrait, geom::customgeom, i) # of geomtype Point + +# Optional +GeoInterface.isclosed(::LineStringTrait, geom::customgeom)::Bool +GeoInterface.issimple(::LineStringTrait, geom::customgeom)::Bool +GeoInterface.length(::LineStringTrait, geom::customgeom)::Real +``` +A `geom::customgeom` with "Polygon"-like traits can implement the following methods: +```julia +GeoInterface.geomtype(geom::customgeom)::DataType = PolygonTrait() +GeoInterface.ncoord(::PolygonTrait, geom::customgeom)::Integer + +# These alias for nring and getring +GeoInterface.ngeom(::PolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::PolygonTrait, geom::customgeom, i)::"LineStringTrait" + +# Optional +GeoInterface.area(::PolygonTrait, geom::customgeom)::Real +GeoInterface.centroid(::PolygonTrait, geom::customgeom)::"PointTrait" +GeoInterface.pointonsurface(::PolygonTrait, geom::customgeom)::"PointTrait" +GeoInterface.boundary(::PolygonTrait, geom::customgeom)::"LineStringTrait" +``` + +A `geom::customgeom` with "GeometryCollection"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::customgeom) = GeometryCollectionTrait() +GeoInterface.ncoord(::GeometryCollectionTrait, geom::customgeom)::Integer +GeoInterface.ngeom(::GeometryCollectionTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeometryCollectionTrait,geom::customgeomm, i)::"GeometryTrait" +``` + +A `geom::customgeom` with "MultiPoint"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::customgeom) = MultiPointTrait() +GeoInterface.ncoord(::MultiPointTrait, geom::customgeom)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::MultiPointTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::MultiPointTrait, geom::customgeom, i)::"PointTrait" +``` + +A `geom::customgeom` with "MultiLineString"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::customgeom) = MultiLineStringTrait() +GeoInterface.ncoord(::MultiLineStringTrait, geom::customgeom)::Integer + +# These alias for nlinestring and getlinestring +GeoInterface.ngeom(::MultiLineStringTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::MultiLineStringTrait,geom::customgeomm, i)::"LineStringTrait" +``` + +A `geom::customgeom` with "MultiPolygon"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::customgeom) = MultiPolygonTrait() +GeoInterface.ncoord(::MultiPolygonTrait, geom::customgeom)::Integer + +# These alias for npolygon and getpolygon +GeoInterface.ngeom(::MultiPolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::MultiPolygonTrait, geom::customgeom, i)::"PolygonTrait" +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..a2013d0b --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,23 @@ +```@meta +CurrentModule = GeoInterface +``` + +# GeoInterface +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliageo.github.io/GeoInterface.jl/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliageo.github.io/GeoInterface.jl/dev) +[![Build Status](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml?query=branch%3Amain) + +*An interface for geospatial vector data in Julia* + +This Package describe a set of traits based on the [Simple Features standard (SF)](https://www.opengeospatial.org/standards/sfa) +for geospatial vector data, including the SQL/MM extension with support for circular geometry. +By using these traits, it should be easy to parse, serialize and use different custom geometries in the Julia ecosystem, +without knowing the specifics of each individual package. In that regard it is similar to Tables.jl, but for geometries instead of tables. + +Packages which support the GeoInterface.jl interface can be found in [Packages](@ref). + +For usage see [Traits interface](@ref), while if you look to implement GeoInterface in your own package, check out [Implementing GeoInterface](@ref). +For background about the interface and Simple Features, see [Changes with respect to SF](@ref). + +!!! compat + This traits interface is new and is a major departure from previous pre-1.0 releases. See [History](@ref) for more information. Feel free to ask questions on Github. diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md new file mode 100644 index 00000000..bf0132f9 --- /dev/null +++ b/docs/src/reference/api.md @@ -0,0 +1,21 @@ +```@meta +CurrentModule = GeoInterface +``` + +# API + +## Functions +```@autodocs +Modules = [GeoInterface] +Order = [:function] +``` + +## Types +```@autodocs +Modules = [GeoInterface] +Order = [:type] +``` + +## Index +```@index +``` diff --git a/docs/src/tutorials/installation.md b/docs/src/tutorials/installation.md new file mode 100644 index 00000000..7ad7ffd1 --- /dev/null +++ b/docs/src/tutorials/installation.md @@ -0,0 +1,7 @@ +# Installation + +Simply do + +```julia +]add GeoInterface +``` diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md new file mode 100644 index 00000000..c1f7bee4 --- /dev/null +++ b/docs/src/tutorials/usage.md @@ -0,0 +1,52 @@ +```@meta +CurrentModule = GeoInterface +``` + +# Traits interface +GeoInterface provides a traits interface, not unlike Tables.jl, by a set of functions and types for geospatial data. + +## Functions +(a) a set of functions: +```julia +isgeometry(geom) +geomtype(geom) +ncoord(geom) +getcoord(geom, i) +ngeom(geom) +getgeom(geom, i) +... +``` + +## Types +(b) a set of trait-types for dispatching on said functions. + +The types tell GeoInterface how to interpret the input object inside a GeoInterface function and are specific for each type of Geometry. + +```julia +abstract GeometryTrait +PointTrait <: AbstractPointTrait <: AbstractGeometryTrait +MultiPointTrait <: AbstractMultiPointGeometryTrait <:AbstractGeometryCollectionTrait <: AbstractGeometryTrait +... +``` + +## Use +For the [Packages](@ref) that implement GeoInterface, instead of needing to write specific methods +to work with their custom geometries, you can just call the above generic functions. For example: + +``` +julia> using ArchGDAL +julia> geom = createpolygon(...)::ArchGDAL.IGeometry # no idea about the interface + +# Inspect with GeoInterface methods +julia> isgeometry(geom) +True +julia> geomtype(geom) +PolygonTrait() +julia> ext = exterior(geom); +julia> geomtype(ext) +LineStringTrait() +julia> getcoords.(getpoint.(Ref(ext), 1:npoint(ext))) +[[1.,2.],[2.,3.],[1.,2.]] +julia> coordinates(geom) # fallback based on ngeom & npoint above + +``` diff --git a/find_integrations.jl b/find_integrations.jl new file mode 100644 index 00000000..a4bdfb92 --- /dev/null +++ b/find_integrations.jl @@ -0,0 +1,39 @@ +# Adapted from https://raw.githubusercontent.com/JuliaData/Tables.jl/main/find_integrations.jl +#### +#### automatically generate a list of integrations +#### + +### +### Usage +### +### 1. ensure a development version of GeoInterface.jl (`pkg> add GeoInterface`) +### 2. make sure the General registry is up to date (`pkg> up`) +### 3. run this script, which uses the first depot from DEPOT_PATH + +DEPOT = first(DEPOT_PATH) +REGISTRIES = joinpath(DEPOT, "registries") +@info DEPOT +# find each package w/ a direct dependency on GeoInterface.jl +general = joinpath(DEPOT, "General") +mkpath(general) +# run(`tar -xzf General.tar.gz -C $general`) +pkgs = cd(REGISTRIES) do + dirname.(readlines(`grep -rl ^GeoInterface General/. --include=Deps.toml`)) +end + +pkgnames = sort([splitpath(x)[end] for x in pkgs]) + +function parseurl(file) + repo = readlines(file)[end] + return strip(split(repo, " = ")[end], '"') +end + +urls = [parseurl(joinpath(REGISTRIES, "General", string(first(nm)), nm, "Package.toml")) + for nm in pkgnames] + +open(joinpath(dirname(@__DIR__), "GeoInterface.jl", "INTEGRATIONS.md"), "w+") do io + println(io, "# Packages\nPackages currently integrating with GeoInterface.jl:") + for (nm, url) in zip(pkgnames, urls) + println(io, "* [$nm]($url)") + end +end diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index bf36a8d7..c83a5c97 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -1,85 +1,49 @@ -__precompile__() - module GeoInterface - using RecipesBase - - export AbstractPosition, Position, - AbstractGeometry, AbstractGeometryCollection, GeometryCollection, - AbstractPoint, Point, - AbstractMultiPoint, MultiPoint, - AbstractLineString, LineString, - AbstractMultiLineString, MultiLineString, - AbstractPolygon, Polygon, - AbstractMultiPolygon, MultiPolygon, - AbstractFeature, Feature, - AbstractFeatureCollection, FeatureCollection, - - geotype, # methods - xcoord, ycoord, zcoord, hasz, - coordinates, - geometries, - geometry, bbox, crs, properties, - features - - abstract type AbstractPosition{T <: Real} <: AbstractVector{T} end - geotype(::AbstractPosition) = :Position - xcoord(::AbstractPosition) = error("xcoord(::AbstractPosition) not defined.") - ycoord(::AbstractPosition) = error("ycoord(::AbstractPosition) not defined.") - # optional - zcoord(::AbstractPosition) = error("zcoord(::AbstractPosition) not defined.") - hasz(::AbstractPosition) = false - coordinates(p::AbstractPosition) = hasz(p) ? Float64[xcoord(p),ycoord(p),zcoord(p)] : Float64[xcoord(p),ycoord(p)] - # (Array-like indexing # http://julia.readthedocs.org/en/latest/manual/arrays/#arrays) - Base.eltype(p::AbstractPosition{T}) where {T <: Real} = T - Base.ndims(AbstractPosition) = 1 - Base.length(p::AbstractPosition) = hasz(p) ? 3 : 2 - Base.size(p::AbstractPosition) = (length(p),) - Base.size(p::AbstractPosition, n::Int) = (n == 1) ? length(p) : 1 - Base.getindex(p::AbstractPosition, i::Int) = (i==1) ? xcoord(p) : (i==2) ? ycoord(p) : (i==3) ? zcoord(p) : nothing - Base.convert(::Type{Vector{Float64}}, p::AbstractPosition) = coordinates(p) - # Base.linearindexing{T <: AbstractPosition}(::Type{T}) = LinearFast() - - abstract type AbstractGeometry end - coordinates(obj::AbstractGeometry) = error("coordinates(::AbstractGeometry) not defined.") - - abstract type AbstractPoint <: AbstractGeometry end - geotype(::AbstractPoint) = :Point - - abstract type AbstractMultiPoint <: AbstractGeometry end - geotype(::AbstractMultiPoint) = :MultiPoint - - abstract type AbstractLineString <: AbstractGeometry end - geotype(::AbstractLineString) = :LineString - - abstract type AbstractMultiLineString <: AbstractGeometry end - geotype(::AbstractMultiLineString) = :MultiLineString - - abstract type AbstractPolygon <: AbstractGeometry end - geotype(::AbstractPolygon) = :Polygon - - abstract type AbstractMultiPolygon <: AbstractGeometry end - geotype(::AbstractMultiPolygon) = :MultiPolygon - - abstract type AbstractGeometryCollection <: AbstractGeometry end - geotype(::AbstractGeometryCollection) = :GeometryCollection - geometries(obj::AbstractGeometryCollection) = error("geometries(::AbstractGeometryCollection) not defined.") - - abstract type AbstractFeature end - geotype(::AbstractFeature) = :Feature - geometry(obj::AbstractFeature) = error("geometry(::AbstractFeature) not defined.") - # optional - properties(obj::AbstractFeature) = Dict{String,Any}() - - abstract type AbstractFeatureCollection end - geotype(::AbstractFeatureCollection) = :FeatureCollection - features(obj::AbstractFeatureCollection) = error("features(::AbstractFeatureCollection) not defined.") - - # optional - bbox(obj) = nothing - crs(obj) = nothing - - include("operations.jl") - include("geotypes.jl") - include("plotrecipes.jl") -end +using Base.Iterators: flatten + +export testgeometry, isgeometry, geomtype, ncoord, getcoord, ngeom, getgeom + +# traits +export AbstractGeometryTrait, + AbstractGeometryCollectionTrait, + GeometryCollectionTrait, + AbstractPointTrait, + PointTrait, + AbstractCurveTrait, + AbstractLineStringTrait, + LineStringTrait, + LineTrait, + LinearRingTrait, + CircularStringTrait, + CompoundCurveTrait, + AbstractSurfaceTrait, + AbstractCurvePolygonTrait, + CurvePolygonTrait, + AbstractPolygonTrait, + PolygonTrait, + TriangleTrait, + RectangleTrait, + QuadTrait, + PentagonTrait, + HexagonTrait, + AbstractPolyhedralSurfaceTrait, + PolyhedralSurfaceTrait, + TINTrait, + AbstractMultiPointTrait, + MultiPointTrait, + AbstractMultiCurveTrait, + MultiCurveTrait, + AbstractMultiLineStringTrait, + MultiLineStringTrait, + AbstractMultiSurfaceTrait, + MultiSurfaceTrait, + AbstractMultiPolygonTrait, + MultiPolygonTrait + +include("types.jl") +include("interface.jl") +include("fallbacks.jl") +include("utils.jl") + +end # module diff --git a/src/fallbacks.jl b/src/fallbacks.jl new file mode 100644 index 00000000..7bb30fe9 --- /dev/null +++ b/src/fallbacks.jl @@ -0,0 +1,144 @@ +# Defaults for many of the interface functions are defined here as fallback. +# Methods here should take a trait instance as first argument and should already be defined +# in the `interface.jl` first as a generic f(geom) method. + +## Coords +# Four options in SF, xy, xyz, xym, xyzm +const default_coord_names = (:X, :Y, :Z, :M) + +coordnames(t::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(t, geom)] + +# Maybe hardcode dimension order? At least for X and Y? +x(t::AbstractPointTrait, geom) = getcoord(t, geom, findfirst(isequal(:X), coordnames(geom))) +y(t::AbstractPointTrait, geom) = getcoord(t, geom, findfirst(isequal(:Y), coordnames(geom))) +z(t::AbstractPointTrait, geom) = getcoord(t, geom, findfirst(isequal(:Z), coordnames(geom))) +m(t::AbstractPointTrait, geom) = getcoord(t, geom, findfirst(isequal(:M), coordnames(geom))) + +is3d(::AbstractPointTrait, geom) = :Z in coordnames(geom) +ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) +isempty(T, geom) = false + +## Points +ngeom(::AbstractPointTrait, geom) = 0 +getgeom(::AbstractPointTrait, geom) = nothing +getgeom(::AbstractPointTrait, geom, i) = nothing + +## LineStrings +npoint(t::AbstractCurveTrait, geom) = ngeom(t, geom) +getpoint(t::AbstractCurveTrait, geom) = getgeom(t, geom) +getpoint(t::AbstractCurveTrait, geom, i) = getgeom(t, geom, i) +startpoint(t::AbstractCurveTrait, geom) = getpoint(t, geom, 1) +endpoint(t::AbstractCurveTrait, geom) = getpoint(t, geom, npoint(geom)) + +## Polygons +nring(t::AbstractPolygonTrait, geom) = ngeom(t, geom) +getring(t::AbstractPolygonTrait, geom) = getgeom(t, geom) +getring(t::AbstractPolygonTrait, geom, i) = getgeom(t, geom, i) +getexterior(t::AbstractPolygonTrait, geom) = getring(t, geom, 1) +nhole(t::AbstractPolygonTrait, geom) = nring(t, geom) - 1 +gethole(t::AbstractPolygonTrait, geom) = (getgeom(t, geom, i) for i in 2:ngeom(t, geom)) +gethole(t::AbstractPolygonTrait, geom, i) = getring(t, geom, i + 1) +npoint(t::AbstractPolygonTrait, geom) = sum(npoint(p) for p in getring(t, geom)) +getpoint(t::AbstractPolygonTrait, geom) = flatten((p for p in getpoint(r)) for r in getring(t, geom)) + +## MultiPoint +npoint(t::AbstractMultiPointTrait, geom) = ngeom(t, geom) +getpoint(t::AbstractMultiPointTrait, geom) = getgeom(t, geom) +getpoint(t::AbstractMultiPointTrait, geom, i) = getgeom(t, geom, i) + +## MultiLineString +nlinestring(t::AbstractMultiCurveTrait, geom) = ngeom(t, geom) +getlinestring(t::AbstractMultiCurveTrait, geom) = getgeom(t, geom) +getlinestring(t::AbstractMultiCurveTrait, geom, i) = getgeom(t, geom, i) +npoint(t::AbstractMultiCurveTrait, geom) = sum(npoint(ls) for ls in getgeom(t, geom)) +getpoint(t::AbstractMultiCurveTrait, geom) = flatten((p for p in getpoint(ls)) for ls in getgeom(t, geom)) + +## MultiPolygon +npolygon(t::AbstractMultiPolygonTrait, geom) = ngeom(t, geom) +getpolygon(t::AbstractMultiPolygonTrait, geom) = getgeom(t, geom) +getpolygon(t::AbstractMultiPolygonTrait, geom, i) = getgeom(t, geom, i) +nring(t::AbstractMultiPolygonTrait, geom) = sum(nring(p) for p in getpolygon(t, geom)) +getring(t::AbstractMultiPolygonTrait, geom) = flatten((r for r in getring(p)) for p in getpolygon(t, geom)) +npoint(t::AbstractMultiPolygonTrait, geom) = sum(npoint(r) for r in getring(t, geom)) +getpoint(t::AbstractMultiPolygonTrait, geom) = flatten((p for p in getpoint(r)) for r in getring(t, geom)) + +## Surface +npatch(t::AbstractPolyhedralSurfaceTrait, geom)::Integer = ngeom(t, geom) +getpatch(t::AbstractPolyhedralSurfaceTrait, geom) = getgeom(t, geom) +getpatch(t::AbstractPolyhedralSurfaceTrait, geom, i::Integer) = getgeom(t, geom, i) + +## Default iterator +getgeom(t::AbstractGeometryTrait, geom) = (getgeom(t, geom, i) for i in 1:ngeom(t, geom)) +getcoord(t::AbstractPointTrait, geom) = (getcoord(t, geom, i) for i in 1:ncoord(t, geom)) + +## Special geometries +npoint(::LineTrait, geom) = 2 +npoint(::TriangleTrait, geom) = 3 +nring(::TriangleTrait, geom) = 1 +npoint(::RectangleTrait, geom) = 4 +nring(::RectangleTrait, geom) = 1 +npoint(::QuadTrait, geom) = 4 +nring(::QuadTrait, geom) = 1 +npoint(::PentagonTrait, geom) = 5 +nring(::PentagonTrait, geom) = 1 +npoint(::HexagonTrait, geom) = 6 +nring(::HexagonTrait, geom) = 1 + +# TODO Only simple if it's also not intersecting itself, except for its endpoints +issimple(t::AbstractCurveTrait, geom) = allunique((getpoint(t, geom, i) for i in 1:npoint(geom)-1)) && allunique((getpoint(t, geom, i) for i in 2:npoint(t, geom))) +isclosed(t::AbstractCurveTrait, geom) = getpoint(t, geom, 1) == getpoint(t, geom, npoint(t, geom)) +isring(t::AbstractCurveTrait, geom) = issimple(t, geom) && isclosed(t, geom) + +issimple(t::AbstractMultiPointTrait, geom) = allunique((getgeom(t, geom))) + +issimple(t::AbstractMultiCurveTrait, geom) = all(issimple.(getgeom(t, geom))) +isclosed(t::AbstractMultiCurveTrait, geom) = all(isclosed.(getgeom(t, geom))) + +crs(::AbstractGeometryTrait, geom) = nothing +extent(::AbstractGeometryTrait, geom) = nothing + +# Backwards compatibility +function coordinates(t::AbstractPointTrait, geom) + collect(getcoord(t, geom)) +end +function coordinates(t::AbstractGeometryTrait, geom) + collect(coordinates.(getgeom(t, geom))) +end + +Base.convert(T::Type, ::AbstractGeometryTrait, geom) = error("Conversion is enabled for type $T, but not implemented. Please report this issue to the package maintainer.") + +# Subtraits + +""" + subtrait(t::AbstractGeometryTrait) + +Gets the expected, possible abstract, (sub)trait for subgeometries (retrieved with +[`getgeom`](@ref)) of trait `t`. This follows the [Type hierarchy](@ref) of Simple Features. + +# Examples +```jldoctest; setup = :(using GeoInterface) +julia> GeoInterface.subtrait(LineStringTrait()) +AbstractPointTrait +julia> GeoInterface.subtrait(PolygonTrait()) # Any of LineStringTrait, LineTrait, LinearRingTrait +AbstractLineStringTrait +``` +```jldoctest; setup = :(using GeoInterface) +# `nothing` is returned when there's no subtrait or when it's not known beforehand +julia> isnothing(GeoInterface.subtrait(PointTrait())) +true +julia> isnothing(GeoInterface.subtrait(GeometryCollectionTrait())) +true +``` +""" +subtrait(::AbstractPointTrait) = nothing +subtrait(::AbstractCurveTrait) = AbstractPointTrait +subtrait(::AbstractCurvePolygonTrait) = AbstractCurveTrait +subtrait(::AbstractPolygonTrait) = AbstractLineStringTrait +subtrait(::AbstractPolyhedralSurfaceTrait) = AbstractPolygonTrait +subtrait(::TINTrait) = TriangleTrait +subtrait(::AbstractMultiPointTrait) = AbstractPointTrait +subtrait(::AbstractMultiLineStringTrait) = AbstractLineStringTrait +subtrait(::AbstractMultiPolygonTrait) = AbstractPolygonTrait +subtrait(::AbstractMultiSurfaceTrait) = AbstractSurfaceTrait +subtrait(::AbstractGeometryCollectionTrait) = nothing +subtrait(::AbstractGeometryTrait) = nothing # fallback diff --git a/src/geotypes.jl b/src/geotypes.jl deleted file mode 100644 index 4653863a..00000000 --- a/src/geotypes.jl +++ /dev/null @@ -1,145 +0,0 @@ -# Coordinate Reference System Objects -# (has keys "type" and "properties") -# TODO: Handle full CRS spec -const CRS = Dict{String,Any} - -# Bounding Boxes -# The value of the bbox member must be a 2*n array, -# where n is the number of dimensions represented in the contained geometries, -# with the lowest values for all axes followed by the highest values. - -# The axes order of a bbox follows the axes order of geometries. -# In addition, the coordinate reference system for the bbox is assumed to match -# the coordinate reference system of the GeoJSON object of which it is a member. -const BBox = Vector{Float64} - -const Position = Vector{Float64} -# (x, y, [z, ...]) - meaning of additional elements undefined. -# In an object's contained geometries, Positions must have uniform dimensions. -geotype(::Position) = :Position -xcoord(p::Position) = p[1] -ycoord(p::Position) = p[2] -zcoord(p::Position) = hasz(p) ? p[3] : zero(T) -hasz(p::Position) = length(p) >= 3 -coordinates(obj::Position) = obj - -coordinates(obj::Vector{Position}) = obj -coordinates(obj::Vector{T}) where {T <: AbstractPosition} = Position[map(coordinates, obj)...] -coordinates(obj::Vector{T}) where {T <: AbstractPoint} = Position[map(coordinates, obj)...] - -coordinates(obj::Vector{Vector{Position}}) = obj -coordinates(obj::Vector{Vector{T}}) where {T <: AbstractPosition} = Vector{Position}[map(coordinates, obj)...] -coordinates(obj::Vector{Vector{T}}) where {T <: AbstractPoint} = Vector{Position}[map(coordinates, obj)...] -coordinates(obj::Vector{T}) where {T <: AbstractLineString} = Vector{Position}[map(coordinates, obj)...] - -coordinates(obj::Vector{Vector{Vector{Position}}}) = obj -coordinates(obj::Vector{Vector{Vector{T}}}) where {T <: AbstractPosition} = Vector{Vector{Position}}[map(coordinates, obj)...] -coordinates(obj::Vector{Vector{Vector{T}}}) where {T <: AbstractPoint} = Vector{Vector{Position}}[map(coordinates, obj)...] -coordinates(obj::Vector{Vector{T}}) where {T <: AbstractLineString} = Vector{Vector{Position}}[map(coordinates, obj)...] -coordinates(obj::Vector{T}) where {T <: AbstractPolygon} = Vector{Vector{Position}}[map(coordinates, obj)...] - -mutable struct Point <: AbstractPoint - coordinates::Position -end -Point(x::Float64,y::Float64) = Point([x,y]) -Point(x::Float64,y::Float64,z::Float64) = Point([x,y,z]) -Point(point::AbstractPosition) = Point(coordinates(point)) -Point(point::AbstractPoint) = Point(coordinates(point)) - -mutable struct MultiPoint <: AbstractMultiPoint - coordinates::Vector{Position} -end -MultiPoint(point::Position) = MultiPoint(Position[point]) -MultiPoint(point::AbstractPosition) = MultiPoint(Position[coordinates(point)]) -MultiPoint(point::AbstractPoint) = MultiPoint(Position[coordinates(point)]) - -MultiPoint(points::Vector{T}) where {T <: AbstractPosition} = MultiPoint(coordinates(points)) -MultiPoint(points::Vector{T}) where {T <: AbstractPoint} = MultiPoint(coordinates(points)) -MultiPoint(points::AbstractMultiPoint) = MultiPoint(coordinates(points)) -MultiPoint(line::AbstractLineString) = MultiPoint(coordinates(line)) - -mutable struct LineString <: AbstractLineString - coordinates::Vector{Position} -end -LineString(points::Vector{T}) where {T <: AbstractPosition} = LineString(coordinates(points)) -LineString(points::Vector{T}) where {T <: AbstractPoint} = LineString(coordinates(points)) -LineString(points::AbstractMultiPoint) = LineString(coordinates(points)) -LineString(line::AbstractLineString) = LineString(coordinates(line)) - -mutable struct MultiLineString <: AbstractMultiLineString - coordinates::Vector{Vector{Position}} -end -MultiLineString(line::Vector{Position}) = MultiLineString(Vector{Position}[line]) -MultiLineString(line::Vector{T}) where {T <: AbstractPosition} = MultiLineString(Vector{Position}[coordinates(line)]) -MultiLineString(line::Vector{T}) where {T <: AbstractPoint} = MultiLineString(Vector{Position}[coordinates(line)]) -MultiLineString(line::AbstractLineString) = MultiLineString(Vector{Position}[coordinates(line)]) - -MultiLineString(lines::Vector{Vector{T}}) where {T <: AbstractPosition} = MultiLineString(coordinates(lines)) -MultiLineString(lines::Vector{Vector{T}}) where {T <: AbstractPoint} = MultiLineString(coordinates(lines)) -MultiLineString(lines::Vector{T}) where {T <: AbstractLineString} = MultiLineString(Vector{Position}[map(coordinates,lines)]) -MultiLineString(lines::AbstractMultiLineString) = MultiLineString(coordinates(lines)) -MultiLineString(poly::AbstractPolygon) = MultiLineString(coordinates(poly)) - -mutable struct Polygon <: AbstractPolygon - coordinates::Vector{Vector{Position}} -end -Polygon(line::Vector{Position}) = Polygon(Vector{Position}[line]) -Polygon(line::Vector{T}) where {T <: AbstractPosition} = Polygon(Vector{Position}[coordinates(line)]) -Polygon(line::Vector{T}) where {T <: AbstractPoint} = Polygon(Vector{Position}[coordinates(line)]) -Polygon(line::AbstractLineString) = Polygon(Vector{Position}[coordinates(line)]) - -Polygon(lines::Vector{Vector{T}}) where {T <: AbstractPosition} = Polygon(coordinates(lines)) -Polygon(lines::Vector{Vector{T}}) where {T <: AbstractPoint} = Polygon(coordinates(lines)) -Polygon(lines::Vector{T}) where {T <: AbstractLineString} = Polygon(coordinates(lines)) -Polygon(lines::AbstractMultiLineString) = Polygon(coordinates(lines)) -Polygon(poly::AbstractPolygon) = Polygon(coordinates(poly)) - -mutable struct MultiPolygon <: AbstractMultiPolygon - coordinates::Vector{Vector{Vector{Position}}} -end -MultiPolygon(line::Vector{Position}) = MultiPolygon(Vector{Vector{Position}}[Vector{Position}[line]]) -MultiPolygon(line::Vector{T}) where {T <: AbstractPosition} = MultiPolygon(Vector{Vector{Position}}[Vector{Position}[coordinates(line)]]) -MultiPolygon(line::Vector{T}) where {T <: AbstractPoint} = MultiPolygon(Vector{Vector{Position}}[Vector{Position}[coordinates(line)]]) -MultiPolygon(line::AbstractLineString) = MultiPolygon(Vector{Vector{Position}}[Vector{Position}[coordinates(line)]]) - -MultiPolygon(poly::Vector{Vector{T}}) where {T <: AbstractPosition} = MultiPolygon(Vector{Vector{Position}}[coordinates(poly)]) -MultiPolygon(poly::Vector{Vector{T}}) where {T <: AbstractPoint} = MultiPolygon(Vector{Vector{Position}}[coordinates(poly)]) -MultiPolygon(poly::Vector{T}) where {T <: AbstractLineString} = MultiPolygon(Vector{Vector{Position}}[coordinates(poly)]) -MultiPolygon(poly::AbstractMultiLineString) = MultiPolygon(Vector{Vector{Position}}[coordinates(poly)]) -MultiPolygon(poly::AbstractPolygon) = MultiPolygon(Vector{Vector{Position}}[coordinates(poly)]) - -MultiPolygon(polys::Vector{Vector{Vector{T}}}) where {T <: AbstractPosition} = MultiPolygon(coordinates(polys)) -MultiPolygon(polys::Vector{Vector{Vector{T}}}) where {T <: AbstractPoint} = MultiPolygon(coordinates(polys)) -MultiPolygon(polys::Vector{Vector{T}}) where {T <: AbstractLineString} = MultiPolygon(coordinates(polys)) -MultiPolygon(polys::Vector{T}) where {T <: AbstractPolygon} = MultiPolygon(coordinates(polys)) -MultiPolygon(polys::AbstractMultiPolygon) = MultiPolygon(coordinates(polys)) - -for geom in (:MultiPolygon, :Polygon, :MultiLineString, :LineString, :MultiPoint, :Point) - @eval coordinates(obj::$geom) = obj.coordinates -end - -mutable struct GeometryCollection <: AbstractGeometryCollection - geometries::Vector -end -geometries(collection::GeometryCollection) = collection.geometries - -mutable struct Feature <: AbstractFeature - geometry::Union{Nothing, AbstractGeometry} - properties::Union{Nothing, Dict{String,Any}} -end -Feature(geometry::Union{Nothing,GeoInterface.AbstractGeometry}) = Feature(geometry, Dict{String,Any}()) -Feature(properties::Dict{String,Any}) = Feature(nothing, properties) -geometry(feature::Feature) = feature.geometry -properties(feature::Feature) = feature.properties -bbox(feature::Feature) = get(feature.properties, "bbox", nothing) -crs(feature::Feature) = get(feature.properties, "crs", nothing) - -mutable struct FeatureCollection{T <: AbstractFeature} <: AbstractFeatureCollection - features::Vector{T} - bbox::Union{Nothing, BBox} - crs::Union{Nothing, CRS} -end -FeatureCollection(fc::Vector{T}) where {T <: AbstractFeature} = FeatureCollection(fc, nothing, nothing) -features(fc::FeatureCollection) = fc.features -bbox(fc::FeatureCollection) = fc.bbox -crs(fc::FeatureCollection) = fc.crs diff --git a/src/interface.jl b/src/interface.jl new file mode 100644 index 00000000..0c5d70a4 --- /dev/null +++ b/src/interface.jl @@ -0,0 +1,578 @@ +# All Geometries +""" + GeoInterface.isgeometry(x) => Bool + +Check if an object `x` is a geometry and thus implicitly supports GeoInterface methods. +It is recommended that for users implementing `MyType`, they define only +`isgeometry(::Type{MyType})`. `isgeometry(::MyType)` will then automatically delegate to this +method. +""" +isgeometry(x::T) where {T} = isgeometry(T) +isgeometry(::Type{T}) where {T} = false + +""" + GeoInterface.isfeature(x) => Bool + +Check if an object `x` is a feature and thus implicitly supports some GeoInterface methods. +A feature is a combination of a geometry and properties, not unlike a row in a table. +It is recommended that for users implementing `MyType`, they define only +`isfeature(::Type{MyType})`. `isfeature(::MyType)` will then automatically delegate to this +method. + +Ensures backwards compatibility with GeoInterface version 0. +""" +isfeature(x::T) where {T} = isfeature(T) +isfeature(::Type{T}) where {T} = false + +""" + GeoInterface.geometry(feat) => geom + +Retrieve the geometry of `feat`. It is expected that `isgeometry(geom) === true`. +Ensures backwards compatibility with GeoInterface version 0. +""" +geometry(feat) = nothing + +""" + GeoInterface.properties(feat) => properties + +Retrieve the properties of `feat`. This can be any Iterable that behaves like an AbstractRow. +Ensures backwards compatibility with GeoInterface version 0. +""" +properties(feat) = nothing + +""" + GeoInterface.geomtype(geom) => T <: AbstractGeometry + +Returns the geometry type, such as [`PolygonTrait`](@ref) or [`PointTrait`](@ref). +""" +geomtype(geom) = nothing + +# All types +""" + ncoord(geom) -> Integer + +Return the number of coordinate dimensions (such as 3 for X,Y,Z) for the geometry. +Note that SF distinguishes between dimensions, spatial dimensions and topological dimensions, which we do not. +""" +ncoord(geom) = ncoord(geomtype(geom), geom) + +""" + coordnames(geom) -> Tuple{Symbol} + +Return the names of coordinate dimensions (such for (:X,:Y,:Z)) for the geometry. +""" +coordnames(geom) = coordnames(geomtype(geom), geom) + +""" + isempty(geom) -> Bool + +Return `true` when the geometry is empty. +""" +isempty(geom) = isempty(geomtype(geom), geom) + +""" + issimple(geom) -> Bool + +Return `true` when the geometry is simple, i.e. doesn't cross or touch itself. +""" +issimple(geom) = issimple(geomtype(geom), geom) + +""" + getcoord(geom, i) -> Number + +Return the `i`th coordinate for a given `geom`. +Note that this is only valid for individual [`AbstractPointTrait`](@ref)s. +""" +getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) +""" + getcoord(geom) -> iterator +""" +getcoord(geom) = getcoord(geomtype(geom), geom) + +# Curve, LineString, MultiPoint +""" + npoint(geom) -> Int + +Return the number of points in given `geom`. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s and [`AbstractMultiPointTrait`](@ref)s. +""" +npoint(geom) = npoint(geomtype(geom), geom) + +""" + getpoint(geom, i::Integer) -> Point + +Return the `i`th Point in given `geom`. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s and [`AbstractMultiPointTrait`](@ref)s. +""" +getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) + +""" + getpoint(geom) -> iterator + +Returns an iterator over all points in `geom`. +""" +getpoint(geom) = getpoint(geomtype(geom), geom) + +# Curve +""" + startpoint(geom) -> Point + +Return the first point in the `geom`. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. +""" +startpoint(geom) = startpoint(geomtype(geom), geom) + +""" + endpoint(geom) -> Point + +Return the last point in the `geom`. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. +""" +endpoint(geom) = endpoint(geomtype(geom), geom) + +""" + isclosed(geom) -> Bool + +Return whether the `geom` is closed, i.e. whether +the `startpoint` is the same as the `endpoint`. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. +""" +isclosed(geom) = isclosed(geomtype(geom), geom) + +""" + isring(geom) -> Bool + +Return whether the `geom` is a ring, i.e. whether +the `geom` [`isclosed`](@ref) and [`issimple`](@ref). +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. +""" +isring(geom) = isclosed(geom) && issimple(geom) + +""" + length(geom) -> Number + +Return the length of `geom` in its 2d coordinate system. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. +""" +length(geom) = length(geomtype(geom), geom) + +# Surface +""" + area(geom) -> Number + +Return the area of `geom` in its 2d coordinate system. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. +""" +area(geom) = area(geomtype(geom), geom) + +""" + centroid(geom) -> Point + +The mathematical centroid for this Surface as a Point. +The result is not guaranteed to be on this Surface. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. +""" +centroid(geom) = centroid(geomtype(geom), geom) + +""" + pointonsurface(geom) -> Point + +A Point guaranteed to be on this geometry (as opposed to [`centroid`](@ref)). +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. +""" +pointonsurface(geom) = pointonsurface(geomtype(geom), geom) + +""" + boundary(geom) -> Curve + +Return the boundary of `geom`. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. +""" +boundary(geom) = boundary(geomtype(geom), geom) + +# Polygon/Triangle +""" + nring(geom) -> Integer + +Return the number of rings in given `geom`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s and +[`AbstractMultiPolygonTrait`](@ref)s +""" +nring(geom) = nring(geomtype(geom), geom) + +""" + getring(geom, i::Integer) -> AbstractCurve + +A specific ring `i` in a polygon or multipolygon (exterior and holes). +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s and +[`AbstractMultiPolygonTrait`](@ref)s. +""" +getring(geom, i::Integer) = getring(geomtype(geom), geom, i) + +""" + getring(geom) -> iterator + +Returns an iterator over all rings in `geom`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s and +[`AbstractMultiPolygonTrait`](@ref)s in single-argument form. +""" +getring(geom) = getring(geomtype(geom), geom) + +""" + getexterior(geom) -> Curve + +Returns the exterior ring of a Polygon as a `AbstractCurve`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. +""" +getexterior(geom) = getexterior(geomtype(geom), geom) + +""" + nhole(geom) -> Integer + +Returns the number of holes for this given `geom`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. +""" +nhole(geom)::Integer = nhole(geomtype(geom), geom) + +""" + gethole(geom, i::Integer) -> Curve + +Returns the `i`th interior ring for this given `geom`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. +""" +gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) + +""" + gethole(geom) -> iterator + +Returns an iterator over all holes in `geom`. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. +""" +gethole(geom) = gethole(geomtype(geom), geom) + +# PolyhedralSurface +""" + npatch(geom) + +Returns the number of patches for the given `geom`. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. +""" +npatch(geom)::Integer = npatch(geomtype(geom), geom) + +""" + getpatch(geom, i::Integer) -> AbstractPolygon + +Returns the `i`th patch for the given `geom`. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. +""" +getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) + +""" + getpatch(geom) -> iterator + +Returns an iterator over all patches in `geom`. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. +""" +getpatch(geom) = getpatch(geomtype(geom), geom) + +""" + boundingpolygons(geom, i) -> AbstractMultiPolygon + +Returns the collection of polygons in this surface that bounds the `i`th patch in the given `geom`. +""" +boundingpolygons(geom, i) = boundingpolygons(geomtype(geom), geom, i) + +# GeometryCollection +""" + ngeom(geom) -> Integer + +Returns the number of geometries for the given `geom`. +""" +ngeom(geom) = ngeom(geomtype(geom), geom) + +""" + getgeom(geom, i::Integer) -> AbstractGeometry + +Returns the `i`th geometry for the given `geom`. +""" +getgeom(geom, i::Integer) = getgeom(geomtype(geom), geom, i) + +""" + getgeom(geom) -> iterator + +Returns an iterator over all geometry components in `geom`. +""" +getgeom(geom) = getgeom(geomtype(geom), geom) + +# MultiLineString +""" + nlinestring(geom) -> Integer + +Returns the number of curves for the given `geom`. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. +""" +nlinestring(geom) = nlinestring(geomtype(geom), geom) + +""" + getlinestring(geom, i::Integer) -> AbstractCurve + +Returns the `i`th linestring for the given `geom`. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. +""" +getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) + +""" + getlinestring(geom) -> iterator + +Returns an iterator over all linestrings in a geometry. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. +""" +getlinestring(geom) = getlinestring(geomtype(geom), geom) + +# MultiPolygon +""" + npolygon(geom) -> Integer + +Returns the number of polygons for the given `geom`. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. +""" +npolygon(geom) = npolygon(geomtype(geom), geom) + +""" + getpolygon(geom, i::Integer) -> AbstractCurve + +Returns the `i`th polygon for the given `geom`. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. +""" +getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) + +""" + getpolygon(geom) -> iterator + +Returns an iterator over all polygons in a geometry. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. +""" +getpolygon(geom) = getpolygon(geomtype(geom), geom) + +# Other methods +""" + crs(geom) -> T <: GeoFormatTypes.CoordinateReferenceSystemFormat + +Retrieve Coordinate Reference System for given geom. +In SF this is defined as `SRID`. +""" +crs(geom) = crs(geomtype(geom), geom) + +""" + extent(geom) -> T <: Extents.Extent + +Retrieve the extent (bounding box) for given geom. +In SF this is defined as `envelope`. +""" +extent(geom) = extent(geomtype(geom), geom) + +""" + bbox(geom) -> T <: Extents.Extent + +Alias for [`extent`](@ref), for compatibility with +GeoJSON and the Python geointerface. +Ensures backwards compatibility with GeoInterface version 0. +""" +bbox(geom) = extent(geom) + +# DE-9IM, see https://en.wikipedia.org/wiki/DE-9IM +""" + equals(a, b) -> Bool + +Returns whether `a` and `b` are equal. +Equivalent to ([`within`](@ref) && [`contains`](@ref)). +""" +equals(a, b)::Bool = equals(geomtype(a), geomtype(b), a, b) + +""" + disjoint(a, b) -> Bool + +Returns whether `a` and `b` are disjoint. +Inverse of [`intersects`](@ref). +""" +disjoint(a, b)::Bool = disjoint(geomtype(a), geomtype(b), a, b) + +""" + intersects(a, b) -> Bool + +Returns whether `a` and `b` intersect. +Inverse of [`disjoint`](@ref). +""" +intersects(a, b)::Bool = intersects(geomtype(a), geomtype(b), a, b) + +""" + touches(a, b) -> Bool + +Returns whether `a` and `b` touch. +""" +touches(a, b)::Bool = touches(geomtype(a), geomtype(b), a, b) + +""" + within(a, b) -> Bool + +Returns whether `a` is within `b`. +The order of arguments is important. +Equivalent to [`contains`](@ref) with reversed arguments. +""" +within(a, b)::Bool = within(geomtype(a), geomtype(b), a, b) + +""" + contains(a, b) -> Bool + +Returns whether `a` contains `b`. +The order of arguments is important. +Equivalent to [`within`](@ref) with reversed arguments. +""" +contains(a, b)::Bool = contains(geomtype(a), geomtype(b), a, b) + +""" + overlaps(a, b) -> Bool + +Returns whether `a` and `b` overlap. Also called `covers` in DE-9IM. +""" +overlaps(a, b)::Bool = overlaps(geomtype(a), geomtype(b), a, b) + +""" + crosses(a, b) -> Bool + +Returns whether `a` and `b` cross. +""" +crosses(a, b)::Bool = crosses(geomtype(a), geomtype(b), a, b) + +""" + relate(a, b, relationmatrix::String) -> Bool + +Returns whether `a` and `b` relate, based on the provided relation matrix. +""" +relate(a, b, relationmatrix)::Bool = relate(geomtype(a), geomtype(b), a, b, relationmatrix) + +# Set theory +""" + symdifference(a, b) -> AbstractGeometry + +Returns a geometric object that represents the Point set symmetric difference of `a` with `b`. +""" +symdifference(a, b) = symdifference(geomtype(a), geomtype(b), a, b) + +""" + difference(a, b) -> AbstractGeometry + +Returns a geometric object that represents the Point set difference of `a` with `b` +""" +difference(a, b) = difference(geomtype(a), geomtype(b), a, b) + +""" + intersection(a, b) -> AbstractGeometry + +Returns a geometric object that represents the Point set intersection of `a` with `b` +""" +intersection(a, b) = intersection(geomtype(a), geomtype(b), a, b) + +""" + union(a, b) -> AbstractGeometry + +Returns a geometric object that represents the Point set union of `a` with `b` +""" +union(a, b) = union(geomtype(a), geomtype(b), a, b) + +# Spatial analysis +""" + distance(a, b) -> Number + +Returns the shortest distance between `a` with `b`. +""" +distance(a, b) = distance(geomtype(a), geomtype(b), a, b) + +""" + buffer(geom, distance) -> AbstractGeometry + +Returns a geometric object that represents a buffer of the given `geom` with `distance`. +""" +buffer(geom, distance) = buffer(geomtype(geom), geom, distance) + +""" + convexhull(geom) -> AbstractCurve + +Returns a geometric object that represents the convex hull of the given `geom`. +""" +convexhull(geom) = convexhull(geomtype(geom), geom) + +""" + x(geom) -> Number + +Return the :X coordinate of the given `geom`. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. +""" +x(geom) = x(geomtype(geom), geom) + +""" + y(geom) -> Number + +Return the :Y coordinate of the given `geom`. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. +""" +y(geom) = y(geomtype(geom), geom) + +""" + z(geom) -> Number + +Return the :Z coordinate of the given `geom`. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. +""" +z(geom) = z(geomtype(geom), geom) + +""" + m(geom) -> Number + +Return the :M coordinate of the given `geom`. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. +""" +m(geom) = m(geomtype(geom), geom) + +""" + is3d(geom) -> Bool + +Return whether the given `geom` has a :Z coordinate. +""" +is3d(geom) = is3d(geomtype(geom), geom) +""" + ismeasured(geom) -> Bool + +Return whether the given `geom` has a :M coordinate. +""" +ismeasured(geom) = ismeasured(geomtype(geom), geom) + +""" + coordinates(geom) -> Vector + +Return (an iterator of) point coordinates. +Ensures backwards compatibility with GeoInterface version 0. +""" +coordinates(geom) = coordinates(geomtype(geom), geom) + +""" + convert(type::CustomGeom, geom) + +Convert `geom` into the `CustomGeom` type if both geom as the CustomGeom package +have implemented GeoInterface. +""" +convert(T, geom) = convert(T, geomtype(geom), geom) + +""" + astext(geom) -> WKT + +Convert `geom` into Well Known Text (WKT) representation, such as `POINT (30 10)`. +""" +astext(geom) = astext(geomtype(geom), geom) + +""" + asbinary(geom) -> WKB + +Convert `geom` into Well Known Binary (WKB) representation, such as `000000000140000000000000004010000000000000`. +""" +asbinary(geom) = asbinary(geomtype(geom), geom) diff --git a/src/operations.jl b/src/operations.jl deleted file mode 100644 index f6b945af..00000000 --- a/src/operations.jl +++ /dev/null @@ -1,10 +0,0 @@ -import Base: ==, hash, isapprox - -# Compare points by coordinate values. -==(x::P, y::P) where {P <: AbstractPoint} = coordinates(x) == coordinates(y) - -# Hash the coordinates for consistency. -hash(x::P) where {P <: AbstractPoint} = hash(coordinates(x)) - -# Compare points approximately by coordinate values. -isapprox(x::P, y::P; kwargs...) where {P <: AbstractPoint} = isapprox(coordinates(x), coordinates(y); kwargs...) diff --git a/src/plotrecipes.jl b/src/plotrecipes.jl deleted file mode 100644 index 872820e8..00000000 --- a/src/plotrecipes.jl +++ /dev/null @@ -1,168 +0,0 @@ -function pointcoords(geom::AbstractGeometry) - @assert geotype(geom) == :Point - [tuple(coordinates(geom)...)] -end - -function multipointcoords(geom::AbstractGeometry) - @assert geotype(geom) == :MultiPoint - coords = coordinates(geom) - first.(coords), last.(coords) -end - -function linestringcoords(geom::AbstractGeometry) - @assert geotype(geom) == :LineString - coords = coordinates(geom) - first.(coords), last.(coords) -end - -function multilinestringcoords(geom::AbstractGeometry) - @assert geotype(geom) == :MultiLineString - x, y = Float64[], Float64[] - for line in coordinates(geom) - append!(x, first.(line)); push!(x, NaN) - append!(y, last.(line)); push!(y, NaN) - end - x, y -end - -function polygoncoords(geom::AbstractGeometry) - @assert geotype(geom) == :Polygon - ring = first(coordinates(geom)) # currently doesn't plot holes - first.(ring), last.(ring) -end - -function multipolygoncoords(geom::AbstractGeometry) - @assert geotype(geom) == :MultiPolygon - x, y = Float64[], Float64[] - for poly in coordinates(geom) - ring = first(coordinates(poly)) # currently doesn't plot holes - append!(x, first.(ring)); push!(x, NaN) - append!(y, last.(ring)); push!(y, NaN) - end - x, y -end - -shapecoords(geom::AbstractPoint) = pointcoords(geom) -shapecoords(geom::AbstractMultiPoint) = multipointcoords(geom) -shapecoords(geom::AbstractLineString) = linestringcoords(geom) -shapecoords(geom::AbstractMultiLineString) = multilinestringcoords(geom) -shapecoords(geom::AbstractPolygon) = polygoncoords(geom) -shapecoords(geom::AbstractMultiPolygon) = multipolygoncoords(geom) - -function shapecoords(geom::AbstractGeometry) - gtype = geotype(geom) - if gtype == :Point - return pointcoords(geom) - elseif gtype == :MultiPoint - return multipointcoords(geom) - elseif gtype == :LineString - return linestringcoords(geom) - elseif gtype == :MultiLineString - return multilinestringcoords(geom) - elseif gtype == :Polygon - return polygoncoords(geom) - elseif gtype == :MultiPolygon - return multipolygoncoords(geom) - else - warn("unknown geometry type: $gtype") - end -end - -RecipesBase.@recipe function f(geom::AbstractPoint) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :scatter - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractMultiPoint) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :scatter - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractLineString) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :path - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractMultiLineString) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :path - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractPolygon) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :shape - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractMultiPolygon) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - seriestype --> :shape - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::AbstractGeometry) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - gtype = geotype(geom) - if gtype == :Point || gtype == :MultiPoint - seriestype := :scatter - elseif gtype == :LineString || gtype == :MultiLineString - seriestype := :path - elseif gtype == :Polygon || gtype == :MultiPolygon - seriestype := :shape - else - @warn("unknown geometry type: $gtype") - end - shapecoords(geom) -end - -RecipesBase.@recipe function f(geom::Vector{<:Union{Missing, AbstractGeometry}}) - label --> :none - if plotattributes[:plot_object].n == 0 - aspect_ratio --> 1 - end - for g in skipmissing(geom) - @series begin - gtype = geotype(g) - if gtype == :Point || gtype == :MultiPoint - seriestype := :scatter - elseif gtype == :LineString || gtype == :MultiLineString - seriestype := :path - elseif gtype == :Polygon || gtype == :MultiPolygon - seriestype := :shape - else - @warn("unknown geometry type: $gtype") - end - shapecoords(g) - end - end -end - -RecipesBase.@recipe f(feature::AbstractFeature) = geometry(feature) -RecipesBase.@recipe f(features::Vector{<:AbstractFeature}) = geometry.(features) -RecipesBase.@recipe f(collection::AbstractFeatureCollection) = features(collection) -RecipesBase.@recipe f(collection::AbstractGeometryCollection) = geometries(collection) diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 00000000..89ccd459 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,81 @@ +"An AbstractGeometryTrait type for all geometries." +abstract type AbstractGeometryTrait end + +"An AbstractGeometryCollectionTrait type for all geometrycollections." +abstract type AbstractGeometryCollectionTrait <: AbstractGeometryTrait end +"A GeometryCollection is a collection of `Geometry`s." +struct GeometryCollectionTrait <: AbstractGeometryCollectionTrait end + +"An AbstractPointTrait for all points." +abstract type AbstractPointTrait <: AbstractGeometryTrait end +"A single point." +struct PointTrait <: AbstractPointTrait end + +"An AbstractCurveTrait type for all curves." +abstract type AbstractCurveTrait <: AbstractGeometryTrait end +"An AbstractLineString type for all linestrings." +abstract type AbstractLineStringTrait <: AbstractCurveTrait end +"A LineStringTrait is a collection of straight lines between its `PointTrait`s." +struct LineStringTrait <: AbstractLineStringTrait end +"A LineTrait is [`LineStringTrait`](@ref) with just two points." +struct LineTrait <: AbstractLineStringTrait end +"A LinearRingTrait is a [`LineStringTrait`](@ref) with the same begin and endpoint." +struct LinearRingTrait <: AbstractLineStringTrait end + +"A CircularStringTrait is a curve, with an odd number of points. +A single segment consists of three points, where the first and last are the beginning and end, +while the second is halfway the curve." +struct CircularStringTrait <: AbstractCurveTrait end +"A CompoundCurveTrait is a curve that combines straight [`LineStringTrait`](@ref)s and curved [`CircularStringTrait`](@ref)s." +struct CompoundCurveTrait <: AbstractCurveTrait end + +"An AbstractSurfaceTrait type for all surfaces." +abstract type AbstractSurfaceTrait <: AbstractGeometryTrait end +"An AbstractCurvePolygonTrait type for all curved polygons." +abstract type AbstractCurvePolygonTrait <: AbstractSurfaceTrait end +"An [`AbstractCurvePolygonTrait`](@ref) that can contain either circular or straight curves as rings." +struct CurvePolygonTrait <: AbstractCurvePolygonTrait end +"An AbstractPolygonTrait type for all polygons." +abstract type AbstractPolygonTrait <: AbstractCurvePolygonTrait end +"An [`AbstractSurfaceTrait`](@ref) with straight rings either as exterior or interior(s)." +struct PolygonTrait <: AbstractPolygonTrait end +"A [`PolygonTrait`](@ref) that is triangular." +struct TriangleTrait <: AbstractPolygonTrait end +"A [`PolygonTrait`](@ref) that is rectangular and could be described by the minimum and maximum vertices." +struct RectangleTrait <: AbstractPolygonTrait end +"A [`PolygonTrait`](@ref) with four vertices." +struct QuadTrait <: AbstractPolygonTrait end +"A [`PolygonTrait`](@ref) with five vertices." +struct PentagonTrait <: AbstractPolygonTrait end +"A [`PolygonTrait`](@ref) with six vertices." +struct HexagonTrait <: AbstractPolygonTrait end + +"An AbstractPolyhedralSurfaceTrait type for all polyhedralsurfaces." +abstract type AbstractPolyhedralSurfaceTrait <: AbstractSurfaceTrait end +"A PolyhedralSurfaceTrait is a connected surface consisting of [`PolygonTrait`](@ref)s." +struct PolyhedralSurfaceTrait <: AbstractPolyhedralSurfaceTrait end +"A TINTrait is a [`PolyhedralSurfaceTrait`](@ref) consisting of [`TriangleTrait`](@ref)s." +struct TINTrait <: AbstractPolyhedralSurfaceTrait end # Surface consisting of Triangles + +"An AbstractMultiPointTrait type for all multipoints." +abstract type AbstractMultiPointTrait <: AbstractGeometryCollectionTrait end +"A MultiPointTrait is a collection of [`PointTrait`](@ref)s." +struct MultiPointTrait <: AbstractMultiPointTrait end + +"An AbstractMultiCurveTrait type for all multicurves." +abstract type AbstractMultiCurveTrait <: AbstractGeometryCollectionTrait end +"A MultiCurveTrait is a collection of [`CircularStringTrait`](@ref)s." +struct MultiCurveTrait <: AbstractMultiCurveTrait end +"An AbstractMultiLineStringTrait type for all multilinestrings." +abstract type AbstractMultiLineStringTrait <: AbstractMultiCurveTrait end +"A MultiLineStringTrait is a collection of [`LineStringTrait`](@ref)s." +struct MultiLineStringTrait <: AbstractMultiLineStringTrait end + +"An AbstractMultiSurfaceTrait type for all multisurfaces." +abstract type AbstractMultiSurfaceTrait <: AbstractGeometryCollectionTrait end +"A MultiSurfaceTrait is a collection of [`AbstractSurfaceTrait`](@ref)s." +struct MultiSurfaceTrait <: AbstractMultiSurfaceTrait end +"An AbstractMultiPolygonTrait type for all multipolygons." +abstract type AbstractMultiPolygonTrait <: AbstractMultiSurfaceTrait end +"A MultiPolygonTrait is a collection of [`PolygonTrait`](@ref)s." +struct MultiPolygonTrait <: AbstractMultiPolygonTrait end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 00000000..26d2461a --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,47 @@ +"""Test whether the required interface for your `geom` has been implemented correctly.""" +function testgeometry(geom) + try + @assert isgeometry(geom) + type = geomtype(geom) + + if type == PointTrait() + n = ncoord(geom) + if n >= 1 # point could be empty + getcoord(geom, 1) # point always needs at least 2 + end + else + n = ngeom(geom) + if n >= 1 # geometry could be empty + g2 = getgeom(geom, 1) + subtype = subtrait(type) + if !isnothing(subtype) + issub = geomtype(g2) isa subtype + !issub && error("Implemented hierarchy for this geometry type is incorrect. Subgeometry should be a $subtype") + end + @assert testgeometry(g2) # recursive testing of subgeometries + end + end + catch e + if e isa MethodError + println("You're missing an implementation: $e") + else + throw(e) + end + return false + end + return true +end + +"""Test whether the required interface for your `feature` has been implemented correctly.""" +function testfeature(feature) + try + @assert isfeature(feature) + geom = geometry(feature) + @assert isgeometry(geom) + props = properties(feature) + catch e + println("You're missing an implementation: $e") + return false + end + return true +end diff --git a/test/runtests.jl b/test/runtests.jl index 06c70568..d8047867 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,37 +1,6 @@ using GeoInterface +using Documenter using Test -@testset "Comparison operators" begin - # Points are now compared by value: - pt1, pt2, pt3 = Point([0.0, 0.0]), Point([0.0, 0.0]), Point([0.0, 1.0]) - @test pt1 == pt1 - @test pt1 == pt2 - @test pt1 != pt3 - - # Also, the hash is based on the coordinates - @test hash(pt1) == hash(pt1) - @test hash(pt1) == hash(pt2) - @test hash(pt1) != hash(pt3) - - # Implicitly, this should also work for `isequal`: - @test isequal(pt1, pt1) - @test isequal(pt1, pt2) - @test !isequal(pt1, pt3) - - # Can also do approximate comparisons - pt4 = Point([0.0, 1.001]) - @test pt3 != pt4 - @test pt3 ≉ pt4 atol=0.0001 - @test pt3 ≉ pt4 rtol=0.0001 - @test pt3 ≈ pt4 atol=0.001 - @test pt3 ≈ pt4 rtol=0.001 - - # The same is not true for other geometry types: The representation is not - # unique, so comparing the coordinates directly might be misleading. - pg1 = Polygon([[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]]) - pg2 = Polygon([[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]]) - pg3 = Polygon([[[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]]]) - @test pg1 == pg1 # same objects - @test pg1 != pg2 # same values, but different objects - @test pg1 != pg3 # equivalent, but not same values -end +include("test_primitives.jl") +doctest(GeoInterface) diff --git a/test/test_primitives.jl b/test/test_primitives.jl new file mode 100644 index 00000000..09c88994 --- /dev/null +++ b/test/test_primitives.jl @@ -0,0 +1,292 @@ +using GeoInterface +using Test + +@testset "Developer" begin + # Implement interface + struct MyPoint end + struct MyEmptyPoint end + struct MyCurve end + struct MyPolygon end + struct MyTriangle end + struct MyMultiPoint end + struct MyMultiCurve end + struct MyMultiPolygon end + struct MyTIN end + struct MyCollection end + + GeoInterface.isgeometry(::MyPoint) = true + GeoInterface.geomtype(::MyPoint) = PointTrait() + GeoInterface.ncoord(::PointTrait, geom::MyPoint) = 2 + GeoInterface.getcoord(::PointTrait, geom::MyPoint, i) = [1, 2][i] + + GeoInterface.isgeometry(::MyEmptyPoint) = true + GeoInterface.geomtype(::MyEmptyPoint) = PointTrait() + GeoInterface.ncoord(::PointTrait, geom::MyEmptyPoint) = 0 + GeoInterface.isempty(::PointTrait, geom::MyEmptyPoint) = true + + GeoInterface.isgeometry(::MyCurve) = true + GeoInterface.geomtype(::MyCurve) = LineStringTrait() + GeoInterface.ngeom(::LineStringTrait, geom::MyCurve) = 2 + GeoInterface.getgeom(::LineStringTrait, geom::MyCurve, i) = MyPoint() + Base.convert(T::Type{MyCurve}, geom::X) where {X} = Base.convert(T, geomtype(geom), geom) + Base.convert(::Type{MyCurve}, ::LineStringTrait, geom::MyCurve) = geom + + GeoInterface.isgeometry(::MyPolygon) = true + GeoInterface.geomtype(::MyPolygon) = PolygonTrait() + GeoInterface.ngeom(::PolygonTrait, geom::MyPolygon) = 2 + GeoInterface.getgeom(::PolygonTrait, geom::MyPolygon, i) = MyCurve() + + GeoInterface.isgeometry(::MyTriangle) = true + GeoInterface.geomtype(::MyTriangle) = TriangleTrait() + GeoInterface.ngeom(::TriangleTrait, geom::MyTriangle) = 3 + GeoInterface.getgeom(::TriangleTrait, geom::MyTriangle, i) = MyCurve() + + GeoInterface.isgeometry(::MyMultiPoint) = true + GeoInterface.geomtype(::MyMultiPoint) = MultiPointTrait() + GeoInterface.ngeom(::MultiPointTrait, geom::MyMultiPoint) = 2 + GeoInterface.getgeom(::MultiPointTrait, geom::MyMultiPoint, i) = MyPoint() + + GeoInterface.isgeometry(::MyMultiCurve) = true + GeoInterface.geomtype(::MyMultiCurve) = MultiCurveTrait() + GeoInterface.ngeom(::MultiCurveTrait, geom::MyMultiCurve) = 2 + GeoInterface.getgeom(::MultiCurveTrait, geom::MyMultiCurve, i) = MyCurve() + + GeoInterface.isgeometry(::MyMultiPolygon) = true + GeoInterface.geomtype(::MyMultiPolygon) = MultiPolygonTrait() + GeoInterface.ngeom(::MultiPolygonTrait, geom::MyMultiPolygon) = 2 + GeoInterface.getgeom(::MultiPolygonTrait, geom::MyMultiPolygon, i) = MyPolygon() + + GeoInterface.isgeometry(::MyTIN) = true + GeoInterface.geomtype(::MyTIN) = PolyhedralSurfaceTrait() + GeoInterface.ngeom(::PolyhedralSurfaceTrait, geom::MyTIN) = 2 + GeoInterface.getgeom(::PolyhedralSurfaceTrait, geom::MyTIN, i) = MyTriangle() + + GeoInterface.isgeometry(::MyCollection) = true + GeoInterface.geomtype(::MyCollection) = GeometryCollectionTrait() + GeoInterface.ngeom(::GeometryCollectionTrait, geom::MyCollection) = 2 + GeoInterface.getgeom(::GeometryCollectionTrait, geom::MyCollection, i) = MyCurve() + + @testset "Point" begin + geom = MyPoint() + @test testgeometry(geom) + @test GeoInterface.x(geom) === 1 + @test GeoInterface.y(geom) === 2 + @test_throws ArgumentError GeoInterface.z(geom) + @test_throws ArgumentError GeoInterface.m(geom) + @test ncoord(geom) === 2 + @test collect(getcoord(geom)) == [1, 2] + @test getcoord(geom, 1) === 1 + @test GeoInterface.coordnames(geom) == (:X, :Y) + @test !GeoInterface.isempty(geom) + @test !GeoInterface.is3d(geom) + @test !GeoInterface.ismeasured(geom) + + geom = MyEmptyPoint() + @test GeoInterface.coordnames(geom) == () + @test GeoInterface.isempty(geom) + + @test isnothing(GeoInterface.crs(geom)) + @test isnothing(GeoInterface.extent(geom)) + @test isnothing(GeoInterface.bbox(geom)) + end + + @testset "LineString" begin + geom = MyCurve() + @test testgeometry(geom) + + @test GeoInterface.npoint(geom) == 2 # defaults to ngeom + @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] + points = GeoInterface.getpoint(geom) + point = GeoInterface.getpoint(geom, 1) + pointa = GeoInterface.startpoint(geom) + pointb = GeoInterface.endpoint(geom) + @test GeoInterface.y(point) == 2 + + @test_throws MethodError GeoInterface.length(geom) + + @test GeoInterface.issimple(geom) + @test GeoInterface.isclosed(geom) + @test GeoInterface.isring(geom) + + end + + @testset "Polygon" begin + geom = MyPolygon() + @test testgeometry(geom) + # Test that half a implementation yields an error + + @test GeoInterface.nring(geom) == 2 + @test GeoInterface.nhole(geom) == 1 + @test GeoInterface.coordinates(geom) == [[[1, 2], [1, 2]], [[1, 2], [1, 2]]] + lines = GeoInterface.getring(geom) + line = GeoInterface.getring(geom, 1) + lines = GeoInterface.gethole(geom) + line = GeoInterface.gethole(geom, 1) + line = GeoInterface.getexterior(geom) + @test GeoInterface.npoint(geom) == 4 + @test collect(GeoInterface.getpoint(geom)) == [MyPoint(), MyPoint(), MyPoint(), MyPoint()] + + @test_throws MethodError GeoInterface.area(geom) + + geom = MyTriangle() + @test testgeometry(geom) + @test GeoInterface.nring(geom) == 1 + @test GeoInterface.nhole(geom) == 0 + @test GeoInterface.npoint(geom) == 3 + end + + @testset "MultiPoint" begin + geom = MyMultiPoint() + @test testgeometry(geom) + + @test GeoInterface.npoint(geom) == 2 + points = GeoInterface.getpoint(geom) + point = GeoInterface.getpoint(geom, 1) + @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] + @test collect(points) == [MyPoint(), MyPoint()] + + @test !GeoInterface.issimple(geom) + end + + @testset "MultiLineString" begin + geom = MyMultiCurve() + @test testgeometry(geom) + + @test GeoInterface.nlinestring(geom) == 2 + lines = GeoInterface.getlinestring(geom) + line = GeoInterface.getlinestring(geom, 1) + @test GeoInterface.coordinates(geom) == [[[1, 2], [1, 2]], [[1, 2], [1, 2]]] + @test collect(lines) == [MyCurve(), MyCurve()] + end + + @testset "MultiPolygon" begin + geom = MyMultiPolygon() + @test testgeometry(geom) + + @test GeoInterface.npolygon(geom) == 2 + polygons = GeoInterface.getpolygon(geom) + polygon = GeoInterface.getpolygon(geom, 1) + @test GeoInterface.coordinates(geom) == [[[[1, 2], [1, 2]], [[1, 2], [1, 2]]], [[[1, 2], [1, 2]], [[1, 2], [1, 2]]]] + @test collect(polygons) == [MyPolygon(), MyPolygon()] + end + + @testset "Surface" begin + geom = MyTIN() + @test testgeometry(geom) + + @test GeoInterface.npatch(geom) == 2 + polygons = GeoInterface.getpatch(geom) + polygon = GeoInterface.getpatch(geom, 1) + @test GeoInterface.coordinates(geom) == [[[[1, 2], [1, 2]], [[1, 2], [1, 2]], [[1, 2], [1, 2]]], [[[1, 2], [1, 2]], [[1, 2], [1, 2]], [[1, 2], [1, 2]]]] + @test collect(polygons) == [MyTriangle(), MyTriangle()] + end + + @testset "GeometryCollection" begin + geom = MyCollection() + @test testgeometry(geom) + + @test GeoInterface.ngeom(geom) == 2 + geoms = GeoInterface.getgeom(geom) + thing = GeoInterface.getgeom(geom, 1) + @test GeoInterface.coordinates(geom) == [[[1, 2], [1, 2]], [[1, 2], [1, 2]]] + @test collect(geoms) == [MyCurve(), MyCurve()] + end + +end + +@testset "Defaults" begin + @test GeoInterface.subtrait(TINTrait()) == TriangleTrait + @test GeoInterface.nring(QuadTrait(), ()) == 1 + @test GeoInterface.npoint(QuadTrait(), ()) == 4 +end + +@testset "Feature" begin + struct Row end + struct Point end + + GeoInterface.isgeometry(::Point) = true + GeoInterface.geomtype(::Point) = PointTrait() + GeoInterface.ncoord(::PointTrait, geom::Point) = 2 + GeoInterface.getcoord(::PointTrait, geom::Point, i) = [1, 2][i] + + GeoInterface.isfeature(::Row) = true + GeoInterface.geometry(r::Row) = Point() + GeoInterface.properties(r::Row) = (; test=1) + + @test GeoInterface.testfeature(Row()) + +end + +@testset "Conversion" begin + struct XCurve end + struct XPolygon end + + Base.convert(T::Type{XCurve}, geom::X) where {X} = Base.convert(T, geomtype(geom), geom) + Base.convert(::Type{XCurve}, ::LineStringTrait, geom::XCurve) = geom # fast fallthrough + Base.convert(::Type{XCurve}, ::LineStringTrait, geom) = geom + + geom = MyCurve() + @test !isnothing(convert(MyCurve, geom)) + + Base.convert(T::Type{XPolygon}, geom::X) where {X} = Base.convert(T, geomtype(geom), geom) + @test_throws Exception convert(MyPolygon, geom) +end + +@testset "Operations" begin + struct XGeom end + + GeoInterface.isgeometry(::XGeom) = true + GeoInterface.geomtype(::XGeom) = PointTrait() + GeoInterface.ncoord(::PointTrait, geom::XGeom) = 2 + GeoInterface.getcoord(::PointTrait, geom::XGeom, i) = [1, 2][i] + + GeoInterface.equals(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.disjoint(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.intersects(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.touches(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.within(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.contains(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.overlaps(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + GeoInterface.crosses(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = true + + GeoInterface.relate(::PointTrait, ::PointTrait, ::XGeom, ::XGeom, matrix) = true + + GeoInterface.symdifference(::PointTrait, ::PointTrait, a::XGeom, ::XGeom) = a + GeoInterface.difference(::PointTrait, ::PointTrait, a::XGeom, ::XGeom) = a + GeoInterface.intersection(::PointTrait, ::PointTrait, a::XGeom, ::XGeom) = a + GeoInterface.union(::PointTrait, ::PointTrait, ::XGeom, a::XGeom) = a + + GeoInterface.distance(::PointTrait, ::PointTrait, ::XGeom, ::XGeom) = rand() + + GeoInterface.buffer(::PointTrait, a::XGeom, distance) = a + GeoInterface.convexhull(::PointTrait, a::XGeom) = a + + GeoInterface.astext(::PointTrait, ::XGeom) = "POINT (1 2)" + GeoInterface.asbinary(::PointTrait, ::XGeom) = [0x0, 0x0] + + geom = XGeom() + + @test GeoInterface.equals(geom, geom) + @test GeoInterface.disjoint(geom, geom) + @test GeoInterface.intersects(geom, geom) + @test GeoInterface.touches(geom, geom) + @test GeoInterface.within(geom, geom) + @test GeoInterface.contains(geom, geom) + @test GeoInterface.overlaps(geom, geom) + @test GeoInterface.crosses(geom, geom) + + @test GeoInterface.relate(geom, geom, ["a"]) + + @test GeoInterface.isgeometry(GeoInterface.symdifference(geom, geom)) + @test GeoInterface.isgeometry(GeoInterface.difference(geom, geom)) + @test GeoInterface.isgeometry(GeoInterface.intersection(geom, geom)) + @test GeoInterface.isgeometry(GeoInterface.union(geom, geom)) + + @test GeoInterface.distance(geom, geom) isa Number + + @test GeoInterface.isgeometry(GeoInterface.buffer(geom, 1.0)) + @test GeoInterface.isgeometry(GeoInterface.convexhull(geom)) + + @test GeoInterface.astext(geom) isa String + @test GeoInterface.asbinary(geom) isa Vector{UInt8} +end