From 40d97dbe4fa12667258a72c59df8140e5c64d6ee Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 01:55:55 +0100 Subject: [PATCH 01/10] allow FeatureCollection be constructed from a table --- src/features.jl | 50 +++++++++++++++++++++++++++++++++--------------- test/runtests.jl | 5 +++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/features.jl b/src/features.jl index 1024278..bdb8612 100644 --- a/src/features.jl +++ b/src/features.jl @@ -88,25 +88,37 @@ struct FeatureCollection{T,O,A} <: AbstractVector{T} types::Dict{Symbol,Type} end function FeatureCollection(object::O) where O - features = object.features - if isempty(features) - names = Symbol[:geometry] - types = Dict{Symbol,Type}(:geometry => Union{Missing,Geometry}) - T = Feature{Any} + if Tables.istable(object) + # Construct a FeatureCollection from a table + names = Tables.columnnames(object) + geomcolname = first(GI.geometrycolumns(object)) + othercolnames = Tuple(cn for cn in Tables.columnnames(object) if cn != geomcolname) + features = [_feature_from_row(row, geomcolname, othercolnames) for row in Tables.rowtable(object)] + return FeatureCollection(features) else - names, types = property_schema(features) - insert!(names, 1, :geometry) - types[:geometry] = Union{Missing,Geometry} - f1 = first(features) - T = if f1 isa JSON3.Object - typeof(Feature(f1, names)) - elseif f1 isa NamedTuple && isconcretetype(eltype(features)) - typeof(Feature(f1, names)) - else + # 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} + f1 = first(features) + T = if f1 isa JSON3.Object + typeof(Feature(f1, names)) + elseif f1 isa NamedTuple && isconcretetype(eltype(features)) + typeof(Feature(f1, names)) + else + T = Feature{Any} + end end + return FeatureCollection{T,O,typeof(features)}(object, features, names, types) end - return FeatureCollection{T,O,typeof(features)}(object, features, names, types) end function FeatureCollection(; features::AbstractVector{T}, kwargs...) where {T} object = merge((type = "FeatureCollection", features), kwargs) @@ -118,6 +130,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) diff --git a/test/runtests.jl b/test/runtests.jl index 560765f..b1b7d03 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -127,6 +127,11 @@ include("geojson_samples.jl") DataFrame(GeoJSON.FeatureCollection((type="FeatureCollection", features=[f]))) == DataFrame(GeoJSON.FeatureCollection(; features)) + # Round trip DataFrame -> FeatureCollection -> DataFrame + features = [GeoJSON.Feature(geometry = p, properties = (a = 1, b = 2)), GeoJSON.Feature(geometry = p, properties = (a = 1,))] + GeoJSON.FeatureCollection(features) + @test df == DataFrame(GeoJSON.FeatureCollection(df)) + # Mixed name vector f2 = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2, c = 3)) GeoJSON.FeatureCollection((type = "FeatureCollection", features = [f, f2])) From d4997943da6452c57456c5d2a4a6fc4966ed1250 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:01:38 +0100 Subject: [PATCH 02/10] clean up a little --- src/features.jl | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/features.jl b/src/features.jl index bdb8612..a14fe8f 100644 --- a/src/features.jl +++ b/src/features.jl @@ -88,37 +88,39 @@ struct FeatureCollection{T,O,A} <: AbstractVector{T} types::Dict{Symbol,Type} end function FeatureCollection(object::O) where O + # First check if object is a table if Tables.istable(object) - # Construct a FeatureCollection from a table names = Tables.columnnames(object) geomcolname = first(GI.geometrycolumns(object)) - othercolnames = Tuple(cn for cn in Tables.columnnames(object) if cn != geomcolname) + colnames = Tables.columnnames(object) + geomcolname in columnnames || throw(ArgumentError("Table does not contain a `:geometry` column")) + 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 - # 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} + # 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} + f1 = first(features) + T = if f1 isa JSON3.Object + typeof(Feature(f1, names)) + elseif f1 isa NamedTuple && isconcretetype(eltype(features)) + typeof(Feature(f1, names)) 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} - f1 = first(features) - T = if f1 isa JSON3.Object - typeof(Feature(f1, names)) - elseif f1 isa NamedTuple && isconcretetype(eltype(features)) - typeof(Feature(f1, names)) - else - T = Feature{Any} - end + T = Feature{Any} end - return FeatureCollection{T,O,typeof(features)}(object, features, names, types) end + return FeatureCollection{T,O,typeof(features)}(object, features, names, types) end function FeatureCollection(; features::AbstractVector{T}, kwargs...) where {T} object = merge((type = "FeatureCollection", features), kwargs) From 6096945dd32181f0e9fcf529f62cfe4bf07f5760 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:07:49 +0100 Subject: [PATCH 03/10] bugfix tests --- src/features.jl | 2 +- test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features.jl b/src/features.jl index a14fe8f..d46b536 100644 --- a/src/features.jl +++ b/src/features.jl @@ -93,7 +93,7 @@ function FeatureCollection(object::O) where O names = Tables.columnnames(object) geomcolname = first(GI.geometrycolumns(object)) colnames = Tables.columnnames(object) - geomcolname in columnnames || throw(ArgumentError("Table does not contain a `:geometry` column")) + geomcolname in colnames || throw(ArgumentError("Table does not contain a `:geometry` column")) 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) diff --git a/test/runtests.jl b/test/runtests.jl index b1b7d03..5d04ccc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -128,8 +128,8 @@ include("geojson_samples.jl") DataFrame(GeoJSON.FeatureCollection(; features)) # Round trip DataFrame -> FeatureCollection -> DataFrame - features = [GeoJSON.Feature(geometry = p, properties = (a = 1, b = 2)), GeoJSON.Feature(geometry = p, properties = (a = 1,))] - GeoJSON.FeatureCollection(features) + 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)) # Mixed name vector From eea0adfaf9d7c10b00da0fac956db4f8f03d4237 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:18:41 +0100 Subject: [PATCH 04/10] add a :geometrycolumn keyword --- src/features.jl | 4 ++-- test/runtests.jl | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/features.jl b/src/features.jl index d46b536..8fd1ecb 100644 --- a/src/features.jl +++ b/src/features.jl @@ -87,11 +87,11 @@ struct FeatureCollection{T,O,A} <: AbstractVector{T} 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 = first(GI.geometrycolumns(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")) othercolnames = Tuple(cn for cn in colnames if cn != geomcolname) diff --git a/test/runtests.jl b/test/runtests.jl index 5d04ccc..ef0b16b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -121,6 +121,14 @@ 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 + f = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2)) + p = GeoJSON.Point(coordinates = [1.1, 2.2]) # 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)))]) == @@ -132,9 +140,12 @@ include("geojson_samples.jl") df = DataFrame(GeoJSON.FeatureCollection(features)) @test df == DataFrame(GeoJSON.FeatureCollection(df)) - # Mixed name vector - f2 = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2, c = 3)) - GeoJSON.FeatureCollection((type = "FeatureCollection", features = [f, f2])) + @test df == DataFrame(GeoJSON.FeatureCollection(df)) + + df_custom_col = DataFrame(:points => [p, p], :x => [1, 2]) + 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 From 0c90fd77753bf3e2fefc9bd4ac697ecf0a76469c Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:27:43 +0100 Subject: [PATCH 05/10] better docs and error messages --- src/features.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/features.jl b/src/features.jl index 8fd1ecb..89e58b4 100644 --- a/src/features.jl +++ b/src/features.jl @@ -76,10 +76,20 @@ 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 +this does not work and the column name is not `:geometry`. """ struct FeatureCollection{T,O,A} <: AbstractVector{T} object::O @@ -93,7 +103,7 @@ function FeatureCollection(object::O; geometrycolumn::Union{Symbol,Nothing}=noth 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")) + geomcolname in colnames || throw(ArgumentError("Table does not contain a `:geometry` column. You may need to specify the column name with the `:geometrycolumn` keyword")) 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) From d2002e389c08a9ae519c9b8f6879aea537703a4d Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:28:28 +0100 Subject: [PATCH 06/10] fix tests --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index ef0b16b..376a0c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -127,8 +127,8 @@ include("geojson_samples.jl") end @testset "Tables" begin - f = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2)) p = GeoJSON.Point(coordinates = [1.1, 2.2]) + f = GeoJSON.Feature(p; properties = (a = 1, geometry = "g", b = 2)) # 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)))]) == From bfaf4f53facd5614522ad45e96a6b87ebe449faa Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:31:17 +0100 Subject: [PATCH 07/10] minor cleanup --- test/runtests.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 376a0c8..83bbb94 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -140,8 +140,6 @@ include("geojson_samples.jl") df = DataFrame(GeoJSON.FeatureCollection(features)) @test df == DataFrame(GeoJSON.FeatureCollection(df)) - @test df == DataFrame(GeoJSON.FeatureCollection(df)) - df_custom_col = DataFrame(:points => [p, p], :x => [1, 2]) df_converted = DataFrame(GeoJSON.FeatureCollection(df_custom_col; geometrycolumn=:points)) @test df_custom_col.points == df_converted.geometry From 7eda9345f10f4aaca0c1c752f2954d41f4005935 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Thu, 29 Dec 2022 02:36:44 +0100 Subject: [PATCH 08/10] one last test fix --- test/runtests.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 83bbb94..8909318 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -129,10 +129,11 @@ include("geojson_samples.jl") @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)) # Round trip DataFrame -> FeatureCollection -> DataFrame From a27487e97062b3254aff8daa16df4aabd6284e9a Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 31 Dec 2022 19:28:00 +0100 Subject: [PATCH 09/10] Update src/features.jl Co-authored-by: Martijn Visser --- src/features.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features.jl b/src/features.jl index 89e58b4..7fe50f5 100644 --- a/src/features.jl +++ b/src/features.jl @@ -88,7 +88,7 @@ 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 +(usually `:geometry`) but `geometrycolumn` can be specified manually where this does not work and the column name is not `:geometry`. """ struct FeatureCollection{T,O,A} <: AbstractVector{T} From ae08e5f5775067b90166d2c2c19765392182eb0f Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 31 Dec 2022 19:28:38 +0100 Subject: [PATCH 10/10] Update src/features.jl Co-authored-by: Martijn Visser --- src/features.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features.jl b/src/features.jl index 7fe50f5..c09fabb 100644 --- a/src/features.jl +++ b/src/features.jl @@ -103,7 +103,7 @@ function FeatureCollection(object::O; geometrycolumn::Union{Symbol,Nothing}=noth 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")) + geomcolname in colnames || throw(ArgumentError("Cannot find a geometry column. You may need to specify the column name with the `geometrycolumn` keyword")) 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)