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}