use std::collections::{HashSet, VecDeque};
use geo_types::{Coord, Line, LineString, MultiPolygon, Polygon};
use spade::handles::{FixedFaceHandle, InnerTag};
use spade::{ConstrainedDelaunayTriangulation, Triangulation};
use crate::algorithm::area::get_linestring_area;
use crate::algorithm::triangulate_delaunay::{
DelaunayTriangulationConfig, SpadeTriangulationFloat, TriangulationError,
split_segments_at_intersections, sweep_line_ord, sweep_normalize_line,
};
use crate::coordinate_position::{CoordPos, coord_pos_relative_to_ring};
use crate::{GeoFloat, LinesIter};
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RepairPolygonError {
CoordinateIsNaN,
CoordinateTooLarge,
CoordinateTooSmall,
ConstraintFailure,
}
impl std::fmt::Display for RepairPolygonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CoordinateIsNaN => write!(f, "coordinate value is NaN"),
Self::CoordinateTooLarge => {
write!(f, "coordinate value exceeds triangulation kernel limit")
}
Self::CoordinateTooSmall => write!(
f,
"coordinate value is non-zero but below triangulation kernel minimum"
),
Self::ConstraintFailure => {
write!(f, "internal constraint edge insertion failed")
}
}
}
}
impl std::error::Error for RepairPolygonError {}
impl From<TriangulationError> for RepairPolygonError {
fn from(err: TriangulationError) -> Self {
match err {
TriangulationError::SpadeError(spade::InsertionError::NAN) => Self::CoordinateIsNaN,
TriangulationError::SpadeError(spade::InsertionError::TooLarge) => {
Self::CoordinateTooLarge
}
TriangulationError::SpadeError(spade::InsertionError::TooSmall) => {
Self::CoordinateTooSmall
}
TriangulationError::ConstraintFailure | TriangulationError::LoopTrap => {
Self::ConstraintFailure
}
}
}
}
pub trait MakeValid<T: GeoFloat> {
fn make_valid(&self) -> Result<MultiPolygon<T>, RepairPolygonError>;
}
impl<T> MakeValid<T> for Polygon<T>
where
T: SpadeTriangulationFloat,
{
fn make_valid(&self) -> Result<MultiPolygon<T>, RepairPolygonError> {
repair_polygon(self)
}
}
impl<T> MakeValid<T> for MultiPolygon<T>
where
T: SpadeTriangulationFloat,
{
fn make_valid(&self) -> Result<MultiPolygon<T>, RepairPolygonError> {
repair_multi_polygon(self)
}
}
fn repair_polygon<T: SpadeTriangulationFloat>(
polygon: &Polygon<T>,
) -> Result<MultiPolygon<T>, RepairPolygonError> {
let lines = extract_lines(polygon);
if lines.is_empty() {
return Ok(MultiPolygon::new(vec![]));
}
repair_from_lines(lines)
}
fn repair_multi_polygon<T: SpadeTriangulationFloat>(
mp: &MultiPolygon<T>,
) -> Result<MultiPolygon<T>, RepairPolygonError> {
let lines: Vec<Line<T>> = mp.iter().flat_map(extract_lines).collect();
if lines.is_empty() {
return Ok(MultiPolygon::new(vec![]));
}
repair_from_lines(lines)
}
fn repair_from_lines<T: SpadeTriangulationFloat>(
lines: Vec<Line<T>>,
) -> Result<MultiPolygon<T>, RepairPolygonError> {
for line in &lines {
for coord in [line.start, line.end] {
if coord.x.is_nan() || coord.y.is_nan() {
return Err(RepairPolygonError::CoordinateIsNaN);
}
if coord.x.is_infinite() || coord.y.is_infinite() {
return Err(RepairPolygonError::CoordinateTooLarge);
}
}
}
let snap_radius = DelaunayTriangulationConfig::<T>::default().snap_radius;
let cdt = build_cdt(lines, snap_radius)?;
if cdt.num_inner_faces() == 0 {
return Ok(MultiPolygon::empty());
}
let interior = label_faces(&cdt);
if interior.is_empty() {
return Ok(MultiPolygon::new(vec![]));
}
let rings = trace_rings(&cdt, &interior);
let rings: Vec<LineString<T>> = rings
.into_iter()
.flat_map(|r| split_ring_at_pinch_points(r.0))
.map(LineString::new)
.collect();
Ok(assemble_polygons(rings))
}
fn extract_lines<T: GeoFloat>(polygon: &Polygon<T>) -> Vec<Line<T>> {
polygon.lines_iter().collect()
}
fn prepare_constraint_lines<T: SpadeTriangulationFloat>(
lines: Vec<Line<T>>,
snap_radius: T,
) -> Result<Vec<Line<T>>, RepairPolygonError> {
use crate::algorithm::triangulate_delaunay::snap_or_register_point;
use rstar::RTree;
let mut known_coords: RTree<Coord<T>> = RTree::new();
let mut lines: 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();
odd_even_filter(&mut lines);
let mut lines = split_segments_at_intersections(lines, known_coords, snap_radius)?;
odd_even_filter(&mut lines);
Ok(lines)
}
fn build_cdt<T: SpadeTriangulationFloat>(
lines: Vec<Line<T>>,
snap_radius: T,
) -> Result<ConstrainedDelaunayTriangulation<Coord<T>>, RepairPolygonError> {
let lines = prepare_constraint_lines(lines, 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 start != end && cdt.can_add_constraint(start, end) {
cdt.add_constraint(start, end);
}
Ok(cdt)
},
)
}
fn odd_even_filter<T: GeoFloat>(lines: &mut Vec<Line<T>>) {
for line in lines.iter_mut() {
*line = sweep_normalize_line(*line);
}
lines.sort_by(sweep_line_ord);
let mut result = Vec::new();
let mut i = 0;
while i < lines.len() {
let start = i;
while i < lines.len() && lines[i] == lines[start] {
i += 1;
}
if (i - start) % 2 == 1 {
result.push(lines[start]);
}
}
*lines = result;
}
fn label_faces<T: SpadeTriangulationFloat>(
cdt: &ConstrainedDelaunayTriangulation<Coord<T>>,
) -> HashSet<FixedFaceHandle<InnerTag>> {
let mut interior: HashSet<FixedFaceHandle<InnerTag>> = HashSet::new();
let mut visited: HashSet<FixedFaceHandle<InnerTag>> = HashSet::new();
let mut queue: VecDeque<(FixedFaceHandle<InnerTag>, bool)> = VecDeque::new();
for edge in cdt.directed_edges() {
if !edge.face().is_outer() {
continue;
}
let opposite = edge.rev().face();
let Some(inner) = opposite.as_inner() else {
debug_assert!(
false,
"Each directed edge whose face() is the outer face has a reversed edge whose face() is an inner face"
);
continue;
};
let handle = inner.fix();
if visited.insert(handle) {
let is_interior = edge.is_constraint_edge();
if is_interior {
interior.insert(handle);
}
queue.push_back((handle, is_interior));
}
}
while let Some((face_handle, face_is_interior)) = queue.pop_front() {
let face = cdt.face(face_handle);
for edge in face.adjacent_edges() {
let neighbour = edge.rev().face();
if neighbour.is_outer() {
continue;
}
let Some(inner_neighbour) = neighbour.as_inner() else {
continue;
};
let n_handle = inner_neighbour.fix();
if !visited.insert(n_handle) {
continue;
}
let crosses_constraint = edge.is_constraint_edge();
let neighbour_is_interior = if crosses_constraint {
!face_is_interior
} else {
face_is_interior
};
if neighbour_is_interior {
interior.insert(n_handle);
}
queue.push_back((n_handle, neighbour_is_interior));
}
}
interior
}
fn trace_rings<T: SpadeTriangulationFloat>(
cdt: &ConstrainedDelaunayTriangulation<Coord<T>>,
interior: &HashSet<FixedFaceHandle<InnerTag>>,
) -> Vec<LineString<T>> {
use spade::handles::FixedDirectedEdgeHandle;
let is_face_interior = |face: spade::handles::FaceHandle<
'_,
spade::handles::PossiblyOuterTag,
Coord<T>,
_,
_,
_,
>| {
face.as_inner()
.map(|f| interior.contains(&f.fix()))
.unwrap_or(false)
};
let boundary_edges: HashSet<FixedDirectedEdgeHandle> = cdt
.directed_edges()
.filter(|e| is_face_interior(e.face()) && !is_face_interior(e.rev().face()))
.map(|e| e.fix())
.collect();
let mut used: HashSet<FixedDirectedEdgeHandle> = HashSet::new();
let mut rings = Vec::new();
for &start_handle in &boundary_edges {
if used.contains(&start_handle) {
continue;
}
let mut ring_coords: Vec<Coord<T>> = Vec::new();
let mut current_fix = start_handle;
let mut outer_safety = boundary_edges.len();
loop {
used.insert(current_fix);
let current = cdt.directed_edge(current_fix);
let pos = current.from().position();
ring_coords.push(Coord { x: pos.x, y: pos.y });
let mut candidate = current.next();
let mut inner_safety = cdt.num_directed_edges();
loop {
if boundary_edges.contains(&candidate.fix()) && !used.contains(&candidate.fix()) {
break;
}
if candidate.fix() == start_handle {
break;
}
candidate = candidate.rev().next();
inner_safety = inner_safety.saturating_sub(1);
if inner_safety == 0 {
debug_assert!(
false,
"inner rotation safety limit reached: could not find next boundary edge around vertex"
);
break;
}
}
current_fix = candidate.fix();
if current_fix == start_handle {
break;
}
outer_safety = outer_safety.saturating_sub(1);
if outer_safety == 0 {
debug_assert!(
false,
"outer ring safety limit reached: ring has more edges than boundary edges"
);
break;
}
}
if let Some(&first) = ring_coords.first() {
ring_coords.push(first);
}
if ring_coords.len() >= 4 {
rings.push(LineString::new(ring_coords));
}
}
rings
}
fn split_ring_at_pinch_points<T: GeoFloat>(ring: Vec<Coord<T>>) -> Vec<Vec<Coord<T>>> {
let mut coords = ring;
debug_assert!(
coords.len() >= 2 && coords.first() == coords.last(),
"ring passed to split_ring_at_pinch_points must be closed"
);
if coords.len() >= 2 && coords.first() == coords.last() {
coords.pop();
}
if coords.len() < 3 {
return vec![];
}
let mut result: Vec<Vec<Coord<T>>> = Vec::new();
loop {
let repeat = coords.iter().enumerate().find_map(|(i, c)| {
coords[i + 1..]
.iter()
.position(|other| other == c)
.map(|j| (i, i + 1 + j))
});
match repeat {
Some((first, second)) => {
let sub: Vec<Coord<T>> = coords[first..=second].to_vec();
coords.drain((first + 1)..=second);
if sub.len() >= 4 {
result.extend(split_ring_at_pinch_points(sub));
}
}
None => break,
}
}
if coords.len() >= 3 {
let first = coords[0];
coords.push(first);
result.push(coords);
}
result
}
fn assemble_polygons<T: SpadeTriangulationFloat>(rings: Vec<LineString<T>>) -> MultiPolygon<T> {
let mut exteriors: Vec<LineString<T>> = Vec::new();
let mut holes: Vec<LineString<T>> = Vec::new();
for ring in rings {
let area = get_linestring_area(&ring);
if area > T::zero() {
exteriors.push(ring);
} else if area < T::zero() {
holes.push(ring);
}
}
if exteriors.is_empty() {
return MultiPolygon::new(vec![]);
}
let mut polygons: Vec<(LineString<T>, Vec<LineString<T>>)> =
exteriors.into_iter().map(|ext| (ext, Vec::new())).collect();
'next_hole: for hole in holes {
for pt in &hole.0 {
for (ext, ext_holes) in &mut polygons {
if coord_pos_relative_to_ring(*pt, ext) == CoordPos::Inside {
ext_holes.push(hole);
continue 'next_hole;
}
}
}
}
MultiPolygon::new(
polygons
.into_iter()
.map(|(ext, holes)| Polygon::new(ext, holes))
.collect(),
)
}