diff --git a/src/geointerface.jl b/src/geointerface.jl index c8ebfd3a..61a6a171 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -10,6 +10,7 @@ const lookup_method = Dict{DataType,Function}( GeoInterface.MultiLineStringTrait => createmultilinestring, GeoInterface.PolygonTrait => createpolygon, GeoInterface.MultiPolygonTrait => createmultipolygon, + GeoInterface.GeometryCollectionTrait => creategeomcollection, ) let pointtypes = (wkbPoint, wkbPoint25D, wkbPointM, wkbPointZM), @@ -280,18 +281,25 @@ let pointtypes = (wkbPoint, wkbPoint25D, wkbPointM, wkbPointZM), return toWKT(geom) end - function GeoInterface.convert( - ::Type{T}, - type::GeometryTraits, - geom, - ) where {T<:IGeometry} - f = get(lookup_method, typeof(type), nothing) - isnothing(f) && error( - "Cannot convert an object of $(typeof(geom)) with the $(typeof(type)) trait (yet). Please report an issue.", - ) - return f(GeoInterface.coordinates(geom)) + function GeoInterface.convert(::Type{T}, trait::GeometryTraits, geom) where {T<:IGeometry} + f = get(lookup_method, typeof(trait), nothing) + isnothing(f) && _convert_error(geom, trait) + coords = GeoInterface.coordinates(geom) + return f(coords) end + function GeoInterface.convert(::Type{T}, trait::GeoInterface.GeometryCollectionTrait, collection) where {T<:IGeometry} + gdal_collection = creategeomcollection() + for geom in GeoInterface.getgeom(collection) + addgeom!(gdal_collection, to_gdal(geom)) + end + return gdal_collection + end + + @noinline _convert_error(geom, trait) = error( + "Cannot convert an object of $(typeof(geom)) with the $(typeof(trait)) trait (yet). Please report an issue at https://github.com/yeesian/ArchGDAL.jl if you need this", + ) + function GeoInterface.geomtrait( geom::Union{map(T -> AbstractGeometry{T}, pointtypes)...}, ) diff --git a/src/ogr/geometry.jl b/src/ogr/geometry.jl index a8471748..01e27410 100644 --- a/src/ogr/geometry.jl +++ b/src/ogr/geometry.jl @@ -1,3 +1,15 @@ +# Local convert method converts any GeoInterface geometry +# to the equivalent ArchGDAL geometry. +# `geom` can be points, so this is a hot path that needs to be fast. +function to_gdal(geom) + GeoInterface.isgeometry(geom) || _not_a_geom_error() + trait = GeoInterface.geomtrait(geom) + typ = geointerface_geomtype(trait) + return GeoInterface.convert(typ, trait, geom) +end + +@noinline _not_a_geom_error() = + throw(ArgumentError("Ojbect of type $(typeof(geom)) is not a GeoInterface compatible geometry")) """ fromWKB(data) @@ -110,6 +122,7 @@ function clone(geom::AbstractGeometry{T}) where {T} return IGeometry{T}(GDAL.ogr_g_clone(geom)) end end +clone(geom) = clone(to_gdal(geom)) function unsafe_clone(geom::AbstractGeometry{T}) where {T} if geom.ptr == C_NULL @@ -171,6 +184,7 @@ with the geometry multiple times, by storing caches of calculated geometry infor function preparegeom(geom::AbstractGeometry{T}) where {T} return IPreparedGeometry{T}(GDAL.ogrcreatepreparedgeometry(geom)) end +preparegeom(geom) = preparegeom(to_gdal(geom)) function unsafe_preparegeom(geom::AbstractGeometry{T}) where {T} return PreparedGeometry{T}(GDAL.ogrcreatepreparedgeometry(geom)) @@ -263,6 +277,7 @@ function envelope(geom::AbstractGeometry)::GDAL.OGREnvelope GDAL.ogr_g_getenvelope(geom, envelope) return envelope[] end +envelope(geom) = envelope(to_gdal(geom)) """ envelope3d(geom::AbstractGeometry) @@ -274,6 +289,7 @@ function envelope3d(geom::AbstractGeometry)::GDAL.OGREnvelope3D GDAL.ogr_g_getenvelope3d(geom, envelope) return envelope[] end +envelope3d(geom) = envelope3d(to_gdal(geom)) """ boundingbox(geom::AbstractGeometry) @@ -293,6 +309,7 @@ function boundingbox(geom::AbstractGeometry)::IGeometry [MinX, MaxY], ]) end +boundingbox(geom) = boundingbox(to_gdal(geom)) """ toWKB(geom::AbstractGeometry, order::OGRwkbByteOrder = wkbNDR) @@ -312,6 +329,7 @@ function toWKB( @ogrerr result "Failed to export geometry to WKB" return buffer end +toWKB(geom, order::OGRwkbByteOrder=wkbNDR) = toWKB(to_gdal(geom), order) """ toISOWKB(geom::AbstractGeometry, order::OGRwkbByteOrder = wkbNDR) @@ -331,6 +349,7 @@ function toISOWKB( @ogrerr result "Failed to export geometry to ISO WKB" return buffer end +toISOWKB(geom, order::OGRwkbByteOrder=wkbNDR) = toISOWKB(to_gdal(geom), order) """ wkbsize(geom::AbstractGeometry) @@ -338,6 +357,7 @@ end Returns size (in bytes) of related binary representation. """ wkbsize(geom::AbstractGeometry)::Integer = GDAL.ogr_g_wkbsize(geom) +wkbsize(geom) = wkbsize(to_gdal(geom)) """ toWKT(geom::AbstractGeometry) @@ -352,6 +372,7 @@ function toWKT(geom::AbstractGeometry)::String GDAL.vsifree(pointer(wkt_ptr[])) return wkt end +toWKT(geom) = toWKT(to_gdal(geom)) """ toISOWKT(geom::AbstractGeometry) @@ -366,6 +387,7 @@ function toISOWKT(geom::AbstractGeometry)::String GDAL.vsifree(pointer(isowkt_ptr[])) return wkt end +toISOWKT(geom) = toISOWKT(to_gdal(geom)) """ getgeomtype(geom::AbstractGeometry) @@ -386,6 +408,7 @@ function geomname(geom::AbstractGeometry)::Union{String,Missing} GDAL.ogr_g_getgeometryname(geom) end end +geomname(geom) = geomname(to_gdal(geom)) """ flattento2d!(geom::AbstractGeometry) @@ -400,6 +423,18 @@ function flattento2d!(geom::G)::G where {G<:AbstractGeometry} return geom end +""" + flattento2d(geom) + +Convert any GeoInterface compatible geometry to strictly 2D. + +A new geometry object will always be created. +""" +function flattento2d(geom::G)::G where {G<:AbstractGeometry} + flattento2d(clone(geom)) +end +flattento2d(geom) = flattento2d!(to_gdal(geom)) + """ closerings!(geom::AbstractGeometry) @@ -413,6 +448,18 @@ function closerings!(geom::G)::G where {G<:AbstractGeometry} return geom end +""" + closerings(geom) + +Convert any GeoInterface compatible geometry to strictly 2D. + +A new geometry object will always be created. +""" +function closerings(geom::G)::G where {G<:AbstractGeometry} + closerings(clone(geom)) +end +closerings(geom) = closerings!(to_gdal(geom)) + """ fromGML(data) @@ -430,19 +477,21 @@ fromGML(data)::IGeometry = IGeometry(GDAL.ogr_g_createfromgml(data)) unsafe_fromGML(data)::Geometry = Geometry(GDAL.ogr_g_createfromgml(data)) """ - toGML(geom::AbstractGeometry) + toGML(geom) -Convert a geometry into GML format. +Convert any GeoInterface compatible geometry into GML format. """ toGML(geom::AbstractGeometry)::String = GDAL.ogr_g_exporttogml(geom) +toGML(geom) = toGML(to_gdal(geom)) """ - toKML(geom::AbstractGeometry, altitudemode = C_NULL) + toKML(geom, altitudemode = C_NULL) -Convert a geometry into KML format. +Convert any GeoInterface compatible geometry into KML format. """ toKML(geom::AbstractGeometry, altitudemode = C_NULL)::String = GDAL.ogr_g_exporttokml(geom, altitudemode) +toKML(geom) = toKML(to_gdal(geom)) # ↑ * `altitudemode`: value to write in altitudeMode element, or NULL. """ @@ -467,9 +516,11 @@ A GeoJSON fragment or NULL in case of error. """ toJSON(geom::AbstractGeometry; kwargs...)::String = GDAL.ogr_g_exporttojsonex(geom, String["$k=$v" for (k, v) in kwargs]) +toJSON(geom; kwargs...) = toJSON(to_gdal(geom); kwargs...) toJSON(geom::AbstractGeometry, options::Vector{String})::String = GDAL.ogr_g_exporttojsonex(geom, options) +toJSON(geom, options::Vector{String}) = toJSON(to_gdal(geom), options) """ fromJSON(data::String) @@ -550,6 +601,22 @@ function transform!( return geom end +""" + transform(geom, coordtransform::CoordTransform) + +Apply arbitrary coordinate transformation to geometry, +always creating a new object. + +### Parameters +* `geom`: any GeoInterface compatible geometry. +* `coordtransform`: transformation to apply. +""" +function transform(geom::G, coordtransform::CoordTransform)::G where {G<:AbstractGeometry} + transform!(clone(geom), coordtransform) +end +transform(geom, coordtransform::CoordTransform) = + transform!(to_gdal(geom), coordtransform) + # """ # Transform geometry to new spatial reference system. @@ -590,14 +657,15 @@ Compute a simplified geometry. """ simplify(geom::AbstractGeometry, tol::Real)::IGeometry = IGeometry(GDAL.ogr_g_simplify(geom, tol)) +simplify(geom, tol::Real) = simplify(to_gdal(geom), tol) unsafe_simplify(geom::AbstractGeometry, tol::Real)::Geometry = Geometry(GDAL.ogr_g_simplify(geom, tol)) """ - simplifypreservetopology(geom::AbstractGeometry, tol::Real) + simplifypreservetopology(geom, tol::Real) -Simplify the geometry while preserving topology. +Simplify a GeoInterface compatible geometry while preserving topology. ### Parameters * `geom`: the geometry. @@ -605,12 +673,13 @@ Simplify the geometry while preserving topology. """ simplifypreservetopology(geom::AbstractGeometry, tol::Real)::IGeometry = IGeometry(GDAL.ogr_g_simplifypreservetopology(geom, tol)) +simplifypreservetopology(geom, tol::Real) = simplifypreservetopology(to_gdal(geom), tol) unsafe_simplifypreservetopology(geom::AbstractGeometry, tol::Real)::Geometry = Geometry(GDAL.ogr_g_simplifypreservetopology(geom, tol)) """ - delaunaytriangulation(geom::AbstractGeometry, tol::Real, onlyedges::Bool) + delaunaytriangulation(geom, tol::Real, onlyedges::Bool) Return a Delaunay triangulation of the vertices of the geometry. @@ -627,6 +696,7 @@ function delaunaytriangulation( )::IGeometry return IGeometry(GDAL.ogr_g_delaunaytriangulation(geom, tol, onlyedges)) end +delaunaytriangulation(geom, tol::Real, onlyedges) = delaunaytriangulation(to_gdal(geom), tol, onlyedges) function unsafe_delaunaytriangulation( geom::AbstractGeometry, @@ -654,7 +724,24 @@ function segmentize!(geom::G, maxlength::Real)::G where {G<:AbstractGeometry} end """ - intersects(g1::AbstractGeometry, g2::AbstractGeometry) + segmentize(geom, maxlength::Real) + +Modify a GeoInterface compatible geometry such it has no segment longer than the given distance. + +Interpolated points will have Z and M values (if needed) set to 0. Distance +computation is performed in 2d only + +### Parameters +* `geom`: the geometry to segmentize +* `maxlength`: the maximum distance between 2 points after segmentization +""" +function segmentize(geom::G, maxlength::Real)::G where {G<:AbstractGeometry} + segmentize!(clone(geom), maxlength) +end +segmentize(geom, maxlength::Real) = segmentize!(to_gdal(geom), maxlength) + +""" + intersects(g1, g2) Returns whether the geometries intersect @@ -664,75 +751,82 @@ boxes) of the two geometries overlap. """ intersects(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_intersects(g1, g2)) +intersects(g1, g2)::Bool = intersects(to_gdal(g1), to_gdal(g2)) intersects(g1::AbstractPreparedGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogrpreparedgeometryintersects(g1, g2)) """ - equals(g1::AbstractGeometry, g2::AbstractGeometry) + equals(g1, g2) Returns `true` if the geometries are equivalent. """ equals(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_equals(g1, g2)) +equals(g1, g2)::Bool = equals(to_gdal(g1), to_gdal(g2)) function Base.:(==)(g1::AbstractGeometry, g2::AbstractGeometry) return equals(g1, g2) end """ - disjoint(g1::AbstractGeometry, g2::AbstractGeometry) + disjoint(g1, g2) Returns `true` if the geometries are disjoint. """ disjoint(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_disjoint(g1, g2)) +disjoint(g1, g2)::Bool = disjoint(to_gdal(g1), to_gdal(g2)) """ - touches(g1::AbstractGeometry, g2::AbstractGeometry) + touches(g1, g2) Returns `true` if the geometries are touching. """ touches(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_touches(g1, g2)) +touches(g1, g2)::Bool = touches(to_gdal(g1), to_gdal(g2)) """ - crosses(g1::AbstractGeometry, g2::AbstractGeometry) + crosses(g1, g2) Returns `true` if the geometries are crossing. """ crosses(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_crosses(g1, g2)) +crosses(g1, g2)::Bool = crosses(to_gdal(g1), to_gdal(g2)) """ - within(g1::AbstractGeometry, g2::AbstractGeometry) + within(g1, g2) Returns `true` if g1 is contained within g2. """ within(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_within(g1, g2)) +within(g1, g2)::Bool = within(to_gdal(g1), to_gdal(g2)) """ - contains(g1::AbstractGeometry, g2::AbstractGeometry) + contains(g1, g2) Returns `true` if g1 contains g2. """ contains(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_contains(g1, g2)) - contains(g1::AbstractPreparedGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogrpreparedgeometrycontains(g1, g2)) +contains(g1, g2)::Bool = contains(to_gdal(g1), to_gdal(g2)) """ - overlaps(g1::AbstractGeometry, g2::AbstractGeometry) + overlaps(g1, g2) Returns `true` if the geometries overlap. """ overlaps(g1::AbstractGeometry, g2::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_overlaps(g1, g2)) +overlaps(g1, g2)::Bool = overlaps(to_gdal(g1), to_gdal(g2)) """ - boundary(geom::AbstractGeometry) + boundary(geom) Returns the boundary of the geometry. @@ -741,12 +835,13 @@ geometry on which the method is invoked. """ boundary(geom::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_boundary(geom)) +boundary(geom) = boundary(to_gdal(geom)) unsafe_boundary(geom::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_boundary(geom)) """ - convexhull(geom::AbstractGeometry) + convexhull(geom) Returns the convex hull of the geometry. @@ -755,12 +850,13 @@ geometry on which the method is invoked. """ convexhull(geom::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_convexhull(geom)) +convexhull(geom) = convexhull(to_gdal(geom)) unsafe_convexhull(geom::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_convexhull(geom)) """ - buffer(geom::AbstractGeometry, dist::Real, quadsegs::Integer = 30) + buffer(geom, dist::Real, quadsegs::Integer = 30) Compute buffer of geometry. @@ -784,6 +880,8 @@ accuracy of the result. """ buffer(geom::AbstractGeometry, dist::Real, quadsegs::Integer = 30)::IGeometry = IGeometry(GDAL.ogr_g_buffer(geom, dist, quadsegs)) +buffer(geom, dist::Real, quadsegs::Integer = 30) = + buffer(to_gdal(geom), dist, quadsegs) function unsafe_buffer( geom::AbstractGeometry, @@ -794,7 +892,7 @@ function unsafe_buffer( end """ - intersection(g1::AbstractGeometry, g2::AbstractGeometry) + intersection(g1, g2) Returns a new geometry representing the intersection of the geometries, or NULL if there is no intersection or an error occurs. @@ -805,6 +903,7 @@ two geometries intersect. """ intersection(g1::AbstractGeometry, g2::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_intersection(g1, g2)) +intersection(g1, g2) = intersection(to_gdal(g1), to_gdal(g2)) unsafe_intersection(g1::AbstractGeometry, g2::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_intersection(g1, g2)) @@ -816,6 +915,7 @@ Returns a new geometry representing the union of the geometries. """ union(g1::AbstractGeometry, g2::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_union(g1, g2)) +union(g1, g2) = union(to_gdal(g1), to_gdal(g2)) unsafe_union(g1::AbstractGeometry, g2::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_union(g1, g2)) @@ -832,12 +932,13 @@ multisurfaces (multipolygons). """ pointonsurface(geom::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_pointonsurface(geom)) +pointonsurface(g) = pointonsurface(to_gdal(g)) unsafe_pointonsurface(geom::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_pointonsurface(geom)) """ - difference(g1::AbstractGeometry, g2::AbstractGeometry) + difference(g1, g2) Generates a new geometry which is the region of this geometry with the region of the other geometry removed. @@ -848,43 +949,53 @@ if the difference is empty. """ difference(g1::AbstractGeometry, g2::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_difference(g1, g2)) +difference(g1, g2) = difference(to_gdal(g1), to_gdal(g2)) unsafe_difference(g1::AbstractGeometry, g2::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_difference(g1, g2)) """ - symdifference(g1::AbstractGeometry, g2::AbstractGeometry) + symdifference(g1, g2) Returns a new geometry representing the symmetric difference of the geometries or NULL if the difference is empty or an error occurs. """ symdifference(g1::AbstractGeometry, g2::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_symdifference(g1, g2)) +symdifference(g1, g2) = symdifference(to_gdal(g1), to_gdal(g2)) unsafe_symdifference(g1::AbstractGeometry, g2::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_symdifference(g1, g2)) """ - distance(g1::AbstractGeometry, g2::AbstractGeometry) + distance(g1, g2) Returns the distance between the geometries or -1 if an error occurs. """ distance(g1::AbstractGeometry, g2::AbstractGeometry)::Float64 = GDAL.ogr_g_distance(g1, g2) +distance(g1, g2) = distance(to_gdal(g1), to_gdal(g2)) """ - geomlength(geom::AbstractGeometry) + geomlength(geom) Returns the length of the geometry, or 0.0 for unsupported geometry types. """ geomlength(geom::AbstractGeometry)::Float64 = GDAL.ogr_g_length(geom) +geomlength(geom) = geomlength(to_gdal(geom)) +# TODO Add `length` method to match GeoInterface naming conventions? +# this will mean using `Base.length` everywhere """ - geomarea(geom::AbstractGeometry) + geomarea(geom) Returns the area of the geometry or 0.0 for unsupported geometry types. """ geomarea(geom::AbstractGeometry)::Float64 = GDAL.ogr_g_area(geom) +geomarea(geom) = geomarea(to_gdal(geom)) + +# Add `area` method to match GeoInterface naming conventions +area(geom) = geomarea(geom) """ centroid!(geom::AbstractGeometry, centroid::AbstractGeometry) @@ -910,7 +1021,7 @@ function centroid!( end """ - centroid(geom::AbstractGeometry) + centroid(geom) Compute the geometry centroid. @@ -928,6 +1039,7 @@ function centroid(geom::AbstractGeometry)::IGeometry centroid!(geom, point) return point end +centroid(geom) = centroid(to_gdal(geom)) function unsafe_centroid(geom::AbstractGeometry)::Geometry point = unsafe_createpoint() @@ -936,7 +1048,7 @@ function unsafe_centroid(geom::AbstractGeometry)::Geometry end """ - pointalongline(geom::AbstractGeometry, distance::Real) + pointalongline(geom, distance::Real) Fetch point at given distance along curve. @@ -950,6 +1062,7 @@ a point or NULL. """ pointalongline(geom::AbstractGeometry, distance::Real)::IGeometry = IGeometry(GDAL.ogr_g_value(geom, distance)) +pointalongline(geom, distance::Real) = pointalongline(to_gdal(geom), distance) unsafe_pointalongline(geom::AbstractGeometry, distance::Real)::Geometry = Geometry(GDAL.ogr_g_value(geom, distance)) @@ -1113,6 +1226,7 @@ ismeasured(geom::_AbstractGeometryNoM) = false Returns `true` if the geometry has no points, otherwise `false`. """ isempty(geom::AbstractGeometry)::Bool = GDAL.ogr_g_isempty(geom) != 0 +isempty(geom)::Bool = isempty(to_gdal(geom)) """ isvalid(geom::AbstractGeometry) @@ -1120,6 +1234,7 @@ isempty(geom::AbstractGeometry)::Bool = GDAL.ogr_g_isempty(geom) != 0 Returns `true` if the geometry is valid, otherwise `false`. """ isvalid(geom::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_isvalid(geom)) +isvalid(geom)::Bool = isvalid(to_gdal(geom)) """ issimple(geom::AbstractGeometry) @@ -1127,6 +1242,7 @@ isvalid(geom::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_isvalid(geom)) Returns `true` if the geometry is simple, otherwise `false`. """ issimple(geom::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_issimple(geom)) +issimple(geom)::Bool = issimple(to_gdal(geom)) """ isring(geom::AbstractGeometry) @@ -1134,6 +1250,7 @@ issimple(geom::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_issimple(geom)) Returns `true` if the geometry is a ring, otherwise `false`. """ isring(geom::AbstractGeometry)::Bool = Bool(GDAL.ogr_g_isring(geom)) +isring(geom)::Bool = isring(to_gdal(geom)) """ polygonize(geom::AbstractGeometry) @@ -1147,6 +1264,7 @@ impossible due to topological inconsistencies. """ polygonize(geom::AbstractGeometry)::IGeometry = IGeometry(GDAL.ogr_g_polygonize(geom)) +polygonize(geom) = polygonize(to_gdal(geom)) unsafe_polygonize(geom::AbstractGeometry)::Geometry = Geometry(GDAL.ogr_g_polygonize(geom)) @@ -1515,6 +1633,8 @@ function removegeom!( return geom end +removegeom(geom, i::Integer) = removegeom!(clone(geom), i, true) + """ removeallgeoms!(geom::AbstractGeometry, todelete::Bool = true) @@ -1563,7 +1683,6 @@ MULTICURVE or MULTISURFACE in it, by approximating curve geometries. * `options`: options as a null-terminated list of strings or NULL. See OGRGeometryFactory::curveToLineString() for valid options. """ - function lineargeom( geom::AbstractGeometry, stepsize::Real = 0; diff --git a/src/spatialref.jl b/src/spatialref.jl index 087f26e4..14dab5a9 100644 --- a/src/spatialref.jl +++ b/src/spatialref.jl @@ -169,6 +169,21 @@ function reproject( end end +function reproject( + geom, + sourcecrs::GFT.GeoFormat, + targetcrs::GFT.GeoFormat; + kwargs..., +) + if GI.isgeometry(geom) + reproject(to_gdal(geom), sourcecrs, targetcrs; kwargs...) + elseif geom isa AbstractArray && (length(geom) > 0) && GI.isgeometry(first(geom)) + reproject(to_gdal.(geom), Ref(sourcecrs), Ref(targetcrs); kwargs...) + else + throw(ArgumentError("geom is not a GeoInterface compatible geometry")) + end +end + """ crs2transform(f::Function, sourcecrs::GeoFormat, targetcrs::GeoFormat; kwargs...) diff --git a/test/test_geometry.jl b/test/test_geometry.jl index fdb85c5a..3d5f1b53 100644 --- a/test/test_geometry.jl +++ b/test/test_geometry.jl @@ -37,6 +37,7 @@ import GeoFormatTypes as GFT @testset "Create a Point" begin # Method 1 AG.createpoint(100, 70) do point + wrapped_point = GeoInterface.convert(GeoInterface, point) @test point isa AG.Geometry{AG.wkbPoint} @test isapprox(GI.coordinates(point), [100, 70], atol = 1e-6) @test AG.geomdim(point) == 0 @@ -115,14 +116,18 @@ import GeoFormatTypes as GFT 0x00, ] @test AG.toKML(point, "relativeToGround") == + AG.toKML(wrapped_point, "relativeToGround") == "relativeToGround" * "100,70,0" @test AG.toKML(point, "clampToGround") == + AG.toKML(wrapped_point, "clampToGround") == "clampToGround" * "100,70,0" @test AG.toKML(point) == + AG.toKML(wrapped_point) == "100,70,0" @test AG.toJSON(point) == + AG.toJSON(wrapped_point) == "{ \"type\": \"Point\", \"coordinates\": " * "[ 100.0, 70.0, 0.0 ] }" @test startswith( @@ -165,20 +170,31 @@ import GeoFormatTypes as GFT # Method 2 point = AG.createpoint(100, 70) + tuple_point = (100, 70) @test AG.geomdim(point) == 0 + @test AG.geomdim(tuple_point) == 0 @test AG.getcoorddim(point) == 2 AG.setcoorddim!(point, 3) @test AG.getcoorddim(point) == 3 @test AG.isvalid(point) == true + @test AG.isvalid(tuple_point) == true @test AG.issimple(point) == true + @test AG.issimple(tuple_point) == true @test AG.isring(point) == false + @test AG.isring(tuple_point) == false @test AG.getz(point, 0) == 0 @test typeof(point) == AG.IGeometry{AG.wkbPoint} @test sprint(print, AG.envelope(point)) == + sprint(print, AG.envelope(tuple_point)) == + "GDAL.OGREnvelope(100.0, 100.0, 70.0, 70.0)" + @test sprint(print, AG.envelope(tuple_point)) == "GDAL.OGREnvelope(100.0, 100.0, 70.0, 70.0)" @test sprint(print, AG.envelope3d(point)) == "GDAL.OGREnvelope3D(100.0, 100.0, 70.0, 70.0, 0.0, 0.0)" - @test AG.toISOWKB(point, AG.wkbNDR) == UInt8[ + @test sprint(print, AG.envelope3d(tuple_point)) == + "GDAL.OGREnvelope3D(100.0, 100.0, 70.0, 70.0, 0.0, 0.0)" + @test AG.toISOWKB(point, AG.wkbNDR) == + AG.toISOWKB(point, tuple_point) == UInt8[ 0x01, 0xe9, 0x03, @@ -209,7 +225,8 @@ import GeoFormatTypes as GFT 0x00, 0x00, ] - @test AG.toISOWKB(point, AG.wkbXDR) == UInt8[ + @test AG.toISOWKB(point, AG.wkbXDR) == + AG.toISOWKB(tuple_point, AG.wkbXDR) == UInt8[ 0x00, 0x00, 0x00, @@ -241,17 +258,22 @@ import GeoFormatTypes as GFT 0x00, ] @test AG.toKML(point, "relativeToGround") == + AG.toKML(tuple_point, "relativeToGround") == "relativeToGround" * "100,70,0" @test AG.toKML(point, "clampToGround") == + AG.toKML(tuple_point, "clampToGround") == "clampToGround" * "100,70,0" @test AG.toKML(point) == + AG.toKML(tuple_point) == "100,70,0" @test AG.toJSON(point) == + AG.toJSON(tuple_point) == "{ \"type\": \"Point\", \"coordinates\": [ 100.0, 70.0, 0.0 ] }" @test AG.equals(point, AG.createpoint(100, 70, 0)) == true @test AG.equals(point, AG.createpoint((100, 70, 0))) == true + @test AG.equals(tuple_point, AG.createpoint(100, 70, 0)) == true AG.flattento2d!(point) @test AG.getcoorddim(point) == 2 @test AG.getnonlineargeomflag() == true @@ -260,7 +282,9 @@ import GeoFormatTypes as GFT AG.setnonlineargeomflag!(true) @test AG.getnonlineargeomflag() == true AG.closerings!(point) + closed_tuple = AG.closerings(tuple_point) @test AG.toJSON(point) == + AG.toJSON(closed_tuple) == "{ \"type\": \"Point\", \"coordinates\": [ 100.0, 70.0 ] }" end @@ -673,19 +697,27 @@ import GeoFormatTypes as GFT [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], ) + wrapped_geom1 = GeoInterface.convert(GeoInterface, geom1) + wrapped_geom2 = GeoInterface.convert(GeoInterface, geom2) AG.closerings!(geom1) @test AG.disjoint(geom1, geom2) == false + @test AG.disjoint(wrapped_geom1, wrapped_geom2) == false @test AG.touches(geom1, geom2) == true + @test AG.touches(wrapped_geom1, wrapped_geom2) == true @test AG.crosses(geom1, geom2) == false + @test AG.crosses(wrapped_geom1, wrapped_geom2) == false @test AG.overlaps(geom1, geom2) == false + @test AG.overlaps(wrapped_geom1, wrapped_geom2) == false @test AG.toWKT(AG.boundary(geom2)) == "GEOMETRYCOLLECTION EMPTY" + @test AG.toWKT(AG.boundary(wrapped_geom2)) == "GEOMETRYCOLLECTION EMPTY" AG.boundary(geom2) do result @test AG.toWKT(result) == "GEOMETRYCOLLECTION EMPTY" end @test AG.toWKT(AG.union(geom1, geom2)) == + AG.toWKT(AG.union(wrapped_geom1, wrapped_geom2)) == "GEOMETRYCOLLECTION (" * "POLYGON (" * "(0 4 8,4 4 8,4 0 8,0 0 8,0 4 8)," * @@ -709,6 +741,7 @@ import GeoFormatTypes as GFT end @test AG.toWKT(AG.difference(geom1, geom2)) == + AG.toWKT(AG.difference(wrapped_geom1, wrapped_geom2)) == "MULTIPOLYGON (" * "((0 4 8,4 4 8,4 0 8,0 0 8,0 4 8)," * "(3 1 8,3 3 8,1 3 8,1 1 8,3 1 8))," * @@ -769,6 +802,7 @@ import GeoFormatTypes as GFT end @test AG.toWKT(AG.symdifference(geom1, geom2)) == + AG.toWKT(AG.symdifference(wrapped_geom1, wrapped_geom2)) == "GEOMETRYCOLLECTION (" * "POLYGON (" * "(0 4 8,4 4 8,4 0 8,0 0 8,0 4 8)," * @@ -813,6 +847,7 @@ import GeoFormatTypes as GFT "(11 1 8,13 1 8,13 3 8,11 3 8,11 1 8))," * "POINT EMPTY)", ) + wrapped_geom3 = GeoInterface.convert(GeoInterface, geom3) AG.clone(geom3) do geom4 @test sprint(print, AG.clone(geom3)) == "Geometry: GEOMETRYCOLLECTION (" * @@ -826,11 +861,25 @@ import GeoFormatTypes as GFT " ... MPTY)" @test typeof(geom4) == AG.Geometry{AG.wkbGeometryCollection25D} end + AG.clone(wrapped_geom3) do geom4 + @test sprint(print, AG.clone(geom3)) == + "Geometry: GEOMETRYCOLLECTION (" * + "POINT (2 5 8)," * + "POLYGON ((0 0 8," * + " ... MPTY)" + @test sprint(print, AG.clone(geom4)) == + "Geometry: GEOMETRYCOLLECTION (" * + "POINT (2 5 8)," * + "POLYGON ((0 0 8," * + " ... MPTY)" + @test typeof(geom4) == AG.Geometry{AG.wkbGeometryCollection25D} + end AG.clone(AG.getgeom(geom3, 3)) do geom4 @test sprint(print, geom4) == "Geometry: POINT EMPTY" end @test AG.toISOWKT(geom3) == + AG.toISOWKT(wrapped_geom3) == "GEOMETRYCOLLECTION Z (" * "POINT Z (2 5 8)," * "POLYGON Z (" * diff --git a/test/test_geos_operations.jl b/test/test_geos_operations.jl index bcc2e4af..2ad6f9e6 100644 --- a/test/test_geos_operations.jl +++ b/test/test_geos_operations.jl @@ -1,6 +1,9 @@ using Test import ArchGDAL as AG +import GeoInterface using Extents +const GI = GeoInterface + @testset "test_geos_operations.jl" begin function equivalent_to_wkt(geom::AG.Geometry, wkt::String) @@ -25,7 +28,8 @@ using Extents AG.pointalongline(ls, dist) do pt1 AG.createpoint(dest) do pt2 @test AG.toWKT(pt1) == AG.toWKT(pt2) - end + @test AG.toWKT(GI.convert(GI, pt1)) == AG.toWKT(GI.convert(GI, pt2)) + end end @test AG.toWKT(AG.pointalongline(ls, dist)) == AG.toWKT(AG.createpoint(dest)) @@ -38,6 +42,9 @@ using Extents AG.fromWKT("POLYGON EMPTY") do g2 @test AG.contains(g1, g2) == false @test AG.contains(g2, g1) == false + # Empty geometries dont work so well accross packages yet + @test_broken AG.contains(GI.convert(GI, g1), GI.convert(GI, g2)) == false + @test_broken AG.contains(GI.convert(GI, g2), GI.convert(GI, g1)) == false end end @@ -45,6 +52,8 @@ using Extents AG.fromWKT("POINT(2 2)") do g2 @test AG.contains(g1, g2) == true @test AG.contains(g2, g1) == false + @test AG.contains(GI.convert(GI, g1), g2) == true + @test AG.contains(GI.convert(GI, g2), g1) == false end end @@ -52,6 +61,8 @@ using Extents AG.fromWKT("POLYGON((1 1,1 2,2 2,2 1,1 1))") do g2 @test AG.contains(g1, g2) == true @test AG.contains(g2, g1) == false + @test AG.contains(g1, GI.convert(GI, g2)) == true + @test AG.contains(g2, GI.convert(GI, g1)) == false end end end @@ -65,7 +76,9 @@ using Extents @test AG.isempty(output) == false @test AG.toWKT(output) == AG.toWKT(expected) end - @test AG.toWKT(AG.convexhull(input)) == AG.toWKT(expected) + @test AG.toWKT(AG.convexhull(input)) == + AG.toWKT(AG.convexhull(GI.convert(GI, input))) == + AG.toWKT(expected) end end end @@ -77,29 +90,36 @@ using Extents @test AG.isempty(g2) == true @test AG.toWKT(g2) == "MULTILINESTRING EMPTY" end - @test AG.toWKT(AG.delaunaytriangulation(g1, 0, true)) == - "MULTILINESTRING EMPTY" + @test AG.toWKT(AG.delaunaytriangulation(g1, 0, true)) == "MULTILINESTRING EMPTY" + # Empty geometries dont work so well accross packages yet + @test_broken AG.toWKT(AG.delaunaytriangulation(GI.convert(GI, g1), 0, true)) == "MULTILINESTRING EMPTY" end AG.fromWKT("POINT(0 0)") do g1 AG.delaunaytriangulation(g1, 0, false) do g2 - @test AG.isempty(g2) == true - @test AG.toWKT(g2) == "GEOMETRYCOLLECTION EMPTY" + @test AG.isempty(g2) == AG.isempty(GI.convert(GI, g2)) == true + @test AG.toWKT(g2) == AG.toWKT(GI.convert(GI, g2)) == "GEOMETRYCOLLECTION EMPTY" end @test AG.toWKT(AG.delaunaytriangulation(g1, 0, false)) == + AG.toWKT(AG.delaunaytriangulation(GI.convert(GI, g1), 0, false)) == "GEOMETRYCOLLECTION EMPTY" end AG.fromWKT("MULTIPOINT(0 0, 5 0, 10 0)") do g1 AG.delaunaytriangulation(g1, 0, false) do g2 - @test AG.toWKT(g2) == "GEOMETRYCOLLECTION EMPTY" + @test AG.toWKT(g2) == + AG.toWKT(GI.convert(GI, g2)) == + "GEOMETRYCOLLECTION EMPTY" end AG.delaunaytriangulation(g1, 0, true) do g2 - @test AG.toWKT(g2) == "MULTILINESTRING ((5 0,10 0),(0 0,5 0))" + @test AG.toWKT(g2) == + AG.toWKT(GI.convert(GI, g2)) == + "MULTILINESTRING ((5 0,10 0),(0 0,5 0))" end end AG.fromWKT("MULTIPOINT(0 0, 10 0, 10 10, 11 10)") do g1 AG.delaunaytriangulation(g1, 2.0, true) do g2 - @test AG.toWKT(g2) == - "MULTILINESTRING ((0 0,10 10),(0 0,10 0),(10 0,10 10))" + @test AG.toWKT(g2) == + AG.toWKT(GI.convert(GI, g2)) == + "MULTILINESTRING ((0 0,10 10),(0 0,10 0),(10 0,10 10))" end end end @@ -108,6 +128,7 @@ using Extents AG.fromWKT("POINT(10 10)") do g1 AG.fromWKT("POINT(3 6)") do g2 @test AG.distance(g1, g2) ≈ 8.06225774829855 atol = 1e-12 + @test AG.distance(GI.convert(GI, g1), GI.convert(GI, g2)) ≈ 8.06225774829855 atol = 1e-12 end end end @@ -122,6 +143,10 @@ using Extents @test AG.toWKT(result) == wkt2 end @test AG.toWKT(f(geom)) == wkt2 + if parentmodule(f) != GeoInterface && GI.ngeom(geom) != 0 + wrapped_geom = GI.convert(GI, geom) + @test AG.toWKT(f(wrapped_geom)) == wkt2 + end end end @@ -164,6 +189,11 @@ using Extents @test AG.toWKT(result) == wkt3 end @test AG.toWKT(f(geom1, geom2)) == wkt3 + if parentmodule(f) != GeoInterface && GI.ngeom(geom1) != 0 && GI.ngeom(geom2) != 0 + wrapped_geom1 = GI.convert(GI, geom1) + wrapped_geom2 = GI.convert(GI, geom2) + @test AG.toWKT(f(wrapped_geom1, wrapped_geom2)) == wkt3 + end end end end @@ -176,91 +206,145 @@ using Extents AG.fromWKT(wkt1) do geom1 AG.fromWKT(wkt2) do geom2 @test AG.toWKT(f(geom1, geom2)) == wkt3 + if parentmodule(f) != GeoInterface && GI.ngeom(geom1) != 0 && GI.ngeom(geom2) != 0 + wrapped_geom1 = GI.convert(GI, geom1) + wrapped_geom2 = GI.convert(GI, geom2) + @test AG.toWKT(f(wrapped_geom1, wrapped_geom2)) == wkt3 + end end end end + function test_method_simple(f1::Function, f2::Function, args...) + test_method_simple(f1, args...) + test_method_simple(f2, args...) + end function test_predicate(f::Function, wkt1, wkt2, result::Bool) AG.fromWKT(wkt1) do geom1 AG.fromWKT(wkt2) do geom2 + # Test GDAL geoms @test f(geom1, geom2) == result + # Test GeoInterface geoms + if parentmodule(f) != GeoInterface && GI.ngeom(geom1) != 0 && GI.ngeom(geom2) != 0 + wrapped_geom1 = GI.convert(GI, geom1) + wrapped_geom2 = GI.convert(GI, geom2) + @test f(wrapped_geom1, wrapped_geom2) == result + end end end end + function test_predicate(f1::Function, f2::Function, wkt1, wkt2, result::Bool) + test_predicate(f1, wkt1, wkt2, result::Bool) + test_predicate(f2, wkt1, wkt2, result::Bool) + end - @testset "GeoInterface" begin + @testset "Predicates" begin test_predicate( - GI.intersects, + GI.intersects, AG.intersects, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", true, ) test_predicate( - GI.equals, + GI.equals, AG.equals, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", false, ) test_predicate( - GI.disjoint, + GI.disjoint, AG.disjoint, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", false, ) test_predicate( - GI.touches, + GI.touches, AG.touches, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(1 1)", true, ) test_predicate( - GI.crosses, + GI.crosses, AG.crosses, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", false, ) test_predicate( - GI.within, + GI.within, AG.within, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", false, ) test_predicate( - GI.contains, + GI.contains, AG.contains, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", true, ) test_predicate( - GI.overlaps, + GI.overlaps, AG.overlaps, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", false, ) + end + + @testset "Simple methods" begin test_method_simple( - GI.union, + GI.union, AG.union, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", "POLYGON ((1 5,5 5,5 1,1 1,1 5))", ) test_method_simple( - GI.intersection, + GI.intersection, AG.intersection, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", "POINT (2 2)", ) test_method_simple( - GI.difference, + GI.difference, AG.difference, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", "POLYGON ((1 5,5 5,5 1,1 1,1 5))", ) test_method_simple( - GI.symdifference, + GI.symdifference, AG.symdifference, "POLYGON((1 1,1 5,5 5,5 1,1 1))", "POINT(2 2)", "POLYGON ((1 5,5 5,5 1,1 1,1 5))", ) + end + + @testset "ArchGDAL methods" begin + @test AG.distance( + AG.fromWKT("POLYGON((1 1,1 5,5 5,5 1,1 1))"), + AG.fromWKT("POINT(2 2)"), + ) == 0.0 + + # This could be `length` to match the GeoInterface method + @test AG.geomlength(AG.fromWKT("LINESTRING(0 0, 10 0)")) == 10 + + @test AG.area(AG.fromWKT("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))")) == 100 + + @test GI.coordinates( + AG.buffer(AG.fromWKT("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"), 1), + )[1][1] == [-1.0, 0.0] + + @test AG.toWKT( + AG.convexhull(AG.fromWKT("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))")), + ) == "POLYGON ((0 0,0 10,10 10,10 0,0 0))" + + # @test AG.asbinary( + # AG.fromWKT("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"), + # )[1:10] == + # UInt8[0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05] + + # @test AG.astext(AG.fromWKT("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))")) == + # "POLYGON ((0 0,10 0,10 10,0 10,0 0))" + end + + @testset "GeoInterface methods" begin @test GI.distance( AG.fromWKT("POLYGON((1 1,1 5,5 5,5 1,1 1))"), AG.fromWKT("POINT(2 2)"), @@ -398,6 +482,7 @@ using Extents @test AG.toWKT(g2) == "POLYGON EMPTY" end @test AG.toWKT(AG.simplify(g1, 0.0)) == "POLYGON EMPTY" + @test AG.toWKT(AG.simplify(GI.convert(GI, g1), 0.0)) == "POLYGON EMPTY" end AG.fromWKT( @@ -408,6 +493,7 @@ using Extents "POLYGON ((56.5286666667 25.2101666667,56.529 25.2105,56.5288333333 25.2103333333,56.5286666667 25.2101666667))" end @test AG.toWKT(AG.simplifypreservetopology(g1, 43.2)) == + AG.toWKT(AG.simplifypreservetopology(GI.convert(GI, g1), 43.2)) == "POLYGON ((56.5286666667 25.2101666667,56.529 25.2105,56.5288333333 25.2103333333,56.5286666667 25.2101666667))" end end