use geo_types::{Coord, Line, Point, Triangle};
use rstar::{RTree, RTreeNum};
use spade::{ConstrainedDelaunayTriangulation, DelaunayTriangulation, SpadeNum, Triangulation};
use crate::{Centroid, Contains};
use crate::{CoordsIter, Distance, Euclidean, GeoFloat, LineIntersection, LinesIter, Vector2DOps};
#[derive(Debug, Clone)]
pub struct DelaunayTriangulationConfig<T: SpadeTriangulationFloat> {
pub snap_radius: T,
}
impl<T> Default for DelaunayTriangulationConfig<T>
where
T: SpadeTriangulationFloat,
{
fn default() -> Self {
Self {
snap_radius: <T as std::convert::From<f32>>::from(0.000_1),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TriangulationError {
SpadeError(spade::InsertionError),
LoopTrap,
ConstraintFailure,
}
impl std::fmt::Display for TriangulationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for TriangulationError {}
pub type TriangulationResult<T> = Result<T, TriangulationError>;
pub trait SpadeTriangulationFloat: GeoFloat + SpadeNum + RTreeNum {}
impl<T: GeoFloat + SpadeNum + RTreeNum> SpadeTriangulationFloat for T {}
pub type Triangles<T> = Vec<Triangle<T>>;
mod private {
use super::*;
pub(crate) type CoordsIter<'a, T> = Box<dyn Iterator<Item = Coord<T>> + 'a>;
pub trait UnconstrainedRequirementTrait<'a, T>
where
T: SpadeTriangulationFloat,
{
fn coords(&'a self) -> CoordsIter<'a, T>;
}
pub trait ConstrainedRequirementTrait<'a, T>: UnconstrainedRequirementTrait<'a, T>
where
T: SpadeTriangulationFloat,
{
fn lines(&'a self) -> Vec<Line<T>>;
fn contains_point(&'a self, p: Point<T>) -> bool;
fn cleanup_lines(lines: Vec<Line<T>>, snap_radius: T) -> TriangulationResult<Vec<Line<T>>> {
let (known_coords, lines) = preprocess_lines(lines, snap_radius);
let mut lines = split_segments_at_intersections(lines, known_coords, snap_radius)?;
for line in lines.iter_mut() {
*line = sweep_normalize_line(*line);
}
lines.sort_by(sweep_line_ord);
lines.dedup();
Ok(lines)
}
}
}
pub trait TriangulateDelaunayUnconstrained<'a, T>:
private::UnconstrainedRequirementTrait<'a, T>
where
T: SpadeTriangulationFloat,
{
fn unconstrained_triangulation(&'a self) -> TriangulationResult<Triangles<T>> {
self.unconstrained_triangulation_raw()
.map(triangulation_to_triangles)
}
fn unconstrained_triangulation_raw(
&'a self,
) -> TriangulationResult<DelaunayTriangulation<Coord<T>>> {
let points = self.coords();
points.into_iter().try_fold(
DelaunayTriangulation::<Coord<T>>::new(),
|mut tris, coord| {
tris.insert(coord).map_err(TriangulationError::SpadeError)?;
Ok(tris)
},
)
}
fn unconstrained_triangulation_raw_with_config(
&'a self,
config: DelaunayTriangulationConfig<T>,
) -> TriangulationResult<DelaunayTriangulation<Coord<T>>> {
let mut known_coords: RTree<Coord<T>> = RTree::new();
for coord in self.coords() {
snap_or_register_point(coord, &mut known_coords, config.snap_radius);
}
known_coords.into_iter().try_fold(
DelaunayTriangulation::<Coord<T>>::new(),
|mut tris, coord| {
tris.insert(coord).map_err(TriangulationError::SpadeError)?;
Ok(tris)
},
)
}
}
pub trait TriangulateDelaunay<'a, T>:
TriangulateDelaunayUnconstrained<'a, T> + private::ConstrainedRequirementTrait<'a, T>
where
T: SpadeTriangulationFloat,
{
fn constrained_outer_triangulation(
&'a self,
config: DelaunayTriangulationConfig<T>,
) -> TriangulationResult<Triangles<T>> {
let lines = self.lines();
let lines = Self::cleanup_lines(lines, config.snap_radius)?;
lines
.into_iter()
.try_fold(
ConstrainedDelaunayTriangulation::<Coord<T>>::new(),
|mut cdt, line| {
let start = cdt
.insert(line.start)
.map_err(TriangulationError::SpadeError)?;
let end = cdt
.insert(line.end)
.map_err(TriangulationError::SpadeError)?;
if !cdt.can_add_constraint(start, end) {
return Err(TriangulationError::ConstraintFailure);
}
cdt.add_constraint(start, end);
Ok(cdt)
},
)
.map(triangulation_to_triangles)
}
fn constrained_triangulation(
&'a self,
config: DelaunayTriangulationConfig<T>,
) -> TriangulationResult<Triangles<T>> {
self.constrained_outer_triangulation(config)
.map(|triangles| {
triangles
.into_iter()
.filter(|triangle| {
let center = triangle.centroid();
self.contains_point(center)
})
.collect::<Vec<_>>()
})
}
}
fn triangulation_to_triangles<T, F>(triangulation: T) -> Triangles<F>
where
T: Triangulation<Vertex = Coord<F>>,
F: SpadeTriangulationFloat,
{
triangulation
.inner_faces()
.map(|face| {
let [v0, v1, v2] = face.vertices();
Triangle::new(*v0.data(), *v1.data(), *v2.data())
})
.collect::<Vec<_>>()
}
impl<'a, T, G> TriangulateDelaunayUnconstrained<'a, T> for G
where
T: SpadeTriangulationFloat,
G: private::UnconstrainedRequirementTrait<'a, T>,
{
}
impl<'a, T, G> TriangulateDelaunay<'a, T> for G
where
T: SpadeTriangulationFloat,
G: private::ConstrainedRequirementTrait<'a, T>,
{
}
impl<'a, T, G> private::UnconstrainedRequirementTrait<'a, T> for G
where
T: SpadeTriangulationFloat,
G: CoordsIter<Scalar = T>,
{
fn coords(&'a self) -> private::CoordsIter<'a, T> {
Box::new(self.coords_iter())
}
}
impl<'a, 'l, T, G> private::ConstrainedRequirementTrait<'a, T> for G
where
'a: 'l,
T: SpadeTriangulationFloat,
G: LinesIter<'l, Scalar = T> + CoordsIter<Scalar = T> + Contains<Point<T>>,
{
fn lines(&'a self) -> Vec<Line<T>> {
self.lines_iter().collect()
}
fn contains_point(&'a self, p: Point<T>) -> bool {
self.contains(&p)
}
}
impl<'a, T, G> private::UnconstrainedRequirementTrait<'a, T> for Vec<G>
where
T: SpadeTriangulationFloat + 'a,
G: TriangulateDelaunayUnconstrained<'a, T>,
{
fn coords(&'a self) -> private::CoordsIter<'a, T> {
Box::new(self.iter().flat_map(|g| g.coords()))
}
}
impl<'a, T, G> private::ConstrainedRequirementTrait<'a, T> for Vec<G>
where
T: SpadeTriangulationFloat + 'a,
G: TriangulateDelaunay<'a, T>,
{
fn lines(&'a self) -> Vec<Line<T>> {
self.iter().flat_map(|g| g.lines()).collect::<Vec<_>>()
}
fn contains_point(&'a self, p: Point<T>) -> bool {
self.iter().any(|g| g.contains_point(p))
}
}
impl<'a, 'l, T, G> private::UnconstrainedRequirementTrait<'a, T> for &[G]
where
'a: 'l,
T: SpadeTriangulationFloat + 'a,
G: TriangulateDelaunayUnconstrained<'a, T> + LinesIter<'l, Scalar = T>,
{
fn coords(&'a self) -> private::CoordsIter<'a, T> {
Box::new(self.iter().flat_map(|g| g.coords()))
}
}
impl<'a, 'l, T, G> private::ConstrainedRequirementTrait<'a, T> for &[G]
where
'a: 'l,
T: SpadeTriangulationFloat + 'a,
G: TriangulateDelaunay<'a, T> + LinesIter<'l, Scalar = T>,
{
fn lines(&'a self) -> Vec<Line<T>> {
self.iter().flat_map(|g| g.lines()).collect::<Vec<_>>()
}
fn contains_point(&'a self, p: Point<T>) -> bool {
self.iter().any(|g| g.contains_point(p))
}
}
pub(crate) fn split_segments_at_intersections<T: SpadeTriangulationFloat>(
lines: Vec<Line<T>>,
mut known_points: RTree<Coord<T>>,
snap_radius: T,
) -> Result<Vec<Line<T>>, TriangulationError> {
use crate::algorithm::sweep::Intersections;
if lines.len() < 2 {
return Ok(lines);
}
let indexed: Vec<SweepIndexedLine<T>> = lines
.iter()
.enumerate()
.map(|(i, l)| SweepIndexedLine { index: i, line: *l })
.collect();
let mut split_points: Option<Vec<Vec<Coord<T>>>> = None;
for (seg_a, seg_b, intersection) in Intersections::from_iter(indexed) {
match intersection {
LineIntersection::SinglePoint {
intersection: pt,
is_proper,
} => {
if is_proper {
let sp = split_points.get_or_insert_with(|| vec![Vec::new(); lines.len()]);
let pt = snap_or_register_point(pt, &mut known_points, snap_radius);
sp[seg_a.index].push(pt);
sp[seg_b.index].push(pt);
}
}
LineIntersection::Collinear {
intersection: overlap,
} => {
if overlap.start != overlap.end {
let sp = split_points.get_or_insert_with(|| vec![Vec::new(); lines.len()]);
let s = snap_or_register_point(overlap.start, &mut known_points, snap_radius);
let e = snap_or_register_point(overlap.end, &mut known_points, snap_radius);
for idx in [seg_a.index, seg_b.index] {
sp[idx].push(s);
sp[idx].push(e);
}
}
}
}
}
let Some(mut split_points) = split_points else {
return Ok(lines);
};
let extra: usize = split_points.iter().map(|v| v.len()).sum();
let mut result = Vec::with_capacity(lines.len() + extra);
for (i, line) in lines.iter().enumerate() {
let pts = &mut split_points[i];
pts.retain(|p| *p != line.start && *p != line.end);
if pts.is_empty() {
result.push(*line);
continue;
}
let dir = line.end - line.start;
pts.sort_by(|a, b| {
let ta = (*a - line.start).dot_product(dir);
let tb = (*b - line.start).dot_product(dir);
ta.total_cmp(&tb)
});
pts.dedup();
let mut prev = line.start;
for &pt in pts.iter() {
if pt != prev {
result.push(Line::new(prev, pt));
}
prev = pt;
}
if prev != line.end {
result.push(Line::new(prev, line.end));
}
}
result.retain(|l| l.start != l.end);
Ok(result)
}
#[derive(Clone, Debug)]
struct SweepIndexedLine<T: GeoFloat> {
index: usize,
line: Line<T>,
}
impl<T: GeoFloat> crate::algorithm::sweep::Cross for SweepIndexedLine<T> {
type Scalar = T;
fn line(&self) -> Line<T> {
self.line
}
}
pub(crate) fn sweep_normalize_line<T: GeoFloat>(line: Line<T>) -> Line<T> {
let cmp = line
.start
.x
.total_cmp(&line.end.x)
.then_with(|| line.start.y.total_cmp(&line.end.y));
if cmp == std::cmp::Ordering::Greater {
Line::new(line.end, line.start)
} else {
line
}
}
pub(crate) fn sweep_line_ord<T: GeoFloat>(a: &Line<T>, b: &Line<T>) -> std::cmp::Ordering {
a.start
.x
.total_cmp(&b.start.x)
.then_with(|| a.start.y.total_cmp(&b.start.y))
.then_with(|| a.end.x.total_cmp(&b.end.x))
.then_with(|| a.end.y.total_cmp(&b.end.y))
}
pub(crate) fn snap_or_register_point<T: SpadeTriangulationFloat>(
point: Coord<T>,
known_points: &mut RTree<Coord<T>>,
snap_radius: T,
) -> Coord<T> {
known_points
.nearest_neighbor(&point)
.filter(|nearest_point| Euclidean.distance(**nearest_point, point) < snap_radius)
.cloned()
.unwrap_or_else(|| {
known_points.insert(point);
point
})
}
pub(crate) fn preprocess_lines<T: SpadeTriangulationFloat>(
lines: Vec<Line<T>>,
snap_radius: T,
) -> (RTree<Coord<T>>, Vec<Line<T>>) {
let mut known_coords: RTree<Coord<T>> = RTree::new();
let mut snapped: Vec<Line<T>> = lines
.into_iter()
.map(|mut line| {
line.start = snap_or_register_point(line.start, &mut known_coords, snap_radius);
line.end = snap_or_register_point(line.end, &mut known_coords, snap_radius);
line
})
.filter(|l| l.start != l.end)
.collect();
for line in snapped.iter_mut() {
*line = sweep_normalize_line(*line);
}
snapped.sort_by(sweep_line_ord);
snapped.dedup();
(known_coords, snapped)
}
#[cfg(test)]
mod spade_triangulation {
use super::*;
use geo_types::*;
fn assert_num_triangles<T: SpadeTriangulationFloat>(
triangulation: &TriangulationResult<Triangles<T>>,
num: usize,
) {
assert_eq!(
triangulation
.as_ref()
.map(|tris| tris.len())
.expect("triangulation success"),
num
)
}
#[test]
fn basic_triangle_triangulates() {
let triangulation = Triangle::new(
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 0.0, y: 1.0 },
)
.unconstrained_triangulation();
assert_num_triangles(&triangulation, 1);
}
#[test]
fn basic_rectangle_triangulates() {
let triangulation = Rect::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 })
.unconstrained_triangulation();
assert_num_triangles(&triangulation, 2);
}
#[test]
fn basic_polygon_triangulates() {
let triangulation = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 1.0 },
Coord { x: -1.0, y: 0.0 },
Coord { x: -0.5, y: -1.0 },
Coord { x: 0.5, y: -1.0 },
Coord { x: 1.0, y: 0.0 },
]),
vec![],
)
.unconstrained_triangulation();
assert_num_triangles(&triangulation, 3);
}
#[test]
fn overlapping_triangles_triangulate_unconstrained() {
let triangles = vec![
Triangle::new(
Coord { x: 0.0, y: 0.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 0.0, y: 2.0 },
),
Triangle::new(
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: 1.0, y: -1.0 },
),
];
let unconstrained_triangulation = triangles.unconstrained_triangulation();
assert_num_triangles(&unconstrained_triangulation, 4);
}
#[test]
fn overlapping_triangles_triangulate_constrained_outer() {
let triangles = vec![
Triangle::new(
Coord { x: 0.0, y: 0.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 0.0, y: 2.0 },
),
Triangle::new(
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: 1.0, y: -1.0 },
),
];
let constrained_outer_triangulation =
triangles.constrained_outer_triangulation(Default::default());
assert_num_triangles(&constrained_outer_triangulation, 8);
}
#[test]
fn overlapping_triangles_triangulate_constrained() {
let triangles = vec![
Triangle::new(
Coord { x: 0.0, y: 0.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 0.0, y: 2.0 },
),
Triangle::new(
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: 1.0, y: -1.0 },
),
];
let constrained_outer_triangulation =
triangles.constrained_triangulation(Default::default());
assert_num_triangles(&constrained_outer_triangulation, 6);
}
#[test]
fn u_shaped_polygon_triangulates_unconstrained() {
let u_shape = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: 2.0, y: 1.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 3.0, y: 0.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 0.0, y: 3.0 },
]),
vec![],
);
let unconstrained_triangulation = u_shape.unconstrained_triangulation();
assert_num_triangles(&unconstrained_triangulation, 8);
}
#[test]
fn u_shaped_polygon_triangulates_constrained_outer() {
let u_shape = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: 2.0, y: 1.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 3.0, y: 0.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 0.0, y: 3.0 },
]),
vec![],
);
let constrained_outer_triangulation =
u_shape.constrained_outer_triangulation(Default::default());
assert_num_triangles(&constrained_outer_triangulation, 8);
}
#[test]
fn u_shaped_polygon_triangulates_constrained_inner() {
let u_shape = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: 2.0, y: 1.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 3.0, y: 0.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 0.0, y: 3.0 },
]),
vec![],
);
let constrained_triangulation = u_shape.constrained_triangulation(Default::default());
assert_num_triangles(&constrained_triangulation, 6);
}
#[test]
fn various_snap_radius_works() {
let u_shape = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: 2.0, y: 1.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 3.0, y: 0.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 0.0, y: 3.0 },
]),
vec![],
);
for snap_with in (1..6).map(|pow| 0.1_f64.powi(pow)) {
let constrained_triangulation =
u_shape.constrained_triangulation(DelaunayTriangulationConfig {
snap_radius: snap_with,
});
assert_num_triangles(&constrained_triangulation, 6);
}
}
#[test]
fn multipoint_triangulates_unconstrained() {
use super::TriangulateDelaunayUnconstrained;
let points: MultiPoint<f64> = wkt!(MULTIPOINT(0. 0., 1. 0., 0.5 1., 0.5 0.5));
let triangulation = points.unconstrained_triangulation();
assert_num_triangles(&triangulation, 3);
}
#[test]
fn point_triangulates_unconstrained() {
use super::TriangulateDelaunayUnconstrained;
let point: Point<f64> = wkt!(POINT(0. 0.));
let triangulation = point.unconstrained_triangulation();
assert_num_triangles(&triangulation, 0);
}
#[test]
fn coord_slice_triangulates_unconstrained() {
use super::TriangulateDelaunayUnconstrained;
let coords: &[Coord<f64>] = &[
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 0.5, y: 1.0 },
];
let triangulation = coords.unconstrained_triangulation();
assert_num_triangles(&triangulation, 1);
}
#[test]
fn polygon_slice_triangulates() {
use super::{TriangulateDelaunay, TriangulateDelaunayUnconstrained};
let polygons: Vec<Polygon<f64>> = vec![
wkt!(POLYGON((0. 0., 1. 0., 0.5 1., 0. 0.))),
wkt!(POLYGON((2. 0., 3. 0., 2.5 1., 2. 0.))),
];
let slice: &[Polygon<f64>] = &polygons;
let triangulation = slice.unconstrained_triangulation();
assert_num_triangles(&triangulation, 4);
let constrained = slice.constrained_triangulation(Default::default());
assert_num_triangles(&constrained, 2);
}
#[test]
fn prepare_intersection_no_crossings() {
let lines = vec![
Line::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 10.0, y: 0.0 }),
Line::new(Coord { x: 0.0, y: 5.0 }, Coord { x: 10.0, y: 5.0 }),
];
let result = split_segments_at_intersections(lines, RTree::new(), 0.0001).unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn prepare_intersection_single_crossing() {
let lines = vec![
Line::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 10.0, y: 10.0 }),
Line::new(Coord { x: 0.0, y: 10.0 }, Coord { x: 10.0, y: 0.0 }),
];
let result = split_segments_at_intersections(lines, RTree::new(), 0.0001).unwrap();
assert_eq!(result.len(), 4, "two crossing lines -> 4 sub-segments");
}
#[test]
fn prepare_intersection_multiple_splits() {
let lines = vec![
Line::new(Coord { x: 0.0, y: 5.0 }, Coord { x: 10.0, y: 5.0 }),
Line::new(Coord { x: 3.0, y: 0.0 }, Coord { x: 3.0, y: 10.0 }),
Line::new(Coord { x: 7.0, y: 0.0 }, Coord { x: 7.0, y: 10.0 }),
];
let result = split_segments_at_intersections(lines, RTree::new(), 0.0001).unwrap();
assert_eq!(result.len(), 7);
}
#[test]
fn prepare_intersection_diagonal_sort() {
let lines = vec![
Line::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 10.0, y: 10.0 }),
Line::new(Coord { x: 0.0, y: 3.0 }, Coord { x: 10.0, y: 3.0 }),
Line::new(Coord { x: 0.0, y: 7.0 }, Coord { x: 10.0, y: 7.0 }),
];
let result = split_segments_at_intersections(lines, RTree::new(), 0.0001).unwrap();
assert_eq!(result.len(), 7);
for line in &result {
assert_ne!(line.start, line.end, "sub-segment should not be degenerate");
}
}
#[test]
fn prepare_intersection_endpoint_touch() {
let lines = vec![
Line::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 5.0, y: 5.0 }),
Line::new(Coord { x: 5.0, y: 5.0 }, Coord { x: 10.0, y: 0.0 }),
];
let result = split_segments_at_intersections(lines, RTree::new(), 0.0001).unwrap();
assert_eq!(
result.len(),
2,
"shared endpoint should not cause splitting"
);
}
}