use alloc::vec::Vec;
use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_model::{MultiPolygon, Polygon};
use geometry_tag::SameAs;
use geometry_trait::PointMut;
use crate::operation::{OverlayError, union_poly};
use crate::relate::overlaps;
#[inline]
#[must_use = "merging can fail and the merged geometry should be used"]
pub fn merge_polygons<P>(
polygons: Vec<Polygon<P>>,
) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let mut work = polygons;
while let Some((i, j)) = first_overlapping_pair(&work) {
let b = work.remove(j); let a = work.remove(i);
let unioned = union_poly(&a, &b)?;
for pg in unioned.0 {
work.push(pg);
}
}
Ok(MultiPolygon(work))
}
#[inline]
#[must_use = "merging can fail and the merged geometry should be used"]
pub fn merge_elements<P, I>(polygons: I) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
I: IntoIterator<Item = Polygon<P>>,
{
merge_polygons(polygons.into_iter().collect())
}
#[inline]
#[must_use = "merging can fail and the merged geometry should be used"]
pub fn merge_multipolygon<P>(
mp: MultiPolygon<Polygon<P>>,
) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
merge_polygons(mp.0)
}
#[must_use = "stitching can fail and the assembled polygons should be used"]
pub fn stitch_triangles<P, I>(triangles: I) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
I: IntoIterator<Item = Polygon<P>>,
{
let mut work: Vec<Polygon<P>> = triangles.into_iter().collect();
while let Some((first, second, polygon)) = first_stitchable_pair(&work)? {
work.remove(second);
work.remove(first);
work.push(polygon);
}
Ok(MultiPolygon(work))
}
fn first_stitchable_pair<P>(
polygons: &[Polygon<P>],
) -> Result<Option<(usize, usize, Polygon<P>)>, OverlayError>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
for first in 0..polygons.len() {
for second in (first + 1)..polygons.len() {
let mut unioned = union_poly(&polygons[first], &polygons[second])?
.0
.into_iter();
let (Some(polygon), None) = (unioned.next(), unioned.next()) else {
continue;
};
return Ok(Some((first, second, polygon)));
}
}
Ok(None)
}
fn first_overlapping_pair<P>(polygons: &[Polygon<P>]) -> Option<(usize, usize)>
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
for i in 0..polygons.len() {
for j in (i + 1)..polygons.len() {
if overlaps(&polygons[i], &polygons[j]).unwrap_or(false) {
return Some((i, j));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{merge_polygons, stitch_triangles};
use geometry_algorithm::ring_area;
use geometry_cs::Cartesian;
use geometry_model::{MultiPolygon, Point2D, Polygon, polygon};
use geometry_trait::{MultiPolygon as _, Polygon as _};
type P = Point2D<f64, Cartesian>;
fn square(x: f64, y: f64, s: f64) -> Polygon<P> {
polygon![[(x, y), (x + s, y), (x + s, y + s), (x, y + s), (x, y)]]
}
fn total_area(mp: &MultiPolygon<Polygon<P>>) -> f64 {
mp.polygons().map(|pg| ring_area(pg.exterior()).abs()).sum()
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-5 * a.abs().max(b.abs()).max(1.0)
}
#[test]
fn overlapping_pair_merges_to_one() {
let a = square(0.0, 0.0, 2.0);
let b = square(1.0, 1.0, 2.0);
let merged = merge_polygons(vec![a, b]).unwrap();
assert_eq!(merged.polygons().count(), 1);
assert!(close(total_area(&merged), 7.0));
}
#[test]
fn disjoint_polygons_stay_separate() {
let a = square(0.0, 0.0, 1.0);
let b = square(5.0, 5.0, 1.0);
let merged = merge_polygons(vec![a, b]).unwrap();
assert_eq!(merged.polygons().count(), 2);
}
#[test]
fn mixed_overlap_and_disjoint() {
let a = square(0.0, 0.0, 2.0);
let b = square(1.0, 1.0, 2.0); let far = square(9.0, 9.0, 1.0); let merged = merge_polygons(vec![a, b, far]).unwrap();
assert_eq!(merged.polygons().count(), 2);
}
#[test]
fn empty_input_empty_output() {
let merged = merge_polygons::<P>(vec![]).unwrap();
assert_eq!(merged.polygons().count(), 0);
}
#[test]
fn single_element_returned_unchanged() {
let a = square(0.0, 0.0, 2.0);
let merged = merge_polygons(vec![a]).unwrap();
assert_eq!(merged.polygons().count(), 1);
assert!(close(total_area(&merged), 4.0));
}
#[test]
fn shared_edge_triangles_stitch_to_one_polygon() {
let first: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]];
let second: Polygon<P> = polygon![[(0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]];
let stitched = stitch_triangles([first, second]).unwrap();
assert_eq!(stitched.polygons().count(), 1);
assert!(close(total_area(&stitched), 1.0));
}
#[test]
fn vertex_only_touch_does_not_fuse() {
let a = square(0.0, 0.0, 2.0);
let b = square(2.0, 2.0, 2.0); let merged = merge_polygons(vec![a, b]).unwrap();
assert_eq!(merged.polygons().count(), 2);
assert!(close(total_area(&merged), 8.0));
}
#[test]
fn multipolygon_wrapper_merges_members() {
let mp = MultiPolygon(vec![
square(0.0, 0.0, 2.0),
square(1.0, 1.0, 2.0), square(9.0, 9.0, 1.0), ]);
let merged = super::merge_multipolygon(mp).unwrap();
assert_eq!(merged.polygons().count(), 2);
}
#[test]
fn touching_only_pair_does_not_abort_the_merge() {
let a = square(0.0, 0.0, 2.0);
let b = square(1.0, 1.0, 2.0); let c = square(10.0, 0.0, 2.0);
let d = square(12.0, 0.0, 2.0); let merged = merge_polygons(vec![a, b, c, d]).unwrap();
assert_eq!(merged.polygons().count(), 3);
}
}