|
| 1 | +#= |
| 2 | +# Voronoi Tessellation |
| 3 | +
|
| 4 | +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. |
| 5 | +Each region contains all points closer to one generator point than to any other. |
| 6 | +
|
| 7 | +GeometryOps.jl provides a method for computing the Voronoi tessellation of a set of points, |
| 8 | +using the [DelaunayTriangulation.jl](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) package. |
| 9 | +
|
| 10 | +Right now, the GeometryOps.jl method can only provide clipped voronoi tesselations, as the function returns a list of GeoInterface polygons. |
| 11 | +If you need an unbounded tessellation, open an issue and we can discuss the best way to represent unbounded polygons within GeometryOps. |
| 12 | +
|
| 13 | +## Example |
| 14 | +
|
| 15 | +### Simple tessellation |
| 16 | +```@example simple |
| 17 | +import GeometryOps as GO, GeoInterface as GI |
| 18 | +using CairoMakie # to plot |
| 19 | +
|
| 20 | +points = tuple.(randn(20), randn(20)) |
| 21 | +polygons = GO.voronoi(points) |
| 22 | +f, a, p = plot(polygons[1]; label = "Voronoi cell 1") |
| 23 | +for (i, poly) in enumerate(polygons[2:end]) |
| 24 | + plot!(a, poly; label = "Voronoi cell $(i+1)") |
| 25 | +end |
| 26 | +scatter!(a, points; color = :black, markersize = 10, label = "Generators") |
| 27 | +axislegend(a) |
| 28 | +f |
| 29 | +``` |
| 30 | +
|
| 31 | +## Implementation |
| 32 | +
|
| 33 | +This implementation mainly just preforms some assertion checks before passing the Arguments |
| 34 | +to the DelaunayTriangulation package. We always set the argument `clip` to the DelaunayTriangulation |
| 35 | +`voronoi` call to `True` such that we can return a list of valid polygons. The default clipping polygon |
| 36 | +is the convex hull of the tessleation, but the user can pass in a bounding polygon with the `clip_polygon` |
| 37 | +argument. After the call to `voronoi`, the call then unpacks the voronoi output into GeoInterface |
| 38 | +polygons, whose point match the float type input by the user. |
| 39 | +=# |
| 40 | + |
| 41 | +struct __NoCRSProvided end |
| 42 | + |
| 43 | +""" |
| 44 | + voronoi(geometries, [T = Float64]; clip_polygon = nothing, kwargs...) |
| 45 | +
|
| 46 | +Compute the Voronoi tessellation of the points in `geometries`. |
| 47 | +Returns a vector of `GI.Polygon` objects representing the Voronoi cells, |
| 48 | +in the same order as the input points. |
| 49 | +
|
| 50 | +## Arguments |
| 51 | +- `geometries`: Any GeoInterface-compatible geometry or collection of geometries |
| 52 | + that can be decomposed into points |
| 53 | +- `T`: Float-type for returned polygons points (default: Float64) |
| 54 | +
|
| 55 | +## Keyword Arguments |
| 56 | +- `clip_polygon`: what bounding shape should the Voronoi cells be clipped to? (default: nothing -> clipped to the convex hull) |
| 57 | + clip_polygon can of several types: (1) a GeoInterface polygon, (2) a two-element tuple where the first element is a list of tuple points |
| 58 | + and the second element is a list of integer indices to indicate the order of the provided points, or (3) a a two-element tuple where the |
| 59 | + first element is a tuple of tuple points and the second element is a tuple of integer indices to indicate the order of the provided points |
| 60 | +- $CRS_KEYWORD |
| 61 | +- `rng`: random number generator to generating the voronoi tesselation |
| 62 | +
|
| 63 | +!!! warning |
| 64 | + This interface only computes the 2-dimensional Voronoi tessellation! |
| 65 | + Only clipped voronoi tesselations can be created! |
| 66 | + Only `T = Float64` or `Float32` are guaranteed good results by the underlying package DelaunayTriangulation. |
| 67 | + |
| 68 | +!!! note |
| 69 | + The polygons are returned in the same order as the input points after flattening. |
| 70 | + Each polygon corresponds to the Voronoi cell of the point at the same index. |
| 71 | +
|
| 72 | +## Examples |
| 73 | +An example with default clipping to the convex hull. |
| 74 | +
|
| 75 | +```jldoctest voronoi |
| 76 | +import GeometryOps as GO |
| 77 | +import GeoInterface as GI |
| 78 | +using Random |
| 79 | +
|
| 80 | +rng = Xoshiro(0) |
| 81 | +points = [(rand(rng), rand(rng)) .* 5 for i in range(1, 3)] |
| 82 | +GO.voronoi(points; rng = rng) |
| 83 | +# output |
| 84 | +3-element Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}: |
| 85 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(4.310704285977424, 0.42985432929210976), … (2) … , (4.310704285977424, 0.42985432929210976)])]) |
| 86 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(3.7949144210695653, 0.4101636087384888), … (4) … , (3.7949144210695653, 0.4101636087384888)])]) |
| 87 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(2.685897788908803, 0.3678259474564151), … (2) … , (2.685897788908803, 0.3678259474564151)])]) |
| 88 | +``` |
| 89 | +
|
| 90 | +An example with clipping to a GeoInterface polygon. |
| 91 | +```jldoctest voronoi |
| 92 | +clip_points = ((0.0,0.0), (5.0,0.0), (5.0,5.0), (0.0,5.0), (0.0,0.0)) |
| 93 | +clip_order = (1, 2, 3, 4, 1) |
| 94 | +clip_poly1 = GI.Polygon([collect(clip_points)]) |
| 95 | +GO.voronoi(points; clip_polygon = clip_poly1, rng = rng) |
| 96 | +# output |
| 97 | +3-element Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}: |
| 98 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(5.0, 0.0), … (3) … , (5.0, 0.0)])]) |
| 99 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(3.7328227614527916, 0.0), … (3) … , (3.7328227614527916, 0.0)])]) |
| 100 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(0.0, 5.0), … (3) … , (0.0, 5.0)])]) |
| 101 | +``` |
| 102 | +
|
| 103 | +An example with clipping to a tuple of tuples. |
| 104 | +```jldoctest voronoi |
| 105 | +clip_poly2 = (clip_points, clip_order) # tuples |
| 106 | +GO.voronoi(points; clip_polygon = clip_poly2, rng = rng) |
| 107 | +# output |
| 108 | +3-element Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}: |
| 109 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(5.0, 0.0), … (3) … , (5.0, 0.0)])]) |
| 110 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(3.7328227614527916, 0.0), … (3) … , (3.7328227614527916, 0.0)])]) |
| 111 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(0.0, 5.0), … (3) … , (0.0, 5.0)])]) |
| 112 | +``` |
| 113 | +
|
| 114 | +An example with clipping to a tuple of vectors. |
| 115 | +```jldoctest voronoi |
| 116 | +clip_poly3 = (collect(clip_points), collect(clip_order)) # vectors |
| 117 | +GO.voronoi(points; clip_polygon = clip_poly3, rng = rng) |
| 118 | +# output |
| 119 | +3-element Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}: |
| 120 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(5.0, 0.0), … (3) … , (5.0, 0.0)])]) |
| 121 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(3.7328227614527916, 0.0), … (3) … , (3.7328227614527916, 0.0)])]) |
| 122 | + GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LinearRing([(0.0, 5.0), … (3) … , (0.0, 5.0)])]) |
| 123 | +``` |
| 124 | +
|
| 125 | +""" |
| 126 | +function voronoi(geometries, ::Type{T} = Float64; kwargs...) where T |
| 127 | + return voronoi(Planar(), geometries, T; kwargs...) |
| 128 | +end |
| 129 | + |
| 130 | +function voronoi(::Planar, geometries, ::Type{T} = Float64; clip_polygon = nothing, crs = __NoCRSProvided(), kwargs...) where T |
| 131 | + # Extract all points as tuples using GO.flatten |
| 132 | + # This handles any GeoInterface-compatible input |
| 133 | + points_iter = collect(flatten(tuples, GI.PointTrait, geometries)) |
| 134 | + if crs isa __NoCRSProvided |
| 135 | + crs = GI.crs(geometries) |
| 136 | + end |
| 137 | + # if we have not figured it out yet, we can't do anything |
| 138 | + if crs isa __NoCRSProvided |
| 139 | + 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.") |
| 140 | + end |
| 141 | + |
| 142 | + # Handle edge case of too few points |
| 143 | + if length(points_iter) < 3 |
| 144 | + throw(ArgumentError("Voronoi tessellation requires at least 3 points, got $(length(points_iter))")) |
| 145 | + end |
| 146 | + |
| 147 | + # Compute Delaunay triangulation |
| 148 | + tri = DelTri.triangulate(points_iter; kwargs...) |
| 149 | + |
| 150 | + # Compute Voronoi tessellation from the triangulation |
| 151 | + _clip_polygon = if isnothing(clip_polygon) |
| 152 | + nothing |
| 153 | + elseif GI.geomtrait(clip_polygon) isa GI.PolygonTrait |
| 154 | + _clean_voronoi_clip_polygon_inputs(clip_polygon) |
| 155 | + else |
| 156 | + _clean_voronoi_clip_point_inputs(clip_polygon) |
| 157 | + end |
| 158 | + # if isclockwise(clip_polygon) |
| 159 | + vorn = DelTri.voronoi(tri; clip = true, clip_polygon = _clip_polygon) |
| 160 | + |
| 161 | + polygons = GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{T, T}}, Nothing, Nothing}}, Nothing, typeof(crs)}[] |
| 162 | + sizehint!(polygons, DelTri.num_polygons(vorn)) |
| 163 | + # Implementation below copied from Makie.jl |
| 164 | + # see https://github.com/MakieOrg/Makie.jl/blob/687c4466ce00154714297e36a7f610443c6ad5be/Makie/src/basic_recipes/voronoiplot.jl#L101-L110 |
| 165 | + for i in DelTri.each_generator(vorn) |
| 166 | + !DelTri.has_polygon(vorn, i) && continue |
| 167 | + polygon_coords = DelTri.getxy.(DelTri.get_polygon_coordinates(vorn, i)) |
| 168 | + push!(polygons, GI.Polygon([GI.LinearRing(polygon_coords)], crs = crs)) |
| 169 | + # The code below gets the generator point, but we don't need it |
| 170 | + # gp = DelTri.getxy(DelTri.get_generator(vorn, i)) |
| 171 | + # !isempty(polygon_coords) && push!(generators, gp) |
| 172 | + end |
| 173 | + |
| 174 | + return polygons |
| 175 | +end |
| 176 | + |
| 177 | +function _clean_voronoi_clip_polygon_inputs(clip_polygon) |
| 178 | + @assert GI.nhole(clip_polygon) == 0 |
| 179 | + points = collect(flatten(tuples, GI.PointTrait, clip_polygon)) |
| 180 | + npoints = GI.npoint(clip_polygon) |
| 181 | + if points[1] == points[end] |
| 182 | + npoints -= 1 |
| 183 | + points = points[1:npoints] |
| 184 | + end |
| 185 | + point_order = collect(1:npoints) |
| 186 | + return _clean_voronoi_clip_point_inputs((points, point_order)) |
| 187 | +end |
| 188 | + |
| 189 | +function _clean_voronoi_clip_point_inputs((points, point_order)::Tuple{Vector{<:Tuple{<:Any, <:Any}}, Vector{<:Integer}}) |
| 190 | + combined_data = collect(zip(points, point_order)) |
| 191 | + sort!(combined_data, by = last) |
| 192 | + unique!(combined_data) |
| 193 | + |
| 194 | + points, point_order = first.(combined_data), last.(combined_data) |
| 195 | + push!(points, points[1]) |
| 196 | + push!(point_order, 1) |
| 197 | + |
| 198 | + if isclockwise(GI.LineString(points)) |
| 199 | + reverse!(points) |
| 200 | + end |
| 201 | + return points, point_order |
| 202 | +end |
| 203 | + |
| 204 | +_clean_voronoi_clip_point_inputs((points, point_order)::Tuple{NTuple{<:Any, <:Tuple{<:Any, <:Any}}, NTuple{<:Any, <:Integer}}) = |
| 205 | + _clean_voronoi_clip_point_inputs((collect(points), collect(point_order))) |
| 206 | + |
| 207 | +function _clean_voronoi_clip_point_inputs(clip_polygon) |
| 208 | + 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))") |
| 209 | + return |
| 210 | +end |
0 commit comments