rapidgeo_simplify/lib.rs
1//! Douglas-Peucker polyline simplification with pluggable distance backends.
2//!
3//! This crate implements the [Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
4//! for simplifying polylines while preserving their essential shape characteristics.
5//! The algorithm reduces the number of points in a polyline by removing points that
6//! don't significantly contribute to the overall shape, based on a distance threshold.
7//!
8//! # Quick Start
9//!
10//! ```rust
11//! use rapidgeo_distance::LngLat;
12//! use rapidgeo_simplify::{simplify_dp_into, SimplifyMethod};
13//!
14//! let points = vec![
15//! LngLat::new_deg(-122.4194, 37.7749), // San Francisco
16//! LngLat::new_deg(-122.0, 37.5), // Detour
17//! LngLat::new_deg(-121.5, 37.0), // Another point
18//! LngLat::new_deg(-118.2437, 34.0522), // Los Angeles
19//! ];
20//!
21//! let mut simplified = Vec::new();
22//! let count = simplify_dp_into(
23//! &points,
24//! 50000.0, // 50km tolerance
25//! SimplifyMethod::GreatCircleMeters,
26//! &mut simplified,
27//! );
28//!
29//! assert_eq!(count, simplified.len());
30//! // Endpoints are always preserved
31//! assert_eq!(simplified[0], points[0]);
32//! assert_eq!(simplified[count - 1], points[points.len() - 1]);
33//! ```
34//!
35//! # Distance Methods
36//!
37//! - [`SimplifyMethod::GreatCircleMeters`]: Spherical distance for global accuracy
38//! - [`SimplifyMethod::PlanarMeters`]: ENU projection for regional work
39//! - [`SimplifyMethod::EuclidRaw`]: Raw coordinates for non-geographic data
40//!
41//! # Batch Processing
42//!
43//! Enable the `batch` feature for parallel processing of multiple polylines:
44//!
45//! ```toml
46//! [dependencies]
47//! rapidgeo-simplify = { version = "0.1", features = ["batch"] }
48//! ```
49
50pub mod dp;
51pub mod xt;
52
53#[cfg(feature = "batch")]
54pub mod batch;
55
56use rapidgeo_distance::LngLat;
57
58/// Distance calculation method for Douglas-Peucker simplification.
59///
60/// Each method trades off between accuracy and performance for different use cases.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum SimplifyMethod {
63 /// ENU projection around the polyline's midpoint.
64 ///
65 /// Best for regional datasets where reasonable accuracy is needed with better
66 /// performance than great circle calculations. Accuracy decreases for very
67 /// large geographic regions.
68 PlanarMeters,
69
70 /// Spherical distance calculations using the great circle method.
71 ///
72 /// Best for global datasets requiring high accuracy. Uses more computation
73 /// than planar methods but works correctly at any scale.
74 GreatCircleMeters,
75
76 /// Direct Euclidean distance between raw coordinates.
77 ///
78 /// Best for non-geographic coordinate systems, screen coordinates, or
79 /// already-projected data where coordinates represent planar distances.
80 EuclidRaw,
81}
82
83/// Simplify a polyline using the Douglas-Peucker algorithm.
84///
85/// Returns the simplified points in the output vector and the count of points kept.
86/// The output vector is cleared before simplification begins.
87///
88/// # Arguments
89///
90/// * `pts` - Input polyline points in longitude, latitude order
91/// * `tolerance_m` - Maximum perpendicular distance threshold in meters (or coordinate units for EuclidRaw)
92/// * `method` - Distance calculation method to use
93/// * `out` - Output vector to store simplified points (will be cleared)
94///
95/// # Returns
96///
97/// Number of points in the simplified polyline (same as `out.len()` after the call)
98///
99/// # Examples
100///
101/// ```rust
102/// use rapidgeo_distance::LngLat;
103/// use rapidgeo_simplify::{simplify_dp_into, SimplifyMethod};
104///
105/// let points = vec![
106/// LngLat::new_deg(-122.0, 37.0),
107/// LngLat::new_deg(-121.5, 37.5), // This might be removed if tolerance is high
108/// LngLat::new_deg(-121.0, 37.0),
109/// ];
110///
111/// let mut simplified = Vec::new();
112/// let count = simplify_dp_into(
113/// &points,
114/// 1000.0, // 1km tolerance
115/// SimplifyMethod::GreatCircleMeters,
116/// &mut simplified,
117/// );
118///
119/// assert!(count >= 2); // At least endpoints preserved
120/// assert_eq!(simplified[0], points[0]); // First point preserved
121/// assert_eq!(simplified[count - 1], points[points.len() - 1]); // Last point preserved
122/// ```
123///
124/// # Algorithm
125///
126/// Implements the [Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm):
127///
128/// 1. Draw line between first and last points
129/// 2. Find point with maximum perpendicular distance to this line
130/// 3. If distance > tolerance, keep the point and recurse on both segments
131/// 4. Otherwise, drop all intermediate points
132pub fn simplify_dp_into(
133 pts: &[LngLat],
134 tolerance_m: f64,
135 method: SimplifyMethod,
136 out: &mut Vec<LngLat>,
137) -> usize {
138 out.clear();
139
140 let mut mask = vec![false; pts.len()];
141 simplify_dp_mask(pts, tolerance_m, method, &mut mask);
142
143 for (i, &keep) in mask.iter().enumerate() {
144 if keep {
145 out.push(pts[i]);
146 }
147 }
148
149 out.len()
150}
151
152/// Generate a boolean mask indicating which points to keep during simplification.
153///
154/// This is useful when you need to know which original points were kept,
155/// or when you want to apply the same simplification to multiple related datasets.
156///
157/// # Arguments
158///
159/// * `pts` - Input polyline points
160/// * `tolerance_m` - Maximum perpendicular distance threshold in meters (or coordinate units for EuclidRaw)
161/// * `method` - Distance calculation method to use
162/// * `mask` - Output boolean vector (will be resized to match input length)
163///
164/// # Examples
165///
166/// ```rust
167/// use rapidgeo_distance::LngLat;
168/// use rapidgeo_simplify::{simplify_dp_mask, SimplifyMethod};
169///
170/// let points = vec![
171/// LngLat::new_deg(-122.0, 37.0),
172/// LngLat::new_deg(-121.5, 37.5),
173/// LngLat::new_deg(-121.0, 37.0),
174/// ];
175///
176/// let mut mask = Vec::new();
177/// simplify_dp_mask(
178/// &points,
179/// 1000.0,
180/// SimplifyMethod::GreatCircleMeters,
181/// &mut mask,
182/// );
183///
184/// assert_eq!(mask.len(), points.len());
185/// assert!(mask[0]); // First endpoint always kept
186/// assert!(mask[2]); // Last endpoint always kept
187///
188/// // Apply mask to get simplified points
189/// let simplified: Vec<_> = points
190/// .iter()
191/// .zip(mask.iter())
192/// .filter_map(|(point, &keep)| if keep { Some(*point) } else { None })
193/// .collect();
194/// ```
195pub fn simplify_dp_mask(
196 pts: &[LngLat],
197 tolerance_m: f64,
198 method: SimplifyMethod,
199 mask: &mut Vec<bool>,
200) {
201 use xt::*;
202
203 match method {
204 SimplifyMethod::GreatCircleMeters => {
205 let backend = XtGreatCircle;
206 dp::simplify_mask(pts, tolerance_m, &backend, mask);
207 }
208 SimplifyMethod::PlanarMeters => {
209 let midpoint = compute_midpoint(pts);
210 let backend = XtEnu { origin: midpoint };
211 dp::simplify_mask(pts, tolerance_m, &backend, mask);
212 }
213 SimplifyMethod::EuclidRaw => {
214 let backend = XtEuclid;
215 dp::simplify_mask(pts, tolerance_m, &backend, mask);
216 }
217 }
218}
219
220/// Compute the arithmetic midpoint of a set of points.
221///
222/// Used internally for ENU projection origin when using [`SimplifyMethod::PlanarMeters`].
223/// Returns (0,0) for empty input.
224pub(crate) fn compute_midpoint(pts: &[LngLat]) -> LngLat {
225 if pts.is_empty() {
226 return LngLat::new_deg(0.0, 0.0);
227 }
228
229 let mut sum_lng = 0.0;
230 let mut sum_lat = 0.0;
231
232 for pt in pts {
233 sum_lng += pt.lng_deg;
234 sum_lat += pt.lat_deg;
235 }
236
237 LngLat::new_deg(sum_lng / pts.len() as f64, sum_lat / pts.len() as f64)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_compute_midpoint_with_empty_points() {
246 let pts = vec![];
247 let res = compute_midpoint(&pts);
248 assert_eq!(res, LngLat::new_deg(0.0, 0.0));
249 }
250
251 #[test]
252 fn test_endpoints_always_preserved() {
253 let pts = vec![
254 LngLat::new_deg(-122.0, 37.0),
255 LngLat::new_deg(-121.5, 37.5),
256 LngLat::new_deg(-121.0, 37.0),
257 ];
258
259 let mut mask = Vec::new();
260 simplify_dp_mask(&pts, 1000.0, SimplifyMethod::GreatCircleMeters, &mut mask);
261
262 assert_eq!(mask.len(), 3);
263 assert!(mask[0]); // first endpoint
264 assert!(mask[2]); // last endpoint
265 }
266
267 #[test]
268 fn test_zero_length_segments() {
269 let pts = vec![
270 LngLat::new_deg(-122.0, 37.0),
271 LngLat::new_deg(-122.0, 37.0),
272 LngLat::new_deg(-122.0, 37.0),
273 ];
274
275 let mut mask = Vec::new();
276 simplify_dp_mask(&pts, 10.0, SimplifyMethod::GreatCircleMeters, &mut mask);
277
278 for &keep in &mask {
279 assert!(keep);
280 }
281 }
282
283 #[test]
284 fn test_tolerance_zero_returns_original() {
285 let pts = vec![
286 LngLat::new_deg(-122.0, 37.0),
287 LngLat::new_deg(-121.9, 37.1),
288 LngLat::new_deg(-121.8, 37.2),
289 LngLat::new_deg(-121.7, 37.0),
290 ];
291
292 let mut mask = Vec::new();
293 simplify_dp_mask(&pts, 0.0, SimplifyMethod::GreatCircleMeters, &mut mask);
294
295 for &keep in &mask {
296 assert!(keep);
297 }
298 }
299
300 #[test]
301 fn test_very_large_tolerance_returns_endpoints() {
302 let pts = vec![
303 LngLat::new_deg(-122.0, 37.0),
304 LngLat::new_deg(-121.9, 37.1),
305 LngLat::new_deg(-121.8, 37.2),
306 LngLat::new_deg(-121.7, 37.0),
307 ];
308
309 let mut mask = Vec::new();
310 simplify_dp_mask(
311 &pts,
312 1_000_000.0,
313 SimplifyMethod::GreatCircleMeters,
314 &mut mask,
315 );
316
317 assert!(mask[0]); // first endpoint
318 assert!(mask[3]); // last endpoint
319 assert!(!mask[1] || !mask[2]); // at least one middle point should be removed
320 }
321
322 #[test]
323 fn test_antimeridian_crossing() {
324 let pts = vec![
325 LngLat::new_deg(179.0, 0.0),
326 LngLat::new_deg(179.5, 0.1),
327 LngLat::new_deg(-179.5, 0.2),
328 LngLat::new_deg(-179.0, 0.0),
329 ];
330
331 let mut mask = Vec::new();
332 simplify_dp_mask(&pts, 1000.0, SimplifyMethod::GreatCircleMeters, &mut mask);
333
334 assert!(mask[0]); // first endpoint
335 assert!(mask[3]); // last endpoint
336 }
337
338 #[test]
339 fn test_high_latitude_longitude_squeeze() {
340 let pts = vec![
341 LngLat::new_deg(-1.0, 89.0),
342 LngLat::new_deg(0.0, 89.1),
343 LngLat::new_deg(1.0, 89.0),
344 ];
345
346 let mut mask = Vec::new();
347 simplify_dp_mask(&pts, 1000.0, SimplifyMethod::GreatCircleMeters, &mut mask);
348
349 assert!(mask[0]); // first endpoint
350 assert!(mask[2]); // last endpoint
351 }
352
353 #[test]
354 fn test_simplify_dp_into() {
355 let pts = vec![
356 LngLat::new_deg(-122.0, 37.0),
357 LngLat::new_deg(-121.5, 37.5),
358 LngLat::new_deg(-121.0, 37.0),
359 ];
360
361 let mut out = Vec::new();
362 let count = simplify_dp_into(&pts, 1000.0, SimplifyMethod::GreatCircleMeters, &mut out);
363
364 assert_eq!(count, out.len());
365 assert!(count >= 2); // at least endpoints
366 assert_eq!(out[0], pts[0]); // first endpoint preserved
367 assert_eq!(out[out.len() - 1], pts[pts.len() - 1]); // last endpoint preserved
368 }
369
370 #[test]
371 fn test_different_methods() {
372 let pts = vec![
373 LngLat::new_deg(-122.0, 37.0),
374 LngLat::new_deg(-121.5, 37.5),
375 LngLat::new_deg(-121.0, 37.0),
376 ];
377
378 for method in [
379 SimplifyMethod::GreatCircleMeters,
380 SimplifyMethod::PlanarMeters,
381 SimplifyMethod::EuclidRaw,
382 ] {
383 let mut mask = Vec::new();
384 simplify_dp_mask(&pts, 1000.0, method, &mut mask);
385
386 assert!(mask[0]);
387 assert!(mask[2]);
388 }
389 }
390
391 #[test]
392 fn test_single_point() {
393 let pts = vec![LngLat::new_deg(-122.0, 37.0)];
394
395 let mut mask = Vec::new();
396 simplify_dp_mask(&pts, 10.0, SimplifyMethod::GreatCircleMeters, &mut mask);
397
398 assert_eq!(mask.len(), 1);
399 assert!(mask[0]);
400 }
401
402 #[test]
403 fn test_two_points() {
404 let pts = vec![LngLat::new_deg(-122.0, 37.0), LngLat::new_deg(-121.0, 37.0)];
405
406 let mut mask = Vec::new();
407 simplify_dp_mask(&pts, 10.0, SimplifyMethod::GreatCircleMeters, &mut mask);
408
409 assert_eq!(mask.len(), 2);
410 assert!(mask[0]);
411 assert!(mask[1]);
412 }
413
414 #[test]
415 fn test_empty_points() {
416 let pts = vec![];
417
418 let mut mask = Vec::new();
419 simplify_dp_mask(&pts, 10.0, SimplifyMethod::GreatCircleMeters, &mut mask);
420
421 assert_eq!(mask.len(), 0);
422 }
423}