Skip to content
This repository has been archived by the owner on Jul 4, 2022. It is now read-only.

final push #15

Merged
merged 5 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"

[compat]
Aqua = "0.5"
Extents = "0.1"
GeoFormatTypes = "0.4"
GeoInterface = "1"
Expand All @@ -19,7 +20,8 @@ Tables = "1"
julia = "1.6"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
test = ["Aqua", "Test"]
5 changes: 2 additions & 3 deletions src/GeoJSONTables.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
module GeoJSONTables

import JSON3, Tables, GeoFormatTypes, GeoInterface, Extents

const GI = GeoInterface
import JSON3, Tables, GeoFormatTypes, Extents
import GeoInterface as GI

include("geometries.jl")
include("features.jl")
Expand Down
87 changes: 62 additions & 25 deletions src/features.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,28 @@ struct Feature{T}
object::T
end

# these features always have type="Feature", so exclude that
Feature{T}(f::Feature{T}) where {T} = f
Feature(; geometry::Geometry, kwargs...) =
Feature(merge((type = "Feature", geometry), kwargs))
Feature(geometry::Geometry; kwargs...) =
Feature(merge((type = "Feature", geometry), kwargs))

# the keys in properties are added here for direct access
Base.propertynames(f::Feature) = keys(properties(f))
function Base.propertynames(f::Feature)
propnames = keys(properties(f))
# properties named "geometry" are shadowed by the geometry
return (:geometry, filter(!=(:geometry), propnames)...)
end

function Base.getproperty(f::Feature, nm::Symbol)
x = if nm == :geometry
geometry(f)
else
props = properties(f)
getproperty(props, nm)
end
return ifelse(x === nothing, missing, x)
end

"Access the properties JSON3.Object of a Feature"
properties(f::Feature) = object(f).properties
Expand All @@ -36,11 +55,41 @@ struct FeatureCollection{T,O,A} <: AbstractVector{T}
features::A
end

function FeatureCollection(object::O) where {O}
features = object.features
T = isempty(features) ? Feature{Any} : typeof(Feature(first(features)))
return FeatureCollection{T,O,typeof(features)}(object, features)
end

function FeatureCollection(; features::AbstractVector{T}, kwargs...) where {T}
FT = ifelse(T <: Feature, T, Feature{T})
object = merge((type = "FeatureCollection", features), kwargs)
return FeatureCollection{FT,typeof(object),typeof(features)}(object, features)
end

function FeatureCollection(features::AbstractVector{T}; kwargs...) where {T}
FT = ifelse(T <: Feature, T, Feature{T})
object = merge((type = "FeatureCollection", features), kwargs)
return FeatureCollection{FT,typeof(object),typeof(features)}(object, features)
end

"Access the vector of features in the FeatureCollection"
features(f::FeatureCollection) = getfield(f, :features)
features(fc::FeatureCollection) = getfield(fc, :features)

# Base methods

function Base.propertynames(fc::FeatureCollection)
# get the propertynames from the first feature, if it exists
return if isempty(fc)
(:geometry,)
else
f = first(fc)
propertynames(f)
end
end

Base.getproperty(fc::FeatureCollection, nm::Symbol) = getproperty.(fc, nm)

Base.IteratorSize(::Type{<:FeatureCollection}) = Base.HasLength()
Base.length(fc::FeatureCollection) = length(features(fc))
Base.IteratorEltype(::Type{<:FeatureCollection}) = Base.HasEltype()
Expand All @@ -51,30 +100,17 @@ Base.eltype(::FeatureCollection{T}) where {T<:Feature} = T
Base.getindex(fc::FeatureCollection{T}, i) where {T<:Feature} = T(features(fc)[i])
Base.IndexStyle(::Type{<:FeatureCollection}) = IndexLinear()

"""
Get a specific property of the Feature

Returns missing for null/nothing or not present, to work nicely with
properties that are not defined for every feature. If it is a table,
it should in some sense be defined.
"""
function Base.getproperty(f::Feature, nm::Symbol)
props = properties(f)
val = get(props, nm, missing)
miss(val)
end

@inline function Base.iterate(fc::FeatureCollection{T}) where {T<:Feature}
st = iterate(features(fc))
st === nothing && return nothing
val, state = st
function Base.iterate(fc::FeatureCollection{T}) where {T<:Feature}
x = iterate(features(fc))
x === nothing && return nothing
val, state = x
return T(val), state
end

@inline function Base.iterate(fc::FeatureCollection{T}, st) where {T<:Feature}
st = iterate(features(fc), st)
st === nothing && return nothing
val, state = st
function Base.iterate(fc::FeatureCollection{T}, state) where {T<:Feature}
x = iterate(features(fc), state)
x === nothing && return nothing
val, state = x
return T(val), state
end

Expand All @@ -85,7 +121,7 @@ function Base.show(io::IO, f::Feature)
geom = geometry(f)
propnames = propertynames(f)
n = length(propnames)
if isnothing(geom)
if geom === nothing
print(io, "Feature with null geometry")
else
print(io, "Feature with a ", type(geom))
Expand All @@ -112,6 +148,7 @@ correctly back to GeoJSON strings.
object(x::GeoJSONObject) = getfield(x, :object)

