Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement all applicable GeoInterface methods for DataFrames #70

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ function read(ds, layer)
rename!(df, Symbol("") => :geometry)
replace!(gnames, Symbol("") => :geometry)
end
metadata!(df, "crs", sr.ptr == C_NULL ? nothing : GFT.WellKnownText(GFT.CRS(), AG.toWKT(sr)), style=:default)
metadata!(df, "geometrycolumns", Tuple(gnames), style=:default)
println("HELLO")
asinghvi17 marked this conversation as resolved.
Show resolved Hide resolved
crs = sr.ptr == C_NULL ? nothing : GFT.WellKnownText(GFT.CRS(), AG.toWKT(sr))
geometrycolumns = Tuple(gnames)
metadata!(df, "crs", crs, style=:default)
metadata!(df, "geometrycolumns", geometrycolumns, style=:default)
# Also add the GEOINTERFACE:property as a namespaced thing
metadata!(df, "GEOINTERFACE:crs", crs, style=:default)
metadata!(df, "GEOINTERFACE:geometrycolumns", geometrycolumns, style=:default)
return df
end

Expand Down
62 changes: 60 additions & 2 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ function getgeometrycolumns(table)
if GeoInterface.isfeaturecollection(table)
return GeoInterface.geometrycolumns(table)
elseif first(DataAPI.metadatasupport(typeof(table)))
return metadata(table, "geometrycolumns", (:geometry,))
gc = DataAPI.metadata(table, "GEOINTERFACE:geometrycolumns", nothing)
if isnothing(gc) # fall back to searching for "geometrycolumns" as a string
gc = DataAPI.metadata(table, "geometrycolumns", (:geometry,))
end
return gc
else
return (:geometry,)
end
Expand All @@ -20,8 +24,62 @@ function getcrs(table)
if GeoInterface.isfeaturecollection(table)
return GeoInterface.crs(table)
elseif first(DataAPI.metadatasupport(typeof(table)))
return metadata(table, "crs", nothing)
crs = DataAPI.metadata(table, "GEOINTERFACE:crs", nothing)
if isnothing(crs) # fall back to searching for "crs" as a string
crs = DataAPI.metadata(table, "crs", nothing)
end
return crs
else
return nothing
end
end

# Override some GeoInterface functions specifically for DataFrames

# These are the basic metadata definitions from which all else follows.

function GeoInterface.crs(table::DataFrame)
crs = DataAPI.metadata(table, "GEOINTERFACE:crs", nothing)
if isnothing(crs) # fall back to searching for "crs" as a string
crs = DataAPI.metadata(table, "crs", nothing)
end
return crs
end

function GeoInterface.geometrycolumns(table::DataFrame)
gc = DataAPI.metadata(table, "GEOINTERFACE:geometrycolumns", nothing)
if isnothing(gc) # fall back to searching for "geometrycolumns" as a string
gc = DataAPI.metadata(table, "geometrycolumns", (:geometry,))
# TODO: we could search for columns named e.g. `:geometry`, `:geom`. `:shape`, and caps/lowercase versions.
# But should we?
end
return gc
end

# We don't define DataFrames as feature collections explicitly, since
# that would complicate handling. But, we can still implement the
# feature interface, for use in generic code. And dispatch can always
# handle a DataFrame by fixing the trait in a specialized method.

# Here, we define a feature as a DataFrame row.
function GeoInterface.getfeature(df::DataFrame, i::Integer)
return view(df, i, :)
end
# This is simply an optimized method, since we know what we have to do already.
GeoInterface.getfeature(df::DataFrame) = eachrow(df)

# The geometry is defined as the first of the geometry columns.
# TODO: how should we choose between the geometry columns?
function GeoInterface.geometry(row::DataFrameRow)
return row[first(GeoInterface.geometrycolumns(row))]
end

# The properties are all other columns.
function GeoInterface.properties(row::DataFrameRow)
return row[DataFrames.Not(first(GeoInterface.geometrycolumns(row)))]
end

# Since `DataFrameRow` is simply a view of a DataFrame, we can reach back
# to the original DataFrame to get the metadata.
GeoInterface.geometrycolumns(row::DataFrameRow) = GeoInterface.geometrycolumns(getfield(row, :df)) # get the parent of the row view
GeoInterface.crs(row::DataFrameRow) = GeoInterface.crs(getfield(row, :df)) # get the parent of the row view
7 changes: 6 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,12 @@ unknown_crs = GFT.WellKnownText(GFT.CRS(), "GEOGCS[\"Undefined geographic SRS\",
@test isfile(GDF.write(tfn, table))
t = GDF.read(tfn)
meta["crs"] = unknown_crs
@test metadata(t) == meta
meta["GEOINTERFACE:crs"] = unknown_crs
meta["GEOINTERFACE:geometrycolumns"] = meta["geometrycolumns"]
@test isempty(setdiff(keys(meta), metadatakeys(t)))
for pair in meta
@test metadata(t, pair.first, nothing) == pair.second
end
end

end
Loading