Skip to content

Commit

Permalink
Implement crs and geometrycolumn fallbacks via DataAPI (#161)
Browse files Browse the repository at this point in the history
* Implement crs and geometrycolumn fallbacks via DataAPI

* fix function name

* use type not object to query DataAPI

* Move core functionality to a new `metadata.jl`

* add aftercare for geometry columns

* add docs about geometrycolumns and crs for feature collections

* minor bugfixes + add tests

* avoid extra entry

* add constants that define what the keys are, for portability
  • Loading branch information
asinghvi17 authored Dec 13, 2024
1 parent 1db072e commit 334c0ec
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions docs/src/guides/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/GeoInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/fallbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
17 changes: 16 additions & 1 deletion src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/metadata.jl
Original file line number Diff line number Diff line change
@@ -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

40 changes: 40 additions & 0 deletions test/test_dataapi.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 334c0ec

Please sign in to comment.