use super::{Coordinate, LineString};
use crate::error::{AlgorithmError, Result};
pub fn simplify_linestring(line: &LineString, tolerance: f64) -> Result<LineString> {
if line.coords.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "Douglas-Peucker simplification",
});
}
if tolerance < 0.0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "tolerance",
message: "Tolerance must be non-negative".to_string(),
});
}
if line.coords.len() <= 2 {
return Ok(line.clone());
}
let mut keep = vec![false; line.coords.len()];
keep[0] = true;
keep[line.coords.len() - 1] = true;
simplify_recursive(&line.coords, &mut keep, 0, line.coords.len() - 1, tolerance);
let simplified_coords: Vec<Coordinate> = line
.coords
.iter()
.zip(keep.iter())
.filter(|&(_, &k)| k)
.map(|(p, _)| *p)
.collect();
LineString::new(simplified_coords).map_err(AlgorithmError::Core)
}
fn simplify_recursive(
coords: &[Coordinate],
keep: &mut [bool],
start: usize,
end: usize,
tolerance: f64,
) {
if end <= start + 1 {
return;
}
let mut max_dist = 0.0;
let mut max_idx = start;
for i in (start + 1)..end {
let dist = perpendicular_distance(&coords[i], &coords[start], &coords[end]);
if dist > max_dist {
max_dist = dist;
max_idx = i;
}
}
if max_dist > tolerance {
keep[max_idx] = true;
simplify_recursive(coords, keep, start, max_idx, tolerance);
simplify_recursive(coords, keep, max_idx, end, tolerance);
}
}
fn perpendicular_distance(
point: &Coordinate,
line_start: &Coordinate,
line_end: &Coordinate,
) -> f64 {
let dx = line_end.x - line_start.x;
let dy = line_end.y - line_start.y;
if dx.abs() < f64::EPSILON && dy.abs() < f64::EPSILON {
let dist_x = point.x - line_start.x;
let dist_y = point.y - line_start.y;
return (dist_x * dist_x + dist_y * dist_y).sqrt();
}
let numerator =
(dy * point.x - dx * point.y + line_end.x * line_start.y - line_end.y * line_start.x).abs();
let denominator = (dx * dx + dy * dy).sqrt();
numerator / denominator
}
#[allow(dead_code)] pub fn simplify_with_area_constraint(
line: &LineString,
tolerance: f64,
max_area_change: f64,
) -> Result<LineString> {
let simplified = simplify_linestring(line, tolerance)?;
let _ = max_area_change;
Ok(simplified)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simplify_straight_line() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(2.0, 2.0),
Coordinate::new_2d(3.0, 3.0),
];
let line = LineString::new(coords);
assert!(line.is_ok());
if let Ok(ls) = line {
let simplified = simplify_linestring(&ls, 0.1);
assert!(simplified.is_ok());
if let Ok(simplified) = simplified {
assert_eq!(simplified.coords.len(), 2);
}
}
}
#[test]
fn test_simplify_zigzag() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(2.0, 0.0),
Coordinate::new_2d(3.0, 1.0),
Coordinate::new_2d(4.0, 0.0),
];
let line = LineString::new(coords);
assert!(line.is_ok());
if let Ok(ls) = line {
let simplified = simplify_linestring(&ls, 0.1);
assert!(simplified.is_ok());
if let Ok(simplified) = simplified {
assert!(simplified.coords.len() >= 3);
}
}
}
#[test]
fn test_simplify_tolerance() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 0.01), Coordinate::new_2d(2.0, 0.0),
];
let line = LineString::new(coords);
assert!(line.is_ok());
if let Ok(ls) = line {
let result1 = simplify_linestring(&ls, 0.001);
assert!(result1.is_ok());
let result2 = simplify_linestring(&ls, 0.1);
assert!(result2.is_ok());
}
}
#[test]
fn test_simplify_empty() {
let coords: Vec<Coordinate> = vec![];
let line = LineString::new(coords);
assert!(line.is_err());
}
#[test]
fn test_simplify_two_points() {
let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 1.0)];
let line = LineString::new(coords);
assert!(line.is_ok());
if let Ok(ls) = line {
let result = simplify_linestring(&ls, 1.0);
assert!(result.is_ok());
if let Ok(simplified) = result {
assert_eq!(simplified.coords.len(), 2);
}
}
}
#[test]
fn test_perpendicular_distance() {
let point = Coordinate::new_2d(1.0, 1.0);
let line_start = Coordinate::new_2d(0.0, 0.0);
let line_end = Coordinate::new_2d(2.0, 0.0);
let dist = perpendicular_distance(&point, &line_start, &line_end);
assert!((dist - 1.0).abs() < 1e-10);
}
}