Skip to main content

geo_repair/validation/
core.rs

1use geo::{Coord, GeoFloat, Line, LineString, MultiLineString, MultiPoint, Point, Rect, Triangle};
2use thiserror::Error;
3
4/// Errors reported by OGC geometry validation.
5///
6/// Each variant corresponds to an OGC Simple Features validity rule.
7#[derive(Error, Clone, Debug, PartialEq)]
8pub enum GeometryValidationError {
9    /// One or more coordinates contain NaN or infinite values.
10    #[error("Coordinate is NaN")]
11    CoordinateNaN,
12
13    /// A ring does not have enough distinct vertices (min 4 for rings).
14    #[error("Ring has too few points: found {found}, minimum required {min}")]
15    RingTooFewPoints { found: usize, min: usize },
16
17    /// A ring's first and last coordinates are not equal (not closed).
18    #[error("Ring is not closed: first {first:?} != last {last:?}")]
19    RingNotClosed { first: Coord<f64>, last: Coord<f64> },
20
21    /// A ring has edges that cross or overlap non-adjacent edges.
22    #[error("Ring has self-intersections")]
23    SelfIntersection,
24
25    /// A ring has a non-consecutive repeated vertex (pinch point).
26    #[error("Ring has repeated non-consecutive vertices (pinch point)")]
27    PinchPoint,
28
29    /// A polygon hole lies partially or fully outside its shell.
30    #[error("Hole lies outside shell")]
31    HoleOutsideShell,
32
33    /// Two or more polygon holes are nested inside each other.
34    #[error("Holes are nested")]
35    NestedHoles,
36
37    /// An interior ring is disconnected from the shell (touching at ≥ 2 points or edges crossing).
38    #[error("Interior ring is disconnected from shell")]
39    DisconnectedInteriorRing,
40
41    /// Ring winding direction is incorrect (exterior must be CCW, interior CW).
42    #[error("Wrong ring orientation: exterior should be CCW, interior CW")]
43    WrongOrientation,
44
45    /// All vertices of a ring are collinear (zero area).
46    #[error("Collinear ring: all points lie on a line")]
47    CollinearRing,
48
49    /// Consecutive duplicate coordinates found in a geometry.
50    #[error("Geometry has repeated (duplicate) points")]
51    RepeatedPoint,
52
53    /// A polygon contains two or more identical rings.
54    #[error("Geometry contains duplicate rings")]
55    DuplicatedRings,
56
57    /// A MultiPoint contains the same point more than once.
58    #[error("MultiPoint contains duplicate points")]
59    MultiPointDuplicatePoints,
60
61    /// A MultiLineString contains the same linestring more than once.
62    #[error("MultiLineString contains duplicate linestrings")]
63    MultiLineStringDuplicateLines,
64
65    /// A Line has zero length (start and end coordinates are equal).
66    #[error("Line has zero length (start == end at {0:?})")]
67    ZeroLengthLine(Coord<f64>),
68
69    /// A polygon's exterior ring has degenerated to a line or point.
70    #[error("Polygon exterior ring is degenerate (collapsed)")]
71    DegenerateExterior,
72
73    /// A LineString or MultiLineString has components that intersect at interior points.
74    #[error("Geometry is not simple: components intersect at interior points")]
75    NotSimple,
76
77    /// A GeometryCollection has exceeded the maximum nesting depth.
78    #[error("GeometryCollection nesting exceeds maximum depth")]
79    ExcessiveNesting,
80}
81
82/// Result of an OGC validity check.
83///
84/// Contains the overall valid/invalid status and a list of detailed
85/// [`GeometryValidationError`] entries describing each violation found.
86///
87/// # Examples
88///
89/// ```rust
90/// # use geo::{Geometry, Point};
91/// # let geometry = Geometry::Point(Point::new(0.0, 0.0));
92/// use geo_repair::{validate, ValidationResult};
93///
94/// let result = validate(&geometry);
95/// if result.valid {
96///     println!("Geometry is valid");
97/// } else {
98///     for err in &result.errors {
99///         println!("  Violation: {err}");
100///     }
101/// }
102/// ```
103#[derive(Clone, Debug, PartialEq)]
104pub struct ValidationResult {
105    /// Whether the geometry passed all OGC validity checks.
106    pub valid: bool,
107    /// List of validity violations found. Empty when `valid` is true.
108    pub errors: Vec<GeometryValidationError>,
109}
110
111impl ValidationResult {
112    /// Create a result indicating a valid geometry (no errors).
113    pub fn valid() -> Self {
114        Self {
115            valid: true,
116            errors: Vec::new(),
117        }
118    }
119
120    /// Create a result indicating an invalid geometry with the given errors.
121    pub fn invalid(errors: Vec<GeometryValidationError>) -> Self {
122        Self {
123            valid: false,
124            errors,
125        }
126    }
127
128    /// Human-readable validity reason (like GEOS `isValidReason`).
129    ///
130    /// Returns `"Valid Geometry"` when valid, or a semicolon-separated list of
131    /// violations when invalid.
132    pub fn reason(&self) -> String {
133        if self.valid {
134            "Valid Geometry".to_string()
135        } else {
136            self.errors
137                .iter()
138                .map(|e| e.to_string())
139                .collect::<Vec<_>>()
140                .join("; ")
141        }
142    }
143}
144
145/// Trait for OGC geometry validation.
146///
147/// Implemented for all geometry types. Call [`validate`](GeoValidation::validate)
148/// to get a [`ValidationResult`] with all violations, or
149/// [`is_valid`](GeoValidation::is_valid) for a quick boolean check.
150pub trait GeoValidation {
151    /// The scalar coordinate type (e.g. `f64`, `f32`).
152    type Scalar: GeoFloat;
153
154    /// Quick validity check — returns `true` if the geometry passes all OGC rules.
155    fn is_valid(&self) -> bool {
156        self.validate().valid
157    }
158
159    /// Full validation — returns a [`ValidationResult`] with all violations found.
160    fn validate(&self) -> ValidationResult;
161
162    /// Human-readable validity reason (like GEOS `isValidReason`).
163    ///
164    /// Returns `"Valid Geometry"` when valid, or a semicolon-separated list
165    /// of violation descriptions when invalid.
166    fn validate_reason(&self) -> String {
167        let result = self.validate();
168        if result.valid {
169            "Valid Geometry".to_string()
170        } else {
171            result
172                .errors
173                .iter()
174                .map(|e| e.to_string())
175                .collect::<Vec<_>>()
176                .join("; ")
177        }
178    }
179}
180
181pub(crate) fn ring_has_non_finite(ring: &[Coord<f64>]) -> bool {
182    ring.iter().any(|c| !c.x.is_finite() || !c.y.is_finite())
183}
184
185pub(crate) fn check_ring_validity(
186    ring: &[Coord<f64>],
187    is_exterior: bool,
188) -> Vec<GeometryValidationError> {
189    let mut errors = Vec::new();
190
191    if ring_has_non_finite(ring) {
192        errors.push(GeometryValidationError::CoordinateNaN);
193        return errors;
194    }
195
196    if ring.len() < 4 {
197        errors.push(GeometryValidationError::RingTooFewPoints {
198            found: ring.len(),
199            min: 4,
200        });
201        return errors;
202    }
203
204    if ring.first() != ring.last() {
205        errors.push(GeometryValidationError::RingNotClosed {
206            first: ring[0],
207            last: ring[ring.len() - 1],
208        });
209        return errors;
210    }
211
212    let n = ring.len() - 1;
213
214    let (mut min_x, mut max_x, mut min_y, mut max_y) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
215    for c in &ring[..n] {
216        min_x = min_x.min(c.x);
217        max_x = max_x.max(c.x);
218        min_y = min_y.min(c.y);
219        max_y = max_y.max(c.y);
220    }
221    let scale = (max_x - min_x).abs().max((max_y - min_y).abs()).max(1.0);
222    let eps = 1e-12 * scale;
223    if (max_x - min_x).abs() < f64::EPSILON * scale || (max_y - min_y).abs() < f64::EPSILON * scale
224    {
225        if is_exterior {
226            errors.push(GeometryValidationError::DegenerateExterior);
227        } else {
228            errors.push(GeometryValidationError::CollinearRing);
229        }
230        return errors;
231    }
232
233    {
234        let mut prev_coord = &ring[0];
235        for c in &ring[1..n] {
236            if c.x == prev_coord.x && c.y == prev_coord.y {
237                errors.push(GeometryValidationError::RepeatedPoint);
238                break;
239            }
240            prev_coord = c;
241        }
242    }
243
244    let mut seen: rustc_hash::FxHashMap<(u64, u64), usize> =
245        rustc_hash::FxHashMap::with_capacity_and_hasher(n, Default::default());
246    for (idx, c) in ring[..n].iter().enumerate() {
247        let key = (c.x.to_bits(), c.y.to_bits());
248        if let Some(&prev) = seen.get(&key) {
249            if prev + 1 == idx {
250                continue;
251            }
252            errors.push(GeometryValidationError::PinchPoint);
253            break;
254        } else {
255            seen.insert(key, idx);
256        }
257    }
258
259    #[cfg(feature = "rstar")]
260    {
261        struct EdgeEnv {
262            idx: u32,
263            env: rstar::AABB<[f64; 2]>,
264        }
265        impl rstar::RTreeObject for EdgeEnv {
266            type Envelope = rstar::AABB<[f64; 2]>;
267            fn envelope(&self) -> Self::Envelope {
268                self.env
269            }
270        }
271        if n > 64 {
272            let mut edges = Vec::with_capacity(n);
273            for i in 0..n {
274                let (lo_x, hi_x) = if ring[i].x < ring[(i + 1) % n].x {
275                    (ring[i].x, ring[(i + 1) % n].x)
276                } else {
277                    (ring[(i + 1) % n].x, ring[i].x)
278                };
279                let (lo_y, hi_y) = if ring[i].y < ring[(i + 1) % n].y {
280                    (ring[i].y, ring[(i + 1) % n].y)
281                } else {
282                    (ring[(i + 1) % n].y, ring[i].y)
283                };
284                let ext = (hi_x - lo_x).abs().max((hi_y - lo_y).abs()).max(1.0) * 1e-10;
285                edges.push(EdgeEnv {
286                    idx: i as u32,
287                    env: rstar::AABB::from_corners(
288                        [lo_x - ext, lo_y - ext],
289                        [hi_x + ext, hi_y + ext],
290                    ),
291                });
292            }
293            let tree = rstar::RTree::bulk_load(edges);
294            for i in 0..n {
295                let (lo_x, hi_x) = if ring[i].x < ring[(i + 1) % n].x {
296                    (ring[i].x, ring[(i + 1) % n].x)
297                } else {
298                    (ring[(i + 1) % n].x, ring[i].x)
299                };
300                let (lo_y, hi_y) = if ring[i].y < ring[(i + 1) % n].y {
301                    (ring[i].y, ring[(i + 1) % n].y)
302                } else {
303                    (ring[(i + 1) % n].y, ring[i].y)
304                };
305                let ext = (hi_x - lo_x).abs().max((hi_y - lo_y).abs()).max(1.0) * 1e-10;
306                let env =
307                    rstar::AABB::from_corners([lo_x - ext, lo_y - ext], [hi_x + ext, hi_y + ext]);
308                let found = tree.locate_in_envelope_intersecting_int(&env, |c| {
309                    let j = c.idx as usize;
310                    if j <= i {
311                        return std::ops::ControlFlow::Continue(());
312                    }
313                    if i.abs_diff(j) <= 1 || (i == 0 && j == n - 1) {
314                        return std::ops::ControlFlow::Continue(());
315                    }
316                    if check_edge_pair_intersection(ring, i, j, eps) {
317                        std::ops::ControlFlow::Break(())
318                    } else {
319                        std::ops::ControlFlow::<(), ()>::Continue(())
320                    }
321                });
322                if found.is_break() {
323                    errors.push(GeometryValidationError::SelfIntersection);
324                    return errors;
325                }
326            }
327        } else {
328            for i in 0..n {
329                for j in i + 2..n {
330                    if i == 0 && j == n - 1 {
331                        continue;
332                    }
333                    if check_edge_pair_intersection(ring, i, j, eps) {
334                        errors.push(GeometryValidationError::SelfIntersection);
335                        return errors;
336                    }
337                }
338            }
339        }
340    }
341    #[cfg(not(feature = "rstar"))]
342    {
343        for i in 0..n {
344            for j in i + 2..n {
345                if i == 0 && j == n - 1 {
346                    continue;
347                }
348                if check_edge_pair_intersection(ring, i, j, eps) {
349                    errors.push(GeometryValidationError::SelfIntersection);
350                    return errors;
351                }
352            }
353        }
354    }
355
356    errors
357}
358
359pub(crate) fn edges_intersect_general(
360    a1: Coord<f64>,
361    a2: Coord<f64>,
362    b1: Coord<f64>,
363    b2: Coord<f64>,
364    eps: f64,
365) -> bool {
366    // Fast f64 orient2d for validation. The repair pipeline uses Shewchuk
367    // for precision-critical intersection computation.
368    let o1 = crate::orient::orient2d_fast(a1, a2, b1);
369    let o2 = crate::orient::orient2d_fast(a1, a2, b2);
370    let o3 = crate::orient::orient2d_fast(b1, b2, a1);
371    let o4 = crate::orient::orient2d_fast(b1, b2, a2);
372
373    // Proper crossing
374    if o1 * o2 < 0.0 && o3 * o4 < 0.0 {
375        return true;
376    }
377
378    // Collinear overlap (excluding endpoint-only touching)
379    let collinear = o1.abs() <= eps && o2.abs() <= eps;
380    if collinear {
381        let dx = a2.x - a1.x;
382        let dy = a2.y - a1.y;
383        let len2 = dx * dx + dy * dy;
384        if len2 > eps {
385            let t1 = ((b1.x - a1.x) * dx + (b1.y - a1.y) * dy) / len2;
386            let t2 = ((b2.x - a1.x) * dx + (b2.y - a1.y) * dy) / len2;
387            let lo = 0.0f64.max(t1.min(t2));
388            let hi = 1.0f64.min(t1.max(t2));
389            if hi - lo > eps {
390                return true;
391            }
392        }
393    }
394
395    false
396}
397
398pub(crate) fn check_edge_pair_intersection(
399    coords: &[Coord<f64>],
400    i: usize,
401    j: usize,
402    eps: f64,
403) -> bool {
404    let n = coords.len() - 1;
405    let a1 = coords[i];
406    let a2 = coords[(i + 1) % n];
407    let b1 = coords[j];
408    let b2 = coords[(j + 1) % n];
409    edges_intersect_general(a1, a2, b1, b2, eps)
410}
411
412/// Minimal edge-index wrapper for R-tree intersection queries.
413#[cfg(feature = "rstar")]
414struct EdgeIdx {
415    idx: usize,
416    env: rstar::AABB<[f64; 2]>,
417}
418#[cfg(feature = "rstar")]
419impl rstar::RTreeObject for EdgeIdx {
420    type Envelope = rstar::AABB<[f64; 2]>;
421    fn envelope(&self) -> Self::Envelope {
422        self.env
423    }
424}
425
426/// Build an R-tree over a ring's edges (wrapping at len-1 for closing point).
427#[cfg(feature = "rstar")]
428fn build_ring_edge_tree(ring: &[Coord<f64>]) -> rstar::RTree<EdgeIdx> {
429    let n = ring.len() - 1;
430    rstar::RTree::bulk_load(
431        (0..n)
432            .map(|i| {
433                let a = ring[i];
434                let b = ring[(i + 1) % n];
435                let (lo_x, hi_x) = if a.x < b.x { (a.x, b.x) } else { (b.x, a.x) };
436                let (lo_y, hi_y) = if a.y < b.y { (a.y, b.y) } else { (b.y, a.y) };
437                EdgeIdx {
438                    idx: i,
439                    env: rstar::AABB::from_corners([lo_x, lo_y], [hi_x, hi_y]),
440                }
441            })
442            .collect(),
443    )
444}
445
446/// Build an R-tree over a linestring's segments (non-ring, no wrap-around).
447#[cfg(feature = "rstar")]
448fn build_ls_edge_tree(coords: &[Coord<f64>]) -> rstar::RTree<EdgeIdx> {
449    let n = coords.len() - 1;
450    if n < 1 {
451        return rstar::RTree::bulk_load(Vec::new());
452    }
453    rstar::RTree::bulk_load(
454        (0..n)
455            .map(|i| {
456                let a = coords[i];
457                let b = coords[i + 1];
458                let (lo_x, hi_x) = if a.x < b.x { (a.x, b.x) } else { (b.x, a.x) };
459                let (lo_y, hi_y) = if a.y < b.y { (a.y, b.y) } else { (b.y, a.y) };
460                EdgeIdx {
461                    idx: i,
462                    env: rstar::AABB::from_corners([lo_x, lo_y], [hi_x, hi_y]),
463                }
464            })
465            .collect(),
466    )
467}
468
469/// Check whether two rings (from different polygons) have any intersecting edges.
470/// Touching at a single vertex is allowed (OGC), but crossing, overlapping, or
471/// touching along an edge is not.
472pub(crate) fn check_rings_intersect(ring1: &[Coord<f64>], ring2: &[Coord<f64>], eps: f64) -> bool {
473    let n1 = ring1.len().max(2) - 1;
474    let n2 = ring2.len().max(2) - 1;
475    if n1 < 2 || n2 < 2 {
476        return false;
477    }
478
479    // Brute-force when both rings are small — faster than building a tree.
480    if n1.max(n2) <= 64 {
481        for i in 0..n1 {
482            let a1 = ring1[i];
483            let a2 = ring1[(i + 1) % n1];
484            for j in 0..n2 {
485                let b1 = ring2[j];
486                let b2 = ring2[(j + 1) % n2];
487                if edges_intersect_general(a1, a2, b1, b2, eps) {
488                    return true;
489                }
490            }
491        }
492        return false;
493    }
494
495    // Large rings: build tree over the smaller ring, query each edge of the
496    // larger ring via envelope intersection.
497    #[cfg(feature = "rstar")]
498    {
499        let (build_ring, query_ring, n_query) = if n1 < n2 {
500            (ring1, ring2, n2)
501        } else {
502            (ring2, ring1, n1)
503        };
504        let n_build = build_ring.len() - 1;
505        let tree = build_ring_edge_tree(build_ring);
506
507        for i in 0..n_query {
508            let a1 = query_ring[i];
509            let a2 = query_ring[(i + 1) % n_query];
510            let (lo_x, hi_x) = if a1.x < a2.x {
511                (a1.x, a2.x)
512            } else {
513                (a2.x, a1.x)
514            };
515            let (lo_y, hi_y) = if a1.y < a2.y {
516                (a1.y, a2.y)
517            } else {
518                (a2.y, a1.y)
519            };
520            let query = rstar::AABB::from_corners([lo_x, lo_y], [hi_x, hi_y]);
521            let found = tree.locate_in_envelope_intersecting_int(&query, |c| {
522                let b1 = build_ring[c.idx];
523                let b2 = build_ring[(c.idx + 1) % n_build];
524                if edges_intersect_general(a1, a2, b1, b2, eps) {
525                    std::ops::ControlFlow::Break(())
526                } else {
527                    std::ops::ControlFlow::<(), ()>::Continue(())
528                }
529            });
530            if found.is_break() {
531                return true;
532            }
533        }
534    }
535    #[cfg(not(feature = "rstar"))]
536    {
537        for i in 0..n1 {
538            let a1 = ring1[i];
539            let a2 = ring1[(i + 1) % n1];
540            for j in 0..n2 {
541                let b1 = ring2[j];
542                let b2 = ring2[(j + 1) % n2];
543                if edges_intersect_general(a1, a2, b1, b2, eps) {
544                    return true;
545                }
546            }
547        }
548    }
549    false
550}
551
552pub(crate) fn check_orientation(ring: &[Coord<f64>]) -> bool {
553    if ring.len() < 4 {
554        return true;
555    }
556    crate::util::shoelace_sum(ring) > 0.0
557}
558
559pub(crate) fn point_in_ring_exclusive(pt: Coord<f64>, ring: &[Coord<f64>]) -> bool {
560    let n = ring.len();
561    let mut wn = 0i32;
562    for i in 0..n - 1 {
563        let p1 = ring[i];
564        let p2 = ring[i + 1];
565        if p1.y <= pt.y {
566            if p2.y > pt.y {
567                let o = (p2.x - p1.x) * (pt.y - p1.y) - (p2.y - p1.y) * (pt.x - p1.x);
568                if o > 0.0 {
569                    wn += 1;
570                }
571            }
572        } else if p2.y <= pt.y {
573            let o = (p2.x - p1.x) * (pt.y - p1.y) - (p2.y - p1.y) * (pt.x - p1.x);
574            if o < 0.0 {
575                wn -= 1;
576            }
577        }
578    }
579    wn != 0
580}
581
582pub(crate) fn point_on_segment(pt: Coord<f64>, a: Coord<f64>, b: Coord<f64>, eps: f64) -> bool {
583    let o = (b.x - a.x) * (pt.y - a.y) - (b.y - a.y) * (pt.x - a.x);
584    if o.abs() > eps {
585        return false;
586    }
587    let min_x = a.x.min(b.x) - eps;
588    let max_x = a.x.max(b.x) + eps;
589    let min_y = a.y.min(b.y) - eps;
590    let max_y = a.y.max(b.y) + eps;
591    pt.x >= min_x && pt.x <= max_x && pt.y >= min_y && pt.y <= max_y
592}
593
594pub(crate) fn point_on_ring(pt: Coord<f64>, ring: &[Coord<f64>], eps: f64) -> bool {
595    let n = ring.len() - 1;
596    if n == 0 {
597        return false;
598    }
599    for i in 0..n {
600        if point_on_segment(pt, ring[i], ring[(i + 1) % n], eps) {
601            return true;
602        }
603    }
604    false
605}
606
607/// Fast rotation-invariant fingerprint for duplicate ring detection.
608///
609/// Finds the index of the lexicographically-minimum coordinate, hashes the
610/// ring starting from that index in both forward and reverse directions, and
611/// XORs the two hashes together.  Two rings that are rotated duplicates will
612/// produce the same fingerprint regardless of winding order.
613pub(crate) fn ring_dup_fingerprint(ring: &[Coord<f64>]) -> (usize, u64) {
614    let n = ring.len() - 1;
615    if n == 0 {
616        return (ring.len(), 0);
617    }
618    let min_idx = {
619        let mut idx = 0usize;
620        for i in 1..n {
621            let c = ring[i];
622            let m = ring[idx];
623            if c.x < m.x || (c.x == m.x && c.y < m.y) {
624                idx = i;
625            }
626        }
627        idx
628    };
629    let mut h_fwd = 0u64;
630    let mut h_rev = 0u64;
631    for i in 0..n {
632        let c = ring[(min_idx + i) % n];
633        h_fwd = h_fwd
634            .wrapping_mul(6364136223846793005)
635            .wrapping_add(c.x.to_bits());
636        h_fwd = h_fwd
637            .wrapping_mul(6364136223846793005)
638            .wrapping_add(c.y.to_bits());
639        let d = ring[(min_idx + n - i) % n];
640        h_rev = h_rev
641            .wrapping_mul(6364136223846793005)
642            .wrapping_add(d.x.to_bits());
643        h_rev = h_rev
644            .wrapping_mul(6364136223846793005)
645            .wrapping_add(d.y.to_bits());
646    }
647    (ring.len(), h_fwd ^ h_rev)
648}
649
650/// Check whether two rings (with closing point) are duplicates starting at a
651/// different vertex. Both rings must have the same length and contain the same
652/// sequence of coordinates up to a cyclic rotation.
653pub(crate) fn is_rotated_duplicate(a: &[Coord<f64>], b: &[Coord<f64>]) -> bool {
654    if a.len() != b.len() || a.len() < 2 {
655        return false;
656    }
657    // Rings: last == first, so compare n-1 vertices
658    let n = a.len() - 1;
659    if n == 0 {
660        return false;
661    }
662    // Forward scan (same winding order)
663    for start in 0..n {
664        if a[start] != b[0] {
665            continue;
666        }
667        let mut match_ = true;
668        for i in 0..n {
669            if a[(start + i) % n] != b[i] {
670                match_ = false;
671                break;
672            }
673        }
674        if match_ {
675            return true;
676        }
677    }
678    // Reverse scan (opposite winding order)
679    for start in 0..n {
680        if a[start] != b[0] {
681            continue;
682        }
683        let mut match_ = true;
684        for i in 0..n {
685            if a[(start + n - i) % n] != b[i] {
686                match_ = false;
687                break;
688            }
689        }
690        if match_ {
691            return true;
692        }
693    }
694    false
695}
696
697pub(crate) fn check_holes_valid(
698    shell: &[Coord<f64>],
699    interiors: &[LineString<f64>],
700) -> Vec<GeometryValidationError> {
701    let mut errors = Vec::new();
702
703    // Compute scale-relative epsilon for boundary checks
704    #[cfg(feature = "simd")]
705    let (min_x, max_x, min_y, max_y) = crate::simd::aabb_minmax_simd(shell);
706    #[cfg(not(feature = "simd"))]
707    let (mut min_x, mut max_x, mut min_y, mut max_y) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
708    #[cfg(not(feature = "simd"))]
709    for c in shell {
710        min_x = min_x.min(c.x);
711        max_x = max_x.max(c.x);
712        min_y = min_y.min(c.y);
713        max_y = max_y.max(c.y);
714    }
715    let scale = (max_x - min_x).abs().max((max_y - min_y).abs()).max(1.0);
716    let eps = 1e-12 * scale;
717
718    for hole in interiors {
719        // Check if hole edges cross the shell boundary (hole not fully inside)
720        if check_rings_intersect(&hole.0[..], shell, eps) {
721            errors.push(GeometryValidationError::HoleOutsideShell);
722            return errors;
723        }
724
725        // A hole touching the shell at ≥ 2 distinct points may disconnect the interior
726        let touch_count = hole
727            .0
728            .iter()
729            .filter(|&&hp| point_on_ring(hp, shell, eps))
730            .count();
731        if touch_count >= 2 {
732            errors.push(GeometryValidationError::DisconnectedInteriorRing);
733            return errors;
734        }
735
736        // If no hole vertex is strictly inside the shell, the hole is entirely outside.
737        // Single-point tangent touches (touch_count == 1) are valid per OGC.
738        let any_inside = hole.0.iter().any(|&hp| point_in_ring_exclusive(hp, shell));
739        if !any_inside {
740            errors.push(GeometryValidationError::HoleOutsideShell);
741            return errors;
742        }
743    }
744    let holes: Vec<&[Coord<f64>]> = interiors.iter().map(|h| &h.0[..]).collect();
745    if holes.len() > 1 {
746        // --- hole-hole edge intersection check (disconnected interior) ---
747        for i in 0..holes.len() {
748            for j in (i + 1)..holes.len() {
749                if check_rings_intersect(holes[i], holes[j], eps) {
750                    errors.push(GeometryValidationError::DisconnectedInteriorRing);
751                    return errors;
752                }
753            }
754        }
755
756        // --- nesting check ---
757        #[cfg(feature = "rstar")]
758        {
759            struct HoleEnv2 {
760                idx: usize,
761                env: rstar::AABB<[f64; 2]>,
762            }
763            impl rstar::RTreeObject for HoleEnv2 {
764                type Envelope = rstar::AABB<[f64; 2]>;
765                fn envelope(&self) -> Self::Envelope {
766                    self.env
767                }
768            }
769            let mut envs = Vec::with_capacity(holes.len());
770            for (i, h) in holes.iter().enumerate() {
771                let first = h.first().map(|c| (c.x, c.y)).unwrap_or((0.0, 0.0));
772                let (mut min_x, mut max_x, mut min_y, mut max_y) =
773                    (first.0, first.0, first.1, first.1);
774                for c in *h {
775                    min_x = min_x.min(c.x);
776                    max_x = max_x.max(c.x);
777                    min_y = min_y.min(c.y);
778                    max_y = max_y.max(c.y);
779                }
780                envs.push(HoleEnv2 {
781                    idx: i,
782                    env: rstar::AABB::from_corners([min_x, min_y], [max_x, max_y]),
783                });
784            }
785            let tree = rstar::RTree::bulk_load(envs);
786            for (i, h2) in holes.iter().enumerate() {
787                let Some(pt) = h2.first().copied() else {
788                    continue;
789                };
790                let query = rstar::AABB::from_corners([pt.x, pt.y], [pt.x, pt.y]);
791                let mut overlaps = false;
792                let _ = tree.locate_in_envelope_intersecting_int(&query, |c| {
793                    if c.idx != i && point_in_ring_exclusive(pt, holes[c.idx]) {
794                        overlaps = true;
795                        std::ops::ControlFlow::Break(())
796                    } else {
797                        std::ops::ControlFlow::<(), ()>::Continue(())
798                    }
799                });
800                if overlaps {
801                    errors.push(GeometryValidationError::NestedHoles);
802                    return errors;
803                }
804            }
805        }
806        #[cfg(not(feature = "rstar"))]
807        {
808            for i in 0..holes.len() {
809                for j in 0..holes.len() {
810                    if i == j {
811                        continue;
812                    }
813                    if let Some(pt) = holes[j].first().copied()
814                        && point_in_ring_exclusive(pt, holes[i]) {
815                        errors.push(GeometryValidationError::NestedHoles);
816                        return errors;
817                    }
818                }
819            }
820        }
821    }
822    errors
823}
824
825impl GeoValidation for Point<f64> {
826    type Scalar = f64;
827
828    fn validate(&self) -> ValidationResult {
829        if !self.0.x.is_finite() || !self.0.y.is_finite() {
830            return ValidationResult::invalid(vec![GeometryValidationError::CoordinateNaN]);
831        }
832        ValidationResult::valid()
833    }
834}
835
836impl GeoValidation for MultiPoint<f64> {
837    type Scalar = f64;
838
839    fn validate(&self) -> ValidationResult {
840        let mut errors = Vec::new();
841        for p in &self.0 {
842            let r = p.validate();
843            if !r.valid {
844                errors.extend(r.errors);
845            }
846        }
847        // OGC Simple Features: MultiPoint must not contain duplicate points
848        if self.0.len() > 1 {
849            let mut seen: rustc_hash::FxHashSet<(u64, u64)> =
850                rustc_hash::FxHashSet::with_capacity_and_hasher(self.0.len(), Default::default());
851            for p in &self.0 {
852                let key = (p.x().to_bits(), p.y().to_bits());
853                if !seen.insert(key) {
854                    errors.push(GeometryValidationError::MultiPointDuplicatePoints);
855                    break;
856                }
857            }
858        }
859        if errors.is_empty() {
860            ValidationResult::valid()
861        } else {
862            ValidationResult::invalid(errors)
863        }
864    }
865}
866
867impl GeoValidation for Line<f64> {
868    type Scalar = f64;
869
870    fn validate(&self) -> ValidationResult {
871        if !self.start.x.is_finite()
872            || !self.start.y.is_finite()
873            || !self.end.x.is_finite()
874            || !self.end.y.is_finite()
875        {
876            return ValidationResult::invalid(vec![GeometryValidationError::CoordinateNaN]);
877        }
878        if self.start == self.end {
879            return ValidationResult::invalid(vec![GeometryValidationError::ZeroLengthLine(
880                self.start,
881            )]);
882        }
883        ValidationResult::valid()
884    }
885}
886
887impl GeoValidation for LineString<f64> {
888    type Scalar = f64;
889
890    fn validate(&self) -> ValidationResult {
891        let coords = &self.0;
892        if coords.len() < 2 {
893            return ValidationResult::invalid(vec![GeometryValidationError::RingTooFewPoints {
894                found: coords.len(),
895                min: 2,
896            }]);
897        }
898        if ring_has_non_finite(coords) {
899            return ValidationResult::invalid(vec![GeometryValidationError::CoordinateNaN]);
900        }
901        for i in 1..coords.len() {
902            if coords[i] == coords[i - 1] {
903                return ValidationResult::invalid(vec![GeometryValidationError::RepeatedPoint]);
904            }
905        }
906        // OGC Simple Features: LineString must be simple (no self-intersection)
907        if check_linestring_self_intersection(coords) {
908            return ValidationResult::invalid(vec![GeometryValidationError::NotSimple]);
909        }
910        ValidationResult::valid()
911    }
912}
913
914/// Check if a non-closed LineString has self-intersecting segments.
915pub(crate) fn check_linestring_self_intersection(coords: &[Coord<f64>]) -> bool {
916    let n = coords.len() - 1;
917    if n < 3 {
918        return false;
919    }
920    let scale = {
921        let mut min_x = f64::MAX;
922        let mut max_x = f64::MIN;
923        let mut min_y = f64::MAX;
924        let mut max_y = f64::MIN;
925        for c in coords {
926            min_x = min_x.min(c.x);
927            max_x = max_x.max(c.x);
928            min_y = min_y.min(c.y);
929            max_y = max_y.max(c.y);
930        }
931        (max_x - min_x).abs().max((max_y - min_y).abs()).max(1.0)
932    };
933    let eps = 1e-12 * scale;
934
935    // Brute force for small inputs
936    if n <= 64 {
937        for i in 0..n {
938            let a1 = coords[i];
939            let a2 = coords[i + 1];
940            for j in i + 2..n {
941                let b1 = coords[j];
942                let b2 = coords[j + 1];
943                if edges_intersect_general(a1, a2, b1, b2, eps) {
944                    return true;
945                }
946            }
947        }
948        return false;
949    }
950
951    #[cfg(feature = "rstar")]
952    {
953        let tree = build_ls_edge_tree(coords);
954        for i in 0..n {
955            let a1 = coords[i];
956            let a2 = coords[i + 1];
957            let (lo_x, hi_x) = if a1.x < a2.x {
958                (a1.x, a2.x)
959            } else {
960                (a2.x, a1.x)
961            };
962            let (lo_y, hi_y) = if a1.y < a2.y {
963                (a1.y, a2.y)
964            } else {
965                (a2.y, a1.y)
966            };
967            let query = rstar::AABB::from_corners([lo_x, lo_y], [hi_x, hi_y]);
968            let found = tree.locate_in_envelope_intersecting_int(&query, |c| {
969                let j = c.idx;
970                if j <= i + 1 {
971                    return std::ops::ControlFlow::<(), ()>::Continue(());
972                }
973                let b1 = coords[j];
974                let b2 = coords[j + 1];
975                if edges_intersect_general(a1, a2, b1, b2, eps) {
976                    std::ops::ControlFlow::Break(())
977                } else {
978                    std::ops::ControlFlow::<(), ()>::Continue(())
979                }
980            });
981            if found.is_break() {
982                return true;
983            }
984        }
985        false
986    }
987    #[cfg(not(feature = "rstar"))]
988    {
989        for i in 0..n {
990            let a1 = coords[i];
991            let a2 = coords[i + 1];
992            for j in i + 2..n {
993                let b1 = coords[j];
994                let b2 = coords[j + 1];
995                if edges_intersect_general(a1, a2, b1, b2, eps) {
996                    return true;
997                }
998            }
999        }
1000        false
1001    }
1002}
1003
1004/// Check whether two LineString components have any intersecting edges.
1005pub(crate) fn check_line_components_intersect(
1006    ls1: &[Coord<f64>],
1007    ls2: &[Coord<f64>],
1008    eps: f64,
1009) -> bool {
1010    let n1 = ls1.len();
1011    let n2 = ls2.len();
1012    if n1 < 2 || n2 < 2 {
1013        return false;
1014    }
1015
1016    // Brute force when both components are small
1017    if n1.max(n2) <= 64 {
1018        for i in 0..n1 - 1 {
1019            let a1 = ls1[i];
1020            let a2 = ls1[i + 1];
1021            for j in 0..n2 - 1 {
1022                let b1 = ls2[j];
1023                let b2 = ls2[j + 1];
1024                if edges_intersect_general(a1, a2, b1, b2, eps) {
1025                    return true;
1026                }
1027            }
1028        }
1029        return false;
1030    }
1031
1032    #[cfg(feature = "rstar")]
1033    {
1034        let (small, large) = if n1 < n2 { (ls1, ls2) } else { (ls2, ls1) };
1035        let n_small = small.len();
1036        let tree = build_ls_edge_tree(large);
1037
1038        for i in 0..n_small - 1 {
1039            let a1 = small[i];
1040            let a2 = small[i + 1];
1041            let (lo_x, hi_x) = if a1.x < a2.x {
1042                (a1.x, a2.x)
1043            } else {
1044                (a2.x, a1.x)
1045            };
1046            let (lo_y, hi_y) = if a1.y < a2.y {
1047                (a1.y, a2.y)
1048            } else {
1049                (a2.y, a1.y)
1050            };
1051            let query = rstar::AABB::from_corners([lo_x, lo_y], [hi_x, hi_y]);
1052            let found = tree.locate_in_envelope_intersecting_int(&query, |c| {
1053                let b1 = large[c.idx];
1054                let b2 = large[c.idx + 1];
1055                if edges_intersect_general(a1, a2, b1, b2, eps) {
1056                    std::ops::ControlFlow::Break(())
1057                } else {
1058                    std::ops::ControlFlow::<(), ()>::Continue(())
1059                }
1060            });
1061            if found.is_break() {
1062                return true;
1063            }
1064        }
1065        false
1066    }
1067    #[cfg(not(feature = "rstar"))]
1068    {
1069        for i in 0..n1 - 1 {
1070            let a1 = ls1[i];
1071            let a2 = ls1[i + 1];
1072            for j in 0..n2 - 1 {
1073                let b1 = ls2[j];
1074                let b2 = ls2[j + 1];
1075                if edges_intersect_general(a1, a2, b1, b2, eps) {
1076                    return true;
1077                }
1078            }
1079        }
1080        false
1081    }
1082}
1083
1084impl GeoValidation for MultiLineString<f64> {
1085    type Scalar = f64;
1086
1087    fn validate(&self) -> ValidationResult {
1088        let mut errors = Vec::new();
1089        for ls in &self.0 {
1090            let r = ls.validate();
1091            if !r.valid {
1092                errors.extend(r.errors);
1093            }
1094        }
1095        // OGC Simple Features: MultiLineString must not contain duplicate linestrings
1096        if self.0.len() > 1 {
1097            let mut seen: rustc_hash::FxHashSet<Vec<(u64, u64)>> =
1098                rustc_hash::FxHashSet::with_capacity_and_hasher(self.0.len(), Default::default());
1099            for ls in &self.0 {
1100                let key: Vec<(u64, u64)> =
1101                    ls.0.iter()
1102                        .map(|c| (c.x.to_bits(), c.y.to_bits()))
1103                        .collect();
1104                if !seen.insert(key) {
1105                    errors.push(GeometryValidationError::MultiLineStringDuplicateLines);
1106                    return ValidationResult::invalid(errors);
1107                }
1108            }
1109        }
1110        // Cross-component intersection check
1111        if self.0.len() > 1 {
1112            // Compute global scale for epsilon
1113            let (mut gmin_x, mut gmax_x, mut gmin_y, mut gmax_y) =
1114                (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
1115            for ls in &self.0 {
1116                for c in &ls.0 {
1117                    gmin_x = gmin_x.min(c.x);
1118                    gmax_x = gmax_x.max(c.x);
1119                    gmin_y = gmin_y.min(c.y);
1120                    gmax_y = gmax_y.max(c.y);
1121                }
1122            }
1123            let scale = (gmax_x - gmin_x)
1124                .abs()
1125                .max((gmax_y - gmin_y).abs())
1126                .max(1.0);
1127            let eps = 1e-12 * scale;
1128
1129            for i in 0..self.0.len() {
1130                for j in (i + 1)..self.0.len() {
1131                    if check_line_components_intersect(&self.0[i].0, &self.0[j].0, eps) {
1132                        errors.push(GeometryValidationError::NotSimple);
1133                        return ValidationResult::invalid(errors);
1134                    }
1135                }
1136            }
1137        }
1138        if errors.is_empty() {
1139            ValidationResult::valid()
1140        } else {
1141            ValidationResult::invalid(errors)
1142        }
1143    }
1144}
1145
1146impl GeoValidation for Rect<f64> {
1147    type Scalar = f64;
1148
1149    fn validate(&self) -> ValidationResult {
1150        if !self.min().x.is_finite()
1151            || !self.min().y.is_finite()
1152            || !self.max().x.is_finite()
1153            || !self.max().y.is_finite()
1154        {
1155            return ValidationResult::invalid(vec![GeometryValidationError::CoordinateNaN]);
1156        }
1157        if (self.max().x - self.min().x).abs() < f64::EPSILON
1158            || (self.max().y - self.min().y).abs() < f64::EPSILON
1159        {
1160            return ValidationResult::invalid(vec![GeometryValidationError::DegenerateExterior]);
1161        }
1162        ValidationResult::valid()
1163    }
1164}
1165
1166impl GeoValidation for Triangle<f64> {
1167    type Scalar = f64;
1168
1169    fn validate(&self) -> ValidationResult {
1170        let coords = [self.v1(), self.v2(), self.v3()];
1171        if ring_has_non_finite(&coords) {
1172            return ValidationResult::invalid(vec![GeometryValidationError::CoordinateNaN]);
1173        }
1174        if coords[0] == coords[1] || coords[1] == coords[2] || coords[0] == coords[2] {
1175            return ValidationResult::invalid(vec![GeometryValidationError::DegenerateExterior]);
1176        }
1177        // Zero or near-zero area (collinear)
1178        let area = ((coords[1].x - coords[0].x) * (coords[2].y - coords[0].y)
1179            - (coords[1].y - coords[0].y) * (coords[2].x - coords[0].x))
1180            .abs();
1181        if area < 1e-12 {
1182            return ValidationResult::invalid(vec![GeometryValidationError::CollinearRing]);
1183        }
1184        ValidationResult::valid()
1185    }
1186}
1187
1188// ---------------------------------------------------------------------------
1189// Free functions — convenience wrappers around GeoValidation
1190// ---------------------------------------------------------------------------
1191
1192/// Check whether a geometry is OGC-valid.
1193///
1194/// Convenience wrapper around [`GeoValidation::is_valid`] that does not
1195/// require importing the trait.
1196pub fn is_valid(geom: &geo::Geometry<f64>) -> bool {
1197    GeoValidation::is_valid(geom)
1198}
1199
1200/// Validate a geometry, returning all OGC violations found.
1201///
1202/// Convenience wrapper around [`GeoValidation::validate`].
1203pub fn validate(geom: &geo::Geometry<f64>) -> ValidationResult {
1204    GeoValidation::validate(geom)
1205}
1206
1207/// Validate and return a human-readable description of violations.
1208///
1209/// Returns `"Valid Geometry"` when the geometry passes all checks.
1210///
1211/// Convenience wrapper around [`GeoValidation::validate_reason`].
1212pub fn validate_reason(geom: &geo::Geometry<f64>) -> String {
1213    GeoValidation::validate_reason(geom)
1214}