type(x::GeoJSONObject) = String(object(x).type)
type(x) = String(x.type)
bbox(x::GeoJSONObject) = get(object(x), :bbox, nothing)

Base.show(io::IO, ::MIME"text/plain", x::GeoJSONObject) = show(io, x)
19 changes: 10 additions & 9 deletions src/geointerface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@ GI.getcoord(::GI.PointTrait, g::Point, i::Int) = g[i]

GI.ncoord(::GI.LineStringTrait, g::LineString) = length(first(g))
GI.ngeom(::GI.LineStringTrait, g::LineString) = length(g)
GI.getgeom(::GI.LineStringTrait, g::LineString, i::Integer) = Point(g[i])
GI.getpoint(::GI.LineStringTrait, g::LineString, i::Int) = Point(g[i])
GI.getgeom(::GI.LineStringTrait, g::LineString, i::Integer) = Point(coordinates = g[i])
GI.getpoint(::GI.LineStringTrait, g::LineString, i::Int) = Point(coordinates = g[i])
# TODO what to return for length 0 and 1?
# TODO should this be an approximate equals for floating point?
GI.isclosed(::GI.LineStringTrait, g::LineString) = first(g) == last(g)

GI.ngeom(::GI.PolygonTrait, g::Polygon) = length(g)
GI.getgeom(::GI.PolygonTrait, g::Polygon, i::Integer) = LineString(g[i])
GI.getgeom(::GI.PolygonTrait, g::Polygon, i::Integer) = LineString(coordinates = g[i])
GI.ncoord(::GI.PolygonTrait, g::Polygon) = length(first(first(g)))
GI.getexterior(::GI.PolygonTrait, g::Polygon) = LineString(first(g))
GI.getexterior(::GI.PolygonTrait, g::Polygon) = LineString(coordinates = first(g))
GI.nhole(::GI.PolygonTrait, g::Polygon) = length(g) - 1
GI.gethole(::GI.PolygonTrait, g::Polygon, i::Int) = LineString(g[i+1])
GI.gethole(::GI.PolygonTrait, g::Polygon, i::Int) = LineString(coordinates = g[i+1])

GI.ncoord(::GI.MultiPointTrait, g::MultiPoint) = length(first(g))
GI.ngeom(::GI.MultiPointTrait, g::MultiPoint) = length(g)
GI.getgeom(::GI.MultiPointTrait, g::MultiPoint, i::Int) = Point(g[i])
GI.getgeom(::GI.MultiPointTrait, g::MultiPoint, i::Int) = Point(coordinates = g[i])

GI.ncoord(::GI.MultiLineStringTrait, g::MultiLineString) = length(first(first(g)))
GI.ngeom(::GI.MultiLineStringTrait, g::MultiLineString) = length(g)
GI.getgeom(::GI.MultiLineStringTrait, g::MultiLineString, i::Int) = LineString(g[i])
GI.getgeom(::GI.MultiLineStringTrait, g::MultiLineString, i::Int) =
LineString(coordinates = g[i])

GI.ncoord(::GI.MultiPolygonTrait, g::MultiPolygon) = length(first(first(first(g))))
GI.ngeom(::GI.MultiPolygonTrait, g::MultiPolygon) = length(g)
GI.getgeom(::GI.MultiPolygonTrait, g::MultiPolygon, i::Int) = Polygon(g[i])
GI.getgeom(::GI.MultiPolygonTrait, g::MultiPolygon, i::Int) = Polygon(coordinates = g[i])

GI.ncoord(::GI.GeometryCollectionTrait, g::GeometryCollection) = GI.ncoord(first(g))
GI.ngeom(::GI.GeometryCollectionTrait, g::GeometryCollection) = length(g)
Expand All @@ -62,7 +63,7 @@ GI.nfeature(::GI.FeatureCollectionTrait, fc::FeatureCollection) = length(fc)
# Any GeoJSON Object
function GI.extent(x::GeoJSONObject)
bb = bbox(x)
if isnothing(bb)
if bb === nothing
return nothing
else
if length(bb) == 4
Expand Down
56 changes: 29 additions & 27 deletions src/geometries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,44 @@ struct GeometryCollection{T} <: Geometry
end

# Construct using keywords that become a NamedTuple object.
# Useful for constructing geometries yourself rather than
# reading a JSON string, which gives JSON3.Object.
Point(; kwargs...) = Point(merge((type = "Point",), kwargs))
LineString(; kwargs...) = LineString(merge((type = "LineString",), kwargs))
Polygon(; kwargs...) = Polygon(merge((type = "Polygon",), kwargs))
MultiPoint(; kwargs...) = MultiPoint(merge((type = "MultiPoint",), kwargs))
MultiLineString(; kwargs...) = MultiLineString(merge((type = "MultiLineString",), kwargs))
MultiPolygon(; kwargs...) = MultiPolygon(merge((type = "MultiPolygon",), kwargs))
GeometryCollection(; kwargs...) =
GeometryCollection(merge((type = "GeometryCollection",), kwargs))

