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 a GEOS algorithm in an extension #100

Merged
merged 35 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
87906e8
Move types to a new `types.jl` file
asinghvi17 Apr 5, 2024
d630a20
Add GEOSCorrection and LibGEOSExt
asinghvi17 Apr 5, 2024
628e14e
Minor fixes
asinghvi17 Apr 7, 2024
1b8ad33
Write out stubs for not-yet-implemented functions
asinghvi17 Apr 7, 2024
e768045
Implement `GEOS` algorithm
asinghvi17 Apr 7, 2024
b6f36ed
Update ext/GeometryOpsLibGEOSExt/segmentize.jl
asinghvi17 Apr 10, 2024
2a99ce3
switch from typed keys to no keys
asinghvi17 Apr 14, 2024
75fe1dc
fix erroneous typing in buffer
asinghvi17 Apr 17, 2024
88d6acf
add test skeleton + move enforce to GO proper
asinghvi17 Apr 17, 2024
0c23cbb
Switch to SafeTestsets for tests
asinghvi17 Apr 20, 2024
f852c43
Test segmentize properly
asinghvi17 Apr 20, 2024
29dbaec
Apply suggestions from code review
asinghvi17 Apr 20, 2024
1ee00bb
Fix tests
asinghvi17 Apr 20, 2024
4415982
Allow LibGEOS to perform natural conversion on DE-9IM and poly set ops
asinghvi17 Apr 20, 2024
ed0d3b9
Describe exactly why the filter statement exists in the extension
asinghvi17 Apr 20, 2024
6d57bc3
Remove GEOS correction (not working yet)
asinghvi17 Jun 9, 2024
b45f3d3
Add TopologyPreserve simplification method
asinghvi17 Jun 9, 2024
d717e8e
Add a basic comment to not_implemented_yet.jl
asinghvi17 Jun 9, 2024
5f9ed1d
Add a "default" implementation for buffer that returns GO geoms
asinghvi17 Jun 9, 2024
b6c216a
Switch back to explicit conversion to LG in all simple overrides
asinghvi17 Jun 9, 2024
04e1c67
Fix incorrect references
asinghvi17 Jun 9, 2024
52254d6
Add Downloads and NaturalEarth to the test env
asinghvi17 Jun 9, 2024
6c2a539
Fix primitive tests
asinghvi17 Jun 9, 2024
1993d1e
Add a hack for LibGEOS geometrycollection conversion
asinghvi17 Jun 9, 2024
0e97a22
Fix accidental swapping in x and y in test
asinghvi17 Jun 9, 2024
39f2307
Add overlaps testing for GEOS
asinghvi17 Jun 9, 2024
e41f2f2
Bump version to v0.1.7 (#150)
asinghvi17 Jun 9, 2024
a99d1ca
Set compat for LibGEOS to be after the GeometryCollection patch
asinghvi17 Jun 10, 2024
ba04065
Force `simplify` to always be called with GEOS() for GEOS functions
asinghvi17 Jun 10, 2024
c46c2d8
Go back to regular testsets from SafeTestsets
asinghvi17 Jun 11, 2024
458b62b
Revert the geom relations tests for #135
asinghvi17 Jun 11, 2024
5b0e2a5
Get simplify working as well
asinghvi17 Jun 11, 2024
e02e332
Activate the LibGEOS listed tests
asinghvi17 Jun 11, 2024
60cac60
Fix the segmentize test
asinghvi17 Jun 11, 2024
a06a683
Fix buffer
asinghvi17 Jun 11, 2024
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
11 changes: 9 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
name = "GeometryOps"
uuid = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab"
authors = ["Anshul Singhvi <anshulsinghvi@gmail.com> and contributors"]
version = "0.1.6"
version = "0.1.7"

[deps]
CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
ExactPredicates = "429591f6-91af-11e9-00e2-59fbe8cec110"
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
SortTileRecursiveTree = "746ee33f-1797-42c2-866d-db2fce69d14d"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"

[weakdeps]
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
FlexiJoins = "e37f2e79-19fa-4eb7-8510-b63b51fe0a37"
Proj = "c94c279d-25a6-4763-9509-64d165bea63e"

[extensions]
GeometryOpsLibGEOSExt = "LibGEOS"
GeometryOpsFlexiJoinsExt = "FlexiJoins"
GeometryOpsProjExt = "Proj"

Expand All @@ -27,6 +30,7 @@ ExactPredicates = "2.2.8"
FlexiJoins = "0.1.30"
GeoInterface = "1.2"
GeometryBasics = "0.4.7"
LibGEOS = "0.9.2"
LinearAlgebra = "1"
Proj = "1"
SortTileRecursiveTree = "0.1"
Expand All @@ -40,17 +44,20 @@ CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
FlexiJoins = "e37f2e79-19fa-4eb7-8510-b63b51fe0a37"
GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f"
GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689"
NaturalEarth = "436b0209-26ab-4e65-94a9-6526d86fea76"
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
Proj = "c94c279d-25a6-4763-9509-64d165bea63e"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["ArchGDAL", "CoordinateTransformations", "DataFrames", "Distributions", "DimensionalData", "FlexiJoins", "GeoFormatTypes", "GeoJSON", "Proj", "JLD2", "LibGEOS", "Random", "Rasters", "OffsetArrays", "Shapefile", "Test"]
test = ["ArchGDAL", "CoordinateTransformations", "DataFrames", "Distributions", "DimensionalData", "Downloads", "FlexiJoins", "GeoFormatTypes", "GeoJSON", "Proj", "JLD2", "LibGEOS", "Random", "Rasters", "NaturalEarth", "OffsetArrays", "SafeTestsets", "Shapefile", "Test"]
24 changes: 24 additions & 0 deletions ext/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module GeometryOpsLibGEOSExt

import GeometryOps as GO, LibGEOS as LG
import GeometryOps: GI

import GeometryOps: GEOS, enforce

using GeometryOps
# The filter statement is required because in Julia, each module has its own versions of these
# functions, which serve to evaluate or include code inside the scope of the module.
# However, if you import those from another module (which you would with `all=true`),
# that creates an ambiguity which causes a warning during precompile/load time.
# In order to avoid this, we filter out these special functions.
for name in filter(!in((:var"#eval", :eval, :var"#include", :include)), names(GeometryOps; all = true))
@eval using GeometryOps: $name
end

include("buffer.jl")
include("segmentize.jl")
include("simplify.jl")

include("simple_overrides.jl")

end
33 changes: 33 additions & 0 deletions ext/GeometryOpsLibGEOSExt/buffer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const _GEOS_CAPSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufCapStyles}(
:round => LG.GEOSBUF_CAP_ROUND,
:flat => LG.GEOSBUF_CAP_FLAT,
:square => LG.GEOSBUF_CAP_SQUARE,
)

