Skip to main content

geometry_algorithm/
simplify.rs

1//! `simplify(g, max_distance)` — return a Douglas–Peucker-simplified
2//! copy of `g`.
3//!
4//! Mirrors `boost::geometry::simplify(g, max_distance)` from
5//! `boost/geometry/algorithms/simplify.hpp`. The Cartesian default is
6//! [`geometry_strategy::DouglasPeucker`]`<`[`geometry_strategy::PointToSegment`]`<`[`geometry_strategy::Pythagoras`]`>>`,
7//! matching Boost's default from
8//! `boost/geometry/strategies/agnostic/simplify_douglas_peucker.hpp`.
9//!
10//! v1 ships linestring simplify only. Ring / polygon simplify (which
11//! must preserve ring closure) lands with the areal-simplify task.
12
13use geometry_cs::{CartesianFamily, CoordinateSystem};
14use geometry_strategy::{DouglasPeucker, PointToSegment, Pythagoras, SimplifyStrategy};
15use geometry_tag::SameAs;
16use geometry_trait::{Linestring, Point, PointMut};
17
18/// Return a Douglas–Peucker-simplified copy of the linestring `g`.
19///
20/// Vertices whose perpendicular distance to the chord between their
21/// retained neighbours is below `max_distance` are dropped; the first
22/// and last vertices are always kept.
23///
24/// Mirrors `boost::geometry::simplify(linestring, max_distance)` from
25/// `boost/geometry/algorithms/simplify.hpp`.
26#[must_use]
27pub fn simplify<G, P>(g: &G, max_distance: f64) -> geometry_model::Linestring<P>
28where
29    G: Linestring<Point = P>,
30    P: Point<Scalar = f64> + PointMut + Default + Copy,
31    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
32{
33    let strategy = DouglasPeucker::<PointToSegment<Pythagoras>>::default();
34    strategy.simplify(g, max_distance)
35}
36
37/// Return a simplified copy of `g` using an explicit strategy.
38///
39/// Mirrors the strategy overload of `boost::geometry::simplify` from
40/// `boost/geometry/algorithms/simplify.hpp:993-1011`. The strategy is the
41/// final argument so calls read consistently with other `_with` entries in
42/// this crate.
43#[inline]
44#[must_use]
45#[allow(
46    clippy::needless_pass_by_value,
47    reason = "simplify strategies are zero-sized or small Copy values, matching the crate's other _with entries"
48)]
49pub fn simplify_with<G, S>(g: &G, max_distance: f64, strategy: S) -> S::Output
50where
51    S: SimplifyStrategy<G>,
52{
53    strategy.simplify(g, max_distance)
54}
55
56#[cfg(test)]
57#[allow(
58    clippy::float_cmp,
59    reason = "Simplified coordinates are exact literals."
60)]
61mod tests {
62    //! Reference behaviour from
63    //! `boost/geometry/test/algorithms/simplify.cpp` — a straight
64    //! polyline collapses to its endpoints and a wiggle below tolerance
65    //! is dropped while a spike above tolerance is kept.
66
67    use super::simplify;
68    use geometry_cs::Cartesian;
69    use geometry_model::{Linestring, Point2D, linestring};
70    use geometry_trait::Point as _;
71
72    type Pt = Point2D<f64, Cartesian>;
73
74    fn coords(ls: &Linestring<Pt>) -> alloc::vec::Vec<(f64, f64)> {
75        ls.0.iter().map(|p| (p.get::<0>(), p.get::<1>())).collect()
76    }
77
78    #[test]
79    fn straight_three_point_line_collapses() {
80        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
81        let s = simplify(&ls, 0.5);
82        assert_eq!(coords(&s), alloc::vec![(0., 0.), (2., 0.)]);
83    }
84
85    #[test]
86    fn zigzag_with_tiny_deviation_collapses() {
87        // Every interior vertex sits within 0.05 of the (0,0)-(4,0)
88        // chord, so a 0.5 tolerance drops them all.
89        let ls: Linestring<Pt> =
90            linestring![(0., 0.), (1., 0.05), (2., -0.05), (3., 0.05), (4., 0.)];
91        let s = simplify(&ls, 0.5);
92        assert_eq!(coords(&s), alloc::vec![(0., 0.), (4., 0.)]);
93    }
94
95    #[test]
96    fn spike_above_tolerance_survives() {
97        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.01), (2., 5.), (3., 0.01), (4., 0.)];
98        let s = simplify(&ls, 0.5);
99        let c = coords(&s);
100        assert_eq!(c.first().unwrap(), &(0., 0.));
101        assert_eq!(c.last().unwrap(), &(4., 0.));
102        assert!(c.contains(&(2., 5.)));
103    }
104}