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
49impl<P, L, D> SimplifyStrategy<L> for DouglasPeucker<D>
50where
51 P: Point<Scalar = f64> + PointMut + Default + Copy,
52 L: Linestring<Point = P>,
53 D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
54 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
55{
56 type Output = geometry_model::Linestring<P>;
57
58 fn simplify(&self, ls: &L, max_distance: f64) -> Self::Output {
59 let pts: Vec<P> = ls.points().copied().collect();
60 if pts.len() < 3 || max_distance < 0.0 {
70 return geometry_model::Linestring::from_vec(pts);
71 }
72
73 let mut keep = vec![false; pts.len()];
74 keep[0] = true;
75 keep[pts.len() - 1] = true;
76 dp_recurse(&pts, 0, pts.len() - 1, max_distance, &self.0, &mut keep);
77
78 let out: Vec<P> = pts
79 .iter()
80 .zip(keep.iter())
81 .filter_map(|(p, &k)| if k { Some(*p) } else { None })
82 .collect();
83 geometry_model::Linestring::from_vec(out)
84 }
85}
86
87fn dp_recurse<P, D>(pts: &[P], lo: usize, hi: usize, eps: f64, dist: &D, keep: &mut [bool])
94where
95 P: Point<Scalar = f64> + PointMut + Default + Copy,
96 D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
97 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
98{
99 if hi <= lo + 1 {
100 return;
101 }
102 let seg = geometry_model::Segment::new(pts[lo], pts[hi]);
103
104 let mut max_d = 0.0_f64;
106 let mut max_i = lo;
107 for (i, p) in pts.iter().enumerate().take(hi).skip(lo + 1) {
108 let d = dist.distance(p, &seg);
109 if d > max_d {
110 max_d = d;
111 max_i = i;
112 }
113 }
114
115 if max_d > eps {
119 keep[max_i] = true;
120 dp_recurse(pts, lo, max_i, eps, dist, keep);
121 dp_recurse(pts, max_i, hi, eps, dist, keep);
122 }
123}
124
125#[cfg(test)]
126#[allow(
127 clippy::float_cmp,
128 reason = "Simplified coordinates are exact literals."
129)]
130mod tests {
131 extern crate alloc;
137
138 use super::{DouglasPeucker, SimplifyStrategy};
139 use crate::cartesian::{PointToSegment, Pythagoras};
140 use alloc::vec;
141 use alloc::vec::Vec;
142 use geometry_cs::Cartesian;
143 use geometry_model::{Linestring, Point2D, linestring};
144 use geometry_trait::Point as _;
145
146 type Pt = Point2D<f64, Cartesian>;
147
148 fn default_dp() -> DouglasPeucker<PointToSegment<Pythagoras>> {
149 DouglasPeucker::default()
150 }
151
152 fn coords(ls: &Linestring<Pt>) -> Vec<(f64, f64)> {
153 ls.0.iter().map(|p| (p.get::<0>(), p.get::<1>())).collect()
154 }
155
156 #[test]
157 fn straight_three_point_line_collapses_to_endpoints() {
158 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
159 let s = default_dp().simplify(&ls, 0.5);
160 assert_eq!(coords(&s), vec![(0., 0.), (2., 0.)]);
161 }
162
163 #[test]
164 fn collinear_polyline_collapses_to_endpoints() {
165 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.), (2., 2.), (3., 3.)];
166 let s = default_dp().simplify(&ls, 0.001);
167 assert_eq!(coords(&s), vec![(0., 0.), (3., 3.)]);
168 }
169
170 #[test]
171 fn tiny_deviation_drops_but_large_deviation_kept() {
172 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.01), (2., 0.), (3., 5.), (4., 0.)];
175 let s = default_dp().simplify(&ls, 0.5);
176 let c = coords(&s);
177 assert_eq!(c.first().unwrap(), &(0., 0.));
178 assert_eq!(c.last().unwrap(), &(4., 0.));
179 assert!(c.contains(&(3., 5.)));
181 assert!(!c.contains(&(1., 0.01)));
182 }
183
184 #[test]
185 fn short_linestring_is_returned_unchanged() {
186 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.)];
187 let s = default_dp().simplify(&ls, 10.0);
188 assert_eq!(coords(&s), vec![(0., 0.), (1., 1.)]);
189 }
190
191 #[test]
192 fn negative_tolerance_copies_through_without_overflow() {
193 let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
199 let s = default_dp().simplify(&ls, -0.5);
200 assert_eq!(coords(&s), vec![(0., 0.), (1., 0.), (2., 0.)]);
201 let dup: Linestring<Pt> = linestring![(0., 0.), (0., 0.), (0., 0.), (5., 0.)];
203 let s2 = default_dp().simplify(&dup, -1.0);
204 assert_eq!(s2.0.len(), 4, "negative tolerance keeps every vertex");
205 }
206}