1use 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
28pub fn correct<G: Correct>(g: &mut G) {
33 g.correct();
34}
35
36#[doc(hidden)]
38pub trait Correct {
39 fn correct(&mut self);
40}
41
42fn fix_closure<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>)
45where
46 P: PointTrait + Copy,
47{
48 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), (false, true) => {
61 r.0.pop(); }
63 _ => {}
64 }
65}
66
67fn 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
82fn 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 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 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 fix_orientation(&mut self.outer, true);
129 for inner in &mut self.inners {
130 fix_closure(inner);
131 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 #![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 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 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 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 #[test]
208 fn ccw_ring_correctly_wound_is_a_noop() {
209 let mut r: Ring<P, false> = Ring::from_vec(vec![
215 P::new(1.0, 0.0),
216 P::new(0.0, 1.0),
217 P::new(-1.0, 0.0),
218 P::new(0.0, -1.0),
219 P::new(1.0, 0.0),
220 ]);
221 assert_eq!(ring_area(&r), 2.0, "precondition: correctly wound");
222 correct(&mut r);
223 assert_eq!(ring_area(&r), 2.0, "correct() must be a no-op");
224 }
225
226 #[test]
227 fn ccw_ring_wrongly_wound_is_reversed() {
228 let mut r: Ring<P, false> = Ring::from_vec(vec![
231 P::new(1.0, 0.0),
232 P::new(0.0, -1.0),
233 P::new(-1.0, 0.0),
234 P::new(0.0, 1.0),
235 P::new(1.0, 0.0),
236 ]);
237 assert_eq!(ring_area(&r), -2.0, "precondition: wrongly wound");
238 correct(&mut r);
239 assert_eq!(ring_area(&r), 2.0);
240 }
241
242 #[test]
243 fn ccw_polygon_with_hole_correctly_wound_is_a_noop() {
244 use geometry_model::Polygon;
249 let outer: Ring<P, false> = Ring::from_vec(vec![
250 P::new(0.0, 0.0),
251 P::new(4.0, 0.0),
252 P::new(4.0, 4.0),
253 P::new(0.0, 4.0),
254 P::new(0.0, 0.0),
255 ]);
256 let hole: Ring<P, false> = Ring::from_vec(vec![
257 P::new(1.0, 1.0),
258 P::new(1.0, 2.0),
259 P::new(2.0, 2.0),
260 P::new(2.0, 1.0),
261 P::new(1.0, 1.0),
262 ]);
263 let mut pg: Polygon<P, false> = Polygon::new(outer);
264 pg.inners.push(hole);
265 assert_eq!(ring_area(&pg.outer), 16.0, "precondition: outer CCW-stored");
266 assert_eq!(
267 ring_area(&pg.inners[0]),
268 -1.0,
269 "precondition: hole CW-stored"
270 );
271 correct(&mut pg);
272 assert_eq!(ring_area(&pg.outer), 16.0, "outer must be untouched");
273 assert_eq!(ring_area(&pg.inners[0]), -1.0, "hole must be untouched");
274 }
275
276 #[test]
277 fn ccw_polygon_wrongly_wound_is_fixed() {
278 use geometry_model::Polygon;
281 let outer: Ring<P, false> = Ring::from_vec(vec![
282 P::new(0.0, 0.0),
283 P::new(0.0, 4.0),
284 P::new(4.0, 4.0),
285 P::new(4.0, 0.0),
286 P::new(0.0, 0.0),
287 ]);
288 let hole: Ring<P, false> = Ring::from_vec(vec![
289 P::new(1.0, 1.0),
290 P::new(2.0, 1.0),
291 P::new(2.0, 2.0),
292 P::new(1.0, 2.0),
293 P::new(1.0, 1.0),
294 ]);
295 let mut pg: Polygon<P, false> = Polygon::new(outer);
296 pg.inners.push(hole);
297 assert_eq!(ring_area(&pg.outer), -16.0, "precondition: outer wrong");
298 assert_eq!(ring_area(&pg.inners[0]), 1.0, "precondition: hole wrong");
299 correct(&mut pg);
300 assert_eq!(ring_area(&pg.outer), 16.0);
301 assert_eq!(ring_area(&pg.inners[0]), -1.0);
302 }
303}