Skip to main content

geo/algorithm/
stitch.rs

1use std::collections::HashMap;
2
3use geo_types::{Coord, Line, LineString, MultiPolygon, Polygon, Triangle};
4
5use crate::winding_order::{WindingOrder, triangle_winding_order};
6use crate::{Contains, GeoFloat};
7
8// ========= Error Type ============
9
10#[derive(Debug)]
11pub enum LineStitchingError {
12    IncompleteRing(&'static str),
13}
14
15impl std::fmt::Display for LineStitchingError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "{self:?}")
18    }
19}
20
21impl std::error::Error for LineStitchingError {}
22
23pub(crate) type TriangleStitchingResult<T> = Result<T, LineStitchingError>;
24
25// ========= Main Algo ============
26
27#[deprecated(
28    since = "0.32.1",
29    note = "Output is not always valid - use unary_union which is typically faster and produces valid output. Convert your triangles to polygon `triangle.to_polygon()` first."
30)]
31/// Trait to stitch together split up triangles.
32pub trait StitchTriangles<T: GeoFloat>: private::Stitchable<T> {
33    /// This stitching only happens along identical edges which are located in two separate
34    /// geometries. Please read about the required pre conditions of the inputs!
35    ///
36    /// ```text
37    /// ┌─────x        ┌─────┐
38    /// │    /│        │     │
39    /// │   / │        │     │
40    /// │  /  │  ───►  │     │
41    /// │ /   │        │     │
42    /// │/    │        │     │
43    /// x─────┘        └─────┘
44    /// ```
45    ///
46    /// # Pre Conditions
47    ///
48    /// - The triangles in the input must not overlap! This also forbids identical triangles in the
49    ///   input set. If you want to do a union on overlapping triangles then c.f. `SpadeBoolops`.
50    /// - Input triangles should be valid polygons. For a definition of validity
51    ///   c.f. <https://www.postgis.net/workshops/postgis-intro/validity.html>
52    ///
53    /// # Examples
54    ///
55    /// ```text
56    /// use geo::StitchTriangles;
57    /// use geo::{Coord, Triangle, polygon};
58    ///
59    /// let tri1 = Triangle::from([
60    ///     Coord { x: 0.0, y: 0.0 },
61    ///     Coord { x: 1.0, y: 0.0 },
62    ///     Coord { x: 0.0, y: 1.0 },
63    /// ]);
64    /// let tri2 = Triangle::from([
65    ///     Coord { x: 1.0, y: 1.0 },
66    ///     Coord { x: 1.0, y: 0.0 },
67    ///     Coord { x: 0.0, y: 1.0 },
68    /// ]);
69    ///
70    /// let result = vec![tri1, tri2].stitch_triangulation();
71    ///
72    /// assert!(result.is_ok());
73    ///
74    /// let mp = result.unwrap();
75    ///
76    /// assert_eq!(mp.0.len(), 1);
77    ///
78    /// let poly = mp[0].clone();
79    /// // 4 coords + 1 duplicate for closed-ness
80    /// assert_eq!(poly.exterior().0.len(), 4 + 1);
81    ///
82    /// let expected = polygon![
83    ///     Coord { x: 1.0, y: 1.0 },
84    ///     Coord { x: 0.0, y: 1.0 },
85    ///     Coord { x: 0.0, y: 0.0 },
86    ///     Coord { x: 1.0, y: 0.0 },
87    /// ];
88    ///
89    /// assert_eq!(poly, expected);
90    /// ```
91    ///
92    /// # Additional Notes
93    ///
94    /// Stitching triangles which result in a polygon with a hole which touches the outline
95    /// (mentioned here [banana polygon](https://postgis.net/workshops/postgis-intro/validity.html#repairing-invalidity))
96    /// will result in a single polygon without interiors instead of a polygon with a single
97    /// interior
98    ///
99    /// ```text
100    /// ┌────────x────────┐
101    /// │\....../ \....../│
102    /// │.\..../   \..../.│
103    /// │..\../     \../..│
104    /// │...\/       \/...│
105    /// │...───────────...│
106    /// │../\....^..../\..│
107    /// │./..\../.\../..\.│
108    /// │/....\/...\/....\│
109    /// └─────────────────┘
110    ///
111    ///     │    │    │
112    ///     ▼    ▼    ▼
113    ///
114    /// ┌────────x────────┐
115    /// │       / \       │
116    /// │      /   \      │
117    /// │     /     \     │
118    /// │    /       \    │
119    /// │   ───────────   │
120    /// │                 │
121    /// │                 │
122    /// │                 │
123    /// └─────────────────┘
124    /// ```
125    ///
126    /// ---
127    ///
128    /// If you want to do something more general like a
129    /// [`Boolean Operation Union`](https://en.wikipedia.org/wiki/Boolean_operations_on_polygons)
130    /// you should use the trait `BooleanOps` or `SpadeBoolops`.
131    fn stitch_triangulation(&self) -> TriangleStitchingResult<MultiPolygon<T>>;
132}
133
134mod private {
135    use super::*;
136
137    pub trait Stitchable<T: GeoFloat>: AsRef<[Triangle<T>]> {}
138    impl<S, T> Stitchable<T> for S
139    where
140        S: AsRef<[Triangle<T>]>,
141        T: GeoFloat,
142    {
143    }
144}
145
146#[allow(deprecated)]
147impl<S, T> StitchTriangles<T> for S
148where
149    S: private::Stitchable<T>,
150    T: GeoFloat,
151{
152    fn stitch_triangulation(&self) -> TriangleStitchingResult<MultiPolygon<T>> {
153        stitch_triangles(self.as_ref().iter())
154    }
155}
156
157// main stitching algorithm
158fn stitch_triangles<'a, T, S>(triangles: S) -> TriangleStitchingResult<MultiPolygon<T>>
159where
160    T: GeoFloat + 'a,
161    S: Iterator<Item = &'a Triangle<T>>,
162{
163    let lines = triangles.flat_map(ccw_lines).collect::<Vec<_>>();
164
165    let boundary_lines = find_boundary_lines(lines);
166    let stitched_multipolygon = stitch_multipolygon_from_lines(boundary_lines)?;
167
168    let polys = stitched_multipolygon
169        .into_iter()
170        .map(find_and_fix_holes_in_exterior)
171        .collect::<Vec<_>>();
172
173    Ok(MultiPolygon::new(polys))
174}
175
176/// returns the triangle's lines with ccw orientation
177fn ccw_lines<T: GeoFloat>(tri: &Triangle<T>) -> [Line<T>; 3] {
178    match triangle_winding_order(tri) {
179        Some(WindingOrder::CounterClockwise) => tri.to_lines(),
180        _ => {
181            let [a, b, c] = tri.to_array();
182            [(b, a), (a, c), (c, b)].map(|(start, end)| Line::new(start, end))
183        }
184    }
185}
186
187/// checks whether the two lines are equal or inverted forms of each other
188#[inline]
189fn same_line<T: GeoFloat>(l1: &Line<T>, l2: &Line<T>) -> bool {
190    (l1.start == l2.start && l1.end == l2.end) || (l1.start == l2.end && l2.start == l1.end)
191}
192
193/// given a collection of lines from multiple polygons which partition an area we can have two
194/// kinds of lines:
195///
196/// - boundary lines: these are the unique lines on the boundary of the compound shape which is
197///   formed by the collection of polygons
198/// - inner lines: these are all non-boundary lines. They are not unique and have exactly one
199///   duplicate on one adjacent polygon in the collection (as long as the input is valid!)
200fn find_boundary_lines<T: GeoFloat>(lines: Vec<Line<T>>) -> Vec<Line<T>> {
201    lines.into_iter().fold(Vec::new(), |mut lines, new_line| {
202        if let Some(idx) = lines.iter().position(|line| same_line(line, &new_line)) {
203            lines.remove(idx);
204        } else {
205            lines.push(new_line);
206        }
207        lines
208    })
209}
210
211// Notes for future: This probably belongs into a `Validify` trait or something
212/// finds holes in polygon exterior and fixes them
213///
214/// This is important for scenarios like the banana polygon. Which is considered invalid
215/// https://www.postgis.net/workshops/postgis-intro/validity.html#repairing-invalidity
216fn find_and_fix_holes_in_exterior<F: GeoFloat>(mut poly: Polygon<F>) -> Polygon<F> {
217    fn detect_if_rings_closed_with_point<F: GeoFloat>(
218        points: &mut Vec<Coord<F>>,
219        p: Coord<F>,
220    ) -> Option<Vec<Coord<F>>> {
221        // early return here if nothing was found
222        let pos = points.iter().position(|&c| c == p)?;
223
224        // create ring by collecting the points if something was found
225        let ring = points
226            .drain(pos..)
227            .chain(std::iter::once(p))
228            .collect::<Vec<_>>();
229        Some(ring)
230    }
231
232    // find rings
233    let rings = {
234        let (points, mut rings) =
235            poly.exterior()
236                .into_iter()
237                .fold((vec![], vec![]), |(mut points, mut rings), coord| {
238                    rings.extend(detect_if_rings_closed_with_point(&mut points, *coord));
239                    points.push(*coord);
240                    (points, rings)
241                });
242
243        // add leftover coords as last ring
244        rings.push(points);
245
246        rings
247    };
248
249    // convert to polygons for containment checks
250    let mut rings = rings
251        .into_iter()
252        // filter out degenerate polygons which may be produced from the code above
253        .filter(|cs| cs.len() >= 3)
254        .map(|cs| Polygon::new(LineString::new(cs), vec![]))
255        .collect::<Vec<_>>();
256
257    // PERF: O(n^2) maybe someone can reduce this. Please benchmark!
258    fn find_outmost_ring<F: GeoFloat>(rings: &[Polygon<F>]) -> Option<usize> {
259        let enumerated_rings = || rings.iter().enumerate();
260        enumerated_rings()
261            .find(|(i, ring)| {
262                enumerated_rings()
263                    .filter(|(j, _)| i != j)
264                    .all(|(_, other)| ring.contains(other))
265            })
266            .map(|(i, _)| i)
267    }
268
269    // if exterior ring exists that contains all other rings, recreate the poly with:
270    //
271    // - exterior ring as exterior
272    // - other rings are counted to interiors
273    // - previously existing interiors are preserved
274    if let Some(outer_index) = find_outmost_ring(&rings) {
275        let exterior = rings.remove(outer_index).exterior().clone();
276        let interiors = poly
277            .interiors()
278            .iter()
279            .cloned()
280            .chain(rings.into_iter().map(|p| p.exterior().clone()))
281            .collect::<Vec<_>>();
282        poly = Polygon::new(exterior, interiors);
283    }
284    poly
285}
286
287/// Inputs to this function is a unordered set of lines that must form a valid multipolygon
288fn stitch_multipolygon_from_lines<F: GeoFloat>(
289    lines: Vec<Line<F>>,
290) -> TriangleStitchingResult<MultiPolygon<F>> {
291    let rings = stitch_rings_from_lines(lines)?;
292
293    fn find_parent_idxs<F: GeoFloat>(
294        ring_idx: usize,
295        ring: &LineString<F>,
296        all_rings: &[LineString<F>],
297    ) -> Vec<usize> {
298        all_rings
299            .iter()
300            .enumerate()
301            .filter(|(other_idx, _)| ring_idx != *other_idx)
302            .filter_map(|(idx, maybe_parent)| {
303                Polygon::new(maybe_parent.clone(), vec![])
304                    .contains(ring)
305                    .then_some(idx)
306            })
307            .collect()
308    }
309
310    // Associates every ring with its parents (the rings that contain it)
311    let parents_of: HashMap<usize, Vec<usize>> = rings
312        .iter()
313        .enumerate()
314        .map(|(ring_idx, ring)| {
315            let parent_idxs = find_parent_idxs(ring_idx, ring, &rings);
316            (ring_idx, parent_idxs)
317        })
318        .collect();
319
320    // Associates outer rings with their inner rings
321    let mut polygons_idxs: HashMap<usize, Vec<usize>> = HashMap::default();
322
323    // the direct parent is the parent ring which has itself the most parent rings
324    fn find_direct_parent(
325        parent_rings: &[usize],
326        parents_of: &HashMap<usize, Vec<usize>>,
327    ) -> Option<usize> {
328        parent_rings
329            .iter()
330            .filter_map(|ring_idx| {
331                parents_of
332                    .get(ring_idx)
333                    .map(|grandparent_rings| (ring_idx, grandparent_rings))
334            })
335            .max_by_key(|(_, grandparent_rings)| grandparent_rings.len())
336            .map(|(idx, _)| idx)
337            .copied()
338    }
339
340    // For each ring, we check how many parents it has  otherwise it's an outer ring
341    //
342    // This is important in the scenarios of "donuts" where you have an outer donut shaped
343    // polygon which completely contains a smaller polygon inside its hole
344    for (ring_index, parent_idxs) in parents_of.iter() {
345        let parent_count = parent_idxs.len();
346
347        // if it has an even number of parents, it's an outer ring so we can just add it if it's
348        // missing
349        if parent_count % 2 == 0 {
350            polygons_idxs.entry(*ring_index).or_default();
351            continue;
352        }
353
354        // if it has an odd number of parents, it's an inner ring
355
356        // to find the specific outer ring it is related to, we search for the direct parent.
357        let maybe_direct_parent = find_direct_parent(parent_idxs, &parents_of);
358
359        // As stated above the amount of parents here is odd, so it's at least one.
360        // Since every ring is registered in the `parents` hashmap, we find at least one element
361        // while iterating. Hence the `max_by_key` will always return `Some` since the iterator
362        // is never empty
363        debug_assert!(
364            maybe_direct_parent.is_some(),
365            "A direct parent has to exist"
366        );
367
368        // I'm not unwrapping here since I'm scared of panics
369        if let Some(direct_parent) = maybe_direct_parent {
370            polygons_idxs
371                .entry(direct_parent)
372                .or_default()
373                .push(*ring_index);
374        }
375    }
376
377    // lookup rings by index and create polygons
378    let polygons = polygons_idxs
379        .into_iter()
380        .map(|(parent_idx, children_idxs)| {
381            // PERF: extensive cloning here, maybe someone can improve this. Please benchmark!
382            let exterior = rings[parent_idx].clone();
383            let interiors = children_idxs
384                .into_iter()
385                .map(|child_idx| rings[child_idx].clone())
386                .collect::<Vec<_>>();
387            (exterior, interiors)
388        })
389        .map(|(exterior, interiors)| Polygon::new(exterior, interiors));
390
391    Ok(polygons.collect())
392}
393
394// ============== Helpers ================
395
396fn stitch_rings_from_lines<F: GeoFloat>(
397    lines: Vec<Line<F>>,
398) -> TriangleStitchingResult<Vec<LineString<F>>> {
399    // initial ring parts are just lines which will be stitch together progressively
400    let mut ring_parts: Vec<Vec<Coord<F>>> = lines
401        .iter()
402        .map(|line| vec![line.start, line.end])
403        .collect();
404
405    let mut rings: Vec<LineString<F>> = vec![];
406    // terminates since every loop we'll merge two elements into one so the total number of
407    // elements decreases each loop by at least one (two in the case of completing a ring)
408    while let Some(last_part) = ring_parts.pop() {
409        let (j, compound_part) = ring_parts
410            .iter()
411            .enumerate()
412            .find_map(|(j, other_part)| {
413                let new_part = try_stitch(&last_part, other_part)?;
414                Some((j, new_part))
415            })
416            .ok_or(LineStitchingError::IncompleteRing("Couldn't reconstruct polygons from the inputs. Please check them for invalidities."))?;
417        ring_parts.remove(j);
418
419        let is_ring = compound_part.first() == compound_part.last() && !compound_part.is_empty();
420
421        if is_ring {
422            let new_ring = LineString::new(compound_part);
423            rings.push(new_ring);
424        } else {
425            ring_parts.push(compound_part);
426        }
427    }
428
429    Ok(rings)
430}
431
432fn try_stitch<F: GeoFloat>(a: &[Coord<F>], b: &[Coord<F>]) -> Option<Vec<Coord<F>>> {
433    let a_first = a.first()?;
434    let a_last = a.last()?;
435    let b_first = b.first()?;
436    let b_last = b.last()?;
437
438    let a = || a.iter();
439    let b = || b.iter();
440
441    // _ -> X  |  X -> _
442    (a_last == b_first)
443        .then(|| a().chain(b().skip(1)).cloned().collect())
444        // X -> _  |  _ -> X
445        .or_else(|| (a_first == b_last).then(|| b().chain(a().skip(1)).cloned().collect()))
446}
447
448// ============= Tests ===========
449
450#[allow(deprecated)]
451#[cfg(test)]
452mod polygon_stitching_tests {
453
454    use crate::{Relate, TriangulateEarcut, Validation, Winding};
455
456    use super::*;
457    use geo_types::*;
458
459    #[test]
460    fn poly_inside_a_donut() {
461        _ = pretty_env_logger::try_init();
462        let zero = Coord::zero();
463        let one = Point::new(1.0, 1.0).0;
464        let outer_outer = Rect::new(zero, one * 5.0);
465        let inner_outer = Rect::new(one, one * 4.0);
466        let outer = Polygon::new(
467            outer_outer.to_polygon().exterior().clone(),
468            vec![inner_outer.to_polygon().exterior().clone()],
469        );
470        let inner = Rect::new(one * 2.0, one * 3.0).to_polygon();
471
472        let mp = MultiPolygon::new(vec![outer.clone(), inner.clone()]);
473
474        let tris = [inner, outer].map(|p| p.earcut_triangles()).concat();
475
476        let result = tris.stitch_triangulation().unwrap();
477
478        assert!(mp.relate(&result).is_equal_topo());
479    }
480
481    #[test]
482    fn stitch_independent_of_orientation() {
483        _ = pretty_env_logger::try_init();
484        let mut tri1 = Triangle::from([
485            Coord { x: 0.0, y: 0.0 },
486            Coord { x: 1.0, y: 0.0 },
487            Coord { x: 0.0, y: 1.0 },
488        ])
489        .to_polygon();
490        let mut tri2 = Triangle::from([
491            Coord { x: 1.0, y: 1.0 },
492            Coord { x: 1.0, y: 0.0 },
493            Coord { x: 0.0, y: 1.0 },
494        ])
495        .to_polygon();
496
497        tri1.exterior_mut(|ls| ls.make_ccw_winding());
498        tri2.exterior_mut(|ls| ls.make_ccw_winding());
499        let result_1 = [tri1.clone(), tri2.clone()]
500            .map(|tri| tri.earcut_triangles())
501            .concat()
502            .stitch_triangulation()
503            .unwrap();
504
505        tri1.exterior_mut(|ls| ls.make_cw_winding());
506        tri2.exterior_mut(|ls| ls.make_ccw_winding());
507        let result_2 = [tri1.clone(), tri2.clone()]
508            .map(|tri| tri.earcut_triangles())
509            .concat()
510            .stitch_triangulation()
511            .unwrap();
512
513        tri1.exterior_mut(|ls| ls.make_cw_winding());
514        tri2.exterior_mut(|ls| ls.make_cw_winding());
515        let result_3 = [tri1.clone(), tri2.clone()]
516            .map(|tri| tri.earcut_triangles())
517            .concat()
518            .stitch_triangulation()
519            .unwrap();
520
521        tri1.exterior_mut(|ls| ls.make_ccw_winding());
522        tri2.exterior_mut(|ls| ls.make_cw_winding());
523        let result_4 = [tri1, tri2]
524            .map(|tri| tri.earcut_triangles())
525            .concat()
526            .stitch_triangulation()
527            .unwrap();
528
529        assert!(result_1.relate(&result_2).is_equal_topo());
530        assert!(result_2.relate(&result_3).is_equal_topo());
531        assert!(result_3.relate(&result_4).is_equal_topo());
532    }
533
534    #[test]
535    fn stitch_creating_hole() {
536        let poly1 = wkt!(POLYGON((0.0 0.0,1.0 0.0,1.0 1.0,1.0 2.0,2.0 2.0,2.0 1.0,2.0 0.0,3.0 0.0,3.0 3.0,0.0 3.0,0.0 0.0)));
537        let poly2 = wkt!(POLYGON((1.0 0.0,2.0 0.0,2.0 1.0,1.0 1.0,1.0 0.0)));
538
539        let result = [poly1, poly2]
540            .map(|p| p.earcut_triangles())
541            .concat()
542            .stitch_triangulation()
543            .unwrap();
544
545        assert_eq!(result.0.len(), 1);
546        assert_eq!(result[0].interiors().len(), 1);
547    }
548
549    #[test]
550    fn inner_banana_produces_hole() {
551        let poly = wkt!(POLYGON((0.0 0.0,4.0 0.0,3.0 2.0,5.0 2.0,4.0 0.0,8.0 0.0,4.0 4.0,0.0 0.0)));
552
553        let result = [poly]
554            .map(|p| p.earcut_triangles())
555            .concat()
556            .stitch_triangulation()
557            .unwrap();
558
559        assert_eq!(result.0.len(), 1);
560        assert_eq!(result[0].interiors().len(), 1);
561    }
562
563    #[test]
564    #[should_panic(expected = "should have 2 separate polygons")]
565    fn outer_banana_doesnt_produce_hole() {
566        let poly =
567            wkt!(POLYGON((0.0 0.0,4.0 0.0,3.0 -2.0,5.0 -2.0,4.0 0.0,8.0 0.0,4.0 4.0,0.0 0.0)));
568
569        let result = [poly]
570            .map(|p| p.earcut_triangles())
571            .concat()
572            .stitch_triangulation()
573            .unwrap();
574
575        // Currently panics: stitch_rings_from_lines produces a self-intersecting ring.
576        // See `document_bug_in_stitch_rings_order_dependent_at_non_manifold_vertices` for an exploration.
577        assert_eq!(result.0.len(), 2, "should have 2 separate polygons");
578        result[0].check_validation().unwrap();
579
580        assert_eq!(result[0].interiors().len(), 0);
581    }
582
583    // This test documents a bug in our stitch implementation.
584    //
585    // The polygon from `outer_banana_doesnt_produce_hole` self-touches at (4,0): that vertex appears twice in the ring.
586    // Triangulating it yields 3 triangles that share only that point, not an edge, so the
587    // boundary has 4 incident edges at (4,0).
588    //
589    // We verified (by running the same input against the old `earcutr` library) that both
590    // libraries emit geometrically identical triangles. The only difference is the *order*
591    // they are emitted. That order difference causes `stitch_rings_from_lines` to produce different
592    // ring topologies.
593    #[test]
594    fn document_bug_in_stitch_rings_order_dependent_at_non_manifold_vertices() {
595        // The old earcutr crate's triangulation happend to work with our stitch trait.
596        let old_earcutr_order = vec![
597            wkt!(TRIANGLE(4.0 0.0,4.0 4.0,0.0 0.0)),   // left body
598            wkt!(TRIANGLE(4.0 0.0,3.0 -2.0,5.0 -2.0)), // notch (before right body)
599            wkt!(TRIANGLE(8.0 0.0,4.0 4.0,4.0 0.0)),   // right body
600        ];
601        let old_result = old_earcutr_order.stitch_triangulation().unwrap();
602        assert_eq!(old_result.0.len(), 2); // 2 valid polygons: main body + notch
603        old_result.check_validation().unwrap();
604
605        // The new earcut crate has a different order of triangles, but it's still valid.
606        // It should work, but some nuance of our stitch algorithm was predicated on the
607        // arbitrary ordering of the previous triangulation.
608        let new_earcut_order = vec![
609            wkt!(TRIANGLE(4.0 0.0,4.0 4.0,0.0 0.0)),   // left body
610            wkt!(TRIANGLE(4.0 0.0,8.0 0.0,4.0 4.0)),   // right body (before notch!)
611            wkt!(TRIANGLE(4.0 0.0,3.0 -2.0,5.0 -2.0)), // notch
612        ];
613        let poly =
614            wkt!(POLYGON((0.0 0.0,4.0 0.0,3.0 -2.0,5.0 -2.0,4.0 0.0,8.0 0.0,4.0 4.0,0.0 0.0)));
615        assert_eq!(poly.earcut_triangles(), new_earcut_order);
616
617        let new_result = new_earcut_order.stitch_triangulation().unwrap();
618        assert_eq!(new_result.0.len(), 1); // 1 self-intersecting polygon
619        new_result.check_validation().unwrap_err();
620    }
621}