# if the only argument is an AbstractVector it can be interpreted as the coordinates
Point(c::AbstractVector; kwargs...) =
Point(merge((type = "Point", coordinates = c), kwargs))
LineString(c::AbstractVector; kwargs...) =
LineString(merge((type = "LineString", coordinates = c), kwargs))
Polygon(c::AbstractVector; kwargs...) =
Polygon(merge((type = "Polygon", coordinates = c), kwargs))
MultiPoint(c::AbstractVector; kwargs...) =
MultiPoint(merge((type = "MultiPoint", coordinates = c), kwargs))
MultiLineString(c::AbstractVector; kwargs...) =
MultiLineString(merge((type = "MultiLineString", coordinates = c), kwargs))
MultiPolygon(c::AbstractVector; kwargs...) =
MultiPolygon(merge((type = "MultiPolygon", coordinates = c), kwargs))
GeometryCollection(g::AbstractVector; kwargs...) =
GeometryCollection(merge((type = "GeometryCollection", geometries = c), kwargs))
Point(; coordinates, kwargs...) = Point(merge((type = "Point", coordinates), kwargs))
LineString(; coordinates, kwargs...) =
LineString(merge((type = "LineString", coordinates), kwargs))
Polygon(; coordinates, kwargs...) = Polygon(merge((type = "Polygon", coordinates), kwargs))
MultiPoint(; coordinates, kwargs...) =
MultiPoint(merge((type = "MultiPoint", coordinates), kwargs))
MultiLineString(; coordinates, kwargs...) =
MultiLineString(merge((type = "MultiLineString", coordinates), kwargs))
MultiPolygon(; coordinates, kwargs...) =
MultiPolygon(merge((type = "MultiPolygon", coordinates), kwargs))
GeometryCollection(; geometries, kwargs...) =
GeometryCollection(merge((type = "GeometryCollection", geometries), kwargs))

coordinates(g::Geometry) = object(g).coordinates
coordinates(g::GeometryCollection) = geometries(g)
geometries(g::GeometryCollection) = object(g).geometries
Base.propertynames(g::Geometry) = propertynames(object(g))
Base.getproperty(g::Geometry, nm::Symbol) = getproperty(object(g), nm)

function Base.iterate(g::Geometry)
x = iterate(coordinates(g))
x === nothing && return nothing
val, state = x
return val, state
end

function Base.iterate(g::Geometry, state)
x = iterate(coordinates(g), state)
x === nothing && return nothing
val, state = x
return val, state
end

# read only partial array interface like JSON3.Array
Base.size(g::Geometry) = size(coordinates(g))
Base.getindex(g::Geometry, i::Int) = getindex(coordinates(g), i::Int)
Base.IndexStyle(::Type{<:Geometry}) = Base.IndexLinear()
Base.iterate(g::Geometry, st = (1, 3)) = iterate(coordinates(g), st)
Base.length(g::Geometry) = length(coordinates(g))

Base.show(io::IO, g::Geometry) = print(io, type(g))
Base.show(io::IO, g::Point) = print(io, type(g), '(', coordinates(g), ')')
27 changes: 11 additions & 16 deletions src/json.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@ function read(source)
if object_type == "FeatureCollection"
features = get(object, :features, nothing)
features isa JSON3.Array || error("GeoJSON field \"features\" is not an array")
feature_object_type = isempty(features) ? Any : typeof(first(features))
return FeatureCollection{
Feature{feature_object_type},
typeof(object),
typeof(features),
}(
object,
features,
)
return FeatureCollection(object)
elseif object_type == "Feature"
return Feature(object)
elseif object_type === nothing
Expand Down Expand Up @@ -85,20 +77,22 @@ _lower(::GI.AbstractGeometryCollectionTrait, obj) =

_add_bbox(::Nothing, nt::NamedTuple) = nt
function _add_bbox(ext::Extents.Extent, nt::NamedTuple)
bbox = [ext.X[1], ext.Y[1], ext.X[2], ext.Y[2]]
if haskey(ext, :Z)
bbox = [ext.X[1], ext.Y[1], ext.Z[1], ext.X[2], ext.Y[2], ext.Z[2]]
else
bbox = [ext.X[1], ext.Y[1], ext.X[2], ext.Y[2]]
end
merge(nt, (; bbox))
end

miss(x) = ifelse(x === nothing, missing, x)

"""
geometry(g::JSON3.Object)
geometry(g)

Convert a GeoJSON geometry from JSON object to a struct specific
Convert a GeoJSON geometry from JSON style object to a struct specific
to that geometry type.
"""
function geometry(g::JSON3.Object)
t = g.type
function geometry(g)
t = type(g)
if t == "Point"
Point(g)
elseif t == "LineString"
Expand All @@ -117,4 +111,5 @@ function geometry(g::JSON3.Object)
throw(ArgumentError("Unknown geometry type $t"))
end
end
geometry(g::Geometry) = g
geometry(::Nothing) = nothing
Loading