-
Notifications
You must be signed in to change notification settings - Fork 5
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
Add a convex hull algorithm #160
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
aeaa09f
Implement a convex hull algorithm
asinghvi17 c600313
Update src/methods/convex_hull.jl
asinghvi17 db0ce48
Refactor name to `MonotoneChainMethod
asinghvi17 d3adab7
clean up
asinghvi17 0c92a84
Merge branch 'main' into as/deltri
asinghvi17 44dbe49
import Tables
asinghvi17 7139ec5
Minor fixes
asinghvi17 73cd12b
Add tests
asinghvi17 be75a24
Fix comments + test against LibGEOS
asinghvi17 e0e189f
DelTri -> DelaunayTriangulation
asinghvi17 e3a1b39
Add GEOS convex hull algorithm, use it in tests
asinghvi17 69d8984
Add more comments to the source file
asinghvi17 2516a91
Talk about winding order differences here
asinghvi17 27f9067
Make the code a bit clearer
asinghvi17 d940e0d
Fix tests - not sure how this worked locally...
asinghvi17 26eb885
Fix doc build
asinghvi17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
#= | ||
# Convex hull | ||
|
||
The [_convex hull_](https://en.wikipedia.org/wiki/Convex_hull) of a set of points is the smallest [**convex**](https://en.wikipedia.org/wiki/Convex_set) polygon that contains all the points. | ||
|
||
GeometryOps.jl provides a number of methods for computing the convex hull of a set of points, usually | ||
linked to other Julia packages. | ||
|
||
For now, we expose one algorithm, [MonotoneChainMethod](@ref), which uses the [DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) | ||
package. The `GEOS()` interface also supports convex hulls. | ||
|
||
Future work could include other algorithms, such as [Quickhull.jl](https://github.com/augustt198/Quickhull.jl), or similar, via package extensions. | ||
|
||
|
||
## Example | ||
|
||
### Simple hull | ||
```@example simple | ||
import GeometryOps as GO, GeoInterface as GI | ||
using CairoMakie # to plot | ||
|
||
points = randn(GO.Point2f, 100) | ||
f, a, p = plot(points; label = "Points") | ||
hull_poly = GO.convex_hull(points) | ||
lines!(a, hull_poly; label = "Convex hull", color = Makie.wong_colors()[2]) | ||
axislegend(a) | ||
f | ||
``` | ||
|
||
## Convex hull of the USA | ||
```@example usa | ||
import GeometryOps as GO, GeoInterface as GI | ||
using CairoMakie # to plot | ||
using NaturalEarth # for data | ||
|
||
all_adm0 = naturalearth("admin_0_countries", 110) | ||
usa = all_adm0.geometry[findfirst(==("USA"), all_adm0.ADM0_A3)] | ||
f, a, p = lines(usa) | ||
lines!(a, GO.convex_hull(usa); color = Makie.wong_colors()[2]) | ||
f | ||
``` | ||
|
||
## Investigating the winding order | ||
|
||
The winding order of the monotone chain method is counterclockwise, | ||
while the winding order of the GEOS method is clockwise. | ||
|
||
GeometryOps' convexity detection says that the GEOS hull is convex, | ||
while the monotone chain method hull is not. However, they are both going | ||
over the same points (we checked), it's just that the winding order is different. | ||
|
||
In reality, both sets are convex, but we need to fix the GeometryOps convexity detector | ||
([`isconcave`](@ref))! | ||
|
||
We may also decide at a later date to change the returned winding order of the polygon, but | ||
most algorithms are robust to that, and you can always [`fix`](@ref) it... | ||
|
||
```@example windingorder | ||
import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG | ||
using CairoMakie # to plot | ||
|
||
points = rand(Point2{Float64}, 100) | ||
go_hull = GO.convex_hull(GO.MonotoneChainMethod(), points) | ||
lg_hull = GO.convex_hull(GO.GEOS(), points) | ||
|
||
fig = Figure() | ||
a1, p1 = lines(fig[1, 1], go_hull; color = 1:GI.npoint(go_hull), axis = (; title = "MonotoneChainMethod()")) | ||
a2, p2 = lines(fig[2, 1], lg_hull; color = 1:GI.npoint(lg_hull), axis = (; title = "GEOS()")) | ||
cb = Colorbar(fig[1:2, 2], p1; label = "Vertex number") | ||
fig | ||
``` | ||
|
||
## Implementation | ||
|
||
=# | ||
|
||
""" | ||
convex_hull([method], geometries) | ||
|
||
Compute the convex hull of the points in `geometries`. | ||
Returns a `GI.Polygon` representing the convex hull. | ||
|
||
Note that the polygon returned is wound counterclockwise | ||
as in the Simple Features standard by default. If you | ||
choose GEOS, the winding order will be inverted. | ||
|
||
!!! warning | ||
This interface only computes the 2-dimensional convex hull! | ||
|
||
For higher dimensional hulls, use the relevant package (Qhull.jl, Quickhull.jl, or similar). | ||
""" | ||
function convex_hull end | ||
|
||
""" | ||
MonotoneChainMethod() | ||
|
||
This is an algorithm for the [`convex_hull`](@ref) function. | ||
|
||
Uses [`DelaunayTriangulation.jl`](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) to compute the convex hull. | ||
This is a pure Julia algorithm which provides an optimal Delaunay triangulation. | ||
|
||
See also [`convex_hull`](@ref) | ||
""" | ||
struct MonotoneChainMethod end | ||
|
||
# GrahamScanMethod, etc. can be implemented in GO as well, if someone wants to. | ||
# If we add an extension on Quickhull.jl, then that would be another algorithm. | ||
|
||
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries) | ||
|
||
# TODO: have this respect the CRS by pulling it out of `geometries`. | ||
function convex_hull(::MonotoneChainMethod, geometries) | ||
# Extract all points as tuples. We have to collect and allocate | ||
# here, because DelaunayTriangulation only accepts vectors of | ||
# point-like geoms. | ||
|
||
# Cleanest would be to use the iterable from GO.flatten directly, | ||
# but that would require us to implement the convex hull algorithm | ||
# directly. | ||
|
||
# TODO: create a specialized method that extracts only the information | ||
# required, GeometryBasics points can be passed through directly. | ||
points = collect(flatten(tuples, GI.PointTrait, geometries)) | ||
# Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl). | ||
hull = DelaunayTriangulation.convex_hull(points) | ||
# Convert the result to a `GI.Polygon` and return it. | ||
# View would be more efficient here, but re-allocating | ||
# is cleaner. | ||
point_vec = DelaunayTriangulation.get_points(hull)[DelaunayTriangulation.get_vertices(hull)] | ||
return GI.Polygon([GI.LinearRing(point_vec)]) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
using Test | ||
import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG | ||
|
||
@testset "Basic example" begin | ||
points = tuple.(rand(100), rand(100)) | ||
hull = GO.convex_hull(points) | ||
@test !GO.isconcave(hull) skip=true # TODO: fix | ||
@test !GO.isclockwise(GI.getexterior(hull)) # exterior should be ccw | ||
@test all(x -> GO.covers(hull, x), points) # but the orientation is right | ||
# Test against LibGEOS, by testing that all the points are the same | ||
# This is robust against winding order and starting at a different point, etc. | ||
@test isempty( | ||
setdiff( | ||
collect(GO.flatten(GO.tuples, GI.PointTrait, hull)), | ||
collect(GO.flatten(GO.tuples, GI.PointTrait, GO.convex_hull(GEOS(), points))) | ||
) | ||
) | ||
end | ||
|
||
@testset "Duplicated points" begin | ||
points = tuple.(rand(100), rand(100)) | ||
@test_nowarn hull = GO.convex_hull(vcat(points, points)) | ||
single_hull = GO.convex_hull(points) | ||
double_hull = GO.convex_hull(vcat(points, points)) | ||
|
||
@test GO.equals(GI.getexterior(single_hull), GI.getexterior(double_hull)) | ||
@test !GO.isconcave(double_hull) skip=true # TODO: fix | ||
@test !GO.isclockwise(GI.getexterior(double_hull)) # exterior should be ccw | ||
@test all(x -> GO.covers(single_hull, x), points) | ||
@test all(x -> GO.covers(double_hull, x), points) | ||
# Test against LibGEOS, by testing that all the points are the same | ||
# This is robust against winding order and starting at a different point, etc. | ||
@test isempty( | ||
setdiff( | ||
collect(GO.flatten(GO.tuples, GI.PointTrait, double_hull)), | ||
collect(GO.flatten(GO.tuples, GI.PointTrait, GO.convex_hull(GEOS(), points))) | ||
) | ||
) | ||
end | ||
|
||
# The rest of the tests are handled in DelaunayTriangulation.jl, this is simply testing | ||
# that the methods work as expected. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this true? You can provide whatever point types you like so long as you extend the interfaces (which are hopefully sufficiently documented) from DelaunayTriangulation.jl appropriately. I give a complete example here https://juliageometry.github.io/DelaunayTriangulation.jl/dev/tutorials/custom_primitive/, maybe something goes wrong in
convex_hull
that I'm not aware of?Highlighted the wrong part of the text for this comment but I'm referring to the need to flatten and collect the points
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to flatten anyway to get a vector, and getting x and y values from C pointers is pretty bad for performance - best to do it once. I can update the comment with better verbiage, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plus, we'd have to extend the interface for every GeoInterface compatible type, which would probably mean either piracy or changes on the DelaunayTriangulation end. (Though if you do want to allow GeoInterface as a fallback it would be pretty easy I think).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no preference either way just wanted to see if you were aware (I wasn't aware of the need for getting the values from C pointers, so fair enough). The verbiage is alright
It would probably be welcome as a package extension in DelaunayTriangulation. I'm not familiar with the interfaces though, so I probably won't be delving into that. Just leaving the code as is is fine anyway, it works and would be fast enough :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably just be: