Skip to main content

geometry_strategy/
equals.rs

1//! Per-CS strategy for the `equals` set-relation algorithm.
2//!
3//! Compares two geometries for equality up to **vertex sequence**: a
4//! ring may start at a different vertex and run in either direction and
5//! still compare equal, but the two rings must have the *same distinct
6//! vertices*. This is a v1 simplification of `boost::geometry::equals`
7//! (`boost/geometry/algorithms/equals.hpp`), whose areal arm is fully
8//! topological (point-set + area via `collect_vectors`, so a ring with a
9//! redundant collinear vertex still equals the same ring without it).
10//!
11//! Consequence: two polygons that describe the same region but differ in
12//! how many collinear vertices they list compare **not equal** here. Run
13//! `remove_spikes` / `unique` first to normalise vertices if full
14//! topological equality is needed. The full topological comparison is
15//! deferred; see `algorithms/detail/equals/implementation.hpp:36-160`.
16//!
17//! ## Symmetry
18//!
19//! `equals` is symmetric: `equals(a, b) == equals(b, a)`. Only the three
20//! diagonal (same-kind) pairs are implemented, and the algorithm layer
21//! does not need a reversed direction, so no `Reversed` wrapper is
22//! required here.
23//!
24//! ## Tag dispatch (open to foreign types)
25//!
26//! Each diagonal pair is a distinct per-pair strategy struct
27//! ([`EqPointPoint`], [`EqSegmentSegment`], [`EqPolygonPolygon`]) with a
28//! single concept-pair-bounded [`EqualsStrategy`] impl; the tag-keyed
29//! [`EqualsPairStrategy`] picker routes `(A::Kind, B::Kind)` to the right
30//! struct. Because it keys on the tags, a concept-adapted foreign type
31//! resolves through the same path as the equivalent `geometry-model`
32//! value (see `specs/open-tag-dispatch/`).
33
34use geometry_coords::CoordinateScalar;
35use geometry_cs::{CartesianFamily, CoordinateSystem};
36use geometry_tag::{PointTag, PolygonTag, SameAs, SegmentTag};
37use geometry_trait::{
38    Point as PointTrait, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
39    Segment as SegmentTrait, segment_end, segment_start,
40};
41
42/// A strategy for "do these two geometries describe the same point
43/// set?".
44///
45/// Mirrors `boost::geometry::equals(g1, g2)` from
46/// `boost/geometry/algorithms/equals.hpp`.
47pub trait EqualsStrategy<A, B> {
48    /// `true` iff `a` and `b` describe the same point set.
49    fn equals(&self, a: &A, b: &B) -> bool;
50}
51
52/// Cartesian equals for a pair of points. See the [module docs](self).
53#[derive(Debug, Default, Clone, Copy)]
54pub struct EqPointPoint;
55/// Cartesian equals for a pair of segments. See the [module docs](self).
56#[derive(Debug, Default, Clone, Copy)]
57pub struct EqSegmentSegment;
58/// Cartesian equals for a pair of polygons. See the [module docs](self).
59#[derive(Debug, Default, Clone, Copy)]
60pub struct EqPolygonPolygon;
61
62// ---- Point × Point ---------------------------------------------------
63//
64// Coordinate-wise equality. Mirrors the pointlike/pointlike arm at
65// `algorithms/detail/equals/implementation.hpp:36-71`.
66
67impl<A, B> EqualsStrategy<A, B> for EqPointPoint
68where
69    A: PointTrait,
70    B: PointTrait<Scalar = A::Scalar>,
71    <A::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
72{
73    #[inline]
74    fn equals(&self, a: &A, b: &B) -> bool {
75        let mut i = 0;
76        while i < A::DIM {
77            let eq = match i {
78                0 => a.get::<0>() == b.get::<0>(),
79                1 => a.get::<1>() == b.get::<1>(),
80                2 => a.get::<2>() == b.get::<2>(),
81                _ => true,
82            };
83            if !eq {
84                return false;
85            }
86            i += 1;
87        }
88        true
89    }
90}
91
92// ---- Segment × Segment -----------------------------------------------
93//
94// Two segments are equal iff they describe the same point set —
95// matching endpoints in either direction. Mirrors the segment/segment
96// arm at `algorithms/detail/equals/implementation.hpp:73-120`.
97
98impl<A, B, P> EqualsStrategy<A, B> for EqSegmentSegment
99where
100    A: SegmentTrait<Point = P>,
101    B: SegmentTrait<Point = P>,
102    P: PointTrait + PointMut + Default,
103    P::Scalar: CoordinateScalar,
104    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
105{
106    #[inline]
107    fn equals(&self, a: &A, b: &B) -> bool {
108        let (a1, a2) = (segment_start(a), segment_end(a));
109        let (b1, b2) = (segment_start(b), segment_end(b));
110        (point_eq_2d(&a1, &b1) && point_eq_2d(&a2, &b2))
111            || (point_eq_2d(&a1, &b2) && point_eq_2d(&a2, &b1))
112    }
113}
114
115// ---- Polygon × Polygon -----------------------------------------------
116//
117// Two polygons are equal iff their exterior rings describe the same
118// closed loop (modulo starting vertex and traversal direction) and
119// their interior rings match pairwise under some permutation.
120// Mirrors the polygon/polygon arm at
121// `algorithms/detail/equals/implementation.hpp:120-160`.
122
123impl<A, B, P> EqualsStrategy<A, B> for EqPolygonPolygon
124where
125    A: PolygonTrait<Point = P>,
126    // Both operands share the same ring type — this keeps the pair a
127    // true diagonal (a `ModelPolygon<P, CW, CL>` compares only against a
128    // polygon with the same `Ring<P, CW, CL>`) so vertex order/closure
129    // conventions line up and the two operands' const params unify.
130    B: PolygonTrait<Point = P, Ring = A::Ring>,
131    P: PointTrait,
132    P::Scalar: CoordinateScalar,
133    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
134{
135    fn equals(&self, a: &A, b: &B) -> bool {
136        if !rings_equal(a.exterior(), b.exterior()) {
137            return false;
138        }
139        if a.interiors().count() != b.interiors().count() {
140            return false;
141        }
142        // For each inner ring in `a`, find a matching inner ring in
143        // `b` not yet consumed. v1: O(n^2) — interior counts are
144        // tiny in practice.
145        let bh: alloc::vec::Vec<&B::Ring> = b.interiors().collect();
146        let mut matched = alloc::vec![false; bh.len()];
147        for ha in a.interiors() {
148            let mut found = false;
149            for (j, hb) in bh.iter().enumerate() {
150                if !matched[j] && rings_equal(ha, *hb) {
151                    matched[j] = true;
152                    found = true;
153                    break;
154                }
155            }
156            if !found {
157                return false;
158            }
159        }
160        true
161    }
162}
163
164/// Type-level "which `EqualsStrategy` struct does this ordered pair of
165/// geometry *kinds* use". A trait parameterised by the second tag `K2`,
166/// keyed on the first tag `Self` — disjoint on the pair, so no overlap.
167/// Only the three diagonal (same-kind) pairs are implemented. The
168/// [`crate::equals`] free function routes `(A::Kind, B::Kind)` through
169/// this trait.
170#[doc(hidden)]
171pub trait EqualsPairStrategy<K2> {
172    /// The per-pair [`EqualsStrategy`] struct this tag pair is computed
173    /// with.
174    type S: Default;
175}
176
177impl EqualsPairStrategy<PointTag> for PointTag {
178    type S = EqPointPoint;
179}
180impl EqualsPairStrategy<SegmentTag> for SegmentTag {
181    type S = EqSegmentSegment;
182}
183impl EqualsPairStrategy<PolygonTag> for PolygonTag {
184    type S = EqPolygonPolygon;
185}
186
187extern crate alloc;
188
189// ---- Kernels ---------------------------------------------------------
190
191#[inline]
192fn point_eq_2d<Pa, Pb>(a: &Pa, b: &Pb) -> bool
193where
194    Pa: PointTrait,
195    Pb: PointTrait<Scalar = Pa::Scalar>,
196{
197    a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
198}
199
200/// Are two rings equal as closed loops? Equal distinct-vertex sequence
201/// up to rotation and reversal (free starting vertex, free direction).
202/// This is the v1 vertex-sequence simplification of Boost's topological
203/// `equals::ring_or_polygon::apply`
204/// (`algorithms/detail/equals/implementation.hpp:120-160`): a ring with
205/// a redundant collinear vertex will *not* match one without it.
206fn rings_equal<Ra, Rb>(a: &Ra, b: &Rb) -> bool
207where
208    Ra: RingTrait,
209    Rb: RingTrait,
210    Ra::Point: PointTrait,
211    Rb::Point: PointTrait<Scalar = <Ra::Point as PointTrait>::Scalar>,
212{
213    let av = normalise_ring(a);
214    let bv = normalise_ring(b);
215    if av.len() != bv.len() {
216        return false;
217    }
218    let n = av.len();
219    if n == 0 {
220        return true;
221    }
222    // Try every rotation of `b`, forward and reversed.
223    for start in 0..n {
224        if cyclic_match(&av, &bv, start, false) {
225            return true;
226        }
227        if cyclic_match(&av, &bv, start, true) {
228            return true;
229        }
230    }
231    false
232}
233
234/// Strip the trailing closing vertex from a closed ring so the
235/// rotation search compares only the distinct loop vertices.
236fn normalise_ring<R>(r: &R) -> alloc::vec::Vec<&R::Point>
237where
238    R: RingTrait,
239    R::Point: PointTrait,
240{
241    let pts: alloc::vec::Vec<&R::Point> = r.points().collect();
242    if pts.len() >= 2 && point_eq_2d(pts[0], pts[pts.len() - 1]) {
243        pts[..pts.len() - 1].to_vec()
244    } else {
245        pts
246    }
247}
248
249/// Does `a` match `b` when `b` is read starting at `start` and
250/// optionally in reverse?
251fn cyclic_match<Pa, Pb>(a: &[&Pa], b: &[&Pb], start: usize, reverse: bool) -> bool
252where
253    Pa: PointTrait,
254    Pb: PointTrait<Scalar = Pa::Scalar>,
255{
256    let n = a.len();
257    for (i, ai) in a.iter().enumerate() {
258        let j = if reverse {
259            (start + n - i) % n
260        } else {
261            (start + i) % n
262        };
263        if !point_eq_2d(*ai, b[j]) {
264            return false;
265        }
266    }
267    true
268}
269
270#[cfg(test)]
271mod tests {
272    use super::{EqPointPoint, EqPolygonPolygon, EqSegmentSegment, EqualsStrategy};
273    use geometry_cs::Cartesian;
274    use geometry_model::{Point2D, Polygon, Segment, polygon};
275
276    type P = Point2D<f64, Cartesian>;
277
278    fn pt(x: f64, y: f64) -> P {
279        Point2D::new(x, y)
280    }
281
282    #[test]
283    fn equals_same_point() {
284        assert!(EqPointPoint.equals(&pt(1.0, 2.0), &pt(1.0, 2.0)));
285        assert!(!EqPointPoint.equals(&pt(1.0, 2.0), &pt(1.0, 2.1)));
286    }
287
288    #[test]
289    fn equals_segment_either_direction() {
290        let a = Segment::new(pt(0.0, 0.0), pt(1.0, 1.0));
291        let b = Segment::new(pt(1.0, 1.0), pt(0.0, 0.0));
292        assert!(EqSegmentSegment.equals(&a, &b));
293        let c = Segment::new(pt(0.0, 0.0), pt(1.0, 2.0));
294        assert!(!EqSegmentSegment.equals(&a, &c));
295    }
296
297    #[test]
298    fn equals_polygon_rotated_start() {
299        let a: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]];
300        // Same loop, different starting vertex.
301        let b: Polygon<P> = polygon![[(4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0), (4.0, 0.0)]];
302        assert!(EqPolygonPolygon.equals(&a, &b));
303    }
304
305    #[test]
306    fn equals_polygon_reversed_direction() {
307        let a: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]];
308        let b: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)]];
309        assert!(EqPolygonPolygon.equals(&a, &b));
310    }
311
312    #[test]
313    fn polygon_not_equals_different_shape() {
314        let a: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]];
315        let b: Polygon<P> = polygon![[(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)]];
316        assert!(!EqPolygonPolygon.equals(&a, &b));
317    }
318
319    // KC1.T2 witness: proves this strategy accepts read-only `Point`
320    // operands (that need not implement `PointMut`). If it compiles,
321    // the read-only bound is locked.
322    fn _accepts_readonly_point<A, B, S>(s: &S, a: &A, b: &B) -> bool
323    where
324        A: geometry_trait::Point,
325        B: geometry_trait::Point,
326        S: EqualsStrategy<A, B>,
327    {
328        s.equals(a, b)
329    }
330}