diff --git a/Project.toml b/Project.toml index aeb7bdd..714f6ce 100644 --- a/Project.toml +++ b/Project.toml @@ -4,10 +4,12 @@ authors = ["JuliaGeo and contributors"] version = "1.3.8" [deps] +DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f" [compat] +DataAPI = "1" Extents = "0.1.1" GeoFormatTypes = "0.4" julia = "1" diff --git a/docs/src/guides/developer.md b/docs/src/guides/developer.md index 432e2a5..32f739e 100644 --- a/docs/src/guides/developer.md +++ b/docs/src/guides/developer.md @@ -86,6 +86,22 @@ GeoInterface.geometrycolumns(::customcollection) = (:geometry,) # can be multip The `geometrycolumns` enables other packages to know which field in a row, or column in a table, contains the geometry or geometries. +It's important to note that the `geometrycolumns` should always return a `Tuple` of `Symbol`s. However, it does have a fallback method +that uses [DataAPI.jl](https://github.com/JuliaData/DataAPI.jl) metadata, if it exists, to retrieve the geometry columns. This relies on +the `"GEOINTERFACE:geometrycolumns"` metadata key. GeoInterface.jl compatible writers may set this metadata key if writing to a format +that does not have its own mechanism to store known geometry columns, like Arrow. + +Optionally, the `crs` method can also be implemented: +```julia +GeoInterface.crs(::customcollection) +``` + +This should return a `GeoFormatTypes.CoordinateReferenceSystem` type, such as `EPSG(code::Int)`, `WellKnownText(GeoFormatTypes.CRS(), wkt::String)`, +or `ProjString(p4::String)`. See [GeoFormatTypes.jl](https://github.com/JuliaGeo/GeoFormatTypes.jl) for more information. + +The `crs` method also has a fallback that uses [DataAPI.jl](https://github.com/JuliaData/DataAPI.jl) metadata, if it exists, to retrieve the CRS. +GeoInterface searches for the `"GEOINTERFACE:crs"` metadata key to retrieve the CRS. + ## Geospatial Operations ```julia distance(geomtrait(a), geomtrait(b), a, b) diff --git a/src/GeoInterface.jl b/src/GeoInterface.jl index b2c16c2..f3c5de7 100644 --- a/src/GeoInterface.jl +++ b/src/GeoInterface.jl @@ -3,6 +3,7 @@ module GeoInterface using Extents: Extents, Extent using GeoFormatTypes: CoordinateReferenceSystemFormat using Base.Iterators: flatten +import DataAPI export testgeometry, isgeometry, trait, geomtrait, ncoord, getcoord, ngeom, getgeom @@ -55,6 +56,7 @@ include("fallbacks.jl") include("utils.jl") include("base.jl") include("wrappers.jl") +include("metadata.jl") using .Wrappers using .Wrappers: geointerface_geomtype diff --git a/src/fallbacks.jl b/src/fallbacks.jl index 9c501ad..f55efa1 100644 --- a/src/fallbacks.jl +++ b/src/fallbacks.jl @@ -95,8 +95,11 @@ issimple(t::AbstractMultiPointTrait, geom) = allunique((getgeom(t, geom))) issimple(t::AbstractMultiCurveTrait, geom) = all(issimple.(getgeom(t, geom))) isclosed(t::AbstractMultiCurveTrait, geom) = all(isclosed.(getgeom(t, geom))) -crs(::Nothing, geom) = nothing -crs(::AbstractTrait, geom) = nothing +"The key used to retrieve and store the CRS from DataAPI.jl metadata, if no other solution exists in that format." +const GEOINTERFACE_CRS_KEY = "GEOINTERFACE:crs" + +crs(::Nothing, geom) = _get_dataapi_metadata(geom, GEOINTERFACE_CRS_KEY, nothing) # see `metadata.jl` +crs(::AbstractTrait, geom) = _get_dataapi_metadata(geom, GEOINTERFACE_CRS_KEY, nothing) # see `metadata.jl` # FeatureCollection getfeature(t::AbstractFeatureCollectionTrait, fc) = (getfeature(t, fc, i) for i in 1:nfeature(t, fc)) diff --git a/src/interface.jl b/src/interface.jl index 3acb556..9369435 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -38,13 +38,28 @@ automatically delegate to this method. isfeaturecollection(x::T) where {T} = isfeaturecollection(T) isfeaturecollection(::Type{T}) where {T} = false +""" +The key used to retrieve and store the geometrycolumns from DataAPI.jl metadata, if no other solution exists in that format. +""" +const GEOINTERFACE_GEOMETRYCOLUMNS_KEY = "GEOINTERFACE:geometrycolumns" + """ GeoInterface.geometrycolumns(featurecollection) => (:geometry,) Retrieve the geometrycolumn(s) of `featurecollection`; the fields (or columns in a table) which contain geometries that support GeoInterface. + +This is always a `Tuple` of `Symbol`s. """ -geometrycolumns(featurecollection) = (:geometry,) +function geometrycolumns(featurecollection) + gcs = _get_dataapi_metadata(featurecollection, GEOINTERFACE_GEOMETRYCOLUMNS_KEY, (:geometry,)) # see `metadata.jl` + return _aftercare_geometrycolumns(gcs) +end + +_aftercare_geometrycolumns(gcs::Tuple{Vararg{Symbol}}) = gcs +_aftercare_geometrycolumns(gcs::Tuple{Vararg{String}}) = Symbol.(gcs) +_aftercare_geometrycolumns(gcs::String) = (Symbol(gcs),) +_aftercare_geometrycolumns(gcs::Symbol) = (gcs,) """ GeoInterface.geometry(feat) => geom diff --git a/src/metadata.jl b/src/metadata.jl new file mode 100644 index 0000000..a2ff0b3 --- /dev/null +++ b/src/metadata.jl @@ -0,0 +1,23 @@ +# This file contains internal helper functions to get metadata from DataAPI objects. +# At some point, it may also contain methods to set metadata. + +""" + Internal function. + +## Extended help + + _get_dataapi_metadata(geom, key, default) + +Get metadata associated with key `key` from some object, `geom`, that has DataAPI.jl metadata support. + +If the object does not have metadata support, or the key does not exist, return `default`. +""" +function _get_dataapi_metadata(geom::GeomType, key, default) where GeomType + if DataAPI.metadatasupport(GeomType).read # check that the type has metadata, and supports reading it + if key in DataAPI.metadatakeys(geom) # check that the key exists + return DataAPI.metadata(geom, key; style = false) # read the metadata + end + end + return default +end + diff --git a/test/test_dataapi.jl b/test/test_dataapi.jl new file mode 100644 index 0000000..6b50307 --- /dev/null +++ b/test/test_dataapi.jl @@ -0,0 +1,40 @@ +using Test +using GeoInterface +using GeoFormatTypes: EPSG +using GeoInterface.DataAPI + +struct TestMetadata + geometrycolumns + crs +end + +DataAPI.metadatasupport(::Type{TestMetadata}) = (; read = true, write = false) +DataAPI.metadatakeys(::TestMetadata) = ("GEOINTERFACE:geometrycolumns", "GEOINTERFACE:crs") +function DataAPI.metadata(x::TestMetadata, key::String; style::Bool=false) + if key === "GEOINTERFACE:geometrycolumns" + style ? (x.geometrycolumns, :note) : x.geometrycolumns + elseif key === "GEOINTERFACE:crs" + style ? (x.crs, :note) : x.crs + else + nothing + end +end +DataAPI.metadata(x::TestMetadata, key::String, default; style::Bool=false) = something(DataAPI.metadata(x, key; style), style ? (default, :note) : default) + + + + + +@testset "DataAPI" begin + td = TestMetadata((:g,), nothing) + @test GeoInterface.geometrycolumns(td) == (:g,) + @test GeoInterface.crs(td) === nothing + + td = TestMetadata((:g,), EPSG(4326)) + @test GeoInterface.geometrycolumns(td) == (:g,) + @test GeoInterface.crs(td) == EPSG(4326) + + td = TestMetadata("geometry1", EPSG(4326)) + @test GeoInterface.geometrycolumns(td) == (:geometry1,) + @test GeoInterface.crs(td) == EPSG(4326) +end