const _GEOS_JOINSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufJoinStyles}(
:round => LG.GEOSBUF_JOIN_ROUND,
:mitre => LG.GEOSBUF_JOIN_MITRE,
:bevel => LG.GEOSBUF_JOIN_BEVEL,
)

to_cap_style(style::Symbol) = _GEOS_CAPSTYLE_LOOKUP[style]
to_cap_style(style::LG.GEOSBufCapStyles) = style
to_cap_style(num::Integer) = num

to_join_style(style::Symbol) = _GEOS_JOINSTYLE_LOOKUP[style]
to_join_style(style::LG.GEOSBufJoinStyles) = style
to_join_style(num::Integer) = num

function GO.buffer(alg::GEOS, geometry, distance)
# The reason we use apply here is so that this also works with featurecollections,
# tables, vectors of geometries, etc!
return apply(TraitTarget{GI.AbstractGeometryTrait}(), geometry) do geom
LG.bufferWithStyle(
GI.convert(LG, geom), distance;
quadsegs = get(alg, :quadsegs, 8),
endCapStyle = to_cap_style(get(alg, :endCapStyle, :round)),
joinStyle = to_join_style(get(alg, :joinStyle, :round)),
mitreLimit = get(alg, :mitreLimit, 5.0),
)
end
end
30 changes: 30 additions & 0 deletions ext/GeometryOpsLibGEOSExt/segmentize.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# # Segmentize
import GeometryOps: segmentize, apply

#=
This file implements the LibGEOS segmentization method for GeometryOps.
=#

function _segmentize_geos(geom::LG.AbstractGeometry, max_distance)
context = LG.get_context(geom)
result = LG.GEOSDensify_r(context, geom, max_distance)
if result == C_NULL
error("LibGEOS: Error in GEOSDensify")
end
return LG.geomFromGEOS(result, context)
end

_segmentize_geos(geom, max_distance) = _segmentize_geos(GI.convert(LG, geom), max_distance)

