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