use alloc::vec::Vec;
use geometry_algorithm::{ring_area, within};
use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_model::{MultiPolygon, Polygon, Ring};
use geometry_tag::{PolygonTag, SameAs};
use geometry_trait::{
Geometry, Point as PointTrait, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
};
use crate::surface_point::point_on_surface;
#[must_use]
#[allow(
clippy::missing_panics_doc,
reason = "the take().unwrap()s cannot fire: each ring is either an \
outer (taken once in the outers pass) or a hole (taken once \
in the holes pass) — `container_of` assigns exactly one role"
)]
pub fn assemble_multipolygon<P>(rings: Vec<Ring<P>>) -> MultiPolygon<Polygon<P>>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let n = rings.len();
let mut container_of: Vec<Option<usize>> = alloc::vec![None; n];
for i in 0..n {
let Some(rep) = representative_point(&rings[i]) else {
continue;
};
container_of[i] = smallest_ring_container(&rings, i, &rep);
}
let depths: Vec<usize> = (0..n)
.map(|index| containment_depth(&container_of, index))
.collect();
let mut slots: Vec<Option<Ring<P>>> = rings.into_iter().map(Some).collect();
let mut outer_slot: Vec<Option<usize>> = alloc::vec![None; n];
let mut polygons: Vec<Polygon<P>> = Vec::new();
for i in 0..n {
if depths[i] % 2 == 0 {
outer_slot[i] = Some(polygons.len());
let mut outer = slots[i].take().unwrap();
orient_ring(&mut outer, true);
polygons.push(Polygon::new(outer));
}
}
for i in 0..n {
if depths[i] % 2 == 1 {
let parent_ring = container_of[i]
.expect("an odd containment depth necessarily has an immediate parent");
let slot = outer_slot[parent_ring]
.expect("the immediate parent of an odd-depth ring has even depth");
let mut hole = slots[i].take().unwrap();
orient_ring(&mut hole, false);
polygons[slot].inners.push(hole);
}
}
MultiPolygon(polygons)
}
fn containment_depth(parents: &[Option<usize>], mut index: usize) -> usize {
let mut depth = 0;
while let Some(parent) = parents[index] {
depth += 1;
index = parent;
}
depth
}
fn orient_ring<P>(ring: &mut Ring<P>, clockwise: bool)
where
P: PointTrait,
P::Scalar: CoordinateScalar,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let is_clockwise = ring_area(ring) > P::Scalar::ZERO;
if is_clockwise != clockwise {
ring.0.reverse();
}
}
fn smallest_ring_container<P>(rings: &[Ring<P>], i: usize, rep: &P) -> Option<usize>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let own_area = ring_area(&rings[i]).abs();
let mut best: Option<(usize, P::Scalar)> = None;
for (j, other) in rings.iter().enumerate() {
if j == i {
continue;
}
let a = ring_area(other).abs();
if a <= own_area {
continue;
}
if within(rep, other) {
match best {
Some((_, ba)) if ba <= a => {}
_ => best = Some((j, a)),
}
}
}
best.map(|(j, _)| j)
}
fn representative_point<P>(ring: &Ring<P>) -> Option<P>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar,
{
point_on_surface(&RingPolygon(ring)).or_else(|| ring.points().next().copied())
}
struct RingPolygon<'a, P: PointTrait>(&'a Ring<P>);
impl<P: PointTrait> Geometry for RingPolygon<'_, P> {
type Kind = PolygonTag;
type Point = P;
}
impl<P: PointTrait> PolygonTrait for RingPolygon<'_, P> {
type Ring = Ring<P>;
fn exterior(&self) -> &Ring<P> {
self.0
}
fn interiors(&self) -> impl ExactSizeIterator<Item = &Ring<P>> {
core::iter::empty()
}
}
#[cfg(test)]
mod tests {
use super::assemble_multipolygon;
use geometry_cs::Cartesian;
use geometry_model::{Point2D, Ring};
use geometry_trait::{MultiPolygon as _, Point as _, Polygon as _, Ring as _};
type P = Point2D<f64, Cartesian>;
fn ccw_square(x: f64, y: f64, s: f64) -> Ring<P> {
Ring::from_vec(vec![
P::new(x, y),
P::new(x + s, y),
P::new(x + s, y + s),
P::new(x, y + s),
P::new(x, y),
])
}
fn cw_square(x: f64, y: f64, s: f64) -> Ring<P> {
Ring::from_vec(vec![
P::new(x, y),
P::new(x, y + s),
P::new(x + s, y + s),
P::new(x + s, y),
P::new(x, y),
])
}
#[test]
fn single_outer_no_holes() {
let mp = assemble_multipolygon(vec![ccw_square(0.0, 0.0, 4.0)]);
assert_eq!(mp.polygons().count(), 1);
let pg = mp.polygons().next().unwrap();
assert_eq!(pg.interiors().count(), 0);
}
#[test]
fn degenerate_ring_falls_back_to_its_first_vertex() {
let ring: Ring<P> = Ring::from_vec(vec![P::new(1.0, 2.0)]);
let mp = assemble_multipolygon(vec![ring]);
assert_eq!(mp.polygons().count(), 1);
}
#[test]
fn two_disjoint_outers() {
let mp = assemble_multipolygon(vec![ccw_square(0.0, 0.0, 1.0), ccw_square(5.0, 5.0, 1.0)]);
assert_eq!(mp.polygons().count(), 2);
}
#[test]
fn hole_nested_in_outer() {
let outer = ccw_square(0.0, 0.0, 10.0);
let hole = cw_square(3.0, 3.0, 2.0);
let mp = assemble_multipolygon(vec![outer, hole]);
assert_eq!(mp.polygons().count(), 1);
let pg = mp.polygons().next().unwrap();
assert_eq!(pg.interiors().count(), 1);
let hv = pg.interiors().next().unwrap().points().next().unwrap();
assert!(hv.get::<0>() > 0.0 && hv.get::<0>() < 10.0);
}
#[test]
fn same_winding_outer_and_hole_do_not_form_a_cycle() {
let outer = ccw_square(0.0, 0.0, 6.0); let hole = ccw_square(2.0, 2.0, 2.0); let mp = assemble_multipolygon(vec![outer, hole]);
assert_eq!(mp.polygons().count(), 1, "must not drop the polygon");
assert_eq!(mp.polygons().next().unwrap().interiors().count(), 1);
}
#[test]
fn hole_sharing_a_vertex_with_outer_still_nests() {
let outer = ccw_square(0.0, 0.0, 10.0);
let hole: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 4.0), P::new(0.0, 6.0),
P::new(2.0, 6.0),
P::new(2.0, 4.0),
P::new(0.0, 4.0),
]);
let mp = assemble_multipolygon(vec![outer, hole]);
assert_eq!(
mp.polygons().count(),
1,
"hole must not become a separate outer"
);
assert_eq!(mp.polygons().next().unwrap().interiors().count(), 1);
}
}