From c65b527cc9768c434ba5ca73f595dcf43d641ec8 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 19 Sep 2024 18:28:07 -0700 Subject: [PATCH 1/2] Add a Tables.jl extension for FeatureCollection and Feature --- Project.toml | 9 ++- ext/GeoInterfaceTablesExt.jl | 126 +++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 ext/GeoInterfaceTablesExt.jl diff --git a/Project.toml b/Project.toml index 858c1cbb..a5b1e118 100644 --- a/Project.toml +++ b/Project.toml @@ -7,10 +7,17 @@ version = "1.3.6" Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f" +[weakdeps] +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + +[extensions] +GeoInterfaceTablesExt = "Tables" + [compat] Extents = "0.1.1" GeoFormatTypes = "0.4" -julia = "1" +Tables = "1" +julia = "1.9" [extras] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/ext/GeoInterfaceTablesExt.jl b/ext/GeoInterfaceTablesExt.jl new file mode 100644 index 00000000..ce639875 --- /dev/null +++ b/ext/GeoInterfaceTablesExt.jl @@ -0,0 +1,126 @@ +module GeoInterfaceTablesExt + +using GeoInterface +using GeoInterface.Wrappers +using Tables + +# This module is meant to extend the Tables.jl interface to features and feature collections, such that they can be used with Tables.jl. +# This enables the use of the Tables.jl ecosystem with GeoInterface wrapper geometries. + +# First, define the Tables interface + +Tables.istable(::Type{<: Wrappers.FeatureCollection}) = true +Tables.isrowtable(::Type{<: Wrappers.FeatureCollection}) = true +Tables.rowaccess(::Type{<: Wrappers.FeatureCollection}) = true +Tables.rows(fc::Wrappers.FeatureCollection{P, C, E}) where {P <: Union{AbstractArray{<: Wrappers.Feature}, Tuple{Vararg{<: Wrappers.Feature}}}, C, E} = GeoInterface.getfeature(fc) +Tables.rows(fc::Wrappers.FeatureCollection) = Iterators.map(Wrappers.Feature, GeoInterface.getfeature(fc)) +Tables.schema(fc::Wrappers.FeatureCollection) = nothing + +# Define the row access interface for feature wrappers +function Tables.getcolumn(row::Wrappers.Feature, i::Int) + if i == 1 + return GeoInterface.geometry(row) + else + return GeoInterface.properties(row)[i-1] + end +end +Tables.getcolumn(row::Wrappers.Feature, nm::Symbol) = nm === :geometry ? GeoInterface.geometry(row) : Tables.getcolumn(GeoInterface.properties(row), nm) +Tables.columnnames(row::Wrappers.Feature) = (:geometry, propertynames(GeoInterface.properties(row))...) + +# Copied from GeoJSON.jl +# Credit to [Rafael Schouten](@rafaqz) +# Adapted from JSONTables.jl jsontable method +# We cannot simply use their method as we have concrete types and need the key/value pairs +# of the properties field, rather than the main object +# TODO: Is `missT` required? +# TODO: The `getfield` is probably required once +function property_schema(features) + # Otherwise find the shared names + names = Set{Symbol}() + types = Dict{Symbol,Type}() + for feature in features + props = properties(feature) + isnothing(props) && continue + if isempty(names) + for k in keys(props) + k === :geometry && continue + push!(names, k) + types[k] = missT(typeof(props[k])) + end + push!(names, :geometry) + types[:geometry] = missT(typeof(geometry(feature))) + else + for nm in names + T = types[nm] + if haskey(props, nm) + v = props[nm] + if !(missT(typeof(v)) <: T) + types[nm] = Union{T,missT(typeof(v))} + end + elseif hasfield(typeof(feature), nm) + v = getfield(feature, nm) + if !(missT(typeof(v)) <: T) + types[nm] = Union{T,missT(typeof(v))} + end + elseif !(T isa Union && T.a === Missing) + types[nm] = Union{Missing,types[nm]} + end + end + for (k, v) in pairs(props) + k === :geometry && continue + if !(k in names) + push!(names, k) + types[k] = Union{Missing,missT(typeof(v))} + end + end + end + end + return collect(names), types +end + + + +# Finally, define the metadata interface. FeatureCollection wrappers have no metadata, so we simply specify geometry columns and CRS. + +Tables.DataAPI.metadatasupport(::Type{<: Wrappers.FeatureCollection}) = (; read = true, write = false) +Tables.DataAPI.metadatakeys(::Wrappers.FeatureCollection) = ("GEOINTERFACE:geometrycolumns", "GEOINTERFACE:crs") +function Tables.DataAPI.metadata(fc::Wrappers.FeatureCollection, key::AbstractString; style = false) + result = if key == "GEOINTERFACE:geometrycolumns" + (:geometry,) + elseif key == "GEOINTERFACE:crs" + if isnothing(GeoInterface.crs(fc)) + nothing + # or + #= + GeoFormatTypes.ESRIWellKnownText( + """ + ENGCRS["Undefined Cartesian SRS with unknown unit", + EDATUM["Unknown engineering datum"], + CS[Cartesian,2], + AXIS["X",unspecified, + ORDER[1], + LENGTHUNIT["unknown",0]], + AXIS["Y",unspecified, + ORDER[2], + LENGTHUNIT["unknown",0]]] + """ + ) + =# + else + GeoInterface.crs(fc) + end + else + throw(KeyError(key)) + end + + if style + return (result, :note) + else + return result + end +end + + + + +end # module \ No newline at end of file From c5174cde3b45fd8c2453062a27f72cd49007c5be Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 19 Sep 2024 18:36:47 -0700 Subject: [PATCH 2/2] make property_schema work but it actually increases runtime...something else is going on! --- ext/GeoInterfaceTablesExt.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ext/GeoInterfaceTablesExt.jl b/ext/GeoInterfaceTablesExt.jl index ce639875..9d75f60c 100644 --- a/ext/GeoInterfaceTablesExt.jl +++ b/ext/GeoInterfaceTablesExt.jl @@ -14,7 +14,7 @@ Tables.isrowtable(::Type{<: Wrappers.FeatureCollection}) = true Tables.rowaccess(::Type{<: Wrappers.FeatureCollection}) = true Tables.rows(fc::Wrappers.FeatureCollection{P, C, E}) where {P <: Union{AbstractArray{<: Wrappers.Feature}, Tuple{Vararg{<: Wrappers.Feature}}}, C, E} = GeoInterface.getfeature(fc) Tables.rows(fc::Wrappers.FeatureCollection) = Iterators.map(Wrappers.Feature, GeoInterface.getfeature(fc)) -Tables.schema(fc::Wrappers.FeatureCollection) = nothing +Tables.schema(fc::Wrappers.FeatureCollection) = property_schema(GeoInterface.getfeature(fc)) # Define the row access interface for feature wrappers function Tables.getcolumn(row::Wrappers.Feature, i::Int) @@ -34,12 +34,15 @@ Tables.columnnames(row::Wrappers.Feature) = (:geometry, propertynames(GeoInterfa # of the properties field, rather than the main object # TODO: Is `missT` required? # TODO: The `getfield` is probably required once +missT(::Type{Nothing}) = Missing +missT(::Type{T}) where {T} = T + function property_schema(features) # Otherwise find the shared names names = Set{Symbol}() types = Dict{Symbol,Type}() for feature in features - props = properties(feature) + props = GeoInterface.properties(feature) isnothing(props) && continue if isempty(names) for k in keys(props) @@ -48,7 +51,7 @@ function property_schema(features) types[k] = missT(typeof(props[k])) end push!(names, :geometry) - types[:geometry] = missT(typeof(geometry(feature))) + types[:geometry] = missT(typeof(GeoInterface.geometry(feature))) else for nm in names T = types[nm]