Skip to main content

geometry_strategy/
within.rs

1//! Per-CS strategy for point-in-polygon containment (`within` /
2//! `covered_by`).
3//!
4//! Mirrors the pieces of Boost.Geometry that collaborate to make
5//! `boost::geometry::within(p, g)` / `boost::geometry::covered_by(p, g)`
6//! work for any (point, polygonal | box) pair in any coordinate system:
7//!
8//! * `boost/geometry/strategies/within.hpp` — the per-CS
9//!   `within`-strategy concept (apply/result two-phase),
10//! * `boost/geometry/strategies/covered_by.hpp` — same concept reused,
11//! * `boost/geometry/strategies/cartesian/point_in_poly_winding.hpp` —
12//!   `cartesian_winding`, the default Cartesian PIP, implementing the
13//!   classic winding-number algorithm with on-segment detection,
14//! * `boost/geometry/strategies/cartesian/point_in_box.hpp` —
15//!   `cartesian_point_in_box`, the per-corner strict / non-strict
16//!   comparisons used to fold "is the point inside this axis-aligned
17//!   box" into the dispatch.
18//!
19//! The Boost concept exposes a stateful three-step API — construct a
20//! `state_type`, call `apply(point, s1, s2, state)` for every segment,
21//! then call `result(state)` to read off `-1` / `0` / `+1` (outside /
22//! boundary / interior). The Rust analogue collapses that three-step
23//! shape into a single `within` / `covered_by` pair on
24//! [`WithinStrategy`] because the per-segment walk is identical for
25//! every CS — only the per-segment kernel changes.
26//!
27//! ## Coherence note
28//!
29//! Boost dispatches on the geometry's tag via partial template
30//! specialisation — `dispatch::within<Point, Ring, _, ring_tag>` and
31//! `dispatch::within<Point, Polygon, _, polygon_tag>` are mutually
32//! exclusive because the C++ side can prove tags distinct. Rust's
33//! trait system cannot prove a downstream type does not implement
34//! several geometry traits at once, so two open blankets on one strategy
35//! struct would collide (E0119). The port reproduces Boost's tag
36//! dispatch instead: one **per-kind strategy struct** ([`WithinBox`],
37//! [`WithinRing`], [`WithinPoly`]) carries a single concept-bounded
38//! `WithinStrategy` impl — distinct `Self`, so no overlap — and the
39//! tag-keyed [`WithinStrategyForKind`] picker routes `G::Kind` to the
40//! right struct. Because the picker keys on the tag, any concept-adapted
41//! foreign type resolves through the same path as the equivalent
42//! `geometry-model` value.
43//!
44//! [`crate::intersects`] reaches point-in-polygon containment through
45//! the open [`WithinPoly`] strategy directly (not the algorithm-layer
46//! `covered_by` free fn — that would be an upward crate dependency /
47//! cycle), so both crates share the one open kernel.
48//!
49//! ## Result-code convention
50//!
51//! Mirrors Boost's `cartesian_winding::result` at
52//! `strategy/cartesian/point_in_poly_winding.hpp:69-74`:
53//!
54//! | Boost code | Meaning            | `within` | `covered_by` |
55//! |-----------:|--------------------|---------:|-------------:|
56//! |       `-1` | outside            |  `false` |      `false` |
57//! |        `0` | on the boundary    |  `false` |       `true` |
58//! |       `+1` | strict interior    |   `true` |       `true` |
59//!
60//! ## Precision limit (Cartesian, `f64`)
61//!
62//! The winding kernel decides each point's side of a segment from the
63//! sign of a cross product of coordinate *differences*. For `f64` that
64//! sign is exact only while the operands stay within the mantissa: past
65//! roughly `±2^26` (`67_108_864`) the products no longer fit in 53 bits
66//! and the sign can flip, so a strict-interior / boundary / exterior
67//! classification may be wrong for coordinates beyond that magnitude.
68//! This is the same limit the overlay engine gates on with its
69//! `SAFE_ABS_MAX` range guard, and it matches Boost: the non-rescaled
70//! `cartesian_winding` shares the bound (Boost's rescaling only ever
71//! applied at the overlay/turn layer, not here). `within` does **not**
72//! reject out-of-range input — callers that work with coordinates beyond
73//! `±2^26` must scale down first.
74
75use geometry_coords::CoordinateScalar;
76use geometry_cs::{CartesianFamily, CoordinateSystem};
77use geometry_tag::{BoxTag, PolygonTag, RingTag, SameAs};
78use geometry_trait::{
79    Box as BoxTrait, Point as PointTrait, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
80    corner,
81};
82
83/// A strategy for point-in-geometry containment.
84///
85/// Mirrors the per-CS `within` strategy concept declared in
86/// `boost/geometry/strategies/within.hpp` and refined per coordinate
87/// system in `strategies/cartesian/point_in_poly_winding.hpp` /
88/// `strategies/spherical/point_in_poly_winding.hpp`. The Boost concept
89/// exposes a stateful `apply(point, s1, s2, state)` accumulator plus a
90/// final `result(state)` reduction; the Rust analogue collapses the
91/// two phases into a single `within` / `covered_by` pair keyed on the
92/// geometry type, because the per-segment walk shape is identical for
93/// every CS — only the per-segment kernel changes.
94pub trait WithinStrategy<P: PointTrait, G> {
95    /// `true` iff `p` lies in the strict interior of `g`.
96    ///
97    /// Mirrors `boost::geometry::within(p, g, strategy)` from
98    /// `boost/geometry/algorithms/within.hpp` resolved through
99    /// `cartesian_winding::result == 1` at
100    /// `strategy/cartesian/point_in_poly_winding.hpp:69-74`.
101    fn within(&self, p: &P, g: &G) -> bool;
102
103    /// `true` iff `p` lies in the strict interior **or** on the
104    /// boundary of `g`.
105    ///
106    /// Mirrors `boost::geometry::covered_by(p, g, strategy)` from
107    /// `boost/geometry/algorithms/covered_by.hpp` resolved through
108    /// `cartesian_winding::result >= 0` at the same lines.
109    fn covered_by(&self, p: &P, g: &G) -> bool;
110}
111
112// =====================================================================
113// Per-kind strategy structs + tag-keyed picker
114// =====================================================================
115//
116// Each struct carries the kernel for one kind, bound on the *open*
117// concept (`G: Box`/`Ring`/`Polygon`) so any adapted foreign type
118// resolves. Distinct `Self` per kind ⇒ no overlap.
119//
120// * Box     — `strategy::within::cartesian_point_in_box::apply`
121//             (`strategy/cartesian/point_in_box.hpp:55-93`).
122// * Ring    — `cartesian_winding_base::apply`
123//             (`strategy/cartesian/point_in_poly_winding.hpp:91-131`).
124// * Polygon — `detail::within::point_in_polygon::apply`
125//             (`algorithms/detail/within/point_in_geometry.hpp:200-244`):
126//             within the exterior and not covered_by any hole.
127
128/// Open point-in-box strategy. See the [module docs](self).
129#[derive(Debug, Default, Clone, Copy)]
130pub struct WithinBox;
131/// Open point-in-ring (winding number) strategy. See the [module docs](self).
132#[derive(Debug, Default, Clone, Copy)]
133pub struct WithinRing;
134/// Open point-in-polygon (winding number, hole-aware) strategy. See the
135/// [module docs](self).
136#[derive(Debug, Default, Clone, Copy)]
137pub struct WithinPoly;
138
139impl<P, G> WithinStrategy<P, G> for WithinBox
140where
141    G: BoxTrait<Point = P>,
142    P: PointMut,
143    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
144{
145    #[inline]
146    fn within(&self, p: &P, b: &G) -> bool {
147        let x = p.get::<0>();
148        let y = p.get::<1>();
149        let xmin = b.get_indexed::<{ corner::MIN }, 0>();
150        let ymin = b.get_indexed::<{ corner::MIN }, 1>();
151        let xmax = b.get_indexed::<{ corner::MAX }, 0>();
152        let ymax = b.get_indexed::<{ corner::MAX }, 1>();
153        xmin < x && x < xmax && ymin < y && y < ymax
154    }
155
156    #[inline]
157    fn covered_by(&self, p: &P, b: &G) -> bool {
158        let x = p.get::<0>();
159        let y = p.get::<1>();
160        let xmin = b.get_indexed::<{ corner::MIN }, 0>();
161        let ymin = b.get_indexed::<{ corner::MIN }, 1>();
162        let xmax = b.get_indexed::<{ corner::MAX }, 0>();
163        let ymax = b.get_indexed::<{ corner::MAX }, 1>();
164        xmin <= x && x <= xmax && ymin <= y && y <= ymax
165    }
166}
167
168impl<P, G> WithinStrategy<P, G> for WithinRing
169where
170    G: RingTrait<Point = P>,
171    P: PointTrait,
172    P::Scalar: CoordinateScalar,
173    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
174{
175    #[inline]
176    fn within(&self, p: &P, r: &G) -> bool {
177        winding_result(p, r) == InOut::Interior
178    }
179
180    #[inline]
181    fn covered_by(&self, p: &P, r: &G) -> bool {
182        !matches!(winding_result(p, r), InOut::Exterior)
183    }
184}
185
186impl<P, G> WithinStrategy<P, G> for WithinPoly
187where
188    G: PolygonTrait<Point = P>,
189    P: PointTrait,
190    P::Scalar: CoordinateScalar,
191    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
192{
193    #[inline]
194    fn within(&self, p: &P, pg: &G) -> bool {
195        if !WithinRing.within(p, pg.exterior()) {
196            return false;
197        }
198        for hole in pg.interiors() {
199            if WithinRing.covered_by(p, hole) {
200                return false;
201            }
202        }
203        true
204    }
205
206    #[inline]
207    fn covered_by(&self, p: &P, pg: &G) -> bool {
208        if !WithinRing.covered_by(p, pg.exterior()) {
209            return false;
210        }
211        for hole in pg.interiors() {
212            if WithinRing.within(p, hole) {
213                return false;
214            }
215        }
216        true
217    }
218}
219
220/// Type-level "which `WithinStrategy` struct does this geometry *kind*
221/// use". One impl per [`geometry_tag`] kind tag, keyed on the tag (never a
222/// concept blanket — that would overlap, E0119). The
223/// [`crate::within`]/[`crate::covered_by`] free functions route
224/// `G → G::Kind → S` through this trait.
225#[doc(hidden)]
226pub trait WithinStrategyForKind {
227    /// The per-kind [`WithinStrategy`] struct this tag is computed with.
228    type S: Default;
229}
230
231impl WithinStrategyForKind for BoxTag {
232    type S = WithinBox;
233}
234impl WithinStrategyForKind for RingTag {
235    type S = WithinRing;
236}
237impl WithinStrategyForKind for PolygonTag {
238    type S = WithinPoly;
239}
240
241// ---- Winding-number kernel ------------------------------------------
242
243/// Tri-state outcome of the winding-number walk.
244///
245/// Mirrors the integer return of `cartesian_winding::result` at
246/// `strategy/cartesian/point_in_poly_winding.hpp:69-74`:
247/// `-1` outside, `0` on the boundary, `+1` strict interior.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249enum InOut {
250    Exterior,
251    Boundary,
252    Interior,
253}
254
255/// Walk the segments of `r` accumulating the winding count and the
256/// "on a segment" flag, then collapse to an [`InOut`].
257///
258/// Mirrors `cartesian_winding_base::apply` together with `result` at
259/// `strategy/cartesian/point_in_poly_winding.hpp:91-131, 69-74`. The
260/// closing edge for an open ring is added explicitly here — matches
261/// Boost's `closed_clockwise_view` wrap done one layer up at
262/// `algorithms/detail/within/point_in_geometry.hpp`.
263fn winding_result<P, R>(p: &P, r: &R) -> InOut
264where
265    P: PointTrait,
266    P::Scalar: CoordinateScalar,
267    R: RingTrait<Point = P>,
268{
269    let mut count: i32 = 0;
270    let mut it = r.points();
271    let Some(mut prev) = it.next() else {
272        // Empty ring — outside by convention. Mirrors the
273        // `boost::size(ring) < minimum_ring_size` guard at
274        // `algorithms/detail/within/point_in_geometry.hpp:198`.
275        return InOut::Exterior;
276    };
277    let first = prev;
278    for curr in it {
279        match apply_segment(p, prev, curr) {
280            Step::Touches => return InOut::Boundary,
281            Step::Count(c) => count += c,
282        }
283        prev = curr;
284    }
285    // Open ring: close the loop explicitly. For a closed ring `prev`
286    // already equals `first`, so the kernel below returns `count = 0`
287    // (eq1 && eq2 with equal y-spans → no touch, no contribution).
288    match apply_segment(p, prev, first) {
289        Step::Touches => return InOut::Boundary,
290        Step::Count(c) => count += c,
291    }
292    if count == 0 {
293        InOut::Exterior
294    } else {
295        InOut::Interior
296    }
297}
298
299/// Per-segment outcome of the winding kernel.
300#[derive(Debug, Clone, Copy)]
301enum Step {
302    /// The point lies on this segment; the walk can short-circuit.
303    Touches,
304    /// Contribution to the running winding count.
305    Count(i32),
306}
307
308/// Single-segment contribution of the winding number, plus the
309/// on-segment short-circuit.
310///
311/// Mirrors `cartesian_winding_base::apply` at
312/// `strategy/cartesian/point_in_poly_winding.hpp:91-131` together
313/// with its `check_segment` / `check_touch` / `calculate_count` /
314/// `side_equal` helpers (lines 139-217). The cartesian side strategy
315/// reduces to the cross-product sign
316/// `(s2.x - s1.x) * (p.y - s1.y) - (s2.y - s1.y) * (p.x - s1.x)`
317/// (`strategy/cartesian/side_by_triangle.hpp:178-200`).
318fn apply_segment<P>(p: &P, s1: &P, s2: &P) -> Step
319where
320    P: PointTrait,
321    P::Scalar: CoordinateScalar,
322{
323    let px = p.get::<0>();
324    let py = p.get::<1>();
325    let s1x = s1.get::<0>();
326    let s2x = s2.get::<0>();
327    let s1y = s1.get::<1>();
328    let s2y = s2.get::<1>();
329
330    let eq1 = s1x == px;
331    let eq2 = s2x == px;
332
333    // check_touch: vertical segment exactly on the point's x.
334    // Mirrors lines 154-184 of point_in_poly_winding.hpp.
335    if eq1 && eq2 {
336        let (lo, hi) = if s1y <= s2y { (s1y, s2y) } else { (s2y, s1y) };
337        if lo <= py && py <= hi {
338            return Step::Touches;
339        }
340        return Step::Count(0);
341    }
342
343    // calculate_count: lines 186-203.
344    let count = if eq1 {
345        if s2x > px { 1 } else { -1 }
346    } else if eq2 {
347        if s1x > px { -1 } else { 1 }
348    } else if s1x < px && s2x > px {
349        2
350    } else if s2x < px && s1x > px {
351        -2
352    } else {
353        0
354    };
355
356    if count == 0 {
357        return Step::Count(0);
358    }
359
360    // side: for ±1, side_equal; for ±2, cartesian side cross product.
361    // Mirrors lines 100-110 of point_in_poly_winding.hpp.
362    let side: i32 = if count == 1 || count == -1 {
363        let se = if eq1 { s1 } else { s2 };
364        let sey = se.get::<1>();
365        if py == sey {
366            0
367        } else if py < sey {
368            -count
369        } else {
370            count
371        }
372    } else {
373        // Cartesian side: sign of (s2 - s1) × (p - s1).
374        // Mirrors `side_by_triangle::side_value` at
375        // strategy/cartesian/side_by_triangle.hpp:178-200.
376        let cross = (s2x - s1x) * (py - s1y) - (s2y - s1y) * (px - s1x);
377        if cross > P::Scalar::ZERO {
378            1
379        } else if cross < P::Scalar::ZERO {
380            -1
381        } else {
382            0
383        }
384    };
385
386    if side == 0 {
387        return Step::Touches;
388    }
389
390    if side * count > 0 {
391        Step::Count(count)
392    } else {
393        Step::Count(0)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    //! Reference values from `geometry/test/strategies/winding.cpp:19-73`
400    //! (the Cartesian section). Each test cites the C++ line(s) it
401    //! mirrors.
402
403    use super::{WithinBox, WithinPoly, WithinRing, WithinStrategy};
404    use geometry_cs::Cartesian;
405    use geometry_model::{Box, Point2D, Polygon, Ring, polygon};
406
407    type P = Point2D<f64, Cartesian>;
408
409    fn pt(x: f64, y: f64) -> P {
410        Point2D::new(x, y)
411    }
412
413    fn box_polygon() -> Polygon<P> {
414        polygon![[(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)]]
415    }
416
417    /// `winding.cpp:30` — `b1` interior point.
418    #[test]
419    fn box_b1_inside() {
420        assert!(WithinPoly.within(&pt(1.0, 1.0), &box_polygon()));
421    }
422
423    /// `winding.cpp:31` — `b2` exterior point.
424    #[test]
425    fn box_b2_outside() {
426        assert!(!WithinPoly.within(&pt(3.0, 3.0), &box_polygon()));
427    }
428
429    /// `winding.cpp:34-37` — all four corners are "officially false".
430    #[test]
431    fn box_corners_are_not_within() {
432        let p = box_polygon();
433        for (x, y) in [(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0)] {
434            assert!(!WithinPoly.within(&pt(x, y), &p), "corner ({x},{y})");
435        }
436    }
437
438    /// `winding.cpp:40-43` — all four sides are "officially false".
439    #[test]
440    fn box_sides_are_not_within() {
441        let p = box_polygon();
442        for (x, y) in [(0.0, 1.0), (1.0, 2.0), (2.0, 1.0), (1.0, 0.0)] {
443            assert!(!WithinPoly.within(&pt(x, y), &p), "side ({x},{y})");
444        }
445    }
446
447    /// `winding.cpp:46-47` — triangle interior / exterior.
448    #[test]
449    fn triangle_interior_and_exterior() {
450        let t: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (6.0, 0.0), (0.0, 0.0)]];
451        assert!(WithinPoly.within(&pt(1.0, 1.0), &t));
452        assert!(!WithinPoly.within(&pt(3.0, 3.0), &t));
453    }
454
455    /// `winding.cpp:58-60` — polygon-with-hole semantics: inside the
456    /// outer-but-outside the hole is within; inside the hole is not.
457    #[test]
458    fn hole_semantics() {
459        let with_hole: Polygon<P> = polygon![
460            [(0.0, 0.0), (0.0, 3.0), (3.0, 3.0), (3.0, 0.0), (0.0, 0.0)],
461            [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)]
462        ];
463        // h1
464        assert!(WithinPoly.within(&pt(0.5, 0.5), &with_hole));
465        // h2a — inside the hole
466        assert!(!WithinPoly.within(&pt(1.5, 1.5), &with_hole));
467    }
468
469    /// `covered_by` inverts the boundary rule: corners and sides are
470    /// covered, but external points are not. Mirrors the Boost
471    /// `result >= 0` projection at
472    /// `strategy/cartesian/point_in_poly_winding.hpp:69-74`.
473    #[test]
474    fn covered_by_includes_boundary() {
475        let p = box_polygon();
476        assert!(WithinPoly.covered_by(&pt(0.0, 0.0), &p));
477        assert!(WithinPoly.covered_by(&pt(0.0, 1.0), &p));
478        assert!(WithinPoly.covered_by(&pt(1.0, 1.0), &p));
479        assert!(!WithinPoly.covered_by(&pt(3.0, 3.0), &p));
480    }
481
482    /// `Box`-as-geometry path: strict-vs-non-strict per-dimension.
483    /// Mirrors `cartesian_point_in_box` at
484    /// `strategy/cartesian/point_in_box.hpp:55-93`.
485    #[test]
486    fn box_geometry_strict_vs_non_strict() {
487        let b = Box::from_corners(pt(0.0, 0.0), pt(2.0, 2.0));
488        // strict interior
489        assert!(WithinBox.within(&pt(1.0, 1.0), &b));
490        // boundary: corner
491        assert!(!WithinBox.within(&pt(0.0, 0.0), &b));
492        assert!(WithinBox.covered_by(&pt(0.0, 0.0), &b));
493        // boundary: side
494        assert!(!WithinBox.within(&pt(0.0, 1.0), &b));
495        assert!(WithinBox.covered_by(&pt(0.0, 1.0), &b));
496        // outside
497        assert!(!WithinBox.within(&pt(3.0, 3.0), &b));
498        assert!(!WithinBox.covered_by(&pt(3.0, 3.0), &b));
499    }
500
501    /// Ring-only path — same kernel, no exterior/interior split.
502    #[test]
503    fn ring_within_smoke() {
504        let r: Ring<P> = Ring::from_vec(vec![
505            pt(0.0, 0.0),
506            pt(0.0, 2.0),
507            pt(2.0, 2.0),
508            pt(2.0, 0.0),
509            pt(0.0, 0.0),
510        ]);
511        assert!(WithinRing.within(&pt(1.0, 1.0), &r));
512        assert!(!WithinRing.within(&pt(0.0, 0.0), &r));
513        assert!(WithinRing.covered_by(&pt(0.0, 0.0), &r));
514    }
515
516    /// Open ring (no repeated closing vertex): the kernel must add
517    /// the implicit `last -> first` edge so containment still works.
518    #[test]
519    fn open_ring_closes_implicitly() {
520        let mut r = Ring::<P, true, false>::new();
521        r.push(pt(0.0, 0.0));
522        r.push(pt(0.0, 2.0));
523        r.push(pt(2.0, 2.0));
524        r.push(pt(2.0, 0.0));
525        assert!(WithinRing.within(&pt(1.0, 1.0), &r));
526        assert!(!WithinRing.within(&pt(3.0, 3.0), &r));
527    }
528}