Skip to main content

geometry_overlay/
relate.rs

1//! OVL6.T1 / OVL6.T2 — the DE-9IM relate matrix and the spatial
2//! predicates derived from it.
3//!
4//! Mirrors `boost/geometry/algorithms/relate.hpp`,
5//! `algorithms/relation.hpp`, and `detail/relate/`. The DE-9IM matrix
6//! records, for each pair drawn from {Interior, Boundary, Exterior} of
7//! two geometries, the **dimension** of their intersection. The
8//! `crosses` / `overlaps` / `touches` predicates
9//! (`algorithms/{crosses,overlaps,touches}.hpp`) are then thin tests on
10//! that matrix.
11//!
12//! v1 scope: polygon × polygon (areal × areal), computed from the turn
13//! graph plus interior sampling. The matrix is filled well enough for
14//! the three predicates; the full mask-string interface is deferred.
15
16use geometry_coords::CoordinateScalar;
17use geometry_cs::{CartesianFamily, CoordinateSystem};
18use geometry_model::{Polygon, Ring};
19use geometry_tag::SameAs;
20use geometry_trait::{Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait};
21
22use crate::operation::OverlayError;
23use crate::predicate::range_guard::polygon_in_range;
24use crate::surface_point::point_on_surface;
25use crate::turn::info::Method;
26use crate::turn::{RingKind, get_turns_ring_ring};
27
28/// The dimension of an intersection cell in a [`De9im`] matrix.
29///
30/// Mirrors the per-cell value of Boost's relate matrix: `F` (empty) or
31/// a dimension digit `0` / `1` / `2` (`detail/relate/result.hpp`).
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Dimension {
34    /// Empty intersection — Boost's `F`.
35    Empty,
36    /// Point (0-dimensional) intersection — Boost's `0`.
37    Point,
38    /// Curve (1-dimensional) intersection — Boost's `1`.
39    Curve,
40    /// Area (2-dimensional) intersection — Boost's `2`.
41    Area,
42}
43
44impl Dimension {
45    /// Whether the cell is non-empty (Boost's `T` — "true, any
46    /// dimension").
47    #[must_use]
48    pub fn is_set(self) -> bool {
49        self != Dimension::Empty
50    }
51}
52
53/// A DE-9IM 3×3 intersection matrix between two geometries.
54///
55/// Rows are the first geometry's {Interior, Boundary, Exterior}, columns
56/// the second's. `m[r][c]` is the dimension of the intersection of the
57/// first's feature `r` with the second's feature `c`. Mirrors Boost's
58/// `relate::matrix` (`detail/relate/result.hpp`).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct De9im {
61    /// `[row][col]` over `[Interior, Boundary, Exterior]`.
62    pub m: [[Dimension; 3]; 3],
63}
64
65/// Row / column indices into a [`De9im`] matrix.
66pub mod feature {
67    /// Interior row/column index.
68    pub const INTERIOR: usize = 0;
69    /// Boundary row/column index.
70    pub const BOUNDARY: usize = 1;
71    /// Exterior row/column index.
72    pub const EXTERIOR: usize = 2;
73}
74
75impl De9im {
76    /// The `[Interior][Interior]` cell — do the two interiors meet, and
77    /// in what dimension.
78    #[must_use]
79    pub fn interior_interior(&self) -> Dimension {
80        self.m[feature::INTERIOR][feature::INTERIOR]
81    }
82
83    /// The `[Boundary][Boundary]` cell.
84    #[must_use]
85    pub fn boundary_boundary(&self) -> Dimension {
86        self.m[feature::BOUNDARY][feature::BOUNDARY]
87    }
88
89    /// The `[Interior][Exterior]` cell — is part of the first's interior
90    /// outside the second.
91    #[must_use]
92    pub fn interior_exterior(&self) -> Dimension {
93        self.m[feature::INTERIOR][feature::EXTERIOR]
94    }
95
96    /// The `[Exterior][Interior]` cell.
97    #[must_use]
98    pub fn exterior_interior(&self) -> Dimension {
99        self.m[feature::EXTERIOR][feature::INTERIOR]
100    }
101}
102
103/// Compute the DE-9IM matrix relating two polygons.
104///
105/// Fills the matrix from the turn graph (do the boundaries cross, and
106/// where) and interior sampling (is each interior partly inside / partly
107/// outside the other). Mirrors `boost::geometry::relation`
108/// (`algorithms/relation.hpp`) for the areal × areal case.
109///
110/// # Errors
111///
112/// Returns [`OverlayError::Unsupported`] in two cases:
113///
114/// * Either polygon has a coordinate outside the safe arithmetic range
115///   ([`SAFE_ABS_MAX`](crate::predicate::range_guard::SAFE_ABS_MAX)) — out
116///   of range the turn collector silently drops intersections, so the
117///   relation cannot be trusted.
118/// * The boundaries meet but only *non-transversally* (edge-aligned/
119///   collinear or vertex-only contact) **and** neither interior
120///   representative point falls strictly inside the other. In that state
121///   the turn graph cannot distinguish a pure boundary touch from a
122///   genuine area overlap whose crossings all land on vertices/edges, so
123///   the interior/interior cell is genuinely ambiguous — the same
124///   degenerate class the boolean overlay operations refuse (see
125///   [`crate::intersection`]).
126///
127/// Every transversal crossing, containment, or clean disjoint case is
128/// answered normally.
129///
130/// # Examples
131///
132/// ```
133/// use geometry_cs::Cartesian;
134/// use geometry_model::{polygon, Point2D, Polygon};
135/// use geometry_overlay::relate::{relate, Dimension};
136///
137/// type P = Point2D<f64, Cartesian>;
138/// let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
139/// let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
140/// let matrix = relate(&a, &b).unwrap();
141/// // Overlapping squares: their interiors meet in an area.
142/// assert_eq!(matrix.interior_interior(), Dimension::Area);
143/// ```
144pub fn relate<G1, G2, P>(g1: &G1, g2: &G2) -> Result<De9im, OverlayError>
145where
146    G1: PolygonTrait<Point = P>,
147    G2: PolygonTrait<Point = P>,
148    P: PointMut + Default + Copy,
149    P::Scalar: CoordinateScalar + Into<f64>,
150    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
151{
152    // Out-of-range coordinates make the turn collector silently drop
153    // intersections, so an emptied turn graph would be misread as
154    // disjoint (II = Empty) — a wrong answer. Refuse up front, matching
155    // the boolean overlay operations (see `crate::predicate::range_guard`).
156    if !polygon_in_range(g1) || !polygon_in_range(g2) {
157        return Err(OverlayError::Unsupported);
158    }
159
160    let r1 = g1.exterior();
161    let r2 = g2.exterior();
162    let turns = get_turns_ring_ring(r1, 0, RingKind::Exterior, r2, 1, RingKind::Exterior);
163    let boundaries_meet = !turns.is_empty();
164    // A transversal crossing (not a mere touch/collinear contact) is the
165    // signal that the interiors genuinely overlap in area.
166    let boundaries_cross_transversally = turns.iter().any(|t| t.method == Method::Crosses);
167
168    // Interior representatives.
169    let rep1 = point_on_surface(g1);
170    let rep2 = point_on_surface(g2);
171
172    // Is g1's interior point strictly inside g2, and vice versa? (Boost's
173    // `within` is strict-interior, which is what we want here.)
174    let g2_model = clone_polygon(g2);
175    let g1_model = clone_polygon(g1);
176    let rep1_in_g2 = rep1.is_some_and(|p| geometry_algorithm::within(&p, &g2_model));
177    let rep2_in_g1 = rep2.is_some_and(|p| geometry_algorithm::within(&p, &g1_model));
178
179    // Ambiguous overlap: the boundaries touch, but neither a transversal
180    // crossing nor a rep-point containment witnesses an interior overlap.
181    // A single interior sample can miss an overlap region that lies off to
182    // one side (e.g. two edge-aligned bands, or a diamond whose crossings
183    // all land on B's vertices). We cannot soundly decide II here, so we
184    // refuse rather than under-report. Note the order: transversal cross
185    // OR either containment makes the relation decidable and is handled
186    // below; only their joint absence *with* boundary contact is unsafe.
187    if boundaries_meet && !boundaries_cross_transversally && !rep1_in_g2 && !rep2_in_g1 {
188        return Err(OverlayError::Unsupported);
189    }
190
191    let interiors_overlap = boundaries_cross_transversally || rep1_in_g2 || rep2_in_g1;
192
193    let mut m = [[Dimension::Empty; 3]; 3];
194
195    // Interior/Interior: area when the interiors genuinely overlap.
196    if interiors_overlap {
197        m[feature::INTERIOR][feature::INTERIOR] = Dimension::Area;
198    }
199
200    // Boundary/Boundary: the boundaries meet at the turn points.
201    if boundaries_meet {
202        m[feature::BOUNDARY][feature::BOUNDARY] = Dimension::Point;
203    }
204
205    // Interior/Exterior: part of g1's interior lies outside g2 unless g1
206    // is wholly contained in g2. It is contained only when its interior
207    // point is inside g2 *and* the boundaries do not cross transversally.
208    let g1_contained = rep1_in_g2 && !boundaries_cross_transversally;
209    if !g1_contained {
210        m[feature::INTERIOR][feature::EXTERIOR] = Dimension::Area;
211    }
212    let g2_contained = rep2_in_g1 && !boundaries_cross_transversally;
213    if !g2_contained {
214        m[feature::EXTERIOR][feature::INTERIOR] = Dimension::Area;
215    }
216
217    // Exterior/Exterior is always the unbounded plane outside both.
218    m[feature::EXTERIOR][feature::EXTERIOR] = Dimension::Area;
219
220    Ok(De9im { m })
221}
222
223/// `touches`: the boundaries meet but the interiors do not.
224///
225/// Mirrors `boost::geometry::touches` (`algorithms/touches.hpp`) for the
226/// areal × areal case: `II = F` and the boundaries have non-empty
227/// intersection.
228///
229/// # Errors
230///
231/// Propagates [`OverlayError::Unsupported`] from [`relate`] for the
232/// ambiguous non-transversal-contact class (see [`relate`]'s docs).
233pub fn touches<G1, G2, P>(g1: &G1, g2: &G2) -> Result<bool, OverlayError>
234where
235    G1: PolygonTrait<Point = P>,
236    G2: PolygonTrait<Point = P>,
237    P: PointMut + Default + Copy,
238    P::Scalar: CoordinateScalar + Into<f64>,
239    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
240{
241    let matrix = relate(g1, g2)?;
242    Ok(!matrix.interior_interior().is_set() && matrix.boundary_boundary().is_set())
243}
244
245/// `overlaps`: the interiors intersect, and each geometry has interior
246/// points outside the other, at the same dimension.
247///
248/// Mirrors `boost::geometry::overlaps` (`algorithms/overlaps.hpp`) for
249/// areal × areal: `II = 2`, `IE = 2`, and `EI = 2`.
250///
251/// # Errors
252///
253/// Propagates [`OverlayError::Unsupported`] from [`relate`] for the
254/// ambiguous non-transversal-contact class (see [`relate`]'s docs). This
255/// keeps `overlaps` from silently reporting `false` for two polygons that
256/// genuinely overlap along an edge-aligned or vertex-only boundary.
257pub fn overlaps<G1, G2, P>(g1: &G1, g2: &G2) -> Result<bool, OverlayError>
258where
259    G1: PolygonTrait<Point = P>,
260    G2: PolygonTrait<Point = P>,
261    P: PointMut + Default + Copy,
262    P::Scalar: CoordinateScalar + Into<f64>,
263    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
264{
265    let matrix = relate(g1, g2)?;
266    Ok(matrix.interior_interior() == Dimension::Area
267        && matrix.interior_exterior() == Dimension::Area
268        && matrix.exterior_interior() == Dimension::Area)
269}
270
271/// `crosses`: for two areal geometries this is always `false` — crossing
272/// is defined only for geometries of differing dimension (e.g. a line
273/// crossing an area).
274///
275/// Mirrors `boost::geometry::crosses` (`algorithms/crosses.hpp`); the
276/// areal × areal arm returns `false` by definition. Provided for
277/// completeness so callers get a uniform predicate surface. It never
278/// inspects the (possibly ambiguous) turn graph, so it is infallible;
279/// the `Result` return keeps its signature uniform with the sibling
280/// predicates [`overlaps`] / [`touches`].
281///
282/// # Errors
283///
284/// Never returns an error; the `Ok(false)` is unconditional.
285#[allow(
286    clippy::unnecessary_wraps,
287    reason = "The Result is intentional: it keeps `crosses` signature-compatible with the sibling fallible predicates `overlaps`/`touches`, so callers handle one uniform surface."
288)]
289pub fn crosses<G1, G2, P>(_g1: &G1, _g2: &G2) -> Result<bool, OverlayError>
290where
291    G1: PolygonTrait<Point = P>,
292    G2: PolygonTrait<Point = P>,
293    P: Point,
294{
295    Ok(false)
296}
297
298/// Copy any [`PolygonTrait`] into a concrete `model::Polygon`.
299fn clone_polygon<G, P>(g: &G) -> Polygon<P>
300where
301    G: PolygonTrait<Point = P>,
302    P: Point + Copy,
303{
304    let outer: Ring<P> = Ring::from_vec(g.exterior().points().copied().collect());
305    let inners = g
306        .interiors()
307        .map(|r| Ring::from_vec(r.points().copied().collect()))
308        .collect();
309    Polygon::with_inners(outer, inners)
310}
311
312#[cfg(test)]
313mod tests {
314    //! OVL6.T1 / T2 done-when: matrix + predicate values. Mirrors the
315    //! case families in `test/algorithms/relate/` and the
316    //! `touches` / `overlaps` test files.
317
318    use super::{Dimension, crosses, overlaps, relate, touches};
319    use geometry_cs::Cartesian;
320    use geometry_model::{Point2D, Polygon, polygon};
321
322    type P = Point2D<f64, Cartesian>;
323
324    fn square(x: f64, y: f64, s: f64) -> Polygon<P> {
325        polygon![[(x, y), (x + s, y), (x + s, y + s), (x, y + s), (x, y)]]
326    }
327
328    #[test]
329    fn overlapping_squares_overlap() {
330        let a = square(0.0, 0.0, 2.0);
331        let b = square(1.0, 1.0, 2.0);
332        assert_eq!(relate(&a, &b).unwrap().interior_interior(), Dimension::Area);
333        assert!(overlaps(&a, &b).unwrap());
334        assert!(!touches(&a, &b).unwrap());
335        assert!(!crosses(&a, &b).unwrap());
336    }
337
338    #[test]
339    fn edge_touching_squares_are_unsupported() {
340        // Share the edge x = 2 but interiors are disjoint. The boundaries
341        // meet only collinearly and neither interior point is inside the
342        // other, so the turn graph cannot tell a pure edge-touch from an
343        // edge-aligned overlap — the relation is reported unsupported
344        // rather than a possibly-wrong boolean.
345        use crate::operation::OverlayError;
346        let a = square(0.0, 0.0, 2.0);
347        let b = square(2.0, 0.0, 2.0);
348        assert_eq!(relate(&a, &b), Err(OverlayError::Unsupported));
349        assert_eq!(touches(&a, &b), Err(OverlayError::Unsupported));
350        assert_eq!(overlaps(&a, &b), Err(OverlayError::Unsupported));
351    }
352
353    #[test]
354    fn edge_aligned_overlap_is_unsupported_not_false() {
355        // Regression: A = [0,3]x[0,1], B = [2,5]x[0,1] genuinely overlap
356        // in [2,3]x[0,1], but all boundary contacts are collinear and both
357        // interior samples land outside the other. The old code returned
358        // `overlaps = false` here (a wrong answer); now it refuses.
359        use crate::operation::OverlayError;
360        let a: Polygon<P> = polygon![[(0.0, 0.0), (3.0, 0.0), (3.0, 1.0), (0.0, 1.0), (0.0, 0.0)]];
361        let b: Polygon<P> = polygon![[(2.0, 0.0), (5.0, 0.0), (5.0, 1.0), (2.0, 1.0), (2.0, 0.0)]];
362        assert_eq!(overlaps(&a, &b), Err(OverlayError::Unsupported));
363    }
364
365    #[test]
366    fn out_of_range_coordinates_are_unsupported() {
367        // Regression: past ±2^26 the turn kernel silently drops
368        // intersections, so an emptied turn graph would be misread as
369        // disjoint (II=Empty) for genuinely overlapping huge polygons.
370        // relate must refuse rather than return that wrong matrix.
371        use crate::operation::OverlayError;
372        let a: Polygon<P> = polygon![[
373            (0.0, 0.0),
374            (2e14, 0.0),
375            (2e14, 2e14),
376            (0.0, 2e14),
377            (0.0, 0.0)
378        ]];
379        let b: Polygon<P> = polygon![[
380            (1e14, 1e14),
381            (3e14, 1e14),
382            (3e14, 3e14),
383            (1e14, 3e14),
384            (1e14, 1e14)
385        ]];
386        assert_eq!(relate(&a, &b), Err(OverlayError::Unsupported));
387        assert_eq!(overlaps(&a, &b), Err(OverlayError::Unsupported));
388    }
389
390    #[test]
391    fn disjoint_squares_neither() {
392        let a = square(0.0, 0.0, 1.0);
393        let b = square(5.0, 5.0, 1.0);
394        assert!(!touches(&a, &b).unwrap());
395        assert!(!overlaps(&a, &b).unwrap());
396        assert_eq!(
397            relate(&a, &b).unwrap().interior_interior(),
398            Dimension::Empty
399        );
400    }
401
402    #[test]
403    fn contained_square_does_not_overlap_or_touch() {
404        // small ⊂ big: interiors meet (II = area) but small has no
405        // interior outside big, so it is containment, not overlap. The
406        // rep-point containment makes this decidable (no boundary touch),
407        // so it is answered normally.
408        let big = square(0.0, 0.0, 10.0);
409        let small = square(3.0, 3.0, 2.0);
410        assert_eq!(
411            relate(&big, &small).unwrap().interior_interior(),
412            Dimension::Area
413        );
414        assert!(!overlaps(&big, &small).unwrap());
415        assert!(!touches(&big, &small).unwrap());
416    }
417}