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