Skip to main content

geometry_algorithm/
is_simple.rs

1//! `is_simple(&g) -> bool` — OGC Simple Feature predicate.
2//!
3//! Mirrors `boost::geometry::is_simple(g)` from
4//! `boost/geometry/algorithms/is_simple.hpp`. The v1 linestring
5//! implementation is brute-force `O(n²)`; a sweepline-based variant
6//! lands once `phase_03`'s overlay infrastructure is in place.
7//!
8//! A linestring is *simple* iff no two non-adjacent segments intersect,
9//! adjacent segments touch only at their shared vertex (no zero-length
10//! edge, no collinear doubling-back), and it has no repeated
11//! non-consecutive vertex. A closed linestring / ring is allowed to
12//! share exactly its first and last vertex — that is the ring closure,
13//! not a self-intersection.
14//!
15//! An areal geometry (polygon) is *simple* iff every ring is non-empty
16//! and free of consecutive duplicate vertices — nothing more. This
17//! mirrors Boost exactly (`algorithms/detail/is_simple/areal.hpp`):
18//! "a Polygon is always a simple geometric object provided that it is
19//! valid". Self-intersections, ring touches, and ring crossings are
20//! validity concerns — see `geometry_overlay::validity::is_valid_polygon`.
21
22use alloc::vec::Vec;
23
24use geometry_coords::CoordinateScalar;
25use geometry_model::{Linestring, Polygon, Ring, Segment};
26use geometry_strategy::{CartesianIntersects, IntersectsStrategy};
27use geometry_trait::{Linestring as LinestringTrait, Point, Polygon as PolygonTrait};
28
29/// `true` iff `g` satisfies the OGC "is simple" predicate.
30///
31/// Mirrors `boost::geometry::is_simple` from
32/// `boost/geometry/algorithms/is_simple.hpp`. Dispatch happens through
33/// the [`IsSimple`] trait so linestrings and polygons share one entry
34/// point.
35#[inline]
36#[must_use]
37pub fn is_simple<G: IsSimple>(g: &G) -> bool {
38    g.is_simple()
39}
40
41/// Kind-keyed backing trait for [`is_simple`]. Hidden from the public
42/// surface — callers reach it through the free function only.
43#[doc(hidden)]
44pub trait IsSimple {
45    /// `true` iff `self` is simple.
46    fn is_simple(&self) -> bool;
47}
48
49impl<P> IsSimple for Linestring<P>
50where
51    P: Point,
52    P::Scalar: CoordinateScalar,
53    CartesianIntersects: IntersectsStrategy<Segment<P>, Segment<P>>,
54    P: geometry_trait::PointMut + Default + Copy,
55{
56    fn is_simple(&self) -> bool {
57        let pts: Vec<P> = self.points().copied().collect();
58        linestring_points_simple(&pts)
59    }
60}
61
62impl<P, const CW: bool, const CL: bool> IsSimple for Polygon<P, CW, CL>
63where
64    P: Point,
65    P::Scalar: CoordinateScalar,
66{
67    fn is_simple(&self) -> bool {
68        // Boost's areal is_simple is deliberately shallow: every ring
69        // must be non-empty and lack consecutive duplicate points —
70        // nothing else. "A Polygon is always a simple geometric object
71        // provided that it is valid"
72        // (`algorithms/detail/is_simple/areal.hpp`); ring touches,
73        // crossings, and self-intersections are `is_valid`'s business
74        // (see `geometry_overlay::validity`), not simplicity's.
75        ring_lacks_duplicates(self.exterior()) && self.interiors().all(|r| ring_lacks_duplicates(r))
76    }
77}
78
79/// Boost's `is_simple_ring`: non-empty and no consecutive duplicate
80/// vertices, walked over the closeable view — for an open ring the
81/// implicit (last, first) closing pair is also checked; for a closed
82/// ring the stored closing repetition is the ring closure, not a
83/// duplicate. Mirrors `! detail::is_valid::has_duplicates<Ring>` in
84/// `algorithms/detail/is_simple/areal.hpp`.
85fn ring_lacks_duplicates<P, const CW: bool, const CL: bool>(r: &Ring<P, CW, CL>) -> bool
86where
87    P: Point,
88    P::Scalar: CoordinateScalar,
89{
90    let pts: &[P] = &r.0;
91    if pts.is_empty() {
92        return false;
93    }
94    for w in pts.windows(2) {
95        if points_equal(&w[0], &w[1]) {
96            return false;
97        }
98    }
99    if matches!(
100        geometry_trait::Ring::closure(r),
101        geometry_trait::Closure::Open
102    ) && pts.len() >= 2
103        && points_equal(&pts[pts.len() - 1], &pts[0])
104    {
105        // Open ring whose stored last already equals the first: under
106        // the closeable view that IS a consecutive duplicate.
107        return false;
108    }
109    true
110}
111
112/// The shared linestring-simplicity walk over a point slice.
113fn linestring_points_simple<P>(pts: &[P]) -> bool
114where
115    P: Point + geometry_trait::PointMut + Default + Copy,
116    P::Scalar: CoordinateScalar,
117    CartesianIntersects: IntersectsStrategy<Segment<P>, Segment<P>>,
118{
119    if pts.len() < 2 {
120        return true;
121    }
122
123    // Is this a closed loop (first vertex coincides with the last)? Then
124    // the first and last segments legitimately share that vertex.
125    let closed = points_equal(&pts[0], &pts[pts.len() - 1]);
126
127    let segs: Vec<Segment<P>> = pts.windows(2).map(|w| Segment::new(w[0], w[1])).collect();
128
129    for i in 0..segs.len() {
130        // Zero-length segment (a repeated consecutive vertex) is never
131        // simple.
132        if points_equal(&pts[i], &pts[i + 1]) {
133            return false;
134        }
135        for j in (i + 1)..segs.len() {
136            let adjacent = j == i + 1;
137            if adjacent {
138                // Adjacent segments share their join vertex — permitted.
139                // Reject only a collinear doubling-back (the incoming
140                // and outgoing directions point opposite ways).
141                if doubles_back(&pts[i], &pts[i + 1], &pts[j + 1]) {
142                    return false;
143                }
144            } else if closed && i == 0 && j == segs.len() - 1 {
145                // First and last segment of a closed loop meet only at
146                // the shared closing vertex — that is the ring closure,
147                // not a self-intersection.
148            } else if CartesianIntersects.intersects(&segs[i], &segs[j]) {
149                return false;
150            }
151        }
152    }
153    true
154}
155
156/// Coordinate-wise 2D equality of two points.
157#[inline]
158fn points_equal<P: Point>(a: &P, b: &P) -> bool {
159    a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
160}
161
162/// `true` iff the edge `p0`-`p1` and the adjacent edge `p1`-`p2` are
163/// collinear and fold back over one another (the turn reverses
164/// direction along the same line).
165#[inline]
166fn doubles_back<P: Point>(p0: &P, p1: &P, p2: &P) -> bool
167where
168    P::Scalar: CoordinateScalar,
169{
170    let ax = p1.get::<0>() - p0.get::<0>();
171    let ay = p1.get::<1>() - p0.get::<1>();
172    let bx = p2.get::<0>() - p1.get::<0>();
173    let by = p2.get::<1>() - p1.get::<1>();
174    let cross = ax * by - ay * bx;
175    if cross != P::Scalar::ZERO {
176        return false;
177    }
178    // Collinear: fold-back iff the incoming and outgoing directions have
179    // a negative dot product.
180    ax * bx + ay * by < P::Scalar::ZERO
181}
182
183#[cfg(test)]
184mod tests {
185    //! Reference values from
186    //! `boost/geometry/test/algorithms/is_simple.cpp:46-120`.
187
188    use super::is_simple;
189    use geometry_cs::Cartesian;
190    use geometry_model::{Linestring, Point2D, Polygon, linestring, polygon};
191
192    type Pt = Point2D<f64, Cartesian>;
193
194    #[test]
195    fn two_point_is_simple() {
196        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 2.)];
197        assert!(is_simple(&ls));
198    }
199
200    #[test]
201    fn three_point_is_simple() {
202        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 2.), (2., 3.)];
203        assert!(is_simple(&ls));
204    }
205
206    #[test]
207    fn consecutive_duplicate_not_simple() {
208        let ls: Linestring<Pt> = linestring![(0., 0.), (0., 0.), (1., 0.)];
209        assert!(!is_simple(&ls));
210    }
211
212    #[test]
213    fn figure_eight_not_simple() {
214        let ls: Linestring<Pt> =
215            linestring![(0., 0.), (1., 0.), (2., 0.), (1., 1.), (1., 0.), (1., -1.)];
216        assert!(!is_simple(&ls));
217    }
218
219    #[test]
220    fn bowtie_linestring_not_simple() {
221        // (0,0)-(2,2)-(2,0)-(0,2): the first and third segments cross.
222        let ls: Linestring<Pt> = linestring![(0., 0.), (2., 2.), (2., 0.), (0., 2.)];
223        assert!(!is_simple(&ls));
224    }
225
226    #[test]
227    fn closed_simple_quadrilateral() {
228        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (1., 1.), (0., 0.)];
229        assert!(is_simple(&ls));
230    }
231
232    #[test]
233    fn closed_square_is_simple() {
234        let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.), (10., 10.), (0., 10.), (0., 0.)];
235        assert!(is_simple(&ls));
236    }
237
238    #[test]
239    fn unit_square_polygon_is_simple() {
240        let pg: Polygon<Pt> = polygon![[(0., 0.), (4., 0.), (4., 3.), (0., 3.), (0., 0.)]];
241        assert!(is_simple(&pg));
242    }
243
244    #[test]
245    fn polygon_with_disjoint_hole_is_simple() {
246        let pg: Polygon<Pt> = polygon![
247            [(0., 0.), (10., 0.), (10., 10.), (0., 10.), (0., 0.)],
248            [(2., 2.), (4., 2.), (4., 4.), (2., 4.), (2., 2.)],
249        ];
250        assert!(is_simple(&pg));
251    }
252
253    #[test]
254    fn bowtie_polygon_is_simple_but_invalid() {
255        // Boost parity: the bow-tie has no duplicate vertices, so it is
256        // SIMPLE — and invalid (geometry_overlay::validity::is_valid_ring
257        // reports SelfIntersection; not asserted here because algorithm
258        // must not depend on overlay — that would be a dependency cycle).
259        let pg: Polygon<Pt> = polygon![[(0., 0.), (2., 2.), (0., 2.), (2., 0.), (0., 0.)]];
260        assert!(is_simple(&pg));
261    }
262
263    #[test]
264    fn hole_touching_outer_is_simple() {
265        // Boost parity: areal is_simple checks only per-ring duplicate
266        // points ("a Polygon is always a simple geometric object provided
267        // that it is valid", detail/is_simple/areal.hpp). A hole touching
268        // the exterior is a VALIDITY question, not a simplicity one.
269        let pg: Polygon<Pt> = polygon![
270            [(0., 0.), (10., 0.), (10., 10.), (0., 10.), (0., 0.)],
271            [(0., 0.), (5., 5.), (10., 0.), (5., 0.), (0., 0.)],
272        ];
273        assert!(is_simple(&pg));
274    }
275
276    #[test]
277    fn polygon_with_consecutive_duplicate_is_not_simple() {
278        // Boost's has_duplicates: a consecutive repeated vertex makes
279        // the ring (and so the polygon) non-simple.
280        let pg: Polygon<Pt> =
281            polygon![[(0., 0.), (4., 0.), (4., 0.), (4., 4.), (0., 4.), (0., 0.)]];
282        assert!(!is_simple(&pg));
283    }
284
285    #[test]
286    fn polygon_with_duplicate_in_hole_is_not_simple() {
287        let pg: Polygon<Pt> = polygon![
288            [(0., 0.), (10., 0.), (10., 10.), (0., 10.), (0., 0.)],
289            [(2., 2.), (4., 2.), (4., 2.), (4., 4.), (2., 4.), (2., 2.)],
290        ];
291        assert!(!is_simple(&pg));
292    }
293
294    #[test]
295    fn polygon_with_empty_exterior_is_not_simple() {
296        // Boost: "not empty and lacking duplicate points" — empty
297        // fails the first clause.
298        use geometry_model::Ring;
299        let pg: Polygon<Pt> = Polygon::new(Ring::new());
300        assert!(!is_simple(&pg));
301    }
302}