Skip to main content

geometry_algorithm/
correct.rs

1//! `correct(&mut g)` — fix ring closure and orientation in place.
2//!
3//! Mirrors `boost::geometry::correct` from
4//! `boost/geometry/algorithms/correct.hpp` and the closure-fix helper
5//! at `algorithms/correct_closure.hpp`.
6//!
7//! Per-kind:
8//!
9//! * `Ring<P, CW, true>`   → push closing vertex if missing
10//! * `Ring<P, CW, false>`  → pop closing vertex if duplicated
11//! * exterior ring         → reverse so its *strategy-level* signed
12//!   area (which already folds the declared `PointOrder`) is positive
13//!   — i.e. the stored order matches the declaration, for CW- and
14//!   CCW-declared rings alike
15//! * `Polygon` outer       → as above; inners → opposite of outer
16//! * `MultiPolygon`        → correct each member
17//!
18//! Empty and 1-point rings are left unchanged (silent no-op, matching
19//! Boost). Cartesian-only: the orientation test uses the Cartesian
20//! shoelace area.
21
22use geometry_coords::CoordinateScalar;
23use geometry_cs::CoordinateSystem;
24use geometry_model::{MultiPolygon, Polygon, Ring};
25use geometry_strategy::{AreaStrategy, ShoelaceArea};
26use geometry_trait::{Closure, Point as PointTrait, Ring as RingTrait};
27
28/// Fix closure and orientation of `g` in place.
29///
30/// Mirrors `boost::geometry::correct(g)` from
31/// `boost/geometry/algorithms/correct.hpp`.
32pub fn correct<G: Correct>(g: &mut G) {
33    g.correct();
34}
35
36/// Per-kind correction dispatch.
37#[doc(hidden)]
38pub trait Correct {
39    fn correct(&mut self);
40}
41
42/// Add or drop the closing vertex so the stored point sequence matches
43/// the ring's `CLOSED` const-generic.
44fn fix_closure<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>)
45where
46    P: PointTrait + Copy,
47{
48    // Rings of two or fewer points are degenerate — closing one would
49    // just append a spurious `[a, b, a]`. Boost leaves them untouched
50    // (`algorithms/correct_closure.hpp:59`, `if (size <= 2) return;`).
51    if r.0.len() <= 2 {
52        return;
53    }
54    let first = r.0[0];
55    let last = *r.0.last().unwrap();
56    let should_be_closed = matches!(r.closure(), Closure::Closed);
57    let already_closed = coords_equal(&first, &last);
58    match (should_be_closed, already_closed) {
59        (true, false) => r.0.push(first), // close it
60        (false, true) => {
61            r.0.pop(); // open it
62        }
63        _ => {}
64    }
65}
66
67/// Coordinate-wise equality — `Point<T, D, Cs>` does not derive a
68/// usable `PartialEq` (the derive would demand `Cs: PartialEq`), so we
69/// compare per dimension via `get::<D>`.
70fn coords_equal<P: PointTrait>(a: &P, b: &P) -> bool {
71    geometry_trait::fold_dims(true, a, |acc, _p, d| {
72        acc && match d {
73            0 => a.get::<0>() == b.get::<0>(),
74            1 => a.get::<1>() == b.get::<1>(),
75            2 => a.get::<2>() == b.get::<2>(),
76            3 => a.get::<3>() == b.get::<3>(),
77            _ => unreachable!("fold_dims caps at MAX_DIM"),
78        }
79    })
80}
81
82/// Reverse `r` if its signed area sign disagrees with `want_positive`.
83fn fix_orientation<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>, want_positive: bool)
84where
85    P: PointTrait,
86    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
87{
88    let a = ShoelaceArea.area(&*r);
89    let zero = <P::Scalar as CoordinateScalar>::ZERO;
90    let is_positive = a > zero;
91    let is_negative = a < zero;
92    // Only reverse when the sign is decisively wrong; a zero-area
93    // (degenerate) ring is left as-is.
94    if (want_positive && is_negative) || (!want_positive && is_positive) {
95        r.0.reverse();
96    }
97}
98
99impl<P, const CW: bool, const CL: bool> Correct for Ring<P, CW, CL>
100where
101    P: PointTrait + Copy,
102    P::Cs: CoordinateSystem,
103    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
104{
105    fn correct(&mut self) {
106        fix_closure(self);
107        // `ShoelaceArea` already folds the declared `PointOrder` into
108        // its sign: a ring stored in its declared direction has a
109        // POSITIVE strategy-level area for CW-declared and
110        // CCW-declared rings alike. So the target sign of a corrected
111        // standalone ring is always positive — passing `CW` here would
112        // double-apply the declaration flip and reverse correctly
113        // wound CCW rings.
114        fix_orientation(self, true);
115    }
116}
117
118impl<P, const CW: bool, const CL: bool> Correct for Polygon<P, CW, CL>
119where
120    P: PointTrait + Copy,
121    P::Cs: CoordinateSystem,
122    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
123{
124    fn correct(&mut self) {
125        fix_closure(&mut self.outer);
126        // Exterior: stored order must match the declaration —
127        // strategy-level area positive (see the Ring impl above).
128        fix_orientation(&mut self.outer, true);
129        for inner in &mut self.inners {
130            fix_closure(inner);
131            // Interior rings wind opposite the exterior, i.e. opposite
132            // their own declared order — strategy-level area negative.
133            // That is the state `ShoelacePolygonArea`'s plain ring-sum
134            // relies on (holes arrive negatively signed).
135            fix_orientation(inner, false);
136        }
137    }
138}
139
140impl<Pg: Correct + geometry_trait::Polygon> Correct for MultiPolygon<Pg> {
141    fn correct(&mut self) {
142        for p in &mut self.0 {
143            p.correct();
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    //! Reference behaviour from
151    //! `boost/geometry/test/algorithms/correct.cpp`: a
152    //! counter-clockwise-stored exterior of a clockwise-declared ring
153    //! is reversed so its signed area becomes positive.
154
155    #![allow(clippy::float_cmp, reason = "Areas are exact integer literals.")]
156
157    use super::correct;
158    use crate::area::ring_area;
159    use geometry_cs::Cartesian;
160    use geometry_model::{Point2D, Ring};
161
162    type P = Point2D<f64, Cartesian>;
163
164    #[test]
165    fn ccw_exterior_of_cw_ring_is_reversed() {
166        // A 2×2 square stored counter-clockwise. Declared CW (default),
167        // so its signed area is negative until `correct` reverses it.
168        let mut r: Ring<P> = Ring::from_vec(vec![
169            P::new(0.0, 0.0),
170            P::new(2.0, 0.0),
171            P::new(2.0, 2.0),
172            P::new(0.0, 2.0),
173            P::new(0.0, 0.0),
174        ]);
175        assert!(ring_area(&r) < 0.0, "precondition: CCW ring is negative");
176        correct(&mut r);
177        assert_eq!(ring_area(&r), 4.0);
178    }
179
180    #[test]
181    fn already_correct_ring_is_unchanged() {
182        // Same square stored clockwise — already positive; correct is a
183        // no-op on orientation.
184        let mut r: Ring<P> = Ring::from_vec(vec![
185            P::new(0.0, 0.0),
186            P::new(0.0, 2.0),
187            P::new(2.0, 2.0),
188            P::new(2.0, 0.0),
189            P::new(0.0, 0.0),
190        ]);
191        assert_eq!(ring_area(&r), 4.0);
192        correct(&mut r);
193        assert_eq!(ring_area(&r), 4.0);
194    }
195
196    #[test]
197    fn two_point_ring_is_left_untouched() {
198        // Regression: a degenerate 2-point ring must NOT be "closed" into
199        // a spurious [a, b, a]. Boost leaves rings of size <= 2 alone
200        // (correct_closure.hpp:59).
201        use geometry_trait::Ring as _;
202        let mut r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 1.0)]);
203        correct(&mut r);
204        assert_eq!(r.points().count(), 2, "2-point ring must stay 2 points");
205    }
206
207    /// An *open*-declared ring (`CLOSED = false`) that is stored with a
208    /// redundant closing vertex has it dropped by `fix_closure` — the
209    /// `(should_be_closed=false, already_closed=true)` arm.
210    #[test]
211    fn open_declared_ring_drops_redundant_closing_vertex() {
212        use geometry_trait::Ring as _;
213        // CW (true), OPEN (false), stored closed (first == last).
214        let mut r: Ring<P, true, false> = Ring::from_vec(vec![
215            P::new(0.0, 0.0),
216            P::new(0.0, 2.0),
217            P::new(2.0, 2.0),
218            P::new(2.0, 0.0),
219            P::new(0.0, 0.0),
220        ]);
221        correct(&mut r);
222        // The closing vertex is removed: 5 stored points → 4.
223        assert_eq!(r.points().count(), 4);
224        // Area is unchanged by the closure fix (still the 2×2 square).
225        assert_eq!(ring_area(&r), 4.0);
226    }
227
228    /// `correct` on a `MultiPolygon` corrects each member polygon: a
229    /// CCW-stored member is re-wound to a positive exterior area.
230    #[test]
231    fn multipolygon_corrects_each_member() {
232        use geometry_model::{MultiPolygon, Polygon};
233        let ccw_square = || {
234            Polygon::<P>::new(Ring::from_vec(vec![
235                P::new(0.0, 0.0),
236                P::new(2.0, 0.0),
237                P::new(2.0, 2.0),
238                P::new(0.0, 2.0),
239                P::new(0.0, 0.0),
240            ]))
241        };
242        let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![ccw_square(), ccw_square()]);
243        // Precondition: both exteriors are negative (CCW, CW-declared).
244        assert!(ring_area(&mpg.0[0].outer) < 0.0);
245        correct(&mut mpg);
246        assert_eq!(ring_area(&mpg.0[0].outer), 4.0);
247        assert_eq!(ring_area(&mpg.0[1].outer), 4.0);
248    }
249
250    /// A 3D ring whose stored closing vertex matches the first in all
251    /// three ordinates is recognised as already-closed — exercising the
252    /// `coords_equal` `D == 2` arm.
253    #[test]
254    fn coords_equal_compares_the_third_ordinate() {
255        use geometry_model::Point3D;
256        use geometry_trait::Ring as _;
257        type P3 = Point3D<f64, Cartesian>;
258        // Open-declared 3D ring stored closed in x, y AND z. `fix_closure`
259        // only drops the closer if `coords_equal` returns true across all
260        // three ordinates.
261        let mut r: Ring<P3, true, false> = Ring::from_vec(vec![
262            P3::new(0.0, 0.0, 5.0),
263            P3::new(1.0, 0.0, 5.0),
264            P3::new(1.0, 1.0, 5.0),
265            P3::new(0.0, 0.0, 5.0),
266        ]);
267        // Note: this ring's orientation correction relies on ShoelaceArea,
268        // which is defined for these Cartesian points; we only assert the
269        // closure fix here.
270        let before = r.points().count();
271        super::fix_closure(&mut r);
272        assert_eq!(before, 4);
273        assert_eq!(r.points().count(), 3, "closing vertex dropped");
274
275        // And a ring whose z differs at the closer is NOT already closed.
276        let mut open: Ring<P3, true, false> = Ring::from_vec(vec![
277            P3::new(0.0, 0.0, 5.0),
278            P3::new(1.0, 0.0, 5.0),
279            P3::new(1.0, 1.0, 5.0),
280            P3::new(0.0, 0.0, 9.0), // same x,y — different z
281        ]);
282        super::fix_closure(&mut open);
283        assert_eq!(open.points().count(), 4, "z differs → not closed → kept");
284    }
285
286    #[test]
287    fn ccw_ring_correctly_wound_is_a_noop() {
288        // Regression: `fix_orientation(self, CW)` used to reverse a
289        // CORRECTLY wound CCW-declared ring (+2 → −2), because
290        // `ShoelaceArea` already folds the declared order into its
291        // sign. Fixture mirrors geometry-strategy's
292        // `ccw_declared_ccw_traversed_diamond_is_2`.
293        let mut r: Ring<P, false> = Ring::from_vec(vec![
294            P::new(1.0, 0.0),
295            P::new(0.0, 1.0),
296            P::new(-1.0, 0.0),
297            P::new(0.0, -1.0),
298            P::new(1.0, 0.0),
299        ]);
300        assert_eq!(ring_area(&r), 2.0, "precondition: correctly wound");
301        correct(&mut r);
302        assert_eq!(ring_area(&r), 2.0, "correct() must be a no-op");
303    }
304
305    #[test]
306    fn ccw_ring_wrongly_wound_is_reversed() {
307        // The same diamond stored clockwise under a CCW declaration:
308        // strategy area −2 → correct() reverses → +2.
309        let mut r: Ring<P, false> = Ring::from_vec(vec![
310            P::new(1.0, 0.0),
311            P::new(0.0, -1.0),
312            P::new(-1.0, 0.0),
313            P::new(0.0, 1.0),
314            P::new(1.0, 0.0),
315        ]);
316        assert_eq!(ring_area(&r), -2.0, "precondition: wrongly wound");
317        correct(&mut r);
318        assert_eq!(ring_area(&r), 2.0);
319    }
320
321    #[test]
322    fn ccw_polygon_with_hole_correctly_wound_is_a_noop() {
323        // Outer 4x4 stored CCW (matches declaration, ring_area +16),
324        // hole 1x1 stored CW (opposite, ring_area −1). correct() must
325        // change nothing: this is exactly the state
326        // ShoelacePolygonArea's ring-sum (+15) relies on.
327        use geometry_model::Polygon;
328        let outer: Ring<P, false> = Ring::from_vec(vec![
329            P::new(0.0, 0.0),
330            P::new(4.0, 0.0),
331            P::new(4.0, 4.0),
332            P::new(0.0, 4.0),
333            P::new(0.0, 0.0),
334        ]);
335        let hole: Ring<P, false> = Ring::from_vec(vec![
336            P::new(1.0, 1.0),
337            P::new(1.0, 2.0),
338            P::new(2.0, 2.0),
339            P::new(2.0, 1.0),
340            P::new(1.0, 1.0),
341        ]);
342        let mut pg: Polygon<P, false> = Polygon::new(outer);
343        pg.inners.push(hole);
344        assert_eq!(ring_area(&pg.outer), 16.0, "precondition: outer CCW-stored");
345        assert_eq!(
346            ring_area(&pg.inners[0]),
347            -1.0,
348            "precondition: hole CW-stored"
349        );
350        correct(&mut pg);
351        assert_eq!(ring_area(&pg.outer), 16.0, "outer must be untouched");
352        assert_eq!(ring_area(&pg.inners[0]), -1.0, "hole must be untouched");
353    }
354
355    #[test]
356    fn ccw_polygon_wrongly_wound_is_fixed() {
357        // Outer stored CW (wrong for a CCW declaration), hole stored
358        // CCW (wrong for a hole): correct() reverses both.
359        use geometry_model::Polygon;
360        let outer: Ring<P, false> = Ring::from_vec(vec![
361            P::new(0.0, 0.0),
362            P::new(0.0, 4.0),
363            P::new(4.0, 4.0),
364            P::new(4.0, 0.0),
365            P::new(0.0, 0.0),
366        ]);
367        let hole: Ring<P, false> = Ring::from_vec(vec![
368            P::new(1.0, 1.0),
369            P::new(2.0, 1.0),
370            P::new(2.0, 2.0),
371            P::new(1.0, 2.0),
372            P::new(1.0, 1.0),
373        ]);
374        let mut pg: Polygon<P, false> = Polygon::new(outer);
375        pg.inners.push(hole);
376        assert_eq!(ring_area(&pg.outer), -16.0, "precondition: outer wrong");
377        assert_eq!(ring_area(&pg.inners[0]), 1.0, "precondition: hole wrong");
378        correct(&mut pg);
379        assert_eq!(ring_area(&pg.outer), 16.0);
380        assert_eq!(ring_area(&pg.inners[0]), -1.0);
381    }
382}