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                let interiors_overlap =
400                    matrix.interior_interior() == crate::relate::Dimension::Area;
401                let boundaries = matrix.boundary_boundary();
402
403                // Boost distinguishes two ways members can be wrong, and
404                // tilemaker's `buildWayGeometry` branches on which:
405                //
406                //   overlapping members     failure=21  boundaries cross
407                //   edge-touching members   failure=21  boundaries share a curve
408                //   identical members       failure=21
409                //   one inside another      failure=40  interiors overlap, boundaries do not meet
410                //   point-touching members  valid
411                //   disjoint members        valid
412                //
413                // So a boundary that meets the other at all — sharing a curve,
414                // or crossing it while the interiors overlap — is
415                // `failure_self_intersections`, and only genuine nesting is
416                // `failure_intersecting_interiors`.
417                if boundaries == crate::relate::Dimension::Curve {
418                    return Err(ValidityFailure::SelfIntersection);
419                }
420                if interiors_overlap {
421                    return Err(if boundaries == crate::relate::Dimension::Empty {
422                        ValidityFailure::IntersectingInteriors
423                    } else {
424                        ValidityFailure::SelfIntersection
425                    });
426                }
427            }
428        }
429        Ok(())
430    }
431}
432
433/// Validate a single ring.
434///
435/// Checks point count, closure, coordinate finiteness, that no two
436/// non-adjacent edges intersect, that no vertex triple is a spike, and
437/// that the ring is wound in its declared order. Returns `Ok(())` for a
438/// valid ring.
439///
440/// Mirrors the ring arm of `boost::geometry::is_valid`
441/// (`algorithms/is_valid.hpp`, via `detail/is_valid/ring.hpp`).
442///
443/// # Errors
444///
445/// Returns a [`ValidityFailure`] describing the first rule the ring violates,
446/// including [`ValidityFailure::Spikes`] and
447/// [`ValidityFailure::WrongOrientation`].
448///
449/// # Examples
450///
451/// ```
452/// use geometry_cs::Cartesian;
453/// use geometry_model::{Point2D, Ring};
454/// use geometry_overlay::validity::is_valid_ring;
455///
456/// type P = Point2D<f64, Cartesian>;
457/// let square: Ring<P> = Ring::from_vec(vec![
458///     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),
459/// ]);
460/// assert!(is_valid_ring(&square).is_ok());
461/// ```
462#[inline]
463#[must_use = "validity failures must be handled"]
464pub fn is_valid_ring<R, P>(ring: &R) -> Result<(), ValidityFailure>
465where
466    R: RingTrait<Point = P>,
467    P: PointMut + Default + Copy,
468    P::Scalar: CoordinateScalar + Into<f64>,
469    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
470{
471    is_valid_ring_with(ring, ValidityOptions::STRICT)
472}
473
474/// Validate one ring with explicit validity behavior.
475///
476/// # Errors
477///
478/// Returns the first [`ValidityFailure`] not accepted by `options`.
479#[inline]
480#[must_use = "validity failures must be handled"]
481pub fn is_valid_ring_with<R, P>(ring: &R, options: ValidityOptions) -> Result<(), ValidityFailure>
482where
483    R: RingTrait<Point = P>,
484    P: PointMut + Default + Copy,
485    P::Scalar: CoordinateScalar + Into<f64>,
486    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
487{
488    validate_ring(ring, false, options)
489}
490
491/// Shared ring validation. `is_interior` flips the orientation
492/// expectation: an exterior ring traverses in its declared order
493/// (strategy-level `ShoelaceArea` positive); an interior ring winds
494/// opposite (negative) — mirroring Boost's
495/// `is_properly_oriented<Ring, IsInteriorRing>`.
496fn validate_ring<R, P>(
497    ring: &R,
498    is_interior: bool,
499    options: ValidityOptions,
500) -> Result<(), ValidityFailure>
501where
502    R: RingTrait<Point = P>,
503    P: PointMut + Default + Copy,
504    P::Scalar: CoordinateScalar + Into<f64>,
505    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
506{
507    let mut pts: Vec<P> = ring.points().copied().collect();
508
509    // Coordinate finiteness.
510    for p in &pts {
511        let x: f64 = p.get::<0>().into();
512        let y: f64 = p.get::<1>().into();
513        if !x.is_finite() || !y.is_finite() {
514            return Err(ValidityFailure::InvalidCoordinate);
515        }
516    }
517
518    // Boost's default policy accepts consecutive duplicates. Remove them
519    // before count, spike, intersection, and orientation checks so a
520    // zero-length edge cannot create a secondary failure.
521    if options.allows_duplicates() {
522        let mut deduplicated = Vec::with_capacity(pts.len());
523        for point in pts {
524            if !deduplicated
525                .last()
526                .is_some_and(|previous| same_point(previous, &point))
527            {
528                deduplicated.push(point);
529            }
530        }
531        pts = deduplicated;
532    }
533
534    // Out-of-range coordinates: the self-intersection test below routes
535    // each edge pair through the segment-intersection kernel, which drops
536    // any crossing at coordinates past ±SAFE_ABS_MAX as `OutOfRange`. A
537    // genuinely self-intersecting ring would then pass unnoticed, so
538    // validity cannot be confirmed — refuse rather than claim valid.
539    for p in &pts {
540        if !coordinate_in_range(p) {
541            return Err(ValidityFailure::CoordinateOutOfRange);
542        }
543    }
544
545    // A closed ring needs at least 4 vertices (triangle + closing point).
546    if pts.len() < 4 {
547        return Err(ValidityFailure::FewPoints);
548    }
549
550    // First and last vertex must coincide.
551    if !same_point(&pts[0], &pts[pts.len() - 1]) {
552        return Err(ValidityFailure::NotClosed);
553    }
554
555    if !options.allows_duplicates() && pts.windows(2).any(|pair| same_point(&pair[0], &pair[1])) {
556        return Err(ValidityFailure::DuplicatePoints);
557    }
558
559    // A vertex triple that folds back on itself along one line — Boost's
560    // failure_spikes. Checked before self-intersection so spike inputs
561    // report a deterministic error (matches Boost's check order).
562    if has_spike(&pts) {
563        return Err(ValidityFailure::Spikes);
564    }
565
566    // Orientation — Boost's failure_wrong_orientation. The strategy area
567    // already folds the declared PointOrder: a correctly wound exterior
568    // is positive, a correctly wound hole negative. Zero area
569    // (degenerate) fails either way.
570    //
571    // Checked *before* self-intersection, because that is the order Boost
572    // reports in: a counter-clockwise ring that also crosses itself comes
573    // back `failure_wrong_orientation`, and only a correctly wound ring
574    // goes on to be reported as `failure_self_intersections`. Spikes still
575    // come first — a wrongly wound ring carrying a spike is
576    // `failure_spikes`. Verified against Boost 1.83; see
577    // `orientation_is_reported_before_self_intersection`.
578    let area = ShoelaceArea.area(ring);
579    let zero = <P::Scalar as CoordinateScalar>::ZERO;
580    let properly_oriented = if is_interior {
581        area < zero
582    } else {
583        area > zero
584    };
585    if !properly_oriented {
586        return Err(ValidityFailure::WrongOrientation);
587    }
588
589    // No non-adjacent edge may intersect another.
590    if has_self_intersection(&pts) {
591        return Err(ValidityFailure::SelfIntersection);
592    }
593
594    Ok(())
595}
596
597/// Validate a polygon: its exterior ring (with exterior orientation
598/// expectations), each interior ring (with interior orientation
599/// expectations), and all exterior/interior and interior/interior ring-pair
600/// topology constraints.
601///
602/// Mirrors the polygon arm of `boost::geometry::is_valid`
603/// (`detail/is_valid/polygon.hpp`).
604///
605/// # Errors
606///
607/// Returns a [`ValidityFailure`] describing the first rule the polygon
608/// violates, including failures for outside, nested, crossing, or
609/// disconnecting interior rings.
610///
611/// # Examples
612///
613/// ```
614/// use geometry_cs::Cartesian;
615/// use geometry_model::{polygon, Point2D, Polygon};
616/// use geometry_overlay::validity::is_valid_polygon;
617///
618/// type P = Point2D<f64, Cartesian>;
619/// 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)]];
620/// assert!(is_valid_polygon(&pg).is_ok());
621/// ```
622#[inline]
623#[must_use = "validity failures must be handled"]
624pub fn is_valid_polygon<G, P>(polygon: &G) -> Result<(), ValidityFailure>
625where
626    G: PolygonTrait<Point = P>,
627    P: PointMut + Default + Copy,
628    P::Scalar: CoordinateScalar + Into<f64>,
629    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
630{
631    is_valid_polygon_with(polygon, ValidityOptions::STRICT)
632}
633
634/// Validate one polygon with explicit validity behavior.
635///
636/// # Errors
637///
638/// Returns the first [`ValidityFailure`] not accepted by `options`.
639///
640/// # Panics
641///
642/// Panics if a custom ring implementation passes validation with a non-empty
643/// point iterator but yields no point when iterated again immediately after.
644#[inline]
645#[must_use = "validity failures must be handled"]
646pub fn is_valid_polygon_with<G, P>(
647    polygon: &G,
648    options: ValidityOptions,
649) -> Result<(), ValidityFailure>
650where
651    G: PolygonTrait<Point = P>,
652    P: PointMut + Default + Copy,
653    P::Scalar: CoordinateScalar + Into<f64>,
654    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
655{
656    validate_ring(polygon.exterior(), false, options)?;
657    let inners: Vec<_> = polygon.interiors().collect();
658    for inner in &inners {
659        validate_ring(*inner, true, options)?;
660        let rep = inner
661            .points()
662            .next()
663            .expect("a validated ring contains at least four points");
664        if !WithinRing.covered_by(rep, polygon.exterior()) {
665            return Err(ValidityFailure::InteriorRingOutside);
666        }
667        let interaction = ring_pair_interaction(polygon.exterior(), *inner);
668        // A hole that shares a *curve* with the exterior is a
669        // self-intersection, the same way two holes sharing one is (below).
670        // Only isolated contacts that cut the interior in two are
671        // `failure_disconnected_interior`. Boost 1.83, exterior
672        // (0,0)-(10,10) clockwise:
673        //
674        //   hole fully interior                        valid
675        //   hole touching the exterior at one point    valid
676        //   hole touching it at two points             failure=32
677        //   hole sharing a segment of it               failure=21
678        if interaction.proper_crossing || interaction.overlap {
679            return Err(ValidityFailure::SelfIntersection);
680        }
681        if interaction.contacts.len() > 1 {
682            return Err(ValidityFailure::DisconnectedInterior);
683        }
684    }
685
686    for first in 0..inners.len() {
687        for second in (first + 1)..inners.len() {
688            let interaction = ring_pair_interaction(inners[first], inners[second]);
689            if interaction.proper_crossing || interaction.overlap {
690                return Err(ValidityFailure::SelfIntersection);
691            }
692            if interaction.contacts.len() > 1 {
693                return Err(ValidityFailure::DisconnectedInterior);
694            }
695            let nested = interaction.contacts.is_empty()
696                && (ring_first_point_within(inners[first], inners[second])
697                    || ring_first_point_within(inners[second], inners[first]));
698            (!nested)
699                .then_some(())
700                .ok_or(ValidityFailure::NestedInteriorRings)?;
701        }
702    }
703    Ok(())
704}
705
706#[derive(Default)]
707struct RingPairInteraction<P> {
708    proper_crossing: bool,
709    overlap: bool,
710    contacts: Vec<P>,
711}
712
713fn ring_pair_interaction<R1, R2, P>(first: &R1, second: &R2) -> RingPairInteraction<P>
714where
715    R1: RingTrait<Point = P>,
716    R2: RingTrait<Point = P>,
717    P: PointMut + Default + Copy,
718    P::Scalar: CoordinateScalar + Into<f64>,
719{
720    let first_points: Vec<P> = first.points().copied().collect();
721    let second_points: Vec<P> = second.points().copied().collect();
722    let mut interaction = RingPairInteraction::default();
723    for first_pair in first_points.windows(2) {
724        let first_segment = Segment::new(first_pair[0], first_pair[1]);
725        for second_pair in second_points.windows(2) {
726            let second_segment = Segment::new(second_pair[0], second_pair[1]);
727            match segment_intersection(&first_segment, &second_segment) {
728                SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
729                SegmentIntersection::Collinear { .. } => interaction.overlap = true,
730                SegmentIntersection::Single(point) => {
731                    let endpoint_contact = first_pair.iter().any(|value| same_point(value, &point))
732                        || second_pair.iter().any(|value| same_point(value, &point));
733                    if !endpoint_contact {
734                        interaction.proper_crossing = true;
735                    }
736                    if !interaction
737                        .contacts
738                        .iter()
739                        .any(|value| same_point(value, &point))
740                    {
741                        interaction.contacts.push(point);
742                    }
743                }
744            }
745        }
746    }
747    interaction
748}
749
750fn ring_first_point_within<R1, R2, P>(inner: &R1, outer: &R2) -> bool
751where
752    R1: RingTrait<Point = P>,
753    R2: RingTrait<Point = P>,
754    P: Point,
755    P::Scalar: CoordinateScalar,
756    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
757{
758    inner
759        .points()
760        .next()
761        .is_some_and(|point| WithinRing.within(point, outer))
762}
763
764/// Whether any two non-adjacent edges of the vertex ring intersect. The
765/// closing edge (last→first) is represented by the repeated final
766/// vertex, so edges are the `pts[i] → pts[i+1]` pairs.
767fn has_self_intersection<P>(pts: &[P]) -> bool
768where
769    P: PointMut + Default + Copy,
770    P::Scalar: CoordinateScalar + Into<f64>,
771{
772    let n = pts.len();
773    // Edges: 0..n-1 (the last vertex repeats the first, closing the ring).
774    let edges = n - 1;
775    for i in 0..edges {
776        let a = Segment::new(pts[i], pts[i + 1]);
777        for j in (i + 1)..edges {
778            // Skip edges that share a vertex (adjacent, or the
779            // wrap-around pair of the first and last edge).
780            if j == i + 1 {
781                continue;
782            }
783            if i == 0 && j == edges - 1 {
784                continue;
785            }
786            let b = Segment::new(pts[j], pts[j + 1]);
787            match segment_intersection::<Segment<P>, P>(&a, &b) {
788                SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
789                _ => return true,
790            }
791        }
792    }
793    false
794}
795
796fn same_point<P: Point>(a: &P, b: &P) -> bool
797where
798    P::Scalar: PartialEq,
799{
800    a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
801}
802
803/// `true` iff `b` is a spike between `a` and `c`: collinear
804/// (`cross == 0`) and folding back (`dot < 0`).
805///
806/// Deliberately **stricter** than
807/// `geometry_algorithm::remove_spikes::is_spike_or_equal_2d`, which also
808/// fires on a zero-length step. `remove_spikes` drops a repeated vertex;
809/// `is_valid` does not reject one — Boost's default policy accepts
810/// duplicates (see [`ValidityOptions::BOOST_DEFAULT`]), and a ring
811/// carrying one is valid until `allow_duplicates` is turned off. The two
812/// predicates answer different questions, so they are not shared.
813fn is_spike_triple<P: Point>(a: &P, b: &P, c: &P) -> bool
814where
815    P::Scalar: CoordinateScalar,
816{
817    let ux = b.get::<0>() - a.get::<0>();
818    let uy = b.get::<1>() - a.get::<1>();
819    let vx = c.get::<0>() - b.get::<0>();
820    let vy = c.get::<1>() - b.get::<1>();
821    let zero = <P::Scalar as CoordinateScalar>::ZERO;
822    ux * vy - uy * vx == zero && ux * vx + uy * vy < zero
823}
824
825/// Any spike anywhere on the closed ring cycle, seam included.
826/// `pts` is the stored sequence (closing duplicate present — the
827/// closure check has already passed). The walk drops the duplicate
828/// and indexes the remaining cycle modularly, so triples
829/// `(last-1, last, first)` and `(last, first, second)` are covered.
830fn has_spike<P: Point + Copy>(pts: &[P]) -> bool
831where
832    P::Scalar: CoordinateScalar,
833{
834    // pts.len() >= 4 and pts[0] == pts[len-1] are guaranteed by the
835    // earlier FewPoints / NotClosed checks.
836    let cycle = &pts[..pts.len() - 1];
837    let n = cycle.len(); // >= 3
838    (0..n).any(|i| is_spike_triple(&cycle[(i + n - 1) % n], &cycle[i], &cycle[(i + 1) % n]))
839}
840
841#[cfg(test)]
842mod tests {
843    //! OVL6.T4 done-when: valid / invalid rings and polygons. Mirrors
844    //! the case families in `test/algorithms/is_valid.cpp`.
845
846    use super::{ValidityFailure, is_valid_polygon, is_valid_ring};
847    use geometry_cs::Cartesian;
848    use geometry_model::{Point2D, Polygon, Ring, polygon};
849
850    type P = Point2D<f64, Cartesian>;
851
852    #[test]
853    fn valid_square_ring() {
854        let r: Ring<P> = Ring::from_vec(vec![
855            P::new(0.0, 0.0),
856            P::new(0.0, 1.0),
857            P::new(1.0, 1.0),
858            P::new(1.0, 0.0),
859            P::new(0.0, 0.0),
860        ]);
861        assert!(is_valid_ring(&r).is_ok());
862    }
863
864    #[test]
865    fn too_few_points() {
866        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)]);
867        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::FewPoints));
868    }
869
870    #[test]
871    fn out_of_range_self_intersection_is_not_reported_valid() {
872        // Regression: a self-crossing "bow-tie" ring at coordinates past
873        // ±2^26 had its crossing dropped as OutOfRange by the segment
874        // kernel, so `has_self_intersection` returned false and the ring
875        // was wrongly reported valid. The same shape in range is correctly
876        // SelfIntersection; out of range it must be CoordinateOutOfRange,
877        // never Ok.
878        let s = 2.0e14;
879        let huge_bowtie: Ring<P> = Ring::from_vec(vec![
880            P::new(0.0, 0.0),
881            P::new(s, s),
882            P::new(s, 0.0),
883            P::new(0.0, s),
884            P::new(0.0, 0.0),
885        ]);
886        assert_eq!(
887            is_valid_ring(&huge_bowtie),
888            Err(ValidityFailure::CoordinateOutOfRange)
889        );
890        // The in-range analogue is still caught — as WrongOrientation,
891        // which is what Boost 1.83 reports for it: a bow-tie has zero
892        // signed area, so it fails the orientation test before the
893        // self-intersection test is ever reached.
894        let small_bowtie: Ring<P> = Ring::from_vec(vec![
895            P::new(0.0, 0.0),
896            P::new(2.0, 2.0),
897            P::new(2.0, 0.0),
898            P::new(0.0, 2.0),
899            P::new(0.0, 0.0),
900        ]);
901        assert_eq!(
902            is_valid_ring(&small_bowtie),
903            Err(ValidityFailure::WrongOrientation)
904        );
905    }
906
907    #[test]
908    fn not_closed() {
909        let r: Ring<P> = Ring::from_vec(vec![
910            P::new(0.0, 0.0),
911            P::new(1.0, 0.0),
912            P::new(1.0, 1.0),
913            P::new(0.0, 1.0),
914        ]);
915        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::NotClosed));
916    }
917
918    #[test]
919    fn self_intersecting_bowtie() {
920        // A "bow-tie" quadrilateral whose diagonals cross. Its two lobes
921        // cancel, so its signed area is zero and Boost reports the
922        // orientation failure rather than the crossing:
923        //
924        //   bowtie (zero area)  valid=0 failure=22  area=+0
925        let r: Ring<P> = Ring::from_vec(vec![
926            P::new(0.0, 0.0),
927            P::new(2.0, 2.0),
928            P::new(2.0, 0.0),
929            P::new(0.0, 2.0),
930            P::new(0.0, 0.0),
931        ]);
932        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::WrongOrientation));
933    }
934
935    /// The order the ring checks report in, pinned against Boost 1.83.
936    ///
937    /// Boost runs spikes, then orientation, then self-intersection, and
938    /// stops at the first failure. Getting the last two the wrong way round
939    /// is not cosmetic: a caller that branches on the code — tilemaker's
940    /// `buildWayGeometry` re-clips on `failure_self_intersections` but not
941    /// on `failure_wrong_orientation` — takes a different path.
942    #[test]
943    fn orientation_is_reported_before_self_intersection() {
944        // ccw + selfint no spike   valid=0 failure=22  area=-17
945        let wound_wrong_and_crossing: Ring<P> = Ring::from_vec(vec![
946            P::new(0.0, 0.0),
947            P::new(4.0, 0.0),
948            P::new(4.0, 4.0),
949            P::new(0.0, 4.0),
950            P::new(0.0, 0.0),
951            P::new(1.0, -1.0),
952            P::new(3.0, -3.0),
953            P::new(1.0, -3.0),
954            P::new(3.0, -1.0),
955            P::new(0.0, 0.0),
956        ]);
957        assert_eq!(
958            is_valid_ring(&wound_wrong_and_crossing),
959            Err(ValidityFailure::WrongOrientation)
960        );
961
962        // cw self-int, +area       valid=0 failure=21  area=+98
963        let wound_right_and_crossing: Ring<P> = Ring::from_vec(vec![
964            P::new(0.0, 0.0),
965            P::new(0.0, 10.0),
966            P::new(10.0, 10.0),
967            P::new(10.0, 0.0),
968            P::new(0.0, 0.0),
969            P::new(3.0, -4.0),
970            P::new(7.0, -4.0),
971            P::new(3.0, -8.0),
972            P::new(7.0, -8.0),
973            P::new(0.0, 0.0),
974        ]);
975        assert_eq!(
976            is_valid_ring(&wound_right_and_crossing),
977            Err(ValidityFailure::SelfIntersection)
978        );
979
980        // ccw + real spike         valid=0 failure=12  area=-16
981        // Spikes still win over orientation.
982        let wound_wrong_with_spike: Ring<P> = Ring::from_vec(vec![
983            P::new(0.0, 0.0),
984            P::new(2.0, 0.0),
985            P::new(2.0, -2.0),
986            P::new(2.0, 0.0),
987            P::new(4.0, 0.0),
988            P::new(4.0, 4.0),
989            P::new(0.0, 4.0),
990            P::new(0.0, 0.0),
991        ]);
992        assert_eq!(
993            is_valid_ring(&wound_wrong_with_spike),
994            Err(ValidityFailure::Spikes)
995        );
996    }
997
998    #[test]
999    fn invalid_coordinate() {
1000        let r: Ring<P> = Ring::from_vec(vec![
1001            P::new(0.0, 0.0),
1002            P::new(f64::NAN, 0.0),
1003            P::new(1.0, 1.0),
1004            P::new(0.0, 0.0),
1005        ]);
1006        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::InvalidCoordinate));
1007    }
1008
1009    #[test]
1010    fn valid_polygon() {
1011        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)]];
1012        assert!(is_valid_polygon(&pg).is_ok());
1013    }
1014
1015    #[test]
1016    fn valid_polygon_with_hole() {
1017        let pg: Polygon<P> = polygon![
1018            [
1019                (0.0, 0.0),
1020                (0.0, 10.0),
1021                (10.0, 10.0),
1022                (10.0, 0.0),
1023                (0.0, 0.0)
1024            ],
1025            [(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]
1026        ];
1027        assert!(is_valid_polygon(&pg).is_ok());
1028    }
1029
1030    #[test]
1031    fn wrongly_oriented_ring_is_rejected() {
1032        // CW-declared ring stored counter-clockwise. Boost:
1033        // failure_wrong_orientation.
1034        let r: Ring<P> = Ring::from_vec(vec![
1035            P::new(0.0, 0.0),
1036            P::new(2.0, 0.0),
1037            P::new(2.0, 2.0),
1038            P::new(0.0, 2.0),
1039            P::new(0.0, 0.0),
1040        ]);
1041        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::WrongOrientation));
1042    }
1043
1044    #[test]
1045    fn ccw_declared_ring_correctly_wound_is_ok() {
1046        // CCW-declared ring stored counter-clockwise: strategy-level
1047        // area positive, valid. Locks the convention shared with
1048        // `correct()` (spec correct-orientation).
1049        let r: Ring<P, false> = Ring::from_vec(vec![
1050            P::new(0.0, 0.0),
1051            P::new(2.0, 0.0),
1052            P::new(2.0, 2.0),
1053            P::new(0.0, 2.0),
1054            P::new(0.0, 0.0),
1055        ]);
1056        assert!(is_valid_ring(&r).is_ok());
1057    }
1058
1059    #[test]
1060    fn all_collinear_ring_is_spikes() {
1061        // The finding's repro: a "ring" that is a line. Every edge
1062        // pair is adjacent or the wrap pair, so the old validator
1063        // reported Ok. Boost: failure_spikes.
1064        let flat: Ring<P> = Ring::from_vec(vec![
1065            P::new(0.0, 0.0),
1066            P::new(4.0, 0.0),
1067            P::new(2.0, 0.0),
1068            P::new(0.0, 0.0),
1069        ]);
1070        assert_eq!(is_valid_ring(&flat), Err(ValidityFailure::Spikes));
1071    }
1072
1073    #[test]
1074    fn square_with_spike_is_spikes() {
1075        // A CW square with an out-and-back spur on its bottom edge.
1076        // Both Spikes and SelfIntersection are arguably present; the
1077        // pipeline order pins Spikes (matches Boost's check order).
1078        let r: Ring<P> = Ring::from_vec(vec![
1079            P::new(0.0, 0.0),
1080            P::new(0.0, 4.0),
1081            P::new(4.0, 4.0),
1082            P::new(4.0, 0.0),
1083            P::new(2.0, 0.0),
1084            P::new(2.0, -2.0),
1085            P::new(2.0, 0.0),
1086            P::new(0.0, 0.0),
1087        ]);
1088        assert_eq!(is_valid_ring(&r), Err(ValidityFailure::Spikes));
1089    }
1090
1091    #[test]
1092    fn hole_outside_exterior_is_rejected() {
1093        // The finding's repro. Doc promised InteriorRingOutside; the
1094        // variant was unreachable. Boost:
1095        // failure_interior_rings_outside. (Exterior CW-stored, hole
1096        // CCW-stored — both correctly wound, so orientation passes and
1097        // containment is what fails.)
1098        let pg: Polygon<P> = polygon![
1099            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
1100            [
1101                (10.0, 10.0),
1102                (12.0, 10.0),
1103                (12.0, 12.0),
1104                (10.0, 12.0),
1105                (10.0, 10.0)
1106            ]
1107        ];
1108        assert_eq!(
1109            is_valid_polygon(&pg),
1110            Err(ValidityFailure::InteriorRingOutside)
1111        );
1112    }
1113
1114    #[test]
1115    fn hole_touching_exterior_boundary_is_ok() {
1116        // The hole's first vertex lies ON the exterior boundary:
1117        // covered_by (not within) is the containment predicate, so an
1118        // isolated touch is permitted — matching Boost.
1119        let pg: Polygon<P> = polygon![
1120            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
1121            [(0.0, 2.0), (1.0, 1.0), (1.0, 3.0), (0.0, 2.0)]
1122        ];
1123        assert!(is_valid_polygon(&pg).is_ok());
1124    }
1125
1126    #[test]
1127    fn wrongly_oriented_hole_is_rejected() {
1128        // Correct CW exterior, but the hole is ALSO CW-stored — holes
1129        // must wind opposite. Boost: failure_wrong_orientation.
1130        let pg: Polygon<P> = polygon![
1131            [(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
1132            [(1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0)]
1133        ];
1134        assert_eq!(
1135            is_valid_polygon(&pg),
1136            Err(ValidityFailure::WrongOrientation)
1137        );
1138    }
1139}