use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
#[derive(Debug, Clone, Default)]
pub struct VoronoiOptions {
pub bounds: Option<(f64, f64, f64, f64)>, pub include_infinite: bool,
}
#[derive(Debug, Clone)]
pub struct VoronoiCell {
pub site: Point,
pub site_index: usize,
pub polygon: Option<Polygon>,
pub is_infinite: bool,
}
#[derive(Debug, Clone)]
pub struct VoronoiDiagram {
pub cells: Vec<VoronoiCell>,
pub num_sites: usize,
}
pub fn voronoi_diagram(points: &[Point], options: &VoronoiOptions) -> Result<VoronoiDiagram> {
if points.len() < 3 {
return Err(AlgorithmError::InvalidInput(
"Need at least 3 points for Voronoi diagram".to_string(),
));
}
let delaunator_points: Vec<delaunator::Point> = points
.iter()
.map(|p| delaunator::Point {
x: p.coord.x,
y: p.coord.y,
})
.collect();
let delaunay = delaunator::triangulate(&delaunator_points);
let mut cells = Vec::new();
for (site_idx, point) in points.iter().enumerate() {
let cell = build_voronoi_cell(site_idx, point, &delaunay, points, options)?;
cells.push(cell);
}
Ok(VoronoiDiagram {
cells,
num_sites: points.len(),
})
}
fn build_voronoi_cell(
site_idx: usize,
site: &Point,
delaunay: &delaunator::Triangulation,
points: &[Point],
options: &VoronoiOptions,
) -> Result<VoronoiCell> {
let mut cell_vertices = Vec::new();
let mut is_infinite = false;
for tri_idx in 0..(delaunay.triangles.len() / 3) {
let a = delaunay.triangles[tri_idx * 3];
let b = delaunay.triangles[tri_idx * 3 + 1];
let c = delaunay.triangles[tri_idx * 3 + 2];
if a == site_idx || b == site_idx || c == site_idx {
let pa = &points[a];
let pb = &points[b];
let pc = &points[c];
let circumcenter = compute_circumcenter(
pa.coord.x, pa.coord.y, pb.coord.x, pb.coord.y, pc.coord.x, pc.coord.y,
)?;
cell_vertices.push(circumcenter);
}
}
if let Some((min_x, min_y, max_x, max_y)) = options.bounds {
cell_vertices.retain(|coord| {
coord.x >= min_x && coord.x <= max_x && coord.y >= min_y && coord.y <= max_y
});
is_infinite = cell_vertices.len() < 3;
}
let polygon =
if cell_vertices.len() >= 3 {
cell_vertices.sort_by(|a, b| {
let angle_a = (a.y - site.coord.y).atan2(a.x - site.coord.x);
let angle_b = (b.y - site.coord.y).atan2(b.x - site.coord.x);
angle_a
.partial_cmp(&angle_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(first) = cell_vertices.first().copied() {
cell_vertices.push(first);
}
let exterior = LineString::new(cell_vertices.clone()).map_err(|e| {
AlgorithmError::InvalidGeometry(format!("Invalid cell exterior: {}", e))
})?;
Some(Polygon::new(exterior, vec![]).map_err(|e| {
AlgorithmError::InvalidGeometry(format!("Invalid cell polygon: {}", e))
})?)
} else {
None
};
Ok(VoronoiCell {
site: site.clone(),
site_index: site_idx,
polygon,
is_infinite,
})
}
fn compute_circumcenter(
ax: f64,
ay: f64,
bx: f64,
by: f64,
cx: f64,
cy: f64,
) -> Result<Coordinate> {
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if d.abs() < 1e-10 {
return Err(AlgorithmError::ComputationError(
"Degenerate triangle".to_string(),
));
}
let ux = ((ax * ax + ay * ay) * (by - cy)
+ (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by))
/ d;
let uy = ((ax * ax + ay * ay) * (cx - bx)
+ (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax))
/ d;
Ok(Coordinate::new_2d(ux, uy))
}
#[derive(Debug, Clone)]
pub struct WeightedPoint {
pub point: Point,
pub weight: f64,
}
impl WeightedPoint {
pub fn new(x: f64, y: f64, weight: f64) -> Self {
Self {
point: Point::new(x, y),
weight,
}
}
pub fn unweighted(x: f64, y: f64) -> Self {
Self::new(x, y, 0.0)
}
}
#[derive(Debug, Clone)]
pub struct PowerCell {
pub site_index: usize,
pub polygon: Vec<Coordinate>,
pub is_empty: bool,
}
#[derive(Debug, Clone)]
pub struct PowerDiagram {
pub cells: Vec<PowerCell>,
}
#[derive(Debug, Clone)]
pub struct PowerDiagramOptions {
pub bounding_box: Option<(f64, f64, f64, f64)>,
}
impl Default for PowerDiagramOptions {
fn default() -> Self {
Self { bounding_box: None }
}
}
pub fn power_diagram(
weighted_points: &[WeightedPoint],
options: &PowerDiagramOptions,
) -> crate::error::Result<PowerDiagram> {
if weighted_points.is_empty() {
return Ok(PowerDiagram { cells: vec![] });
}
let bbox = compute_power_bbox(weighted_points, options);
if weighted_points.len() == 1 {
let polygon = bbox_to_polygon(bbox);
return Ok(PowerDiagram {
cells: vec![PowerCell {
site_index: 0,
polygon,
is_empty: false,
}],
});
}
let mut cells = Vec::with_capacity(weighted_points.len());
for i in 0..weighted_points.len() {
let pi = &weighted_points[i];
let mut polygon = bbox_to_polygon(bbox);
for (j, pj) in weighted_points.iter().enumerate() {
if i == j || polygon.is_empty() {
continue;
}
let (a, b, c) = weighted_bisector(
pi.point.coord.x,
pi.point.coord.y,
pi.weight,
pj.point.coord.x,
pj.point.coord.y,
pj.weight,
);
polygon = half_plane_clip(&polygon, -a, -b, -c);
}
let is_empty = polygon.is_empty();
cells.push(PowerCell {
site_index: i,
polygon,
is_empty,
});
}
Ok(PowerDiagram { cells })
}
pub fn weighted_bisector(xi: f64, yi: f64, wi: f64, xj: f64, yj: f64, wj: f64) -> (f64, f64, f64) {
let a = 2.0 * (xj - xi);
let b = 2.0 * (yj - yi);
let c = (xj * xj + yj * yj - wj) - (xi * xi + yi * yi - wi);
(a, b, c)
}
fn compute_power_bbox(
points: &[WeightedPoint],
options: &PowerDiagramOptions,
) -> (f64, f64, f64, f64) {
if let Some(bbox) = options.bounding_box {
return bbox;
}
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for wp in points {
let x = wp.point.coord.x;
let y = wp.point.coord.y;
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
let pad_x = (max_x - min_x) * 0.5 + 1.0;
let pad_y = (max_y - min_y) * 0.5 + 1.0;
(min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y)
}
fn bbox_to_polygon(bbox: (f64, f64, f64, f64)) -> Vec<Coordinate> {
let (min_x, min_y, max_x, max_y) = bbox;
vec![
Coordinate::new_2d(min_x, min_y),
Coordinate::new_2d(max_x, min_y),
Coordinate::new_2d(max_x, max_y),
Coordinate::new_2d(min_x, max_y),
]
}
fn half_plane_clip(polygon: &[Coordinate], a: f64, b: f64, c: f64) -> Vec<Coordinate> {
if polygon.is_empty() {
return vec![];
}
let inside = |p: &Coordinate| a * p.x + b * p.y >= c;
let n = polygon.len();
let mut output: Vec<Coordinate> = Vec::with_capacity(n + 1);
for i in 0..n {
let curr = &polygon[i];
let next = &polygon[(i + 1) % n];
let curr_in = inside(curr);
let next_in = inside(next);
if curr_in {
output.push(*curr);
}
if curr_in != next_in {
let t = intersect_segment_with_halfplane(curr, next, a, b, c);
output.push(Coordinate::new_2d(
curr.x + t * (next.x - curr.x),
curr.y + t * (next.y - curr.y),
));
}
}
output
}
fn intersect_segment_with_halfplane(
p1: &Coordinate,
p2: &Coordinate,
a: f64,
b: f64,
c: f64,
) -> f64 {
let d1 = a * p1.x + b * p1.y - c;
let d2 = a * p2.x + b * p2.y - c;
let denom = d1 - d2;
if denom.abs() < f64::EPSILON {
return 0.5;
}
d1 / denom
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_voronoi_simple() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(5.0, 0.0),
Point::new(2.5, 5.0),
];
let options = VoronoiOptions {
bounds: Some((0.0, 0.0, 10.0, 10.0)),
include_infinite: false,
};
let result = voronoi_diagram(&points, &options);
assert!(result.is_ok());
let diagram = result.expect("Voronoi failed");
assert_eq!(diagram.num_sites, 3);
}
#[test]
fn test_circumcenter() {
let result = compute_circumcenter(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
assert!(result.is_ok());
let center = result.expect("Failed to compute circumcenter");
assert!((center.x - 0.5).abs() < 1e-6);
assert!((center.y - 0.5).abs() < 1e-6);
}
}