use alloc::vec;
use alloc::vec::Vec;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_tag::SameAs;
use geometry_trait::{Linestring, Point, PointMut};
use crate::cartesian::{PointToSegment, Pythagoras};
use crate::distance::DistanceStrategy;
pub trait SimplifyStrategy<G> {
type Output;
fn simplify(&self, g: &G, max_distance: f64) -> Self::Output;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DouglasPeucker<D = PointToSegment<Pythagoras>>(pub D);
impl<P, L, D> SimplifyStrategy<L> for DouglasPeucker<D>
where
P: Point<Scalar = f64> + PointMut + Default + Copy,
L: Linestring<Point = P>,
D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Output = geometry_model::Linestring<P>;
fn simplify(&self, ls: &L, max_distance: f64) -> Self::Output {
let pts: Vec<P> = ls.points().copied().collect();
if pts.len() < 3 || max_distance < 0.0 {
return geometry_model::Linestring::from_vec(pts);
}
let mut keep = vec![false; pts.len()];
keep[0] = true;
keep[pts.len() - 1] = true;
dp_recurse(&pts, 0, pts.len() - 1, max_distance, &self.0, &mut keep);
let out: Vec<P> = pts
.iter()
.zip(keep.iter())
.filter_map(|(p, &k)| if k { Some(*p) } else { None })
.collect();
geometry_model::Linestring::from_vec(out)
}
}
fn dp_recurse<P, D>(pts: &[P], lo: usize, hi: usize, eps: f64, dist: &D, keep: &mut [bool])
where
P: Point<Scalar = f64> + PointMut + Default + Copy,
D: DistanceStrategy<P, geometry_model::Segment<P>, Out = f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
if hi <= lo + 1 {
return;
}
let seg = geometry_model::Segment::new(pts[lo], pts[hi]);
let mut max_d = 0.0_f64;
let mut max_i = lo;
for (i, p) in pts.iter().enumerate().take(hi).skip(lo + 1) {
let d = dist.distance(p, &seg);
if d > max_d {
max_d = d;
max_i = i;
}
}
if max_d > eps {
keep[max_i] = true;
dp_recurse(pts, lo, max_i, eps, dist, keep);
dp_recurse(pts, max_i, hi, eps, dist, keep);
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "Simplified coordinates are exact literals."
)]
mod tests {
extern crate alloc;
use super::{DouglasPeucker, SimplifyStrategy};
use crate::cartesian::{PointToSegment, Pythagoras};
use alloc::vec;
use alloc::vec::Vec;
use geometry_cs::Cartesian;
use geometry_model::{Linestring, Point2D, linestring};
use geometry_trait::Point as _;
type Pt = Point2D<f64, Cartesian>;
fn default_dp() -> DouglasPeucker<PointToSegment<Pythagoras>> {
DouglasPeucker::default()
}
fn coords(ls: &Linestring<Pt>) -> Vec<(f64, f64)> {
ls.0.iter().map(|p| (p.get::<0>(), p.get::<1>())).collect()
}
#[test]
fn straight_three_point_line_collapses_to_endpoints() {
let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
let s = default_dp().simplify(&ls, 0.5);
assert_eq!(coords(&s), vec![(0., 0.), (2., 0.)]);
}
#[test]
fn collinear_polyline_collapses_to_endpoints() {
let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.), (2., 2.), (3., 3.)];
let s = default_dp().simplify(&ls, 0.001);
assert_eq!(coords(&s), vec![(0., 0.), (3., 3.)]);
}
#[test]
fn tiny_deviation_drops_but_large_deviation_kept() {
let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.01), (2., 0.), (3., 5.), (4., 0.)];
let s = default_dp().simplify(&ls, 0.5);
let c = coords(&s);
assert_eq!(c.first().unwrap(), &(0., 0.));
assert_eq!(c.last().unwrap(), &(4., 0.));
assert!(c.contains(&(3., 5.)));
assert!(!c.contains(&(1., 0.01)));
}
#[test]
fn short_linestring_is_returned_unchanged() {
let ls: Linestring<Pt> = linestring![(0., 0.), (1., 1.)];
let s = default_dp().simplify(&ls, 10.0);
assert_eq!(coords(&s), vec![(0., 0.), (1., 1.)]);
}
#[test]
fn negative_tolerance_copies_through_without_overflow() {
let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (2., 0.)];
let s = default_dp().simplify(&ls, -0.5);
assert_eq!(coords(&s), vec![(0., 0.), (1., 0.), (2., 0.)]);
let dup: Linestring<Pt> = linestring![(0., 0.), (0., 0.), (0., 0.), (5., 0.)];
let s2 = default_dp().simplify(&dup, -1.0);
assert_eq!(s2.0.len(), 4, "negative tolerance keeps every vertex");
}
}