Skip to main content

geometry_overlay/
validity.rs

1//! OVL6.T4 — `is_valid` for rings and polygons.
2//!
3//! Mirrors `boost/geometry/algorithms/is_valid.hpp` and the failure
4//! taxonomy in `boost/geometry/algorithms/validity_failure_type.hpp`.
5//! A geometry is valid when it satisfies the OGC simple-feature rules:
6//! finite, in-range coordinates; enough points; a closed boundary; no
7//! spikes; no self-intersections; the expected ring orientation; and,
8//! for polygons, every interior ring covered by the exterior.
9//!
10//! Polygon-level ring pairs and distinct multi-polygon members are checked
11//! after their individual rings pass, including nested holes, disconnected
12//! interiors, and intersecting member interiors.
13//!
14//! Scope: `Ring`, `Polygon`, and `MultiPolygon` validation.
15//! Coordinate validity (NaN / infinity) is checked because the robustness
16//! gate depends on finite input.
17//!
18//! [`is_valid`] preserves this crate's strict behavior for compatibility.
19//! [`is_valid_with`] accepts [`ValidityOptions`], including
20//! [`ValidityOptions::BOOST_DEFAULT`] which permits consecutive repeated
21//! points like `policies/is_valid/default_policy.hpp:26-61`.
22
23use alloc::vec::Vec;
24
25use geometry_coords::CoordinateScalar;
26use geometry_cs::{CartesianFamily, CoordinateSystem};
27use geometry_model::Segment;
28use geometry_strategy::{AreaStrategy, ShoelaceArea, WithinRing, WithinStrategy};
29use geometry_tag::{MultiPolygonTag, PolygonTag, RingTag, SameAs};
30use geometry_trait::{
31    Geometry, MultiPolygon as MultiPolygonTrait, Point, PointMut, Polygon as PolygonTrait,
32    Ring as RingTrait,
33};
34
35use crate::predicate::range_guard::coordinate_in_range;
36use crate::predicate::segment_intersection::{SegmentIntersection, segment_intersection};
37
38/// Why a geometry failed [`is_valid_ring`] / [`is_valid_polygon`].
39///
40/// Mirrors Boost's complete `validity_failure_type` taxonomy
41/// (`algorithms/validity_failure_type.hpp:33-113`). The current areal
42/// validator produces the relevant ring/polygon variants; retaining the
43/// remaining categories keeps reporting stable as kind dispatch expands.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ValidityFailure {
46    /// Fewer than the 4 points a closed ring needs (3 distinct + the
47    /// repeated closing vertex). Boost's `failure_few_points`.
48    FewPoints,
49    /// Two consecutive vertices are equal. Boost's
50    /// `failure_duplicate_points`.
51    DuplicatePoints,
52    /// The ring's first and last vertices differ — it is not closed.
53    /// Boost's `failure_not_closed`.
54    NotClosed,
55    /// Two non-adjacent edges of the ring cross, or an edge touches a
56    /// non-adjacent vertex. Boost's `failure_self_intersections`.
57    SelfIntersection,
58    /// A coordinate is NaN or infinite. Boost's
59    /// `failure_invalid_coordinate`.
60    InvalidCoordinate,
61    /// An interior ring is not contained in the exterior ring. Boost's
62    /// `failure_interior_rings_outside`.
63    InteriorRingOutside,
64    /// A coordinate lies outside the safe arithmetic range
65    /// ([`SAFE_ABS_MAX`](crate::predicate::range_guard::SAFE_ABS_MAX)).
66    /// Past that magnitude the segment-intersection kernel yields
67    /// `OutOfRange` and the self-intersection test would silently miss a
68    /// real crossing, so validity cannot be confirmed. Reported as a
69    /// distinct failure rather than a bogus "valid" (there is no Boost
70    /// analogue — Boost's rescaling policy sidesteps the range limit this
71    /// no-rescale port trades for).
72    CoordinateOutOfRange,
73    /// A vertex triple folds back on itself along one line — the ring
74    /// carries a spike. Boost's `failure_spikes`.
75    Spikes,
76    /// The ring's stored vertex order contradicts its declared
77    /// [`PointOrder`](geometry_trait::PointOrder) (or the ring has zero
78    /// area, which admits no orientation). Exterior rings must traverse
79    /// in their declared order (strategy-level signed area positive);
80    /// interior rings the opposite. Boost's `failure_wrong_orientation`.
81    WrongOrientation,
82    /// One interior ring is contained by another interior ring. Boost's
83    /// `failure_nested_interior_rings`.
84    NestedInteriorRings,
85    /// Ring contacts split the polygon's filled interior into disconnected
86    /// pieces. Boost's `failure_disconnected_interior`.
87    DisconnectedInterior,
88    /// Distinct multi-polygon members overlap in area or share a boundary
89    /// curve. Boost's `failure_intersecting_interiors`.
90    IntersectingInteriors,
91    /// The geometry collapses below its declared topological dimension.
92    /// Boost's `failure_wrong_topological_dimension`.
93    WrongTopologicalDimension,
94    /// A box's maximum corner is lexicographically before its minimum corner.
95    /// Boost's `failure_wrong_corner_order`.
96    WrongCornerOrder,
97    /// Collinear vertices occur on one polyhedral-surface face. Boost's
98    /// `failure_collinear_points_on_face`.
99    CollinearPointsOnFace,
100    /// Vertices of one polyhedral-surface face are not coplanar. Boost's
101    /// `failure_non_coplanar_points_on_face`.
102    NonCoplanarPointsOnFace,
103    /// A polyhedral-surface face contains too few vertices. Boost's
104    /// `failure_few_points_on_face`.
105    FewPointsOnFace,
106    /// A polyhedral-surface edge has inconsistent face orientation. Boost's
107    /// `failure_inconsistent_orientation`.
108    InconsistentOrientation,
109    /// Polyhedral-surface faces intersect away from a shared edge. Boost's
110    /// `failure_invalid_intersection`.
111    InvalidIntersection,
112    /// Polyhedral-surface faces do not form a connected surface. Boost's
113    /// `failure_disconnected_surface`.
114    DisconnectedSurface,
115}
116
117impl ValidityFailure {
118    /// Return the stable reason prefix for this failure.
119    ///
120    /// The areal, linear, box, and coordinate strings are byte-for-byte the
121    /// messages returned by `validity_failure_type_message` in
122    /// `policies/is_valid/failing_reason_policy.hpp:32-63`. Surface messages
123    /// extend that table for the surface failure values added later in
124    /// `algorithms/validity_failure_type.hpp:91-113`.
125    #[must_use]
126    pub const fn message(self) -> &'static str {
127        match self {
128            Self::FewPoints => "Geometry has too few points",
129            Self::WrongTopologicalDimension => "Geometry has wrong topological dimension",
130            Self::Spikes => "Geometry has spikes",
131            Self::DuplicatePoints => "Geometry has duplicate (consecutive) points",
132            Self::NotClosed => "Geometry is defined as closed but is open",
133            Self::SelfIntersection => "Geometry has invalid self-intersections",
134            Self::WrongOrientation => "Geometry has wrong orientation",
135            Self::InteriorRingOutside => {
136                "Geometry has interior rings defined outside the outer boundary"
137            }
138            Self::NestedInteriorRings => "Geometry has nested interior rings",
139            Self::DisconnectedInterior => "Geometry has disconnected interior",
140            Self::IntersectingInteriors => "Multi-polygon has intersecting interiors",
141            Self::WrongCornerOrder => "Box has corners in wrong order",
142            Self::InvalidCoordinate => "Geometry has point(s) with invalid coordinate(s)",
143            Self::CoordinateOutOfRange => {
144                "Geometry has coordinate(s) outside the supported arithmetic range"
145            }
146            Self::CollinearPointsOnFace => "Geometry has collinear points on a face",
147            Self::NonCoplanarPointsOnFace => "Geometry has non-coplanar points on a face",
148            Self::FewPointsOnFace => "Geometry has too few points on a face",
149            Self::InconsistentOrientation => "Geometry has inconsistent surface orientation",
150            Self::InvalidIntersection => "Geometry has invalid face intersections",
151            Self::DisconnectedSurface => "Geometry has a disconnected surface",
152        }
153    }
154}
155
156impl core::fmt::Display for ValidityFailure {
157    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
158        formatter.write_str(self.message())
159    }
160}
161
162#[cfg(feature = "std")]
163impl std::error::Error for ValidityFailure {}
164
165/// Behavior switches applied by [`is_valid_with`].
166///
167/// Mirrors the `AllowDuplicates` and `AllowSpikes` template parameters of
168/// `policies/is_valid/default_policy.hpp:26-61`. The current validator covers
169/// areal geometries, so `allow_spikes_for_linear` is recorded for API parity
170/// but only becomes observable when linear validity dispatch is added.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct ValidityOptions {
173    allow_duplicates: bool,
174    allow_spikes_for_linear: bool,
175}
176
177impl ValidityOptions {
178    /// Existing Rust behavior: report duplicates and spikes.
179    pub const STRICT: Self = Self::new(false, false);
180
181    /// Boost's default validity behavior: permit duplicate points and spikes
182    /// in linear geometries.
183    pub const BOOST_DEFAULT: Self = Self::new(true, true);
184
185    /// Construct validity behavior from Boost's two policy switches.
186    #[must_use]
187    pub const fn new(allow_duplicates: bool, allow_spikes_for_linear: bool) -> Self {
188        Self {
189            allow_duplicates,
190            allow_spikes_for_linear,
191        }
192    }
193
194    /// Whether consecutive duplicate points are accepted.
195    #[must_use]
196    pub const fn allows_duplicates(self) -> bool {
197        self.allow_duplicates
198    }
199
200    /// Whether spikes are accepted when linear validity dispatch is used.
201    #[must_use]
202    pub const fn allows_spikes_for_linear(self) -> bool {
203        self.allow_spikes_for_linear
204    }
205}
206
207impl Default for ValidityOptions {
208    fn default() -> Self {
209        Self::STRICT
210    }
211}
212
213/// Per-kind validity implementation selected by [`is_valid`].
214///
215/// Rust tag-dispatch adapter for `boost::geometry::is_valid` from
216/// `algorithms/detail/is_valid/interface.hpp:153-203`.
217#[doc(hidden)]
218pub trait ValidityStrategy<G> {
219    fn apply(&self, geometry: &G, options: ValidityOptions) -> Result<(), ValidityFailure>;
220}
221
222/// Tag-to-validity implementation picker.
223///
224/// Rust counterpart to the geometry-kind resolution behind
225/// `algorithms/detail/is_valid/interface.hpp:153-203`.
226#[doc(hidden)]
227pub trait ValidityStrategyForKind {
228    type S: Default;
229}
230
231/// Ring validity implementation.
232///
233/// Implements the ring arm selected by the entry at
234/// `algorithms/detail/is_valid/interface.hpp:153-203`.
235#[doc(hidden)]
236#[derive(Debug, Default, Clone, Copy)]
237pub struct RingValidity;
238
239/// Polygon validity implementation.
240///
241/// Implements the polygon arm selected by the entry at
242/// `algorithms/detail/is_valid/interface.hpp:153-203`.
243#[doc(hidden)]
244#[derive(Debug, Default, Clone, Copy)]
245pub struct PolygonValidity;
246
247/// Multi-polygon validity implementation.
248///
249/// Implements the multi-polygon arm selected by the entry at
250/// `algorithms/detail/is_valid/interface.hpp:153-203`.
251#[doc(hidden)]
252#[derive(Debug, Default, Clone, Copy)]
253pub struct MultiPolygonValidity;
254
255/// Selects Boost's ring validity dispatch behind
256/// `algorithms/detail/is_valid/interface.hpp:153-203`.
257impl ValidityStrategyForKind for RingTag {
258    type S = RingValidity;
259}
260
261/// Selects Boost's polygon validity dispatch behind
262/// `algorithms/detail/is_valid/interface.hpp:153-203`.
263impl ValidityStrategyForKind for PolygonTag {
264    type S = PolygonValidity;
265}
266
267/// Selects Boost's multi-polygon validity dispatch behind
268/// `algorithms/detail/is_valid/interface.hpp:153-203`.
269impl ValidityStrategyForKind for MultiPolygonTag {
270    type S = MultiPolygonValidity;
271}
272
273/// Validate an areal geometry through its public geometry-kind tag.
274///
275/// Mirrors `boost::geometry::is_valid` from
276/// `boost/geometry/algorithms/detail/is_valid/interface.hpp:155-202`.
277/// Rings, polygons, and
278/// multi-polygons use their corresponding validators; unsupported kinds fail
279/// at compile time instead of returning an uninformative runtime value.
280///
281/// # Errors
282///
283/// Returns the first [`ValidityFailure`] detected by the selected areal
284/// validator.
285#[inline]
286#[must_use = "validity failures must be handled"]
287pub fn is_valid<G>(geometry: &G) -> Result<(), ValidityFailure>
288where
289    G: Geometry,
290    G::Kind: ValidityStrategyForKind,
291    <G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
292{
293    is_valid_with(geometry, ValidityOptions::STRICT)
294}
295
296/// Validate an areal geometry with explicit validity behavior.
297///
298/// Mirrors the policy-taking overload behind
299/// `algorithms/detail/is_valid/interface.hpp:155-202`. Use
300/// [`ValidityOptions::BOOST_DEFAULT`] to select Boost's default handling of
301/// consecutive duplicates, or [`ValidityOptions::STRICT`] for the behavior of
302/// [`is_valid`].
303///
304/// # Errors
305///
306/// Returns the first [`ValidityFailure`] not accepted by `options`.
307///
308/// # Panics
309///
310/// Panics if a custom ring implementation passes validation with a non-empty
311/// point iterator but yields no point when iterated again immediately after.
312#[inline]
313#[must_use = "validity failures must be handled"]
314pub fn is_valid_with<G>(geometry: &G, options: ValidityOptions) -> Result<(), ValidityFailure>
315where
316    G: Geometry,
317    G::Kind: ValidityStrategyForKind,
318    <G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
319{
320    <<G::Kind as ValidityStrategyForKind>::S as Default>::default().apply(geometry, options)
321}
322
323/// Return Boost's human-readable reason for strict validation.
324///
325/// This is the allocation-free Rust counterpart to the string-output overload
326/// driven by `policies/is_valid/failing_reason_policy.hpp`.
327#[inline]
328#[must_use]
329pub fn validity_reason<G>(geometry: &G) -> &'static str
330where
331    G: Geometry,
332    G::Kind: ValidityStrategyForKind,
333    <G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
334{
335    validity_reason_with(geometry, ValidityOptions::STRICT)
336}
337
338/// Return Boost's human-readable reason using explicit validity behavior.
339#[inline]
340#[must_use]
341pub fn validity_reason_with<G>(geometry: &G, options: ValidityOptions) -> &'static str
342where
343    G: Geometry,
344    G::Kind: ValidityStrategyForKind,
345    <G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
346{
347    match is_valid_with(geometry, options) {
348        Ok(()) => "Geometry is valid",
349        Err(failure) => failure.message(),
350    }
351}
352
353/// Implements the ring validity arm selected by
354/// `algorithms/detail/is_valid/interface.hpp:153-203`.
355impl<G, P> ValidityStrategy<G> for RingValidity
356where
357    G: RingTrait<Point = P>,
358    P: PointMut + Default + Copy,
359    P::Scalar: CoordinateScalar + Into<f64>,
360    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
361{
362    fn apply(&self, ring: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
363        is_valid_ring_with(ring, options)
364    }
365}
366
367/// Implements the polygon validity arm selected by
368/// `algorithms/detail/is_valid/interface.hpp:153-203`.
369impl<G, P> ValidityStrategy<G> for PolygonValidity
370where
371    G: PolygonTrait<Point = P>,
372    P: PointMut + Default + Copy,
373    P::Scalar: CoordinateScalar + Into<f64>,
374    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
375{
376    fn apply(&self, polygon: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
377        is_valid_polygon_with(polygon, options)
378    }
379}
380
381/// Implements the multi-polygon validity arm selected by
382/// `algorithms/detail/is_valid/interface.hpp:153-203`.
383impl<G, P> ValidityStrategy<G> for MultiPolygonValidity
384where
385    G: MultiPolygonTrait<Point = P>,
386    P: PointMut + Default + Copy,
387    P::Scalar: CoordinateScalar + Into<f64>,
388    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
389{
390    fn apply(&self, multi_polygon: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
391        let polygons: Vec<_> = multi_polygon.polygons().collect();
392        for polygon in &polygons {
393            is_valid_polygon_with(*polygon, options)?;
394        }
395        for first in 0..polygons.len() {
396            for second in (first + 1)..polygons.len() {
397                let matrix = crate::relate::relate(polygons[first], polygons[second])
398                    .map_err(|_| ValidityFailure::SelfIntersection)?;
399                if matrix.interior_interior() == crate::relate::Dimension::Area
400                    || matrix.boundary_boundary() == crate::relate::Dimension::Curve
401                {
402                    return Err(ValidityFailure::IntersectingInteriors);
403                }
404            }
405        }
406        Ok(())
407    }
408}
409
410/// Validate a single ring.
411///
412/// Checks point count, closure, coordinate finiteness, that no two
413/// non-adjacent edges intersect, that no vertex triple is a spike, and
414/// that the ring is wound in its declared order. Returns `Ok(())` for a
415/// valid ring.
416///
417/// Mirrors the ring arm of `boost::geometry::is_valid`
418/// (`algorithms/is_valid.hpp`, via `detail/is_valid/ring.hpp`).
419///
420/// # Errors
421///
422/// Returns a [`ValidityFailure`] describing the first rule the ring violates,
423/// including [`ValidityFailure::Spikes`] and
424/// [`ValidityFailure::WrongOrientation`].
425///
426/// # Examples
427///
428/// ```
429/// use geometry_cs::Cartesian;
430/// use geometry_model::{Point2D, Ring};
431/// use geometry_overlay::validity::is_valid_ring;
432///
433/// type P = Point2D<f64, Cartesian>;
434/// let square: Ring<P> = Ring::from_vec(vec![
435///     P::new(0.0, 0.0), P::new(0.0, 1.0), P::new(1.0, 1.0), P::new(1.0, 0.0), P::new(0.0, 0.0),
436/// ]);
437/// assert!(is_valid_ring(&square).is_ok());
438/// ```
439#[inline]
440#[must_use = "validity failures must be handled"]
441pub fn is_valid_ring<R, P>(ring: &R) -> Result<(), ValidityFailure>
442where
443    R: RingTrait<Point = P>,
444    P: PointMut + Default + Copy,
445    P::Scalar: CoordinateScalar + Into<f64>,
446    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
447{
448    is_valid_ring_with(ring, ValidityOptions::STRICT)
449}
450
451/// Validate one ring with explicit validity behavior.
452///
453/// # Errors
454///
455/// Returns the first [`ValidityFailure`] not accepted by `options`.
456#[inline]
457#[must_use = "validity failures must be handled"]
458pub fn is_valid_ring_with<R, P>(ring: &R, options: ValidityOptions) -> Result<(), ValidityFailure>
459where
460    R: RingTrait<Point = P>,
461    P: PointMut + Default + Copy,
462    P::Scalar: CoordinateScalar + Into<f64>,
463    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
464{
465    validate_ring(ring, false, options)
466}
467
468/// Shared ring validation. `is_interior` flips the orientation
469/// expectation: an exterior ring traverses in its declared order
470/// (strategy-level `ShoelaceArea` positive); an interior ring winds
471/// opposite (negative) — mirroring Boost's
472/// `is_properly_oriented<Ring, IsInteriorRing>`.
473fn validate_ring<R, P>(
474    ring: &R,
475    is_interior: bool,
476    options: ValidityOptions,
477) -> Result<(), ValidityFailure>
478where
479    R: RingTrait<Point = P>,
480    P: PointMut + Default + Copy,
481    P::Scalar: CoordinateScalar + Into<f64>,
482    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
483{
484    let mut pts: Vec<P> = ring.points().copied().collect();
485
486    // Coordinate finiteness.
487    for p in &pts {
488        let x: f64 = p.get::<0>().into();
489        let y: f64 = p.get::<1>().into();
490        if !x.is_finite() || !y.is_finite() {
491            return Err(ValidityFailure::InvalidCoordinate);
492        }
493    }
494
495    // Boost's default policy accepts consecutive duplicates. Remove them
496    // before count, spike, intersection, and orientation checks so a
497    // zero-length edge cannot create a secondary failure.
498    if options.allows_duplicates() {
499        let mut deduplicated = Vec::with_capacity(pts.len());
500        for point in pts {
501            if !deduplicated
502                .last()
503                .is_some_and(|previous| same_point(previous, &point))
504            {
505                deduplicated.push(point);
506            }
507        }
508        pts = deduplicated;
509    }
510
511    // Out-of-range coordinates: the self-intersection test below routes
512    // each edge pair through the segment-intersection kernel, which drops
513    // any crossing at coordinates past ±SAFE_ABS_MAX as `OutOfRange`. A
514    // genuinely self-intersecting ring would then pass unnoticed, so
515    // validity cannot be confirmed — refuse rather than claim valid.
516    for p in &pts {
517        if !coordinate_in_range(p) {
518            return Err(ValidityFailure::CoordinateOutOfRange);
519        }
520    }
521
522    // A closed ring needs at least 4 vertices (triangle + closing point).
523    if pts.len() < 4 {
524        return Err(ValidityFailure::FewPoints);
525    }
526
527    // First and last vertex must coincide.
528    if !same_point(&pts[0], &pts[pts.len() - 1]) {
529        return Err(ValidityFailure::NotClosed);
530    }
531
532    if !options.allows_duplicates() && pts.windows(2).any(|pair| same_point(&pair[0], &pair[1])) {
533        return Err(ValidityFailure::DuplicatePoints);
534    }
535
536    // A vertex triple that folds back on itself along one line — Boost's
537    // failure_spikes. Checked before self-intersection so spike inputs
538    // report a deterministic error (matches Boost's check order).
539    if has_spike(&pts) {
540        return Err(ValidityFailure::Spikes);
541    }
542
543    // No non-adjacent edge may intersect another.
544    if has_self_intersection(&pts) {
545        return Err(ValidityFailure::SelfIntersection);
546    }
547
548    // Orientation — Boost's failure_wrong_orientation. The strategy area
549    // already folds the declared PointOrder: a correctly wound exterior
550    // is positive, a correctly wound hole negative. Zero area
551    // (degenerate) fails either way.
552    let area = ShoelaceArea.area(ring);
553    let zero = <P::Scalar as CoordinateScalar>::ZERO;
554    let properly_oriented = if is_interior {
555        area < zero
556    } else {
557        area > zero
558    };
559    if !properly_oriented {
560        return Err(ValidityFailure::WrongOrientation);
561    }
562
563    Ok(())
564}
565
566/// Validate a polygon: its exterior ring (with exterior orientation
567/// expectations), each interior ring (with interior orientation
568/// expectations), and all exterior/interior and interior/interior ring-pair
569/// topology constraints.
570///
571/// Mirrors the polygon arm of `boost::geometry::is_valid`
572/// (`detail/is_valid/polygon.hpp`).
573///
574/// # Errors
575///
576/// Returns a [`ValidityFailure`] describing the first rule the polygon
577/// violates, including failures for outside, nested, crossing, or
578/// disconnecting interior rings.
579///
580/// # Examples
581///
582/// ```
583/// use geometry_cs::Cartesian;
584/// use geometry_model::{polygon, Point2D, Polygon};
585/// use geometry_overlay::validity::is_valid_polygon;
586///
587/// type P = Point2D<f64, Cartesian>;
588/// let pg: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)]];
589/// assert!(is_valid_polygon(&pg).is_ok());
590/// ```
591#[inline]
592#[must_use = "validity failures must be handled"]
593pub fn is_valid_polygon<G, P>(polygon: &G) -> Result<(), ValidityFailure>
594where
595    G: PolygonTrait<Point = P>,
596    P: PointMut + Default + Copy,
597    P::Scalar: CoordinateScalar + Into<f64>,
598    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
599{
600    is_valid_polygon_with(polygon, ValidityOptions::STRICT)
601}
602
603/// Validate one polygon with explicit validity behavior.
604///
605/// # Errors
606///
607/// Returns the first [`ValidityFailure`] not accepted by `options`.
608///
609/// # Panics
610///
611/// Panics if a custom ring implementation passes validation with a non-empty
612/// point iterator but yields no point when iterated again immediately after.
613#[inline]
614#[must_use = "validity failures must be handled"]
615pub fn is_valid_polygon_with<G, P>(
616    polygon: &G,
617    options: ValidityOptions,
618) -> Result<(), ValidityFailure>
619where
620    G: PolygonTrait<Point = P>,
621    P: PointMut + Default + Copy,
622    P::Scalar: CoordinateScalar + Into<f64>,
623    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
624{
625    validate_ring(polygon.exterior(), false, options)?;
626    let inners: Vec<_> = polygon.interiors().collect();
627    for inner in &inners {
628        validate_ring(*inner, true, options)?;
629        let rep = inner
630            .points()
631            .next()
632            .expect("a validated ring contains at least four points");
633        if !WithinRing.covered_by(rep, polygon.exterior()) {
634            return Err(ValidityFailure::InteriorRingOutside);
635        }
636        let interaction = ring_pair_interaction(polygon.exterior(), *inner);
637        if interaction.proper_crossing {
638            return Err(ValidityFailure::SelfIntersection);
639        }
640        if interaction.overlap || interaction.contacts.len() > 1 {
641            return Err(ValidityFailure::DisconnectedInterior);
642        }
643    }
644
645    for first in 0..inners.len() {
646        for second in (first + 1)..inners.len() {
647            let interaction = ring_pair_interaction(inners[first], inners[second]);
648            if interaction.proper_crossing || interaction.overlap {
649                return Err(ValidityFailure::SelfIntersection);
650            }
651            if interaction.contacts.len() > 1 {
652                return Err(ValidityFailure::DisconnectedInterior);
653            }
654            let nested = interaction.contacts.is_empty()
655                && (ring_first_point_within(inners[first], inners[second])
656                    || ring_first_point_within(inners[second], inners[first]));
657            (!nested)
658                .then_some(())
659                .ok_or(ValidityFailure::NestedInteriorRings)?;
660        }
661    }
662    Ok(())
663}
664
665#[derive(Default)]
666struct RingPairInteraction<P> {
667    proper_crossing: bool,
668    overlap: bool,
669    contacts: Vec<P>,
670}
671
672fn ring_pair_interaction<R1, R2, P>(first: &R1, second: &R2) -> RingPairInteraction<P>
673where
674    R1: RingTrait<Point = P>,
675    R2: RingTrait<Point = P>,
676    P: PointMut + Default + Copy,
677    P::Scalar: CoordinateScalar + Into<f64>,
678{
679    let first_points: Vec<P> = first.points().copied().collect();
680    let second_points: Vec<P> = second.points().copied().collect();
681    let mut interaction = RingPairInteraction::default();
682    for first_pair in first_points.windows(2) {
683        let first_segment = Segment::new(first_pair[0], first_pair[1]);
684        for second_pair in second_points.windows(2) {
685            let second_segment = Segment::new(second_pair[0], second_pair[1]);
686            match segment_intersection(&first_segment, &second_segment) {
687                SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
688                SegmentIntersection::Collinear { .. } => interaction.overlap = true,
689                SegmentIntersection::Single(point) => {
690                    let endpoint_contact = first_pair.iter().any(|value| same_point(value, &point))
691                        || second_pair.iter().any(|value| same_point(value, &point));
692                    if !endpoint_contact {
693                        interaction.proper_crossing = true;
694                    }
695                    if !interaction
696                        .contacts
697                        .iter()
698                        .any(|value| same_point(value, &point))
699                    {
700                        interaction.contacts.push(point);
701                    }
702                }
703            }
704        }
705    }
706    interaction
707}
708
709fn ring_first_point_within<R1, R2, P>(inner: &R1, outer: &R2) -> bool
710where
711    R1: RingTrait<Point = P>,
712    R2: RingTrait<Point = P>,
713    P: Point,
714    P::Scalar: CoordinateScalar,
715    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
716{
717    inner
718        .points()
719        .next()
720        .is_some_and(|point| WithinRing.within(point, outer))
721}
722
723/// Whether any two non-adjacent edges of the vertex ring intersect. The
724/// closing edge (last→first) is represented by the repeated final
725/// vertex, so edges are the `pts[i] → pts[i+1]` pairs.
726fn has_self_intersection<P>(pts: &[P]) -> bool
727where
728    P: PointMut + Default + Copy,
729    P::Scalar: CoordinateScalar + Into<f64>,
730{
731    let n = pts.len();
732    // Edges: 0..n-1 (the last vertex repeats the first, closing the ring).
733    let edges = n - 1;
734    for i in 0..edges {
735        let a = Segment::new(pts[i], pts[i + 1]);
736        for j in (i + 1)..edges {
737            // Skip edges that share a vertex (adjacent, or the
738            // wrap-around pair of the first and last edge).
739            if j == i + 1 {
740                continue;
741            }
742            if i == 0 && j == edges - 1 {
743                continue;
744            }
745            let b = Segment::new(pts[j], pts[j + 1]);
746            match segment_intersection::<Segment<P>, P>(&a, &b) {
747                SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
748                _ => return true,
749            }
750        }
751    }
752    false
753}
754
755fn same_point<P: Point>(a: &P, b: &P) -> bool
756where
757    P::Scalar: PartialEq,
758{
759    a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
760}
761
762/// `true` iff `b` is a spike between `a` and `c`: collinear
763/// (`cross == 0`) and folding back (`dot < 0`). Same 2-D kernel as
764/// `geometry_algorithm::remove_spikes::is_spike_2d` (private there);
765/// duplicated locally rather than widening that crate's public
766/// surface — if a third consumer appears, hoist the predicate into a
767/// shared home per the aggregate-slicing rules.
768fn is_spike_triple<P: Point>(a: &P, b: &P, c: &P) -> bool
769where
770    P::Scalar: CoordinateScalar,
771{
772    let ux = b.get::<0>() - a.get::<0>();
773    let uy = b.get::<1>() - a.get::<1>();
774    let vx = c.get::<0>() - b.get::<0>();
775    let vy = c.get::<1>() - b.get::<1>();
776    let zero = <P::Scalar as CoordinateScalar>::ZERO;
777    ux * vy - uy * vx == zero && ux * vx + uy * vy < zero
778}
779
780/// Any spike anywhere on the closed ring cycle, seam included.
781/// `pts` is the stored sequence (closing duplicate present — the
782/// closure check has already passed). The walk drops the duplicate
783/// and indexes the remaining cycle modularly, so triples
784/// `(last-1, last, first)` and `(last, first, second)` are covered.
785fn has_spike<P: Point + Copy>(pts: &[P]) -> bool
786where
787    P::Scalar: CoordinateScalar,
788{
789    // pts.len() >= 4 and pts[0] == pts[len-1] are guaranteed by the
790    // earlier FewPoints / NotClosed checks.
791    let cycle = &pts[..pts.len() - 1];
792    let n = cycle.len(); // >= 3
793    (0..n).any(|i| is_spike_triple(&cycle[(i + n - 1) % n], &cycle[i], &cycle[(i + 1) % n]))
794}
795
796#[cfg(test)]
797mod tests {
798    //! OVL6.T4 done-when: valid / invalid rings and polygons. Mirrors
799    //! the case families in `test/algorithms/is_valid.cpp`.
800
801    use super::{ValidityFailure, is_valid_polygon, is_valid_ring};
802    use geometry_cs::Cartesian;
803    use geometry_model::{Point2D, Polygon, Ring, polygon};
804
805    type P = Point2D<f64, Cartesian>;
806
807    #[test]
808    fn valid_square_ring() {
809        let r: Ring<P> = Ring::from_vec(vec![
810            P::new(0.0, 0.0),
811            P::new(0.0, 1.0),
812            P::new(1.0, 1.0),
813            P::new(1.0, 0.0),
814            P::new(0.0, 0.0),
815        ]);
816        assert!(is_valid_ring(&r).is_ok());
817    }
818
819    #[test]
820    fn too_few_points() {
821        let r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 0.0), P::new(0.0, 0.0)]);
822        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::FewPoints));
823    }
824
825    #[test]
826    fn out_of_range_self_intersection_is_not_reported_valid() {
827        // Regression: a self-crossing "bow-tie" ring at coordinates past
828        // ±2^26 had its crossing dropped as OutOfRange by the segment
829        // kernel, so `has_self_intersection` returned false and the ring
830        // was wrongly reported valid. The same shape in range is correctly
831        // SelfIntersection; out of range it must be CoordinateOutOfRange,
832        // never Ok.
833        let s = 2.0e14;
834        let huge_bowtie: Ring<P> = Ring::from_vec(vec![
835            P::new(0.0, 0.0),
836            P::new(s, s),
837            P::new(s, 0.0),
838            P::new(0.0, s),
839            P::new(0.0, 0.0),
840        ]);
841        assert_eq!(
842            is_valid_ring(&huge_bowtie),
843            Err(ValidityFailure::CoordinateOutOfRange)
844        );
845        // The in-range analogue is still caught as a self-intersection.
846        let small_bowtie: Ring<P> = Ring::from_vec(vec![
847            P::new(0.0, 0.0),
848            P::new(2.0, 2.0),
849            P::new(2.0, 0.0),
850            P::new(0.0, 2.0),
851            P::new(0.0, 0.0),
852        ]);
853        assert_eq!(
854            is_valid_ring(&small_bowtie),
855            Err(ValidityFailure::SelfIntersection)
856        );
857    }
858
859    #[test]
860    fn not_closed() {
861        let r: Ring<P> = Ring::from_vec(vec![
862            P::new(0.0, 0.0),
863            P::new(1.0, 0.0),
864            P::new(1.0, 1.0),
865            P::new(0.0, 1.0),
866        ]);
867        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::NotClosed));
868    }
869
870    #[test]
871    fn self_intersecting_bowtie() {
872        // A "bow-tie" quadrilateral whose diagonals cross.
873        let r: Ring<P> = Ring::from_vec(vec![
874            P::new(0.0, 0.0),
875            P::new(2.0, 2.0),
876            P::new(2.0, 0.0),
877            P::new(0.0, 2.0),
878            P::new(0.0, 0.0),
879        ]);
880        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::SelfIntersection));
881    }
882
883    #[test]
884    fn invalid_coordinate() {
885        let r: Ring<P> = Ring::from_vec(vec![
886            P::new(0.0, 0.0),
887            P::new(f64::NAN, 0.0),
888            P::new(1.0, 1.0),
889            P::new(0.0, 0.0),
890        ]);
891        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::InvalidCoordinate));
892    }
893
894    #[test]
895    fn valid_polygon() {
896        let pg: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)]];
897        assert!(is_valid_polygon(&pg).is_ok());
898    }
899
900    #[test]
901    fn valid_polygon_with_hole() {
902        let pg: Polygon<P> = polygon![
903            [
904                (0.0, 0.0),
905                (0.0, 10.0),
906                (10.0, 10.0),
907                (10.0, 0.0),
908                (0.0, 0.0)
909            ],
910            [(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]
911        ];
912        assert!(is_valid_polygon(&pg).is_ok());
913    }
914
915    #[test]
916    fn wrongly_oriented_ring_is_rejected() {
917        // CW-declared ring stored counter-clockwise. Boost:
918        // failure_wrong_orientation.
919        let r: Ring<P> = Ring::from_vec(vec![
920            P::new(0.0, 0.0),
921            P::new(2.0, 0.0),
922            P::new(2.0, 2.0),
923            P::new(0.0, 2.0),
924            P::new(0.0, 0.0),
925        ]);
926        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::WrongOrientation));
927    }
928
929    #[test]
930    fn ccw_declared_ring_correctly_wound_is_ok() {
931        // CCW-declared ring stored counter-clockwise: strategy-level
932        // area positive, valid. Locks the convention shared with
933        // `correct()` (spec correct-orientation).
934        let r: Ring<P, false> = Ring::from_vec(vec![
935            P::new(0.0, 0.0),
936            P::new(2.0, 0.0),
937            P::new(2.0, 2.0),
938            P::new(0.0, 2.0),
939            P::new(0.0, 0.0),
940        ]);
941        assert!(is_valid_ring(&r).is_ok());
942    }
943
944    #[test]
945    fn all_collinear_ring_is_spikes() {
946        // The finding's repro: a "ring" that is a line. Every edge
947        // pair is adjacent or the wrap pair, so the old validator
948        // reported Ok. Boost: failure_spikes.
949        let flat: Ring<P> = Ring::from_vec(vec![
950            P::new(0.0, 0.0),
951            P::new(4.0, 0.0),
952            P::new(2.0, 0.0),
953            P::new(0.0, 0.0),
954        ]);
955        assert_eq!(is_valid_ring(&flat), Err(ValidityFailure::Spikes));
956    }
957
958    #[test]
959    fn square_with_spike_is_spikes() {
960        // A CW square with an out-and-back spur on its bottom edge.
961        // Both Spikes and SelfIntersection are arguably present; the
962        // pipeline order pins Spikes (matches Boost's check order).
963        let r: Ring<P> = Ring::from_vec(vec![
964            P::new(0.0, 0.0),
965            P::new(0.0, 4.0),
966            P::new(4.0, 4.0),
967            P::new(4.0, 0.0),
968            P::new(2.0, 0.0),
969            P::new(2.0, -2.0),
970            P::new(2.0, 0.0),
971            P::new(0.0, 0.0),
972        ]);
973        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::Spikes));
974    }
975
976    #[test]
977    fn hole_outside_exterior_is_rejected() {
978        // The finding's repro. Doc promised InteriorRingOutside; the
979        // variant was unreachable. Boost:
980        // failure_interior_rings_outside. (Exterior CW-stored, hole
981        // CCW-stored — both correctly wound, so orientation passes and
982        // containment is what fails.)
983        let pg: Polygon<P> = polygon![
984            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
985            [
986                (10.0, 10.0),
987                (12.0, 10.0),
988                (12.0, 12.0),
989                (10.0, 12.0),
990                (10.0, 10.0)
991            ]
992        ];
993        assert_eq!(
994            is_valid_polygon(&pg),
995            Err(ValidityFailure::InteriorRingOutside)
996        );
997    }
998
999    #[test]
1000    fn hole_touching_exterior_boundary_is_ok() {
1001        // The hole's first vertex lies ON the exterior boundary:
1002        // covered_by (not within) is the containment predicate, so an
1003        // isolated touch is permitted — matching Boost.
1004        let pg: Polygon<P> = polygon![
1005            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
1006            [(0.0, 2.0), (1.0, 1.0), (1.0, 3.0), (0.0, 2.0)]
1007        ];
1008        assert!(is_valid_polygon(&pg).is_ok());
1009    }
1010
1011    #[test]
1012    fn wrongly_oriented_hole_is_rejected() {
1013        // Correct CW exterior, but the hole is ALSO CW-stored — holes
1014        // must wind opposite. Boost: failure_wrong_orientation.
1015        let pg: Polygon<P> = polygon![
1016            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
1017            [(1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0)]
1018        ];
1019        assert_eq!(
1020            is_valid_polygon(&pg),
1021            Err(ValidityFailure::WrongOrientation)
1022        );
1023    }
1024}