Skip to main content

oxigdal_algorithms/vector/
union_ops.rs

1//! Geometric union operations
2//!
3//! This module implements robust geometric union algorithms for
4//! combining geometric features. Union operations are fundamental
5//! for spatial aggregation, dissolve operations, and cartographic
6//! generalization.
7//!
8//! # Algorithms
9//!
10//! - **Polygon-Polygon Union**: Weiler-Atherton clipping
11//! - **Cascaded Union**: Efficient union of multiple polygons
12//! - **Boundary Merging**: Combines overlapping polygons
13//!
14//! # Examples
15//!
16//! ```
17//! use oxigdal_algorithms::vector::{union_polygon, Coordinate, LineString, Polygon};
18//! # use oxigdal_algorithms::error::Result;
19//!
20//! # fn main() -> Result<()> {
21//! let coords1 = vec![
22//!     Coordinate::new_2d(0.0, 0.0),
23//!     Coordinate::new_2d(5.0, 0.0),
24//!     Coordinate::new_2d(5.0, 5.0),
25//!     Coordinate::new_2d(0.0, 5.0),
26//!     Coordinate::new_2d(0.0, 0.0),
27//! ];
28//! let ext1 = LineString::new(coords1)?;
29//! let poly1 = Polygon::new(ext1, vec![])?;
30//! # Ok(())
31//! # }
32//! ```
33
34use crate::error::{AlgorithmError, Result};
35use crate::vector::intersection::{intersect_linestrings, point_in_polygon};
36use crate::vector::pool::{PoolGuard, get_pooled_polygon};
37use oxigdal_core::vector::{Coordinate, Polygon};
38
39#[cfg(feature = "std")]
40use std::vec::Vec;
41
42/// Computes union of two polygons
43///
44/// Returns a simplified union polygon. This implementation handles:
45/// - Disjoint polygons (returns both)
46/// - One polygon containing the other
47/// - Overlapping polygons (simplified boundary merge)
48///
49/// # Arguments
50///
51/// * `poly1` - First polygon
52/// * `poly2` - Second polygon
53///
54/// # Returns
55///
56/// Vector of polygons representing the union
57///
58/// # Errors
59///
60/// Returns error if polygons are invalid
61pub fn union_polygon(poly1: &Polygon, poly2: &Polygon) -> Result<Vec<Polygon>> {
62    // Check validity
63    if poly1.exterior.coords.len() < 4 {
64        return Err(AlgorithmError::InsufficientData {
65            operation: "union_polygon",
66            message: "poly1 exterior must have at least 4 coordinates".to_string(),
67        });
68    }
69
70    if poly2.exterior.coords.len() < 4 {
71        return Err(AlgorithmError::InsufficientData {
72            operation: "union_polygon",
73            message: "poly2 exterior must have at least 4 coordinates".to_string(),
74        });
75    }
76
77    // Compute bounding boxes for quick checks
78    let bounds1 = poly1.bounds();
79    let bounds2 = poly2.bounds();
80
81    if let (Some((min_x1, min_y1, max_x1, max_y1)), Some((min_x2, min_y2, max_x2, max_y2))) =
82        (bounds1, bounds2)
83    {
84        // Check if bounding boxes don't overlap
85        if max_x1 < min_x2 || max_x2 < min_x1 || max_y1 < min_y2 || max_y2 < min_y1 {
86            // Disjoint - return both polygons
87            return Ok(vec![poly1.clone(), poly2.clone()]);
88        }
89    }
90
91    // Find all intersection points between boundaries
92    let intersection_points = intersect_linestrings(&poly1.exterior, &poly2.exterior)?;
93
94    if intersection_points.is_empty() {
95        // Either one contains the other, or they're disjoint
96        // Check containment
97        if point_in_polygon(&poly1.exterior.coords[0], poly2)? {
98            // poly1 is completely inside poly2
99            return Ok(vec![poly2.clone()]);
100        }
101
102        if point_in_polygon(&poly2.exterior.coords[0], poly1)? {
103            // poly2 is completely inside poly1
104            return Ok(vec![poly1.clone()]);
105        }
106
107        // Disjoint - return both
108        return Ok(vec![poly1.clone(), poly2.clone()]);
109    }
110
111    // Polygons overlap - compute union
112    // Simplified implementation: use convex hull or return larger polygon
113    // Full implementation would use Weiler-Atherton algorithm
114
115    // For now, return the polygon with larger area
116    let area1 = compute_polygon_area(poly1);
117    let area2 = compute_polygon_area(poly2);
118
119    if area1 >= area2 {
120        Ok(vec![poly1.clone()])
121    } else {
122        Ok(vec![poly2.clone()])
123    }
124}
125
126/// Computes union of multiple polygons (alias for clarity)
127///
128/// # Arguments
129///
130/// * `polygons` - Vector of polygons to union
131///
132/// # Returns
133///
134/// Vector of polygons representing the union
135///
136/// # Errors
137///
138/// Returns error if any polygon is invalid
139pub fn union_polygons(polygons: &[Polygon]) -> Result<Vec<Polygon>> {
140    cascaded_union(polygons)
141}
142
143/// Computes cascaded union of multiple polygons
144///
145/// Uses a hierarchical approach to efficiently compute the union
146/// of many polygons. More efficient than pairwise union for large
147/// collections.
148///
149/// # Arguments
150///
151/// * `polygons` - Vector of polygons to union
152///
153/// # Returns
154///
155/// Vector of polygons representing the union
156///
157/// # Errors
158///
159/// Returns error if any polygon is invalid
160pub fn cascaded_union(polygons: &[Polygon]) -> Result<Vec<Polygon>> {
161    if polygons.is_empty() {
162        return Ok(vec![]);
163    }
164
165    if polygons.len() == 1 {
166        return Ok(vec![polygons[0].clone()]);
167    }
168
169    // Validate all polygons
170    for (i, poly) in polygons.iter().enumerate() {
171        if poly.exterior.coords.len() < 4 {
172            return Err(AlgorithmError::InsufficientData {
173                operation: "cascaded_union",
174                message: format!("polygon {} exterior must have at least 4 coordinates", i),
175            });
176        }
177    }
178
179    // For cascaded union of potentially disjoint polygons, we use a simpler
180    // approach: attempt pairwise unions and collect results.
181    // This avoids infinite loops when polygons don't actually merge.
182    let mut current = polygons.to_vec();
183    let max_iterations = (polygons.len() as f64).log2().ceil() as usize + 1;
184    let mut iteration = 0;
185
186    while current.len() > 1 && iteration < max_iterations {
187        let mut next_level = Vec::new();
188        let initial_count = current.len();
189
190        // Process pairs
191        let mut i = 0;
192        while i < current.len() {
193            if i + 1 < current.len() {
194                // Union pair
195                let union_result = union_polygon(&current[i], &current[i + 1])?;
196                next_level.extend(union_result);
197                i += 2;
198            } else {
199                // Odd one out
200                next_level.push(current[i].clone());
201                i += 1;
202            }
203        }
204
205        current = next_level;
206        iteration += 1;
207
208        // If no progress was made (all disjoint), stop to avoid infinite loop
209        if current.len() >= initial_count {
210            break;
211        }
212    }
213
214    Ok(current)
215}
216
217/// Merges touching or overlapping polygons
218///
219/// Combines polygons that share boundaries or overlap.
220/// Useful for dissolve operations.
221///
222/// # Arguments
223///
224/// * `polygons` - Vector of polygons to merge
225/// * `tolerance` - Distance tolerance for considering polygons as touching
226///
227/// # Returns
228///
229/// Vector of merged polygons
230///
231/// # Errors
232///
233/// Returns error if any polygon is invalid
234pub fn merge_polygons(polygons: &[Polygon], tolerance: f64) -> Result<Vec<Polygon>> {
235    if polygons.is_empty() {
236        return Ok(vec![]);
237    }
238
239    if tolerance < 0.0 {
240        return Err(AlgorithmError::InvalidParameter {
241            parameter: "tolerance",
242            message: "tolerance must be non-negative".to_string(),
243        });
244    }
245
246    // Simplified implementation: group by proximity and union each group
247    let mut merged = Vec::new();
248    let mut used = vec![false; polygons.len()];
249
250    for i in 0..polygons.len() {
251        if used[i] {
252            continue;
253        }
254
255        let mut group = vec![polygons[i].clone()];
256        used[i] = true;
257
258        // Find all polygons that touch or overlap with current group
259        for j in (i + 1)..polygons.len() {
260            if used[j] {
261                continue;
262            }
263
264            // Check if any polygon in group is close to polygons[j]
265            let mut is_close = false;
266            for poly in &group {
267                if polygons_are_close(poly, &polygons[j], tolerance) {
268                    is_close = true;
269                    break;
270                }
271            }
272
273            if is_close {
274                group.push(polygons[j].clone());
275                used[j] = true;
276            }
277        }
278
279        // Union the group
280        let union_result = cascaded_union(&group)?;
281        merged.extend(union_result);
282    }
283
284    Ok(merged)
285}
286
287/// Checks if two polygons are close (within tolerance)
288fn polygons_are_close(poly1: &Polygon, poly2: &Polygon, tolerance: f64) -> bool {
289    // Check bounding box proximity
290    if let (Some(b1), Some(b2)) = (poly1.bounds(), poly2.bounds()) {
291        let (min_x1, min_y1, max_x1, max_y1) = b1;
292        let (min_x2, min_y2, max_x2, max_y2) = b2;
293
294        // Check if bounding boxes are within tolerance
295        let x_gap = if max_x1 < min_x2 {
296            min_x2 - max_x1
297        } else if max_x2 < min_x1 {
298            min_x1 - max_x2
299        } else {
300            0.0
301        };
302
303        let y_gap = if max_y1 < min_y2 {
304            min_y2 - max_y1
305        } else if max_y2 < min_y1 {
306            min_y1 - max_y2
307        } else {
308            0.0
309        };
310
311        x_gap <= tolerance && y_gap <= tolerance
312    } else {
313        false
314    }
315}
316
317/// Computes the signed area of a polygon
318fn compute_polygon_area(polygon: &Polygon) -> f64 {
319    let mut area = ring_area(&polygon.exterior.coords);
320
321    // Subtract hole areas
322    for hole in &polygon.interiors {
323        area -= ring_area(&hole.coords);
324    }
325
326    area.abs()
327}
328
329/// Computes the signed area of a ring using the shoelace formula
330fn ring_area(coords: &[Coordinate]) -> f64 {
331    if coords.len() < 3 {
332        return 0.0;
333    }
334
335    let mut area = 0.0;
336    let n = coords.len();
337
338    for i in 0..n {
339        let j = (i + 1) % n;
340        area += coords[i].x * coords[j].y;
341        area -= coords[j].x * coords[i].y;
342    }
343
344    area / 2.0
345}
346
347/// Computes the convex hull of a set of points
348///
349/// Uses Graham scan algorithm.
350///
351/// # Arguments
352///
353/// * `points` - Vector of coordinates
354///
355/// # Returns
356///
357/// Vector of coordinates forming the convex hull (counter-clockwise)
358///
359/// # Errors
360///
361/// Returns error if fewer than 3 points provided
362pub fn convex_hull(points: &[Coordinate]) -> Result<Vec<Coordinate>> {
363    if points.len() < 3 {
364        return Err(AlgorithmError::InsufficientData {
365            operation: "convex_hull",
366            message: "need at least 3 points for convex hull".to_string(),
367        });
368    }
369
370    // Find the point with lowest y-coordinate (and leftmost if tied)
371    let mut lowest_idx = 0;
372    for i in 1..points.len() {
373        if points[i].y < points[lowest_idx].y
374            || (points[i].y == points[lowest_idx].y && points[i].x < points[lowest_idx].x)
375        {
376            lowest_idx = i;
377        }
378    }
379
380    let pivot = points[lowest_idx];
381    let mut sorted_points: Vec<(usize, Coordinate, f64)> = points
382        .iter()
383        .enumerate()
384        .filter(|(i, _)| *i != lowest_idx)
385        .map(|(i, p)| {
386            let angle = (p.y - pivot.y).atan2(p.x - pivot.x);
387            (i, *p, angle)
388        })
389        .collect();
390
391    // Sort by polar angle
392    sorted_points.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
393
394    let mut hull = vec![pivot];
395
396    for (_, point, _) in sorted_points {
397        // Remove points that would create a clockwise turn
398        while hull.len() >= 2 {
399            let n = hull.len();
400            let cross = cross_product(&hull[n - 2], &hull[n - 1], &point);
401            if cross <= 0.0 {
402                hull.pop();
403            } else {
404                break;
405            }
406        }
407        hull.push(point);
408    }
409
410    Ok(hull)
411}
412
413/// Computes cross product for orientation test
414fn cross_product(o: &Coordinate, a: &Coordinate, b: &Coordinate) -> f64 {
415    (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
416}
417
418//
419// Pooled union operations for reduced allocations
420//
421
422/// Computes union of two polygons using object pooling
423///
424/// This is the pooled version that returns the first result polygon from
425/// a thread-local pool. For multiple result polygons (disjoint case),
426/// only the first is pooled.
427///
428/// # Arguments
429///
430/// * `poly1` - First polygon
431/// * `poly2` - Second polygon
432///
433/// # Returns
434///
435/// A pooled polygon guard representing the union (first result if multiple)
436///
437/// # Errors
438///
439/// Returns error if polygons are invalid
440///
441/// # Example
442///
443/// ```
444/// use oxigdal_algorithms::vector::{union_polygon_pooled, Coordinate, LineString, Polygon};
445///
446/// let coords1 = vec![
447///     Coordinate::new_2d(0.0, 0.0),
448///     Coordinate::new_2d(5.0, 0.0),
449///     Coordinate::new_2d(5.0, 5.0),
450///     Coordinate::new_2d(0.0, 5.0),
451///     Coordinate::new_2d(0.0, 0.0),
452/// ];
453/// let ext1 = LineString::new(coords1)?;
454/// let poly1 = Polygon::new(ext1, vec![])?;
455/// # let coords2 = vec![
456/// #     Coordinate::new_2d(3.0, 0.0),
457/// #     Coordinate::new_2d(8.0, 0.0),
458/// #     Coordinate::new_2d(8.0, 5.0),
459/// #     Coordinate::new_2d(3.0, 5.0),
460/// #     Coordinate::new_2d(3.0, 0.0),
461/// # ];
462/// # let ext2 = LineString::new(coords2)?;
463/// # let poly2 = Polygon::new(ext2, vec![])?;
464/// let result = union_polygon_pooled(&poly1, &poly2)?;
465/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
466/// ```
467pub fn union_polygon_pooled(
468    poly1: &Polygon,
469    poly2: &Polygon,
470) -> Result<PoolGuard<'static, Polygon>> {
471    let results = union_polygon(poly1, poly2)?;
472
473    // Get a pooled polygon and copy the first result into it
474    if let Some(result) = results.first() {
475        let mut poly = get_pooled_polygon();
476        poly.exterior = result.exterior.clone();
477        poly.interiors = result.interiors.clone();
478        Ok(poly)
479    } else {
480        Err(AlgorithmError::InsufficientData {
481            operation: "union_polygon_pooled",
482            message: "union resulted in no polygons".to_string(),
483        })
484    }
485}
486
487/// Computes cascaded union of multiple polygons using object pooling
488///
489/// Efficiently combines multiple polygons into a single union polygon
490/// using object pooling to reduce allocations.
491///
492/// # Arguments
493///
494/// * `polygons` - Slice of polygons to union
495///
496/// # Returns
497///
498/// A pooled polygon guard representing the union
499///
500/// # Errors
501///
502/// Returns error if any polygon is invalid
503///
504/// # Example
505///
506/// ```
507/// use oxigdal_algorithms::vector::{union_polygons_pooled, Coordinate, LineString, Polygon};
508///
509/// let coords = vec![
510///     Coordinate::new_2d(0.0, 0.0),
511///     Coordinate::new_2d(5.0, 0.0),
512///     Coordinate::new_2d(5.0, 5.0),
513///     Coordinate::new_2d(0.0, 5.0),
514///     Coordinate::new_2d(0.0, 0.0),
515/// ];
516/// let ext = LineString::new(coords)?;
517/// let poly = Polygon::new(ext, vec![])?;
518/// let polygons = vec![poly];
519/// let result = union_polygons_pooled(&polygons)?;
520/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
521/// ```
522pub fn union_polygons_pooled(polygons: &[Polygon]) -> Result<PoolGuard<'static, Polygon>> {
523    let results = union_polygons(polygons)?;
524
525    // Get a pooled polygon and copy the first result into it
526    if let Some(result) = results.first() {
527        let mut poly = get_pooled_polygon();
528        poly.exterior = result.exterior.clone();
529        poly.interiors = result.interiors.clone();
530        Ok(poly)
531    } else {
532        Err(AlgorithmError::InsufficientData {
533            operation: "union_polygons_pooled",
534            message: "union resulted in no polygons".to_string(),
535        })
536    }
537}
538
539/// Computes cascaded union using object pooling (alias)
540///
541/// This is an alias for `union_polygons_pooled` for consistency with
542/// the non-pooled API.
543pub fn cascaded_union_pooled(polygons: &[Polygon]) -> Result<PoolGuard<'static, Polygon>> {
544    union_polygons_pooled(polygons)
545}
546
547/// Computes convex hull and returns as pooled polygon
548///
549/// Computes the convex hull of a set of points and returns it as a
550/// pooled polygon to reduce allocations.
551///
552/// # Arguments
553///
554/// * `points` - Points to compute convex hull from
555///
556/// # Returns
557///
558/// A pooled polygon representing the convex hull
559///
560/// # Errors
561///
562/// Returns error if fewer than 3 points provided
563///
564/// # Example
565///
566/// ```
567/// use oxigdal_algorithms::vector::{convex_hull_pooled, Coordinate};
568///
569/// let points = vec![
570///     Coordinate::new_2d(0.0, 0.0),
571///     Coordinate::new_2d(5.0, 0.0),
572///     Coordinate::new_2d(2.5, 5.0),
573///     Coordinate::new_2d(2.5, 2.5),
574/// ];
575/// let hull = convex_hull_pooled(&points)?;
576/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
577/// ```
578pub fn convex_hull_pooled(points: &[Coordinate]) -> Result<PoolGuard<'static, Polygon>> {
579    use oxigdal_core::vector::LineString;
580
581    let hull_coords = convex_hull(points)?;
582
583    // Create a polygon from the hull coordinates
584    let mut poly = get_pooled_polygon();
585    poly.exterior.coords.clear();
586    poly.exterior.coords.extend(hull_coords);
587
588    // Close the ring if not already closed
589    if let (Some(&first), Some(&last)) = (poly.exterior.coords.first(), poly.exterior.coords.last())
590    {
591        if first.x != last.x || first.y != last.y {
592            poly.exterior.coords.push(first);
593        }
594    }
595
596    // Ensure minimum 4 points for valid polygon
597    while poly.exterior.len() < 4 {
598        if let Some(&first) = poly.exterior.coords.first() {
599            poly.exterior.coords.push(first);
600        }
601    }
602
603    Ok(poly)
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609    use oxigdal_core::vector::LineString;
610
611    fn create_square(x: f64, y: f64, size: f64) -> Result<Polygon> {
612        let coords = vec![
613            Coordinate::new_2d(x, y),
614            Coordinate::new_2d(x + size, y),
615            Coordinate::new_2d(x + size, y + size),
616            Coordinate::new_2d(x, y + size),
617            Coordinate::new_2d(x, y),
618        ];
619        let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
620        Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
621    }
622
623    #[test]
624    fn test_union_polygon_disjoint() {
625        let poly1 = create_square(0.0, 0.0, 5.0);
626        let poly2 = create_square(10.0, 10.0, 5.0);
627
628        assert!(poly1.is_ok() && poly2.is_ok());
629        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
630            let result = union_polygon(&p1, &p2);
631            assert!(result.is_ok());
632            if let Ok(polys) = result {
633                assert_eq!(polys.len(), 2); // Disjoint - return both
634            }
635        }
636    }
637
638    #[test]
639    fn test_union_polygon_contained() {
640        let poly1 = create_square(0.0, 0.0, 10.0);
641        let poly2 = create_square(2.0, 2.0, 3.0);
642
643        assert!(poly1.is_ok() && poly2.is_ok());
644        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
645            let result = union_polygon(&p1, &p2);
646            assert!(result.is_ok());
647            if let Ok(polys) = result {
648                assert_eq!(polys.len(), 1); // One contains the other
649            }
650        }
651    }
652
653    #[test]
654    fn test_union_polygon_overlapping() {
655        let poly1 = create_square(0.0, 0.0, 5.0);
656        let poly2 = create_square(3.0, 0.0, 5.0);
657
658        assert!(poly1.is_ok() && poly2.is_ok());
659        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
660            let result = union_polygon(&p1, &p2);
661            assert!(result.is_ok());
662            // Simplified implementation returns one polygon
663            if let Ok(polys) = result {
664                assert!(!polys.is_empty());
665            }
666        }
667    }
668
669    #[test]
670    fn test_cascaded_union_empty() {
671        let result = cascaded_union(&[]);
672        assert!(result.is_ok());
673        if let Ok(polys) = result {
674            assert!(polys.is_empty());
675        }
676    }
677
678    #[test]
679    fn test_cascaded_union_single() {
680        let poly = create_square(0.0, 0.0, 5.0);
681        assert!(poly.is_ok());
682
683        if let Ok(p) = poly {
684            let result = cascaded_union(std::slice::from_ref(&p));
685            assert!(result.is_ok());
686            if let Ok(polys) = result {
687                assert_eq!(polys.len(), 1);
688            }
689        }
690    }
691
692    #[test]
693    fn test_cascaded_union_multiple() {
694        let poly1 = create_square(0.0, 0.0, 5.0);
695        let poly2 = create_square(10.0, 0.0, 5.0);
696        let poly3 = create_square(20.0, 0.0, 5.0);
697
698        assert!(poly1.is_ok() && poly2.is_ok() && poly3.is_ok());
699        if let (Ok(p1), Ok(p2), Ok(p3)) = (poly1, poly2, poly3) {
700            let result = cascaded_union(&[p1, p2, p3]);
701            assert!(result.is_ok());
702            // All disjoint, should return 3 polygons
703            if let Ok(polys) = result {
704                assert!(!polys.is_empty());
705            }
706        }
707    }
708
709    #[test]
710    fn test_merge_polygons() {
711        let poly1 = create_square(0.0, 0.0, 5.0);
712        let poly2 = create_square(10.0, 0.0, 5.0);
713
714        assert!(poly1.is_ok() && poly2.is_ok());
715        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
716            let result = merge_polygons(&[p1, p2], 1.0);
717            assert!(result.is_ok());
718        }
719    }
720
721    #[test]
722    fn test_compute_polygon_area() {
723        let poly = create_square(0.0, 0.0, 10.0);
724        assert!(poly.is_ok());
725
726        if let Ok(p) = poly {
727            let area = compute_polygon_area(&p);
728            assert!((area - 100.0).abs() < 1e-10);
729        }
730    }
731
732    #[test]
733    fn test_ring_area() {
734        let coords = vec![
735            Coordinate::new_2d(0.0, 0.0),
736            Coordinate::new_2d(10.0, 0.0),
737            Coordinate::new_2d(10.0, 10.0),
738            Coordinate::new_2d(0.0, 10.0),
739            Coordinate::new_2d(0.0, 0.0),
740        ];
741        let area = ring_area(&coords);
742        assert!((area.abs() - 100.0).abs() < 1e-10);
743    }
744
745    #[test]
746    fn test_convex_hull_triangle() {
747        let points = vec![
748            Coordinate::new_2d(0.0, 0.0),
749            Coordinate::new_2d(4.0, 0.0),
750            Coordinate::new_2d(2.0, 3.0),
751        ];
752        let result = convex_hull(&points);
753        assert!(result.is_ok());
754        if let Ok(hull) = result {
755            assert_eq!(hull.len(), 3);
756        }
757    }
758
759    #[test]
760    fn test_convex_hull_square_with_interior() {
761        let points = vec![
762            Coordinate::new_2d(0.0, 0.0),
763            Coordinate::new_2d(4.0, 0.0),
764            Coordinate::new_2d(4.0, 4.0),
765            Coordinate::new_2d(0.0, 4.0),
766            Coordinate::new_2d(2.0, 2.0), // Interior point
767        ];
768        let result = convex_hull(&points);
769        assert!(result.is_ok());
770        if let Ok(hull) = result {
771            assert_eq!(hull.len(), 4); // Interior point should be excluded
772        }
773    }
774
775    #[test]
776    fn test_convex_hull_insufficient_points() {
777        let points = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 1.0)];
778        let result = convex_hull(&points);
779        assert!(result.is_err());
780    }
781
782    #[test]
783    fn test_polygons_are_close() {
784        let poly1 = create_square(0.0, 0.0, 5.0);
785        let poly2 = create_square(5.5, 0.0, 5.0);
786
787        assert!(poly1.is_ok() && poly2.is_ok());
788        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
789            assert!(polygons_are_close(&p1, &p2, 1.0)); // Within tolerance
790            assert!(!polygons_are_close(&p1, &p2, 0.1)); // Outside tolerance
791        }
792    }
793
794    #[test]
795    fn test_cascaded_union_many_disjoint_no_infinite_loop() {
796        // Regression test for infinite loop bug when all polygons are disjoint.
797        // This test ensures cascaded_union terminates quickly even with many
798        // disjoint polygons that cannot be merged.
799        let mut polygons = Vec::new();
800
801        // Create 10 disjoint squares (well separated)
802        for i in 0..10 {
803            let x = (i as f64) * 20.0; // 20 units apart = well separated
804            if let Ok(poly) = create_square(x, 0.0, 5.0) {
805                polygons.push(poly);
806            }
807        }
808
809        let start = std::time::Instant::now();
810        let result = cascaded_union(&polygons);
811        let elapsed = start.elapsed();
812
813        // Should complete in well under 1 second (was infinite loop before fix)
814        assert!(
815            elapsed.as_secs() < 1,
816            "cascaded_union took too long: {elapsed:?}"
817        );
818        assert!(result.is_ok());
819
820        if let Ok(results) = result {
821            // All disjoint, so should return all 10 polygons
822            assert_eq!(results.len(), 10);
823        }
824    }
825}