1use alloc::vec;
17use alloc::vec::Vec;
18
19use geometry_cs::{CartesianFamily, CoordinateSystem};
20use geometry_tag::SameAs;
21use geometry_trait::{Linestring, Point, PointMut};
22
23use crate::cartesian::{PointToSegment, Pythagoras};
24use crate::distance::DistanceStrategy;
25
26pub trait SimplifyStrategy<G> {
32 type Output;
34
35 fn simplify(&self, g: &G, max_distance: f64) -> Self::Output;
38}
39
40#[derive(Debug, Default, Clone, Copy)]
47pub struct DouglasPeucker<D = PointToSegment<Pythagoras>>(pub D);
48
49#[derive(Debug, Default, Clone, Copy)]
59pub struct VisvalingamWhyatt;
60
61#[derive(Debug, Default, Clone, Copy)]
69pub struct VisvalingamWhyattPreserve;
70
71impl<P, L, D> SimplifyStrategy<L> for DouglasPeucker<D>
72where
73 P: Point<Scalar = f64> + PointMut + Default + Copy,
74 L: Linestring<Point = P>,
75 D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
76 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
77{
78 type Output = geometry_model::Linestring<P>;
79
80 fn simplify(&self, ls: &L, max_distance: f64) -> Self::Output {
81 let pts: Vec<P> = ls.points().copied().collect();
82 if pts.len() < 3 || max_distance < 0.0 {
92 return geometry_model::Linestring::from_vec(pts);
93 }
94
95 let mut keep = vec![false; pts.len()];
96 keep[0] = true;
97 keep[pts.len() - 1] = true;
98 dp_recurse(&pts, 0, pts.len() - 1, max_distance, &self.0, &mut keep);
99
100 let out: Vec<P> = pts
101 .iter()
102 .zip(keep.iter())
103 .filter_map(|(p, &k)| if k { Some(*p) } else { None })
104 .collect();
105 geometry_model::Linestring::from_vec(out)
106 }
107}
108
109impl<P, L> SimplifyStrategy<L> for VisvalingamWhyatt
110where
111 P: Point<Scalar = f64> + PointMut + Default + Copy,
112 L: Linestring<Point = P>,
113 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
114{
115 type Output = geometry_model::Linestring<P>;
116
117 fn simplify(&self, ls: &L, max_distance: f64) -> Self::Output {
118 visvalingam_whyatt(ls, max_distance, false)
119 }
120}
121
122impl<P, L> SimplifyStrategy<L> for VisvalingamWhyattPreserve
123where
124 P: Point<Scalar = f64> + PointMut + Default + Copy,
125 L: Linestring<Point = P>,
126 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
127{
128 type Output = geometry_model::Linestring<P>;
129
130 fn simplify(&self, ls: &L, max_distance: f64) -> Self::Output {
131 visvalingam_whyatt(ls, max_distance, true)
132 }
133}
134
135fn visvalingam_whyatt<P, L>(
136 ls: &L,
137 minimum_area: f64,
138 preserve_topology: bool,
139) -> geometry_model::Linestring<P>
140where
141 P: Point<Scalar = f64> + PointMut + Default + Copy,
142 L: Linestring<Point = P>,
143{
144 let points: Vec<P> = ls.points().copied().collect();
145 if points.len() < 3 || minimum_area <= 0.0 || minimum_area.is_nan() {
146 return geometry_model::Linestring::from_vec(points);
147 }
148
149 let mut retained: Vec<usize> = (0..points.len()).collect();
150 let mut forced_predecessor = None;
151
152 while retained.len() > 2 {
153 let selected = forced_predecessor
154 .take()
155 .and_then(|index| retained.iter().position(|candidate| *candidate == index))
156 .filter(|slot| *slot > 0 && *slot + 1 < retained.len())
157 .map(|slot| (slot, 0.0))
158 .or_else(|| smallest_triangle(&points, &retained));
159 let (slot, area) = selected.expect("an interior triangle is available");
163 if area > minimum_area {
164 break;
165 }
166
167 let creates_crossing =
168 preserve_topology && removal_creates_crossing(&points, &retained, slot);
169 let predecessor = retained[slot - 1];
170 retained.remove(slot);
171 if creates_crossing {
172 forced_predecessor = Some(predecessor);
173 }
174 }
175
176 geometry_model::Linestring::from_vec(retained.into_iter().map(|index| points[index]).collect())
177}
178
179fn smallest_triangle<P>(points: &[P], retained: &[usize]) -> Option<(usize, f64)>
180where
181 P: Point<Scalar = f64>,
182{
183 let mut selected = None;
184 for slot in 1..retained.len().saturating_sub(1) {
185 let area = triangle_area(
186 &points[retained[slot - 1]],
187 &points[retained[slot]],
188 &points[retained[slot + 1]],
189 );
190 if selected.is_none_or(|(_, smallest)| area < smallest) {
191 selected = Some((slot, area));
192 }
193 }
194 selected
195}
196
197#[inline]
198fn triangle_area<P>(first: &P, middle: &P, last: &P) -> f64
199where
200 P: Point<Scalar = f64>,
201{
202 let twice_area = (middle.get::<0>() - first.get::<0>()) * (last.get::<1>() - first.get::<1>())
203 - (middle.get::<1>() - first.get::<1>()) * (last.get::<0>() - first.get::<0>());
204 twice_area.abs() * 0.5
205}
206
207fn removal_creates_crossing<P>(points: &[P], retained: &[usize], slot: usize) -> bool
208where
209 P: Point<Scalar = f64>,
210{
211 let left = retained[slot - 1];
212 let current = retained[slot];
213 let right = retained[slot + 1];
214
215 retained.windows(2).any(|edge| {
216 let start = edge[0];
217 let end = edge[1];
218 if start == left
219 || end == left
220 || start == current
221 || end == current
222 || start == right
223 || end == right
224 {
225 return false;
226 }
227 segments_intersect(&points[left], &points[right], &points[start], &points[end])
228 })
229}
230
231fn segments_intersect<P>(first: &P, second: &P, third: &P, fourth: &P) -> bool
232where
233 P: Point<Scalar = f64>,
234{
235 let o1 = orientation(first, second, third);
236 let o2 = orientation(first, second, fourth);
237 let o3 = orientation(third, fourth, first);
238 let o4 = orientation(third, fourth, second);
239
240 if o1 != o2 && o3 != o4 && o1 != 0 && o2 != 0 && o3 != 0 && o4 != 0 {
241 return true;
242 }
243 (o1 == 0 && point_on_segment(third, first, second))
244 || (o2 == 0 && point_on_segment(fourth, first, second))
245 || (o3 == 0 && point_on_segment(first, third, fourth))
246 || (o4 == 0 && point_on_segment(second, third, fourth))
247}
248
249#[inline]
250fn orientation<P>(first: &P, second: &P, third: &P) -> i8
251where
252 P: Point<Scalar = f64>,
253{
254 let cross = (second.get::<0>() - first.get::<0>()) * (third.get::<1>() - first.get::<1>())
255 - (second.get::<1>() - first.get::<1>()) * (third.get::<0>() - first.get::<0>());
256 if cross > 0.0 {
257 1
258 } else if cross < 0.0 {
259 -1
260 } else {
261 0
262 }
263}
264
265#[inline]
266fn point_on_segment<P>(point: &P, first: &P, second: &P) -> bool
267where
268 P: Point<Scalar = f64>,
269{
270 let x = point.get::<0>();
271 let y = point.get::<1>();
272 first.get::<0>().min(second.get::<0>()) <= x
273 && x <= first.get::<0>().max(second.get::<0>())
274 && first.get::<1>().min(second.get::<1>()) <= y
275 && y <= first.get::<1>().max(second.get::<1>())
276}
277
278fn dp_recurse<P, D>(pts: &[P], lo: usize, hi: usize, eps: f64, dist: &D, keep: &mut [bool])
285where
286 P: Point<Scalar = f64> + PointMut + Default + Copy,
287 D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
288 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
289{
290 if hi <= lo + 1 {
291 return;
292 }
293 let seg = geometry_model::Segment::new(pts[lo], pts[hi]);
294
295 let mut max_d = 0.0_f64;
297 let mut max_i = lo;
298 for (i, p) in pts.iter().enumerate().take(hi).skip(lo + 1) {
299 let d = dist.distance(p, &seg);
300 if d > max_d {
301 max_d = d;
302 max_i = i;
303 }
304 }
305
306 if max_d > eps {
310 keep[max_i] = true;
311 dp_recurse(pts, lo, max_i, eps, dist, keep);
312 dp_recurse(pts, max_i, hi, eps, dist, keep);
313 }
314}
315
316#[cfg(test)]
317#[allow(
318 clippy::float_cmp,
319 reason = "Simplified coordinates are exact literals."
320)]
321mod tests {
322 extern crate alloc;
328
329 use super::{
330 DouglasPeucker, SimplifyStrategy, orientation, point_on_segment, segments_intersect,
331 };
332 use crate::cartesian::{PointToSegment, Pythagoras};
333 use alloc::vec;
334 use alloc::vec::Vec;
335 use geometry_cs::Cartesian;
336 use geometry_model::{Linestring, Point2D, linestring};
337 use geometry_trait::Point as _;
338
339 type Pt = Point2D<f64, Cartesian>;
340
341 fn default_dp() -> DouglasPeucker<PointToSegment<Pythagoras>> {
342 DouglasPeucker::default()
343 }
344
345 fn coords(ls: &Linestring<Pt>) -> Vec<(f64, f64)> {
346 ls.0.iter().map(|p| (p.get::<0>(), p.get::<1>())).collect()
347 }
348
349 #[test]
350 fn straight_three_point_line_collapses_to_endpoints() {
351 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
352 let s = default_dp().simplify(&ls, 0.5);
353 assert_eq!(coords(&s), vec![(0., 0.), (2., 0.)]);
354 }
355
356 #[test]
357 fn collinear_polyline_collapses_to_endpoints() {
358 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.), (2., 2.), (3., 3.)];
359 let s = default_dp().simplify(&ls, 0.001);
360 assert_eq!(coords(&s), vec![(0., 0.), (3., 3.)]);
361 }
362
363 #[test]
364 fn tiny_deviation_drops_but_large_deviation_kept() {
365 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.01), (2., 0.), (3., 5.), (4., 0.)];
368 let s = default_dp().simplify(&ls, 0.5);
369 let c = coords(&s);
370 assert_eq!(c.first().unwrap(), &(0., 0.));
371 assert_eq!(c.last().unwrap(), &(4., 0.));
372 assert!(c.contains(&(3., 5.)));
374 assert!(!c.contains(&(1., 0.01)));
375 }
376
377 #[test]
378 fn short_linestring_is_returned_unchanged() {
379 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.)];
380 let s = default_dp().simplify(&ls, 10.0);
381 assert_eq!(coords(&s), vec![(0., 0.), (1., 1.)]);
382 }
383
384 #[test]
385 fn negative_tolerance_copies_through_without_overflow() {
386 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
392 let s = default_dp().simplify(&ls, -0.5);
393 assert_eq!(coords(&s), vec![(0., 0.), (1., 0.), (2., 0.)]);
394 let dup: Linestring<Pt> = linestring![(0., 0.), (0., 0.), (0., 0.), (5., 0.)];
396 let s2 = default_dp().simplify(&dup, -1.0);
397 assert_eq!(s2.0.len(), 4, "negative tolerance keeps every vertex");
398 }
399
400 #[test]
401 fn private_collinear_intersection_guards_cover_every_endpoint_order() {
402 let a = Pt::new(0.0, 0.0);
403 let b = Pt::new(2.0, 0.0);
404 assert!(segments_intersect(
405 &a,
406 &b,
407 &Pt::new(1.0, 0.0),
408 &Pt::new(1.0, 1.0)
409 ));
410 assert!(segments_intersect(
411 &a,
412 &b,
413 &Pt::new(1.0, 1.0),
414 &Pt::new(1.0, 0.0)
415 ));
416 assert!(segments_intersect(
417 &Pt::new(1.0, 0.0),
418 &Pt::new(1.0, 1.0),
419 &a,
420 &b,
421 ));
422 assert!(segments_intersect(
423 &Pt::new(1.0, 1.0),
424 &Pt::new(1.0, 0.0),
425 &a,
426 &b,
427 ));
428 assert_eq!(orientation(&a, &b, &Pt::new(1.0, 1.0)), 1);
429 assert_eq!(orientation(&a, &b, &Pt::new(1.0, -1.0)), -1);
430 assert_eq!(orientation(&a, &b, &Pt::new(1.0, 0.0)), 0);
431 assert!(point_on_segment(&Pt::new(1.0, 0.0), &a, &b));
432 }
433}