-
Notifications
You must be signed in to change notification settings - Fork 9
Sg/voronoi #355
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
Open
skygering
wants to merge
13
commits into
main
Choose a base branch
from
sg/voronoi
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Sg/voronoi #355
Changes from 9 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1764ac1
Add a basic voronoi function
asinghvi17 ca402ef
Expand clip polygon options
skygering d1c1830
Add in another option for clip polygon
skygering bbc32b0
Merge branch 'main' into sg/voronoi
skygering a996989
Update package name
skygering e41728b
Update float type options
skygering 23ba648
Get rid of point check
skygering 48ddaca
Add back in number point check
skygering 67cff88
Clean up file
skygering 865a54c
clean up input clip_polygon
skygering dbe4a18
Add tests and update docs
skygering 85627f0
Pare down doctests
skygering 02260f4
Fixed doctest bug
skygering 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 hidden or 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 hidden or 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,114 @@ | ||
| #= | ||
| # Voronoi Tessellation | ||
|
|
||
| The [_Voronoi tessellation_](https://en.wikipedia.org/wiki/Voronoi_diagram) of a set of points is a partitioning of the plane into regions based on distance to points. | ||
| Each region contains all points closer to one generator point than to any other. | ||
|
|
||
| GeometryOps.jl provides a method for computing the Voronoi tessellation of a set of points, | ||
| using the [DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) package. | ||
|
|
||
| ## Example | ||
|
|
||
| ### Simple tessellation | ||
| ```@example simple | ||
| import GeometryOps as GO, GeoInterface as GI | ||
| using CairoMakie # to plot | ||
|
|
||
| points = tuple.(randn(20), randn(20)) | ||
| polygons = GO.voronoi(points) | ||
| f, a, p = plot(polygons[1]; label = "Voronoi cell 1") | ||
| for (i, poly) in enumerate(polygons[2:end]) | ||
| plot!(a, poly; label = "Voronoi cell $(i+1)") | ||
| end | ||
| scatter!(a, points; color = :black, markersize = 10, label = "Generators") | ||
| axislegend(a) | ||
| f | ||
| ``` | ||
|
|
||
| ## Implementation | ||
|
|
||
| This implementation mainly just preforms some assertion checks before passing the Arguments | ||
| to the DelaunayTriangulation package. It then unpacks the voronoi output into GeoInterface | ||
| polygons, whose point match the float type input by the user. | ||
| =# | ||
|
|
||
| struct __NoCRSProvided end | ||
|
|
||
| """ | ||
| voronoi(geometries; clip = true) | ||
|
|
||
| Compute the Voronoi tessellation of the points in `geometries`. | ||
| Returns a vector of `GI.Polygon` objects representing the Voronoi cells, | ||
| in the same order as the input points. | ||
|
|
||
| ## Arguments | ||
| - `geometries`: Any GeoInterface-compatible geometry or collection of geometries | ||
| that can be decomposed into points | ||
|
|
||
| ## Keyword Arguments | ||
| - `clip`: Whether to clip the Voronoi cells to the convex hull of the input points (default: true) | ||
|
|
||
| !!! warning | ||
| This interface only computes the 2-dimensional Voronoi tessellation! | ||
|
|
||
| !!! note | ||
| The polygons are returned in the same order as the input points after flattening. | ||
| Each polygon corresponds to the Voronoi cell of the point at the same index. | ||
| """ | ||
| function voronoi(geometries, ::Type{T} = Float64; kwargs...) where T | ||
| return voronoi(Planar(), geometries, T; kwargs...) | ||
| end | ||
|
|
||
| function voronoi(::Planar, geometries, ::Type{T} = Float64; clip = true, clip_polygon = nothing, crs = __NoCRSProvided()) where T | ||
| # Extract all points as tuples using GO.flatten | ||
| # This handles any GeoInterface-compatible input | ||
| points_iter = collect(flatten(tuples, GI.PointTrait, geometries)) | ||
| if crs isa __NoCRSProvided | ||
| crs = GI.crs(geometries) | ||
| # if isnothing(crs) | ||
| # crs = GI.crs(first(points_iter)) | ||
| # end | ||
| end | ||
| # if we have not figured it out yet, we can't do anything | ||
| if crs isa __NoCRSProvided | ||
| error("This code should be unreachable; please file an issue at https://github.com/JuliaGeometry/GeometryOps.jl/issues with the stacktrace and a reproducible example.") | ||
| end | ||
|
|
||
| # Handle edge case of too few points | ||
| if length(points_iter) < 3 | ||
| throw(ArgumentError("Voronoi tessellation requires at least 3 points, got $(length(points_iter))")) | ||
| end | ||
|
|
||
| # Compute Delaunay triangulation | ||
| tri = DelTri.triangulate(points_iter) | ||
|
|
||
| # Compute Voronoi tessellation from the triangulation | ||
| _clip_polygon = if isnothing(clip_polygon) | ||
| nothing | ||
| elseif GI.geomtrait(clip_polygon) isa GI.PolygonTrait | ||
| @assert GI.nhole(clip_polygon) == 0 | ||
| (collect(flatten(tuples, GI.PointTrait, clip_polygon)), collect(1:GI.npoint(clip_polygon))) | ||
| elseif clip_polygon isa Tuple{Vector{Tuple{T, T}}, Vector{Int}} | ||
| clip_polygon | ||
| elseif clip_polygon isa Tuple{NTuple{<:Any, Tuple{T, T}}, NTuple{<:Any, Int}} | ||
| clip_polygon | ||
| else | ||
| error("Clip polygon must be a polygon or other recognizable form, see the docstring for `DelaunayTriangulation.voronoi` for the recognizable form. Was neither, got $(typeof(clip_polygon))") | ||
| end | ||
| vorn = DelTri.voronoi(tri; clip = clip, clip_polygon = _clip_polygon) | ||
|
|
||
| polygons = GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{T, T}}, Nothing, Nothing}}, Nothing, typeof(crs)}[] | ||
| sizehint!(polygons, DelTri.num_polygons(vorn)) | ||
| # Implementation below copied from Makie.jl | ||
| # see https://github.com/MakieOrg/Makie.jl/blob/687c4466ce00154714297e36a7f610443c6ad5be/Makie/src/basic_recipes/voronoiplot.jl#L101-L110 | ||
| for i in DelTri.each_generator(vorn) | ||
| !DelTri.has_polygon(vorn, i) && continue | ||
| polygon_coords = DelTri.getxy.(DelTri.get_polygon_coordinates(vorn, i)) | ||
| push!(polygons, GI.Polygon([GI.LinearRing(polygon_coords)], crs = crs)) | ||
| # The code below gets the generator point, but we don't need it | ||
| # gp = DelTri.getxy(DelTri.get_generator(vorn, i)) | ||
| # !isempty(polygon_coords) && push!(generators, gp) | ||
| end | ||
|
|
||
| return polygons | ||
| end |
This file contains hidden or 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,123 @@ | ||
| using Test | ||
| using DelaunayTriangulation | ||
|
|
||
| import GeometryOps as GO | ||
| import GeoInterface as GI | ||
|
|
||
| @testset "Voronoi" begin | ||
| @testset "Basic voronoi tessellation" begin | ||
| # Test with simple points | ||
| points = [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)] | ||
| polygons = GO.voronoi(points) | ||
|
|
||
| # Should return 3 polygons, one for each point | ||
| @test length(polygons) == 3 | ||
|
|
||
| # Each should be a valid polygon | ||
| for poly in polygons | ||
| @test GI.isgeometry(poly) | ||
| @test GI.geomtrait(poly) isa GI.PolygonTrait | ||
| end | ||
| end | ||
|
|
||
| @testset "Voronoi with various input types" begin | ||
| # Test with GeoInterface points | ||
| points = [GI.Point(0.0, 0.0), GI.Point(1.0, 0.0), GI.Point(0.5, 1.0)] | ||
| polygons = GO.voronoi(points) | ||
| @test length(polygons) == 3 | ||
|
|
||
| # Test with mixed geometry collection | ||
| geoms = [ | ||
| GI.Point(0.0, 0.0), | ||
| GI.LineString([(1.0, 0.0), (1.5, 0.5)]), # Will extract endpoints | ||
| GI.Point(0.5, 1.0) | ||
| ] | ||
| polygons = GO.voronoi(geoms) | ||
| @test length(polygons) == 4 # 1 + 2 + 1 points | ||
| end | ||
|
|
||
| # @testset "Voronoi with clipping" begin | ||
| # # Test clipped vs unclipped | ||
| # points = [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.5, 0.5)] | ||
|
|
||
| # # Clipped (default) | ||
| # clipped_polygons = GO.voronoi(points; clip = true) | ||
| # @test length(clipped_polygons) == 4 | ||
|
|
||
| # # Unclipped | ||
| # unclipped_polygons = GO.voronoi(points; clip = false) | ||
| # @test length(unclipped_polygons) == 4 | ||
|
|
||
| # # The polygons should be different (unclipped extends to infinity conceptually) | ||
| # # But both should have valid polygons | ||
| # for poly in clipped_polygons | ||
| # @test GI.isgeometry(poly) | ||
| # end | ||
| # for poly in unclipped_polygons | ||
| # @test GI.isgeometry(poly) | ||
| # end | ||
| # end | ||
|
|
||
| # @testset "Voronoi with custom boundary" begin | ||
| # # Create points inside a square | ||
| # points = [(0.25, 0.25), (0.75, 0.25), (0.75, 0.75), (0.25, 0.75), (0.5, 0.5)] | ||
|
|
||
| # # Create a square boundary | ||
| # boundary = GI.Polygon([ | ||
| # GI.LinearRing([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]) | ||
| # ]) | ||
|
|
||
| # polygons = GO.voronoi(points, boundary) | ||
| # @test length(polygons) == 5 | ||
|
|
||
| # # All polygons should be valid | ||
| # for poly in polygons | ||
| # @test GI.isgeometry(poly) | ||
| # @test GI.geomtrait(poly) isa GI.PolygonTrait | ||
| # end | ||
| # end | ||
|
|
||
| @testset "Grid of points" begin | ||
| # Create a regular grid | ||
| xs = 0.0:0.5:2.0 | ||
| ys = 0.0:0.5:2.0 | ||
| points = [(x, y) for x in xs for y in ys] | ||
|
|
||
| polygons = GO.voronoi(points) | ||
| @test length(polygons) == length(points) | ||
|
|
||
| # Each polygon should be valid | ||
| for poly in polygons | ||
| @test GI.isgeometry(poly) | ||
|
|
||
| # Get the exterior ring to check it's closed | ||
| ring = GI.getexterior(poly) | ||
| coords = GI.coordinates(ring) | ||
| @test first(coords) ≈ last(coords) # Ring should be closed | ||
| end | ||
| end | ||
|
|
||
| @testset "Error handling" begin | ||
| # Too few points | ||
| @test_throws ArgumentError GO.voronoi([(0.0, 0.0)]) | ||
| @test_throws ArgumentError GO.voronoi([(0.0, 0.0), (1.0, 0.0)]) | ||
|
|
||
| # Empty input | ||
| @test_throws ArgumentError GO.voronoi([]) | ||
| end | ||
|
|
||
| @testset "Random points stress test" begin | ||
| # Test with more points | ||
| n = 50 | ||
| points = [(rand(), rand()) for _ in 1:n] | ||
|
|
||
| polygons = GO.voronoi(points) | ||
| @test length(polygons) == n | ||
|
|
||
| # All should be valid polygons | ||
| for poly in polygons | ||
| @test GI.isgeometry(poly) | ||
| @test GI.geomtrait(poly) isa GI.PolygonTrait | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or 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.
Did this test fail? What was the issue here?
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.
Ah! These were commented out on your branch and I didn't look at them. I will work on that later and update the PR
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.
Oops! My implementation must not have been working then. I'll try to get it to work with DelaunayTriangulation's api then.