use crate::algorithm::triangulate_delaunay::{
DelaunayTriangulationConfig, SpadeTriangulationFloat,
};
use crate::algorithm::vector_ops::Vector2DOps;
use crate::line_intersection::{LineIntersection, line_intersection};
use crate::utils::lex_cmp;
use geo_types::{Coord, Line, LineString, Point, Polygon, Rect};
use num_traits::Float;
use spade::Triangulation;
use spade::handles::{VoronoiVertex::Inner, VoronoiVertex::Outer};
use crate::algorithm::bool_ops::BoolOpsNum;
use crate::algorithm::triangulate_delaunay::TriangulationError;
use crate::{
BooleanOps, BoundingRect, Contains, Distance, Euclidean, TriangulateDelaunayUnconstrained,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VoronoiError {
Triangulation(TriangulationError),
CollinearInput,
InsufficientVertices,
}
impl std::fmt::Display for VoronoiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VoronoiError::Triangulation(e) => write!(f, "triangulation error: {e}"),
VoronoiError::CollinearInput => {
write!(
f,
"input points are collinear; Voronoi cells cannot be computed"
)
}
VoronoiError::InsufficientVertices => {
write!(
f,
"fewer than 2 unique vertices; cannot compute Voronoi diagram"
)
}
}
}
}
impl std::error::Error for VoronoiError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
VoronoiError::Triangulation(e) => Some(e),
VoronoiError::CollinearInput | VoronoiError::InsufficientVertices => None,
}
}
}
impl From<TriangulationError> for VoronoiError {
fn from(e: TriangulationError) -> Self {
VoronoiError::Triangulation(e)
}
}
#[derive(Debug, Clone, Default)]
pub enum VoronoiClip<'a, T: SpadeTriangulationFloat> {
#[default]
Padded,
Envelope,
Polygon(&'a Polygon<T>),
}
#[derive(Debug, Clone)]
pub struct VoronoiParams<'a, T: SpadeTriangulationFloat> {
pub tolerance: T,
pub clip: VoronoiClip<'a, T>,
}
impl<'a, T: SpadeTriangulationFloat> Default for VoronoiParams<'a, T> {
fn default() -> Self {
Self::new()
}
}
impl<'a, T: SpadeTriangulationFloat> VoronoiParams<'a, T> {
pub fn new() -> Self {
Self {
tolerance: T::zero(),
clip: VoronoiClip::Padded,
}
}
pub fn tolerance(mut self, tolerance: T) -> Self {
self.tolerance = tolerance;
self
}
pub fn clip(mut self, clip: VoronoiClip<'a, T>) -> Self {
self.clip = clip;
self
}
}
pub trait Voronoi<'a, T>
where
T: SpadeTriangulationFloat,
{
fn voronoi_edges(&'a self) -> Result<Vec<Line<T>>, VoronoiError>;
fn voronoi_edges_with_params(
&'a self,
params: VoronoiParams<'a, T>,
) -> Result<Vec<Line<T>>, VoronoiError>;
fn voronoi_cells(&'a self) -> Result<Vec<Polygon<T>>, VoronoiError>
where
T: BoolOpsNum;
fn voronoi_cells_with_params(
&'a self,
params: VoronoiParams<'a, T>,
) -> Result<Vec<Polygon<T>>, VoronoiError>
where
T: BoolOpsNum;
}
impl<'a, T, G> Voronoi<'a, T> for G
where
T: SpadeTriangulationFloat + 'a,
G: TriangulateDelaunayUnconstrained<'a, T>,
{
fn voronoi_edges(&'a self) -> Result<Vec<Line<T>>, VoronoiError> {
self.voronoi_edges_with_params(VoronoiParams::new())
}
fn voronoi_edges_with_params(
&'a self,
params: VoronoiParams<'a, T>,
) -> Result<Vec<Line<T>>, VoronoiError> {
let triangulation = if params.tolerance > T::zero() {
self.unconstrained_triangulation_raw_with_config(DelaunayTriangulationConfig {
snap_radius: params.tolerance,
})?
} else {
self.unconstrained_triangulation_raw()?
};
let base_bounds = compute_bounds_from_vertices(triangulation.vertices().map(|v| *v.data()));
let bounds = padded_bounds(base_bounds, <T as num_traits::NumCast>::from(0.5).unwrap());
let width = base_bounds.width();
let height = base_bounds.height();
let mut edges = Vec::new();
for edge in triangulation.undirected_voronoi_edges() {
match edge.vertices() {
[Inner(from), Inner(to)] => {
let from_point = from.circumcenter();
let to_point = to.circumcenter();
edges.push(Line::new(
Coord {
x: from_point.x,
y: from_point.y,
},
Coord {
x: to_point.x,
y: to_point.y,
},
));
}
[Inner(from), Outer(edge)] | [Outer(edge), Inner(from)] => {
let start = from.circumcenter();
let dir = edge.direction_vector();
let extended_end = Point::new(
start.x + dir.x * (width + height),
start.y + dir.y * (width + height),
);
let infinite_voronoi_edge =
Line::new(Point::new(start.x, start.y), extended_end);
let intersections = bounds
.to_lines()
.iter()
.filter_map(|edge| line_intersection(infinite_voronoi_edge, *edge))
.filter_map(|inter| match inter {
LineIntersection::SinglePoint {
intersection,
is_proper: _,
} => Some(intersection),
LineIntersection::Collinear { intersection: _ } => None,
})
.collect::<Vec<_>>();
debug_assert!(
!intersections.is_empty(),
"No infinite edges intersect the bounding box. Degenerate or invalid geometry?"
);
if let Some(intersection) = intersections.into_iter().min_by(|a, b| {
let dist_a: T = Euclidean
.distance(&Point::new(start.x, start.y), &Point::new(a.x, a.y));
let dist_b: T = Euclidean
.distance(&Point::new(start.x, start.y), &Point::new(b.x, b.y));
dist_a.total_cmp(&dist_b)
}) {
edges.push(Line::new(
Coord {
x: start.x,
y: start.y,
},
intersection,
));
}
}
[Outer(edge1), Outer(_)] => {
let mut points: Vec<_> = triangulation.vertices().map(|v| *v.data()).collect();
points.sort_by(|a, b| lex_cmp(a, b));
let bisector_index = edges.len();
if bisector_index + 1 >= points.len() {
continue;
}
let p_a = points[bisector_index];
let p_b = points[bisector_index + 1];
let midpoint = (p_a + p_b) / <T as num_traits::NumCast>::from(2.0).unwrap();
let dir = edge1.direction_vector();
let extension = width + height;
let extended_start = Coord {
x: midpoint.x - dir.x * extension,
y: midpoint.y - dir.y * extension,
};
let extended_end = Coord {
x: midpoint.x + dir.x * extension,
y: midpoint.y + dir.y * extension,
};
edges.push(Line::new(extended_start, extended_end))
}
}
}
Ok(edges)
}
fn voronoi_cells(&'a self) -> Result<Vec<Polygon<T>>, VoronoiError>
where
T: BoolOpsNum,
{
self.voronoi_cells_with_params(VoronoiParams::new())
}
fn voronoi_cells_with_params(
&'a self,
params: VoronoiParams<'a, T>,
) -> Result<Vec<Polygon<T>>, VoronoiError>
where
T: BoolOpsNum,
{
let (raw_cells, base_bounds) = build_raw_voronoi_cells(self, ¶ms)?;
let clip_poly: Polygon<T> = match ¶ms.clip {
VoronoiClip::Padded => {
padded_bounds(base_bounds, <T as num_traits::NumCast>::from(0.5).unwrap())
.to_polygon()
}
VoronoiClip::Envelope => base_bounds.to_polygon(),
VoronoiClip::Polygon(boundary) => (*boundary).clone(),
};
let clip_rect = clip_poly.bounding_rect();
Ok(raw_cells
.into_iter()
.flat_map(|cell| {
let contained_by_clip = clip_rect
.as_ref()
.zip(cell.bounding_rect())
.is_some_and(|(cr, cell_rect)| cr.contains(&cell_rect));
if contained_by_clip {
vec![cell]
} else {
cell.intersection(&clip_poly).0
}
})
.collect())
}
}
fn build_raw_voronoi_cells<'a, T, G>(
geom: &'a G,
params: &VoronoiParams<'a, T>,
) -> Result<(Vec<Polygon<T>>, Rect<T>), VoronoiError>
where
T: SpadeTriangulationFloat + BoolOpsNum,
G: TriangulateDelaunayUnconstrained<'a, T>,
{
let triangulation = if params.tolerance > T::zero() {
geom.unconstrained_triangulation_raw_with_config(DelaunayTriangulationConfig {
snap_radius: params.tolerance,
})?
} else {
geom.unconstrained_triangulation_raw()?
};
let num_vertices = triangulation.num_vertices();
if num_vertices < 2 {
return Err(VoronoiError::InsufficientVertices);
}
let base_bounds = compute_bounds_from_vertices(triangulation.vertices().map(|v| *v.data()));
let padded = padded_bounds(base_bounds, <T as num_traits::NumCast>::from(0.5).unwrap());
let extension =
(padded.width() + padded.height()) * <T as num_traits::NumCast>::from(2.0).unwrap();
let mut raw_cells: Vec<Polygon<T>> = Vec::new();
for face in triangulation.voronoi_faces() {
let edges: Vec<_> = face.adjacent_edges().collect();
if edges.is_empty() {
continue;
}
let site_coord = *face.as_delaunay_vertex().data();
let mut circumcenters: Vec<Coord<T>> = Vec::new();
let mut rays: Vec<(Coord<T>, Coord<T>)> = Vec::new();
for edge in &edges {
let from_vertex = edge.from();
let to_vertex = edge.to();
if let Inner(inner_face) = &from_vertex {
let cc = inner_face.circumcenter();
let coord = Coord { x: cc.x, y: cc.y };
if !circumcenters.contains(&coord) {
circumcenters.push(coord);
}
}
if let Inner(inner_face) = &to_vertex {
let cc = inner_face.circumcenter();
let coord = Coord { x: cc.x, y: cc.y };
if !circumcenters.contains(&coord) {
circumcenters.push(coord);
}
}
if let (Inner(inner_face), Outer(outer_edge)) = (&from_vertex, &to_vertex) {
let ref_pt = inner_face.circumcenter();
let dir = outer_edge.direction_vector();
rays.push((
Coord {
x: ref_pt.x,
y: ref_pt.y,
},
Coord { x: dir.x, y: dir.y },
));
}
if let (Outer(outer_edge), Inner(inner_face)) = (&from_vertex, &to_vertex) {
let ref_pt = inner_face.circumcenter();
let dir = outer_edge.direction_vector();
rays.push((
Coord {
x: ref_pt.x,
y: ref_pt.y,
},
Coord { x: dir.x, y: dir.y },
));
}
}
let mut vertices: Vec<Coord<T>> = circumcenters.clone();
if rays.is_empty() {
if vertices.len() < 3 {
continue;
}
} else {
for (origin, direction) in &rays {
let Some(unit_dir) = direction.try_normalize() else {
continue;
};
let extended = *origin + unit_dir * extension;
vertices.push(extended);
}
}
if vertices.len() < 3 {
continue;
}
vertices.sort_by(|a, b| {
let angle_a = Float::atan2(a.y - site_coord.y, a.x - site_coord.x);
let angle_b = Float::atan2(b.y - site_coord.y, b.x - site_coord.x);
angle_a.total_cmp(&angle_b)
});
vertices.push(vertices[0]);
let poly = Polygon::new(LineString::new(vertices), vec![]);
raw_cells.push(poly);
}
if raw_cells.is_empty() && num_vertices >= 2 {
return Err(VoronoiError::CollinearInput);
}
Ok((raw_cells, base_bounds))
}
fn compute_bounds_from_vertices<T, I>(vertices: I) -> Rect<T>
where
T: SpadeTriangulationFloat,
I: Iterator<Item = Coord<T>>,
{
let (min_x, min_y, max_x, max_y) = vertices.fold(
(
Float::max_value(),
Float::max_value(),
Float::min_value(),
Float::min_value(),
),
|(min_x, min_y, max_x, max_y), p| {
(
Float::min(min_x, p.x),
Float::min(min_y, p.y),
Float::max(max_x, p.x),
Float::max(max_y, p.y),
)
},
);
Rect::new((min_x, min_y), (max_x, max_y))
}
fn padded_bounds<T: SpadeTriangulationFloat>(base: Rect<T>, padding_factor: T) -> Rect<T> {
let padding = Float::max(base.width(), base.height()) * padding_factor;
Rect::new(
(base.min().x - padding, base.min().y - padding),
(base.max().x + padding, base.max().y + padding),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Relate;
use crate::algorithm::Validation;
use crate::wkt;
use approx::assert_relative_eq;
use geo_types::{MultiLineString, MultiPolygon};
#[test]
fn voronoi_test_simple_triangle() {
let triangle = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 0.0 1.0, 0.0 0.0)));
let voronoi = triangle.voronoi_edges().unwrap();
let result: MultiLineString<_> = voronoi.into_iter().collect();
let expected = wkt!(MULTILINESTRING(
(0.5 0.5, 0.5 -0.5),
(0.5 0.5, -0.5 0.5),
(0.5 0.5, 1.5 1.5)
));
assert!(
result.relate(&expected).is_equal_topo(),
"Expected {expected:?}, got {result:?}"
);
}
#[test]
fn voronoi_test_square() {
let square = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)));
let voronoi = square.voronoi_edges().unwrap();
let result: MultiLineString<_> = voronoi.into_iter().collect();
let expected = wkt!(MULTILINESTRING(
(0.5 0.5, 0.5 -0.5),
(0.5 0.5, 1.5 0.5),
(0.5 0.5, 0.5 0.5),
(0.5 0.5, 0.5 1.5),
(0.5 0.5, -0.5 0.5)
));
assert!(
result.relate(&expected).is_equal_topo(),
"Expected {expected:?}, got {result:?}"
);
}
#[test]
fn voronoi_test_collinear_points() {
let collinear = wkt!(POLYGON((-1.0 0.0, 0.0 0.0, 1.0 0.0, -1.0 0.0)));
let voronoi = collinear.voronoi_edges().unwrap();
let result: MultiLineString<_> = voronoi.into_iter().collect();
let expected = wkt!(MULTILINESTRING(
(-0.5 -2.0, -0.5 2.0),
(0.5 2.0, 0.5 -2.0)
));
assert!(
result.relate(&expected).is_equal_topo(),
"Expected {expected:?}, got {result:?}"
);
}
#[test]
fn voronoi_cells_collinear_returns_error() {
let collinear = wkt!(LINESTRING(0.0 0.0, 1.0 0.0, 2.0 0.0));
let Err(VoronoiError::CollinearInput) = collinear.voronoi_cells() else {
panic!("Expected CollinearInput error");
};
let edges = collinear.voronoi_edges().unwrap();
assert_eq!(edges.len(), 2);
}
#[test]
fn voronoi_test_two_points() {
let points = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 0.0 0.0)));
let voronoi = points.voronoi_edges().unwrap();
let result: MultiLineString<_> = voronoi.into_iter().collect();
let expected = wkt!(MULTILINESTRING((0.5 -1.0, 0.5 1.0)));
assert!(
result.relate(&expected).is_equal_topo(),
"Expected {expected:?}, got {result:?}"
);
}
#[test]
fn voronoi_cells_triangle() {
let triangle = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 0.0 1.0, 0.0 0.0)));
let cells = triangle.voronoi_cells().unwrap();
assert_eq!(cells.len(), 3);
for cell in &cells {
assert!(cell.exterior().0.len() >= 4);
assert!(
cell.is_valid(),
"Voronoi cell should be a valid polygon: {:?}",
cell.exterior().0
);
}
}
#[test]
fn voronoi_cells_square() {
let square = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)));
let cells = square.voronoi_cells().unwrap();
assert_eq!(cells.len(), 4);
for cell in &cells {
assert!(
cell.is_valid(),
"Voronoi cell should be a valid polygon: {:?}",
cell.exterior().0
);
}
}
#[test]
fn voronoi_cells_clipped_square() {
let square = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)));
let clipped = square
.voronoi_cells_with_params(VoronoiParams::new().clip(VoronoiClip::Envelope))
.unwrap();
assert!(!clipped.is_empty());
}
#[test]
fn voronoi_cells_clipped_to_custom_boundary() {
let points = wkt!(POLYGON((0.0 0.0, 2.0 0.0, 2.0 2.0, 0.0 2.0, 0.0 0.0)));
let clip_boundary = wkt!(POLYGON((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5)));
let clipped = points
.voronoi_cells_with_params(
VoronoiParams::new().clip(VoronoiClip::Polygon(&clip_boundary)),
)
.unwrap();
assert!(!clipped.is_empty());
}
#[test]
fn voronoi_cells_count_matches_input_points() {
let polygon = wkt!(POLYGON((
0.0 0.0, 1.0 0.0, 2.0 0.5, 2.0 1.5, 1.5 2.0, 0.5 2.0, 0.0 1.5, 0.0 0.0
)));
let cells = polygon.voronoi_cells().unwrap();
assert_eq!(
cells.len(),
7,
"voronoi_cells() should produce one cell per unique input vertex"
);
let clipped = polygon
.voronoi_cells_with_params(VoronoiParams::new().clip(VoronoiClip::Envelope))
.unwrap();
assert_eq!(
clipped.len(),
7,
"voronoi_cells_with_params(Envelope) should produce one cell per unique input vertex"
);
}
#[test]
fn voronoi_cells_union_covers_bounding_box() {
use crate::Area;
let square = wkt!(POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)));
let cells = square.voronoi_cells().unwrap();
assert_eq!(cells.len(), 4);
for cell in &cells {
assert!(
cell.is_valid(),
"Voronoi cell should be a valid polygon: {:?}",
cell.exterior().0
);
}
let expected_bounds = Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 1.5, y: 1.5 });
let expected_area = expected_bounds.to_polygon().unsigned_area();
let mut union_result = MultiPolygon(vec![cells[0].clone()]);
for cell in cells.iter().skip(1) {
union_result = union_result.union(&MultiPolygon(vec![cell.clone()]));
}
let union_area = union_result.unsigned_area();
assert_relative_eq!(union_area, expected_area, epsilon = Float::epsilon());
assert_eq!(
union_result.0.len(),
1,
"Union of Voronoi cells should form a single polygon"
);
}
#[test]
fn voronoi_cells_clipped_islington_fills_bbox() {
use crate::Area;
use crate::algorithm::bounding_rect::BoundingRect;
use geo_types::MultiPoint;
let points_mp: MultiPoint<f64> = geo_test_fixtures::islington_post_boxes();
let coords: Vec<Coord<f64>> = points_mp.iter().map(|p| p.0).collect();
let unique_coords: std::collections::HashSet<_> = coords
.iter()
.map(|c| (c.x.to_bits(), c.y.to_bits()))
.collect();
let num_unique_points = unique_coords.len();
let mut ring_coords = coords.clone();
ring_coords.push(coords[0]);
let points = Polygon::new(LineString::from(ring_coords), vec![]);
let clipped_cells = points
.voronoi_cells_with_params(VoronoiParams::new().clip(VoronoiClip::Envelope))
.unwrap();
let bbox = points.bounding_rect().unwrap();
let bbox_area = bbox.to_polygon().unsigned_area();
assert_eq!(
clipped_cells.len(),
num_unique_points,
"Should have one cell per unique input vertex"
);
let individual_sum: f64 = clipped_cells.iter().map(|c| c.unsigned_area()).sum();
assert_relative_eq!(individual_sum, bbox_area, epsilon = 1e-9);
}
#[test]
fn voronoi_test_vertical_collinear_points() {
let collinear = wkt!(POLYGON((0.0 0.0, 0.0 1.0, 0.0 2.0, 0.0 0.0)));
let voronoi = collinear.voronoi_edges().unwrap();
let result: MultiLineString<_> = voronoi.into_iter().collect();
let expected = wkt!(MULTILINESTRING(
(2.0 0.5, -2.0 0.5),
(-2.0 1.5, 2.0 1.5)
));
assert!(
result.relate(&expected).is_equal_topo(),
"Expected {expected:?}, got {result:?}"
);
}
}