use rustial_math::GeoCoord;
pub fn simplify_douglas_peucker(coords: &[GeoCoord], epsilon: f64) -> Vec<GeoCoord> {
if coords.len() <= 2 {
return coords.to_vec();
}
let mut keep = vec![false; coords.len()];
keep[0] = true;
keep[coords.len() - 1] = true;
dp_recurse(coords, 0, coords.len() - 1, epsilon, &mut keep);
coords
.iter()
.zip(keep.iter())
.filter_map(|(c, &k)| if k { Some(*c) } else { None })
.collect()
}
pub fn simplify_polygon_ring(coords: &[GeoCoord], epsilon: f64) -> Vec<GeoCoord> {
if coords.len() <= 3 {
return coords.to_vec();
}
let n = coords.len();
let has_closing = n > 1
&& (coords[0].lat - coords[n - 1].lat).abs() < 1e-12
&& (coords[0].lon - coords[n - 1].lon).abs() < 1e-12;
let open = if has_closing {
&coords[..n - 1]
} else {
coords
};
let simplified = simplify_douglas_peucker(open, epsilon);
if simplified.len() < 3 {
return coords.to_vec();
}
if has_closing {
let mut result = simplified;
result.push(result[0]);
result
} else {
simplified
}
}
fn dp_recurse(coords: &[GeoCoord], start: usize, end: usize, epsilon: f64, keep: &mut [bool]) {
if end <= start + 1 {
return;
}
let mut max_dist = 0.0_f64;
let mut max_idx = start;
let a = coords[start];
let b = coords[end];
for (i, coord) in coords.iter().enumerate().take(end).skip(start + 1) {
let d = perpendicular_distance(coord, &a, &b);
if d > max_dist {
max_dist = d;
max_idx = i;
}
}
if max_dist > epsilon {
keep[max_idx] = true;
dp_recurse(coords, start, max_idx, epsilon, keep);
dp_recurse(coords, max_idx, end, epsilon, keep);
}
}
fn perpendicular_distance(p: &GeoCoord, a: &GeoCoord, b: &GeoCoord) -> f64 {
let dx = b.lon - a.lon;
let dy = b.lat - a.lat;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-24 {
let ex = p.lon - a.lon;
let ey = p.lat - a.lat;
return (ex * ex + ey * ey).sqrt();
}
let cross = ((p.lon - a.lon) * dy - (p.lat - a.lat) * dx).abs();
cross / len_sq.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input() {
let result = simplify_douglas_peucker(&[], 0.1);
assert!(result.is_empty());
}
#[test]
fn single_point() {
let coords = vec![GeoCoord::from_lat_lon(51.0, 17.0)];
let result = simplify_douglas_peucker(&coords, 0.1);
assert_eq!(result.len(), 1);
assert_eq!(result[0], coords[0]);
}
#[test]
fn no_simplification_for_two_points() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 1.0),
];
let result = simplify_douglas_peucker(&coords, 0.1);
assert_eq!(result.len(), 2);
}
#[test]
fn straight_line_simplified() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.5),
GeoCoord::from_lat_lon(0.0, 1.0),
];
let result = simplify_douglas_peucker(&coords, 0.01);
assert_eq!(result.len(), 2);
assert_eq!(result[0], coords[0]);
assert_eq!(result[1], coords[2]);
}
#[test]
fn bent_line_keeps_vertex() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 0.5),
GeoCoord::from_lat_lon(0.0, 1.0),
];
let result = simplify_douglas_peucker(&coords, 0.01);
assert_eq!(result.len(), 3);
}
#[test]
fn large_epsilon_simplifies_everything() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.1, 0.5),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(-0.1, 1.5),
GeoCoord::from_lat_lon(0.0, 2.0),
];
let result = simplify_douglas_peucker(&coords, 10.0);
assert_eq!(result.len(), 2);
}
#[test]
fn zero_epsilon_retains_all() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.1, 0.5),
GeoCoord::from_lat_lon(0.0, 1.0),
];
let result = simplify_douglas_peucker(&coords, 0.0);
assert_eq!(result.len(), 3);
}
#[test]
fn negative_epsilon_retains_all() {
let coords = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.1, 0.5),
GeoCoord::from_lat_lon(0.0, 1.0),
];
let result = simplify_douglas_peucker(&coords, -1.0);
assert_eq!(result.len(), 3);
}
#[test]
fn altitude_is_preserved() {
let coords = vec![
GeoCoord::new(0.0, 0.0, 100.0),
GeoCoord::new(1.0, 0.5, 200.0), GeoCoord::new(0.0, 1.0, 300.0),
];
let result = simplify_douglas_peucker(&coords, 0.01);
assert_eq!(result.len(), 3);
assert_eq!(result[0].alt, 100.0);
assert_eq!(result[1].alt, 200.0);
assert_eq!(result[2].alt, 300.0);
}
#[test]
fn many_collinear_points() {
let coords: Vec<GeoCoord> = (0..100)
.map(|i| GeoCoord::from_lat_lon(0.0, i as f64 * 0.01))
.collect();
let result = simplify_douglas_peucker(&coords, 0.001);
assert_eq!(result.len(), 2, "all collinear points should be removed");
assert_eq!(result[0], coords[0]);
assert_eq!(result[1], coords[99]);
}
#[test]
fn zigzag_retains_peaks() {
let mut coords = Vec::new();
for i in 0..11 {
let lon = i as f64 * 0.1;
let lat = if i % 2 == 0 { 0.0 } else { 1.0 };
coords.push(GeoCoord::from_lat_lon(lat, lon));
}
let result = simplify_douglas_peucker(&coords, 0.01);
assert_eq!(
result.len(),
coords.len(),
"tight epsilon retains all zigzag vertices"
);
}
#[test]
fn ring_preserves_square() {
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.0), ];
let result = simplify_polygon_ring(&ring, 0.01);
assert_eq!(result.len(), 5, "4 corners + closing vertex");
assert_eq!(result[0], result[result.len() - 1]);
}
#[test]
fn ring_simplifies_collinear_edges() {
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.5), GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(0.5, 1.0), GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.5), GeoCoord::from_lat_lon(1.0, 0.0),
GeoCoord::from_lat_lon(0.5, 0.0), GeoCoord::from_lat_lon(0.0, 0.0), ];
let result = simplify_polygon_ring(&ring, 0.01);
assert!(
result.len() < ring.len(),
"expected fewer vertices, got {} (original {})",
result.len(),
ring.len()
);
assert!(
result.len() >= 4,
"ring must have at least 3 corners + close"
);
assert_eq!(result[0], result[result.len() - 1], "ring must be closed");
}
#[test]
fn ring_open_without_closing_vertex() {
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
];
let result = simplify_polygon_ring(&ring, 0.01);
assert_eq!(result.len(), 4);
}
#[test]
fn ring_too_small_is_returned_unchanged() {
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
];
let result = simplify_polygon_ring(&ring, 100.0);
assert_eq!(result.len(), 3);
}
#[test]
fn ring_degenerate_returns_unchanged() {
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 1.0),
];
let result = simplify_polygon_ring(&ring, 0.01);
assert_eq!(result.len(), 2);
}
#[test]
fn distance_on_line_is_zero() {
let a = GeoCoord::from_lat_lon(0.0, 0.0);
let b = GeoCoord::from_lat_lon(0.0, 1.0);
let p = GeoCoord::from_lat_lon(0.0, 0.5); let d = perpendicular_distance(&p, &a, &b);
assert!(d < 1e-12, "point on line should have ~0 distance, got {d}");
}
#[test]
fn distance_off_line_is_positive() {
let a = GeoCoord::from_lat_lon(0.0, 0.0);
let b = GeoCoord::from_lat_lon(0.0, 1.0);
let p = GeoCoord::from_lat_lon(1.0, 0.5); let d = perpendicular_distance(&p, &a, &b);
assert!((d - 1.0).abs() < 1e-10, "expected ~1.0 degree, got {d}");
}
#[test]
fn distance_coincident_endpoints() {
let a = GeoCoord::from_lat_lon(0.0, 0.0);
let b = GeoCoord::from_lat_lon(0.0, 0.0);
let p = GeoCoord::from_lat_lon(3.0, 4.0);
let d = perpendicular_distance(&p, &a, &b);
assert!((d - 5.0).abs() < 1e-10, "expected 5.0, got {d}");
}
}