From 0ce3aaac54dfb7a746bb50f93371d3d6a02f61ef Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 24 Aug 2020 10:52:47 +0200 Subject: [PATCH 01/41] Moved from GeoInterfaceRFC.jl --- .travis.yml | 26 +++- Project.toml | 4 +- README.md | 278 +++++++++++++++++++++++++++++----------- src/GeoInterface.jl | 86 ------------- src/GeoInterfaceRFC.jl | 17 +++ src/defaults.jl | 46 +++++++ src/geotypes.jl | 145 --------------------- src/interface.jl | 91 +++++++++++++ src/operations.jl | 10 -- src/plotrecipes.jl | 152 ---------------------- src/primitives.jl | 48 +++++++ src/types.jl | 68 ++++++++++ src/utils.jl | 16 +++ test/runtests.jl | 37 +----- test/test_primitives.jl | 18 +++ 15 files changed, 528 insertions(+), 514 deletions(-) delete mode 100644 src/GeoInterface.jl create mode 100644 src/GeoInterfaceRFC.jl create mode 100644 src/defaults.jl delete mode 100644 src/geotypes.jl create mode 100644 src/interface.jl delete mode 100644 src/operations.jl delete mode 100644 src/plotrecipes.jl create mode 100644 src/primitives.jl create mode 100644 src/types.jl create mode 100644 src/utils.jl create mode 100644 test/test_primitives.jl diff --git a/.travis.yml b/.travis.yml index 86a8cec3..2e49209b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,25 @@ +# Documentation: http://docs.travis-ci.com/user/languages/julia language: julia -os: - - linux - - osx +notifications: + email: false julia: - 1.0 - - 1 + - 1.5 - nightly -matrix: +os: + - linux + - osx + - windows +arch: + - x64 + - x86 +cache: + directories: + - ~/.julia/artifacts +jobs: + fast_finish: true allow_failures: - julia: nightly -notifications: - email: false + exclude: + - arch: x86 + os: osx diff --git a/Project.toml b/Project.toml index da90d1a3..2b7592b0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,13 +1,11 @@ name = "GeoInterface" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" license = "MIT" -version = "0.5.4" +version = "1.0" [deps] -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" [compat] -RecipesBase = "0.6, 0.7, 0.8, 1.0" julia = "1" [extras] diff --git a/README.md b/README.md index a57dd88e..94d2c456 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,202 @@ -# GeoInterface.jl - -A Julia Protocol for Geospatial Data - -## 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]). - -## GEO Interface - -### 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/)) +# GeoInterface +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. + +Packages which support the GeoInterfaceRFC.jl interface can be found in [INTEGRATIONS.md](INTEGRATIONS.md). + +## 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 `Xn` becomes `getX`: +```julia +GeometryType -> geomtype +NumGeometries -> ngeom +GeometryN -> getgeom +NumPatches -> npatch +# etc +``` + +We also simplified the dimension functions. From the three original (`dimension`, `coordinateDimension`, `spatialDimension`) there's now only the coordinate dimension, so not to overlap with the Julia `ndims`. +```julia +coordinateDimension -> ncoords +``` + +We've generalized the some functions: +```julia +SRID -> crs +envelope -> extent +``` + +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 +spatialDimension +asText +asBinary +is3D +isMeasured +boundary + +locateAlong +locateBetween + +distance +buffer +convexHull + +``` +While the following functions have no implementation: +```julia +equals +disjoint +touches +within +overlaps +crosses +intersects +contains +relate + +intersection +union +difference +symdifference +``` + + +## Implementation +GeoInterface provides a traits interface, not unlike Tables.jl, by + +(a) a set of functions: +```julia +geomtype(geom) +ncoord(geom) +ngeom(geom) +getgeom(geom::geomtype, i) +... +``` +(b) a set of types for dispatching on said functions. + The types tells GeoInterface how to interpret the input object inside a GeoInterface function. + +```julia +abstract Geometry +Point <: AbstractPoint <: AbstractGeometry +MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: AbstractGeometry +... +``` + +### For developers looking to implement the interface +GeoInterface requires five functions to be defined for a given geom: + +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.X() +GeoInterface.ncoord(geomtype(geom), geom::geomtype)::Integer +GeoInterface.getcoord(geomtype(geom), geom::geomtype, i)::Real # only for Points +GeoInterface.ngeom(geomtype(geom), geom::geomtype)::Integer +GeoInterface.getgeom(geomtype(geom), geom::geomtype, i) # geomtype -> GeoInterface.Y +``` +Where the `getgeom` could be an iterator (without the i) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. + +There are also optional generic methods that could help or speed up operations: +```julia +GeoInterface.crs(geom)::Union{Missing, GeoFormatTypes.CoordinateReferenceSystemFormat} +GeoInterface.extent(geom) # geomtype -> GeoInterface.Rectangle +``` + +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 the next chapter. + +### Examples + +A `geom::geomtype` with "Point"-like traits implements +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Point() +GeoInterface.ncoord(::GeoInterface.Point, geom::geomtype)::Integer +GeoInterface.getcoord(::GeoInterface.Point, geom::geomtype, i)::Real + +# Defaults +GeoInterface.ngeom(::GeoInterface.Point, geom)::Integer = 0 +GeoInterface.getgeom(::GeoInterface.Point, geom::geomtype, i) = nothing +``` + +A `geom::geomtype` with "LineString"-like traits implements the following methods: +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.LineString() +GeoInterface.ncoord(::GeoInterface.LineString, geom::geomtype)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::GeoInterface.LineString, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.LineString, geom::geomtype, i) # of geomtype Point + +# Optional +GeoInterface.isclosed(::GeoInterface.LineString, geom::geomtype)::Bool +GeoInterface.issimple(::GeoInterface.LineString, geom::geomtype)::Bool +GeoInterface.length(::GeoInterface.LineString, geom::geomtype)::Real +``` +A `geom::geomtype` with "Polygon"-like traits can implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Polygon() +GeoInterface.ncoord(::GeoInterface.Polygon, geom::geomtype)::Integer + +# These alias for nring and getring +GeoInterface.ngeom(::GeoInterface.Polygon, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.Polygon, geom::geomtype, i)::"LineString" + +# Optional +GeoInterface.area(::GeoInterface.Polygon, geom::geomtype)::Real +GeoInterface.centroid(::GeoInterface.Polygon, geom::geomtype)::"Point" +GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::geomtype)::"Point" +GeoInterface.boundary(::GeoInterface.Polygon, geom::geomtype)::"LineString" + +``` +A `geom::geomtype` with "GeometryCollection"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.GeometryCollection() +GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::geomtype)::Integer +GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::geomtypem, i)::"Geometry" +``` +A `geom::geomtype` with "MultiPoint"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPoint() +GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::geomtype)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::geomtype, i)::"Point" +``` +A `geom::geomtype` with "MultiLineString"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiLineString() +GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::geomtype)::Integer + +# These alias for nlinestring and getlinestring +GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::geomtypem, i)::"LineString" +``` +A `geom::geomtype` with "MultiPolygon"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPolygon() +GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::geomtype)::Integer + +# These alias for npolygon and getpolygon +GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::geomtype, i)::"Polygon" +``` + + +### Testing the interface +GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. + +```julia +GeoInterface.test_interface_for_geom(geom) +``` diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl deleted file mode 100644 index f9281ad9..00000000 --- a/src/GeoInterface.jl +++ /dev/null @@ -1,86 +0,0 @@ -__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}() - bbox(obj::AbstractFeature) = nothing - crs(obj::AbstractFeature) = nothing - - abstract type AbstractFeatureCollection end - geotype(::AbstractFeatureCollection) = :FeatureCollection - features(obj::AbstractFeatureCollection) = error("features(::AbstractFeatureCollection) not defined.") - # optional - bbox(obj::AbstractFeatureCollection) = nothing - crs(obj::AbstractFeatureCollection) = nothing - - include("operations.jl") - include("geotypes.jl") - include("plotrecipes.jl") -end diff --git a/src/GeoInterfaceRFC.jl b/src/GeoInterfaceRFC.jl new file mode 100644 index 00000000..7ec02beb --- /dev/null +++ b/src/GeoInterfaceRFC.jl @@ -0,0 +1,17 @@ +module GeoInterfaceRFC + +include("types.jl") +include("interface.jl") +include("defaults.jl") +# include("primitives.jl") # needs rethinking +include("utils.jl") + +export test_interface_for_geom + +export geomtype +export ncoord +export getcoord +export ngeom +export getgeom + +end # module diff --git a/src/defaults.jl b/src/defaults.jl new file mode 100644 index 00000000..bd552c45 --- /dev/null +++ b/src/defaults.jl @@ -0,0 +1,46 @@ +# Defaults for many of the interface functions are defined here as fallback. + +## Coords +# Four options in SF, xy, xyz, xym, xyzm +const default_coord_names = (:X, :Y, :Z, :M) + +coordnames(geom) = default_coord_names[1:ncoord(geom)] + +x(geom) = getcoord(geom, findfirst(coordnames(geom), :X)) +y(geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) +z(geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) +m(geom) = getcoord(geom, findfirst(coordnames(geom), :M)) + +## Points +npoint(c::AbstractCurve, geom) = ngeom(c, geom) +getpoint(c::AbstractCurve, geom, i) = getgeom(c, geom, i) + +## Polygons +nring(p::AbstractPolygon, geom) = ngeom(p, geom) +getring(p::AbstractPolygon, geom, i) = getgeom(p, geom, i) +getexterior(p::AbstractPolygon, geom) = getring(p, geom, 1) +nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 +gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) + +nlinestring(::AbstractMultiLineString, geom) = ngeom(p, geom) +nlinestring(::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) + +npolygon(::AbstractMultiPolygon, geom) = ngeom(p, geom) +getpolygon(::AbstractMultiPolygon, geom, i) = getgeom(p, geom, i) + +npoint(::Line, _) = 2 +npoint(::Triangle, _) = 3 +npoint(::Rectangle, _) = 4 +npoint(::Quad, _) = 4 +npoint(::Pentagon, _) = 5 +npoint(::Hexagon, _) = 6 + +issimple(::AbstractCurve, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom) - 1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) +isclosed(::AbstractCurve, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) +isring(x::AbstractCurve, geom) = issimple(x, geom) && isclosed(x, geom) + +# TODO Only simple if it's also not intersecting itself, except for its endpoints +issimple(::AbstractMultiCurve, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) +isclosed(::AbstractMultiCurve, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) + +issimple(::MultiPoint, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) 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..3ed92d8e --- /dev/null +++ b/src/interface.jl @@ -0,0 +1,91 @@ +# All Geometries +"""Returns the geometry type, such as `Polygon` or `Point`.""" +function geomtype(geom) + throw(ErrorException(string("Unknown Geometry type. ", + "Define GeoInterface.geomtype(::$(typeof(geom))) to return the desired type."))) +end + +# All types +""" + ncoord(geom) -> Integer + +Return the number of coordinate dimensions (such as XYZ) for the geometry. +""" +ncoord(geom) = ncoord(geomtype(geom), geom) + +"""Return `true` when the geometry is empty.""" +isempty(geom) = ngeom(geom) == 0 +"""Return `true` when the geometry doesn't cross itself.""" +issimple(geom) = issimple(geomtype(geom), geom) + +# Point +getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) + +# Curve, LineString, MultiPoint +npoint(geom) = npoint(geomtype(geom), geom) +getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) + +# Curve +startpoint(geom) = getpoint(geom, 1) +isclosed(geom) = isclosed(geomtype(geom), geom) +isring(geom) = isclosed(geom) && issimple(geom) +length(geom) = length(geomtype(geom), geom) +endpoint(geom) = getcoord(geom, length(geom)) + +# Surface +area(geom) = area(geomtype(geom), geom) +centroid(geom) = centroid(geomtype(geom), geom) +pointonsurface(geom) = pointonsurface(geomtype(geom), geom) +boundary(geom) = boundary(geomtype(geom), geom) + +# Polygon/Triangle +nring(geom) = nring(geomtype(geom), geom) # TODO If this is more than one, it has interior rings (holes) +getring(geom) = getring(geomtype(geom), geom) + +"""Returns the exterior ring of this Polygon as a `LineString`.""" +getexterior(geom) = getexterior(geomtype(geom), geom) # getring(geom, 1) +"""Returns the number of interior rings in this Polygon.""" +nhole(geom)::Integer = nhole(geomtype(geom), geom) # nrings - 1 +"""Returns the Nth interior ring for this Polygon as a `LineString`.""" +gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) # getring + 1 + +# PolyHedralSurface +"""Returns the number of including polygons.""" +npatch(geom)::Integer = npatch(geomtype(geom), geom) +"""Returns a polygon in this surface, the order is arbitrary.""" +getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) +"""Returns the collection of polygons in this surface that bounds the given polygon “p” for any polygon “p” in the surface.""" +boundingpolygons() + +# GeometryCollection +ngeom(geom) = ngeom(geomtype(geom), geom) +getgeom(geom, i::Integer) = getgeom(geomtype(geom), geom, i) + +# MultiLineString +nlinestring(geom) = nlinestring(geomtype(geom), geom) +getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) + +# MultiPolygon +npolygon(geom) = npolygon(geomtype(geom), geom) +getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) + +# Other methods +crs(geom) = missing # or conforming to <:CoordinateReferenceSystemFormat in GeoFormatTypes + + +# DE-9IM, see https://en.wikipedia.org/wiki/DE-9IM +equals(a, b)::Bool = equals(geomtype(a), geomtype(b), a, b) +disjoint(a, b)::Bool = disjoint(geomtype(a), geomtype(b), a, b) +touches(a, b)::Bool = touches(geomtype(a), geomtype(b), a, b) +within(a, b)::Bool = within(geomtype(a), geomtype(b), a, b) +overlaps(a, b)::Bool = overlaps(geomtype(a), geomtype(b), a, b) +crosses(a, b)::Bool = crosses(geomtype(a), geomtype(b), a, b) +intersects(a, b)::Bool = intersects(geomtype(a), geomtype(b), a, b) +contains(a, b)::Bool = contains(geomtype(a), geomtype(b), a, b) +relate(a, b)::Bool = relate(geomtype(a), geomtype(b), a, b) + +# Set theory +symdifference(a, b) = difference(geomtype(a), geomtype(b), a, b) +difference(a, b, inverse=false) = difference(geomtype(a), geomtype(b), a, b, inverse) +intersection(a, b, inverse=false) = intersection(geomtype(a), geomtype(b), a, b, inverse) +union(a, b) = union(geomtype(a), geomtype(b), a, b) 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 dc7d7ab1..00000000 --- a/src/plotrecipes.jl +++ /dev/null @@ -1,152 +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 f(geom::AbstractPoint) = ( - aspect_ratio := 1; - seriestype --> :scatter; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe f(geom::AbstractMultiPoint) = ( - aspect_ratio := 1; - seriestype --> :scatter; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe f(geom::AbstractLineString) = ( - aspect_ratio := 1; - seriestype --> :path; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe f(geom::AbstractMultiLineString) = ( - aspect_ratio := 1; - seriestype --> :path; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe f(geom::AbstractPolygon) = ( - aspect_ratio := 1; - seriestype --> :shape; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe f(geom::AbstractMultiPolygon) = ( - aspect_ratio := 1; - seriestype --> :shape; - legend --> :false; - shapecoords(geom) -) - -RecipesBase.@recipe function f(geom::AbstractGeometry) - aspect_ratio := 1 - legend --> :false - 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}}) - aspect_ratio := 1 - legend --> :false - 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/primitives.jl b/src/primitives.jl new file mode 100644 index 00000000..88a4622b --- /dev/null +++ b/src/primitives.jl @@ -0,0 +1,48 @@ +const Indexable = Union{AbstractVector,Tuple} + +# Point +geomtype(::AbstractVector{T}) where {T <: Real} = Point() +geomtype(::Tuple{T,U}) where {T,U <: Real} = Point() +geomtype(::Tuple{T,U,V}) where {T,U,V <: Real} = Point() +ncoord(::AbstractPoint, geom::Indexable) = length(geom) +getcoord(::AbstractPoint, geom::Indexable, i::Integer) = geom[i] + +# LineString +ncoord(::AbstractLineString, geom::Indexable) = + ncoord(Point, getpoint(geom, 1)) +npoint(::AbstractLineString, geom::Indexable) = length(geom) +getpoint(::AbstractLineString, geom::Indexable, i::Integer) = geom[i] + +# Polygon +ncoord(::AbstractPolygon, geom::Indexable) = + ncoord(LineString, getexterior(geom)) +getexterior(::AbstractPolygon, geom::Indexable) = geom[1] +nhole(::AbstractPolygon, geom::Indexable) = length(geom) - 1 +gethole(::AbstractPolygon, geom::Indexable, i::Integer) = geom[i + 1] + +# MultiPoint +ncoord(::AbstractMultiPoint, geom::Indexable) = + ncoord(Point, getpoint(geom, 1)) +npoint(::AbstractMultiPoint, geom::Indexable) = length(geom) +getpoint(::AbstractMultiPoint, geom::Indexable, i::Integer) = geom[i] + +# MultiLineString +ncoord(::AbstractMultiLineString, geom::Indexable) = + ncoord(LineString, getlinestring(geom, 1)) +nlinestring(::AbstractMultiLineString, geom::Indexable) = length(geom) +getlinestring(::AbstractMultiLineString, geom::Indexable, i::Integer) = + geom[i] + +# MultiPolygon +ncoord(::AbstractMultiPolygon, geom::Indexable) = + ncoord(Polygon, getpolygon(geom, 1)) +npolygon(::AbstractMultiPolygon, geom::Indexable) = length(geom) +getpolygon(::AbstractMultiPolygon, geom::Indexable, i::Integer) = geom[i] + +# GeometryCollection +ncoord(::AbstractGeometryCollection, geom::Indexable) = + ncoord(geomtype(geom), getgeom(geom, 1)) +ngeom(::AbstractGeometryCollection, collection::Indexable) = + length(collection) +getgeom(::AbstractGeometryCollection, collection::Indexable, i::Integer) = + collection[i] diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 00000000..ef313064 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,68 @@ +abstract type AbstractGeometry end +# """The general `Geometry` type.""" +# struct Geometry <: AbstractGeometry end # commented out as it won't be used directly + +abstract type AbstractGeometryCollection <: AbstractGeometry end +"""A `GeometryCollection` is a collection of `Geometry`s.""" +struct GeometryCollection <: AbstractGeometryCollection end + +abstract type AbstractPoint <: AbstractGeometry end +"""A simple `Point`.""" +struct Point <: AbstractPoint end + +abstract type AbstractCurve <: AbstractGeometry end +abstract type AbstractLineString <: AbstractCurve end +"""A `LineString` is a collection of straight lines between its `Point`s.""" +struct LineString <: AbstractLineString end +"""A Line is `LineString` with just two points.""" +struct Line <: AbstractLineString end +"""A LinearRing is a `LineString` with the same begin and endpoint.""" +struct LinearRing <: AbstractLineString end + +"""A `CircularString` 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 CircularString <: AbstractCurve end +"""A `CompoundCurve` is a curve that combines straight `LineString`s and curved `CircularString`s.""" +struct CompoundCurve <: AbstractCurve end + +abstract type AbstractSurface <: AbstractGeometry end +abstract type AbstractCurvePolygon <: AbstractSurface end +"""A `Polygon` that can contain either circular or straight curves as rings.""" +struct CurvePolygon <: AbstractCurvePolygon end +abstract type AbstractPolygon <: AbstractCurvePolygon end +"""A `Polygon` with straight rings either as exterior or interior(s).""" +struct Polygon <: AbstractPolygon end +"""A `Polygon` with straight rings either as exterior or interior(s).""" +struct Triangle <: AbstractPolygon end + +"""A `Polygon` that is rectangular and could be described by the minimum and maximum vertices.""" +struct Rectangle <: AbstractPolygon end +"""A `Polygon` with four vertices.""" +struct Quad <: AbstractPolygon end +"""A `Polygon` with five vertices.""" +struct Pentagon <: AbstractPolygon end +"""A `Polygon` with six vertices.""" +struct Hexagon <: AbstractPolygon end + +abstract type AbstractPolyHedralSurface <: AbstractSurface end +"""A `PolyHedralSurface` is a connected surface consisting of Polygons.""" +struct PolyHedralSurface <: AbstractPolyHedralSurface end +"""A `TIN` is a `PolyHedralSurface` consisting of `Triangle`s.""" +struct TIN <: AbstractPolyHedralSurface end # Surface consisting of Triangles + +abstract type AbstractMultiPoint <: AbstractGeometryCollection end +"""A `MultiPoint` is a collection of `Point`s.""" +struct MultiPoint <: AbstractMultiPoint end + +abstract type AbstractMultiCurve <: AbstractGeometryCollection end +"""A `MultiCurve` is a collection of `Curve`s.""" +struct MultiCurve <: AbstractMultiCurve end +abstract type AbstractMultiLineString <: AbstractMultiCurve end +"""A `MultiPoint` is a collection of `Point`s.""" +struct MultiLineString <: AbstractMultiLineString end + +abstract type AbstractMultiSurface <: AbstractGeometryCollection end +abstract type AbstractMultiPolygon <: AbstractMultiSurface end +"""A `MultiPolygon` is a collection of `Polygon`s.""" +struct MultiPolygon <: AbstractMultiPolygon end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 00000000..a6ca2c60 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,16 @@ +"""Test whether the interface for your `geom` has been implemented correctly.""" +function test_interface_for_geom(geom) + try + type = geomtype(geom) + ncoord(geom) + ngeom(geom) + getgeom(geom, 1) + if type == Point() + getcoord(geom, 1) + end + 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..f5b11df0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,37 +1,4 @@ -using GeoInterface +using GeoInterfaceRFC 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") diff --git a/test/test_primitives.jl b/test/test_primitives.jl new file mode 100644 index 00000000..54bf3997 --- /dev/null +++ b/test/test_primitives.jl @@ -0,0 +1,18 @@ +struct MyCurve end + +@testset "Primitives" begin + # Implement interface + GeoInterfaceRFC.geomtype(::MyCurve) = GeoInterfaceRFC.LineString() + GeoInterfaceRFC.ncoord(::GeoInterfaceRFC.LineString, geom::MyCurve) = 2 + GeoInterfaceRFC.ngeom(::GeoInterfaceRFC.LineString, geom::MyCurve) = 2 + GeoInterfaceRFC.getgeom(::GeoInterfaceRFC.LineString, geom::MyCurve, i) = [[1,2],[2,3]][i] + + # Test validity + geom = MyCurve() + @test test_interface_for_geom(geom) + + # Check functions + @test GeoInterfaceRFC.npoint(geom) == 2 # defaults to ngeom + @test_throws MethodError GeoInterfaceRFC.area(geom) + +end From 7f7fa43a2d0e7d40cb1d186af536b6f407299e07 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Wed, 26 Aug 2020 18:09:00 +0200 Subject: [PATCH 02/41] Renamed all RFC occurences. --- Project.toml | 4 +--- README.md | 2 +- src/{GeoInterfaceRFC.jl => GeoInterface.jl} | 2 +- test/runtests.jl | 2 +- test/test_primitives.jl | 12 ++++++------ 5 files changed, 10 insertions(+), 12 deletions(-) rename src/{GeoInterfaceRFC.jl => GeoInterface.jl} (91%) diff --git a/Project.toml b/Project.toml index 2b7592b0..62399d7e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,7 @@ name = "GeoInterface" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" license = "MIT" -version = "1.0" - -[deps] +version = "1.0.0" [compat] julia = "1" diff --git a/README.md b/README.md index 94d2c456..8e00cc24 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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. -Packages which support the GeoInterfaceRFC.jl interface can be found in [INTEGRATIONS.md](INTEGRATIONS.md). +Packages which support the GeoInterface.jl interface can be found in [INTEGRATIONS.md](INTEGRATIONS.md). ## Changes with respect to SF While we try to adhere to SF, there are changes and extensions to make it more Julian. diff --git a/src/GeoInterfaceRFC.jl b/src/GeoInterface.jl similarity index 91% rename from src/GeoInterfaceRFC.jl rename to src/GeoInterface.jl index 7ec02beb..45296b25 100644 --- a/src/GeoInterfaceRFC.jl +++ b/src/GeoInterface.jl @@ -1,4 +1,4 @@ -module GeoInterfaceRFC +module GeoInterface include("types.jl") include("interface.jl") diff --git a/test/runtests.jl b/test/runtests.jl index f5b11df0..e5c22054 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -using GeoInterfaceRFC +using GeoInterface using Test include("test_primitives.jl") diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 54bf3997..0105be4c 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -2,17 +2,17 @@ struct MyCurve end @testset "Primitives" begin # Implement interface - GeoInterfaceRFC.geomtype(::MyCurve) = GeoInterfaceRFC.LineString() - GeoInterfaceRFC.ncoord(::GeoInterfaceRFC.LineString, geom::MyCurve) = 2 - GeoInterfaceRFC.ngeom(::GeoInterfaceRFC.LineString, geom::MyCurve) = 2 - GeoInterfaceRFC.getgeom(::GeoInterfaceRFC.LineString, geom::MyCurve, i) = [[1,2],[2,3]][i] + GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString() + GeoInterface.ncoord(::GeoInterface.LineString, geom::MyCurve) = 2 + GeoInterface.ngeom(::GeoInterface.LineString, geom::MyCurve) = 2 + GeoInterface.getgeom(::GeoInterface.LineString, geom::MyCurve, i) = [[1,2],[2,3]][i] # Test validity geom = MyCurve() @test test_interface_for_geom(geom) # Check functions - @test GeoInterfaceRFC.npoint(geom) == 2 # defaults to ngeom - @test_throws MethodError GeoInterfaceRFC.area(geom) + @test GeoInterface.npoint(geom) == 2 # defaults to ngeom + @test_throws MethodError GeoInterface.area(geom) end From 668dc52e7487cef50cccce029050a925c557ce30 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 25 Mar 2022 22:13:59 +0100 Subject: [PATCH 03/41] Small update. --- .github/workflows/CI.yml | 55 ++++++++++++++++ .github/workflows/CompatHelper.yml | 16 ++--- .gitignore | 2 + .travis.yml | 25 -------- Project.toml | 6 +- README.md | 4 ++ docs/Manifest.toml | 100 +++++++++++++++++++++++++++++ docs/Project.toml | 3 + docs/make.jl | 24 +++++++ docs/src/index.md | 14 ++++ 10 files changed, 211 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/CI.yml delete mode 100644 .travis.yml create mode 100644 docs/Manifest.toml create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/index.md diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..f144a444 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,55 @@ +name: CI +on: + push: + branches: + - 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 }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - "1.0" + - "1.7" + - "nightly" + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/cache@v1 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + 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 Template + DocMeta.setdocmeta!(Template, :DocTestSetup, :(using Template); recursive=true) + doctest(Template)' diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 6a23f757..cba9134c 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,24 +1,16 @@ name: CompatHelper - on: schedule: - - cron: '26 11 * * *' - + - cron: 0 0 * * * + workflow_dispatch: jobs: CompatHelper: - runs-on: ${{ matrix.os }} - strategy: - matrix: - julia-version: [1.2.0] - julia-arch: [x86] - os: [ubuntu-latest] + runs-on: ubuntu-latest steps: - - uses: julia-actions/setup-julia@latest - with: - version: ${{ matrix.julia-version }} - 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 }} run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.gitignore b/.gitignore index 42f07ead..2aee81fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.jl.cov *.jl.mem +/Manifest.toml +/docs/build/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2e49209b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Documentation: http://docs.travis-ci.com/user/languages/julia -language: julia -notifications: - email: false -julia: - - 1.0 - - 1.5 - - nightly -os: - - linux - - osx - - windows -arch: - - x64 - - x86 -cache: - directories: - - ~/.julia/artifacts -jobs: - fast_finish: true - allow_failures: - - julia: nightly - exclude: - - arch: x86 - os: osx diff --git a/Project.toml b/Project.toml index 62399d7e..8d23d8cd 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,13 @@ name = "GeoInterface" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -license = "MIT" +authors = ["JuliaGeo and contributors"] version = "1.0.0" +[deps] +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + [compat] +RecipesBase = "1" julia = "1" [extras] diff --git a/README.md b/README.md index 8e00cc24..65e46334 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![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) + # GeoInterface An interface for geospatial vector data in Julia diff --git a/docs/Manifest.toml b/docs/Manifest.toml new file mode 100644 index 00000000..61a55529 --- /dev/null +++ b/docs/Manifest.toml @@ -0,0 +1,100 @@ +# 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.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 = "85b5da0fa43588c75bb1ff986493443f821c70b7" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.2.3" + +[[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.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[deps.Template]] +path = ".." +uuid = "bf04f2de-4e87-4102-bdb5-62115b2b7225" +version = "0.1.0" + +[[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..4d1a6354 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Template = "bf04f2de-4e87-4102-bdb5-62115b2b7225" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 00000000..427bf486 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,24 @@ +using GeoInterface +using Documenter + +DocMeta.setdocmeta!(GeoInterface, :DocTestSetup, :(using GeoInterface); recursive=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", + ] +) + +deploydocs(; + repo="github.com/JuliaGeo/GeoInterface.jl", + devbranch="main" +) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..f1e310ef --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,14 @@ +```@meta +CurrentModule = GeoInterface +``` + +# GeoInterface + +Documentation for [GeoInterface](https://github.com/JuliaGeo/GeoInterface.jl). + +```@index +``` + +```@autodocs +Modules = [GeoInterface] +``` From d47dd88f24aa7d6893bfec67ed29a018a6703842 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 25 Mar 2022 22:30:18 +0100 Subject: [PATCH 04/41] Fix review comments. --- src/interface.jl | 6 +++--- src/utils.jl | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 3ed92d8e..040a3923 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -2,7 +2,7 @@ """Returns the geometry type, such as `Polygon` or `Point`.""" function geomtype(geom) throw(ErrorException(string("Unknown Geometry type. ", - "Define GeoInterface.geomtype(::$(typeof(geom))) to return the desired type."))) + "Define GeoInterface.geomtype(::$(typeof(geom))) to return the desired type."))) end # All types @@ -55,7 +55,7 @@ npatch(geom)::Integer = npatch(geomtype(geom), geom) """Returns a polygon in this surface, the order is arbitrary.""" getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) """Returns the collection of polygons in this surface that bounds the given polygon “p” for any polygon “p” in the surface.""" -boundingpolygons() +boundingpolygons(geom) = nothing # GeometryCollection ngeom(geom) = ngeom(geomtype(geom), geom) @@ -70,7 +70,7 @@ npolygon(geom) = npolygon(geomtype(geom), geom) getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) # Other methods -crs(geom) = missing # or conforming to <:CoordinateReferenceSystemFormat in GeoFormatTypes +crs(geom) = nothing # or conforming to <:CoordinateReferenceSystemFormat in GeoFormatTypes # DE-9IM, see https://en.wikipedia.org/wiki/DE-9IM diff --git a/src/utils.jl b/src/utils.jl index a6ca2c60..dce7c92f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,5 +1,5 @@ """Test whether the interface for your `geom` has been implemented correctly.""" -function test_interface_for_geom(geom) +function testgeometry(geom) try type = geomtype(geom) ncoord(geom) @@ -14,3 +14,13 @@ function test_interface_for_geom(geom) end return true end + +"""Test geomtype for `geom` has been implemented.""" +function isgeometry(geom) + try + type = geomtype(geom) + catch + return false + end + return true +end From 5ce639b21de34e6c5713b56d0f0dc87abec28f8d Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 25 Mar 2022 23:27:05 +0100 Subject: [PATCH 05/41] Use `isgeometry` as a first check. --- .github/workflows/CI.yml | 6 +++--- .gitignore | 1 + README.md | 4 +++- docs/Manifest.toml | 4 ++-- docs/Project.toml | 2 +- docs/src/index.md | 6 +++--- src/GeoInterface.jl | 10 +++++++++- src/interface.jl | 16 ++++++++++++---- src/utils.jl | 11 +---------- test/test_primitives.jl | 5 +++-- 10 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 52da0359..67fad79d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -50,6 +50,6 @@ jobs: - run: | julia --project=docs -e ' using Documenter: DocMeta, doctest - using Template - DocMeta.setdocmeta!(Template, :DocTestSetup, :(using Template); recursive=true) - doctest(Template)' + using GeoInterface + DocMeta.setdocmeta!(GeoInterface, :DocTestSetup, :(using GeoInterface); recursive=true) + doctest(GeoInterface)' diff --git a/.gitignore b/.gitignore index 2aee81fe..50d473a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.jl.mem /Manifest.toml /docs/build/ +.DS_Store diff --git a/README.md b/README.md index 65e46334..3fdfc0e9 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ GeoInterface provides a traits interface, not unlike Tables.jl, by (a) a set of functions: ```julia +isgeometry(geom) geomtype(geom) ncoord(geom) ngeom(geom) @@ -103,6 +104,7 @@ MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: Abstrac GeoInterface requires five functions to be defined for a given geom: ```julia +GeoInterface.isgeometry(geom::geomtype)::Bool = true GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.X() GeoInterface.ncoord(geomtype(geom), geom::geomtype)::Integer GeoInterface.getcoord(geomtype(geom), geom::geomtype, i)::Real # only for Points @@ -202,5 +204,5 @@ GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::geomtype, i)::"Polygon" GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. ```julia -GeoInterface.test_interface_for_geom(geom) +GeoInterface.testgeometry(geom) ``` diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 61a55529..2a437315 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -89,8 +89,8 @@ uuid = "6462fe0b-24de-5631-8697-dd941f90decc" [[deps.Template]] path = ".." -uuid = "bf04f2de-4e87-4102-bdb5-62115b2b7225" -version = "0.1.0" +uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" +version = "1.0.0" [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] diff --git a/docs/Project.toml b/docs/Project.toml index 4d1a6354..e4d2aa00 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,3 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Template = "bf04f2de-4e87-4102-bdb5-62115b2b7225" +GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" diff --git a/docs/src/index.md b/docs/src/index.md index f1e310ef..7b4b5956 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,9 +6,9 @@ CurrentModule = GeoInterface Documentation for [GeoInterface](https://github.com/JuliaGeo/GeoInterface.jl). -```@index -``` - ```@autodocs Modules = [GeoInterface] ``` + +```@index +``` diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index 45296b25..7281f84b 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -1,12 +1,20 @@ module GeoInterface +# Use the README as the module docs +@doc let + path = joinpath(dirname(@__DIR__), "README.md") + include_dependency(path) + read(path, String) +end GeoInterface + include("types.jl") include("interface.jl") include("defaults.jl") # include("primitives.jl") # needs rethinking include("utils.jl") -export test_interface_for_geom +export testgeometry +export isgeometry export geomtype export ncoord diff --git a/src/interface.jl b/src/interface.jl index 040a3923..9ec65f51 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,9 +1,17 @@ # All Geometries +""" + GeoInterface.isgeometry(x) => Bool + +Check if an object has specifically defined that it is a geometry. +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 + """Returns the geometry type, such as `Polygon` or `Point`.""" -function geomtype(geom) - throw(ErrorException(string("Unknown Geometry type. ", - "Define GeoInterface.geomtype(::$(typeof(geom))) to return the desired type."))) -end +geomtype(geom) = nothing # All types """ diff --git a/src/utils.jl b/src/utils.jl index dce7c92f..315a0fdc 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,7 @@ """Test whether the interface for your `geom` has been implemented correctly.""" function testgeometry(geom) try + @assert isgeometry(geom) type = geomtype(geom) ncoord(geom) ngeom(geom) @@ -14,13 +15,3 @@ function testgeometry(geom) end return true end - -"""Test geomtype for `geom` has been implemented.""" -function isgeometry(geom) - try - type = geomtype(geom) - catch - return false - end - return true -end diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 0105be4c..4d07d7cf 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -2,14 +2,15 @@ struct MyCurve end @testset "Primitives" begin # Implement interface + GeoInterface.isgeometry(::Type{MyCurve}) = true GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString() GeoInterface.ncoord(::GeoInterface.LineString, geom::MyCurve) = 2 GeoInterface.ngeom(::GeoInterface.LineString, geom::MyCurve) = 2 - GeoInterface.getgeom(::GeoInterface.LineString, geom::MyCurve, i) = [[1,2],[2,3]][i] + GeoInterface.getgeom(::GeoInterface.LineString, geom::MyCurve, i) = [[1, 2], [2, 3]][i] # Test validity geom = MyCurve() - @test test_interface_for_geom(geom) + @test testgeometry(geom) # Check functions @test GeoInterface.npoint(geom) == 2 # defaults to ngeom From 47281c4ae09dcab72d86324ccaa998814c8b9293 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 25 Mar 2022 23:42:28 +0100 Subject: [PATCH 06/41] Updated integrations. --- INTEGRATIONS.md | 16 ++++++++++++++++ docs/Manifest.toml | 2 +- find_integrations.jl | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 INTEGRATIONS.md create mode 100644 find_integrations.jl diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md new file mode 100644 index 00000000..b15cf1ec --- /dev/null +++ b/INTEGRATIONS.md @@ -0,0 +1,16 @@ +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/docs/Manifest.toml b/docs/Manifest.toml index 2a437315..66af9e47 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -87,7 +87,7 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[deps.Template]] +[[deps.GeoInterface]] path = ".." uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" version = "1.0.0" diff --git a/find_integrations.jl b/find_integrations.jl new file mode 100644 index 00000000..ee319379 --- /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 Tables.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 Tables.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 currently integrating with GeoInterface.jl:") + for (nm, url) in zip(pkgnames, urls) + println(io, "* [$nm]($url)") + end +end From 27dc9fe42332d4d2f12810c417d0cfd8d9986155 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 1 Apr 2022 15:03:36 +0200 Subject: [PATCH 07/41] Improved documentation. --- .gitignore | 2 + INTEGRATIONS.md | 1 + Project.toml | 1 + README.md | 199 +---------------------------- docs/make.jl | 17 +++ docs/src/background/sf.md | 70 ++++++++++ docs/src/guides/defaults.md | 3 + docs/src/guides/developer.md | 110 ++++++++++++++++ docs/src/guides/sink.md | 6 + docs/src/guides/source.md | 6 + docs/src/index.md | 21 ++- docs/src/reference/api.md | 17 +++ docs/src/tutorials/installation.md | 7 + docs/src/tutorials/usage.md | 25 ++++ find_integrations.jl | 2 +- src/GeoInterface.jl | 8 -- src/defaults.jl | 10 +- src/interface.jl | 32 ++++- src/primitives.jl | 48 ------- src/types.jl | 42 +++--- test/test_primitives.jl | 2 +- 21 files changed, 337 insertions(+), 292 deletions(-) create mode 100644 docs/src/background/sf.md create mode 100644 docs/src/guides/defaults.md create mode 100644 docs/src/guides/developer.md create mode 100644 docs/src/guides/sink.md create mode 100644 docs/src/guides/source.md create mode 100644 docs/src/reference/api.md create mode 100644 docs/src/tutorials/installation.md create mode 100644 docs/src/tutorials/usage.md delete mode 100644 src/primitives.jl diff --git a/.gitignore b/.gitignore index 50d473a8..f459254d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /Manifest.toml /docs/build/ .DS_Store +docs/src/reference/integrations.md + diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md index b15cf1ec..d528450e 100644 --- a/INTEGRATIONS.md +++ b/INTEGRATIONS.md @@ -1,3 +1,4 @@ +# Packages Packages currently integrating with GeoInterface.jl: * [AlgebraOfGraphics](https://github.com/JuliaPlots/AlgebraOfGraphics.jl.git) * [ArchGDAL](https://github.com/yeesian/ArchGDAL.jl.git) diff --git a/Project.toml b/Project.toml index 8d23d8cd..0a6c61b0 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["JuliaGeo and contributors"] version = "1.0.0" [deps] +Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" [compat] diff --git a/README.md b/README.md index 3fdfc0e9..7c059075 100644 --- a/README.md +++ b/README.md @@ -7,202 +7,7 @@ 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. +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, but for geometries instead of tables. Packages which support the GeoInterface.jl interface can be found in [INTEGRATIONS.md](INTEGRATIONS.md). - -## 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 `Xn` becomes `getX`: -```julia -GeometryType -> geomtype -NumGeometries -> ngeom -GeometryN -> getgeom -NumPatches -> npatch -# etc -``` - -We also simplified the dimension functions. From the three original (`dimension`, `coordinateDimension`, `spatialDimension`) there's now only the coordinate dimension, so not to overlap with the Julia `ndims`. -```julia -coordinateDimension -> ncoords -``` - -We've generalized the some functions: -```julia -SRID -> crs -envelope -> extent -``` - -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 -spatialDimension -asText -asBinary -is3D -isMeasured -boundary - -locateAlong -locateBetween - -distance -buffer -convexHull - -``` -While the following functions have no implementation: -```julia -equals -disjoint -touches -within -overlaps -crosses -intersects -contains -relate - -intersection -union -difference -symdifference -``` - - -## Implementation -GeoInterface provides a traits interface, not unlike Tables.jl, by - -(a) a set of functions: -```julia -isgeometry(geom) -geomtype(geom) -ncoord(geom) -ngeom(geom) -getgeom(geom::geomtype, i) -... -``` -(b) a set of types for dispatching on said functions. - The types tells GeoInterface how to interpret the input object inside a GeoInterface function. - -```julia -abstract Geometry -Point <: AbstractPoint <: AbstractGeometry -MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: AbstractGeometry -... -``` - -### For developers looking to implement the interface -GeoInterface requires five functions to be defined for a given geom: - -```julia -GeoInterface.isgeometry(geom::geomtype)::Bool = true -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.X() -GeoInterface.ncoord(geomtype(geom), geom::geomtype)::Integer -GeoInterface.getcoord(geomtype(geom), geom::geomtype, i)::Real # only for Points -GeoInterface.ngeom(geomtype(geom), geom::geomtype)::Integer -GeoInterface.getgeom(geomtype(geom), geom::geomtype, i) # geomtype -> GeoInterface.Y -``` -Where the `getgeom` could be an iterator (without the i) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. - -There are also optional generic methods that could help or speed up operations: -```julia -GeoInterface.crs(geom)::Union{Missing, GeoFormatTypes.CoordinateReferenceSystemFormat} -GeoInterface.extent(geom) # geomtype -> GeoInterface.Rectangle -``` - -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 the next chapter. - -### Examples - -A `geom::geomtype` with "Point"-like traits implements -```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Point() -GeoInterface.ncoord(::GeoInterface.Point, geom::geomtype)::Integer -GeoInterface.getcoord(::GeoInterface.Point, geom::geomtype, i)::Real - -# Defaults -GeoInterface.ngeom(::GeoInterface.Point, geom)::Integer = 0 -GeoInterface.getgeom(::GeoInterface.Point, geom::geomtype, i) = nothing -``` - -A `geom::geomtype` with "LineString"-like traits implements the following methods: -```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.LineString() -GeoInterface.ncoord(::GeoInterface.LineString, geom::geomtype)::Integer - -# These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.LineString, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.LineString, geom::geomtype, i) # of geomtype Point - -# Optional -GeoInterface.isclosed(::GeoInterface.LineString, geom::geomtype)::Bool -GeoInterface.issimple(::GeoInterface.LineString, geom::geomtype)::Bool -GeoInterface.length(::GeoInterface.LineString, geom::geomtype)::Real -``` -A `geom::geomtype` with "Polygon"-like traits can implement the following methods: -```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Polygon() -GeoInterface.ncoord(::GeoInterface.Polygon, geom::geomtype)::Integer - -# These alias for nring and getring -GeoInterface.ngeom(::GeoInterface.Polygon, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.Polygon, geom::geomtype, i)::"LineString" - -# Optional -GeoInterface.area(::GeoInterface.Polygon, geom::geomtype)::Real -GeoInterface.centroid(::GeoInterface.Polygon, geom::geomtype)::"Point" -GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::geomtype)::"Point" -GeoInterface.boundary(::GeoInterface.Polygon, geom::geomtype)::"LineString" - -``` -A `geom::geomtype` with "GeometryCollection"-like traits has to implement the following methods: -```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.GeometryCollection() -GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::geomtype)::Integer -GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::geomtypem, i)::"Geometry" -``` -A `geom::geomtype` with "MultiPoint"-like traits has to implement the following methods: -```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPoint() -GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::geomtype)::Integer - -# These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::geomtype, i)::"Point" -``` -A `geom::geomtype` with "MultiLineString"-like traits has to implement the following methods: -```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiLineString() -GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::geomtype)::Integer - -# These alias for nlinestring and getlinestring -GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::geomtypem, i)::"LineString" -``` -A `geom::geomtype` with "MultiPolygon"-like traits has to implement the following methods: -```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPolygon() -GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::geomtype)::Integer - -# These alias for npolygon and getpolygon -GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::geomtype, i)::"Polygon" -``` - - -### Testing the interface -GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. - -```julia -GeoInterface.testgeometry(geom) -``` diff --git a/docs/make.jl b/docs/make.jl index 427bf486..631a4666 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,6 +2,7 @@ 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], @@ -15,6 +16,22 @@ makedocs(; ), pages=[ "Home" => "index.md", + "Background" => Any[ + "Simple Features"=>"background/sf.md", + ], + "Tutorials" => Any[ + "Installation"=>"tutorials/installation.md", + "Usage"=>"tutorials/usage.md", + ], + "Guides" => Any[ + "For developers"=>"guides/developer.md", + "As source"=>"guides/source.md", + "As sink"=>"guides/sink.md", + ], + "Reference" => Any[ + "API" => "reference/api.md" + "Implementations" => "reference/integrations.md" + ], ] ) diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md new file mode 100644 index 00000000..7970a617 --- /dev/null +++ b/docs/src/background/sf.md @@ -0,0 +1,70 @@ +# Simple Features +Test + +## 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 `Xn` becomes `getX`: +```julia +GeometryType -> geomtype +NumGeometries -> ngeom +GeometryN -> getgeom +NumPatches -> npatch +# etc +``` + +We also simplified the dimension functions. From the three original (`dimension`, `coordinateDimension`, `spatialDimension`) there's now only the coordinate dimension, so not to overlap with the Julia `ndims`. +```julia +coordinateDimension -> ncoords # x, y, z, m +``` + +We've generalized the some functions: +```julia +SRID -> crs +envelope -> extent +``` + +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 +spatialDimension +asText +asBinary +boundary + +locateAlong +locateBetween + +distance +buffer +convexHull + +``` +While the following functions have no implementation: +```julia +equals +disjoint +touches +within +overlaps +crosses +intersects +contains +relate + +intersection +union +difference +symdifference +``` + +## History +Test diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md new file mode 100644 index 00000000..d54b46b1 --- /dev/null +++ b/docs/src/guides/defaults.md @@ -0,0 +1,3 @@ +# Fallbacks +!!! warning + TODO diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md new file mode 100644 index 00000000..258e436c --- /dev/null +++ b/docs/src/guides/developer.md @@ -0,0 +1,110 @@ +# Implementing GeoInterface +GeoInterface requires five functions to be defined for a given geom: + +## Required + +```julia +GeoInterface.isgeometry(geom::geomtype)::Bool = true +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.X() +GeoInterface.ncoord(geomtype(geom), geom::geomtype)::Integer +GeoInterface.getcoord(geomtype(geom), geom::geomtype, i)::Real # only for Points +GeoInterface.ngeom(geomtype(geom), geom::geomtype)::Integer +GeoInterface.getgeom(geomtype(geom), geom::geomtype, i) # geomtype -> GeoInterface.Y +``` +Where the `getgeom` could be an iterator (without the i) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. + +## Optional + +There are also optional generic methods that could help or speed up operations: +```julia +GeoInterface.crs(geom)::Union{Missing, GeoFormatTypes.CoordinateReferenceSystemFormat} +GeoInterface.extent(geom)::Extents.Extent # geomtype -> GeoInterface.Rectangle +``` + +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). + +## Examples + +A `geom::geomtype` with "Point"-like traits implements +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Point() +GeoInterface.ncoord(::GeoInterface.Point, geom::geomtype)::Integer +GeoInterface.getcoord(::GeoInterface.Point, geom::geomtype, i)::Real + +# Defaults +GeoInterface.ngeom(::GeoInterface.Point, geom)::Integer = 0 +GeoInterface.getgeom(::GeoInterface.Point, geom::geomtype, i) = nothing +``` + +A `geom::geomtype` with "LineString"-like traits implements the following methods: +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.LineString() +GeoInterface.ncoord(::GeoInterface.LineString, geom::geomtype)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::GeoInterface.LineString, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.LineString, geom::geomtype, i) # of geomtype Point + +# Optional +GeoInterface.isclosed(::GeoInterface.LineString, geom::geomtype)::Bool +GeoInterface.issimple(::GeoInterface.LineString, geom::geomtype)::Bool +GeoInterface.length(::GeoInterface.LineString, geom::geomtype)::Real +``` +A `geom::geomtype` with "Polygon"-like traits can implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Polygon() +GeoInterface.ncoord(::GeoInterface.Polygon, geom::geomtype)::Integer + +# These alias for nring and getring +GeoInterface.ngeom(::GeoInterface.Polygon, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.Polygon, geom::geomtype, i)::"LineString" + +# Optional +GeoInterface.area(::GeoInterface.Polygon, geom::geomtype)::Real +GeoInterface.centroid(::GeoInterface.Polygon, geom::geomtype)::"Point" +GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::geomtype)::"Point" +GeoInterface.boundary(::GeoInterface.Polygon, geom::geomtype)::"LineString" + +``` +A `geom::geomtype` with "GeometryCollection"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.GeometryCollection() +GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::geomtype)::Integer +GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::geomtypem, i)::"Geometry" +``` +A `geom::geomtype` with "MultiPoint"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPoint() +GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::geomtype)::Integer + +# These alias for npoint and getpoint +GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::geomtype, i)::"Point" +``` +A `geom::geomtype` with "MultiLineString"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiLineString() +GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::geomtype)::Integer + +# These alias for nlinestring and getlinestring +GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::geomtypem, i)::"LineString" +``` +A `geom::geomtype` with "MultiPolygon"-like traits has to implement the following methods: +```julia +GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPolygon() +GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::geomtype)::Integer + +# These alias for npolygon and getpolygon +GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::geomtype)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::geomtype, i)::"Polygon" +``` + + +## Testing the interface +GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. + +```julia +GeoInterface.testgeometry(geom) +``` diff --git a/docs/src/guides/sink.md b/docs/src/guides/sink.md new file mode 100644 index 00000000..bab3fa17 --- /dev/null +++ b/docs/src/guides/sink.md @@ -0,0 +1,6 @@ +# Usage as Sink +GeoInterface requires five functions to be defined for a given geom: + +## Using the interface +!!! warning + TODO diff --git a/docs/src/guides/source.md b/docs/src/guides/source.md new file mode 100644 index 00000000..0d3dc5c9 --- /dev/null +++ b/docs/src/guides/source.md @@ -0,0 +1,6 @@ +# Usage as Source +GeoInterface requires five functions to be defined for a given geom: + +## Using the interface +!!! warning + TODO diff --git a/docs/src/index.md b/docs/src/index.md index 7b4b5956..31de2588 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,12 +3,21 @@ 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) -Documentation for [GeoInterface](https://github.com/JuliaGeo/GeoInterface.jl). +*An interface for geospatial vector data in Julia* -```@autodocs -Modules = [GeoInterface] -``` +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, but for geometries instead of tables. -```@index -``` +Packages which support the GeoInterface.jl interface can be found in [Packages](@ref). + +For background about the interface and Simple Features, see [Changes with respect to SF](@ref). +For usage see [Traits interface](@ref), while if you look to implement GeoInterface in your own package, check out [Implementing GeoInterface](@ref). + +!!! compat + This traits interface is new and is a major departure from previous pre-1.0 releases. 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..b3aac14b --- /dev/null +++ b/docs/src/reference/api.md @@ -0,0 +1,17 @@ +# 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..3ed5bff1 --- /dev/null +++ b/docs/src/tutorials/usage.md @@ -0,0 +1,25 @@ +# Traits interface +GeoInterface provides a traits interface, not unlike Tables.jl, by + +## Functions +(a) a set of functions: +```julia +isgeometry(geom) +geomtype(geom) +ncoord(geom) +ngeom(geom) +getgeom(geom::geomtype, i) +... +``` + +## Types +(b) a set of types for dispatching on said functions. + +The types tell GeoInterface how to interpret the input object inside a GeoInterface function. + +```julia +abstract Geometry +Point <: AbstractPoint <: AbstractGeometry +MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: AbstractGeometry +... +``` diff --git a/find_integrations.jl b/find_integrations.jl index ee319379..14108a30 100644 --- a/find_integrations.jl +++ b/find_integrations.jl @@ -32,7 +32,7 @@ urls = [parseurl(joinpath(REGISTRIES, "General", string(first(nm)), nm, "Package for nm in pkgnames] open(joinpath(dirname(@__DIR__), "GeoInterface.jl", "INTEGRATIONS.md"), "w+") do io - println(io, "Packages currently integrating with GeoInterface.jl:") + println(io, "# Packages\nPackages currently integrating with GeoInterface.jl:") for (nm, url) in zip(pkgnames, urls) println(io, "* [$nm]($url)") end diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index 7281f84b..fd379612 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -1,16 +1,8 @@ module GeoInterface -# Use the README as the module docs -@doc let - path = joinpath(dirname(@__DIR__), "README.md") - include_dependency(path) - read(path, String) -end GeoInterface - include("types.jl") include("interface.jl") include("defaults.jl") -# include("primitives.jl") # needs rethinking include("utils.jl") export testgeometry diff --git a/src/defaults.jl b/src/defaults.jl index bd552c45..69fcb3f7 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -2,15 +2,19 @@ ## Coords # Four options in SF, xy, xyz, xym, xyzm -const default_coord_names = (:X, :Y, :Z, :M) +const default_coord_names = (:X, :Y, :Z, :M) # always uppercase? coordnames(geom) = default_coord_names[1:ncoord(geom)] +# Maybe hardcode dimension order? At least for X and Y? x(geom) = getcoord(geom, findfirst(coordnames(geom), :X)) y(geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) z(geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) m(geom) = getcoord(geom, findfirst(coordnames(geom), :M)) +is3d(geom) = :Z in coordnames(geom) +ismeasured(geom) = :M in coordnames(geom) + ## Points npoint(c::AbstractCurve, geom) = ngeom(c, geom) getpoint(c::AbstractCurve, geom, i) = getgeom(c, geom, i) @@ -23,7 +27,7 @@ nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) nlinestring(::AbstractMultiLineString, geom) = ngeom(p, geom) -nlinestring(::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) +getlinestring(::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) npolygon(::AbstractMultiPolygon, geom) = ngeom(p, geom) getpolygon(::AbstractMultiPolygon, geom, i) = getgeom(p, geom, i) @@ -35,7 +39,7 @@ npoint(::Quad, _) = 4 npoint(::Pentagon, _) = 5 npoint(::Hexagon, _) = 6 -issimple(::AbstractCurve, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom) - 1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) +issimple(::AbstractCurve, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) isclosed(::AbstractCurve, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) isring(x::AbstractCurve, geom) = issimple(x, geom) && isclosed(x, geom) diff --git a/src/interface.jl b/src/interface.jl index 9ec65f51..4bb38f42 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -2,7 +2,7 @@ """ GeoInterface.isgeometry(x) => Bool -Check if an object has specifically defined that it is a geometry. +Check if an object `x` is a geometry and thus implicitely 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. @@ -10,20 +10,34 @@ method. isgeometry(x::T) where {T} = isgeometry(T) isgeometry(::Type{T}) where {T} = false -"""Returns the geometry type, such as `Polygon` or `Point`.""" +""" + GeoInterface.geomtype(geom) => T <: AbstractGeometry + +Returns the geometry type, such as [`GeoInterface.Polygon`](@ref) or [`GeoInterface.Point`](@ref). +""" geomtype(geom) = nothing # All types """ ncoord(geom) -> Integer -Return the number of coordinate dimensions (such as XYZ) for the geometry. +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) -"""Return `true` when the geometry is empty.""" +""" + isempty(geom) -> Bool + +Return `true` when the geometry is empty. +""" isempty(geom) = ngeom(geom) == 0 -"""Return `true` when the geometry doesn't cross itself.""" + +""" + issimple(geom) -> Bool + +Return `true` when the geometry is simple, i.e. doesn't cross or touch itself. +""" issimple(geom) = issimple(geomtype(geom), geom) # Point @@ -78,7 +92,13 @@ npolygon(geom) = npolygon(geomtype(geom), geom) getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) # Other methods -crs(geom) = nothing # or conforming to <:CoordinateReferenceSystemFormat in GeoFormatTypes +""" + crs(geom) -> T <: GeoFormatTypes.CoordinateReferenceSystemFormat + +Retrieve Coordinate Reference System for given geom. +In SF this is defined as `SRID`. +""" +crs(geom) = nothing # DE-9IM, see https://en.wikipedia.org/wiki/DE-9IM diff --git a/src/primitives.jl b/src/primitives.jl deleted file mode 100644 index 88a4622b..00000000 --- a/src/primitives.jl +++ /dev/null @@ -1,48 +0,0 @@ -const Indexable = Union{AbstractVector,Tuple} - -# Point -geomtype(::AbstractVector{T}) where {T <: Real} = Point() -geomtype(::Tuple{T,U}) where {T,U <: Real} = Point() -geomtype(::Tuple{T,U,V}) where {T,U,V <: Real} = Point() -ncoord(::AbstractPoint, geom::Indexable) = length(geom) -getcoord(::AbstractPoint, geom::Indexable, i::Integer) = geom[i] - -# LineString -ncoord(::AbstractLineString, geom::Indexable) = - ncoord(Point, getpoint(geom, 1)) -npoint(::AbstractLineString, geom::Indexable) = length(geom) -getpoint(::AbstractLineString, geom::Indexable, i::Integer) = geom[i] - -# Polygon -ncoord(::AbstractPolygon, geom::Indexable) = - ncoord(LineString, getexterior(geom)) -getexterior(::AbstractPolygon, geom::Indexable) = geom[1] -nhole(::AbstractPolygon, geom::Indexable) = length(geom) - 1 -gethole(::AbstractPolygon, geom::Indexable, i::Integer) = geom[i + 1] - -# MultiPoint -ncoord(::AbstractMultiPoint, geom::Indexable) = - ncoord(Point, getpoint(geom, 1)) -npoint(::AbstractMultiPoint, geom::Indexable) = length(geom) -getpoint(::AbstractMultiPoint, geom::Indexable, i::Integer) = geom[i] - -# MultiLineString -ncoord(::AbstractMultiLineString, geom::Indexable) = - ncoord(LineString, getlinestring(geom, 1)) -nlinestring(::AbstractMultiLineString, geom::Indexable) = length(geom) -getlinestring(::AbstractMultiLineString, geom::Indexable, i::Integer) = - geom[i] - -# MultiPolygon -ncoord(::AbstractMultiPolygon, geom::Indexable) = - ncoord(Polygon, getpolygon(geom, 1)) -npolygon(::AbstractMultiPolygon, geom::Indexable) = length(geom) -getpolygon(::AbstractMultiPolygon, geom::Indexable, i::Integer) = geom[i] - -# GeometryCollection -ncoord(::AbstractGeometryCollection, geom::Indexable) = - ncoord(geomtype(geom), getgeom(geom, 1)) -ngeom(::AbstractGeometryCollection, collection::Indexable) = - length(collection) -getgeom(::AbstractGeometryCollection, collection::Indexable, i::Integer) = - collection[i] diff --git a/src/types.jl b/src/types.jl index ef313064..dff61932 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,68 +1,66 @@ abstract type AbstractGeometry end -# """The general `Geometry` type.""" -# struct Geometry <: AbstractGeometry end # commented out as it won't be used directly abstract type AbstractGeometryCollection <: AbstractGeometry end -"""A `GeometryCollection` is a collection of `Geometry`s.""" +"""A [`GeometryCollection`](@ref) is a collection of `Geometry`s.""" struct GeometryCollection <: AbstractGeometryCollection end abstract type AbstractPoint <: AbstractGeometry end -"""A simple `Point`.""" +"""A simple [`Point`](@ref).""" struct Point <: AbstractPoint end abstract type AbstractCurve <: AbstractGeometry end abstract type AbstractLineString <: AbstractCurve end -"""A `LineString` is a collection of straight lines between its `Point`s.""" +"""A [`LineString`](@ref) is a collection of straight lines between its `Point`s.""" struct LineString <: AbstractLineString end -"""A Line is `LineString` with just two points.""" +"""A Line is [`LineString`](@ref) with just two points.""" struct Line <: AbstractLineString end -"""A LinearRing is a `LineString` with the same begin and endpoint.""" +"""A LinearRing is a [`LineString`](@ref) with the same begin and endpoint.""" struct LinearRing <: AbstractLineString end -"""A `CircularString` is a curve, with an odd number of points. +"""A [`CircularString`](@ref) 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 CircularString <: AbstractCurve end -"""A `CompoundCurve` is a curve that combines straight `LineString`s and curved `CircularString`s.""" +"""A [`CompoundCurve`](@ref) is a curve that combines straight [`LineString`](@ref)s and curved [`CircularString`](@ref)s.""" struct CompoundCurve <: AbstractCurve end abstract type AbstractSurface <: AbstractGeometry end abstract type AbstractCurvePolygon <: AbstractSurface end -"""A `Polygon` that can contain either circular or straight curves as rings.""" +"""A [`Polygon`](@ref) that can contain either circular or straight curves as rings.""" struct CurvePolygon <: AbstractCurvePolygon end abstract type AbstractPolygon <: AbstractCurvePolygon end -"""A `Polygon` with straight rings either as exterior or interior(s).""" +"""A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" struct Polygon <: AbstractPolygon end -"""A `Polygon` with straight rings either as exterior or interior(s).""" +"""A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" struct Triangle <: AbstractPolygon end -"""A `Polygon` that is rectangular and could be described by the minimum and maximum vertices.""" +"""A [`Polygon`](@ref) that is rectangular and could be described by the minimum and maximum vertices.""" struct Rectangle <: AbstractPolygon end -"""A `Polygon` with four vertices.""" +"""A [`Polygon`](@ref) with four vertices.""" struct Quad <: AbstractPolygon end -"""A `Polygon` with five vertices.""" +"""A [`Polygon`](@ref) with five vertices.""" struct Pentagon <: AbstractPolygon end -"""A `Polygon` with six vertices.""" +"""A [`Polygon`](@ref) with six vertices.""" struct Hexagon <: AbstractPolygon end abstract type AbstractPolyHedralSurface <: AbstractSurface end -"""A `PolyHedralSurface` is a connected surface consisting of Polygons.""" +"""A [`PolyHedralSurface`](@ref) is a connected surface consisting of Polygons.""" struct PolyHedralSurface <: AbstractPolyHedralSurface end -"""A `TIN` is a `PolyHedralSurface` consisting of `Triangle`s.""" +"""A [`TIN`](@ref) is a [`PolyHedralSurface`](@ref) consisting of [`Triangle`](@ref)s.""" struct TIN <: AbstractPolyHedralSurface end # Surface consisting of Triangles abstract type AbstractMultiPoint <: AbstractGeometryCollection end -"""A `MultiPoint` is a collection of `Point`s.""" +"""A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" struct MultiPoint <: AbstractMultiPoint end abstract type AbstractMultiCurve <: AbstractGeometryCollection end -"""A `MultiCurve` is a collection of `Curve`s.""" +"""A [`MultiCurve`](@ref) is a collection of [`CircularString`](@ref)s.""" struct MultiCurve <: AbstractMultiCurve end abstract type AbstractMultiLineString <: AbstractMultiCurve end -"""A `MultiPoint` is a collection of `Point`s.""" +"""A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" struct MultiLineString <: AbstractMultiLineString end abstract type AbstractMultiSurface <: AbstractGeometryCollection end abstract type AbstractMultiPolygon <: AbstractMultiSurface end -"""A `MultiPolygon` is a collection of `Polygon`s.""" +"""A [`MultiPolygon`](@ref) is a collection of [`Polygon`](@ref)s.""" struct MultiPolygon <: AbstractMultiPolygon end diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 4d07d7cf..75f19083 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -1,6 +1,6 @@ struct MyCurve end -@testset "Primitives" begin +@testset "Developer" begin # Implement interface GeoInterface.isgeometry(::Type{MyCurve}) = true GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString() From b727c07691637467fb8ad8a91b4b405c433be6ca Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 4 Apr 2022 14:09:54 +0200 Subject: [PATCH 08/41] Updated docs. Fixed bugs. --- docs/Manifest.toml | 25 ++- docs/make.jl | 3 +- docs/src/background/sf.md | 40 ++-- docs/src/background/types.png | Bin 0 -> 156363 bytes docs/src/guides/defaults.md | 44 +++- docs/src/guides/developer.md | 169 ++++++++++------ docs/src/guides/sink.md | 6 - docs/src/guides/source.md | 6 - docs/src/tutorials/usage.md | 22 +- src/defaults.jl | 41 +++- src/interface.jl | 371 ++++++++++++++++++++++++++++++++-- src/types.jl | 15 +- 12 files changed, 596 insertions(+), 146 deletions(-) create mode 100644 docs/src/background/types.png delete mode 100644 docs/src/guides/sink.md delete mode 100644 docs/src/guides/source.md diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 66af9e47..a51f6f5d 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -27,6 +27,17 @@ 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" @@ -62,9 +73,9 @@ uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" [[deps.Parsers]] deps = ["Dates"] -git-tree-sha1 = "85b5da0fa43588c75bb1ff986493443f821c70b7" +git-tree-sha1 = "621f4f3b4977325b9128d5fae7a8b4829a0c2222" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.2.3" +version = "2.2.4" [[deps.Printf]] deps = ["Unicode"] @@ -78,6 +89,11 @@ uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 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" @@ -87,11 +103,6 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[deps.GeoInterface]] -path = ".." -uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -version = "1.0.0" - [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/docs/make.jl b/docs/make.jl index 631a4666..0e97ef72 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -25,8 +25,7 @@ makedocs(; ], "Guides" => Any[ "For developers"=>"guides/developer.md", - "As source"=>"guides/source.md", - "As sink"=>"guides/sink.md", + "Defaults"=>"guides/defaults.md", ], "Reference" => Any[ "API" => "reference/api.md" diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md index 7970a617..4cb84740 100644 --- a/docs/src/background/sf.md +++ b/docs/src/background/sf.md @@ -1,5 +1,10 @@ # Simple Features -Test +Simple Features (SF) 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), an interface with SQL (Part II) and an SQL/MM extension with support for circular geometry. + +## Type hierarchy +![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. @@ -14,6 +19,9 @@ NumPatches -> npatch # etc ``` +We generalized [`ngeom`](@ref) and [`getgeom`](@ref) to apply to +all geometries, not just a [`AbstractGeometryCollection`](@ref)s. + We also simplified the dimension functions. From the three original (`dimension`, `coordinateDimension`, `spatialDimension`) there's now only the coordinate dimension, so not to overlap with the Julia `ndims`. ```julia coordinateDimension -> ncoords # x, y, z, m @@ -38,33 +46,15 @@ dimension spatialDimension asText asBinary -boundary locateAlong locateBetween - -distance -buffer -convexHull - -``` -While the following functions have no implementation: -```julia -equals -disjoint -touches -within -overlaps -crosses -intersects -contains -relate - -intersection -union -difference -symdifference ``` ## History -Test +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. + +[^sgillies]: https://gist.github.com/sgillies/2217756 +[^geojson]: https://geojson.org/ diff --git a/docs/src/background/types.png b/docs/src/background/types.png new file mode 100644 index 0000000000000000000000000000000000000000..fa50ce9a77e765c413a02ce6073b65332a9603b4 GIT binary patch literal 156363 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ž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 JX(un;`d@xzR1g3F literal 0 HcmV?d00001 diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index d54b46b1..f08b1fd4 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -1,3 +1,41 @@ -# Fallbacks -!!! warning - TODO +# 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 | +|----------------------------|-------------|---------------| +| [`AbstractPoint`](@ref) | - | - | +| [`AbstractCurve`](@ref), [`MultiPoint`](@ref) | [`npoint`](@ref) | [`getpoint`](@ref) | +| [`AbstractPolygon`](@ref) | [`nring`](@ref) | [`getring`](@ref) | +| [`AbstractMultiLineString`](@ref) | [`nlinestring`](@ref) | [`getlinestring`](@ref) | +| [`AbstractMultiPolygon`](@ref) | [`npolygon`](@ref) | [`getpolygon`](@ref) | +| [`AbstractPolyhedralSurface`](@ref) | [`npatch`](@ref) | [`getpatch`](@ref) | +| [`AbstractGeometryCollection`](@ref) | [`ngeom`](@ref) | [`getgeom`](@ref) | + +## Polygons +Of note are `Polygon`s, which can have holes, for which we automatically add the following +functions based on the `ngeom` implemented by package authors. + +```julia +getexterior(p::AbstractPolygon, geom) = getring(p, geom, 1) +nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 +gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) +``` +## LineStrings +Simarly for LineStrings, 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(::Line, _) = 2 +npoint(::Triangle, _) = 3 +npoint(::Rectangle, _) = 4 +npoint(::Quad, _) = 4 +npoint(::Pentagon, _) = 5 +npoint(::Hexagon, _) = 6 +``` diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 258e436c..b8c23d69 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -1,110 +1,153 @@ # Implementing GeoInterface -GeoInterface requires five functions to be defined for a given geom: +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. -## Required +If your package also supports geospatial operations on geometries--such as intersections--, please +also implement those interfaces where applicable. + +## Required for Geometry ```julia -GeoInterface.isgeometry(geom::geomtype)::Bool = true -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.X() -GeoInterface.ncoord(geomtype(geom), geom::geomtype)::Integer -GeoInterface.getcoord(geomtype(geom), geom::geomtype, i)::Real # only for Points -GeoInterface.ngeom(geomtype(geom), geom::geomtype)::Integer -GeoInterface.getgeom(geomtype(geom), geom::geomtype, i) # geomtype -> GeoInterface.Y +GeoInterface.isgeometry(geom::customgeom)::Bool = true +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.X() +GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer +GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for Points +GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer +GeoInterface.getgeom(geomtype(geom), geom::customgeom, i) # geomtype -> GeoInterface.Y ``` Where the `getgeom` could be an iterator (without the i) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. -## Optional +You read more about the `geomtype` in the [Type hierarchy](@ref). + +## Optional for Geometry -There are also optional generic methods that could help or speed up operations: +There are also optional generic methods that could help with locating this geometry. ```julia -GeoInterface.crs(geom)::Union{Missing, GeoFormatTypes.CoordinateReferenceSystemFormat} -GeoInterface.extent(geom)::Extents.Extent # geomtype -> GeoInterface.Rectangle +GeoInterface.crs(geomtype(geom), geom::customgeom)::GeoFormatTypes.CoordinateReferenceSystemFormat} +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). +## 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 all functions that have been implemented also work as expected. + +```julia +GeoInterface.testgeometry(geom) +``` + ## Examples -A `geom::geomtype` with "Point"-like traits implements +All custom geometies implement ```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Point() -GeoInterface.ncoord(::GeoInterface.Point, geom::geomtype)::Integer -GeoInterface.getcoord(::GeoInterface.Point, geom::geomtype, i)::Real +GeoInterface.isgeometry(geom::customgeom)::Bool = true +``` + +A `geom::customgeom` with "Point"-like traits implements +```julia +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Point() +GeoInterface.ncoord(::GeoInterface.Point, geom::customgeom)::Integer +GeoInterface.getcoord(::GeoInterface.Point, geom::customgeom, i)::Real # Defaults GeoInterface.ngeom(::GeoInterface.Point, geom)::Integer = 0 -GeoInterface.getgeom(::GeoInterface.Point, geom::geomtype, i) = nothing +GeoInterface.getgeom(::GeoInterface.Point, geom::customgeom, i) = nothing ``` -A `geom::geomtype` with "LineString"-like traits implements the following methods: +A `geom::customgeom` with "LineString"-like traits implements the following methods: ```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.LineString() -GeoInterface.ncoord(::GeoInterface.LineString, geom::geomtype)::Integer +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.LineString() +GeoInterface.ncoord(::GeoInterface.LineString, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.LineString, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.LineString, geom::geomtype, i) # of geomtype Point +GeoInterface.ngeom(::GeoInterface.LineString, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.LineString, geom::customgeom, i) # of geomtype Point # Optional -GeoInterface.isclosed(::GeoInterface.LineString, geom::geomtype)::Bool -GeoInterface.issimple(::GeoInterface.LineString, geom::geomtype)::Bool -GeoInterface.length(::GeoInterface.LineString, geom::geomtype)::Real +GeoInterface.isclosed(::GeoInterface.LineString, geom::customgeom)::Bool +GeoInterface.issimple(::GeoInterface.LineString, geom::customgeom)::Bool +GeoInterface.length(::GeoInterface.LineString, geom::customgeom)::Real ``` -A `geom::geomtype` with "Polygon"-like traits can implement the following methods: +A `geom::customgeom` with "Polygon"-like traits can implement the following methods: ```julia -GeoInterface.geomtype(geom::geomtype)::DataType = GeoInterface.Polygon() -GeoInterface.ncoord(::GeoInterface.Polygon, geom::geomtype)::Integer +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Polygon() +GeoInterface.ncoord(::GeoInterface.Polygon, geom::customgeom)::Integer # These alias for nring and getring -GeoInterface.ngeom(::GeoInterface.Polygon, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.Polygon, geom::geomtype, i)::"LineString" +GeoInterface.ngeom(::GeoInterface.Polygon, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.Polygon, geom::customgeom, i)::"LineString" # Optional -GeoInterface.area(::GeoInterface.Polygon, geom::geomtype)::Real -GeoInterface.centroid(::GeoInterface.Polygon, geom::geomtype)::"Point" -GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::geomtype)::"Point" -GeoInterface.boundary(::GeoInterface.Polygon, geom::geomtype)::"LineString" - +GeoInterface.area(::GeoInterface.Polygon, geom::customgeom)::Real +GeoInterface.centroid(::GeoInterface.Polygon, geom::customgeom)::"Point" +GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::customgeom)::"Point" +GeoInterface.boundary(::GeoInterface.Polygon, geom::customgeom)::"LineString" ``` -A `geom::geomtype` with "GeometryCollection"-like traits has to implement the following methods: + +A `geom::customgeom` with "GeometryCollection"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.GeometryCollection() -GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::geomtype)::Integer -GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::geomtypem, i)::"Geometry" +GeoInterface.geomtype(geom::customgeom) = GeoInterface.GeometryCollection() +GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::customgeom)::Integer +GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::customgeomm, i)::"Geometry" ``` -A `geom::geomtype` with "MultiPoint"-like traits has to implement the following methods: + +A `geom::customgeom` with "MultiPoint"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPoint() -GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::geomtype)::Integer +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPoint() +GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::geomtype, i)::"Point" +GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::customgeom, i)::"Point" ``` -A `geom::geomtype` with "MultiLineString"-like traits has to implement the following methods: + +A `geom::customgeom` with "MultiLineString"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiLineString() -GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::geomtype)::Integer +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiLineString() +GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::customgeom)::Integer # These alias for nlinestring and getlinestring -GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::geomtypem, i)::"LineString" +GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::customgeomm, i)::"LineString" ``` -A `geom::geomtype` with "MultiPolygon"-like traits has to implement the following methods: + +A `geom::customgeom` with "MultiPolygon"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::geomtype) = GeoInterface.MultiPolygon() -GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::geomtype)::Integer +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPolygon() +GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::customgeom)::Integer # These alias for npolygon and getpolygon -GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::geomtype)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::geomtype, i)::"Polygon" -``` - - -## Testing the interface -GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. - -```julia -GeoInterface.testgeometry(geom) +GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::customgeom, i)::"Polygon" ``` diff --git a/docs/src/guides/sink.md b/docs/src/guides/sink.md deleted file mode 100644 index bab3fa17..00000000 --- a/docs/src/guides/sink.md +++ /dev/null @@ -1,6 +0,0 @@ -# Usage as Sink -GeoInterface requires five functions to be defined for a given geom: - -## Using the interface -!!! warning - TODO diff --git a/docs/src/guides/source.md b/docs/src/guides/source.md deleted file mode 100644 index 0d3dc5c9..00000000 --- a/docs/src/guides/source.md +++ /dev/null @@ -1,6 +0,0 @@ -# Usage as Source -GeoInterface requires five functions to be defined for a given geom: - -## Using the interface -!!! warning - TODO diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index 3ed5bff1..da62e7ef 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -1,5 +1,5 @@ # Traits interface -GeoInterface provides a traits interface, not unlike Tables.jl, by +GeoInterface provides a traits interface, not unlike Tables.jl, by a set of functions and types. ## Functions (a) a set of functions: @@ -23,3 +23,23 @@ Point <: AbstractPoint <: AbstractGeometry MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: AbstractGeometry ... ``` + +## 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 + +# With GeoInterface +julia> isgeometry(geom) +True +julia> geomtype(geom) +GeoInterface.Polygon +julia> ext = exterior(geom); +julia> geomtype(ext) +GeoInterface.LineString +julia> getpoint.(Ref(ext), 1:npoint(ext)) +[[1.,2.],[2.,3.],[1.,2.]] +``` diff --git a/src/defaults.jl b/src/defaults.jl index 69fcb3f7..a262921a 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -1,23 +1,31 @@ # Defaults for many of the interface functions are defined here as fallback. +# Methods here should take a type 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) # always uppercase? -coordnames(geom) = default_coord_names[1:ncoord(geom)] +coordnames(::AbstractGeometry, geom) = default_coord_names[1:ncoord(geom)] # Maybe hardcode dimension order? At least for X and Y? -x(geom) = getcoord(geom, findfirst(coordnames(geom), :X)) -y(geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) -z(geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) -m(geom) = getcoord(geom, findfirst(coordnames(geom), :M)) +x(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) +y(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) +z(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) +m(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) -is3d(geom) = :Z in coordnames(geom) -ismeasured(geom) = :M in coordnames(geom) +is3d(::AbstractPoint, geom) = :Z in coordnames(geom) +ismeasured(::AbstractPoint, geom) = :M in coordnames(geom) ## Points +ngeom(::AbstractPoint, geom)::Integer = 0 +getgeom(::AbstractPoint, geom, i) = nothing + +## LineStrings npoint(c::AbstractCurve, geom) = ngeom(c, geom) getpoint(c::AbstractCurve, geom, i) = getgeom(c, geom, i) +startpoint(c::AbstractCurve, geom) = getpoint(c, geom, 1) +endpoint(c::AbstractCurve, geom) = getpoint(c, geom, length(geom)) ## Polygons nring(p::AbstractPolygon, geom) = ngeom(p, geom) @@ -26,12 +34,20 @@ getexterior(p::AbstractPolygon, geom) = getring(p, geom, 1) nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) -nlinestring(::AbstractMultiLineString, geom) = ngeom(p, geom) -getlinestring(::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) +## MultiLineString +nlinestring(p::AbstractMultiLineString, geom) = ngeom(p, geom) +getlinestring(p::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) + +## MultiPolygon +npolygon(p::AbstractMultiPolygon, geom) = ngeom(p, geom) +getpolygon(p::AbstractMultiPolygon, geom, i) = getgeom(p, geom, i) -npolygon(::AbstractMultiPolygon, geom) = ngeom(p, geom) -getpolygon(::AbstractMultiPolygon, geom, i) = getgeom(p, geom, i) +## Surface +npatch(p::AbstractPolyHedralSurface, geom)::Integer = ngeom(p, geom) +getpatch(p::AbstractPolyHedralSurface, geom, i::Integer) = getgeom(p, geom, i) + +## Npoints npoint(::Line, _) = 2 npoint(::Triangle, _) = 3 npoint(::Rectangle, _) = 4 @@ -48,3 +64,6 @@ issimple(::AbstractMultiCurve, geom) = all(i -> issimple(getgeom(geom, i)), 1:ng isclosed(::AbstractMultiCurve, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) issimple(::MultiPoint, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) + +crs(::AbstractGeometry, geom) = nothing +extent(::AbstractGeometry, geom) = nothing diff --git a/src/interface.jl b/src/interface.jl index 4bb38f42..0ff63fa5 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -26,6 +26,13 @@ Note that SF distinguishes between dimensions, spatial dimensions and topologica """ 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 @@ -40,55 +47,219 @@ Return `true` when the geometry is simple, i.e. doesn't cross or touch itself. """ issimple(geom) = issimple(geomtype(geom), geom) -# Point +""" + getcoord(geom, i) -> Number + +Return the `i`th coordinate for a given `geom`. +Note that this is only valid for individual [`AbstractPoint`]s. +""" getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) # Curve, LineString, MultiPoint +""" + npoint(geom) -> Int + +Return the number of points in given `geom`. +Note that this is only valid for [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@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 [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@ref)s. +""" getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) # Curve -startpoint(geom) = getpoint(geom, 1) +""" + startpoint(geom) -> Point + +Return the first point in the `geom`. +Note that this is only valid for [`AbstractCurve`](@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 [`AbstractCurve`](@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 [`AbstractCurve`](@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 [`AbstractCurve`](@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 [`AbstractCurve`](@ref)s. +""" length(geom) = length(geomtype(geom), geom) -endpoint(geom) = getcoord(geom, length(geom)) # Surface +""" + area(geom) -> Number + +Return the area of `geom` in its 2d coordinate system. +Note that this is only valid for [`AbstractSurface`](@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 [`AbstractSurface`](@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 [`AbstractSurface`](@ref)s. +""" pointonsurface(geom) = pointonsurface(geomtype(geom), geom) + +""" + boundary(geom) -> Curve + +Return the boundary of `geom`. +Note that this is only valid for [`AbstractSurface`](@ref)s. +""" boundary(geom) = boundary(geomtype(geom), geom) # Polygon/Triangle -nring(geom) = nring(geomtype(geom), geom) # TODO If this is more than one, it has interior rings (holes) +""" + nring(geom) -> Integer + +Return the number of rings in given `geom`. +Note that this is only valid for [`AbstractPolygon`](@ref)s. +""" +nring(geom) = nring(geomtype(geom), geom) + +""" + getring(geom, i::Integer) -> Int + +Return the `i`th ring for a given `geom`. +Note that this is only valid for [`AbstractPolygon`](@ref)s. +""" getring(geom) = getring(geomtype(geom), geom) -"""Returns the exterior ring of this Polygon as a `LineString`.""" -getexterior(geom) = getexterior(geomtype(geom), geom) # getring(geom, 1) -"""Returns the number of interior rings in this Polygon.""" -nhole(geom)::Integer = nhole(geomtype(geom), geom) # nrings - 1 -"""Returns the Nth interior ring for this Polygon as a `LineString`.""" -gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) # getring + 1 +""" + getexterior(geom) -> Curve + +Returns the exterior ring of a Polygon as a `AbstractCurve`. +Note that this is only valid for [`AbstractPolygon`](@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 [`AbstractPolygon`](@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 [`AbstractPolygon`](@ref)s. +""" +gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) # PolyHedralSurface -"""Returns the number of including polygons.""" +""" + npatch(geom) + +Returns the number of patches for the given `geom`. +Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. +""" npatch(geom)::Integer = npatch(geomtype(geom), geom) -"""Returns a polygon in this surface, the order is arbitrary.""" + +""" + getpatch(geom, i::Integer) -> AbstractPolygon + +Returns the `i`th patch for the given `geom`. +Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. +""" getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) -"""Returns the collection of polygons in this surface that bounds the given polygon “p” for any polygon “p” in the surface.""" -boundingpolygons(geom) = nothing + +""" + 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) # MultiLineString +""" + nlinestring(geom) -> Integer + +Returns the number of curves for the given `geom`. +Note that this is only valid for [`AbstractMultiLineString`](@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 [`AbstractMultiLineString`](@ref)s. +""" getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) # MultiPolygon +""" + npolygon(geom) -> Integer + +Returns the number of polygons for the given `geom`. +Note that this is only valid for [`AbstractMultiPolygon`](@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 [`AbstractMultiPolygon`](@ref)s. +""" getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) # Other methods @@ -98,22 +269,180 @@ getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) Retrieve Coordinate Reference System for given geom. In SF this is defined as `SRID`. """ -crs(geom) = nothing +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) # 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) -intersects(a, b)::Bool = intersects(geomtype(a), geomtype(b), a, b) -contains(a, b)::Bool = contains(geomtype(a), geomtype(b), a, b) -relate(a, b)::Bool = relate(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) = difference(geomtype(a), geomtype(b), a, b) -difference(a, b, inverse=false) = difference(geomtype(a), geomtype(b), a, b, inverse) -intersection(a, b, inverse=false) = intersection(geomtype(a), geomtype(b), a, b, inverse) +""" + 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 [`AbstractPoint`](@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 [`AbstractPoint`](@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 [`AbstractPoint`](@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 [`AbstractPoint`](@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) diff --git a/src/types.jl b/src/types.jl index dff61932..4720bd7b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,14 +1,19 @@ +"""An AbstractGeometry type for all geometries.""" abstract type AbstractGeometry end +"""An AbstractGeometryCollection type for all geometrycollections.""" abstract type AbstractGeometryCollection <: AbstractGeometry end """A [`GeometryCollection`](@ref) is a collection of `Geometry`s.""" struct GeometryCollection <: AbstractGeometryCollection end +"""A abstract [`Point`](@ref).""" abstract type AbstractPoint <: AbstractGeometry end """A simple [`Point`](@ref).""" struct Point <: AbstractPoint end +"""An AbstractCurve type for all curves.""" abstract type AbstractCurve <: AbstractGeometry end +"""An AbstractLineString type for all linestrings.""" abstract type AbstractLineString <: AbstractCurve end """A [`LineString`](@ref) is a collection of straight lines between its `Point`s.""" struct LineString <: AbstractLineString end @@ -24,16 +29,18 @@ struct CircularString <: AbstractCurve end """A [`CompoundCurve`](@ref) is a curve that combines straight [`LineString`](@ref)s and curved [`CircularString`](@ref)s.""" struct CompoundCurve <: AbstractCurve end +"""An AbstractSurface type for all surfaces.""" abstract type AbstractSurface <: AbstractGeometry end +"""An AbstractCurvePolygon type for all curved polygons.""" abstract type AbstractCurvePolygon <: AbstractSurface end """A [`Polygon`](@ref) that can contain either circular or straight curves as rings.""" struct CurvePolygon <: AbstractCurvePolygon end +"""An AbstractPolygon type for all polygons.""" abstract type AbstractPolygon <: AbstractCurvePolygon end """A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" struct Polygon <: AbstractPolygon end """A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" struct Triangle <: AbstractPolygon end - """A [`Polygon`](@ref) that is rectangular and could be described by the minimum and maximum vertices.""" struct Rectangle <: AbstractPolygon end """A [`Polygon`](@ref) with four vertices.""" @@ -43,24 +50,30 @@ struct Pentagon <: AbstractPolygon end """A [`Polygon`](@ref) with six vertices.""" struct Hexagon <: AbstractPolygon end +"""An AbstractPolyHedralSurface type for all polyhedralsurfaces.""" abstract type AbstractPolyHedralSurface <: AbstractSurface end """A [`PolyHedralSurface`](@ref) is a connected surface consisting of Polygons.""" struct PolyHedralSurface <: AbstractPolyHedralSurface end """A [`TIN`](@ref) is a [`PolyHedralSurface`](@ref) consisting of [`Triangle`](@ref)s.""" struct TIN <: AbstractPolyHedralSurface end # Surface consisting of Triangles +"""An AbstractMultiPoint type for all multipoints.""" abstract type AbstractMultiPoint <: AbstractGeometryCollection end """A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" struct MultiPoint <: AbstractMultiPoint end +"""An AbstractMultiCurve type for all multicurves.""" abstract type AbstractMultiCurve <: AbstractGeometryCollection end """A [`MultiCurve`](@ref) is a collection of [`CircularString`](@ref)s.""" struct MultiCurve <: AbstractMultiCurve end +"""An AbstractMultiLineString type for all multilinestrings.""" abstract type AbstractMultiLineString <: AbstractMultiCurve end """A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" struct MultiLineString <: AbstractMultiLineString end +"""An AbstractMultiSurface type for all multisurfaces.""" abstract type AbstractMultiSurface <: AbstractGeometryCollection end +"""An AbstractMultiPolygon type for all multipolygons.""" abstract type AbstractMultiPolygon <: AbstractMultiSurface end """A [`MultiPolygon`](@ref) is a collection of [`Polygon`](@ref)s.""" struct MultiPolygon <: AbstractMultiPolygon end From 4637713120bc546ce82a8338f6d169cd758212d5 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 8 Apr 2022 11:39:01 +0200 Subject: [PATCH 09/41] Update src/defaults.jl Co-authored-by: Rafael Schouten --- src/defaults.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaults.jl b/src/defaults.jl index a262921a..a89ac769 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -4,7 +4,7 @@ ## Coords # Four options in SF, xy, xyz, xym, xyzm -const default_coord_names = (:X, :Y, :Z, :M) # always uppercase? +const default_coord_names = (:X, :Y, :Z, :M) coordnames(::AbstractGeometry, geom) = default_coord_names[1:ncoord(geom)] From 45dd59ad2da5ef815e70ff4e8ce2befea2db0b7a Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 8 Apr 2022 11:39:14 +0200 Subject: [PATCH 10/41] Update docs/src/guides/developer.md Co-authored-by: Rafael Schouten --- docs/src/guides/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index b8c23d69..8cf3da93 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -23,7 +23,7 @@ You read more about the `geomtype` in the [Type hierarchy](@ref). There are also optional generic methods that could help with locating this geometry. ```julia -GeoInterface.crs(geomtype(geom), geom::customgeom)::GeoFormatTypes.CoordinateReferenceSystemFormat} +GeoInterface.crs(geomtype(geom), geom::customgeom)::GeoFormatTypes.GeoFormat} GeoInterface.extent(geomtype(geom), geom::customgeom)::Extents.Extent ``` From 2ae95813d020073afb85ee4a6b827fedfe40b6ab Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 10 Apr 2022 21:14:49 +0200 Subject: [PATCH 11/41] Changes based on ArchGDAL implementation. --- docs/src/guides/developer.md | 2 +- docs/src/tutorials/usage.md | 2 +- src/defaults.jl | 84 ++++++++++++++++++++---------------- src/interface.jl | 8 ++++ src/utils.jl | 10 +++-- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 8cf3da93..3e34de56 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -9,7 +9,7 @@ also implement those interfaces where applicable. ```julia GeoInterface.isgeometry(geom::customgeom)::Bool = true -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.X() +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.X() # <: AbstractGeometry GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for Points GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index da62e7ef..789e6d53 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -40,6 +40,6 @@ GeoInterface.Polygon julia> ext = exterior(geom); julia> geomtype(ext) GeoInterface.LineString -julia> getpoint.(Ref(ext), 1:npoint(ext)) +julia> getcoords.(getpoint.(Ref(ext), 1:npoint(ext))) [[1.,2.],[2.,3.],[1.,2.]] ``` diff --git a/src/defaults.jl b/src/defaults.jl index a89ac769..f41db1d2 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -6,64 +6,72 @@ # Four options in SF, xy, xyz, xym, xyzm const default_coord_names = (:X, :Y, :Z, :M) -coordnames(::AbstractGeometry, geom) = default_coord_names[1:ncoord(geom)] +coordnames(::Type{<:AbstractGeometry}, geom) = default_coord_names[1:ncoord(geom)] # Maybe hardcode dimension order? At least for X and Y? -x(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) -y(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) -z(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) -m(::AbstractPoint, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) +x(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) +y(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) +z(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) +m(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) -is3d(::AbstractPoint, geom) = :Z in coordnames(geom) -ismeasured(::AbstractPoint, geom) = :M in coordnames(geom) +is3d(::Type{<:AbstractPoint}, geom) = :Z in coordnames(geom) +ismeasured(::Type{<:AbstractPoint}, geom) = :M in coordnames(geom) ## Points -ngeom(::AbstractPoint, geom)::Integer = 0 -getgeom(::AbstractPoint, geom, i) = nothing +ngeom(::Type{<:AbstractPoint}, geom)::Integer = 0 +getgeom(::Type{<:AbstractPoint}, geom, i) = nothing ## LineStrings -npoint(c::AbstractCurve, geom) = ngeom(c, geom) -getpoint(c::AbstractCurve, geom, i) = getgeom(c, geom, i) -startpoint(c::AbstractCurve, geom) = getpoint(c, geom, 1) -endpoint(c::AbstractCurve, geom) = getpoint(c, geom, length(geom)) +npoint(c::Type{<:AbstractCurve}, geom) = ngeom(c, geom) +getpoint(c::Type{<:AbstractCurve}, geom, i) = getgeom(c, geom, i) +startpoint(c::Type{<:AbstractCurve}, geom) = getpoint(c, geom, 1) +endpoint(c::Type{<:AbstractCurve}, geom) = getpoint(c, geom, length(geom)) ## Polygons -nring(p::AbstractPolygon, geom) = ngeom(p, geom) -getring(p::AbstractPolygon, geom, i) = getgeom(p, geom, i) -getexterior(p::AbstractPolygon, geom) = getring(p, geom, 1) -nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 -gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) +nring(p::Type{<:AbstractPolygon}, geom) = ngeom(p, geom) +getring(p::Type{<:AbstractPolygon}, geom, i) = getgeom(p, geom, i) +getexterior(p::Type{<:AbstractPolygon}, geom) = getring(p, geom, 1) +nhole(p::Type{<:AbstractPolygon}, geom) = nring(p, geom) - 1 +gethole(p::Type{<:AbstractPolygon}, geom, i) = getring(p, geom, i + 1) ## MultiLineString -nlinestring(p::AbstractMultiLineString, geom) = ngeom(p, geom) -getlinestring(p::AbstractMultiLineString, geom, i) = getgeom(p, geom, i) +nlinestring(p::Type{<:AbstractMultiLineString}, geom) = ngeom(p, geom) +getlinestring(p::Type{<:AbstractMultiLineString}, geom, i) = getgeom(p, geom, i) ## MultiPolygon -npolygon(p::AbstractMultiPolygon, geom) = ngeom(p, geom) -getpolygon(p::AbstractMultiPolygon, geom, i) = getgeom(p, geom, i) +npolygon(p::Type{<:AbstractMultiPolygon}, geom) = ngeom(p, geom) +getpolygon(p::Type{<:AbstractMultiPolygon}, geom, i) = getgeom(p, geom, i) ## Surface -npatch(p::AbstractPolyHedralSurface, geom)::Integer = ngeom(p, geom) -getpatch(p::AbstractPolyHedralSurface, geom, i::Integer) = getgeom(p, geom, i) +npatch(p::Type{<:AbstractPolyHedralSurface}, geom)::Integer = ngeom(p, geom) +getpatch(p::Type{<:AbstractPolyHedralSurface}, geom, i::Integer) = getgeom(p, geom, i) ## Npoints -npoint(::Line, _) = 2 -npoint(::Triangle, _) = 3 -npoint(::Rectangle, _) = 4 -npoint(::Quad, _) = 4 -npoint(::Pentagon, _) = 5 -npoint(::Hexagon, _) = 6 - -issimple(::AbstractCurve, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) -isclosed(::AbstractCurve, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) +npoint(::Type{Line}, _) = 2 +npoint(::Type{Triangle}, _) = 3 +npoint(::Type{Rectangle}, _) = 4 +npoint(::Type{Quad}, _) = 4 +npoint(::Type{Pentagon}, _) = 5 +npoint(::Type{Hexagon}, _) = 6 + +issimple(::Type{<:AbstractCurve}, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) +isclosed(::Type{<:AbstractCurve}, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) isring(x::AbstractCurve, geom) = issimple(x, geom) && isclosed(x, geom) # TODO Only simple if it's also not intersecting itself, except for its endpoints -issimple(::AbstractMultiCurve, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) -isclosed(::AbstractMultiCurve, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) +issimple(::Type{<:AbstractMultiCurve}, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) +isclosed(::Type{<:AbstractMultiCurve}, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) -issimple(::MultiPoint, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) +issimple(::Type{MultiPoint}, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) -crs(::AbstractGeometry, geom) = nothing -extent(::AbstractGeometry, geom) = nothing +crs(::Type{<:AbstractGeometry}, geom) = nothing +extent(::Type{<:AbstractGeometry}, geom) = nothing + +# Backwards compatibility +function coordinates(::Type{Point}, geom) + collect(getcoord(geom, i) for i in 1:ncoord(geom)) +end +function coordinates(::Type{<:AbstractGeometry}, geom) + collect(coordinates(getgeom(geom, i)) for i in 1:ngeom(geom)) +end diff --git a/src/interface.jl b/src/interface.jl index 0ff63fa5..4487cdfd 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -446,3 +446,11 @@ is3d(geom) = is3d(geomtype(geom), geom) Return whether the given `geom` has a :M coordinate. """ ismeasured(geom) = ismeasured(geomtype(geom), geom) + +""" + coordinates(geom) -> Vector + +Return (an iterator of) point coordinates. +Ensure backwards compatibility with the older GeoInterface +""" +coordinates(geom) = coordinates(geomtype(geom), geom) diff --git a/src/utils.jl b/src/utils.jl index 315a0fdc..b9db8128 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -3,11 +3,13 @@ function testgeometry(geom) try @assert isgeometry(geom) type = geomtype(geom) - ncoord(geom) - ngeom(geom) - getgeom(geom, 1) - if type == Point() + + if type == Point getcoord(geom, 1) + ncoord(geom) + else + ngeom(geom) + getgeom(geom, 1) end catch e println("You're missing an implementation: $e") From b252cd1e0095be6fee8abb04a77174bfb7625c26 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 10 Apr 2022 21:28:13 +0200 Subject: [PATCH 12/41] Fixed tests. --- test/test_primitives.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 75f19083..aedb61fa 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -2,11 +2,11 @@ struct MyCurve end @testset "Developer" begin # Implement interface - GeoInterface.isgeometry(::Type{MyCurve}) = true - GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString() - GeoInterface.ncoord(::GeoInterface.LineString, geom::MyCurve) = 2 - GeoInterface.ngeom(::GeoInterface.LineString, geom::MyCurve) = 2 - GeoInterface.getgeom(::GeoInterface.LineString, geom::MyCurve, i) = [[1, 2], [2, 3]][i] + GeoInterface.isgeometry(::MyCurve) = true + GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString + GeoInterface.ncoord(::Type{GeoInterface.LineString}, geom::MyCurve) = 2 + GeoInterface.ngeom(::Type{GeoInterface.LineString}, geom::MyCurve) = 2 + GeoInterface.getgeom(::Type{GeoInterface.LineString}, geom::MyCurve, i) = [[1, 2], [2, 3]][i] # Test validity geom = MyCurve() From 33df32f6752c6fa56e8484ef0eaec2e1dcda9d76 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Fri, 15 Apr 2022 14:09:45 +0200 Subject: [PATCH 13/41] Reverted Type traits. Suffixed Trait to all types. --- docs/src/background/sf.md | 4 +- docs/src/guides/defaults.md | 34 ++++----- docs/src/guides/developer.md | 62 ++++++++-------- docs/src/tutorials/usage.md | 16 +++-- src/defaults.jl | 82 +++++++++++----------- src/interface.jl | 7 ++ src/types.jl | 132 +++++++++++++++++------------------ src/utils.jl | 2 +- test/test_primitives.jl | 8 +-- 9 files changed, 179 insertions(+), 168 deletions(-) diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md index 4cb84740..9c82a9da 100644 --- a/docs/src/background/sf.md +++ b/docs/src/background/sf.md @@ -3,6 +3,8 @@ Simple Features (SF) are OGC standards describing two dimensional geographic fea The standards describe a hierarchy of types (Part 1), an interface with SQL (Part II) and an SQL/MM extension with support for circular geometry. ## 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.` @@ -20,7 +22,7 @@ NumPatches -> npatch ``` We generalized [`ngeom`](@ref) and [`getgeom`](@ref) to apply to -all geometries, not just a [`AbstractGeometryCollection`](@ref)s. +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, so not to overlap with the Julia `ndims`. ```julia diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index f08b1fd4..c00b30c7 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -4,22 +4,22 @@ Of note here are the `ngeom` and `getgeom` for each geometry type, which transla | | ngeom | getgeom | |----------------------------|-------------|---------------| -| [`AbstractPoint`](@ref) | - | - | -| [`AbstractCurve`](@ref), [`MultiPoint`](@ref) | [`npoint`](@ref) | [`getpoint`](@ref) | -| [`AbstractPolygon`](@ref) | [`nring`](@ref) | [`getring`](@ref) | -| [`AbstractMultiLineString`](@ref) | [`nlinestring`](@ref) | [`getlinestring`](@ref) | -| [`AbstractMultiPolygon`](@ref) | [`npolygon`](@ref) | [`getpolygon`](@ref) | -| [`AbstractPolyhedralSurface`](@ref) | [`npatch`](@ref) | [`getpatch`](@ref) | -| [`AbstractGeometryCollection`](@ref) | [`ngeom`](@ref) | [`getgeom`](@ref) | +| [`AbstractPointTrait`](@ref) | - | - | +| [`AbstractCurveTrait`](@ref), [`MultiPointTrait`](@ref) | [`npoint`](@ref) | [`getpoint`](@ref) | +| [`AbstractPolygonTrait`](@ref) | [`nring`](@ref) | [`getring`](@ref) | +| [`AbstractMultiLineStringTrait`](@ref) | [`nlinestring`](@ref) | [`getlinestring`](@ref) | +| [`AbstractMultiPolygonTrait`](@ref) | [`npolygon`](@ref) | [`getpolygon`](@ref) | +| [`AbstractPolyhedralSurfaceTrait`](@ref) | [`npatch`](@ref) | [`getpatch`](@ref) | +| [`AbstractGeometryCollectionTrait`](@ref) | [`ngeom`](@ref) | [`getgeom`](@ref) | ## Polygons -Of note are `Polygon`s, which can have holes, for which we automatically add the following +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. ```julia -getexterior(p::AbstractPolygon, geom) = getring(p, geom, 1) -nhole(p::AbstractPolygon, geom) = nring(p, geom) - 1 -gethole(p::AbstractPolygon, geom, i) = getring(p, geom, i + 1) +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 Simarly for LineStrings, we have the following @@ -32,10 +32,10 @@ endpoint(geom) = getpoint(geom, length(geom)) In some cases, we know the return value of a function for a specific geometry (sub)type beforehand and have implemented them. ```julia -npoint(::Line, _) = 2 -npoint(::Triangle, _) = 3 -npoint(::Rectangle, _) = 4 -npoint(::Quad, _) = 4 -npoint(::Pentagon, _) = 5 -npoint(::Hexagon, _) = 6 +npoint(::LineTrait, _) = 2 +npoint(::TriangleTrait, _) = 3 +npoint(::RectangleTrait, _) = 4 +npoint(::QuadTrait, _) = 4 +npoint(::PentagonTrait, _) = 5 +npoint(::HexagonTrait, _) = 6 ``` diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 3e34de56..9627453f 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -9,9 +9,9 @@ also implement those interfaces where applicable. ```julia GeoInterface.isgeometry(geom::customgeom)::Bool = true -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.X() # <: AbstractGeometry +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.XTrait() # <: AbstractGeometryTrait GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer -GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for Points +GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for PointTraits GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer GeoInterface.getgeom(geomtype(geom), geom::customgeom, i) # geomtype -> GeoInterface.Y ``` @@ -76,78 +76,78 @@ GeoInterface.isgeometry(geom::customgeom)::Bool = true A `geom::customgeom` with "Point"-like traits implements ```julia GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Point() -GeoInterface.ncoord(::GeoInterface.Point, geom::customgeom)::Integer -GeoInterface.getcoord(::GeoInterface.Point, geom::customgeom, i)::Real +GeoInterface.ncoord(::GeoInterface.PointTrait, geom::customgeom)::Integer +GeoInterface.getcoord(::GeoInterface.PointTrait, geom::customgeom, i)::Real # Defaults -GeoInterface.ngeom(::GeoInterface.Point, geom)::Integer = 0 -GeoInterface.getgeom(::GeoInterface.Point, geom::customgeom, i) = nothing +GeoInterface.ngeom(::GeoInterface.PointTrait, geom)::Integer = 0 +GeoInterface.getgeom(::GeoInterface.PointTrait, geom::customgeom, i) = nothing ``` A `geom::customgeom` with "LineString"-like traits implements the following methods: ```julia GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.LineString() -GeoInterface.ncoord(::GeoInterface.LineString, geom::customgeom)::Integer +GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.LineString, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.LineString, geom::customgeom, i) # of geomtype Point +GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::customgeom, i) # of geomtype Point # Optional -GeoInterface.isclosed(::GeoInterface.LineString, geom::customgeom)::Bool -GeoInterface.issimple(::GeoInterface.LineString, geom::customgeom)::Bool -GeoInterface.length(::GeoInterface.LineString, geom::customgeom)::Real +GeoInterface.isclosed(::GeoInterface.LineStringTrait, geom::customgeom)::Bool +GeoInterface.issimple(::GeoInterface.LineStringTrait, geom::customgeom)::Bool +GeoInterface.length(::GeoInterface.LineStringTrait, geom::customgeom)::Real ``` A `geom::customgeom` with "Polygon"-like traits can implement the following methods: ```julia GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Polygon() -GeoInterface.ncoord(::GeoInterface.Polygon, geom::customgeom)::Integer +GeoInterface.ncoord(::GeoInterface.PolygonTrait, geom::customgeom)::Integer # These alias for nring and getring -GeoInterface.ngeom(::GeoInterface.Polygon, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.Polygon, geom::customgeom, i)::"LineString" +GeoInterface.ngeom(::GeoInterface.PolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.PolygonTrait, geom::customgeom, i)::"LineStringTrait" # Optional -GeoInterface.area(::GeoInterface.Polygon, geom::customgeom)::Real -GeoInterface.centroid(::GeoInterface.Polygon, geom::customgeom)::"Point" -GeoInterface.pointonsurface(::GeoInterface.Polygon, geom::customgeom)::"Point" -GeoInterface.boundary(::GeoInterface.Polygon, geom::customgeom)::"LineString" +GeoInterface.area(::GeoInterface.PolygonTrait, geom::customgeom)::Real +GeoInterface.centroid(::GeoInterface.PolygonTrait, geom::customgeom)::"PointTrait" +GeoInterface.pointonsurface(::GeoInterface.PolygonTrait, geom::customgeom)::"PointTrait" +GeoInterface.boundary(::GeoInterface.PolygonTrait, geom::customgeom)::"LineStringTrait" ``` A `geom::customgeom` with "GeometryCollection"-like traits has to implement the following methods: ```julia GeoInterface.geomtype(geom::customgeom) = GeoInterface.GeometryCollection() -GeoInterface.ncoord(::GeoInterface.GeometryCollection, geom::customgeom)::Integer -GeoInterface.ngeom(::GeoInterface.GeometryCollection, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.GeometryCollection,geom::customgeomm, i)::"Geometry" +GeoInterface.ncoord(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer +GeoInterface.ngeom(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.GeometryCollectionTrait,geom::customgeomm, i)::"GeometryTrait" ``` A `geom::customgeom` with "MultiPoint"-like traits has to implement the following methods: ```julia GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPoint() -GeoInterface.ncoord(::GeoInterface.MultiPoint, geom::customgeom)::Integer +GeoInterface.ncoord(::GeoInterface.MultiPointTrait, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.MultiPoint, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPoint, geom::customgeom, i)::"Point" +GeoInterface.ngeom(::GeoInterface.MultiPointTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPointTrait, geom::customgeom, i)::"PointTrait" ``` A `geom::customgeom` with "MultiLineString"-like traits has to implement the following methods: ```julia GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiLineString() -GeoInterface.ncoord(::GeoInterface.MultiLineString, geom::customgeom)::Integer +GeoInterface.ncoord(::GeoInterface.MultiLineStringTrait, geom::customgeom)::Integer # These alias for nlinestring and getlinestring -GeoInterface.ngeom(::GeoInterface.MultiLineString, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiLineString,geom::customgeomm, i)::"LineString" +GeoInterface.ngeom(::GeoInterface.MultiLineStringTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiLineStringTrait,geom::customgeomm, i)::"LineStringTrait" ``` A `geom::customgeom` with "MultiPolygon"-like traits has to implement the following methods: ```julia GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPolygon() -GeoInterface.ncoord(::GeoInterface.MultiPolygon, geom::customgeom)::Integer +GeoInterface.ncoord(::GeoInterface.MultiPolygonTrait, geom::customgeom)::Integer # These alias for npolygon and getpolygon -GeoInterface.ngeom(::GeoInterface.MultiPolygon, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPolygon, geom::customgeom, i)::"Polygon" +GeoInterface.ngeom(::GeoInterface.MultiPolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::GeoInterface.MultiPolygonTrait, geom::customgeom, i)::"PolygonTrait" ``` diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index 789e6d53..0412a462 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -13,14 +13,14 @@ getgeom(geom::geomtype, i) ``` ## Types -(b) a set of types for dispatching on said functions. +(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. +The types tell GeoInterface how to interpret the input object inside a GeoInterface function and are specific for each type of Geometry. ```julia -abstract Geometry -Point <: AbstractPoint <: AbstractGeometry -MultiPoint <: AbstractMultiPointGeometry <:AbstractGeometryCollection <: AbstractGeometry +abstract GeometryTrait +PointTrait <: AbstractPointTrait <: AbstractGeometryTrait +MultiPointTrait <: AbstractMultiPointGeometryTrait <:AbstractGeometryCollectionTrait <: AbstractGeometryTrait ... ``` @@ -36,10 +36,12 @@ julia> geom = createpolygon(...)::ArchGDAL.IGeometry # no idea about the interf julia> isgeometry(geom) True julia> geomtype(geom) -GeoInterface.Polygon +GeoInterface.PolygonTrait() julia> ext = exterior(geom); julia> geomtype(ext) -GeoInterface.LineString +GeoInterface.LineStringTrait() julia> getcoords.(getpoint.(Ref(ext), 1:npoint(ext))) [[1.,2.],[2.,3.],[1.,2.]] +julia> coords(geom) # fallback based on ngeom & npoint above + ``` diff --git a/src/defaults.jl b/src/defaults.jl index f41db1d2..05340de7 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -4,74 +4,74 @@ ## Coords # Four options in SF, xy, xyz, xym, xyzm -const default_coord_names = (:X, :Y, :Z, :M) +const default_coord_names = (:X, :Y, :Z, :M) -coordnames(::Type{<:AbstractGeometry}, geom) = default_coord_names[1:ncoord(geom)] +coordnames(::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(geom)] # Maybe hardcode dimension order? At least for X and Y? -x(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) -y(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) -z(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) -m(::Type{<:AbstractPoint}, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) +x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) +y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) +z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) +m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) -is3d(::Type{<:AbstractPoint}, geom) = :Z in coordnames(geom) -ismeasured(::Type{<:AbstractPoint}, geom) = :M in coordnames(geom) +is3d(::AbstractPointTrait, geom) = :Z in coordnames(geom) +ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) ## Points -ngeom(::Type{<:AbstractPoint}, geom)::Integer = 0 -getgeom(::Type{<:AbstractPoint}, geom, i) = nothing +ngeom(::AbstractPointTrait, geom)::Integer = 0 +getgeom(::AbstractPointTrait, geom, i) = nothing ## LineStrings -npoint(c::Type{<:AbstractCurve}, geom) = ngeom(c, geom) -getpoint(c::Type{<:AbstractCurve}, geom, i) = getgeom(c, geom, i) -startpoint(c::Type{<:AbstractCurve}, geom) = getpoint(c, geom, 1) -endpoint(c::Type{<:AbstractCurve}, geom) = getpoint(c, geom, length(geom)) +npoint(c::AbstractCurveTrait, geom) = ngeom(c, geom) +getpoint(c::AbstractCurveTrait, geom, i) = getgeom(c, geom, i) +startpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, 1) +endpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, length(geom)) ## Polygons -nring(p::Type{<:AbstractPolygon}, geom) = ngeom(p, geom) -getring(p::Type{<:AbstractPolygon}, geom, i) = getgeom(p, geom, i) -getexterior(p::Type{<:AbstractPolygon}, geom) = getring(p, geom, 1) -nhole(p::Type{<:AbstractPolygon}, geom) = nring(p, geom) - 1 -gethole(p::Type{<:AbstractPolygon}, geom, i) = getring(p, geom, i + 1) +nring(p::AbstractPolygonTrait, geom) = ngeom(p, geom) +getring(p::AbstractPolygonTrait, geom, i) = getgeom(p, geom, i) +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) ## MultiLineString -nlinestring(p::Type{<:AbstractMultiLineString}, geom) = ngeom(p, geom) -getlinestring(p::Type{<:AbstractMultiLineString}, geom, i) = getgeom(p, geom, i) +nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) ## MultiPolygon -npolygon(p::Type{<:AbstractMultiPolygon}, geom) = ngeom(p, geom) -getpolygon(p::Type{<:AbstractMultiPolygon}, geom, i) = getgeom(p, geom, i) +npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) +getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) ## Surface -npatch(p::Type{<:AbstractPolyHedralSurface}, geom)::Integer = ngeom(p, geom) -getpatch(p::Type{<:AbstractPolyHedralSurface}, geom, i::Integer) = getgeom(p, geom, i) +npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) +getpatch(p::AbstractPolyHedralSurfaceTrait, geom, i::Integer) = getgeom(p, geom, i) ## Npoints -npoint(::Type{Line}, _) = 2 -npoint(::Type{Triangle}, _) = 3 -npoint(::Type{Rectangle}, _) = 4 -npoint(::Type{Quad}, _) = 4 -npoint(::Type{Pentagon}, _) = 5 -npoint(::Type{Hexagon}, _) = 6 +npoint(::LineTrait, _) = 2 +npoint(::TriangleTrait, _) = 3 +npoint(::RectangleTrait, _) = 4 +npoint(::QuadTrait, _) = 4 +npoint(::PentagonTrait, _) = 5 +npoint(::HexagonTrait, _) = 6 -issimple(::Type{<:AbstractCurve}, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) -isclosed(::Type{<:AbstractCurve}, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) -isring(x::AbstractCurve, geom) = issimple(x, geom) && isclosed(x, geom) +issimple(::AbstractCurveTrait, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) +isclosed(::AbstractCurveTrait, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) +isring(x::AbstractCurveTrait, geom) = issimple(x, geom) && isclosed(x, geom) # TODO Only simple if it's also not intersecting itself, except for its endpoints -issimple(::Type{<:AbstractMultiCurve}, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) -isclosed(::Type{<:AbstractMultiCurve}, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) +issimple(::AbstractMultiCurveTrait, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) +isclosed(::AbstractMultiCurveTrait, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) -issimple(::Type{MultiPoint}, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) +issimple(::MultiPointTrait, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) -crs(::Type{<:AbstractGeometry}, geom) = nothing -extent(::Type{<:AbstractGeometry}, geom) = nothing +crs(::AbstractGeometryTrait, geom) = nothing +extent(::AbstractGeometryTrait, geom) = nothing # Backwards compatibility -function coordinates(::Type{Point}, geom) +function coordinates(::AbstractPointTrait, geom) collect(getcoord(geom, i) for i in 1:ncoord(geom)) end -function coordinates(::Type{<:AbstractGeometry}, geom) +function coordinates(::AbstractGeometryTrait, geom) collect(coordinates(getgeom(geom, i)) for i in 1:ngeom(geom)) end diff --git a/src/interface.jl b/src/interface.jl index 4487cdfd..51d52eee 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -279,6 +279,13 @@ 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. +""" +bbox(geom) = extent(geom) # DE-9IM, see https://en.wikipedia.org/wiki/DE-9IM """ diff --git a/src/types.jl b/src/types.jl index 4720bd7b..e9888ced 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,79 +1,79 @@ -"""An AbstractGeometry type for all geometries.""" -abstract type AbstractGeometry end +"""An AbstractGeometryTrait type for all geometries.""" +abstract type AbstractGeometryTrait end -"""An AbstractGeometryCollection type for all geometrycollections.""" -abstract type AbstractGeometryCollection <: AbstractGeometry end +"""An AbstractGeometryCollectionTrait type for all geometrycollections.""" +abstract type AbstractGeometryCollectionTrait <: AbstractGeometryTrait end """A [`GeometryCollection`](@ref) is a collection of `Geometry`s.""" -struct GeometryCollection <: AbstractGeometryCollection end +struct GeometryCollectionTrait <: AbstractGeometryCollectionTrait end -"""A abstract [`Point`](@ref).""" -abstract type AbstractPoint <: AbstractGeometry end -"""A simple [`Point`](@ref).""" -struct Point <: AbstractPoint end +"""A abstract [`PointTrait`](@ref).""" +abstract type AbstractPointTrait <: AbstractGeometryTrait end +"""A simple [`PointTrait`](@ref).""" +struct PointTrait <: AbstractPointTrait end -"""An AbstractCurve type for all curves.""" -abstract type AbstractCurve <: AbstractGeometry end +"""An AbstractCurveTrait type for all curves.""" +abstract type AbstractCurveTrait <: AbstractGeometryTrait end """An AbstractLineString type for all linestrings.""" -abstract type AbstractLineString <: AbstractCurve end -"""A [`LineString`](@ref) is a collection of straight lines between its `Point`s.""" -struct LineString <: AbstractLineString end -"""A Line is [`LineString`](@ref) with just two points.""" -struct Line <: AbstractLineString end -"""A LinearRing is a [`LineString`](@ref) with the same begin and endpoint.""" -struct LinearRing <: AbstractLineString end +abstract type AbstractLineStringTrait <: AbstractCurveTrait end +"""A [`LineStringTrait`](@ref) 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 [`CircularString`](@ref) is a curve, with an odd number of points. +"""A [`CircularStringTrait`](@ref) 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 CircularString <: AbstractCurve end -"""A [`CompoundCurve`](@ref) is a curve that combines straight [`LineString`](@ref)s and curved [`CircularString`](@ref)s.""" -struct CompoundCurve <: AbstractCurve end +struct CircularStringTrait <: AbstractCurveTrait end +"""A [`CompoundCurveTrait`](@ref) is a curve that combines straight [`LineStringTrait`](@ref)s and curved [`CircularStringTrait`](@ref)s.""" +struct CompoundCurveTrait <: AbstractCurveTrait end -"""An AbstractSurface type for all surfaces.""" -abstract type AbstractSurface <: AbstractGeometry end -"""An AbstractCurvePolygon type for all curved polygons.""" -abstract type AbstractCurvePolygon <: AbstractSurface end -"""A [`Polygon`](@ref) that can contain either circular or straight curves as rings.""" -struct CurvePolygon <: AbstractCurvePolygon end -"""An AbstractPolygon type for all polygons.""" -abstract type AbstractPolygon <: AbstractCurvePolygon end -"""A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" -struct Polygon <: AbstractPolygon end -"""A [`Polygon`](@ref) with straight rings either as exterior or interior(s).""" -struct Triangle <: AbstractPolygon end -"""A [`Polygon`](@ref) that is rectangular and could be described by the minimum and maximum vertices.""" -struct Rectangle <: AbstractPolygon end -"""A [`Polygon`](@ref) with four vertices.""" -struct Quad <: AbstractPolygon end -"""A [`Polygon`](@ref) with five vertices.""" -struct Pentagon <: AbstractPolygon end -"""A [`Polygon`](@ref) with six vertices.""" -struct Hexagon <: AbstractPolygon end +"""An AbstractSurfaceTrait type for all surfaces.""" +abstract type AbstractSurfaceTrait <: AbstractGeometryTrait end +"""An AbstractCurvePolygonTrait type for all curved polygons.""" +abstract type AbstractCurvePolygonTrait <: AbstractSurfaceTrait end +"""A [`PolygonTrait`](@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 +"""A [`PolygonTrait`](@ref) with straight rings either as exterior or interior(s).""" +struct PolygonTrait <: AbstractPolygonTrait end +"""A [`PolygonTrait`](@ref) with straight rings either as exterior or interior(s).""" +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 AbstractPolyHedralSurface type for all polyhedralsurfaces.""" -abstract type AbstractPolyHedralSurface <: AbstractSurface end -"""A [`PolyHedralSurface`](@ref) is a connected surface consisting of Polygons.""" -struct PolyHedralSurface <: AbstractPolyHedralSurface end -"""A [`TIN`](@ref) is a [`PolyHedralSurface`](@ref) consisting of [`Triangle`](@ref)s.""" -struct TIN <: AbstractPolyHedralSurface end # Surface consisting of Triangles +"""An AbstractPolyHedralSurfaceTrait type for all polyhedralsurfaces.""" +abstract type AbstractPolyHedralSurfaceTrait <: AbstractSurfaceTrait end +"""A [`PolyHedralSurfaceTrait`](@ref) is a connected surface consisting of PolygonsTraits.""" +struct PolyHedralSurfaceTrait <: AbstractPolyHedralSurfaceTrait end +"""A [`TINTrait`](@ref) is a [`PolyHedralSurfaceTrait`](@ref) consisting of [`TriangleTrait`](@ref)s.""" +struct TINTrait <: AbstractPolyHedralSurfaceTrait end # Surface consisting of Triangles -"""An AbstractMultiPoint type for all multipoints.""" -abstract type AbstractMultiPoint <: AbstractGeometryCollection end -"""A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" -struct MultiPoint <: AbstractMultiPoint end +"""An AbstractMultiPointTrait type for all multipoints.""" +abstract type AbstractMultiPointTrait <: AbstractGeometryCollectionTrait end +"""A [`MultiPointTrait`](@ref) is a collection of [`PointTrait`](@ref)s.""" +struct MultiPointTrait <: AbstractMultiPointTrait end -"""An AbstractMultiCurve type for all multicurves.""" -abstract type AbstractMultiCurve <: AbstractGeometryCollection end -"""A [`MultiCurve`](@ref) is a collection of [`CircularString`](@ref)s.""" -struct MultiCurve <: AbstractMultiCurve end -"""An AbstractMultiLineString type for all multilinestrings.""" -abstract type AbstractMultiLineString <: AbstractMultiCurve end -"""A [`MultiPoint`](@ref) is a collection of [`Point`](@ref)s.""" -struct MultiLineString <: AbstractMultiLineString end +"""An AbstractMultiCurveTrait type for all multicurves.""" +abstract type AbstractMultiCurveTrait <: AbstractGeometryCollectionTrait end +"""A [`MultiCurveTrait`](@ref) is a collection of [`CircularStringTrait`](@ref)s.""" +struct MultiCurveTrait <: AbstractMultiCurveTrait end +"""An AbstractMultiLineStringTrait type for all multilinestrings.""" +abstract type AbstractMultiLineStringTrait <: AbstractMultiCurveTrait end +"""A [`MultiLineStringTrait`](@ref) is a collection of [`LineStringTrait`](@ref)s.""" +struct MultiLineStringTrait <: AbstractMultiLineStringTrait end -"""An AbstractMultiSurface type for all multisurfaces.""" -abstract type AbstractMultiSurface <: AbstractGeometryCollection end -"""An AbstractMultiPolygon type for all multipolygons.""" -abstract type AbstractMultiPolygon <: AbstractMultiSurface end -"""A [`MultiPolygon`](@ref) is a collection of [`Polygon`](@ref)s.""" -struct MultiPolygon <: AbstractMultiPolygon end +"""An AbstractMultiSurfaceTrait type for all multisurfaces.""" +abstract type AbstractMultiSurfaceTrait <: AbstractGeometryCollectionTrait end +"""An AbstractMultiPolygonTrait type for all multipolygons.""" +abstract type AbstractMultiPolygonTrait <: AbstractMultiSurfaceTrait end +"""A [`MultiPolygonTrait`](@ref) is a collection of [`PolygonTrait`](@ref)s.""" +struct MultiPolygonTrait <: AbstractMultiPolygonTrait end diff --git a/src/utils.jl b/src/utils.jl index b9db8128..9a96eb4c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -4,7 +4,7 @@ function testgeometry(geom) @assert isgeometry(geom) type = geomtype(geom) - if type == Point + if type == PointTrait() getcoord(geom, 1) ncoord(geom) else diff --git a/test/test_primitives.jl b/test/test_primitives.jl index aedb61fa..599d1304 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -3,10 +3,10 @@ struct MyCurve end @testset "Developer" begin # Implement interface GeoInterface.isgeometry(::MyCurve) = true - GeoInterface.geomtype(::MyCurve) = GeoInterface.LineString - GeoInterface.ncoord(::Type{GeoInterface.LineString}, geom::MyCurve) = 2 - GeoInterface.ngeom(::Type{GeoInterface.LineString}, geom::MyCurve) = 2 - GeoInterface.getgeom(::Type{GeoInterface.LineString}, geom::MyCurve, i) = [[1, 2], [2, 3]][i] + GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() + GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 + GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 + GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = [[1, 2], [2, 3]][i] # Test validity geom = MyCurve() From 49a4d11c1c239467bba75db7d3014e1c2b7f1afc Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sat, 23 Apr 2022 11:59:20 +0200 Subject: [PATCH 14/41] Add Julia Computing as contributor. Fix `isempty` and add `convert` method. --- LICENSE.md | 2 +- README.md | 2 ++ src/defaults.jl | 1 + src/interface.jl | 10 +++++++++- test/test_primitives.jl | 3 +++ 5 files changed, 16 insertions(+), 2 deletions(-) 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/README.md b/README.md index 7c059075..980bcab9 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,5 @@ Using these traits, it should be easy to parse, serialize and use different geom 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 [INTEGRATIONS.md](INTEGRATIONS.md). + +We thank Julia Computing for supporting contributions to this package. diff --git a/src/defaults.jl b/src/defaults.jl index 05340de7..6c5f8c88 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -16,6 +16,7 @@ m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) is3d(::AbstractPointTrait, geom) = :Z in coordnames(geom) ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) +isempty(T, geom) = false ## Points ngeom(::AbstractPointTrait, geom)::Integer = 0 diff --git a/src/interface.jl b/src/interface.jl index 51d52eee..b6e09d3d 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -38,7 +38,7 @@ coordnames(geom) = coordnames(geomtype(geom), geom) Return `true` when the geometry is empty. """ -isempty(geom) = ngeom(geom) == 0 +isempty(geom) = isempty(geomtype(geom), geom) """ issimple(geom) -> Bool @@ -461,3 +461,11 @@ Return (an iterator of) point coordinates. Ensure backwards compatibility with the older GeoInterface """ 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) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 599d1304..dbc3535d 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -7,11 +7,14 @@ struct MyCurve end GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = [[1, 2], [2, 3]][i] + GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom # Test validity geom = MyCurve() @test testgeometry(geom) + @test !isnothing(GeoInterface.convert(MyCurve, geom)) + # Check functions @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @test_throws MethodError GeoInterface.area(geom) From 7af21e40cef63adf3df15ff900a423f975ba90fa Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 25 Apr 2022 10:17:09 +0200 Subject: [PATCH 15/41] Add `asbinary` and `astext` (wkb and wkt) methods. --- src/interface.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interface.jl b/src/interface.jl index b6e09d3d..d4d8651e 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -469,3 +469,17 @@ 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) From 1ba01f5c6f824ae1fe7509f17c9fd0016012a313 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Tue, 3 May 2022 12:04:38 +0200 Subject: [PATCH 16/41] Added iterators. Added feature interface. Updated documentation based on review. --- docs/src/background/sf.md | 20 ++++++----- docs/src/guides/defaults.md | 16 +++++---- docs/src/guides/developer.md | 9 ++++- docs/src/tutorials/usage.md | 7 ++-- src/defaults.jl | 22 ++++++++++-- src/interface.jl | 65 +++++++++++++++++++++++++++++++++++- src/types.jl | 30 ++++++++--------- src/utils.jl | 32 +++++++++++++++--- test/test_primitives.jl | 10 ++++-- 9 files changed, 165 insertions(+), 46 deletions(-) diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md index 9c82a9da..0e35a577 100644 --- a/docs/src/background/sf.md +++ b/docs/src/background/sf.md @@ -1,6 +1,6 @@ # Simple Features -Simple Features (SF) 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), an interface with SQL (Part II) and an SQL/MM extension with support for circular geometry. +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. @@ -12,7 +12,7 @@ All types used here come from the SF. We added `Trait` to all geometry types her 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 `Xn` becomes `getX`: +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 @@ -24,15 +24,18 @@ NumPatches -> npatch 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, so not to overlap with the Julia `ndims`. +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 some functions: +We've generalized the naming of some functions: ```julia SRID -> crs -envelope -> extent +envelope -> extent # also aliased to bbox ``` And added a helper method to clarify the naming of coordinates. @@ -44,10 +47,9 @@ coordnames = (:X, :Y, :Z, :M) 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 +dimension # topological dimensions spatialDimension -asText -asBinary + locateAlong locateBetween diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index c00b30c7..3642eab6 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -5,16 +5,18 @@ Of note here are the `ngeom` and `getgeom` for each geometry type, which transla | | ngeom | getgeom | |----------------------------|-------------|---------------| | [`AbstractPointTrait`](@ref) | - | - | -| [`AbstractCurveTrait`](@ref), [`MultiPointTrait`](@ref) | [`npoint`](@ref) | [`getpoint`](@ref) | -| [`AbstractPolygonTrait`](@ref) | [`nring`](@ref) | [`getring`](@ref) | -| [`AbstractMultiLineStringTrait`](@ref) | [`nlinestring`](@ref) | [`getlinestring`](@ref) | -| [`AbstractMultiPolygonTrait`](@ref) | [`npolygon`](@ref) | [`getpolygon`](@ref) | -| [`AbstractPolyhedralSurfaceTrait`](@ref) | [`npatch`](@ref) | [`getpatch`](@ref) | -| [`AbstractGeometryCollectionTrait`](@ref) | [`ngeom`](@ref) | [`getgeom`](@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. +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) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 9627453f..772f2058 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -15,7 +15,7 @@ GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for Poi GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer GeoInterface.getgeom(geomtype(geom), geom::customgeom, i) # geomtype -> GeoInterface.Y ``` -Where the `getgeom` could be an iterator (without the i) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. +Where the `getgeom` and `getcoord` could be an iterator (without the `i`) as well. It will return a new geom with the correct `geomtype`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. You read more about the `geomtype` in the [Type hierarchy](@ref). @@ -29,6 +29,13 @@ 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). +## 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) diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index 0412a462..a9939d36 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -1,5 +1,5 @@ # Traits interface -GeoInterface provides a traits interface, not unlike Tables.jl, by a set of functions and types. +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: @@ -7,8 +7,9 @@ GeoInterface provides a traits interface, not unlike Tables.jl, by a set of func isgeometry(geom) geomtype(geom) ncoord(geom) +getcoord(geom, i) ngeom(geom) -getgeom(geom::geomtype, i) +getgeom(geom, i) ... ``` @@ -32,7 +33,7 @@ to work with their custom geometries, you can just call the above generic functi julia> using ArchGDAL julia> geom = createpolygon(...)::ArchGDAL.IGeometry # no idea about the interface -# With GeoInterface +# Inspect with GeoInterface methods julia> isgeometry(geom) True julia> geomtype(geom) diff --git a/src/defaults.jl b/src/defaults.jl index 6c5f8c88..cee13115 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -20,16 +20,19 @@ isempty(T, geom) = false ## Points ngeom(::AbstractPointTrait, geom)::Integer = 0 +getgeom(::AbstractPointTrait, geom) = nothing getgeom(::AbstractPointTrait, geom, i) = nothing ## LineStrings npoint(c::AbstractCurveTrait, geom) = ngeom(c, geom) +getpoint(c::AbstractCurveTrait, geom) = getgeom(c, geom) getpoint(c::AbstractCurveTrait, geom, i) = getgeom(c, geom, i) startpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, 1) endpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, length(geom)) ## Polygons nring(p::AbstractPolygonTrait, geom) = ngeom(p, geom) +getring(p::AbstractPolygonTrait, geom) = getgeom(p, geom) getring(p::AbstractPolygonTrait, geom, i) = getgeom(p, geom, i) getexterior(p::AbstractPolygonTrait, geom) = getring(p, geom, 1) nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1 @@ -37,16 +40,22 @@ gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) ## MultiLineString nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) ## MultiPolygon npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) +getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) ## Surface npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) +getpatch(p::AbstractPolyHedralSurfaceTrait, geom) = getgeom(p, geom) getpatch(p::AbstractPolyHedralSurfaceTrait, geom, i::Integer) = getgeom(p, geom, i) +## Default iterator +getgeom(p::AbstractGeometryTrait, geom) = (getgeom(p, geom, i) for i in 1:ngeom(p, geom)) +getcoord(p::AbstractPointTrait, geom) = (getcoord(p, geom, i) for i in 1:ncoord(p, geom)) ## Npoints npoint(::LineTrait, _) = 2 @@ -71,8 +80,17 @@ extent(::AbstractGeometryTrait, geom) = nothing # Backwards compatibility function coordinates(::AbstractPointTrait, geom) - collect(getcoord(geom, i) for i in 1:ncoord(geom)) + collect(getcoord(geom)) end function coordinates(::AbstractGeometryTrait, geom) - collect(coordinates(getgeom(geom, i)) for i in 1:ngeom(geom)) + collect(coordinates(getgeom(geom))) end + +# Subtraits +subtrait(::PointTrait) = nothing +subtrait(::LineStringTrait) = PointTrait +subtrait(::PolygonTrait) = LineStringTrait +subtrait(::MultiPointTrait) = PointTrait +subtrait(::MultiLineStringTrait) = LineStringTrait +subtrait(::MultiPolygonTrait) = PolygonTrait +subtrait(::GeometryCollectionTrait) = nothing diff --git a/src/interface.jl b/src/interface.jl index d4d8651e..e0016dfd 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -10,6 +10,35 @@ 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 implicitely 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 the older GeoInterface. +""" +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 the older GeoInterface. +""" +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 the older GeoInterface. +""" +properties(feat) = nothing + """ GeoInterface.geomtype(geom) => T <: AbstractGeometry @@ -54,6 +83,10 @@ Return the `i`th coordinate for a given `geom`. Note that this is only valid for individual [`AbstractPoint`]s. """ getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) +""" + getcoord(geom) -> Iterator +""" +getcoord(geom) = getcoord(geomtype(geom), geom) # Curve, LineString, MultiPoint """ @@ -71,6 +104,10 @@ Return the `i`th Point in given `geom`. Note that this is only valid for [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@ref)s. """ getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) +""" + getpoint(geom) -> Iterator +""" +getpoint(geom) = getpoint(geomtype(geom), geom) # Curve """ @@ -164,6 +201,10 @@ nring(geom) = nring(geomtype(geom), geom) Return the `i`th ring for a given `geom`. Note that this is only valid for [`AbstractPolygon`](@ref)s. """ +getring(geom, i::Integer) = getring(geomtype(geom), geom, i) +""" + getring(geom) -> Iterator +""" getring(geom) = getring(geomtype(geom), geom) """ @@ -189,6 +230,10 @@ Returns the `i`th interior ring for this given `geom`. Note that this is only valid for [`AbstractPolygon`](@ref)s. """ gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) +""" + gethole(geom) -> Iterator +""" +gethole(geom) = gethole(geomtype(geom), geom) # PolyHedralSurface """ @@ -206,6 +251,10 @@ Returns the `i`th patch for the given `geom`. Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. """ getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) +""" + getpatch(geom) -> Iterator +""" +getpatch(geom) = getpatch(geomtype(geom), geom) """ boundingpolygons(geom, i) -> AbstractMultiPolygon @@ -228,6 +277,10 @@ ngeom(geom) = ngeom(geomtype(geom), geom) Returns the `i`th geometry for the given `geom`. """ getgeom(geom, i::Integer) = getgeom(geomtype(geom), geom, i) +""" + getgeom(geom) -> Iterator +""" +getgeom(geom) = getgeom(geomtype(geom), geom) # MultiLineString """ @@ -245,6 +298,10 @@ Returns the `i`th linestring for the given `geom`. Note that this is only valid for [`AbstractMultiLineString`](@ref)s. """ getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) +""" + getlinestring(geom) -> Iterator +""" +getlinestring(geom) = getlinestring(geomtype(geom), geom) # MultiPolygon """ @@ -254,6 +311,7 @@ Returns the number of polygons for the given `geom`. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ npolygon(geom) = npolygon(geomtype(geom), geom) + """ getpolygon(geom, i::Integer) -> AbstractCurve @@ -261,6 +319,10 @@ Returns the `i`th polygon for the given `geom`. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) +""" + getpolygon(geom) -> Iterator +""" +getpolygon(geom) = getpolygon(geomtype(geom), geom) # Other methods """ @@ -284,6 +346,7 @@ extent(geom) = extent(geomtype(geom), geom) Alias for [`extent`](@ref), for compatibility with GeoJSON and the Python geointerface. +Ensures backwards compatibility with the older GeoInterface. """ bbox(geom) = extent(geom) @@ -458,7 +521,7 @@ ismeasured(geom) = ismeasured(geomtype(geom), geom) coordinates(geom) -> Vector Return (an iterator of) point coordinates. -Ensure backwards compatibility with the older GeoInterface +Ensures backwards compatibility with the older GeoInterface. """ coordinates(geom) = coordinates(geomtype(geom), geom) diff --git a/src/types.jl b/src/types.jl index e9888ced..ea958388 100644 --- a/src/types.jl +++ b/src/types.jl @@ -3,43 +3,43 @@ abstract type AbstractGeometryTrait end """An AbstractGeometryCollectionTrait type for all geometrycollections.""" abstract type AbstractGeometryCollectionTrait <: AbstractGeometryTrait end -"""A [`GeometryCollection`](@ref) is a collection of `Geometry`s.""" +"""A GeometryCollection is a collection of `Geometry`s.""" struct GeometryCollectionTrait <: AbstractGeometryCollectionTrait end -"""A abstract [`PointTrait`](@ref).""" +"""An AbstractPointTrait for all points.""" abstract type AbstractPointTrait <: AbstractGeometryTrait end -"""A simple [`PointTrait`](@ref).""" +"""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`](@ref) is a collection of straight lines between its `PointTrait`s.""" +"""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`](@ref) is a curve, with an odd number of points. +"""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`](@ref) is a curve that combines straight [`LineStringTrait`](@ref)s and curved [`CircularStringTrait`](@ref)s.""" +"""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 -"""A [`PolygonTrait`](@ref) that can contain either circular or straight curves as rings.""" +"""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 -"""A [`PolygonTrait`](@ref) with straight rings either as exterior or interior(s).""" +"""An [`AbstractSurfaceTrait`](@ref) with straight rings either as exterior or interior(s).""" struct PolygonTrait <: AbstractPolygonTrait end -"""A [`PolygonTrait`](@ref) with straight rings either as exterior or interior(s).""" +"""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 @@ -52,28 +52,28 @@ struct HexagonTrait <: AbstractPolygonTrait end """An AbstractPolyHedralSurfaceTrait type for all polyhedralsurfaces.""" abstract type AbstractPolyHedralSurfaceTrait <: AbstractSurfaceTrait end -"""A [`PolyHedralSurfaceTrait`](@ref) is a connected surface consisting of PolygonsTraits.""" +"""A PolyHedralSurfaceTrait is a connected surface consisting of [`PolygonsTraits`](@ref).""" struct PolyHedralSurfaceTrait <: AbstractPolyHedralSurfaceTrait end -"""A [`TINTrait`](@ref) is a [`PolyHedralSurfaceTrait`](@ref) consisting of [`TriangleTrait`](@ref)s.""" +"""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`](@ref) is a collection of [`PointTrait`](@ref)s.""" +"""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`](@ref) is a collection of [`CircularStringTrait`](@ref)s.""" +"""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`](@ref) is a collection of [`LineStringTrait`](@ref)s.""" +"""A MultiLineStringTrait is a collection of [`LineStringTrait`](@ref)s.""" struct MultiLineStringTrait <: AbstractMultiLineStringTrait end """An AbstractMultiSurfaceTrait type for all multisurfaces.""" abstract type AbstractMultiSurfaceTrait <: AbstractGeometryCollectionTrait end """An AbstractMultiPolygonTrait type for all multipolygons.""" abstract type AbstractMultiPolygonTrait <: AbstractMultiSurfaceTrait end -"""A [`MultiPolygonTrait`](@ref) is a collection of [`PolygonTrait`](@ref)s.""" +"""A MultiPolygonTrait is a collection of [`PolygonTrait`](@ref)s.""" struct MultiPolygonTrait <: AbstractMultiPolygonTrait end diff --git a/src/utils.jl b/src/utils.jl index 9a96eb4c..cd707787 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,15 +1,23 @@ -"""Test whether the interface for your `geom` has been implemented correctly.""" +using InteractiveUtils + +"""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() - getcoord(geom, 1) - ncoord(geom) + n = ncoord(geom) + if n >= 1 # point could be empty + getcoord(geom, 1) # point always needs at least 2 + end else - ngeom(geom) - getgeom(geom, 1) + n = ngeom(geom) + if n >= 1 # geometry could be empty + g2 = getgeom(geom, 1) + geomtype(g2) == subtrait(type) + @assert testgeometry(g2) # recursive testing of subgeometries + end end catch e println("You're missing an implementation: $e") @@ -17,3 +25,17 @@ function testgeometry(geom) end return true end + +"""Test whether the required interface for your `feature` has been implemented correctly.""" +function testfeature(feature) + try + @assert isfeature(geom) + 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/test_primitives.jl b/test/test_primitives.jl index dbc3535d..bf716f77 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -1,18 +1,22 @@ struct MyCurve end +struct MyPoint end @testset "Developer" begin # Implement interface + + GeoInterface.isgeometry(::MyPoint) = true GeoInterface.isgeometry(::MyCurve) = true + GeoInterface.geomtype(::MyPoint) = GeoInterface.PointTrait() GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() - GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 + GeoInterface.ncoord(::GeoInterface.PointTrait, geom::MyPoint) = 2 + GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint, i) = [[1, 2], [2, 3]][i] GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 - GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = [[1, 2], [2, 3]][i] + GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom # Test validity geom = MyCurve() @test testgeometry(geom) - @test !isnothing(GeoInterface.convert(MyCurve, geom)) # Check functions From e6feb6ba2b7613d012dd5bd4da5e397103253cf9 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Tue, 3 May 2022 12:08:59 +0200 Subject: [PATCH 17/41] Removed unused import. --- src/utils.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index cd707787..4a7cd797 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,5 +1,3 @@ -using InteractiveUtils - """Test whether the required interface for your `geom` has been implemented correctly.""" function testgeometry(geom) try From 0df59c995e5a5e895873ca053e44da5f6d2bb5a6 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Sun, 1 May 2022 09:15:38 +0400 Subject: [PATCH 18/41] add getring function to the interface --- src/defaults.jl | 2 ++ src/interface.jl | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index cee13115..8f27dff4 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -47,6 +47,8 @@ getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) +getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getrings(p) for p in getpolygons(geom)) +nring(p::AbstractPolygonTrait, geom) = sum(nring(p) for p in getpolygons(p)) ## Surface npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) diff --git a/src/interface.jl b/src/interface.jl index e0016dfd..af548360 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -191,21 +191,26 @@ boundary(geom) = boundary(geomtype(geom), geom) nring(geom) -> Integer Return the number of rings in given `geom`. -Note that this is only valid for [`AbstractPolygon`](@ref)s. +Note that this is only valid for [`AbstractPolygon`](@ref)s and +[`AbstractMultiPolygon`](@ref)s """ nring(geom) = nring(geomtype(geom), geom) """ - getring(geom, i::Integer) -> Int + getring(geom, [i::Integer]) -> Int Return the `i`th ring for a given `geom`. -Note that this is only valid for [`AbstractPolygon`](@ref)s. +Without the `i` argument, an iterator over all rings is returned. + +Note that this is only valid for [`AbstractPolygon`](@ref)s and +[`AbstractMultiPolygon`](@ref)s in single-argument form. """ getring(geom, i::Integer) = getring(geomtype(geom), geom, i) """ getring(geom) -> Iterator """ getring(geom) = getring(geomtype(geom), geom) +getring(geom, i) = getring(geomtype(geom), geom, i) """ getexterior(geom) -> Curve @@ -313,17 +318,31 @@ Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. npolygon(geom) = npolygon(geomtype(geom), geom) """ - getpolygon(geom, i::Integer) -> AbstractCurve + getpolygon(geom, [i::Integer]) -> AbstractCurve Returns the `i`th polygon for the given `geom`. +Where no `i` index is passed, an iterator over all polygons is returned. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ +getpolygon(geom) = getpolygon(geomtype(geom), geom) getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) """ getpolygon(geom) -> Iterator """ getpolygon(geom) = getpolygon(geomtype(geom), geom) +""" + getring(geom, i::Integer) -> iterable + +A specific ring `i` in a polygon or multipolygon (exterior and holes). +Where no `i` index is passed, an iterator over all rings is returned. + +Note that this is only valid for [`AbstractPolygon`](@ref)s and +[`AbstractMultiPolygon`](@ref)s. +""" +getring(geom) = getring(geomtype(geom), geom) +getring(geom, i::Integer) = getring(geomtype(geom), geom, i) + # Other methods """ crs(geom) -> T <: GeoFormatTypes.CoordinateReferenceSystemFormat From 497abd378fa7525910b17d30df87254327383161 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Tue, 3 May 2022 20:55:22 +0400 Subject: [PATCH 19/41] add getpoint iterators --- src/defaults.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index 8f27dff4..53d6e4b4 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -30,24 +30,26 @@ getpoint(c::AbstractCurveTrait, geom, i) = getgeom(c, geom, i) startpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, 1) endpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, length(geom)) +## MultiLineString +nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) +getpoint(g::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(p) for g in getlinestring(geom)) + ## Polygons nring(p::AbstractPolygonTrait, geom) = ngeom(p, geom) getring(p::AbstractPolygonTrait, geom) = getgeom(p, geom) getring(p::AbstractPolygonTrait, geom, i) = getgeom(p, geom, i) getexterior(p::AbstractPolygonTrait, geom) = getring(p, geom, 1) -nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1 +nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1ring gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) -## MultiLineString -nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) - ## MultiPolygon npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getrings(p) for p in getpolygons(geom)) +getpoint(g::AbstractMultiPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) nring(p::AbstractPolygonTrait, geom) = sum(nring(p) for p in getpolygons(p)) ## Surface From 40c4bcc82cd6434f127f41fb49b9ae46835a146b Mon Sep 17 00:00:00 2001 From: rafaqz Date: Tue, 3 May 2022 21:47:51 +0400 Subject: [PATCH 20/41] bugfix defaults --- src/defaults.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index 53d6e4b4..ec2e98c5 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -48,9 +48,9 @@ gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) -getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getrings(p) for p in getpolygons(geom)) +getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getring(p) for p in getpolygon(geom)) getpoint(g::AbstractMultiPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) -nring(p::AbstractPolygonTrait, geom) = sum(nring(p) for p in getpolygons(p)) +nring(p::AbstractPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) ## Surface npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) From b15d130036d1bb1d83c43faf7475273c089f321c Mon Sep 17 00:00:00 2001 From: rafaqz Date: Tue, 3 May 2022 21:48:16 +0400 Subject: [PATCH 21/41] fix functions and standardise iterator docstrings --- src/defaults.jl | 2 +- src/interface.jl | 71 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index ec2e98c5..85f11510 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -50,7 +50,7 @@ getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getring(p) for p in getpolygon(geom)) getpoint(g::AbstractMultiPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) -nring(p::AbstractPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) +nring(p::AbstractMultiPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) ## Surface npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) diff --git a/src/interface.jl b/src/interface.jl index af548360..b334abca 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -84,7 +84,7 @@ Note that this is only valid for individual [`AbstractPoint`]s. """ getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) """ - getcoord(geom) -> Iterator + getcoord(geom) -> iterator """ getcoord(geom) = getcoord(geomtype(geom), geom) @@ -104,8 +104,11 @@ Return the `i`th Point in given `geom`. Note that this is only valid for [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@ref)s. """ getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) + """ - getpoint(geom) -> Iterator + getpoint(geom) -> iterator + +Returns an iterator over all points in `geom`. """ getpoint(geom) = getpoint(geomtype(geom), geom) @@ -200,17 +203,20 @@ nring(geom) = nring(geomtype(geom), geom) getring(geom, [i::Integer]) -> Int Return the `i`th ring for a given `geom`. -Without the `i` argument, an iterator over all rings is returned. -Note that this is only valid for [`AbstractPolygon`](@ref)s and -[`AbstractMultiPolygon`](@ref)s in single-argument form. +Note that this is only valid for [`AbstractPolygon`](@ref)s. """ getring(geom, i::Integer) = getring(geomtype(geom), geom, i) + """ - getring(geom) -> Iterator + getring(geom) -> iterator + +Returns an iterator over all rings in `geom`. + +Note that this is only valid for [`AbstractPolygon`](@ref)s and +[`AbstractMultiPolygon`](@ref)s in single-argument form. """ getring(geom) = getring(geomtype(geom), geom) -getring(geom, i) = getring(geomtype(geom), geom, i) """ getexterior(geom) -> Curve @@ -220,7 +226,6 @@ Note that this is only valid for [`AbstractPolygon`](@ref)s. """ getexterior(geom) = getexterior(geomtype(geom), geom) - """ nhole(geom) -> Integer @@ -228,6 +233,7 @@ Returns the number of holes for this given `geom`. Note that this is only valid for [`AbstractPolygon`](@ref)s. """ nhole(geom)::Integer = nhole(geomtype(geom), geom) + """ gethole(geom, i::Integer) -> Curve @@ -235,8 +241,12 @@ Returns the `i`th interior ring for this given `geom`. Note that this is only valid for [`AbstractPolygon`](@ref)s. """ gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) + """ - gethole(geom) -> Iterator + gethole(geom) -> iterator + +Returns an iterator over all holes in `geom`. +Note that this is only valid for [`AbstractPolygon`](@ref)s. """ gethole(geom) = gethole(geomtype(geom), geom) @@ -256,8 +266,12 @@ Returns the `i`th patch for the given `geom`. Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. """ getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) + """ - getpatch(geom) -> Iterator + getpatch(geom) -> iterator + +Returns an iterator over all patches in `geom`. +Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. """ getpatch(geom) = getpatch(geomtype(geom), geom) @@ -282,8 +296,11 @@ ngeom(geom) = ngeom(geomtype(geom), geom) Returns the `i`th geometry for the given `geom`. """ getgeom(geom, i::Integer) = getgeom(geomtype(geom), geom, i) + """ - getgeom(geom) -> Iterator + getgeom(geom) -> iterator + +Returns an iterator over all geometry components in `geom`. """ getgeom(geom) = getgeom(geomtype(geom), geom) @@ -303,8 +320,12 @@ Returns the `i`th linestring for the given `geom`. Note that this is only valid for [`AbstractMultiLineString`](@ref)s. """ getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) + """ - getlinestring(geom) -> Iterator + getlinestring(geom, i::Integer) -> iterator + +Returns an iterator over all linestrings in a geometry. +Note that this is only valid for [`AbstractMultiLineString`](@ref)s. """ getlinestring(geom) = getlinestring(geomtype(geom), geom) @@ -318,30 +339,38 @@ Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. npolygon(geom) = npolygon(geomtype(geom), geom) """ - getpolygon(geom, [i::Integer]) -> AbstractCurve + getpolygon(geom) -> AbstractCurve Returns the `i`th polygon for the given `geom`. -Where no `i` index is passed, an iterator over all polygons is returned. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ getpolygon(geom) = getpolygon(geomtype(geom), geom) -getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) + """ - getpolygon(geom) -> Iterator + getpolygon(geom) -> AbstractCurve + +Returns an iterator over all polygons in a geometry. +Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ -getpolygon(geom) = getpolygon(geomtype(geom), geom) +getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) """ - getring(geom, i::Integer) -> iterable + getring(geom, i::Integer) -> AbstractCurve A specific ring `i` in a polygon or multipolygon (exterior and holes). -Where no `i` index is passed, an iterator over all rings is returned. +Note that this is only valid for [`AbstractPolygon`](@ref)s and +[`AbstractMultiPolygon`](@ref)s. +""" +getring(geom, i::Integer) = getring(geomtype(geom), geom, i) + +""" + getring(geom) -> iterable +Returns an iterator over all rings in a geometry. Note that this is only valid for [`AbstractPolygon`](@ref)s and [`AbstractMultiPolygon`](@ref)s. """ getring(geom) = getring(geomtype(geom), geom) -getring(geom, i::Integer) = getring(geomtype(geom), geom, i) # Other methods """ @@ -471,7 +500,7 @@ union(a, b) = union(geomtype(a), geomtype(b), a, b) # Spatial analysis """ - distance(a, b) -> Number +3 distance(a, b) -> Number Returns the shortest distance between `a` with `b`. """ From c537100df01174503d84695059fedffd7cf5a1b4 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Tue, 3 May 2022 21:55:25 +0400 Subject: [PATCH 22/41] bugfix --- src/defaults.jl | 20 ++++++++++++-------- src/interface.jl | 15 +++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index 85f11510..b91f571f 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -30,27 +30,31 @@ getpoint(c::AbstractCurveTrait, geom, i) = getgeom(c, geom, i) startpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, 1) endpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, length(geom)) -## MultiLineString -nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) -getpoint(g::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(p) for g in getlinestring(geom)) - ## Polygons nring(p::AbstractPolygonTrait, geom) = ngeom(p, geom) getring(p::AbstractPolygonTrait, geom) = getgeom(p, geom) getring(p::AbstractPolygonTrait, geom, i) = getgeom(p, geom, i) getexterior(p::AbstractPolygonTrait, geom) = getring(p, geom, 1) -nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1ring +nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1 gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) +npoint(p::AbstractPolygonTrait, geom) = sum(npoint(p) for p in getring(p)) +getpoint(g::AbstractPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) + +## MultiLineString +nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) +getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) +npoint(g::AbstractMultiLineStringTrait, geom) = sum(npoint(l) for ls in getlinestring(geom)) +getpoint(g::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(ls) for l in getlinestring(geom)) ## MultiPolygon npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) +nring(p::AbstractMultiPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getring(p) for p in getpolygon(geom)) +npoint(p::AbstractMultiPolygonTrait, geom) = sum(npoint(r) for r in getring(geom)) getpoint(g::AbstractMultiPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) -nring(p::AbstractMultiPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) ## Surface npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) diff --git a/src/interface.jl b/src/interface.jl index b334abca..8ae724ac 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -200,7 +200,7 @@ Note that this is only valid for [`AbstractPolygon`](@ref)s and nring(geom) = nring(geomtype(geom), geom) """ - getring(geom, [i::Integer]) -> Int + getring(geom, i::Integer) -> Int Return the `i`th ring for a given `geom`. @@ -212,7 +212,6 @@ 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 [`AbstractPolygon`](@ref)s and [`AbstractMultiPolygon`](@ref)s in single-argument form. """ @@ -322,7 +321,7 @@ Note that this is only valid for [`AbstractMultiLineString`](@ref)s. getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) """ - getlinestring(geom, i::Integer) -> iterator + getlinestring(geom) -> iterator Returns an iterator over all linestrings in a geometry. Note that this is only valid for [`AbstractMultiLineString`](@ref)s. @@ -339,20 +338,20 @@ Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. npolygon(geom) = npolygon(geomtype(geom), geom) """ - getpolygon(geom) -> AbstractCurve + getpolygon(geom, i::Integer) -> AbstractCurve Returns the `i`th polygon for the given `geom`. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ -getpolygon(geom) = getpolygon(geomtype(geom), geom) +getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) """ - getpolygon(geom) -> AbstractCurve + getpolygon(geom) -> iterator Returns an iterator over all polygons in a geometry. Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. """ -getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) +getpolygon(geom) = getpolygon(geomtype(geom), geom) """ getring(geom, i::Integer) -> AbstractCurve @@ -500,7 +499,7 @@ union(a, b) = union(geomtype(a), geomtype(b), a, b) # Spatial analysis """ -3 distance(a, b) -> Number + distance(a, b) -> Number Returns the shortest distance between `a` with `b`. """ From 9ab7e2c81b549da479558b5477511dc8613ca381 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 8 May 2022 16:45:34 +0200 Subject: [PATCH 23/41] Fixed doc reference. Removed duplicate getring function. --- docs/src/background/sf.md | 4 ++ docs/src/guides/defaults.md | 6 ++- docs/src/guides/developer.md | 20 +++++--- docs/src/reference/api.md | 4 ++ docs/src/tutorials/usage.md | 4 ++ src/interface.jl | 94 +++++++++++++++--------------------- src/types.jl | 2 +- 7 files changed, 68 insertions(+), 66 deletions(-) diff --git a/docs/src/background/sf.md b/docs/src/background/sf.md index 0e35a577..7a1ffa44 100644 --- a/docs/src/background/sf.md +++ b/docs/src/background/sf.md @@ -1,3 +1,7 @@ +```@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`. diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index 3642eab6..7f925512 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -1,3 +1,7 @@ +```@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: @@ -9,7 +13,7 @@ Of note here are the `ngeom` and `getgeom` for each geometry type, which transla | [`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) | +| [`AbstractPolyHedralSurfaceTrait`](@ref) | [`npatch(geom)`](@ref) | [`getpatch(geom)`](@ref) | | [`AbstractGeometryCollectionTrait`](@ref) | [`ngeom(geom)`](@ref) | [`getgeom(geom)`](@ref) | ## Polygons diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 772f2058..a17ba611 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -1,3 +1,7 @@ +```@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. @@ -9,7 +13,7 @@ also implement those interfaces where applicable. ```julia GeoInterface.isgeometry(geom::customgeom)::Bool = true -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.XTrait() # <: AbstractGeometryTrait +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.XTraitTrait() # <: AbstractGeometryTrait GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for PointTraits GeoInterface.ngeom(geomtype(geom), geom::customgeom)::Integer @@ -82,7 +86,7 @@ GeoInterface.isgeometry(geom::customgeom)::Bool = true A `geom::customgeom` with "Point"-like traits implements ```julia -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Point() +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.PointTrait() GeoInterface.ncoord(::GeoInterface.PointTrait, geom::customgeom)::Integer GeoInterface.getcoord(::GeoInterface.PointTrait, geom::customgeom, i)::Real @@ -93,7 +97,7 @@ GeoInterface.getgeom(::GeoInterface.PointTrait, geom::customgeom, i) = nothing A `geom::customgeom` with "LineString"-like traits implements the following methods: ```julia -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.LineString() +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.LineStringTrait() GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::customgeom)::Integer # These alias for npoint and getpoint @@ -107,7 +111,7 @@ GeoInterface.length(::GeoInterface.LineStringTrait, geom::customgeom)::Real ``` A `geom::customgeom` with "Polygon"-like traits can implement the following methods: ```julia -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.Polygon() +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.PolygonTrait() GeoInterface.ncoord(::GeoInterface.PolygonTrait, geom::customgeom)::Integer # These alias for nring and getring @@ -123,7 +127,7 @@ GeoInterface.boundary(::GeoInterface.PolygonTrait, geom::customgeom)::"LineStrin A `geom::customgeom` with "GeometryCollection"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::customgeom) = GeoInterface.GeometryCollection() +GeoInterface.geomtype(geom::customgeom) = GeoInterface.GeometryCollectionTrait() GeoInterface.ncoord(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer GeoInterface.ngeom(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer GeoInterface.getgeom(::GeoInterface.GeometryCollectionTrait,geom::customgeomm, i)::"GeometryTrait" @@ -131,7 +135,7 @@ GeoInterface.getgeom(::GeoInterface.GeometryCollectionTrait,geom::customgeomm, i A `geom::customgeom` with "MultiPoint"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPoint() +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPointTrait() GeoInterface.ncoord(::GeoInterface.MultiPointTrait, geom::customgeom)::Integer # These alias for npoint and getpoint @@ -141,7 +145,7 @@ GeoInterface.getgeom(::GeoInterface.MultiPointTrait, geom::customgeom, i)::"Poin A `geom::customgeom` with "MultiLineString"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiLineString() +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiLineStringTrait() GeoInterface.ncoord(::GeoInterface.MultiLineStringTrait, geom::customgeom)::Integer # These alias for nlinestring and getlinestring @@ -151,7 +155,7 @@ GeoInterface.getgeom(::GeoInterface.MultiLineStringTrait,geom::customgeomm, i):: A `geom::customgeom` with "MultiPolygon"-like traits has to implement the following methods: ```julia -GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPolygon() +GeoInterface.geomtype(geom::customgeom) = GeoInterface.MultiPolygonTrait() GeoInterface.ncoord(::GeoInterface.MultiPolygonTrait, geom::customgeom)::Integer # These alias for npolygon and getpolygon diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index b3aac14b..bf0132f9 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -1,3 +1,7 @@ +```@meta +CurrentModule = GeoInterface +``` + # API ## Functions diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index a9939d36..a6ae89b1 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -1,3 +1,7 @@ +```@meta +CurrentModule = GeoInterface +``` + # Traits interface GeoInterface provides a traits interface, not unlike Tables.jl, by a set of functions and types for geospatial data. diff --git a/src/interface.jl b/src/interface.jl index 8ae724ac..45df4dec 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -42,7 +42,7 @@ properties(feat) = nothing """ GeoInterface.geomtype(geom) => T <: AbstractGeometry -Returns the geometry type, such as [`GeoInterface.Polygon`](@ref) or [`GeoInterface.Point`](@ref). +Returns the geometry type, such as [`GeoInterface.PolygonTrait`](@ref) or [`GeoInterface.PointTrait`](@ref). """ geomtype(geom) = nothing @@ -80,7 +80,7 @@ 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 [`AbstractPoint`]s. +Note that this is only valid for individual [`AbstractPointTrait`](@ref)s. """ getcoord(geom, i::Integer) = getcoord(geomtype(geom), geom, i) """ @@ -93,7 +93,7 @@ getcoord(geom) = getcoord(geomtype(geom), geom) npoint(geom) -> Int Return the number of points in given `geom`. -Note that this is only valid for [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s and [`AbstractMultiPointTrait`](@ref)s. """ npoint(geom) = npoint(geomtype(geom), geom) @@ -101,7 +101,7 @@ 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 [`AbstractCurve`](@ref)s and [`AbstractMultiPoint`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s and [`AbstractMultiPointTrait`](@ref)s. """ getpoint(geom, i::Integer) = getpoint(geomtype(geom), geom, i) @@ -117,7 +117,7 @@ getpoint(geom) = getpoint(geomtype(geom), geom) startpoint(geom) -> Point Return the first point in the `geom`. -Note that this is only valid for [`AbstractCurve`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. """ startpoint(geom) = startpoint(geomtype(geom), geom) @@ -125,7 +125,7 @@ startpoint(geom) = startpoint(geomtype(geom), geom) endpoint(geom) -> Point Return the last point in the `geom`. -Note that this is only valid for [`AbstractCurve`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. """ endpoint(geom) = endpoint(geomtype(geom), geom) @@ -134,7 +134,7 @@ endpoint(geom) = endpoint(geomtype(geom), geom) Return whether the `geom` is closed, i.e. whether the `startpoint` is the same as the `endpoint`. -Note that this is only valid for [`AbstractCurve`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. """ isclosed(geom) = isclosed(geomtype(geom), geom) @@ -143,7 +143,7 @@ isclosed(geom) = isclosed(geomtype(geom), geom) Return whether the `geom` is a ring, i.e. whether the `geom` [`isclosed`](@ref) and [`issimple`](@ref). -Note that this is only valid for [`AbstractCurve`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. """ isring(geom) = isclosed(geom) && issimple(geom) @@ -151,7 +151,7 @@ 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 [`AbstractCurve`](@ref)s. +Note that this is only valid for [`AbstractCurveTrait`](@ref)s. """ length(geom) = length(geomtype(geom), geom) @@ -160,7 +160,7 @@ length(geom) = length(geomtype(geom), geom) area(geom) -> Number Return the area of `geom` in its 2d coordinate system. -Note that this is only valid for [`AbstractSurface`](@ref)s. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. """ area(geom) = area(geomtype(geom), geom) @@ -169,7 +169,7 @@ area(geom) = area(geomtype(geom), geom) 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 [`AbstractSurface`](@ref)s. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. """ centroid(geom) = centroid(geomtype(geom), geom) @@ -177,7 +177,7 @@ 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 [`AbstractSurface`](@ref)s. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. """ pointonsurface(geom) = pointonsurface(geomtype(geom), geom) @@ -185,7 +185,7 @@ pointonsurface(geom) = pointonsurface(geomtype(geom), geom) boundary(geom) -> Curve Return the boundary of `geom`. -Note that this is only valid for [`AbstractSurface`](@ref)s. +Note that this is only valid for [`AbstractSurfaceTrait`](@ref)s. """ boundary(geom) = boundary(geomtype(geom), geom) @@ -194,17 +194,17 @@ boundary(geom) = boundary(geomtype(geom), geom) nring(geom) -> Integer Return the number of rings in given `geom`. -Note that this is only valid for [`AbstractPolygon`](@ref)s and -[`AbstractMultiPolygon`](@ref)s +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s and +[`AbstractMultiPolygonTrait`](@ref)s """ nring(geom) = nring(geomtype(geom), geom) """ - getring(geom, i::Integer) -> Int - -Return the `i`th ring for a given `geom`. + getring(geom, i::Integer) -> AbstractCurve -Note that this is only valid for [`AbstractPolygon`](@ref)s. +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) @@ -212,8 +212,8 @@ 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 [`AbstractPolygon`](@ref)s and -[`AbstractMultiPolygon`](@ref)s in single-argument form. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s and +[`AbstractMultiPolygonTrait`](@ref)s in single-argument form. """ getring(geom) = getring(geomtype(geom), geom) @@ -221,7 +221,7 @@ 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 [`AbstractPolygon`](@ref)s. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. """ getexterior(geom) = getexterior(geomtype(geom), geom) @@ -229,7 +229,7 @@ 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 [`AbstractPolygon`](@ref)s. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. """ nhole(geom)::Integer = nhole(geomtype(geom), geom) @@ -237,7 +237,7 @@ 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 [`AbstractPolygon`](@ref)s. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. """ gethole(geom, i::Integer) = gethole(geomtype(geom), geom, i) @@ -245,7 +245,7 @@ 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 [`AbstractPolygon`](@ref)s. +Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. """ gethole(geom) = gethole(geomtype(geom), geom) @@ -254,7 +254,7 @@ gethole(geom) = gethole(geomtype(geom), geom) npatch(geom) Returns the number of patches for the given `geom`. -Note that this is only valid for [`AbstractPolyHedralSurface`](@ref)s. +Note that this is only valid for [`AbstractPolyHedralSurfaceTrait`](@ref)s. """ npatch(geom)::Integer = npatch(geomtype(geom), geom) @@ -262,7 +262,7 @@ 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 [`AbstractPolyHedralSurface`](@ref)s. +Note that this is only valid for [`AbstractPolyHedralSurfaceTrait`](@ref)s. """ getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) @@ -270,7 +270,7 @@ 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 [`AbstractPolyHedralSurface`](@ref)s. +Note that this is only valid for [`AbstractPolyHedralSurfaceTrait`](@ref)s. """ getpatch(geom) = getpatch(geomtype(geom), geom) @@ -308,7 +308,7 @@ getgeom(geom) = getgeom(geomtype(geom), geom) nlinestring(geom) -> Integer Returns the number of curves for the given `geom`. -Note that this is only valid for [`AbstractMultiLineString`](@ref)s. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. """ nlinestring(geom) = nlinestring(geomtype(geom), geom) @@ -316,7 +316,7 @@ 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 [`AbstractMultiLineString`](@ref)s. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. """ getlinestring(geom, i::Integer) = getlinestring(geomtype(geom), geom, i) @@ -324,7 +324,7 @@ 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 [`AbstractMultiLineString`](@ref)s. +Note that this is only valid for [`AbstractMultiLineStringTrait`](@ref)s. """ getlinestring(geom) = getlinestring(geomtype(geom), geom) @@ -333,7 +333,7 @@ getlinestring(geom) = getlinestring(geomtype(geom), geom) npolygon(geom) -> Integer Returns the number of polygons for the given `geom`. -Note that this is only valid for [`AbstractMultiPolygon`](@ref)s. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. """ npolygon(geom) = npolygon(geomtype(geom), geom) @@ -341,7 +341,7 @@ 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 [`AbstractMultiPolygon`](@ref)s. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. """ getpolygon(geom, i::Integer) = getpolygon(geomtype(geom), geom, i) @@ -349,28 +349,10 @@ 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 [`AbstractMultiPolygon`](@ref)s. +Note that this is only valid for [`AbstractMultiPolygonTrait`](@ref)s. """ getpolygon(geom) = getpolygon(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 [`AbstractPolygon`](@ref)s and -[`AbstractMultiPolygon`](@ref)s. -""" -getring(geom, i::Integer) = getring(geomtype(geom), geom, i) - -""" - getring(geom) -> iterable - -Returns an iterator over all rings in a geometry. -Note that this is only valid for [`AbstractPolygon`](@ref)s and -[`AbstractMultiPolygon`](@ref)s. -""" -getring(geom) = getring(geomtype(geom), geom) - # Other methods """ crs(geom) -> T <: GeoFormatTypes.CoordinateReferenceSystemFormat @@ -523,7 +505,7 @@ convexhull(geom) = convexhull(geomtype(geom), geom) x(geom) -> Number Return the :X coordinate of the given `geom`. -Note that this is only valid for [`AbstractPoint`](@ref)s. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. """ x(geom) = x(geomtype(geom), geom) @@ -531,7 +513,7 @@ x(geom) = x(geomtype(geom), geom) y(geom) -> Number Return the :Y coordinate of the given `geom`. -Note that this is only valid for [`AbstractPoint`](@ref)s. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. """ y(geom) = y(geomtype(geom), geom) @@ -539,7 +521,7 @@ y(geom) = y(geomtype(geom), geom) z(geom) -> Number Return the :Z coordinate of the given `geom`. -Note that this is only valid for [`AbstractPoint`](@ref)s. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. """ z(geom) = z(geomtype(geom), geom) @@ -547,7 +529,7 @@ z(geom) = z(geomtype(geom), geom) m(geom) -> Number Return the :M coordinate of the given `geom`. -Note that this is only valid for [`AbstractPoint`](@ref)s. +Note that this is only valid for [`AbstractPointTrait`](@ref)s. """ m(geom) = m(geomtype(geom), geom) diff --git a/src/types.jl b/src/types.jl index ea958388..851ffa44 100644 --- a/src/types.jl +++ b/src/types.jl @@ -52,7 +52,7 @@ struct HexagonTrait <: AbstractPolygonTrait end """An AbstractPolyHedralSurfaceTrait type for all polyhedralsurfaces.""" abstract type AbstractPolyHedralSurfaceTrait <: AbstractSurfaceTrait end -"""A PolyHedralSurfaceTrait is a connected surface consisting of [`PolygonsTraits`](@ref).""" +"""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 From 7ee9acefd6241dae77a902fc03e2a49a2997ef9b Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 8 May 2022 20:13:56 +0200 Subject: [PATCH 24/41] Added doctests. Expanded the documentation. --- Project.toml | 5 ++--- docs/make.jl | 4 +++- docs/src/background/history.md | 21 +++++++++++++++++++++ docs/src/background/sf.md | 10 +--------- docs/src/guides/defaults.md | 2 +- docs/src/guides/developer.md | 23 +++++++++++++++-------- docs/src/index.md | 6 +++--- docs/src/tutorials/usage.md | 2 +- src/defaults.jl | 22 ++++++++++++++++++++++ test/runtests.jl | 2 ++ 10 files changed, 71 insertions(+), 26 deletions(-) create mode 100644 docs/src/background/history.md diff --git a/Project.toml b/Project.toml index 0a6c61b0..fff74913 100644 --- a/Project.toml +++ b/Project.toml @@ -5,14 +5,13 @@ version = "1.0.0" [deps] Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" [compat] -RecipesBase = "1" julia = "1" [extras] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test", "Documenter"] diff --git a/docs/make.jl b/docs/make.jl index 0e97ef72..c9cb5a7a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,6 +18,7 @@ makedocs(; "Home" => "index.md", "Background" => Any[ "Simple Features"=>"background/sf.md", + "History"=>"background/history.md", ], "Tutorials" => Any[ "Installation"=>"tutorials/installation.md", @@ -31,7 +32,8 @@ makedocs(; "API" => "reference/api.md" "Implementations" => "reference/integrations.md" ], - ] + ], + doctest=true ) deploydocs(; 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 index 7a1ffa44..5df83ca5 100644 --- a/docs/src/background/sf.md +++ b/docs/src/background/sf.md @@ -12,6 +12,7 @@ All types used here come from the SF. We added `Trait` to all geometry types her ![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. @@ -54,15 +55,6 @@ Not all SF functions are implemented, either as a possibly slower fallback or em dimension # topological dimensions spatialDimension - locateAlong locateBetween ``` - -## 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. - -[^sgillies]: https://gist.github.com/sgillies/2217756 -[^geojson]: https://geojson.org/ diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index 7f925512..41b2d95e 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -28,7 +28,7 @@ nhole(p::AbstractPolygonTrait, geom) = nring(p, geom) - 1 gethole(p::AbstractPolygonTrait, geom, i) = getring(p, geom, i + 1) ``` ## LineStrings -Simarly for LineStrings, we have the following +Similarly for `LineStringTrait`s, we have the following ```julia startpoint(geom) = getpoint(geom, 1) endpoint(geom) = getpoint(geom, length(geom)) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index a17ba611..cef0a43b 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -4,24 +4,30 @@ 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. +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 = GeoInterface.XTraitTrait() # <: AbstractGeometryTrait +GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.XTrait() # <: AbstractGeometryTrait +# for PointTraits GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer -GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real # only for PointTraits +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) # geomtype -> GeoInterface.Y +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`. The `ngeom` and `getgeom` are aliases for their geom specific counterparts, such as `npoints` and `getpoint` for LineStrings. +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. -You read more about the `geomtype` in the [Type hierarchy](@ref). ## Optional for Geometry @@ -71,15 +77,16 @@ union(geomtype(a), geomtype(b), a, b) ``` ## Testing the interface -GeoInterface provides a Testsuite for a geom type to check whether all functions that have been implemented also work as expected. +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 geometies implement +All custom geometries implement ```julia GeoInterface.isgeometry(geom::customgeom)::Bool = true ``` diff --git a/docs/src/index.md b/docs/src/index.md index 31de2588..a2013d0b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -11,13 +11,13 @@ CurrentModule = GeoInterface 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, +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 background about the interface and Simple Features, see [Changes with respect to SF](@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. Feel free to ask questions on Github. + 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/tutorials/usage.md b/docs/src/tutorials/usage.md index a6ae89b1..bf6de7e2 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -47,6 +47,6 @@ julia> geomtype(ext) GeoInterface.LineStringTrait() julia> getcoords.(getpoint.(Ref(ext), 1:npoint(ext))) [[1.,2.],[2.,3.],[1.,2.]] -julia> coords(geom) # fallback based on ngeom & npoint above +julia> coordinates(geom) # fallback based on ngeom & npoint above ``` diff --git a/src/defaults.jl b/src/defaults.jl index b91f571f..f50caddf 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -95,6 +95,28 @@ function coordinates(::AbstractGeometryTrait, geom) end # Subtraits + +""" + subtrait(t::AbstractGeometryTrait) + +Gets the expected (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(GeoInterface.LineStringTrait()) +GeoInterface.PointTrait +julia> GeoInterface.subtrait(GeoInterface.MultiPointTrait()) +GeoInterface.PointTrait +``` +```jldoctest; setup = :(using GeoInterface) +# `nothing` is returned when there's no subtrait or when it's not known beforehand +julia> isnothing(GeoInterface.subtrait(GeoInterface.PointTrait())) +true +julia> isnothing(GeoInterface.subtrait(GeoInterface.GeometryCollectionTrait())) +true +``` +""" subtrait(::PointTrait) = nothing subtrait(::LineStringTrait) = PointTrait subtrait(::PolygonTrait) = LineStringTrait diff --git a/test/runtests.jl b/test/runtests.jl index e5c22054..d8047867 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,6 @@ using GeoInterface +using Documenter using Test include("test_primitives.jl") +doctest(GeoInterface) From 1805746120eab14c29207a41758405c9dcc07235 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 8 May 2022 20:48:21 +0200 Subject: [PATCH 25/41] Fixed bug in coordinates. --- src/defaults.jl | 2 +- test/test_primitives.jl | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index f50caddf..dc4656fb 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -91,7 +91,7 @@ function coordinates(::AbstractPointTrait, geom) collect(getcoord(geom)) end function coordinates(::AbstractGeometryTrait, geom) - collect(coordinates(getgeom(geom))) + collect(coordinates.(getgeom(geom))) end # Subtraits diff --git a/test/test_primitives.jl b/test/test_primitives.jl index bf716f77..f90cb7a6 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -9,7 +9,7 @@ struct MyPoint end GeoInterface.geomtype(::MyPoint) = GeoInterface.PointTrait() GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() GeoInterface.ncoord(::GeoInterface.PointTrait, geom::MyPoint) = 2 - GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint, i) = [[1, 2], [2, 3]][i] + GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint, i) = [1, 2][i] GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom @@ -21,6 +21,7 @@ struct MyPoint end # Check functions @test GeoInterface.npoint(geom) == 2 # defaults to ngeom + @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] @test_throws MethodError GeoInterface.area(geom) end From 567683b044b906e60063123cda3a7f2867a5c2f9 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 9 May 2022 11:46:40 +0200 Subject: [PATCH 26/41] Fix bug in x, y, z, m lookup in getcoord. --- src/defaults.jl | 8 ++++---- test/test_primitives.jl | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index dc4656fb..17ea79f0 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -9,10 +9,10 @@ const default_coord_names = (:X, :Y, :Z, :M) coordnames(::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(geom)] # Maybe hardcode dimension order? At least for X and Y? -x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :X)) -y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :Y)) -z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :Z)) -m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(coordnames(geom), :M)) +x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :X, coordnames(geom))) +y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :Y, coordnames(geom))) +z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :Z, coordnames(geom))) +m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :M, coordnames(geom))) is3d(::AbstractPointTrait, geom) = :Z in coordnames(geom) ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index f90cb7a6..3a29296e 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -23,5 +23,8 @@ struct MyPoint end @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] @test_throws MethodError GeoInterface.area(geom) + point = GeoInterface.getgeom(geom, 1) + @test GeoInterface.y(point) == 2 + end From ddee19813b173d5ecf2c8f2aedb05d1d9f8bb368 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 9 May 2022 11:54:51 +0200 Subject: [PATCH 27/41] Improved performance of coordinate name lookup. --- src/defaults.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index 17ea79f0..7bd81af4 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -9,10 +9,10 @@ const default_coord_names = (:X, :Y, :Z, :M) coordnames(::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(geom)] # Maybe hardcode dimension order? At least for X and Y? -x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :X, coordnames(geom))) -y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :Y, coordnames(geom))) -z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :Z, coordnames(geom))) -m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(x -> x === :M, coordnames(geom))) +x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:X), coordnames(geom))) +y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:Y), coordnames(geom))) +z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:Z), coordnames(geom))) +m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:M), coordnames(geom))) is3d(::AbstractPointTrait, geom) = :Z in coordnames(geom) ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) From 63031887b9267229d46e77dd94b4d28b67b9b561 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Mon, 9 May 2022 22:52:18 +0400 Subject: [PATCH 28/41] standardise trait arg passing --- src/defaults.jl | 93 +++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index 7bd81af4..cd5934ab 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -6,13 +6,13 @@ # Four options in SF, xy, xyz, xym, xyzm const default_coord_names = (:X, :Y, :Z, :M) -coordnames(::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(geom)] +coordnames(t::AbstractGeometryTrait, geom) = default_coord_names[1:ncoord(t, geom)] # Maybe hardcode dimension order? At least for X and Y? -x(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:X), coordnames(geom))) -y(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:Y), coordnames(geom))) -z(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:Z), coordnames(geom))) -m(::AbstractPointTrait, geom) = getcoord(geom, findfirst(isequal(:M), coordnames(geom))) +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) @@ -24,46 +24,46 @@ getgeom(::AbstractPointTrait, geom) = nothing getgeom(::AbstractPointTrait, geom, i) = nothing ## LineStrings -npoint(c::AbstractCurveTrait, geom) = ngeom(c, geom) -getpoint(c::AbstractCurveTrait, geom) = getgeom(c, geom) -getpoint(c::AbstractCurveTrait, geom, i) = getgeom(c, geom, i) -startpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, 1) -endpoint(c::AbstractCurveTrait, geom) = getpoint(c, geom, length(geom)) +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, length(geom)) ## Polygons -nring(p::AbstractPolygonTrait, geom) = ngeom(p, geom) -getring(p::AbstractPolygonTrait, geom) = getgeom(p, geom) -getring(p::AbstractPolygonTrait, geom, i) = getgeom(p, geom, i) -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) -npoint(p::AbstractPolygonTrait, geom) = sum(npoint(p) for p in getring(p)) -getpoint(g::AbstractPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) +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, i) = getring(t, geom, i + 1) +npoint(p::AbstractPolygonTrait, geom) = sum(npoint(p) for p in getring(t, geom)) +getpoint(t::AbstractPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(t, geom)) ## MultiLineString -nlinestring(p::AbstractMultiLineStringTrait, geom) = ngeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom) = getgeom(p, geom) -getlinestring(p::AbstractMultiLineStringTrait, geom, i) = getgeom(p, geom, i) -npoint(g::AbstractMultiLineStringTrait, geom) = sum(npoint(l) for ls in getlinestring(geom)) -getpoint(g::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(ls) for l in getlinestring(geom)) +nlinestring(t::AbstractMultiLineStringTrait, geom) = ngeom(t, geom) +getlinestring(t::AbstractMultiLineStringTrait, geom) = getgeom(t, geom) +getlinestring(t::AbstractMultiLineStringTrait, geom, i) = getgeom(t, geom, i) +npoint(t::AbstractMultiLineStringTrait, geom) = sum(npoint(ls) for ls in getgeom(t, geom)) +getpoint(t::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(ls) for ls in getgeom(t, geom)) ## MultiPolygon -npolygon(p::AbstractMultiPolygonTrait, geom) = ngeom(p, geom) -getpolygon(p::AbstractMultiPolygonTrait, geom) = getgeom(p, geom) -getpolygon(p::AbstractMultiPolygonTrait, geom, i) = getgeom(p, geom, i) -nring(p::AbstractMultiPolygonTrait, geom) = sum(nring(p) for p in getpolygon(p)) -getring(g::AbstractMultiPolygonTrait, geom) = (r for r in getring(p) for p in getpolygon(geom)) -npoint(p::AbstractMultiPolygonTrait, geom) = sum(npoint(r) for r in getring(geom)) -getpoint(g::AbstractMultiPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(geom)) +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) = (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) = (p for p in getpoint(r) for r in getring(t, geom)) ## Surface -npatch(p::AbstractPolyHedralSurfaceTrait, geom)::Integer = ngeom(p, geom) -getpatch(p::AbstractPolyHedralSurfaceTrait, geom) = getgeom(p, geom) -getpatch(p::AbstractPolyHedralSurfaceTrait, geom, i::Integer) = getgeom(p, geom, i) +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(p::AbstractGeometryTrait, geom) = (getgeom(p, geom, i) for i in 1:ngeom(p, geom)) -getcoord(p::AbstractPointTrait, geom) = (getcoord(p, geom, i) for i in 1:ncoord(p, geom)) +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)) ## Npoints npoint(::LineTrait, _) = 2 @@ -73,25 +73,28 @@ npoint(::QuadTrait, _) = 4 npoint(::PentagonTrait, _) = 5 npoint(::HexagonTrait, _) = 6 -issimple(::AbstractCurveTrait, geom) = allunique([getpoint(geom, i) for i in 1:npoint(geom)-1]) && allunique([getpoint(geom, i) for i in 2:npoint(geom)]) -isclosed(::AbstractCurveTrait, geom) = getpoint(geom, 1) == getpoint(geom, npoint(geom)) -isring(x::AbstractCurveTrait, geom) = issimple(x, geom) && isclosed(x, geom) +issimple(::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) # TODO Only simple if it's also not intersecting itself, except for its endpoints -issimple(::AbstractMultiCurveTrait, geom) = all(i -> issimple(getgeom(geom, i)), 1:ngeom(geom)) -isclosed(::AbstractMultiCurveTrait, geom) = all(i -> isclosed(getgeom(geom, i)), 1:ngeom(geom)) +issimple(t::AbstractMultiCurveTrait, geom) = all(i -> issimple(getgeom(t, geom, i)), 1:ngeom(t, geom)) +isclosed(t::AbstractMultiCurveTrait, geom) = all(i -> isclosed(getgeom(t, geom, i)), 1:ngeom(t, geom)) -issimple(::MultiPointTrait, geom) = allunique((getgeom(geom, i) for i in 1:ngeom(geom))) +issimple(t::AbstractMultiPointTrait, geom) = allunique((getgeom(t, geom, i) for i in 1:ngeom(t, geom))) +getpoint(t::AbstractMultiPointTrait, geom) = getgeom(t, geom) +getpoint(t::AbstractMultiPointTrait, geom, i) = getgeom(t, geom, i) crs(::AbstractGeometryTrait, geom) = nothing extent(::AbstractGeometryTrait, geom) = nothing # Backwards compatibility -function coordinates(::AbstractPointTrait, geom) - collect(getcoord(geom)) +function coordinates(t::AbstractPointTrait, geom) + collect(getcoord(t, geom)) end -function coordinates(::AbstractGeometryTrait, geom) - collect(coordinates.(getgeom(geom))) +function coordinates(t::AbstractGeometryTrait, geom) + collect(coordinates.(getgeom(t, geom))) end # Subtraits From 3305aef21064dc4a0cf6a1395a30ea26f20de548 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Tue, 10 May 2022 22:40:15 +0200 Subject: [PATCH 29/41] Completed subtrait method. --- src/defaults.jl | 25 +++++++++++++++---------- src/types.jl | 2 ++ src/utils.jl | 5 ++++- test/test_primitives.jl | 9 ++++++--- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/defaults.jl b/src/defaults.jl index cd5934ab..0f1950ef 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -102,15 +102,15 @@ end """ subtrait(t::AbstractGeometryTrait) -Gets the expected (sub)trait for subgeometries (retrieved with [`getgeom`](@ref)) of trait `t`. +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(GeoInterface.LineStringTrait()) GeoInterface.PointTrait -julia> GeoInterface.subtrait(GeoInterface.MultiPointTrait()) -GeoInterface.PointTrait +julia> GeoInterface.subtrait(GeoInterface.PolygonTrait()) # Any of LineStringTrait, LineTrait, LinearRingTrait +GeoInterface.AbstractLineStringTrait ``` ```jldoctest; setup = :(using GeoInterface) # `nothing` is returned when there's no subtrait or when it's not known beforehand @@ -120,10 +120,15 @@ julia> isnothing(GeoInterface.subtrait(GeoInterface.GeometryCollectionTrait())) true ``` """ -subtrait(::PointTrait) = nothing -subtrait(::LineStringTrait) = PointTrait -subtrait(::PolygonTrait) = LineStringTrait -subtrait(::MultiPointTrait) = PointTrait -subtrait(::MultiLineStringTrait) = LineStringTrait -subtrait(::MultiPolygonTrait) = PolygonTrait -subtrait(::GeometryCollectionTrait) = nothing +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/types.jl b/src/types.jl index 851ffa44..a5e44f75 100644 --- a/src/types.jl +++ b/src/types.jl @@ -73,6 +73,8 @@ 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.""" diff --git a/src/utils.jl b/src/utils.jl index 4a7cd797..b7106cab 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -13,7 +13,10 @@ function testgeometry(geom) n = ngeom(geom) if n >= 1 # geometry could be empty g2 = getgeom(geom, 1) - geomtype(g2) == subtrait(type) + subtype = subtrait(type) + if !isnothing(subtype) + @assert geomtype(g2) isa subtype + end @assert testgeometry(g2) # recursive testing of subgeometries end end diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 3a29296e..f9158635 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -1,9 +1,9 @@ -struct MyCurve end -struct MyPoint end + @testset "Developer" begin # Implement interface - + struct MyCurve end + struct MyPoint end GeoInterface.isgeometry(::MyPoint) = true GeoInterface.isgeometry(::MyCurve) = true GeoInterface.geomtype(::MyPoint) = GeoInterface.PointTrait() @@ -26,5 +26,8 @@ struct MyPoint end point = GeoInterface.getgeom(geom, 1) @test GeoInterface.y(point) == 2 +end +@testset "Defaults" begin + @test GeoInterface.subtrait(GeoInterface.TINTrait()) == GeoInterface.TriangleTrait end From 19946d719c65b0cdd77642967b7d3c714bf0ae9d Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Wed, 11 May 2022 08:10:40 +0200 Subject: [PATCH 30/41] Fixed doctest. --- src/defaults.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaults.jl b/src/defaults.jl index 0f1950ef..42ca06bc 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -108,7 +108,7 @@ This follows the [Type hierarchy](@ref) of Simple Features. # Examples ```jldoctest; setup = :(using GeoInterface) julia> GeoInterface.subtrait(GeoInterface.LineStringTrait()) -GeoInterface.PointTrait +GeoInterface.AbstractPointTrait julia> GeoInterface.subtrait(GeoInterface.PolygonTrait()) # Any of LineStringTrait, LineTrait, LinearRingTrait GeoInterface.AbstractLineStringTrait ``` From fa4a26d0af3226c3ed73e5934cf89f8767e154ee Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Thu, 12 May 2022 16:11:58 +0200 Subject: [PATCH 31/41] Fixed bugs in nested iterator. Expanded tests and enabled code coverage. --- .github/workflows/CI.yml | 4 +++ src/GeoInterface.jl | 2 ++ src/defaults.jl | 17 +++++---- src/utils.jl | 2 +- test/test_primitives.jl | 78 +++++++++++++++++++++++++++++++++------- 5 files changed, 83 insertions(+), 20 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 67fad79d..8dfc474d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,6 +34,10 @@ jobs: - 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@v2 + with: + files: lcov.info docs: name: Documentation runs-on: ubuntu-latest diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index fd379612..e01b0f30 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -1,5 +1,7 @@ module GeoInterface +using Base.Iterators + include("types.jl") include("interface.jl") include("defaults.jl") diff --git a/src/defaults.jl b/src/defaults.jl index 42ca06bc..14be4ef8 100644 --- a/src/defaults.jl +++ b/src/defaults.jl @@ -19,7 +19,7 @@ ismeasured(::AbstractPointTrait, geom) = :M in coordnames(geom) isempty(T, geom) = false ## Points -ngeom(::AbstractPointTrait, geom)::Integer = 0 +ngeom(::AbstractPointTrait, geom) = 0 getgeom(::AbstractPointTrait, geom) = nothing getgeom(::AbstractPointTrait, geom, i) = nothing @@ -37,24 +37,24 @@ 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, i) = getring(t, geom, i + 1) -npoint(p::AbstractPolygonTrait, geom) = sum(npoint(p) for p in getring(t, geom)) -getpoint(t::AbstractPolygonTrait, geom) = (p for p in getpoint(r) for r in getring(t, geom)) +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)) ## MultiLineString nlinestring(t::AbstractMultiLineStringTrait, geom) = ngeom(t, geom) getlinestring(t::AbstractMultiLineStringTrait, geom) = getgeom(t, geom) getlinestring(t::AbstractMultiLineStringTrait, geom, i) = getgeom(t, geom, i) npoint(t::AbstractMultiLineStringTrait, geom) = sum(npoint(ls) for ls in getgeom(t, geom)) -getpoint(t::AbstractMultiLineStringTrait, geom) = (p for p in getpoint(ls) for ls in getgeom(t, geom)) +getpoint(t::AbstractMultiLineStringTrait, 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) = (r for r in getring(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) = (p for p in getpoint(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) @@ -68,10 +68,15 @@ getcoord(t::AbstractPointTrait, geom) = (getcoord(t, geom, i) for i in 1:ncoord( ## Npoints npoint(::LineTrait, _) = 2 npoint(::TriangleTrait, _) = 3 +nring(::TriangleTrait, _) = 1 npoint(::RectangleTrait, _) = 4 +nring(::RectangleTrait, _) = 1 npoint(::QuadTrait, _) = 4 +nring(::QuadTrait, _) = 1 npoint(::PentagonTrait, _) = 5 +nring(::PentagonTrait, _) = 1 npoint(::HexagonTrait, _) = 6 +nring(::HexagonTrait, _) = 1 issimple(::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)]) diff --git a/src/utils.jl b/src/utils.jl index b7106cab..fa820413 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -30,7 +30,7 @@ end """Test whether the required interface for your `feature` has been implemented correctly.""" function testfeature(feature) try - @assert isfeature(geom) + @assert isfeature(feature) geom = geometry(feature) @assert isgeometry(geom) props = properties(feature) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index f9158635..41e69228 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -2,32 +2,84 @@ @testset "Developer" begin # Implement interface - struct MyCurve end struct MyPoint end + struct MyCurve end + struct MyPolygon end + struct MyMultiPoint end + struct MyMultiCurve end + struct MyMultiPolygon end + struct MyCollection end + GeoInterface.isgeometry(::MyPoint) = true - GeoInterface.isgeometry(::MyCurve) = true GeoInterface.geomtype(::MyPoint) = GeoInterface.PointTrait() - GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() GeoInterface.ncoord(::GeoInterface.PointTrait, geom::MyPoint) = 2 GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint, i) = [1, 2][i] + + GeoInterface.isgeometry(::MyCurve) = true + GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom - # Test validity - geom = MyCurve() - @test testgeometry(geom) - @test !isnothing(GeoInterface.convert(MyCurve, geom)) + GeoInterface.isgeometry(::MyPolygon) = true + GeoInterface.geomtype(::MyPolygon) = GeoInterface.PolygonTrait() + GeoInterface.ngeom(::GeoInterface.PolygonTrait, geom::MyPolygon) = 2 + GeoInterface.getgeom(::GeoInterface.PolygonTrait, geom::MyPolygon, i) = MyCurve() + + + @testset "Point" begin + geom = MyPoint() + @test GeoInterface.testgeometry(geom) + @test GeoInterface.x(geom) === 1 + @test GeoInterface.y(geom) === 2 + @test GeoInterface.ncoord(geom) === 2 + end - # Check functions - @test GeoInterface.npoint(geom) == 2 # defaults to ngeom - @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] - @test_throws MethodError GeoInterface.area(geom) - point = GeoInterface.getgeom(geom, 1) - @test GeoInterface.y(point) == 2 + @testset "LineString" begin + geom = MyCurve() + @test GeoInterface.testgeometry(geom) + @test !isnothing(GeoInterface.convert(MyCurve, geom)) + + @test GeoInterface.npoint(geom) == 2 # defaults to ngeom + @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] + @test_throws MethodError GeoInterface.area(geom) + point = GeoInterface.getpoint(geom, 1) + @test GeoInterface.y(point) == 2 + end + + @testset "Polygon" begin + geom = MyPolygon() + @test GeoInterface.testgeometry(geom) + + @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.gethole(geom, 1) + line = GeoInterface.getexterior(geom) + @test GeoInterface.npoint(geom) == 4 + @test collect(GeoInterface.getpoint(geom)) == [MyPoint(), MyPoint(), MyPoint(), MyPoint()] + end end @testset "Defaults" begin @test GeoInterface.subtrait(GeoInterface.TINTrait()) == GeoInterface.TriangleTrait end + +@testset "Feature" begin + struct Row end + struct Point end + + GeoInterface.isgeometry(::Point) = true + GeoInterface.geomtype(::Point) = GeoInterface.PointTrait() + GeoInterface.ncoord(::GeoInterface.PointTrait, geom::Point) = 2 + GeoInterface.getcoord(::GeoInterface.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 From 4630f13cb6df21bf6b45c27351a1e1ffacd6dee3 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 13:33:23 +0200 Subject: [PATCH 32/41] rename defaults.jl to fallbacks.jl This fits the header description better, and is the same filename as used for Tables.jl --- src/GeoInterface.jl | 2 +- src/{defaults.jl => fallbacks.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{defaults.jl => fallbacks.jl} (100%) diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index e01b0f30..20df9d1c 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -4,7 +4,7 @@ using Base.Iterators include("types.jl") include("interface.jl") -include("defaults.jl") +include("fallbacks.jl") include("utils.jl") export testgeometry diff --git a/src/defaults.jl b/src/fallbacks.jl similarity index 100% rename from src/defaults.jl rename to src/fallbacks.jl From 2c00af7fb772003f4f6b50e2e446fe3790ec492c Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 13:34:35 +0200 Subject: [PATCH 33/41] editing --- .gitignore | 1 - README.md | 15 +++++---- find_integrations.jl | 4 +-- src/GeoInterface.jl | 13 ++------ src/fallbacks.jl | 6 ++-- src/interface.jl | 15 ++++----- src/types.jl | 72 ++++++++++++++++++++++---------------------- 7 files changed, 61 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index f459254d..e5a16dee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ /docs/build/ .DS_Store docs/src/reference/integrations.md - diff --git a/README.md b/README.md index 980bcab9..c51e7a95 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,16 @@ [![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) # GeoInterface -An interface for geospatial vector data in Julia +An interface for geospatial vector data in [Julia](https://julialang.org/). -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, but for geometries instead of tables. +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. -Packages which support the GeoInterface.jl interface can be found in [INTEGRATIONS.md](INTEGRATIONS.md). +Packages which support the GeoInterface.jl interface can be found in +[INTEGRATIONS.md](INTEGRATIONS.md). We thank Julia Computing for supporting contributions to this package. diff --git a/find_integrations.jl b/find_integrations.jl index 14108a30..a4bdfb92 100644 --- a/find_integrations.jl +++ b/find_integrations.jl @@ -6,14 +6,14 @@ ### ### Usage ### -### 1. ensure a development version of Tables.jl (`pkg> add GeoInterface`) +### 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 Tables.jl +# find each package w/ a direct dependency on GeoInterface.jl general = joinpath(DEPOT, "General") mkpath(general) # run(`tar -xzf General.tar.gz -C $general`) diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index 20df9d1c..6dda5923 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -1,19 +1,12 @@ module GeoInterface -using Base.Iterators +using Base.Iterators: flatten + +export testgeometry, isgeometry, geomtype, ncoord, getcoord, ngeom, getgeom include("types.jl") include("interface.jl") include("fallbacks.jl") include("utils.jl") -export testgeometry -export isgeometry - -export geomtype -export ncoord -export getcoord -export ngeom -export getgeom - end # module diff --git a/src/fallbacks.jl b/src/fallbacks.jl index 14be4ef8..25b7c3f6 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -1,5 +1,5 @@ # Defaults for many of the interface functions are defined here as fallback. -# Methods here should take a type as first argument and should already be defined +# 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 @@ -107,8 +107,8 @@ end """ 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. +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) diff --git a/src/interface.jl b/src/interface.jl index 45df4dec..e1fe99bb 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -2,7 +2,7 @@ """ GeoInterface.isgeometry(x) => Bool -Check if an object `x` is a geometry and thus implicitely supports GeoInterface methods. +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. @@ -13,12 +13,13 @@ isgeometry(::Type{T}) where {T} = false """ GeoInterface.isfeature(x) => Bool -Check if an object `x` is a feature and thus implicitely supports some GeoInterface methods. +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 the older GeoInterface. + +Ensures backwards compatibility with GeoInterface version 0. """ isfeature(x::T) where {T} = isfeature(T) isfeature(::Type{T}) where {T} = false @@ -27,7 +28,7 @@ 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 the older GeoInterface. +Ensures backwards compatibility with GeoInterface version 0. """ geometry(feat) = nothing @@ -35,7 +36,7 @@ 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 the older GeoInterface. +Ensures backwards compatibility with GeoInterface version 0. """ properties(feat) = nothing @@ -375,7 +376,7 @@ extent(geom) = extent(geomtype(geom), geom) Alias for [`extent`](@ref), for compatibility with GeoJSON and the Python geointerface. -Ensures backwards compatibility with the older GeoInterface. +Ensures backwards compatibility with GeoInterface version 0. """ bbox(geom) = extent(geom) @@ -550,7 +551,7 @@ ismeasured(geom) = ismeasured(geomtype(geom), geom) coordinates(geom) -> Vector Return (an iterator of) point coordinates. -Ensures backwards compatibility with the older GeoInterface. +Ensures backwards compatibility with GeoInterface version 0. """ coordinates(geom) = coordinates(geomtype(geom), geom) diff --git a/src/types.jl b/src/types.jl index a5e44f75..eb79fee4 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,81 +1,81 @@ -"""An AbstractGeometryTrait type for all geometries.""" +"An AbstractGeometryTrait type for all geometries." abstract type AbstractGeometryTrait end -"""An AbstractGeometryCollectionTrait type for all geometrycollections.""" +"An AbstractGeometryCollectionTrait type for all geometrycollections." abstract type AbstractGeometryCollectionTrait <: AbstractGeometryTrait end -"""A GeometryCollection is a collection of `Geometry`s.""" +"A GeometryCollection is a collection of `Geometry`s." struct GeometryCollectionTrait <: AbstractGeometryCollectionTrait end -"""An AbstractPointTrait for all points.""" +"An AbstractPointTrait for all points." abstract type AbstractPointTrait <: AbstractGeometryTrait end -"""A single point.""" +"A single point." struct PointTrait <: AbstractPointTrait end -"""An AbstractCurveTrait type for all curves.""" +"An AbstractCurveTrait type for all curves." abstract type AbstractCurveTrait <: AbstractGeometryTrait end -"""An AbstractLineString type for all linestrings.""" +"An AbstractLineString type for all linestrings." abstract type AbstractLineStringTrait <: AbstractCurveTrait end -"""A LineStringTrait is a collection of straight lines between its `PointTrait`s.""" +"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.""" +"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.""" +"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 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.""" +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.""" +"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.""" +"An AbstractSurfaceTrait type for all surfaces." abstract type AbstractSurfaceTrait <: AbstractGeometryTrait end -"""An AbstractCurvePolygonTrait type for all curved polygons.""" +"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.""" +"An [`AbstractCurvePolygonTrait`](@ref) that can contain either circular or straight curves as rings." struct CurvePolygonTrait <: AbstractCurvePolygonTrait end -"""An AbstractPolygonTrait type for all polygons.""" +"An AbstractPolygonTrait type for all polygons." abstract type AbstractPolygonTrait <: AbstractCurvePolygonTrait end -"""An [`AbstractSurfaceTrait`](@ref) with straight rings either as exterior or interior(s).""" +"An [`AbstractSurfaceTrait`](@ref) with straight rings either as exterior or interior(s)." struct PolygonTrait <: AbstractPolygonTrait end -"""A [`PolygonTrait`](@ref) that is triangular.""" +"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.""" +"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.""" +"A [`PolygonTrait`](@ref) with four vertices." struct QuadTrait <: AbstractPolygonTrait end -"""A [`PolygonTrait`](@ref) with five vertices.""" +"A [`PolygonTrait`](@ref) with five vertices." struct PentagonTrait <: AbstractPolygonTrait end -"""A [`PolygonTrait`](@ref) with six vertices.""" +"A [`PolygonTrait`](@ref) with six vertices." struct HexagonTrait <: AbstractPolygonTrait end -"""An AbstractPolyHedralSurfaceTrait type for all polyhedralsurfaces.""" +"An AbstractPolyHedralSurfaceTrait type for all polyhedralsurfaces." abstract type AbstractPolyHedralSurfaceTrait <: AbstractSurfaceTrait end -"""A PolyHedralSurfaceTrait is a connected surface consisting of [`PolygonTrait`](@ref)s.""" +"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.""" +"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.""" +"An AbstractMultiPointTrait type for all multipoints." abstract type AbstractMultiPointTrait <: AbstractGeometryCollectionTrait end -"""A MultiPointTrait is a collection of [`PointTrait`](@ref)s.""" +"A MultiPointTrait is a collection of [`PointTrait`](@ref)s." struct MultiPointTrait <: AbstractMultiPointTrait end -"""An AbstractMultiCurveTrait type for all multicurves.""" +"An AbstractMultiCurveTrait type for all multicurves." abstract type AbstractMultiCurveTrait <: AbstractGeometryCollectionTrait end -"""A MultiCurveTrait is a collection of [`CircularStringTrait`](@ref)s.""" +"A MultiCurveTrait is a collection of [`CircularStringTrait`](@ref)s." struct MultiCurveTrait <: AbstractMultiCurveTrait end -"""An AbstractMultiLineStringTrait type for all multilinestrings.""" +"An AbstractMultiLineStringTrait type for all multilinestrings." abstract type AbstractMultiLineStringTrait <: AbstractMultiCurveTrait end -"""A MultiLineStringTrait is a collection of [`LineStringTrait`](@ref)s.""" +"A MultiLineStringTrait is a collection of [`LineStringTrait`](@ref)s." struct MultiLineStringTrait <: AbstractMultiLineStringTrait end -"""An AbstractMultiSurfaceTrait type for all multisurfaces.""" +"An AbstractMultiSurfaceTrait type for all multisurfaces." abstract type AbstractMultiSurfaceTrait <: AbstractGeometryCollectionTrait end -"""A MultiSurfaceTrait is a collection of [`AbstractSurfaceTrait`](@ref)s.""" +"A MultiSurfaceTrait is a collection of [`AbstractSurfaceTrait`](@ref)s." struct MultiSurfaceTrait <: AbstractMultiSurfaceTrait end -"""An AbstractMultiPolygonTrait type for all multipolygons.""" +"An AbstractMultiPolygonTrait type for all multipolygons." abstract type AbstractMultiPolygonTrait <: AbstractMultiSurfaceTrait end -"""A MultiPolygonTrait is a collection of [`PolygonTrait`](@ref)s.""" +"A MultiPolygonTrait is a collection of [`PolygonTrait`](@ref)s." struct MultiPolygonTrait <: AbstractMultiPolygonTrait end From 69b38a709ac13e2405cd5a520f8bc65ee2bac537 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 13:41:19 +0200 Subject: [PATCH 34/41] replace argument _ with geom This makes it more clear what the unused argument is supposed to be. --- docs/src/guides/defaults.md | 12 ++++++------ src/fallbacks.jl | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index 41b2d95e..00873495 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -38,10 +38,10 @@ endpoint(geom) = getpoint(geom, length(geom)) 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, _) = 2 -npoint(::TriangleTrait, _) = 3 -npoint(::RectangleTrait, _) = 4 -npoint(::QuadTrait, _) = 4 -npoint(::PentagonTrait, _) = 5 -npoint(::HexagonTrait, _) = 6 +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/src/fallbacks.jl b/src/fallbacks.jl index 25b7c3f6..b15f41c8 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -66,17 +66,17 @@ getgeom(t::AbstractGeometryTrait, geom) = (getgeom(t, geom, i) for i in 1:ngeom( getcoord(t::AbstractPointTrait, geom) = (getcoord(t, geom, i) for i in 1:ncoord(t, geom)) ## Npoints -npoint(::LineTrait, _) = 2 -npoint(::TriangleTrait, _) = 3 -nring(::TriangleTrait, _) = 1 -npoint(::RectangleTrait, _) = 4 -nring(::RectangleTrait, _) = 1 -npoint(::QuadTrait, _) = 4 -nring(::QuadTrait, _) = 1 -npoint(::PentagonTrait, _) = 5 -nring(::PentagonTrait, _) = 1 -npoint(::HexagonTrait, _) = 6 -nring(::HexagonTrait, _) = 1 +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 issimple(::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)]) From 7bef23760f85907be2968cbf691a40c4fe0559e2 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 13:51:59 +0200 Subject: [PATCH 35/41] drop module name for exported functions --- test/test_primitives.jl | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 41e69228..78fa613b 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -1,4 +1,5 @@ - +using GeoInterface +using Test @testset "Developer" begin # Implement interface @@ -19,7 +20,7 @@ GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() - GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom + convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom GeoInterface.isgeometry(::MyPolygon) = true GeoInterface.geomtype(::MyPolygon) = GeoInterface.PolygonTrait() @@ -29,15 +30,15 @@ @testset "Point" begin geom = MyPoint() - @test GeoInterface.testgeometry(geom) + @test testgeometry(geom) @test GeoInterface.x(geom) === 1 @test GeoInterface.y(geom) === 2 - @test GeoInterface.ncoord(geom) === 2 + @test ncoord(geom) === 2 end @testset "LineString" begin geom = MyCurve() - @test GeoInterface.testgeometry(geom) + @test testgeometry(geom) @test !isnothing(GeoInterface.convert(MyCurve, geom)) @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @@ -49,7 +50,7 @@ @testset "Polygon" begin geom = MyPolygon() - @test GeoInterface.testgeometry(geom) + @test testgeometry(geom) @test GeoInterface.nring(geom) == 2 @test GeoInterface.nhole(geom) == 1 @@ -71,10 +72,10 @@ end struct Row end struct Point end - GeoInterface.isgeometry(::Point) = true - GeoInterface.geomtype(::Point) = GeoInterface.PointTrait() - GeoInterface.ncoord(::GeoInterface.PointTrait, geom::Point) = 2 - GeoInterface.getcoord(::GeoInterface.PointTrait, geom::Point, i) = [1, 2][i] + isgeometry(::Point) = true + geomtype(::Point) = GeoInterface.PointTrait() + ncoord(::GeoInterface.PointTrait, geom::Point) = 2 + getcoord(::GeoInterface.PointTrait, geom::Point, i) = [1, 2][i] GeoInterface.isfeature(::Row) = true GeoInterface.geometry(r::Row) = Point() From d1d95898fe1d13d749b0d6b57a744abd70f06f44 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 14:21:54 +0200 Subject: [PATCH 36/41] fix tests --- test/test_primitives.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 78fa613b..9c837252 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -20,7 +20,7 @@ using Test GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() - convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom + Base.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom GeoInterface.isgeometry(::MyPolygon) = true GeoInterface.geomtype(::MyPolygon) = GeoInterface.PolygonTrait() @@ -39,7 +39,7 @@ using Test @testset "LineString" begin geom = MyCurve() @test testgeometry(geom) - @test !isnothing(GeoInterface.convert(MyCurve, geom)) + @test !isnothing(convert(MyCurve, geom)) @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] @@ -72,10 +72,10 @@ end struct Row end struct Point end - isgeometry(::Point) = true - geomtype(::Point) = GeoInterface.PointTrait() - ncoord(::GeoInterface.PointTrait, geom::Point) = 2 - getcoord(::GeoInterface.PointTrait, geom::Point, i) = [1, 2][i] + GeoInterface.isgeometry(::Point) = true + GeoInterface.geomtype(::Point) = GeoInterface.PointTrait() + GeoInterface.ncoord(::GeoInterface.PointTrait, geom::Point) = 2 + GeoInterface.getcoord(::GeoInterface.PointTrait, geom::Point, i) = [1, 2][i] GeoInterface.isfeature(::Row) = true GeoInterface.geometry(r::Row) = Point() From f98a1397e30c6c9bd04727f56d6496e8a87e0bca Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sun, 15 May 2022 14:17:26 +0200 Subject: [PATCH 37/41] export the traits --- docs/src/guides/developer.md | 74 ++++++++++++++++++------------------ docs/src/tutorials/usage.md | 4 +- src/GeoInterface.jl | 37 ++++++++++++++++++ src/fallbacks.jl | 12 +++--- src/interface.jl | 2 +- test/test_primitives.jl | 28 +++++++------- 6 files changed, 97 insertions(+), 60 deletions(-) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index cef0a43b..3c7e581e 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -15,7 +15,7 @@ Last but not least, we also provide an interface for features--geometries with p ```julia GeoInterface.isgeometry(geom::customgeom)::Bool = true -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.XTrait() # <: AbstractGeometryTrait +GeoInterface.geomtype(geom::customgeom)::DataType = XTrait() # <: AbstractGeometryTrait # for PointTraits GeoInterface.ncoord(geomtype(geom), geom::customgeom)::Integer GeoInterface.getcoord(geomtype(geom), geom::customgeom, i)::Real @@ -93,79 +93,79 @@ GeoInterface.isgeometry(geom::customgeom)::Bool = true A `geom::customgeom` with "Point"-like traits implements ```julia -GeoInterface.geomtype(geom::customgeom)::DataType = GeoInterface.PointTrait() -GeoInterface.ncoord(::GeoInterface.PointTrait, geom::customgeom)::Integer -GeoInterface.getcoord(::GeoInterface.PointTrait, geom::customgeom, i)::Real +GeoInterface.geomtype(geom::customgeom)::DataType = PointTrait() +GeoInterface.ncoord(::PointTrait, geom::customgeom)::Integer +GeoInterface.getcoord(::PointTrait, geom::customgeom, i)::Real # Defaults -GeoInterface.ngeom(::GeoInterface.PointTrait, geom)::Integer = 0 -GeoInterface.getgeom(::GeoInterface.PointTrait, geom::customgeom, i) = nothing +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 = GeoInterface.LineStringTrait() -GeoInterface.ncoord(::GeoInterface.LineStringTrait, geom::customgeom)::Integer +GeoInterface.geomtype(geom::customgeom)::DataType = LineStringTrait() +GeoInterface.ncoord(::LineStringTrait, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::customgeom, i) # of geomtype Point +GeoInterface.ngeom(::LineStringTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::LineStringTrait, geom::customgeom, i) # of geomtype Point # Optional -GeoInterface.isclosed(::GeoInterface.LineStringTrait, geom::customgeom)::Bool -GeoInterface.issimple(::GeoInterface.LineStringTrait, geom::customgeom)::Bool -GeoInterface.length(::GeoInterface.LineStringTrait, geom::customgeom)::Real +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 = GeoInterface.PolygonTrait() -GeoInterface.ncoord(::GeoInterface.PolygonTrait, geom::customgeom)::Integer +GeoInterface.geomtype(geom::customgeom)::DataType = PolygonTrait() +GeoInterface.ncoord(::PolygonTrait, geom::customgeom)::Integer # These alias for nring and getring -GeoInterface.ngeom(::GeoInterface.PolygonTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.PolygonTrait, geom::customgeom, i)::"LineStringTrait" +GeoInterface.ngeom(::PolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::PolygonTrait, geom::customgeom, i)::"LineStringTrait" # Optional -GeoInterface.area(::GeoInterface.PolygonTrait, geom::customgeom)::Real -GeoInterface.centroid(::GeoInterface.PolygonTrait, geom::customgeom)::"PointTrait" -GeoInterface.pointonsurface(::GeoInterface.PolygonTrait, geom::customgeom)::"PointTrait" -GeoInterface.boundary(::GeoInterface.PolygonTrait, geom::customgeom)::"LineStringTrait" +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) = GeoInterface.GeometryCollectionTrait() -GeoInterface.ncoord(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer -GeoInterface.ngeom(::GeoInterface.GeometryCollectionTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.GeometryCollectionTrait,geom::customgeomm, i)::"GeometryTrait" +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) = GeoInterface.MultiPointTrait() -GeoInterface.ncoord(::GeoInterface.MultiPointTrait, geom::customgeom)::Integer +GeoInterface.geomtype(geom::customgeom) = MultiPointTrait() +GeoInterface.ncoord(::MultiPointTrait, geom::customgeom)::Integer # These alias for npoint and getpoint -GeoInterface.ngeom(::GeoInterface.MultiPointTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPointTrait, geom::customgeom, i)::"PointTrait" +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) = GeoInterface.MultiLineStringTrait() -GeoInterface.ncoord(::GeoInterface.MultiLineStringTrait, geom::customgeom)::Integer +GeoInterface.geomtype(geom::customgeom) = MultiLineStringTrait() +GeoInterface.ncoord(::MultiLineStringTrait, geom::customgeom)::Integer # These alias for nlinestring and getlinestring -GeoInterface.ngeom(::GeoInterface.MultiLineStringTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiLineStringTrait,geom::customgeomm, i)::"LineStringTrait" +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) = GeoInterface.MultiPolygonTrait() -GeoInterface.ncoord(::GeoInterface.MultiPolygonTrait, geom::customgeom)::Integer +GeoInterface.geomtype(geom::customgeom) = MultiPolygonTrait() +GeoInterface.ncoord(::MultiPolygonTrait, geom::customgeom)::Integer # These alias for npolygon and getpolygon -GeoInterface.ngeom(::GeoInterface.MultiPolygonTrait, geom::customgeom)::Integer -GeoInterface.getgeom(::GeoInterface.MultiPolygonTrait, geom::customgeom, i)::"PolygonTrait" +GeoInterface.ngeom(::MultiPolygonTrait, geom::customgeom)::Integer +GeoInterface.getgeom(::MultiPolygonTrait, geom::customgeom, i)::"PolygonTrait" ``` diff --git a/docs/src/tutorials/usage.md b/docs/src/tutorials/usage.md index bf6de7e2..c1f7bee4 100644 --- a/docs/src/tutorials/usage.md +++ b/docs/src/tutorials/usage.md @@ -41,10 +41,10 @@ julia> geom = createpolygon(...)::ArchGDAL.IGeometry # no idea about the interf julia> isgeometry(geom) True julia> geomtype(geom) -GeoInterface.PolygonTrait() +PolygonTrait() julia> ext = exterior(geom); julia> geomtype(ext) -GeoInterface.LineStringTrait() +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/src/GeoInterface.jl b/src/GeoInterface.jl index 6dda5923..41a05ee1 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -4,6 +4,43 @@ 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") diff --git a/src/fallbacks.jl b/src/fallbacks.jl index b15f41c8..c9e1675f 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -112,16 +112,16 @@ Gets the expected, possible abstract, (sub)trait for subgeometries (retrieved wi # Examples ```jldoctest; setup = :(using GeoInterface) -julia> GeoInterface.subtrait(GeoInterface.LineStringTrait()) -GeoInterface.AbstractPointTrait -julia> GeoInterface.subtrait(GeoInterface.PolygonTrait()) # Any of LineStringTrait, LineTrait, LinearRingTrait -GeoInterface.AbstractLineStringTrait +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(GeoInterface.PointTrait())) +julia> isnothing(GeoInterface.subtrait(PointTrait())) true -julia> isnothing(GeoInterface.subtrait(GeoInterface.GeometryCollectionTrait())) +julia> isnothing(GeoInterface.subtrait(GeometryCollectionTrait())) true ``` """ diff --git a/src/interface.jl b/src/interface.jl index e1fe99bb..9666d500 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -43,7 +43,7 @@ properties(feat) = nothing """ GeoInterface.geomtype(geom) => T <: AbstractGeometry -Returns the geometry type, such as [`GeoInterface.PolygonTrait`](@ref) or [`GeoInterface.PointTrait`](@ref). +Returns the geometry type, such as [`PolygonTrait`](@ref) or [`PointTrait`](@ref). """ geomtype(geom) = nothing diff --git a/test/test_primitives.jl b/test/test_primitives.jl index 9c837252..e37b6a24 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -12,20 +12,20 @@ using Test struct MyCollection end GeoInterface.isgeometry(::MyPoint) = true - GeoInterface.geomtype(::MyPoint) = GeoInterface.PointTrait() - GeoInterface.ncoord(::GeoInterface.PointTrait, geom::MyPoint) = 2 - GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint, i) = [1, 2][i] + GeoInterface.geomtype(::MyPoint) = PointTrait() + GeoInterface.ncoord(::PointTrait, geom::MyPoint) = 2 + GeoInterface.getcoord(::PointTrait, geom::MyPoint, i) = [1, 2][i] GeoInterface.isgeometry(::MyCurve) = true - GeoInterface.geomtype(::MyCurve) = GeoInterface.LineStringTrait() - GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 2 - GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve, i) = MyPoint() - Base.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom + GeoInterface.geomtype(::MyCurve) = LineStringTrait() + GeoInterface.ngeom(::LineStringTrait, geom::MyCurve) = 2 + GeoInterface.getgeom(::LineStringTrait, geom::MyCurve, i) = MyPoint() + Base.convert(::Type{MyCurve}, ::LineStringTrait, geom) = geom GeoInterface.isgeometry(::MyPolygon) = true - GeoInterface.geomtype(::MyPolygon) = GeoInterface.PolygonTrait() - GeoInterface.ngeom(::GeoInterface.PolygonTrait, geom::MyPolygon) = 2 - GeoInterface.getgeom(::GeoInterface.PolygonTrait, geom::MyPolygon, i) = MyCurve() + GeoInterface.geomtype(::MyPolygon) = PolygonTrait() + GeoInterface.ngeom(::PolygonTrait, geom::MyPolygon) = 2 + GeoInterface.getgeom(::PolygonTrait, geom::MyPolygon, i) = MyCurve() @testset "Point" begin @@ -65,7 +65,7 @@ using Test end @testset "Defaults" begin - @test GeoInterface.subtrait(GeoInterface.TINTrait()) == GeoInterface.TriangleTrait + @test GeoInterface.subtrait(TINTrait()) == TriangleTrait end @testset "Feature" begin @@ -73,9 +73,9 @@ end struct Point end GeoInterface.isgeometry(::Point) = true - GeoInterface.geomtype(::Point) = GeoInterface.PointTrait() - GeoInterface.ncoord(::GeoInterface.PointTrait, geom::Point) = 2 - GeoInterface.getcoord(::GeoInterface.PointTrait, geom::Point, i) = [1, 2][i] + 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() From ccd276a951badb8f02852127d8ddf5c667e31931 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 15 May 2022 17:10:15 +0200 Subject: [PATCH 38/41] Update .github/workflows/CI.yml Co-authored-by: Rafael Schouten --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8dfc474d..f875ddf6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,7 +19,7 @@ jobs: matrix: version: - "1" - - "1.7" + - "1.6" - "nightly" os: - ubuntu-latest From 2cc0e079099906211444ebc639ddfa56eca54df3 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Sun, 15 May 2022 22:25:42 +0200 Subject: [PATCH 39/41] Expanded tests. --- src/fallbacks.jl | 19 ++++++--- src/utils.jl | 9 ++++- test/test_primitives.jl | 85 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/fallbacks.jl b/src/fallbacks.jl index c9e1675f..d5e4aa4a 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -40,12 +40,17 @@ 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::AbstractMultiLineStringTrait, geom) = ngeom(t, geom) -getlinestring(t::AbstractMultiLineStringTrait, geom) = getgeom(t, geom) -getlinestring(t::AbstractMultiLineStringTrait, geom, i) = getgeom(t, geom, i) -npoint(t::AbstractMultiLineStringTrait, geom) = sum(npoint(ls) for ls in getgeom(t, geom)) -getpoint(t::AbstractMultiLineStringTrait, geom) = flatten((p for p in getpoint(ls)) for ls in getgeom(t, geom)) +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) @@ -65,7 +70,7 @@ getpatch(t::AbstractPolyHedralSurfaceTrait, geom, i::Integer) = getgeom(t, geom, 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)) -## Npoints +## Special geometries npoint(::LineTrait, geom) = 2 npoint(::TriangleTrait, geom) = 3 nring(::TriangleTrait, geom) = 1 @@ -102,6 +107,8 @@ 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 """ diff --git a/src/utils.jl b/src/utils.jl index fa820413..26d2461a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -15,13 +15,18 @@ function testgeometry(geom) g2 = getgeom(geom, 1) subtype = subtrait(type) if !isnothing(subtype) - @assert geomtype(g2) isa 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 - println("You're missing an implementation: $e") + if e isa MethodError + println("You're missing an implementation: $e") + else + throw(e) + end return false end return true diff --git a/test/test_primitives.jl b/test/test_primitives.jl index e37b6a24..cfcff672 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -20,13 +20,34 @@ using Test GeoInterface.geomtype(::MyCurve) = LineStringTrait() GeoInterface.ngeom(::LineStringTrait, geom::MyCurve) = 2 GeoInterface.getgeom(::LineStringTrait, geom::MyCurve, i) = MyPoint() - Base.convert(::Type{MyCurve}, ::LineStringTrait, geom) = geom + 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(::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(::MyCollection) = true + GeoInterface.geomtype(::MyCollection) = GeometryCollectionTrait() + GeoInterface.ngeom(::GeometryCollectionTrait, geom::MyCollection) = 2 + GeoInterface.getgeom(::GeometryCollectionTrait, geom::MyCollection, i) = MyCurve() + @testset "Point" begin geom = MyPoint() @@ -39,7 +60,6 @@ using Test @testset "LineString" begin geom = MyCurve() @test testgeometry(geom) - @test !isnothing(convert(MyCurve, geom)) @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] @@ -51,6 +71,7 @@ using Test @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 @@ -62,6 +83,50 @@ using Test @test collect(GeoInterface.getpoint(geom)) == [MyPoint(), MyPoint(), MyPoint(), MyPoint()] 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()] + 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 "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 @@ -84,3 +149,19 @@ end @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 + + Base.convert(T::Type{XPolygon}, geom::X) where {X} = Base.convert(T, geomtype(geom), geom) + + geom = MyCurve() + @test !isnothing(convert(MyCurve, geom)) + + @test_throws Exception convert(MyPolygon, geom) +end From a22db0981808d8c4962197213080206a00ce9dc6 Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 16 May 2022 08:36:01 +0200 Subject: [PATCH 40/41] Documented conversion interface. --- docs/src/guides/developer.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 3c7e581e..f020953b 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -39,6 +39,17 @@ 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 From b5f56756164956ba572a1de190d4226d35a8e8ed Mon Sep 17 00:00:00 2001 From: Maarten Pronk Date: Mon, 16 May 2022 13:39:27 +0200 Subject: [PATCH 41/41] Expanded test coverage more. --- docs/src/guides/defaults.md | 2 +- src/GeoInterface.jl | 4 +- src/fallbacks.jl | 24 +++---- src/interface.jl | 8 +-- src/types.jl | 12 ++-- test/test_primitives.jl | 133 ++++++++++++++++++++++++++++++++++-- 6 files changed, 153 insertions(+), 30 deletions(-) diff --git a/docs/src/guides/defaults.md b/docs/src/guides/defaults.md index 00873495..7157b7fa 100644 --- a/docs/src/guides/defaults.md +++ b/docs/src/guides/defaults.md @@ -13,7 +13,7 @@ Of note here are the `ngeom` and `getgeom` for each geometry type, which transla | [`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) | +| [`AbstractPolyhedralSurfaceTrait`](@ref) | [`npatch(geom)`](@ref) | [`getpatch(geom)`](@ref) | | [`AbstractGeometryCollectionTrait`](@ref) | [`ngeom(geom)`](@ref) | [`getgeom(geom)`](@ref) | ## Polygons diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index 41a05ee1..c83a5c97 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -27,8 +27,8 @@ export AbstractGeometryTrait, QuadTrait, PentagonTrait, HexagonTrait, - AbstractPolyHedralSurfaceTrait, - PolyHedralSurfaceTrait, + AbstractPolyhedralSurfaceTrait, + PolyhedralSurfaceTrait, TINTrait, AbstractMultiPointTrait, MultiPointTrait, diff --git a/src/fallbacks.jl b/src/fallbacks.jl index d5e4aa4a..7bb30fe9 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -28,7 +28,7 @@ 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, length(geom)) +endpoint(t::AbstractCurveTrait, geom) = getpoint(t, geom, npoint(geom)) ## Polygons nring(t::AbstractPolygonTrait, geom) = ngeom(t, geom) @@ -36,6 +36,7 @@ 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)) @@ -62,9 +63,9 @@ npoint(t::AbstractMultiPolygonTrait, geom) = sum(npoint(r) for r in getring(t, g 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) +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)) @@ -83,18 +84,15 @@ nring(::PentagonTrait, geom) = 1 npoint(::HexagonTrait, geom) = 6 nring(::HexagonTrait, geom) = 1 -issimple(::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)]) +# 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) -# TODO Only simple if it's also not intersecting itself, except for its endpoints -issimple(t::AbstractMultiCurveTrait, geom) = all(i -> issimple(getgeom(t, geom, i)), 1:ngeom(t, geom)) -isclosed(t::AbstractMultiCurveTrait, geom) = all(i -> isclosed(getgeom(t, geom, i)), 1:ngeom(t, geom)) +issimple(t::AbstractMultiPointTrait, geom) = allunique((getgeom(t, geom))) -issimple(t::AbstractMultiPointTrait, geom) = allunique((getgeom(t, geom, i) for i in 1:ngeom(t, geom))) -getpoint(t::AbstractMultiPointTrait, geom) = getgeom(t, geom) -getpoint(t::AbstractMultiPointTrait, geom, i) = getgeom(t, geom, i) +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 @@ -136,7 +134,7 @@ subtrait(::AbstractPointTrait) = nothing subtrait(::AbstractCurveTrait) = AbstractPointTrait subtrait(::AbstractCurvePolygonTrait) = AbstractCurveTrait subtrait(::AbstractPolygonTrait) = AbstractLineStringTrait -subtrait(::AbstractPolyHedralSurfaceTrait) = AbstractPolygonTrait +subtrait(::AbstractPolyhedralSurfaceTrait) = AbstractPolygonTrait subtrait(::TINTrait) = TriangleTrait subtrait(::AbstractMultiPointTrait) = AbstractPointTrait subtrait(::AbstractMultiLineStringTrait) = AbstractLineStringTrait diff --git a/src/interface.jl b/src/interface.jl index 9666d500..0c5d70a4 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -250,12 +250,12 @@ Note that this is only valid for [`AbstractPolygonTrait`](@ref)s. """ gethole(geom) = gethole(geomtype(geom), geom) -# PolyHedralSurface +# PolyhedralSurface """ npatch(geom) Returns the number of patches for the given `geom`. -Note that this is only valid for [`AbstractPolyHedralSurfaceTrait`](@ref)s. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. """ npatch(geom)::Integer = npatch(geomtype(geom), geom) @@ -263,7 +263,7 @@ 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. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. """ getpatch(geom, i::Integer) = getpatch(geomtype(geom), geom, i) @@ -271,7 +271,7 @@ 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. +Note that this is only valid for [`AbstractPolyhedralSurfaceTrait`](@ref)s. """ getpatch(geom) = getpatch(geomtype(geom), geom) diff --git a/src/types.jl b/src/types.jl index eb79fee4..89ccd459 100644 --- a/src/types.jl +++ b/src/types.jl @@ -50,12 +50,12 @@ 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 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 diff --git a/test/test_primitives.jl b/test/test_primitives.jl index cfcff672..09c88994 100644 --- a/test/test_primitives.jl +++ b/test/test_primitives.jl @@ -4,11 +4,14 @@ 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 @@ -16,6 +19,11 @@ using Test 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 @@ -28,6 +36,11 @@ using Test 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 @@ -43,18 +56,38 @@ using Test 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 @@ -63,9 +96,18 @@ using Test @test GeoInterface.npoint(geom) == 2 # defaults to ngeom @test GeoInterface.coordinates(geom) == [[1, 2], [1, 2]] - @test_throws MethodError GeoInterface.area(geom) + 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 @@ -77,10 +119,20 @@ using Test @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 @@ -92,6 +144,8 @@ using Test 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 @@ -116,6 +170,17 @@ using Test @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) @@ -131,6 +196,8 @@ end @testset "Defaults" begin @test GeoInterface.subtrait(TINTrait()) == TriangleTrait + @test GeoInterface.nring(QuadTrait(), ()) == 1 + @test GeoInterface.npoint(QuadTrait(), ()) == 4 end @testset "Feature" begin @@ -158,10 +225,68 @@ end Base.convert(::Type{XCurve}, ::LineStringTrait, geom::XCurve) = geom # fast fallthrough Base.convert(::Type{XCurve}, ::LineStringTrait, geom) = geom - Base.convert(T::Type{XPolygon}, geom::X) where {X} = Base.convert(T, geomtype(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