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::{
25    Box as ModelBox, DynGeometry, DynGeometryCollection, Linestring, MultiLinestring, MultiPoint,
26    MultiPolygon, Point as ModelPoint, Polygon, Ring, Segment,
27};
28use geometry_strategy::{AreaStrategy, ShoelaceArea};
29use geometry_trait::{
30    Closure, Linestring as LinestringTrait, Point as PointTrait, Ring as RingTrait,
31};
32
33/// Fix closure and orientation of `g` in place.
34///
35/// Mirrors `boost::geometry::correct(g)` from
36/// `boost/geometry/algorithms/correct.hpp`.
37pub fn correct<G: Correct>(g: &mut G) {
38    g.correct();
39}
40
41/// Fix only ring closure, leaving orientation unchanged.
42///
43/// Mirrors `boost::geometry::correct_closure(g)` from
44/// `boost/geometry/algorithms/correct_closure.hpp:211-224`. Closed rings
45/// gain a copy of their first point when needed; open rings lose a
46/// duplicated closing point. Polygons apply the same rule to their exterior
47/// and interior rings, and multi-polygons apply it to every member.
48#[inline]
49pub fn correct_closure<G: CorrectClosure>(g: &mut G) {
50    g.correct_closure();
51}
52
53/// Per-kind correction dispatch.
54#[doc(hidden)]
55pub trait Correct {
56    fn correct(&mut self);
57}
58
59/// Per-kind closure-correction dispatch.
60///
61/// Mirrors the tag-specialized `dispatch::correct_closure` family from
62/// `algorithms/correct_closure.hpp:102-160`.
63#[doc(hidden)]
64pub trait CorrectClosure {
65    fn correct_closure(&mut self);
66}
67
68/// Add or drop the closing vertex so the stored point sequence matches
69/// the ring's `CLOSED` const-generic.
70fn fix_closure<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>)
71where
72    P: PointTrait + Copy,
73{
74    // Rings of two or fewer points are degenerate — closing one would
75    // just append a spurious `[a, b, a]`. Boost leaves them untouched
76    // (`algorithms/correct_closure.hpp:59`, `if (size <= 2) return;`).
77    if r.0.len() <= 2 {
78        return;
79    }
80    let first = r.0[0];
81    let last = *r.0.last().unwrap();
82    let should_be_closed = matches!(r.closure(), Closure::Closed);
83    let already_closed = coords_equal(&first, &last);
84    match (should_be_closed, already_closed) {
85        (true, false) => r.0.push(first), // close it
86        (false, true) => {
87            r.0.pop(); // open it
88        }
89        _ => {}
90    }
91}
92
93/// Coordinate-wise equality — `Point<T, D, Cs>` does not derive a
94/// usable `PartialEq` (the derive would demand `Cs: PartialEq`), so we
95/// compare per dimension via `get::<D>`.
96fn coords_equal<P: PointTrait>(a: &P, b: &P) -> bool {
97    geometry_trait::fold_dims(true, a, |acc, _p, d| {
98        acc && match d {
99            0 => a.get::<0>() == b.get::<0>(),
100            1 => a.get::<1>() == b.get::<1>(),
101            2 => a.get::<2>() == b.get::<2>(),
102            3 => a.get::<3>() == b.get::<3>(),
103            _ => unreachable!("fold_dims caps at MAX_DIM"),
104        }
105    })
106}
107
108/// Reverse `r` if its signed area sign disagrees with `want_positive`.
109fn fix_orientation<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>, want_positive: bool)
110where
111    P: PointTrait,
112    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
113{
114    let a = ShoelaceArea.area(&*r);
115    let zero = <P::Scalar as CoordinateScalar>::ZERO;
116    let is_positive = a > zero;
117    let is_negative = a < zero;
118    // Only reverse when the sign is decisively wrong; a zero-area
119    // (degenerate) ring is left as-is.
120    if (want_positive && is_negative) || (!want_positive && is_positive) {
121        r.0.reverse();
122    }
123}
124
125impl<P, const CW: bool, const CL: bool> Correct for Ring<P, CW, CL>
126where
127    P: PointTrait + Copy,
128    P::Cs: CoordinateSystem,
129    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
130{
131    fn correct(&mut self) {
132        fix_closure(self);
133        // `ShoelaceArea` already folds the declared `PointOrder` into
134        // its sign: a ring stored in its declared direction has a
135        // POSITIVE strategy-level area for CW-declared and
136        // CCW-declared rings alike. So the target sign of a corrected
137        // standalone ring is always positive — passing `CW` here would
138        // double-apply the declaration flip and reverse correctly
139        // wound CCW rings.
140        fix_orientation(self, true);
141    }
142}
143
144/// Mirrors the ring-tag closure arm at
145/// `algorithms/correct_closure.hpp:131-134`.
146impl<P, const CW: bool, const CL: bool> CorrectClosure for Ring<P, CW, CL>
147where
148    P: PointTrait + Copy,
149{
150    fn correct_closure(&mut self) {
151        fix_closure(self);
152    }
153}
154
155impl<P, const CW: bool, const CL: bool> Correct for Polygon<P, CW, CL>
156where
157    P: PointTrait + Copy,
158    P::Cs: CoordinateSystem,
159    ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
160{
161    fn correct(&mut self) {
162        fix_closure(&mut self.outer);
163        // Exterior: stored order must match the declaration —
164        // strategy-level area positive (see the Ring impl above).
165        fix_orientation(&mut self.outer, true);
166        for inner in &mut self.inners {
167            fix_closure(inner);
168            // Interior rings wind opposite the exterior, i.e. opposite
169            // their own declared order — strategy-level area negative.
170            // That is the state `ShoelacePolygonArea`'s plain ring-sum
171            // relies on (holes arrive negatively signed).
172            fix_orientation(inner, false);
173        }
174    }
175}
176
177/// Mirrors the polygon-tag closure arm at
178/// `algorithms/correct_closure.hpp:136-139`.
179impl<P, const CW: bool, const CL: bool> CorrectClosure for Polygon<P, CW, CL>
180where
181    P: PointTrait + Copy,
182{
183    fn correct_closure(&mut self) {
184        fix_closure(&mut self.outer);
185        for inner in &mut self.inners {
186            fix_closure(inner);
187        }
188    }
189}
190
191impl<Pg: Correct + geometry_trait::Polygon> Correct for MultiPolygon<Pg> {
192    fn correct(&mut self) {
193        for p in &mut self.0 {
194            p.correct();
195        }
196    }
197}
198
199/// Mirrors the multi-polygon closure arm at
200/// `algorithms/correct_closure.hpp:154-160`.
201impl<Pg> CorrectClosure for MultiPolygon<Pg>
202where
203    Pg: CorrectClosure + geometry_trait::Polygon,
204{
205    fn correct_closure(&mut self) {
206        for polygon in &mut self.0 {
207            polygon.correct_closure();
208        }
209    }
210}
211
212/// Mirrors the no-op point arm at `algorithms/correct_closure.hpp:110-113`.
213impl<T, const D: usize, Cs> CorrectClosure for ModelPoint<T, D, Cs>
214where
215    T: CoordinateScalar,
216    Cs: CoordinateSystem,
217{
218    fn correct_closure(&mut self) {}
219}
220
221/// Mirrors the no-op linestring arm at
222/// `algorithms/correct_closure.hpp:115-118`.
223impl<P: PointTrait> CorrectClosure for Linestring<P> {
224    fn correct_closure(&mut self) {}
225}
226
227/// Mirrors the no-op segment arm at `algorithms/correct_closure.hpp:120-123`.
228impl<P: PointTrait> CorrectClosure for Segment<P> {
229    fn correct_closure(&mut self) {}
230}
231
232/// Mirrors the no-op box arm at `algorithms/correct_closure.hpp:126-129`.
233impl<P: PointTrait> CorrectClosure for ModelBox<P> {
234    fn correct_closure(&mut self) {}
235}
236
237/// Mirrors the no-op multi-point arm at
238/// `algorithms/correct_closure.hpp:142-145`.
239impl<P: PointTrait> CorrectClosure for MultiPoint<P> {
240    fn correct_closure(&mut self) {}
241}
242
243/// Mirrors the no-op multi-linestring arm at
244/// `algorithms/correct_closure.hpp:148-151`.
245impl<L: LinestringTrait> CorrectClosure for MultiLinestring<L> {
246    fn correct_closure(&mut self) {}
247}
248
249/// Mirrors Boost's dynamic-geometry visitor at
250/// `algorithms/correct_closure.hpp:180-189`.
251impl<T, Cs> CorrectClosure for DynGeometry<T, Cs>
252where
253    T: CoordinateScalar,
254    Cs: CoordinateSystem + Copy,
255{
256    fn correct_closure(&mut self) {
257        match self {
258            DynGeometry::Point(_)
259            | DynGeometry::LineString(_)
260            | DynGeometry::MultiPoint(_)
261            | DynGeometry::MultiLineString(_) => {}
262            DynGeometry::Polygon(polygon) => polygon.correct_closure(),
263            DynGeometry::MultiPolygon(multi_polygon) => multi_polygon.correct_closure(),
264            DynGeometry::GeometryCollection(geometries) => {
265                for geometry in geometries {
266                    geometry.correct_closure();
267                }
268            }
269        }
270    }
271}
272
273/// Mirrors Boost's breadth-first geometry-collection visitor at
274/// `algorithms/correct_closure.hpp:192-203`.
275impl<T, Cs> CorrectClosure for DynGeometryCollection<T, Cs>
276where
277    T: CoordinateScalar,
278    Cs: CoordinateSystem + Copy,
279{
280    fn correct_closure(&mut self) {
281        for geometry in &mut self.0 {
282            geometry.correct_closure();
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    //! Reference behaviour from
290    //! `boost/geometry/test/algorithms/correct.cpp`: a
291    //! counter-clockwise-stored exterior of a clockwise-declared ring
292    //! is reversed so its signed area becomes positive.
293
294    #![allow(clippy::float_cmp, reason = "Areas are exact integer literals.")]
295
296    use super::correct;
297    use crate::area::ring_area;
298    use geometry_cs::Cartesian;
299    use geometry_model::{Point2D, Ring};
300
301    type P = Point2D<f64, Cartesian>;
302
303    #[test]
304    fn ccw_exterior_of_cw_ring_is_reversed() {
305        // A 2×2 square stored counter-clockwise. Declared CW (default),
306        // so its signed area is negative until `correct` reverses it.
307        let mut r: Ring<P> = Ring::from_vec(vec![
308            P::new(0.0, 0.0),
309            P::new(2.0, 0.0),
310            P::new(2.0, 2.0),
311            P::new(0.0, 2.0),
312            P::new(0.0, 0.0),
313        ]);
314        assert!(ring_area(&r) < 0.0, "precondition: CCW ring is negative");
315        correct(&mut r);
316        assert_eq!(ring_area(&r), 4.0);
317    }
318
319    #[test]
320    fn already_correct_ring_is_unchanged() {
321        // Same square stored clockwise — already positive; correct is a
322        // no-op on orientation.
323        let mut r: Ring<P> = Ring::from_vec(vec![
324            P::new(0.0, 0.0),
325            P::new(0.0, 2.0),
326            P::new(2.0, 2.0),
327            P::new(2.0, 0.0),
328            P::new(0.0, 0.0),
329        ]);
330        assert_eq!(ring_area(&r), 4.0);
331        correct(&mut r);
332        assert_eq!(ring_area(&r), 4.0);
333    }
334
335    #[test]
336    fn two_point_ring_is_left_untouched() {
337        // Regression: a degenerate 2-point ring must NOT be "closed" into
338        // a spurious [a, b, a]. Boost leaves rings of size <= 2 alone
339        // (correct_closure.hpp:59).
340        use geometry_trait::Ring as _;
341        let mut r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 1.0)]);
342        correct(&mut r);
343        assert_eq!(r.points().count(), 2, "2-point ring must stay 2 points");
344    }
345
346    /// An *open*-declared ring (`CLOSED = false`) that is stored with a
347    /// redundant closing vertex has it dropped by `fix_closure` — the
348    /// `(should_be_closed=false, already_closed=true)` arm.
349    #[test]
350    fn open_declared_ring_drops_redundant_closing_vertex() {
351        use geometry_trait::Ring as _;
352        // CW (true), OPEN (false), stored closed (first == last).
353        let mut r: Ring<P, true, false> = Ring::from_vec(vec![
354            P::new(0.0, 0.0),
355            P::new(0.0, 2.0),
356            P::new(2.0, 2.0),
357            P::new(2.0, 0.0),
358            P::new(0.0, 0.0),
359        ]);
360        correct(&mut r);
361        // The closing vertex is removed: 5 stored points → 4.
362        assert_eq!(r.points().count(), 4);
363        // Area is unchanged by the closure fix (still the 2×2 square).
364        assert_eq!(ring_area(&r), 4.0);
365    }
366
367    /// `correct` on a `MultiPolygon` corrects each member polygon: a
368    /// CCW-stored member is re-wound to a positive exterior area.
369    #[test]
370    fn multipolygon_corrects_each_member() {
371        use geometry_model::{MultiPolygon, Polygon};
372        let ccw_square = || {
373            Polygon::<P>::new(Ring::from_vec(vec![
374                P::new(0.0, 0.0),
375                P::new(2.0, 0.0),
376                P::new(2.0, 2.0),
377                P::new(0.0, 2.0),
378                P::new(0.0, 0.0),
379            ]))
380        };
381        let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![ccw_square(), ccw_square()]);
382        // Precondition: both exteriors are negative (CCW, CW-declared).
383        assert!(ring_area(&mpg.0[0].outer) < 0.0);
384        correct(&mut mpg);
385        assert_eq!(ring_area(&mpg.0[0].outer), 4.0);
386        assert_eq!(ring_area(&mpg.0[1].outer), 4.0);
387    }
388
389    /// A 3D ring whose stored closing vertex matches the first in all
390    /// three ordinates is recognised as already-closed — exercising the
391    /// `coords_equal` `D == 2` arm.
392    #[test]
393    fn coords_equal_compares_the_third_ordinate() {
394        use geometry_model::Point3D;
395        use geometry_trait::Ring as _;
396        type P3 = Point3D<f64, Cartesian>;
397        // Open-declared 3D ring stored closed in x, y AND z. `fix_closure`
398        // only drops the closer if `coords_equal` returns true across all
399        // three ordinates.
400        let mut r: Ring<P3, true, false> = Ring::from_vec(vec![
401            P3::new(0.0, 0.0, 5.0),
402            P3::new(1.0, 0.0, 5.0),
403            P3::new(1.0, 1.0, 5.0),
404            P3::new(0.0, 0.0, 5.0),
405        ]);
406        // Note: this ring's orientation correction relies on ShoelaceArea,
407        // which is defined for these Cartesian points; we only assert the
408        // closure fix here.
409        let before = r.points().count();
410        super::fix_closure(&mut r);
411        assert_eq!(before, 4);
412        assert_eq!(r.points().count(), 3, "closing vertex dropped");
413
414        // And a ring whose z differs at the closer is NOT already closed.
415        let mut open: Ring<P3, true, false> = Ring::from_vec(vec![
416            P3::new(0.0, 0.0, 5.0),
417            P3::new(1.0, 0.0, 5.0),
418            P3::new(1.0, 1.0, 5.0),
419            P3::new(0.0, 0.0, 9.0), // same x,y — different z
420        ]);
421        super::fix_closure(&mut open);
422        assert_eq!(open.points().count(), 4, "z differs → not closed → kept");
423    }
424
425    #[test]
426    fn ccw_ring_correctly_wound_is_a_noop() {
427        // Regression: `fix_orientation(self, CW)` used to reverse a
428        // CORRECTLY wound CCW-declared ring (+2 → −2), because
429        // `ShoelaceArea` already folds the declared order into its
430        // sign. Fixture mirrors geometry-strategy's
431        // `ccw_declared_ccw_traversed_diamond_is_2`.
432        let mut r: Ring<P, false> = Ring::from_vec(vec![
433            P::new(1.0, 0.0),
434            P::new(0.0, 1.0),
435            P::new(-1.0, 0.0),
436            P::new(0.0, -1.0),
437            P::new(1.0, 0.0),
438        ]);
439        assert_eq!(ring_area(&r), 2.0, "precondition: correctly wound");
440        correct(&mut r);
441        assert_eq!(ring_area(&r), 2.0, "correct() must be a no-op");
442    }
443
444    #[test]
445    fn ccw_ring_wrongly_wound_is_reversed() {
446        // The same diamond stored clockwise under a CCW declaration:
447        // strategy area −2 → correct() reverses → +2.
448        let mut r: Ring<P, false> = Ring::from_vec(vec![
449            P::new(1.0, 0.0),
450            P::new(0.0, -1.0),
451            P::new(-1.0, 0.0),
452            P::new(0.0, 1.0),
453            P::new(1.0, 0.0),
454        ]);
455        assert_eq!(ring_area(&r), -2.0, "precondition: wrongly wound");
456        correct(&mut r);
457        assert_eq!(ring_area(&r), 2.0);
458    }
459
460    #[test]
461    fn ccw_polygon_with_hole_correctly_wound_is_a_noop() {
462        // Outer 4x4 stored CCW (matches declaration, ring_area +16),
463        // hole 1x1 stored CW (opposite, ring_area −1). correct() must
464        // change nothing: this is exactly the state
465        // ShoelacePolygonArea's ring-sum (+15) relies on.
466        use geometry_model::Polygon;
467        let outer: Ring<P, false> = Ring::from_vec(vec![
468            P::new(0.0, 0.0),
469            P::new(4.0, 0.0),
470            P::new(4.0, 4.0),
471            P::new(0.0, 4.0),
472            P::new(0.0, 0.0),
473        ]);
474        let hole: Ring<P, false> = Ring::from_vec(vec![
475            P::new(1.0, 1.0),
476            P::new(1.0, 2.0),
477            P::new(2.0, 2.0),
478            P::new(2.0, 1.0),
479            P::new(1.0, 1.0),
480        ]);
481        let mut pg: Polygon<P, false> = Polygon::new(outer);
482        pg.inners.push(hole);
483        assert_eq!(ring_area(&pg.outer), 16.0, "precondition: outer CCW-stored");
484        assert_eq!(
485            ring_area(&pg.inners[0]),
486            -1.0,
487            "precondition: hole CW-stored"
488        );
489        correct(&mut pg);
490        assert_eq!(ring_area(&pg.outer), 16.0, "outer must be untouched");
491        assert_eq!(ring_area(&pg.inners[0]), -1.0, "hole must be untouched");
492    }
493
494    #[test]
495    fn ccw_polygon_wrongly_wound_is_fixed() {
496        // Outer stored CW (wrong for a CCW declaration), hole stored
497        // CCW (wrong for a hole): correct() reverses both.
498        use geometry_model::Polygon;
499        let outer: Ring<P, false> = Ring::from_vec(vec![
500            P::new(0.0, 0.0),
501            P::new(0.0, 4.0),
502            P::new(4.0, 4.0),
503            P::new(4.0, 0.0),
504            P::new(0.0, 0.0),
505        ]);
506        let hole: Ring<P, false> = Ring::from_vec(vec![
507            P::new(1.0, 1.0),
508            P::new(2.0, 1.0),
509            P::new(2.0, 2.0),
510            P::new(1.0, 2.0),
511            P::new(1.0, 1.0),
512        ]);
513        let mut pg: Polygon<P, false> = Polygon::new(outer);
514        pg.inners.push(hole);
515        assert_eq!(ring_area(&pg.outer), -16.0, "precondition: outer wrong");
516        assert_eq!(ring_area(&pg.inners[0]), 1.0, "precondition: hole wrong");
517        correct(&mut pg);
518        assert_eq!(ring_area(&pg.outer), 16.0);
519        assert_eq!(ring_area(&pg.inners[0]), -1.0);
520    }
521}