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}