1use geonative_core::{Coord, Geometry, LineString, Polygon};
13
14pub fn douglas_peucker(coords: &[Coord], tolerance: f64) -> Vec<Coord> {
20 if coords.len() < 3 || !tolerance.is_finite() || tolerance <= 0.0 {
21 return coords.to_vec();
22 }
23 let n = coords.len();
24 let mut keep = vec![false; n];
25 keep[0] = true;
26 keep[n - 1] = true;
27 dp_recurse(coords, 0, n - 1, tolerance * tolerance, &mut keep);
28
29 coords
30 .iter()
31 .zip(keep.iter())
32 .filter_map(|(c, &k)| if k { Some(*c) } else { None })
33 .collect()
34}
35
36fn dp_recurse(coords: &[Coord], start: usize, end: usize, tol_sq: f64, keep: &mut [bool]) {
37 if end <= start + 1 {
38 return;
39 }
40 let (max_dist_sq, max_idx) = farthest_index(coords, start, end);
41 if max_dist_sq > tol_sq {
42 keep[max_idx] = true;
43 dp_recurse(coords, start, max_idx, tol_sq, keep);
44 dp_recurse(coords, max_idx, end, tol_sq, keep);
45 }
46}
47
48fn farthest_index(coords: &[Coord], start: usize, end: usize) -> (f64, usize) {
51 let a = coords[start];
52 let b = coords[end];
53 let dx = b.x - a.x;
54 let dy = b.y - a.y;
55 let len_sq = dx * dx + dy * dy;
56
57 let mut max_d_sq = -1.0;
58 let mut max_idx = start;
59 for (i, p) in coords.iter().enumerate().take(end).skip(start + 1) {
60 let d_sq = if len_sq == 0.0 {
61 let px = p.x - a.x;
62 let py = p.y - a.y;
63 px * px + py * py
64 } else {
65 let num = dy * p.x - dx * p.y + b.x * a.y - b.y * a.x;
67 (num * num) / len_sq
68 };
69 if d_sq > max_d_sq {
70 max_d_sq = d_sq;
71 max_idx = i;
72 }
73 }
74 (max_d_sq, max_idx)
75}
76
77pub fn simplify_geometry(geom: &Geometry, tolerance: f64) -> Geometry {
81 match geom {
82 Geometry::Point(_) | Geometry::MultiPoint(_) | Geometry::Empty(_) => geom.clone(),
83 Geometry::LineString(ls) => Geometry::LineString(simplify_linestring(ls, tolerance)),
84 Geometry::MultiLineString(parts) => Geometry::MultiLineString(
85 parts
86 .iter()
87 .map(|ls| simplify_linestring(ls, tolerance))
88 .collect(),
89 ),
90 Geometry::Polygon(p) => Geometry::Polygon(simplify_polygon(p, tolerance)),
91 Geometry::MultiPolygon(polys) => Geometry::MultiPolygon(
92 polys
93 .iter()
94 .map(|p| simplify_polygon(p, tolerance))
95 .collect(),
96 ),
97 Geometry::GeometryCollection(v) => Geometry::GeometryCollection(
98 v.iter().map(|g| simplify_geometry(g, tolerance)).collect(),
99 ),
100 _ => geom.clone(),
104 }
105}
106
107fn simplify_linestring(ls: &LineString, tolerance: f64) -> LineString {
108 LineString::new(douglas_peucker(&ls.coords, tolerance))
109}
110
111fn simplify_polygon(p: &Polygon, tolerance: f64) -> Polygon {
112 Polygon::new(
113 simplify_ring(&p.exterior, tolerance),
114 p.holes
115 .iter()
116 .map(|h| simplify_ring(h, tolerance))
117 .collect(),
118 )
119}
120
121fn simplify_ring(ring: &LineString, tolerance: f64) -> LineString {
126 if ring.coords.len() <= 4 {
127 return ring.clone();
129 }
130 let mut simplified = douglas_peucker(&ring.coords, tolerance);
131 if let (Some(first), Some(last)) = (simplified.first(), simplified.last()) {
134 if first != last && ring.coords.first() == ring.coords.last() {
135 simplified.push(*first);
136 }
137 }
138 LineString::new(simplified)
139}
140
141pub fn tolerance_for_zoom(z: u8) -> f64 {
152 match z {
153 0..=2 => 0.5,
154 3..=5 => 0.1,
155 6..=8 => 0.02,
156 9..=11 => 0.004,
157 12..=14 => 0.0008,
158 15..=17 => 0.000_15,
159 _ => 0.000_03,
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use geonative_core::GeometryType;
167
168 fn xy(x: f64, y: f64) -> Coord {
169 Coord {
170 x,
171 y,
172 z: None,
173 m: None,
174 }
175 }
176
177 #[test]
178 fn dp_drops_colinear_interior_points() {
179 let pts = vec![
181 xy(0.0, 0.0),
182 xy(1.0, 0.0),
183 xy(2.0, 0.0),
184 xy(3.0, 0.0),
185 xy(4.0, 0.0),
186 ];
187 let out = douglas_peucker(&pts, 0.001);
188 assert_eq!(out, vec![xy(0.0, 0.0), xy(4.0, 0.0)]);
189 }
190
191 #[test]
192 fn dp_keeps_jagged_extremes() {
193 let pts = vec![
197 xy(0.0, 0.0),
198 xy(1.0, 10.0),
199 xy(2.0, 0.0),
200 xy(3.0, -10.0),
201 xy(4.0, 0.0),
202 ];
203 let out = douglas_peucker(&pts, 0.5);
204 assert!(out.contains(&xy(0.0, 0.0)), "endpoint dropped");
205 assert!(out.contains(&xy(4.0, 0.0)), "endpoint dropped");
206 assert!(out.contains(&xy(1.0, 10.0)), "first extreme dropped");
207 assert!(out.contains(&xy(3.0, -10.0)), "second extreme dropped");
208 assert_eq!(out.len(), 4);
209 }
210
211 #[test]
212 fn dp_keeps_non_colinear_intermediate() {
213 let pts = vec![
217 xy(0.0, 0.0),
218 xy(1.0, 10.0),
219 xy(2.0, 5.0),
220 xy(3.0, -10.0),
221 xy(4.0, 0.0),
222 ];
223 let out = douglas_peucker(&pts, 0.1);
224 assert_eq!(out.len(), pts.len(), "all 5 points should be kept");
225 }
226
227 #[test]
228 fn dp_zero_or_negative_tolerance_is_identity() {
229 let pts = vec![xy(0.0, 0.0), xy(1.0, 0.5), xy(2.0, 0.0)];
230 assert_eq!(douglas_peucker(&pts, 0.0), pts);
231 assert_eq!(douglas_peucker(&pts, -1.0), pts);
232 }
233
234 #[test]
235 fn dp_short_input_is_identity() {
236 let pts = vec![xy(0.0, 0.0), xy(1.0, 1.0)];
237 assert_eq!(douglas_peucker(&pts, 0.5), pts);
238 }
239
240 #[test]
241 fn dp_degenerate_chord_uses_distance_to_first_point() {
242 let pts = vec![xy(0.0, 0.0), xy(5.0, 5.0), xy(0.0, 0.0)];
244 let out = douglas_peucker(&pts, 1.0);
245 assert_eq!(out.len(), 3);
247 }
248
249 #[test]
250 fn simplify_geometry_dispatches_per_variant() {
251 let p = Geometry::Point(xy(1.0, 2.0));
253 assert_eq!(simplify_geometry(&p, 0.001), p);
254
255 let e = Geometry::Empty(GeometryType::Polygon);
257 assert_eq!(simplify_geometry(&e, 0.001), e);
258
259 let ls = Geometry::LineString(LineString::new(vec![
261 xy(0.0, 0.0),
262 xy(1.0, 0.0),
263 xy(2.0, 0.0),
264 xy(3.0, 0.0),
265 ]));
266 match simplify_geometry(&ls, 0.001) {
267 Geometry::LineString(out) => assert_eq!(out.coords.len(), 2),
268 _ => panic!(),
269 }
270 }
271
272 #[test]
273 fn simplify_polygon_preserves_ring_closure() {
274 let p = Polygon::new(
275 LineString::new(vec![
276 xy(0.0, 0.0),
277 xy(10.0, 0.0),
278 xy(10.001, 0.0001),
279 xy(10.0, 10.0),
280 xy(0.0, 10.0),
281 xy(0.0, 0.0),
282 ]),
283 vec![],
284 );
285 let simplified = simplify_polygon(&p, 0.01);
286 assert!(simplified.exterior.coords.len() < p.exterior.coords.len());
288 assert_eq!(
290 simplified.exterior.coords.first(),
291 simplified.exterior.coords.last(),
292 "ring closure lost: {:?}",
293 simplified.exterior.coords
294 );
295 }
296
297 #[test]
298 fn simplify_polygon_with_hole_handles_both_rings() {
299 let p = Polygon::new(
300 LineString::new(vec![
301 xy(0.0, 0.0),
302 xy(10.0, 0.0),
303 xy(10.0, 10.0),
304 xy(5.0, 10.0001),
305 xy(0.0, 10.0),
306 xy(0.0, 0.0),
307 ]),
308 vec![LineString::new(vec![
309 xy(2.0, 2.0),
310 xy(4.0, 2.0),
311 xy(4.001, 2.0),
312 xy(4.0, 4.0),
313 xy(2.0, 4.0),
314 xy(2.0, 2.0),
315 ])],
316 );
317 let simplified = simplify_polygon(&p, 0.001);
318 assert_eq!(simplified.holes.len(), 1);
319 assert_eq!(
320 simplified.holes[0].coords.first(),
321 simplified.holes[0].coords.last(),
322 "hole closure lost"
323 );
324 }
325
326 #[test]
327 fn simplify_tiny_polygon_passes_through() {
328 let p = Polygon::new(
330 LineString::new(vec![xy(0.0, 0.0), xy(1.0, 0.0), xy(0.5, 1.0), xy(0.0, 0.0)]),
331 vec![],
332 );
333 let simplified = simplify_polygon(&p, 100.0);
334 assert_eq!(simplified.exterior.coords.len(), 4);
335 }
336
337 #[test]
338 fn tolerance_for_zoom_is_monotonic() {
339 for z in 0u8..18 {
340 assert!(
341 tolerance_for_zoom(z) >= tolerance_for_zoom(z + 1),
342 "tolerance not monotonically non-increasing at z={z}"
343 );
344 }
345 }
346
347 #[test]
348 fn tolerance_for_zoom_low_zoom_aggressive() {
349 assert!(tolerance_for_zoom(0) >= 0.1);
352 assert!(tolerance_for_zoom(20) < 0.001);
353 }
354}