1use geometry_coords::CoordinateScalar;
20use geometry_model::{Linestring, MultiPolygon, Polygon, Ring};
21use geometry_trait::Point as PointTrait;
22
23pub fn remove_spikes<G: RemoveSpikes>(g: &mut G) {
28 g.remove_spikes();
29}
30
31#[doc(hidden)]
33pub trait RemoveSpikes {
34 fn remove_spikes(&mut self);
35}
36
37fn is_spike_2d<P: PointTrait>(a: &P, b: &P, c: &P) -> bool {
40 let ux = b.get::<0>() - a.get::<0>();
41 let uy = b.get::<1>() - a.get::<1>();
42 let vx = c.get::<0>() - b.get::<0>();
43 let vy = c.get::<1>() - b.get::<1>();
44 let cross = ux * vy - uy * vx;
45 let dot = ux * vx + uy * vy;
46 let zero = <P::Scalar as CoordinateScalar>::ZERO;
47 cross == zero && dot < zero
48}
49
50fn walk_spikes<P: PointTrait>(pts: &mut alloc::vec::Vec<P>) {
51 let mut changed = true;
52 while changed && pts.len() >= 3 {
53 changed = false;
54 let mut i = 1;
55 while i + 1 < pts.len() {
56 if is_spike_2d(&pts[i - 1], &pts[i], &pts[i + 1]) {
57 pts.remove(i);
58 changed = true;
59 if i > 1 {
62 i -= 1;
63 }
64 } else {
65 i += 1;
66 }
67 }
68 }
69}
70
71impl<P: PointTrait> RemoveSpikes for Linestring<P> {
72 fn remove_spikes(&mut self) {
73 walk_spikes(&mut self.0);
74 }
75}
76
77fn walk_ring_spikes<P: PointTrait + Copy>(pts: &mut alloc::vec::Vec<P>, closed: bool) {
92 walk_spikes(pts);
94
95 let had_closing = closed && pts.len() >= 2 && same_point(&pts[0], &pts[pts.len() - 1]);
98 if had_closing {
99 pts.pop();
100 }
101
102 let mut found = true;
105 while found {
106 found = false;
107 while pts.len() >= 3 && is_spike_2d(&pts[pts.len() - 2], &pts[pts.len() - 1], &pts[0]) {
109 pts.pop();
110 found = true;
111 }
112 while pts.len() >= 3 && is_spike_2d(&pts[pts.len() - 1], &pts[0], &pts[1]) {
114 pts.remove(0);
115 found = true;
116 }
117 }
118
119 if had_closing && !pts.is_empty() {
121 let first = pts[0];
122 pts.push(first);
123 }
124}
125
126fn same_point<P: PointTrait>(a: &P, b: &P) -> bool {
128 a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
129}
130
131impl<P: PointTrait + Copy, const CW: bool, const CL: bool> RemoveSpikes for Ring<P, CW, CL> {
132 fn remove_spikes(&mut self) {
133 walk_ring_spikes(&mut self.0, CL);
134 }
135}
136
137impl<P: PointTrait + Copy, const CW: bool, const CL: bool> RemoveSpikes for Polygon<P, CW, CL> {
138 fn remove_spikes(&mut self) {
139 walk_ring_spikes(&mut self.outer.0, CL);
140 for inner in &mut self.inners {
141 walk_ring_spikes(&mut inner.0, CL);
142 }
143 }
144}
145
146impl<Pg: RemoveSpikes + geometry_trait::Polygon> RemoveSpikes for MultiPolygon<Pg> {
147 fn remove_spikes(&mut self) {
148 for p in &mut self.0 {
149 p.remove_spikes();
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::remove_spikes;
162 use geometry_cs::Cartesian;
163 use geometry_model::{Point2D, linestring};
164 use geometry_trait::Linestring as _;
165
166 type P = Point2D<f64, Cartesian>;
167
168 #[test]
169 fn out_and_back_spur_is_removed() {
170 let mut ls: geometry_model::Linestring<P> =
174 linestring![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0), (2.0, 0.0)];
175 remove_spikes(&mut ls);
176 let xs: Vec<f64> = ls.points().map(geometry_trait::Point::get::<0>).collect();
177 assert_eq!(xs, vec![0.0, 1.0, 2.0]);
178 }
179
180 #[test]
181 fn spike_free_linestring_is_unchanged() {
182 let mut ls: geometry_model::Linestring<P> = linestring![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
183 remove_spikes(&mut ls);
184 assert_eq!(ls.points().count(), 3);
185 }
186
187 #[test]
192 fn cascading_spikes_all_collapse() {
193 let mut ls: geometry_model::Linestring<P> =
197 linestring![(0.0, 0.0), (2.0, 0.0), (5.0, 0.0), (3.0, 0.0), (1.0, 0.0)];
198 remove_spikes(&mut ls);
199 let xs: Vec<f64> = ls.points().map(geometry_trait::Point::get::<0>).collect();
200 for w in xs.windows(3) {
203 let d1 = w[1] - w[0];
204 let d2 = w[2] - w[1];
205 assert!(d1 * d2 >= 0.0, "residual reversal in {xs:?}");
206 }
207 }
208
209 #[test]
212 fn polygon_removes_spikes_in_outer_and_holes() {
213 use geometry_model::{Polygon, Ring};
214 use geometry_trait::{Point as _, Polygon as _, Ring as _};
215 let outer = Ring::from_vec(vec![
217 P::new(0.0, 0.0),
218 P::new(4.0, 0.0),
219 P::new(5.0, 0.0), P::new(4.0, 0.0),
221 P::new(4.0, 4.0),
222 P::new(0.0, 4.0),
223 P::new(0.0, 0.0),
224 ]);
225 let hole = Ring::from_vec(vec![
227 P::new(1.0, 1.0),
228 P::new(2.0, 1.0),
229 P::new(3.0, 1.0), P::new(2.0, 1.0),
231 P::new(2.0, 2.0),
232 P::new(1.0, 1.0),
233 ]);
234 let mut pg: Polygon<P> = Polygon::with_inners(outer, vec![hole]);
235 remove_spikes(&mut pg);
236 let ext: Vec<(f64, f64)> = pg
238 .exterior()
239 .points()
240 .map(|p| (p.get::<0>(), p.get::<1>()))
241 .collect();
242 assert!(!ext.contains(&(5.0, 0.0)), "outer spike survived: {ext:?}");
243 let hole_pts: Vec<(f64, f64)> = pg
244 .interiors()
245 .next()
246 .unwrap()
247 .points()
248 .map(|p| (p.get::<0>(), p.get::<1>()))
249 .collect();
250 assert!(!hole_pts.contains(&(3.0, 1.0)), "hole spike survived");
251 }
252
253 #[test]
255 fn multipolygon_removes_spikes_from_each_member() {
256 use geometry_model::{MultiPolygon, Polygon, Ring};
257 use geometry_trait::{Point as _, Polygon as _, Ring as _};
258 let spiky = || {
259 Polygon::<P>::new(Ring::from_vec(vec![
260 P::new(0.0, 0.0),
261 P::new(4.0, 0.0),
262 P::new(5.0, 0.0),
263 P::new(4.0, 0.0),
264 P::new(4.0, 4.0),
265 P::new(0.0, 4.0),
266 P::new(0.0, 0.0),
267 ]))
268 };
269 let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![spiky(), spiky()]);
270 remove_spikes(&mut mpg);
271 for pg in &mpg.0 {
272 let pts: Vec<(f64, f64)> = pg
273 .exterior()
274 .points()
275 .map(|p| (p.get::<0>(), p.get::<1>()))
276 .collect();
277 assert!(!pts.contains(&(5.0, 0.0)), "member spike survived");
278 }
279 }
280
281 #[test]
282 fn ring_seam_spike_is_removed() {
283 use geometry_model::Ring;
293 use geometry_trait::{Point as _, Ring as _};
294
295 let mut r: Ring<P> = Ring::from_vec(vec![
296 P::new(0.0, 0.0),
297 P::new(2.0, 0.0),
298 P::new(2.0, 2.0),
299 P::new(0.0, 2.0),
300 P::new(1.0, 0.0),
301 P::new(0.0, 0.0),
302 ]);
303 remove_spikes(&mut r);
304
305 let pts: Vec<(f64, f64)> = r.points().map(|p| (p.get::<0>(), p.get::<1>())).collect();
306 assert!(
308 !pts.contains(&(0.0, 0.0)),
309 "seam spike vertex (0,0) must be gone: {pts:?}"
310 );
311 assert!(r.points().count() >= 4);
313 assert_eq!(pts.first(), pts.last(), "ring must remain closed");
314 }
315}