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

Table to FeatureCollection #59

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
34 changes: 33 additions & 1 deletion src/features.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,48 @@ Base.:(==)(f1::Feature, f2::Feature) = object(f1) == object(f2)
"""
FeatureCollection <: AbstractVector{Feature}

FeatureCollection(table; [geometrycolumn])
FeatureCollection(features::AstractVector; kw...)

A feature collection wrapping a JSON object.

Follows the julia `AbstractArray` interface as a lazy vector of `Feature`,
and similarly the GeoInterface.jl interface.

FeatureCollection can be constructed from an `AbstractVector` of
`GeoJSON.Feature` or from any Tables.jl compatible table.

The first `GeoInterface.geometrycolumns(table)` will be used for geometries
(usually `:geometry`) but `:geometrycolumn` can be specified manually where
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
this does not work and the column name is not `:geometry`.
"""
struct FeatureCollection{T,O,A} <: AbstractVector{T}
object::O
features::A
names::Vector{Symbol}
types::Dict{Symbol,Type}
end
function FeatureCollection(object::O) where O
function FeatureCollection(object::O; geometrycolumn::Union{Symbol,Nothing}=nothing) where O
# First check if object is a table
if Tables.istable(object)
names = Tables.columnnames(object)
geomcolname = isnothing(geometrycolumn) ? first(GI.geometrycolumns(object)) : geometrycolumn
colnames = Tables.columnnames(object)
geomcolname in colnames || throw(ArgumentError("Table does not contain a `:geometry` column. You may need to specify the column name with the `:geometrycolumn` keyword"))
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
othercolnames = Tuple(cn for cn in colnames if cn != geomcolname)
features = [_feature_from_row(row, geomcolname, othercolnames) for row in Tables.rowtable(object)]
return FeatureCollection(features)
end

# Otherwise construct a FeatureCollection from other objects
features = object.features
if isempty(features)
# FeatureCollection without features, get the type for an untyped feature without properties
names = Symbol[:geometry]
types = Dict{Symbol,Type}(:geometry => Union{Missing,Geometry})
T = Feature{Any}
else
# FeatureCollection with features, get the names and field types of all features
names, types = property_schema(features)
insert!(names, 1, :geometry)
types[:geometry] = Union{Missing,Geometry}
Expand All @@ -118,6 +142,14 @@ FeatureCollection(features::AbstractVector; kwargs...) =
names(fc::FeatureCollection) = getfield(fc, :names)
types(fc::FeatureCollection) = getfield(fc, :types)

function _feature_from_row(row, geomcolname::Symbol, othercolnames::Tuple)
geometry = Tables.getcolumn(row, geomcolname)
properties = map(othercolnames) do cn
cn => Tables.getcolumn(row, cn)
end |> NamedTuple
Feature(; geometry, properties)
end

"""
features(fc::FeatureCollection)

Expand Down
23 changes: 19 additions & 4 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,30 @@ include("geojson_samples.jl")
@test iterate(p, 2) === (2.2, 3)
@test iterate(p, 3) === nothing

# Mixed name vector
f2 = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2, c = 3))
GeoJSON.FeatureCollection((type = "FeatureCollection", features = [f, f2]))
end

@testset "Tables" begin
p = GeoJSON.Point(coordinates = [1.1, 2.2])
f = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2))
features = [f]
# other constructors
@test DataFrame([GeoJSON.Feature(geometry = p, properties = (a = 1, geometry = "g", b = 2))]) ==
DataFrame([GeoJSON.Feature((geometry = p, properties = (a = 1, geometry = "g", b = 2)))]) ==
DataFrame(GeoJSON.FeatureCollection((type="FeatureCollection", features=[f]))) ==
DataFrame(GeoJSON.FeatureCollection((; type="FeatureCollection", features))) ==
DataFrame(GeoJSON.FeatureCollection(; features))

# Mixed name vector
f2 = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2, c = 3))
GeoJSON.FeatureCollection((type = "FeatureCollection", features = [f, f2]))
# Round trip DataFrame -> FeatureCollection -> DataFrame
features = [GeoJSON.Feature(geometry = p, properties = (a = 1, b = 2)), GeoJSON.Feature(geometry = p, properties = (a = 5, b = 10))]
df = DataFrame(GeoJSON.FeatureCollection(features))
@test df == DataFrame(GeoJSON.FeatureCollection(df))

df_custom_col = DataFrame(:points => [p, p], :x => [1, 2])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work with other non-GeoJSON.jl geometry types as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I actually forgot about that. So we need the other convert PR merged first.

I'll get that passing and come back to this. It's also a good example of how the traittype method will be useful from within a package as well.

df_converted = DataFrame(GeoJSON.FeatureCollection(df_custom_col; geometrycolumn=:points))
@test df_custom_col.points == df_converted.geometry
@test df_custom_col.x == df_converted.x
end

@testset "extent" begin
Expand Down