use crate::error::{AlgorithmError, Result};
use crate::vector::douglas_peucker::simplify_linestring as dp_simplify;
use oxigdal_core::vector::{Coordinate, LineString, Polygon};
#[cfg(feature = "std")]
use std::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SimplifyMethod {
DouglasPeucker,
VisvalingamWhyatt,
TopologyPreserving,
}
pub fn simplify_linestring(
line: &LineString,
tolerance: f64,
method: SimplifyMethod,
) -> Result<LineString> {
if line.coords.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "simplify_linestring",
});
}
if tolerance < 0.0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "tolerance",
message: "tolerance must be non-negative".to_string(),
});
}
match method {
SimplifyMethod::DouglasPeucker => dp_simplify(line, tolerance),
SimplifyMethod::VisvalingamWhyatt => simplify_visvalingam_whyatt(line, tolerance),
SimplifyMethod::TopologyPreserving => simplify_topology_preserving(line, tolerance),
}
}
pub fn simplify_polygon(
polygon: &Polygon,
tolerance: f64,
method: SimplifyMethod,
) -> Result<Polygon> {
if polygon.exterior.coords.len() < 4 {
return Err(AlgorithmError::InsufficientData {
operation: "simplify_polygon",
message: "polygon exterior must have at least 4 coordinates".to_string(),
});
}
let simplified_exterior = simplify_linestring(&polygon.exterior, tolerance, method)?;
if simplified_exterior.coords.len() < 4 {
return Err(AlgorithmError::GeometryError {
message: "simplified exterior would have fewer than 4 points".to_string(),
});
}
let mut simplified_interiors = Vec::with_capacity(polygon.interiors.len());
for interior in &polygon.interiors {
let simplified_interior = simplify_linestring(interior, tolerance, method)?;
if simplified_interior.coords.len() >= 4 {
simplified_interiors.push(simplified_interior);
}
}
Polygon::new(simplified_exterior, simplified_interiors).map_err(AlgorithmError::Core)
}
fn simplify_visvalingam_whyatt(line: &LineString, tolerance: f64) -> Result<LineString> {
if line.coords.len() <= 2 {
return Ok(line.clone());
}
let n = line.coords.len();
let mut keep = vec![true; n];
let mut areas = vec![f64::INFINITY; n];
keep[0] = true;
keep[n - 1] = true;
for i in 1..n - 1 {
areas[i] = triangle_area(&line.coords[i - 1], &line.coords[i], &line.coords[i + 1]);
}
let mut removed_count = 0;
let target_count = n - 2;
while removed_count < target_count {
let mut min_area = f64::INFINITY;
let mut min_idx = 0;
for i in 1..n - 1 {
if keep[i] && areas[i] < min_area {
min_area = areas[i];
min_idx = i;
}
}
if min_area > tolerance {
break;
}
keep[min_idx] = false;
removed_count += 1;
let prev = find_prev_kept(&keep, min_idx);
let next = find_next_kept(&keep, min_idx);
if let (Some(p), Some(n)) = (prev, next) {
if p > 0 {
if let Some(pp) = find_prev_kept(&keep, p) {
areas[p] = triangle_area(&line.coords[pp], &line.coords[p], &line.coords[n]);
}
}
if n < line.coords.len() - 1 {
if let Some(nn) = find_next_kept(&keep, n) {
areas[n] = triangle_area(&line.coords[p], &line.coords[n], &line.coords[nn]);
}
}
}
}
let simplified_coords: Vec<Coordinate> = line
.coords
.iter()
.zip(keep.iter())
.filter(|&(_, &k)| k)
.map(|(c, _)| *c)
.collect();
LineString::new(simplified_coords).map_err(AlgorithmError::Core)
}
fn simplify_topology_preserving(line: &LineString, tolerance: f64) -> Result<LineString> {
let simplified = dp_simplify(line, tolerance)?;
if has_self_intersection(&simplified) {
let conservative_tolerance = tolerance * 0.5;
if conservative_tolerance < 1e-10 {
return Ok(line.clone());
}
return simplify_topology_preserving(line, conservative_tolerance);
}
Ok(simplified)
}
fn triangle_area(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate) -> f64 {
let ab_x = p2.x - p1.x;
let ab_y = p2.y - p1.y;
let ac_x = p3.x - p1.x;
let ac_y = p3.y - p1.y;
((ab_x * ac_y - ab_y * ac_x).abs()) / 2.0
}
fn find_prev_kept(keep: &[bool], from: usize) -> Option<usize> {
if from == 0 {
return None;
}
(0..from).rev().find(|&i| keep[i])
}
fn find_next_kept(keep: &[bool], from: usize) -> Option<usize> {
for i in from + 1..keep.len() {
if keep[i] {
return Some(i);
}
}
None
}
fn has_self_intersection(line: &LineString) -> bool {
let n = line.coords.len();
if n < 4 {
return false; }
for i in 0..n - 1 {
for j in i + 2..n - 1 {
if j == i + 1 || (i == 0 && j == n - 2) {
continue;
}
if segments_intersect(
&line.coords[i],
&line.coords[i + 1],
&line.coords[j],
&line.coords[j + 1],
) {
return true;
}
}
}
false
}
fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
let d1 = direction(p3, p4, p1);
let d2 = direction(p3, p4, p2);
let d3 = direction(p1, p2, p3);
let d4 = direction(p1, p2, p4);
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
&& ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
{
return true;
}
if d1.abs() < f64::EPSILON && on_segment(p3, p1, p4) {
return true;
}
if d2.abs() < f64::EPSILON && on_segment(p3, p2, p4) {
return true;
}
if d3.abs() < f64::EPSILON && on_segment(p1, p3, p2) {
return true;
}
if d4.abs() < f64::EPSILON && on_segment(p1, p4, p2) {
return true;
}
false
}
fn direction(a: &Coordinate, b: &Coordinate, p: &Coordinate) -> f64 {
(b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y)
}
fn on_segment(p: &Coordinate, q: &Coordinate, r: &Coordinate) -> bool {
q.x <= p.x.max(r.x) && q.x >= p.x.min(r.x) && q.y <= p.y.max(r.y) && q.y >= p.y.min(r.y)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_zigzag_line() -> Result<LineString> {
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),
];
LineString::new(coords).map_err(|e| AlgorithmError::Core(e))
}
fn create_square_polygon() -> Result<Polygon> {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(10.0, 0.0),
Coordinate::new_2d(10.0, 10.0),
Coordinate::new_2d(0.0, 10.0),
Coordinate::new_2d(0.0, 0.0),
];
let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
}
#[test]
fn test_simplify_linestring_douglas_peucker() {
let line = create_zigzag_line();
assert!(line.is_ok());
if let Ok(l) = line {
let result = simplify_linestring(&l, 0.5, SimplifyMethod::DouglasPeucker);
assert!(result.is_ok());
if let Ok(simplified) = result {
assert!(simplified.len() >= 2);
assert!(simplified.len() <= l.len());
}
}
}
#[test]
fn test_simplify_linestring_visvalingam() {
let line = create_zigzag_line();
assert!(line.is_ok());
if let Ok(l) = line {
let result = simplify_linestring(&l, 0.5, SimplifyMethod::VisvalingamWhyatt);
assert!(result.is_ok());
if let Ok(simplified) = result {
assert!(simplified.len() >= 2);
assert!(simplified.len() <= l.len());
}
}
}
#[test]
fn test_simplify_linestring_topology_preserving() {
let line = create_zigzag_line();
assert!(line.is_ok());
if let Ok(l) = line {
let result = simplify_linestring(&l, 0.5, SimplifyMethod::TopologyPreserving);
assert!(result.is_ok());
if let Ok(simplified) = result {
assert!(!has_self_intersection(&simplified));
}
}
}
#[test]
fn test_simplify_polygon() {
let poly = create_square_polygon();
assert!(poly.is_ok());
if let Ok(p) = poly {
let result = simplify_polygon(&p, 0.1, SimplifyMethod::DouglasPeucker);
assert!(result.is_ok());
if let Ok(simplified) = result {
assert_eq!(simplified.exterior.len(), 5);
}
}
}
#[test]
fn test_triangle_area() {
let p1 = Coordinate::new_2d(0.0, 0.0);
let p2 = Coordinate::new_2d(2.0, 0.0);
let p3 = Coordinate::new_2d(1.0, 2.0);
let area = triangle_area(&p1, &p2, &p3);
assert!((area - 2.0).abs() < 1e-10); }
#[test]
fn test_segments_intersect() {
let p1 = Coordinate::new_2d(0.0, 0.0);
let p2 = Coordinate::new_2d(2.0, 2.0);
let p3 = Coordinate::new_2d(0.0, 2.0);
let p4 = Coordinate::new_2d(2.0, 0.0);
assert!(segments_intersect(&p1, &p2, &p3, &p4));
}
#[test]
fn test_segments_no_intersect() {
let p1 = Coordinate::new_2d(0.0, 0.0);
let p2 = Coordinate::new_2d(1.0, 0.0);
let p3 = Coordinate::new_2d(0.0, 1.0);
let p4 = Coordinate::new_2d(1.0, 1.0);
assert!(!segments_intersect(&p1, &p2, &p3, &p4));
}
#[test]
fn test_has_self_intersection_false() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(0.0, 1.0),
];
let line = LineString::new(coords);
assert!(line.is_ok());
if let Ok(l) = line {
assert!(!has_self_intersection(&l));
}
}
#[test]
fn test_simplify_empty_line() {
let coords: Vec<Coordinate> = vec![];
let line = LineString::new(coords);
assert!(line.is_err());
}
#[test]
fn test_simplify_negative_tolerance() {
let line = create_zigzag_line();
assert!(line.is_ok());
if let Ok(l) = line {
let result = simplify_linestring(&l, -1.0, SimplifyMethod::DouglasPeucker);
assert!(result.is_err());
}
}
#[test]
fn test_find_prev_next_kept() {
let keep = vec![true, false, true, false, true];
assert_eq!(find_prev_kept(&keep, 2), Some(0));
assert_eq!(find_next_kept(&keep, 2), Some(4));
assert_eq!(find_prev_kept(&keep, 0), None);
assert_eq!(find_next_kept(&keep, 4), None);
}
}