Skip to main content

oxigdal_algorithms/vector/
difference.rs

1//! Geometric difference operations
2//!
3//! This module implements robust geometric difference (subtraction) algorithms
4//! for computing the symmetric and asymmetric difference of geometric features.
5//! Difference operations are fundamental for overlay analysis, cookie-cutter
6//! operations, and spatial editing.
7//!
8//! # Operations
9//!
10//! - **Difference**: A - B (removes B from A)
11//! - **Symmetric Difference**: (A - B) ∪ (B - A)
12//! - **Multi-polygon Difference**: Handles holes properly
13//!
14//! # Interior Ring (Hole) Handling
15//!
16//! This module fully supports interior rings (holes) in polygons:
17//!
18//! - **Difference with holes**: Existing holes in input polygons are properly
19//!   clipped and preserved in the result
20//! - **Hole creation**: When the subtracted polygon is entirely contained,
21//!   it becomes a new hole
22//! - **Hole clipping**: When clipping to a bounding box, holes are properly
23//!   clipped, removed, or preserved based on their relationship to the box
24//! - **Topology validation**: Results are validated to ensure proper polygon
25//!   structure
26//!
27//! # Examples
28//!
29//! ```
30//! # use oxigdal_core::error::Result;
31//! use oxigdal_algorithms::vector::{difference_polygon, Coordinate, LineString, Polygon};
32//!
33//! # fn main() -> Result<()> {
34//! let coords1 = vec![
35//!     Coordinate::new_2d(0.0, 0.0),
36//!     Coordinate::new_2d(10.0, 0.0),
37//!     Coordinate::new_2d(10.0, 10.0),
38//!     Coordinate::new_2d(0.0, 10.0),
39//!     Coordinate::new_2d(0.0, 0.0),
40//! ];
41//! let ext1 = LineString::new(coords1)?;
42//! let poly1 = Polygon::new(ext1, vec![])?;
43//! # Ok(())
44//! # }
45//! ```
46
47use crate::error::{AlgorithmError, Result};
48use crate::vector::pool::{PoolGuard, get_pooled_polygon};
49use oxigdal_core::vector::{Coordinate, LineString, Polygon};
50
51#[cfg(feature = "std")]
52use std::vec::Vec;
53
54/// Tolerance for coordinate comparisons
55const EPSILON: f64 = 1e-10;
56
57/// Computes the difference of two polygons (poly1 - poly2)
58///
59/// Returns the portion of poly1 that does not overlap with poly2.
60/// Handles:
61/// - Disjoint polygons (returns poly1)
62/// - poly2 contains poly1 (returns empty)
63/// - poly1 contains poly2 (returns poly1 with poly2 as hole)
64/// - Partial overlap (simplified result)
65/// - Existing interior rings (holes) in both polygons
66///
67/// # Interior Ring Handling
68///
69/// - Holes in poly1: Preserved unless they are completely covered by poly2
70/// - Holes in poly2: If poly2 is inside poly1, poly2's holes become new polygon
71///   regions (since subtracting a hole means keeping that area)
72/// - New holes: Created when poly2 is entirely contained within poly1
73///
74/// # Arguments
75///
76/// * `poly1` - The polygon to subtract from
77/// * `poly2` - The polygon to subtract
78///
79/// # Returns
80///
81/// Vector of polygons representing the difference
82///
83/// # Errors
84///
85/// Returns error if polygons are invalid
86pub fn difference_polygon(poly1: &Polygon, poly2: &Polygon) -> Result<Vec<Polygon>> {
87    use oxigdal_core::OxiGdalError;
88
89    // Check validity -- preserve the detailed OxiGdalError builder for backward compat
90    if poly1.exterior.coords.len() < 4 {
91        return Err(OxiGdalError::invalid_parameter_builder(
92            "poly1",
93            format!("exterior must have at least 4 coordinates, got {}", poly1.exterior.coords.len()),
94        )
95        .with_parameter("coordinate_count", poly1.exterior.coords.len().to_string())
96        .with_parameter("min_count", "4")
97        .with_operation("difference_polygon")
98        .with_suggestion("A valid polygon requires at least 4 coordinates (first and last must be identical to close the ring)")
99        .build()
100        .into());
101    }
102
103    if poly2.exterior.coords.len() < 4 {
104        return Err(OxiGdalError::invalid_parameter_builder(
105            "poly2",
106            format!("exterior must have at least 4 coordinates, got {}", poly2.exterior.coords.len()),
107        )
108        .with_parameter("coordinate_count", poly2.exterior.coords.len().to_string())
109        .with_parameter("min_count", "4")
110        .with_operation("difference_polygon")
111        .with_suggestion("A valid polygon requires at least 4 coordinates (first and last must be identical to close the ring)")
112        .build()
113        .into());
114    }
115
116    // Delegate to the Weiler-Atherton clipping engine
117    crate::vector::clipping::clip_polygons(
118        poly1,
119        poly2,
120        crate::vector::clipping::ClipOperation::Difference,
121    )
122}
123
124/// Checks if a point is inside a ring using ray casting.
125///
126/// Kept for use by local helpers (topology validation, hole merging, etc.).
127fn point_in_ring(point: &Coordinate, ring: &[Coordinate]) -> bool {
128    crate::vector::clipping::point_in_ring(point, ring)
129}
130
131/// Computes the centroid of a ring.
132///
133/// Retained as a thin re-export for local tests and potential future helpers;
134/// `is_ring_inside_ring` no longer relies on a centroid test (it uses a robust
135/// crossing + vertex-containment check that handles concave rings).
136#[allow(dead_code)]
137fn compute_ring_centroid(coords: &[Coordinate]) -> Coordinate {
138    crate::vector::clipping::compute_ring_centroid(coords)
139}
140
141/// Computes the difference of multiple polygons
142///
143/// Subtracts all polygons in `subtract` from all polygons in `base`.
144///
145/// # Arguments
146///
147/// * `base` - Base polygons to subtract from
148/// * `subtract` - Polygons to subtract
149///
150/// # Returns
151///
152/// Vector of polygons representing the difference
153///
154/// # Errors
155///
156/// Returns error if any polygon is invalid
157pub fn difference_polygons(base: &[Polygon], subtract: &[Polygon]) -> Result<Vec<Polygon>> {
158    if base.is_empty() {
159        return Ok(vec![]);
160    }
161
162    if subtract.is_empty() {
163        return Ok(base.to_vec());
164    }
165
166    // Validate all polygons
167    for (i, poly) in base.iter().enumerate() {
168        if poly.exterior.coords.len() < 4 {
169            return Err(AlgorithmError::InsufficientData {
170                operation: "difference_polygons",
171                message: format!(
172                    "base polygon {} exterior must have at least 4 coordinates",
173                    i
174                ),
175            });
176        }
177    }
178
179    for (i, poly) in subtract.iter().enumerate() {
180        if poly.exterior.coords.len() < 4 {
181            return Err(AlgorithmError::InsufficientData {
182                operation: "difference_polygons",
183                message: format!(
184                    "subtract polygon {} exterior must have at least 4 coordinates",
185                    i
186                ),
187            });
188        }
189    }
190
191    // Sequentially subtract each polygon in subtract from the result
192    let mut result = base.to_vec();
193
194    for sub_poly in subtract {
195        let mut new_result = Vec::new();
196
197        for base_poly in &result {
198            let diff = difference_polygon(base_poly, sub_poly)?;
199            new_result.extend(diff);
200        }
201
202        result = new_result;
203    }
204
205    Ok(result)
206}
207
208/// Computes the symmetric difference of two polygons
209///
210/// Returns the area that is in either polygon but not in both.
211/// Equivalent to (poly1 - poly2) ∪ (poly2 - poly1).
212///
213/// # Arguments
214///
215/// * `poly1` - First polygon
216/// * `poly2` - Second polygon
217///
218/// # Returns
219///
220/// Vector of polygons representing the symmetric difference
221///
222/// # Errors
223///
224/// Returns error if polygons are invalid
225pub fn symmetric_difference(poly1: &Polygon, poly2: &Polygon) -> Result<Vec<Polygon>> {
226    // Compute poly1 - poly2
227    let diff1 = difference_polygon(poly1, poly2)?;
228
229    // Compute poly2 - poly1
230    let diff2 = difference_polygon(poly2, poly1)?;
231
232    // Union the results
233    let mut result = diff1;
234    result.extend(diff2);
235
236    Ok(result)
237}
238
239/// Clips a polygon to a rectangular bounding box
240///
241/// Returns the portion of the polygon that falls within the box.
242/// This is a specialized form of difference optimized for rectangular
243/// clipping regions.
244///
245/// # Interior Ring Handling
246///
247/// Interior rings (holes) are processed as follows:
248/// - **Completely inside box**: Hole is preserved unchanged
249/// - **Completely outside box**: Hole is removed
250/// - **Partially inside**: Hole is clipped using Sutherland-Hodgman algorithm
251/// - **Straddling box boundary**: Hole is clipped, potentially creating
252///   multiple result polygons if the clipping splits the polygon
253///
254/// # Arguments
255///
256/// * `polygon` - The polygon to clip
257/// * `min_x` - Minimum X coordinate of bounding box
258/// * `min_y` - Minimum Y coordinate of bounding box
259/// * `max_x` - Maximum X coordinate of bounding box
260/// * `max_y` - Maximum Y coordinate of bounding box
261///
262/// # Returns
263///
264/// Vector of clipped polygons
265///
266/// # Errors
267///
268/// Returns error if polygon is invalid or bounding box is invalid
269pub fn clip_to_box(
270    polygon: &Polygon,
271    min_x: f64,
272    min_y: f64,
273    max_x: f64,
274    max_y: f64,
275) -> Result<Vec<Polygon>> {
276    if polygon.exterior.coords.len() < 4 {
277        return Err(AlgorithmError::InsufficientData {
278            operation: "clip_to_box",
279            message: "polygon exterior must have at least 4 coordinates".to_string(),
280        });
281    }
282
283    if min_x >= max_x || min_y >= max_y {
284        return Err(AlgorithmError::InvalidParameter {
285            parameter: "bounding box",
286            message: "invalid bounding box dimensions".to_string(),
287        });
288    }
289
290    // Check if polygon is completely outside box
291    if let Some((poly_min_x, poly_min_y, poly_max_x, poly_max_y)) = polygon.bounds() {
292        if poly_max_x < min_x || poly_min_x > max_x || poly_max_y < min_y || poly_min_y > max_y {
293            // Completely outside
294            return Ok(vec![]);
295        }
296
297        if poly_min_x >= min_x && poly_max_x <= max_x && poly_min_y >= min_y && poly_max_y <= max_y
298        {
299            // Completely inside - return polygon unchanged (with all holes)
300            return Ok(vec![polygon.clone()]);
301        }
302    }
303
304    // Clip exterior ring using Sutherland-Hodgman algorithm
305    let mut clipped_coords = polygon.exterior.coords.clone();
306
307    // Clip against each edge of the box
308    clipped_coords = clip_against_edge(&clipped_coords, min_x, true, false)?; // Left
309    clipped_coords = clip_against_edge(&clipped_coords, max_x, true, true)?; // Right
310    clipped_coords = clip_against_edge(&clipped_coords, min_y, false, false)?; // Bottom
311    clipped_coords = clip_against_edge(&clipped_coords, max_y, false, true)?; // Top
312
313    if clipped_coords.len() < 4 {
314        // Clipped to nothing
315        return Ok(vec![]);
316    }
317
318    // Ensure ring is closed
319    if let (Some(first), Some(last)) = (clipped_coords.first(), clipped_coords.last()) {
320        if (first.x - last.x).abs() > f64::EPSILON || (first.y - last.y).abs() > f64::EPSILON {
321            clipped_coords.push(*first);
322        }
323    }
324
325    let clipped_exterior = LineString::new(clipped_coords).map_err(AlgorithmError::Core)?;
326
327    // Handle interior rings (holes)
328    let clipped_interiors = clip_interior_rings_to_box(
329        &polygon.interiors,
330        min_x,
331        min_y,
332        max_x,
333        max_y,
334        &clipped_exterior,
335    )?;
336
337    let result = Polygon::new(clipped_exterior, clipped_interiors).map_err(AlgorithmError::Core)?;
338
339    Ok(vec![result])
340}
341
342/// Clips interior rings (holes) to a bounding box
343///
344/// For each interior ring:
345/// 1. Check if it's completely outside the box (remove it)
346/// 2. Check if it's completely inside the box (keep it)
347/// 3. Otherwise, clip it using Sutherland-Hodgman algorithm
348///
349/// # Arguments
350///
351/// * `interiors` - The interior rings to clip
352/// * `min_x` - Minimum X coordinate of bounding box
353/// * `min_y` - Minimum Y coordinate of bounding box
354/// * `max_x` - Maximum X coordinate of bounding box
355/// * `max_y` - Maximum Y coordinate of bounding box
356/// * `clipped_exterior` - The clipped exterior ring (for validation)
357///
358/// # Returns
359///
360/// Vector of clipped interior rings
361fn clip_interior_rings_to_box(
362    interiors: &[LineString],
363    min_x: f64,
364    min_y: f64,
365    max_x: f64,
366    max_y: f64,
367    clipped_exterior: &LineString,
368) -> Result<Vec<LineString>> {
369    let mut result = Vec::new();
370
371    for hole in interiors {
372        if hole.coords.len() < 4 {
373            continue;
374        }
375
376        // Check if hole is completely outside or inside the box
377        let hole_bounds = hole.bounds();
378        if let Some((hole_min_x, hole_min_y, hole_max_x, hole_max_y)) = hole_bounds {
379            // Check if hole is completely outside box
380            if hole_max_x < min_x || hole_min_x > max_x || hole_max_y < min_y || hole_min_y > max_y
381            {
382                // Hole is completely outside box - remove it
383                continue;
384            }
385
386            // Check if hole is completely inside box
387            if hole_min_x >= min_x
388                && hole_max_x <= max_x
389                && hole_min_y >= min_y
390                && hole_max_y <= max_y
391            {
392                // Hole is completely inside box - check if it's inside the clipped exterior
393                if is_ring_inside_ring(&hole.coords, &clipped_exterior.coords) {
394                    result.push(hole.clone());
395                }
396                continue;
397            }
398        }
399
400        // Hole straddles the box boundary - clip it
401        let clipped_hole = clip_ring_to_box(&hole.coords, min_x, min_y, max_x, max_y)?;
402
403        if clipped_hole.len() >= 4 {
404            // Check if the clipped hole is inside the clipped exterior
405            if is_ring_inside_ring(&clipped_hole, &clipped_exterior.coords) {
406                // Create a valid closed ring
407                let mut closed_hole = clipped_hole;
408                if let (Some(first), Some(last)) = (closed_hole.first(), closed_hole.last()) {
409                    if (first.x - last.x).abs() > EPSILON || (first.y - last.y).abs() > EPSILON {
410                        closed_hole.push(*first);
411                    }
412                }
413
414                if closed_hole.len() >= 4 {
415                    if let Ok(hole_ring) = LineString::new(closed_hole) {
416                        result.push(hole_ring);
417                    }
418                }
419            }
420        }
421    }
422
423    Ok(result)
424}
425
426/// Clips a ring to a bounding box using Sutherland-Hodgman algorithm
427///
428/// # Arguments
429///
430/// * `coords` - The ring coordinates to clip
431/// * `min_x` - Minimum X coordinate of bounding box
432/// * `min_y` - Minimum Y coordinate of bounding box
433/// * `max_x` - Maximum X coordinate of bounding box
434/// * `max_y` - Maximum Y coordinate of bounding box
435///
436/// # Returns
437///
438/// Vector of clipped coordinates
439fn clip_ring_to_box(
440    coords: &[Coordinate],
441    min_x: f64,
442    min_y: f64,
443    max_x: f64,
444    max_y: f64,
445) -> Result<Vec<Coordinate>> {
446    let mut clipped = coords.to_vec();
447
448    // Clip against each edge of the box
449    clipped = clip_against_edge(&clipped, min_x, true, false)?; // Left
450    clipped = clip_against_edge(&clipped, max_x, true, true)?; // Right
451    clipped = clip_against_edge(&clipped, min_y, false, false)?; // Bottom
452    clipped = clip_against_edge(&clipped, max_y, false, true)?; // Top
453
454    Ok(clipped)
455}
456
457/// Checks if a ring is completely inside another ring.
458///
459/// Robust for concave rings: a single-point (centroid) test is unreliable
460/// because a concave ring's centroid can fall outside the ring itself, or in a
461/// notch of a concave `outer`, misclassifying containment. Instead this checks
462/// that (1) no edge of `inner` *properly* crosses an edge of `outer`, and
463/// (2) at least one vertex of `inner` lies strictly inside `outer`. Together
464/// these imply every vertex of `inner` is inside-or-on `outer`, so `inner` is
465/// contained. Shared/touching boundary segments (common after clipping to a
466/// box) are not proper crossings, so boundary-touching holes are preserved.
467///
468/// # Arguments
469///
470/// * `inner` - The potentially inner ring
471/// * `outer` - The potentially outer ring
472///
473/// # Returns
474///
475/// true if inner is inside outer
476fn is_ring_inside_ring(inner: &[Coordinate], outer: &[Coordinate]) -> bool {
477    if inner.len() < 3 || outer.len() < 3 {
478        return false;
479    }
480
481    // (1) Reject any proper interior crossing between the two boundaries.
482    // `segments_intersect` excludes shared endpoints and collinear overlaps, so
483    // rings that merely touch along a shared edge are not rejected here.
484    for i in 0..(inner.len() - 1) {
485        for j in 0..(outer.len() - 1) {
486            if segments_intersect(&inner[i], &inner[i + 1], &outer[j], &outer[j + 1]) {
487                return false;
488            }
489        }
490    }
491
492    // (2) Require at least one vertex of `inner` strictly inside `outer`.
493    // With no proper crossings, this rules out the "disjoint" and "outer inside
494    // inner" cases (whose inner vertices are all outside `outer`).
495    inner.iter().any(|v| point_in_ring(v, outer))
496}
497
498/// Validates polygon topology after clipping operations
499///
500/// Ensures that:
501/// 1. Exterior ring has at least 4 coordinates and is closed
502/// 2. All interior rings have at least 4 coordinates and are closed
503/// 3. All interior rings are inside the exterior ring
504/// 4. No interior rings overlap with each other
505///
506/// # Arguments
507///
508/// * `polygon` - The polygon to validate
509///
510/// # Returns
511///
512/// Ok(true) if valid, Ok(false) if topology issues detected
513///
514/// # Errors
515///
516/// Returns error if validation fails due to invalid data
517pub fn validate_polygon_topology(polygon: &Polygon) -> Result<bool> {
518    // Check exterior ring
519    if polygon.exterior.coords.len() < 4 {
520        return Ok(false);
521    }
522
523    // Check exterior ring is closed
524    if let (Some(first), Some(last)) = (
525        polygon.exterior.coords.first(),
526        polygon.exterior.coords.last(),
527    ) {
528        if (first.x - last.x).abs() > EPSILON || (first.y - last.y).abs() > EPSILON {
529            return Ok(false);
530        }
531    }
532
533    // Check each interior ring
534    for hole in &polygon.interiors {
535        // Check minimum size
536        if hole.coords.len() < 4 {
537            return Ok(false);
538        }
539
540        // Check ring is closed
541        if let (Some(first), Some(last)) = (hole.coords.first(), hole.coords.last()) {
542            if (first.x - last.x).abs() > EPSILON || (first.y - last.y).abs() > EPSILON {
543                return Ok(false);
544            }
545        }
546
547        // Check hole is inside exterior
548        if !is_ring_inside_ring(&hole.coords, &polygon.exterior.coords) {
549            return Ok(false);
550        }
551    }
552
553    // Check no interior rings overlap
554    for i in 0..polygon.interiors.len() {
555        for j in (i + 1)..polygon.interiors.len() {
556            if rings_overlap(&polygon.interiors[i].coords, &polygon.interiors[j].coords)? {
557                return Ok(false);
558            }
559        }
560    }
561
562    Ok(true)
563}
564
565/// Checks if two rings overlap
566///
567/// Two rings overlap if any vertex of one ring is inside the other,
568/// or if their boundaries intersect.
569fn rings_overlap(ring1: &[Coordinate], ring2: &[Coordinate]) -> Result<bool> {
570    // Quick check: if any vertex of ring1 is inside ring2 (or vice versa)
571    for coord in ring1 {
572        if point_in_ring(coord, ring2) {
573            return Ok(true);
574        }
575    }
576
577    for coord in ring2 {
578        if point_in_ring(coord, ring1) {
579            return Ok(true);
580        }
581    }
582
583    // Check for boundary intersections
584    // For simplicity, we use a basic segment-segment intersection check
585    if ring1.len() < 2 || ring2.len() < 2 {
586        return Ok(false);
587    }
588
589    for i in 0..(ring1.len() - 1) {
590        for j in 0..(ring2.len() - 1) {
591            if segments_intersect(&ring1[i], &ring1[i + 1], &ring2[j], &ring2[j + 1]) {
592                return Ok(true);
593            }
594        }
595    }
596
597    Ok(false)
598}
599
600/// Checks if two line segments intersect (excluding endpoints)
601fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
602    let d1x = p2.x - p1.x;
603    let d1y = p2.y - p1.y;
604    let d2x = p4.x - p3.x;
605    let d2y = p4.y - p3.y;
606
607    let cross = d1x * d2y - d1y * d2x;
608
609    if cross.abs() < EPSILON {
610        // Parallel or collinear
611        return false;
612    }
613
614    let dx = p3.x - p1.x;
615    let dy = p3.y - p1.y;
616
617    let t = (dx * d2y - dy * d2x) / cross;
618    let u = (dx * d1y - dy * d1x) / cross;
619
620    // Exclude endpoints (strict interior intersection)
621    t > EPSILON && t < (1.0 - EPSILON) && u > EPSILON && u < (1.0 - EPSILON)
622}
623
624/// Merges adjacent holes if they share a common boundary
625///
626/// This function identifies holes that touch or overlap and merges them
627/// into a single hole to maintain valid polygon topology.
628///
629/// # Arguments
630///
631/// * `holes` - Vector of interior rings to potentially merge
632///
633/// # Returns
634///
635/// Vector of merged interior rings
636pub fn merge_adjacent_holes(holes: &[LineString]) -> Result<Vec<LineString>> {
637    if holes.is_empty() {
638        return Ok(vec![]);
639    }
640
641    if holes.len() == 1 {
642        return Ok(holes.to_vec());
643    }
644
645    // Simple approach: keep non-overlapping holes
646    // A more sophisticated approach would use union operations
647    let mut result = Vec::new();
648    let mut merged = vec![false; holes.len()];
649
650    for i in 0..holes.len() {
651        if merged[i] {
652            continue;
653        }
654
655        let mut current_hole = holes[i].coords.clone();
656
657        for j in (i + 1)..holes.len() {
658            if merged[j] {
659                continue;
660            }
661
662            if rings_overlap(&current_hole, &holes[j].coords)? {
663                // Merge by taking the convex hull of both holes
664                // Simplified: just take the larger hole
665                if compute_ring_area(&holes[j].coords).abs()
666                    > compute_ring_area(&current_hole).abs()
667                {
668                    current_hole = holes[j].coords.clone();
669                }
670                merged[j] = true;
671            }
672        }
673
674        if current_hole.len() >= 4 {
675            if let Ok(ring) = LineString::new(current_hole) {
676                result.push(ring);
677            }
678        }
679    }
680
681    Ok(result)
682}
683
684/// Clips a polygon against a single edge using Sutherland-Hodgman algorithm
685fn clip_against_edge(
686    coords: &[Coordinate],
687    edge_value: f64,
688    is_vertical: bool,
689    is_max: bool,
690) -> Result<Vec<Coordinate>> {
691    if coords.is_empty() {
692        return Ok(vec![]);
693    }
694
695    let mut result = Vec::new();
696
697    for i in 0..coords.len() {
698        let current = &coords[i];
699        let next = &coords[(i + 1) % coords.len()];
700
701        let current_inside = is_inside(current, edge_value, is_vertical, is_max);
702        let next_inside = is_inside(next, edge_value, is_vertical, is_max);
703
704        if current_inside {
705            result.push(*current);
706        }
707
708        if current_inside != next_inside {
709            // Edge crosses the clip line - compute intersection
710            if let Some(intersection) = compute_intersection(current, next, edge_value, is_vertical)
711            {
712                result.push(intersection);
713            }
714        }
715    }
716
717    Ok(result)
718}
719
720/// Checks if a point is inside relative to a clipping edge
721fn is_inside(point: &Coordinate, edge_value: f64, is_vertical: bool, is_max: bool) -> bool {
722    let value = if is_vertical { point.x } else { point.y };
723
724    if is_max {
725        value <= edge_value
726    } else {
727        value >= edge_value
728    }
729}
730
731/// Computes intersection of a line segment with a clipping edge
732fn compute_intersection(
733    p1: &Coordinate,
734    p2: &Coordinate,
735    edge_value: f64,
736    is_vertical: bool,
737) -> Option<Coordinate> {
738    if is_vertical {
739        // Vertical edge (x = edge_value)
740        let dx = p2.x - p1.x;
741        if dx.abs() < f64::EPSILON {
742            return None; // Parallel to edge
743        }
744
745        let t = (edge_value - p1.x) / dx;
746        if (0.0..=1.0).contains(&t) {
747            let y = p1.y + t * (p2.y - p1.y);
748            Some(Coordinate::new_2d(edge_value, y))
749        } else {
750            None
751        }
752    } else {
753        // Horizontal edge (y = edge_value)
754        let dy = p2.y - p1.y;
755        if dy.abs() < f64::EPSILON {
756            return None; // Parallel to edge
757        }
758
759        let t = (edge_value - p1.y) / dy;
760        if (0.0..=1.0).contains(&t) {
761            let x = p1.x + t * (p2.x - p1.x);
762            Some(Coordinate::new_2d(x, edge_value))
763        } else {
764            None
765        }
766    }
767}
768
769/// Erases small holes from a polygon
770///
771/// Removes interior rings (holes) smaller than a threshold area.
772/// Useful for cleaning up polygon topology.
773///
774/// # Arguments
775///
776/// * `polygon` - The polygon to clean
777/// * `min_area` - Minimum area for holes to keep
778///
779/// # Returns
780///
781/// Cleaned polygon
782///
783/// # Errors
784///
785/// Returns error if polygon is invalid
786pub fn erase_small_holes(polygon: &Polygon, min_area: f64) -> Result<Polygon> {
787    if polygon.exterior.coords.len() < 4 {
788        return Err(AlgorithmError::InsufficientData {
789            operation: "erase_small_holes",
790            message: "polygon exterior must have at least 4 coordinates".to_string(),
791        });
792    }
793
794    if min_area < 0.0 {
795        return Err(AlgorithmError::InvalidParameter {
796            parameter: "min_area",
797            message: "min_area must be non-negative".to_string(),
798        });
799    }
800
801    let mut kept_holes = Vec::new();
802
803    for hole in &polygon.interiors {
804        let area = compute_ring_area(&hole.coords).abs();
805        if area >= min_area {
806            kept_holes.push(hole.clone());
807        }
808    }
809
810    Polygon::new(polygon.exterior.clone(), kept_holes).map_err(AlgorithmError::Core)
811}
812
813/// Computes the signed area of a ring using shoelace formula
814fn compute_ring_area(coords: &[Coordinate]) -> f64 {
815    if coords.len() < 3 {
816        return 0.0;
817    }
818
819    let mut area = 0.0;
820    let n = coords.len();
821
822    for i in 0..n {
823        let j = (i + 1) % n;
824        area += coords[i].x * coords[j].y;
825        area -= coords[j].x * coords[i].y;
826    }
827
828    area / 2.0
829}
830
831//
832// Pooled difference operations for reduced allocations
833//
834
835/// Computes difference of two polygons using object pooling
836///
837/// This is the pooled version of `difference_polygon` that reuses allocated
838/// polygons from a thread-local pool. Returns the first result polygon.
839///
840/// # Arguments
841///
842/// * `subject` - Polygon to subtract from
843/// * `clip` - Polygon to subtract
844///
845/// # Returns
846///
847/// A pooled polygon guard representing the difference (first result if multiple)
848///
849/// # Errors
850///
851/// Returns error if polygons are invalid
852///
853/// # Example
854///
855/// ```
856/// use oxigdal_algorithms::vector::{difference_polygon_pooled, Coordinate, LineString, Polygon};
857///
858/// let coords1 = vec![
859///     Coordinate::new_2d(0.0, 0.0),
860///     Coordinate::new_2d(10.0, 0.0),
861///     Coordinate::new_2d(10.0, 10.0),
862///     Coordinate::new_2d(0.0, 10.0),
863///     Coordinate::new_2d(0.0, 0.0),
864/// ];
865/// let ext1 = LineString::new(coords1)?;
866/// let poly1 = Polygon::new(ext1, vec![])?;
867/// # let coords2 = vec![
868/// #     Coordinate::new_2d(5.0, 5.0),
869/// #     Coordinate::new_2d(15.0, 5.0),
870/// #     Coordinate::new_2d(15.0, 15.0),
871/// #     Coordinate::new_2d(5.0, 15.0),
872/// #     Coordinate::new_2d(5.0, 5.0),
873/// # ];
874/// # let ext2 = LineString::new(coords2)?;
875/// # let poly2 = Polygon::new(ext2, vec![])?;
876/// let result = difference_polygon_pooled(&poly1, &poly2)?;
877/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
878/// ```
879pub fn difference_polygon_pooled(
880    subject: &Polygon,
881    clip: &Polygon,
882) -> Result<PoolGuard<'static, Polygon>> {
883    let results = difference_polygon(subject, clip)?;
884
885    // Get a pooled polygon and copy the first result into it
886    if let Some(result) = results.first() {
887        let mut poly = get_pooled_polygon();
888        poly.exterior = result.exterior.clone();
889        poly.interiors = result.interiors.clone();
890        Ok(poly)
891    } else {
892        Err(AlgorithmError::InsufficientData {
893            operation: "difference_polygon_pooled",
894            message: "difference resulted in no polygons".to_string(),
895        })
896    }
897}
898
899/// Computes difference of base polygons with subtract polygons using object pooling
900///
901/// Subtracts subtract polygons from base polygons.
902/// Returns the first result polygon from the pool.
903///
904/// # Arguments
905///
906/// * `base` - Base polygons
907/// * `subtract` - Polygons to subtract
908///
909/// # Returns
910///
911/// A pooled polygon guard representing the difference
912///
913/// # Errors
914///
915/// Returns error if any polygon is invalid
916///
917/// # Example
918///
919/// ```
920/// use oxigdal_algorithms::vector::{difference_polygons_pooled, Coordinate, LineString, Polygon};
921///
922/// let coords = vec![
923///     Coordinate::new_2d(0.0, 0.0),
924///     Coordinate::new_2d(10.0, 0.0),
925///     Coordinate::new_2d(10.0, 10.0),
926///     Coordinate::new_2d(0.0, 10.0),
927///     Coordinate::new_2d(0.0, 0.0),
928/// ];
929/// let ext = LineString::new(coords)?;
930/// let base_poly = Polygon::new(ext, vec![])?;
931/// let base = vec![base_poly];
932/// let subtract: Vec<Polygon> = vec![]; // Empty for example
933/// let result = difference_polygons_pooled(&base, &subtract)?;
934/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
935/// ```
936pub fn difference_polygons_pooled(
937    base: &[Polygon],
938    subtract: &[Polygon],
939) -> Result<PoolGuard<'static, Polygon>> {
940    let results = difference_polygons(base, subtract)?;
941
942    // Get a pooled polygon and copy the first result into it
943    if let Some(result) = results.first() {
944        let mut poly = get_pooled_polygon();
945        poly.exterior = result.exterior.clone();
946        poly.interiors = result.interiors.clone();
947        Ok(poly)
948    } else {
949        Err(AlgorithmError::InsufficientData {
950            operation: "difference_polygons_pooled",
951            message: "difference resulted in no polygons".to_string(),
952        })
953    }
954}
955
956/// Computes symmetric difference using object pooling
957///
958/// Returns pooled polygon guards for both result polygons. For symmetric
959/// difference (A-B) ∪ (B-A), this returns up to 2 pooled polygons.
960///
961/// # Arguments
962///
963/// * `poly1` - First polygon
964/// * `poly2` - Second polygon
965///
966/// # Returns
967///
968/// Vector of pooled polygon guards representing the symmetric difference
969///
970/// # Errors
971///
972/// Returns error if polygons are invalid
973///
974/// # Example
975///
976/// ```
977/// use oxigdal_algorithms::vector::{symmetric_difference_pooled, Coordinate, LineString, Polygon};
978///
979/// let coords1 = vec![
980///     Coordinate::new_2d(0.0, 0.0),
981///     Coordinate::new_2d(10.0, 0.0),
982///     Coordinate::new_2d(10.0, 10.0),
983///     Coordinate::new_2d(0.0, 10.0),
984///     Coordinate::new_2d(0.0, 0.0),
985/// ];
986/// let ext1 = LineString::new(coords1)?;
987/// let poly1 = Polygon::new(ext1, vec![])?;
988/// # let coords2 = vec![
989/// #     Coordinate::new_2d(5.0, 5.0),
990/// #     Coordinate::new_2d(15.0, 5.0),
991/// #     Coordinate::new_2d(15.0, 15.0),
992/// #     Coordinate::new_2d(5.0, 15.0),
993/// #     Coordinate::new_2d(5.0, 5.0),
994/// # ];
995/// # let ext2 = LineString::new(coords2)?;
996/// # let poly2 = Polygon::new(ext2, vec![])?;
997/// let results = symmetric_difference_pooled(&poly1, &poly2)?;
998/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
999/// ```
1000pub fn symmetric_difference_pooled(
1001    poly1: &Polygon,
1002    poly2: &Polygon,
1003) -> Result<Vec<PoolGuard<'static, Polygon>>> {
1004    let results = symmetric_difference(poly1, poly2)?;
1005
1006    // Convert results to pooled polygons
1007    let mut pooled_results = Vec::new();
1008    for result in results {
1009        let mut poly = get_pooled_polygon();
1010        poly.exterior = result.exterior;
1011        poly.interiors = result.interiors;
1012        pooled_results.push(poly);
1013    }
1014
1015    Ok(pooled_results)
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021
1022    fn create_square(x: f64, y: f64, size: f64) -> Result<Polygon> {
1023        let coords = vec![
1024            Coordinate::new_2d(x, y),
1025            Coordinate::new_2d(x + size, y),
1026            Coordinate::new_2d(x + size, y + size),
1027            Coordinate::new_2d(x, y + size),
1028            Coordinate::new_2d(x, y),
1029        ];
1030        let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
1031        Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
1032    }
1033
1034    #[test]
1035    fn test_difference_polygon_disjoint() {
1036        let poly1 = create_square(0.0, 0.0, 5.0);
1037        let poly2 = create_square(10.0, 10.0, 5.0);
1038
1039        assert!(poly1.is_ok() && poly2.is_ok());
1040        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1041            let result = difference_polygon(&p1, &p2);
1042            assert!(result.is_ok());
1043            if let Ok(polys) = result {
1044                assert_eq!(polys.len(), 1); // poly1 unchanged
1045            }
1046        }
1047    }
1048
1049    #[test]
1050    fn test_difference_polygon_contained() {
1051        let poly1 = create_square(0.0, 0.0, 10.0);
1052        let poly2 = create_square(2.0, 2.0, 3.0);
1053
1054        assert!(poly1.is_ok() && poly2.is_ok());
1055        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1056            let result = difference_polygon(&p1, &p2);
1057            assert!(result.is_ok());
1058            if let Ok(polys) = result {
1059                // poly2 should become a hole in poly1
1060                assert_eq!(polys.len(), 1);
1061                assert_eq!(polys[0].interiors.len(), 1);
1062            }
1063        }
1064    }
1065
1066    #[test]
1067    fn test_difference_polygon_completely_subtracted() {
1068        let poly1 = create_square(2.0, 2.0, 3.0);
1069        let poly2 = create_square(0.0, 0.0, 10.0);
1070
1071        assert!(poly1.is_ok() && poly2.is_ok());
1072        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1073            let result = difference_polygon(&p1, &p2);
1074            assert!(result.is_ok());
1075            if let Ok(polys) = result {
1076                // poly1 completely inside poly2 - result is empty
1077                assert_eq!(polys.len(), 0);
1078            }
1079        }
1080    }
1081
1082    #[test]
1083    fn test_difference_polygons_multiple() {
1084        let base = vec![create_square(0.0, 0.0, 10.0).ok()];
1085        let subtract = vec![
1086            create_square(2.0, 2.0, 2.0).ok(),
1087            create_square(6.0, 6.0, 2.0).ok(),
1088        ];
1089
1090        let base_polys: Vec<_> = base.into_iter().flatten().collect();
1091        let subtract_polys: Vec<_> = subtract.into_iter().flatten().collect();
1092
1093        let result = difference_polygons(&base_polys, &subtract_polys);
1094        assert!(result.is_ok());
1095    }
1096
1097    #[test]
1098    fn test_symmetric_difference() {
1099        let poly1 = create_square(0.0, 0.0, 5.0);
1100        let poly2 = create_square(3.0, 0.0, 5.0);
1101
1102        assert!(poly1.is_ok() && poly2.is_ok());
1103        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1104            let result = symmetric_difference(&p1, &p2);
1105            assert!(result.is_ok());
1106            // Should return non-overlapping parts
1107            if let Ok(polys) = result {
1108                assert!(!polys.is_empty());
1109            }
1110        }
1111    }
1112
1113    #[test]
1114    fn test_clip_to_box_inside() {
1115        let poly = create_square(2.0, 2.0, 3.0);
1116        assert!(poly.is_ok());
1117
1118        if let Ok(p) = poly {
1119            let result = clip_to_box(&p, 0.0, 0.0, 10.0, 10.0);
1120            assert!(result.is_ok());
1121            if let Ok(polys) = result {
1122                // Completely inside - return unchanged
1123                assert_eq!(polys.len(), 1);
1124            }
1125        }
1126    }
1127
1128    #[test]
1129    fn test_clip_to_box_outside() {
1130        let poly = create_square(15.0, 15.0, 3.0);
1131        assert!(poly.is_ok());
1132
1133        if let Ok(p) = poly {
1134            let result = clip_to_box(&p, 0.0, 0.0, 10.0, 10.0);
1135            assert!(result.is_ok());
1136            if let Ok(polys) = result {
1137                // Completely outside - return empty
1138                assert_eq!(polys.len(), 0);
1139            }
1140        }
1141    }
1142
1143    #[test]
1144    fn test_clip_to_box_partial() {
1145        let poly = create_square(5.0, 5.0, 10.0);
1146        assert!(poly.is_ok());
1147
1148        if let Ok(p) = poly {
1149            let result = clip_to_box(&p, 0.0, 0.0, 10.0, 10.0);
1150            assert!(result.is_ok());
1151            if let Ok(polys) = result {
1152                // Partially overlapping - should be clipped
1153                assert_eq!(polys.len(), 1);
1154            }
1155        }
1156    }
1157
1158    #[test]
1159    fn test_erase_small_holes() {
1160        let exterior_coords = vec![
1161            Coordinate::new_2d(0.0, 0.0),
1162            Coordinate::new_2d(20.0, 0.0),
1163            Coordinate::new_2d(20.0, 20.0),
1164            Coordinate::new_2d(0.0, 20.0),
1165            Coordinate::new_2d(0.0, 0.0),
1166        ];
1167
1168        let small_hole_coords = vec![
1169            Coordinate::new_2d(2.0, 2.0),
1170            Coordinate::new_2d(3.0, 2.0),
1171            Coordinate::new_2d(3.0, 3.0),
1172            Coordinate::new_2d(2.0, 3.0),
1173            Coordinate::new_2d(2.0, 2.0),
1174        ];
1175
1176        let large_hole_coords = vec![
1177            Coordinate::new_2d(10.0, 10.0),
1178            Coordinate::new_2d(15.0, 10.0),
1179            Coordinate::new_2d(15.0, 15.0),
1180            Coordinate::new_2d(10.0, 15.0),
1181            Coordinate::new_2d(10.0, 10.0),
1182        ];
1183
1184        let exterior = LineString::new(exterior_coords);
1185        let small_hole = LineString::new(small_hole_coords);
1186        let large_hole = LineString::new(large_hole_coords);
1187
1188        assert!(exterior.is_ok() && small_hole.is_ok() && large_hole.is_ok());
1189
1190        if let (Ok(ext), Ok(sh), Ok(lh)) = (exterior, small_hole, large_hole) {
1191            let polygon = Polygon::new(ext, vec![sh, lh]);
1192            assert!(polygon.is_ok());
1193
1194            if let Ok(poly) = polygon {
1195                let result = erase_small_holes(&poly, 10.0);
1196                assert!(result.is_ok());
1197                if let Ok(cleaned) = result {
1198                    // Small hole should be removed, large hole kept
1199                    assert_eq!(cleaned.interiors.len(), 1);
1200                }
1201            }
1202        }
1203    }
1204
1205    // Helper function to create a square polygon with a hole
1206    fn create_square_with_hole(
1207        x: f64,
1208        y: f64,
1209        size: f64,
1210        hole_x: f64,
1211        hole_y: f64,
1212        hole_size: f64,
1213    ) -> Result<Polygon> {
1214        let exterior_coords = vec![
1215            Coordinate::new_2d(x, y),
1216            Coordinate::new_2d(x + size, y),
1217            Coordinate::new_2d(x + size, y + size),
1218            Coordinate::new_2d(x, y + size),
1219            Coordinate::new_2d(x, y),
1220        ];
1221        let hole_coords = vec![
1222            Coordinate::new_2d(hole_x, hole_y),
1223            Coordinate::new_2d(hole_x + hole_size, hole_y),
1224            Coordinate::new_2d(hole_x + hole_size, hole_y + hole_size),
1225            Coordinate::new_2d(hole_x, hole_y + hole_size),
1226            Coordinate::new_2d(hole_x, hole_y),
1227        ];
1228
1229        let exterior = LineString::new(exterior_coords).map_err(AlgorithmError::Core)?;
1230        let hole = LineString::new(hole_coords).map_err(AlgorithmError::Core)?;
1231
1232        Polygon::new(exterior, vec![hole]).map_err(AlgorithmError::Core)
1233    }
1234
1235    // ========== Tests for Interior Ring (Hole) Handling ==========
1236
1237    #[test]
1238    fn test_difference_poly1_with_hole_disjoint() {
1239        // Polygon with hole, subtracting a disjoint polygon
1240        let poly1 = create_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0);
1241        let poly2 = create_square(30.0, 30.0, 5.0);
1242
1243        assert!(poly1.is_ok() && poly2.is_ok());
1244        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1245            let result = difference_polygon(&p1, &p2);
1246            assert!(result.is_ok());
1247            if let Ok(polys) = result {
1248                // poly1 should be unchanged with its hole
1249                assert_eq!(polys.len(), 1);
1250                assert_eq!(polys[0].interiors.len(), 1);
1251            }
1252        }
1253    }
1254
1255    #[test]
1256    fn test_difference_poly1_with_hole_contained_subtract() {
1257        // Polygon with hole, subtracting a polygon that's inside but not in the hole
1258        let poly1 = create_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0);
1259        let poly2 = create_square(12.0, 12.0, 3.0);
1260
1261        assert!(poly1.is_ok() && poly2.is_ok());
1262        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1263            let result = difference_polygon(&p1, &p2);
1264            assert!(result.is_ok());
1265            if let Ok(polys) = result {
1266                // poly1 should have 2 holes now (original + new one from poly2)
1267                assert_eq!(polys.len(), 1);
1268                assert_eq!(polys[0].interiors.len(), 2);
1269            }
1270        }
1271    }
1272
1273    #[test]
1274    fn test_difference_subtract_poly_with_hole() {
1275        // Subtracting a polygon that has a hole creates new polygon region
1276        let poly1 = create_square(0.0, 0.0, 20.0);
1277        let poly2 = create_square_with_hole(2.0, 2.0, 16.0, 6.0, 6.0, 4.0);
1278
1279        assert!(poly1.is_ok() && poly2.is_ok());
1280        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1281            let result = difference_polygon(&p1, &p2);
1282            assert!(result.is_ok());
1283            if let Ok(polys) = result {
1284                // Should have at least 2 polygons:
1285                // 1. The outer part (poly1 - poly2)
1286                // 2. The hole area of poly2 (which becomes a filled region)
1287                assert!(!polys.is_empty());
1288            }
1289        }
1290    }
1291
1292    #[test]
1293    fn test_difference_poly1_inside_hole_of_poly2() {
1294        // poly1 is inside a hole of poly2 - should return poly1 unchanged
1295        let exterior_coords = vec![
1296            Coordinate::new_2d(0.0, 0.0),
1297            Coordinate::new_2d(30.0, 0.0),
1298            Coordinate::new_2d(30.0, 30.0),
1299            Coordinate::new_2d(0.0, 30.0),
1300            Coordinate::new_2d(0.0, 0.0),
1301        ];
1302        let hole_coords = vec![
1303            Coordinate::new_2d(5.0, 5.0),
1304            Coordinate::new_2d(25.0, 5.0),
1305            Coordinate::new_2d(25.0, 25.0),
1306            Coordinate::new_2d(5.0, 25.0),
1307            Coordinate::new_2d(5.0, 5.0),
1308        ];
1309
1310        let exterior = LineString::new(exterior_coords);
1311        let hole = LineString::new(hole_coords);
1312
1313        assert!(exterior.is_ok() && hole.is_ok());
1314        if let (Ok(ext), Ok(h)) = (exterior, hole) {
1315            let poly2 = Polygon::new(ext, vec![h]);
1316            let poly1 = create_square(10.0, 10.0, 5.0);
1317
1318            assert!(poly1.is_ok() && poly2.is_ok());
1319            if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1320                let result = difference_polygon(&p1, &p2);
1321                assert!(result.is_ok());
1322                if let Ok(polys) = result {
1323                    // poly1 is inside hole of poly2, so not affected
1324                    assert_eq!(polys.len(), 1);
1325                }
1326            }
1327        }
1328    }
1329
1330    #[test]
1331    fn test_clip_to_box_with_hole_inside() {
1332        // Polygon with hole, where hole is completely inside the clipping box
1333        let poly = create_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0);
1334        assert!(poly.is_ok());
1335
1336        if let Ok(p) = poly {
1337            let result = clip_to_box(&p, 0.0, 0.0, 25.0, 25.0);
1338            assert!(result.is_ok());
1339            if let Ok(polys) = result {
1340                // Polygon and hole should be preserved
1341                assert_eq!(polys.len(), 1);
1342                assert_eq!(polys[0].interiors.len(), 1);
1343            }
1344        }
1345    }
1346
1347    #[test]
1348    fn test_clip_to_box_hole_outside() {
1349        // Polygon with hole, where hole is outside the clipping box
1350        let exterior_coords = vec![
1351            Coordinate::new_2d(0.0, 0.0),
1352            Coordinate::new_2d(20.0, 0.0),
1353            Coordinate::new_2d(20.0, 20.0),
1354            Coordinate::new_2d(0.0, 20.0),
1355            Coordinate::new_2d(0.0, 0.0),
1356        ];
1357        let hole_coords = vec![
1358            Coordinate::new_2d(15.0, 15.0),
1359            Coordinate::new_2d(18.0, 15.0),
1360            Coordinate::new_2d(18.0, 18.0),
1361            Coordinate::new_2d(15.0, 18.0),
1362            Coordinate::new_2d(15.0, 15.0),
1363        ];
1364
1365        let exterior = LineString::new(exterior_coords);
1366        let hole = LineString::new(hole_coords);
1367
1368        assert!(exterior.is_ok() && hole.is_ok());
1369        if let (Ok(ext), Ok(h)) = (exterior, hole) {
1370            let poly = Polygon::new(ext, vec![h]);
1371            assert!(poly.is_ok());
1372
1373            if let Ok(p) = poly {
1374                // Clip to box that doesn't include the hole
1375                let result = clip_to_box(&p, 0.0, 0.0, 10.0, 10.0);
1376                assert!(result.is_ok());
1377                if let Ok(polys) = result {
1378                    // Hole should be removed (outside clip box)
1379                    assert_eq!(polys.len(), 1);
1380                    assert_eq!(polys[0].interiors.len(), 0);
1381                }
1382            }
1383        }
1384    }
1385
1386    #[test]
1387    fn test_clip_to_box_hole_partial() {
1388        // Polygon with hole, where hole straddles the clipping box boundary
1389        let exterior_coords = vec![
1390            Coordinate::new_2d(0.0, 0.0),
1391            Coordinate::new_2d(20.0, 0.0),
1392            Coordinate::new_2d(20.0, 20.0),
1393            Coordinate::new_2d(0.0, 20.0),
1394            Coordinate::new_2d(0.0, 0.0),
1395        ];
1396        let hole_coords = vec![
1397            Coordinate::new_2d(8.0, 8.0),
1398            Coordinate::new_2d(12.0, 8.0),
1399            Coordinate::new_2d(12.0, 12.0),
1400            Coordinate::new_2d(8.0, 12.0),
1401            Coordinate::new_2d(8.0, 8.0),
1402        ];
1403
1404        let exterior = LineString::new(exterior_coords);
1405        let hole = LineString::new(hole_coords);
1406
1407        assert!(exterior.is_ok() && hole.is_ok());
1408        if let (Ok(ext), Ok(h)) = (exterior, hole) {
1409            let poly = Polygon::new(ext, vec![h]);
1410            assert!(poly.is_ok());
1411
1412            if let Ok(p) = poly {
1413                // Clip to box that partially includes the hole
1414                let result = clip_to_box(&p, 0.0, 0.0, 10.0, 10.0);
1415                assert!(result.is_ok());
1416                // The hole should be clipped or removed depending on the result
1417            }
1418        }
1419    }
1420
1421    #[test]
1422    fn test_validate_polygon_topology_valid() {
1423        let poly = create_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0);
1424        assert!(poly.is_ok());
1425
1426        if let Ok(p) = poly {
1427            let result = validate_polygon_topology(&p);
1428            assert!(result.is_ok());
1429            if let Ok(valid) = result {
1430                assert!(valid);
1431            }
1432        }
1433    }
1434
1435    #[test]
1436    fn test_validate_polygon_topology_hole_outside() {
1437        // Create polygon with hole outside exterior
1438        let exterior_coords = vec![
1439            Coordinate::new_2d(0.0, 0.0),
1440            Coordinate::new_2d(10.0, 0.0),
1441            Coordinate::new_2d(10.0, 10.0),
1442            Coordinate::new_2d(0.0, 10.0),
1443            Coordinate::new_2d(0.0, 0.0),
1444        ];
1445        let hole_coords = vec![
1446            Coordinate::new_2d(20.0, 20.0),
1447            Coordinate::new_2d(25.0, 20.0),
1448            Coordinate::new_2d(25.0, 25.0),
1449            Coordinate::new_2d(20.0, 25.0),
1450            Coordinate::new_2d(20.0, 20.0),
1451        ];
1452
1453        let exterior = LineString::new(exterior_coords);
1454        let hole = LineString::new(hole_coords);
1455
1456        assert!(exterior.is_ok() && hole.is_ok());
1457        if let (Ok(ext), Ok(h)) = (exterior, hole) {
1458            // Note: Polygon::new doesn't validate that holes are inside exterior
1459            // So we construct the polygon directly for testing
1460            let poly = Polygon {
1461                exterior: ext,
1462                interiors: vec![h],
1463            };
1464
1465            let result = validate_polygon_topology(&poly);
1466            assert!(result.is_ok());
1467            if let Ok(valid) = result {
1468                // Should be invalid because hole is outside
1469                assert!(!valid);
1470            }
1471        }
1472    }
1473
1474    #[test]
1475    fn test_merge_adjacent_holes_no_overlap() {
1476        let hole1_coords = vec![
1477            Coordinate::new_2d(2.0, 2.0),
1478            Coordinate::new_2d(4.0, 2.0),
1479            Coordinate::new_2d(4.0, 4.0),
1480            Coordinate::new_2d(2.0, 4.0),
1481            Coordinate::new_2d(2.0, 2.0),
1482        ];
1483        let hole2_coords = vec![
1484            Coordinate::new_2d(6.0, 6.0),
1485            Coordinate::new_2d(8.0, 6.0),
1486            Coordinate::new_2d(8.0, 8.0),
1487            Coordinate::new_2d(6.0, 8.0),
1488            Coordinate::new_2d(6.0, 6.0),
1489        ];
1490
1491        let hole1 = LineString::new(hole1_coords);
1492        let hole2 = LineString::new(hole2_coords);
1493
1494        assert!(hole1.is_ok() && hole2.is_ok());
1495        if let (Ok(h1), Ok(h2)) = (hole1, hole2) {
1496            let result = merge_adjacent_holes(&[h1, h2]);
1497            assert!(result.is_ok());
1498            if let Ok(merged) = result {
1499                // No overlap, both holes should be preserved
1500                assert_eq!(merged.len(), 2);
1501            }
1502        }
1503    }
1504
1505    #[test]
1506    fn test_merge_adjacent_holes_with_overlap() {
1507        let hole1_coords = vec![
1508            Coordinate::new_2d(2.0, 2.0),
1509            Coordinate::new_2d(6.0, 2.0),
1510            Coordinate::new_2d(6.0, 6.0),
1511            Coordinate::new_2d(2.0, 6.0),
1512            Coordinate::new_2d(2.0, 2.0),
1513        ];
1514        let hole2_coords = vec![
1515            Coordinate::new_2d(4.0, 4.0),
1516            Coordinate::new_2d(8.0, 4.0),
1517            Coordinate::new_2d(8.0, 8.0),
1518            Coordinate::new_2d(4.0, 8.0),
1519            Coordinate::new_2d(4.0, 4.0),
1520        ];
1521
1522        let hole1 = LineString::new(hole1_coords);
1523        let hole2 = LineString::new(hole2_coords);
1524
1525        assert!(hole1.is_ok() && hole2.is_ok());
1526        if let (Ok(h1), Ok(h2)) = (hole1, hole2) {
1527            let result = merge_adjacent_holes(&[h1, h2]);
1528            assert!(result.is_ok());
1529            if let Ok(merged) = result {
1530                // Overlapping holes should be merged into one
1531                assert_eq!(merged.len(), 1);
1532            }
1533        }
1534    }
1535
1536    #[test]
1537    fn test_point_in_ring() {
1538        let ring = vec![
1539            Coordinate::new_2d(0.0, 0.0),
1540            Coordinate::new_2d(10.0, 0.0),
1541            Coordinate::new_2d(10.0, 10.0),
1542            Coordinate::new_2d(0.0, 10.0),
1543            Coordinate::new_2d(0.0, 0.0),
1544        ];
1545
1546        // Point inside
1547        let inside = point_in_ring(&Coordinate::new_2d(5.0, 5.0), &ring);
1548        assert!(inside);
1549
1550        // Point outside
1551        let outside = point_in_ring(&Coordinate::new_2d(15.0, 15.0), &ring);
1552        assert!(!outside);
1553
1554        // Point on edge (behavior may vary)
1555        let on_edge = point_in_ring(&Coordinate::new_2d(5.0, 0.0), &ring);
1556        // Edge cases are implementation-dependent
1557        let _ = on_edge;
1558    }
1559
1560    #[test]
1561    fn test_is_ring_inside_ring() {
1562        let outer = vec![
1563            Coordinate::new_2d(0.0, 0.0),
1564            Coordinate::new_2d(20.0, 0.0),
1565            Coordinate::new_2d(20.0, 20.0),
1566            Coordinate::new_2d(0.0, 20.0),
1567            Coordinate::new_2d(0.0, 0.0),
1568        ];
1569
1570        let inner = vec![
1571            Coordinate::new_2d(5.0, 5.0),
1572            Coordinate::new_2d(15.0, 5.0),
1573            Coordinate::new_2d(15.0, 15.0),
1574            Coordinate::new_2d(5.0, 15.0),
1575            Coordinate::new_2d(5.0, 5.0),
1576        ];
1577
1578        let outside = vec![
1579            Coordinate::new_2d(30.0, 30.0),
1580            Coordinate::new_2d(40.0, 30.0),
1581            Coordinate::new_2d(40.0, 40.0),
1582            Coordinate::new_2d(30.0, 40.0),
1583            Coordinate::new_2d(30.0, 30.0),
1584        ];
1585
1586        assert!(is_ring_inside_ring(&inner, &outer));
1587        assert!(!is_ring_inside_ring(&outside, &outer));
1588    }
1589
1590    #[test]
1591    fn test_is_ring_inside_ring_concave() {
1592        // A concave "C"-shaped outer ring opening to the right. Its notch
1593        // (x in [3,10], y in [3,7]) is OUTSIDE the ring material.
1594        let outer = vec![
1595            Coordinate::new_2d(0.0, 0.0),
1596            Coordinate::new_2d(10.0, 0.0),
1597            Coordinate::new_2d(10.0, 3.0),
1598            Coordinate::new_2d(3.0, 3.0),
1599            Coordinate::new_2d(3.0, 7.0),
1600            Coordinate::new_2d(10.0, 7.0),
1601            Coordinate::new_2d(10.0, 10.0),
1602            Coordinate::new_2d(0.0, 10.0),
1603            Coordinate::new_2d(0.0, 0.0),
1604        ];
1605
1606        // A smaller concave "C" that lies entirely within the outer material,
1607        // but whose CENTROID (5.25, 5.0) falls inside the outer's notch — i.e.
1608        // OUTSIDE the outer ring. The old centroid-only check returned `false`
1609        // here (dropping a genuinely-contained hole); the robust check returns
1610        // `true`.
1611        let inner = vec![
1612            Coordinate::new_2d(1.0, 1.0),
1613            Coordinate::new_2d(9.0, 1.0),
1614            Coordinate::new_2d(9.0, 2.0),
1615            Coordinate::new_2d(2.0, 2.0),
1616            Coordinate::new_2d(2.0, 8.0),
1617            Coordinate::new_2d(9.0, 8.0),
1618            Coordinate::new_2d(9.0, 9.0),
1619            Coordinate::new_2d(1.0, 9.0),
1620            Coordinate::new_2d(1.0, 1.0),
1621        ];
1622
1623        // Confirm the premise: the inner centroid is outside the outer ring, so
1624        // a centroid-only test would give the wrong answer.
1625        let centroid = compute_ring_centroid(&inner);
1626        assert!(
1627            !point_in_ring(&centroid, &outer),
1628            "test premise: inner centroid must fall in the outer's notch"
1629        );
1630
1631        // Robust check correctly reports containment.
1632        assert!(
1633            is_ring_inside_ring(&inner, &outer),
1634            "concave inner ring is genuinely inside the concave outer ring"
1635        );
1636
1637        // A ring straddling the outer's inner spine (x=3): part sits in the
1638        // material (x<3), part in the notch (x>3), so its edges properly cross
1639        // the outer boundary and it must NOT be reported inside.
1640        let straddling = vec![
1641            Coordinate::new_2d(1.0, 4.0),
1642            Coordinate::new_2d(6.0, 4.0), // crosses the spine at x=3
1643            Coordinate::new_2d(6.0, 6.0),
1644            Coordinate::new_2d(1.0, 6.0), // crosses the spine at x=3
1645            Coordinate::new_2d(1.0, 4.0),
1646        ];
1647        assert!(
1648            !is_ring_inside_ring(&straddling, &outer),
1649            "a ring crossing the outer boundary is not contained"
1650        );
1651    }
1652
1653    #[test]
1654    fn test_clip_ring_to_box() {
1655        let ring = vec![
1656            Coordinate::new_2d(0.0, 0.0),
1657            Coordinate::new_2d(20.0, 0.0),
1658            Coordinate::new_2d(20.0, 20.0),
1659            Coordinate::new_2d(0.0, 20.0),
1660            Coordinate::new_2d(0.0, 0.0),
1661        ];
1662
1663        let result = clip_ring_to_box(&ring, 5.0, 5.0, 15.0, 15.0);
1664        assert!(result.is_ok());
1665        if let Ok(clipped) = result {
1666            // Should produce a ring roughly 10x10
1667            assert!(clipped.len() >= 4);
1668        }
1669    }
1670
1671    #[test]
1672    fn test_compute_ring_centroid() {
1673        let ring = vec![
1674            Coordinate::new_2d(0.0, 0.0),
1675            Coordinate::new_2d(10.0, 0.0),
1676            Coordinate::new_2d(10.0, 10.0),
1677            Coordinate::new_2d(0.0, 10.0),
1678            Coordinate::new_2d(0.0, 0.0),
1679        ];
1680
1681        let centroid = compute_ring_centroid(&ring);
1682        // For a square, centroid should be at center
1683        assert!((centroid.x - 4.0).abs() < 1.0); // Approximate due to closing point
1684        assert!((centroid.y - 4.0).abs() < 1.0);
1685    }
1686
1687    #[test]
1688    fn test_difference_preserves_unaffected_holes() {
1689        // Create polygon with multiple holes
1690        let exterior_coords = vec![
1691            Coordinate::new_2d(0.0, 0.0),
1692            Coordinate::new_2d(30.0, 0.0),
1693            Coordinate::new_2d(30.0, 30.0),
1694            Coordinate::new_2d(0.0, 30.0),
1695            Coordinate::new_2d(0.0, 0.0),
1696        ];
1697        let hole1_coords = vec![
1698            Coordinate::new_2d(2.0, 2.0),
1699            Coordinate::new_2d(5.0, 2.0),
1700            Coordinate::new_2d(5.0, 5.0),
1701            Coordinate::new_2d(2.0, 5.0),
1702            Coordinate::new_2d(2.0, 2.0),
1703        ];
1704        let hole2_coords = vec![
1705            Coordinate::new_2d(20.0, 20.0),
1706            Coordinate::new_2d(25.0, 20.0),
1707            Coordinate::new_2d(25.0, 25.0),
1708            Coordinate::new_2d(20.0, 25.0),
1709            Coordinate::new_2d(20.0, 20.0),
1710        ];
1711
1712        let exterior = LineString::new(exterior_coords);
1713        let hole1 = LineString::new(hole1_coords);
1714        let hole2 = LineString::new(hole2_coords);
1715
1716        assert!(exterior.is_ok() && hole1.is_ok() && hole2.is_ok());
1717        if let (Ok(ext), Ok(h1), Ok(h2)) = (exterior, hole1, hole2) {
1718            let poly1 = Polygon::new(ext, vec![h1, h2]);
1719            let poly2 = create_square(10.0, 10.0, 5.0);
1720
1721            assert!(poly1.is_ok() && poly2.is_ok());
1722            if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
1723                let result = difference_polygon(&p1, &p2);
1724                assert!(result.is_ok());
1725                if let Ok(polys) = result {
1726                    // poly2 should become a new hole, existing holes should be preserved
1727                    assert_eq!(polys.len(), 1);
1728                    // Should have at least 3 holes (2 original + 1 new)
1729                    assert!(polys[0].interiors.len() >= 2);
1730                }
1731            }
1732        }
1733    }
1734}