# 2 behaviours:
# - enforce: enforce the presence of a kwargs
# - fetch: fetch the value of a kwargs, or return a default value
@inline function GO.segmentize(alg::GEOS, geom; threaded::Union{Bool, GO.BoolsAsTypes} = _False())
max_distance = enforce(alg, :max_distance, GO.segmentize)
return GO.apply(
Base.Fix2(_segmentize_geos, max_distance),
GO.TraitTarget(GI.GeometryCollectionTrait(), GI.MultiPolygonTrait(), GI.PolygonTrait(), GI.MultiLineStringTrait(), GI.LineStringTrait(), GI.LinearRingTrait(), GI.MultiPointTrait(), GI.PointTrait()),
geom;
threaded
)
end
66 changes: 66 additions & 0 deletions ext/GeometryOpsLibGEOSExt/simple_overrides.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#=
# Simple overrides
This file contains simple overrides for GEOS, essentially only those
functions which have direct counterparts in LG and only
require conversion before calling.
=#
# ## Polygon set operations
# ### Difference
function GO.difference(::GEOS, geom_a, geom_b; target=nothing)
return LG.difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Union
function GO.union(::GEOS, geom_a, geom_b; target=nothing)
return LG.union(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Intersection
function GO.intersection(::GEOS, geom_a, geom_b; target=nothing)
return LG.intersection(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Symmetric difference
function GO.symdifference(::GEOS, geom_a, geom_b; target=nothing)
return LG.symmetric_difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end

# ## DE-9IM boolean methods
# ### Equals
function GO.equals(::GEOS, geom_a, geom_b)
return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Disjoint
function GO.disjoint(::GEOS, geom_a, geom_b)
return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Touches
function GO.touches(::GEOS, geom_a, geom_b)
return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Crosses
function GO.crosses(::GEOS, geom_a, geom_b)
return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Within
function GO.within(::GEOS, geom_a, geom_b)
return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Contains
function GO.contains(::GEOS, geom_a, geom_b)
return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Overlaps
function GO.overlaps(::GEOS, geom_a, geom_b)
return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Covers
function GO.covers(::GEOS, geom_a, geom_b)
return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### CoveredBy
function GO.coveredby(::GEOS, geom_a, geom_b)
return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end
# ### Intersects
function GO.intersects(::GEOS, geom_a, geom_b)
return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
end

31 changes: 31 additions & 0 deletions ext/GeometryOpsLibGEOSExt/simplify.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Address potential ambiguities
GO._simplify(::GI.PointTrait, ::GO.GEOS, geom; kw...) = geom
GO._simplify(::GI.MultiPointTrait, ::GO.GEOS, geom; kw...) = geom

function GO._simplify(::GI.AbstractGeometryTrait, alg::GO.GEOS, geom; kwargs...)
method = get(alg, :method, :TopologyPreserve)
@assert haskey(alg.params, :tol) """
The `:tol` parameter is required for the GEOS algorithm in `simplify`,
but it was not provided.

Provide it by passing `GEOS(; tol = ...,) as the algorithm.
"""
tol = alg.params.tol
if method == :TopologyPreserve
return LG.topologyPreserveSimplify(GI.convert(LG, geom), tol)
elseif method == :DouglasPeucker
return LG.simplify(GI.convert(LG, geom), tol)
else
error("Invalid method passed to `GO.simplify(GEOS(...), ...)`: $method. Please use :TopologyPreserve or :DouglasPeucker")
end
end

function GO._simplify(trait::GI.AbstractCurveTrait, alg::GO.GEOS, geom; kw...)
Base.invoke(
GO._simplify,
Tuple{GI.AbstractGeometryTrait, GO.GEOS, typeof(geom)},
trait, alg, geom;
kw...
)
end

4 changes: 4 additions & 0 deletions src/GeometryOps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ const GB = GeometryBasics
const TuplePoint{T} = Tuple{T, T} where T <: AbstractFloat
const Edge{T} = Tuple{TuplePoint{T},TuplePoint{T}} where T

include("types.jl")
include("primitives.jl")
include("utils.jl")
include("not_implemented_yet.jl")

include("methods/angles.jl")
include("methods/area.jl")
include("methods/barycentric.jl")
include("methods/buffer.jl")
include("methods/centroid.jl")
include("methods/distance.jl")
include("methods/equals.jl")
Expand Down Expand Up @@ -70,6 +73,7 @@ function __init__()
# Handle all available errors!
Base.Experimental.register_error_hint(_reproject_error_hinter, MethodError)
Base.Experimental.register_error_hint(_geodesic_segments_error_hinter, MethodError)
Base.Experimental.register_error_hint(_buffer_error_hinter, MethodError)
end

end
27 changes: 27 additions & 0 deletions src/methods/buffer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#=
# Buffer

Buffering a geometry means computing the region `distance` away from it, and returning
that region as the new geometry.

As of now, we only support `GEOS` as the backend, meaning that LibGEOS must be loaded.
=#

function buffer(geometry, distance; kwargs...)
buffered = buffer(GEOS(; kwargs...), geometry, distance)
return tuples(buffered)
end

# Below is an error handler similar to the others we have for e.g. segmentize,
# which checks if there is a method error for the geos backend.


# Add an error hint for GeodesicSegments if Proj is not loaded!
function _buffer_error_hinter(io, exc, argtypes, kwargs)
if isnothing(Base.get_extension(GeometryOps, :GeometryOpsLibGEOSExt)) && exc.f == buffer && first(exc.argtypes) == GEOS
print(io, "\n\nThe `buffer` method requires the LibGEOS.jl package to be explicitly loaded.\n")
print(io, "You can do this by simply typing ")
printstyled(io, "using LibGEOS"; color = :cyan, bold = true)
println(io, " in your REPL, \nor otherwise loading LibGEOS.jl via using or import.")
end
end
9 changes: 9 additions & 0 deletions src/not_implemented_yet.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# # Not implemented yet
# All of the functions in this file are not implemented in Julia yet.
# Some of them may have implementations in LibGEOS which we can use
# via an extension, but there is no native-Julia implementation for them.

function symdifference end
function buffer end
function convexhull end
function concavehull end
46 changes: 0 additions & 46 deletions src/primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,52 +53,6 @@ TraitTarget

=#


#=
We pass `threading` and `calc_extent` as types, not simple boolean values.

This is to help compilation - with a type to hold on to, it's easier for
the compiler to separate threaded and non-threaded code paths.

Note that if we didn't include the parent abstract type, this would have been really
type unstable, since the compiler couldn't tell what would be returned!

We had to add the type annotation on the `_booltype(::Bool)` method for this reason as well.
=#
abstract type BoolsAsTypes end
struct _True <: BoolsAsTypes end
struct _False <: BoolsAsTypes end

@inline _booltype(x::Bool)::BoolsAsTypes = x ? _True() : _False()
@inline _booltype(x::BoolsAsTypes)::BoolsAsTypes = x

"""
TraitTarget{T}

This struct holds a trait parameter or a union of trait parameters.

It is primarily used for dispatch into methods which select trait levels,
like `apply`, or as a parameter to `target`.

## Constructors
```julia
TraitTarget(GI.PointTrait())
TraitTarget(GI.LineStringTrait(), GI.LinearRingTrait()) # and other traits as you may like
TraitTarget(TraitTarget(...))
# There are also type based constructors available, but that's not advised.
TraitTarget(GI.PointTrait)
TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})
# etc.
```

"""
struct TraitTarget{T} end
TraitTarget(::Type{T}) where T = TraitTarget{T}()
TraitTarget(::T) where T<:GI.AbstractTrait = TraitTarget{T}()
TraitTarget(::TraitTarget{T}) where T = TraitTarget{T}()
TraitTarget(::Type{<:TraitTarget{T}}) where T = TraitTarget{T}()
TraitTarget(traits::GI.AbstractTrait...) = TraitTarget{Union{map(typeof, traits)...}}()

const THREADED_KEYWORD = "- `threaded`: `true` or `false`. Whether to use multithreading. Defaults to `false`."
const CRS_KEYWORD = "- `crs`: The CRS to attach to geometries. Defaults to `nothing`."
const CALC_EXTENT_KEYWORD = "- `calc_extent`: `true` or `false`. Whether to calculate the extent. Defaults to `false`."
Expand Down
2 changes: 1 addition & 1 deletion src/transformations/segmentize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ end
function segmentize(method::SegmentizeMethod, geom; threaded::Union{Bool, BoolsAsTypes} = _False())
@assert method.max_distance > 0 "`max_distance` should be positive and nonzero! Found $(method.max_distance)."
segmentize_function = Base.Fix1(_segmentize, method)
return apply(segmentize_function, Union{GI.LinearRingTrait, GI.LineStringTrait}, geom; threaded)
return apply(segmentize_function, TraitTarget(GI.LinearRingTrait(), GI.LineStringTrait()), geom; threaded)
end

_segmentize(method, geom) = _segmentize(method, geom, GI.trait(geom))
Expand Down
Loading
Loading