Skip to main content

oxigdal_algorithms/vector/
simplify.rs

1//! Line and polygon simplification algorithms
2//!
3//! This module provides multiple algorithms for simplifying linear geometries
4//! while preserving their essential shape characteristics.
5//!
6//! # Algorithms
7//!
8//! - **Douglas-Peucker**: Classic recursive simplification based on perpendicular distance
9//! - **Visvalingam-Whyatt**: Progressive simplification based on triangle area
10//! - **Topology-Preserving**: Ensures simplified geometry maintains topological relationships
11//!
12//! # Examples
13//!
14//! ```
15//! # use oxigdal_algorithms::error::Result;
16//! use oxigdal_algorithms::vector::{Coordinate, LineString, simplify_linestring, SimplifyMethod};
17//!
18//! # fn main() -> Result<()> {
19//! let coords = vec![
20//!     Coordinate::new_2d(0.0, 0.0),
21//!     Coordinate::new_2d(1.0, 0.1),
22//!     Coordinate::new_2d(2.0, 0.0),
23//!     Coordinate::new_2d(3.0, 0.0),
24//! ];
25//! let line = LineString::new(coords)?;
26//! let simplified = simplify_linestring(&line, 0.2, SimplifyMethod::DouglasPeucker)?;
27//! # Ok(())
28//! # }
29//! ```
30
31use crate::error::{AlgorithmError, Result};
32use crate::vector::douglas_peucker::simplify_linestring as dp_simplify;
33use oxigdal_core::vector::{Coordinate, LineString, Polygon};
34
35#[cfg(feature = "std")]
36use std::vec::Vec;
37
38/// Simplification algorithm selection
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SimplifyMethod {
41    /// Douglas-Peucker algorithm (perpendicular distance)
42    DouglasPeucker,
43    /// Visvalingam-Whyatt algorithm (triangle area)
44    VisvalingamWhyatt,
45    /// Topology-preserving simplification
46    TopologyPreserving,
47}
48
49/// Simplifies a linestring using the specified algorithm
50///
51/// # Arguments
52///
53/// * `line` - Input line string to simplify
54/// * `tolerance` - Simplification tolerance (meaning depends on algorithm)
55/// * `method` - Simplification algorithm to use
56///
57/// # Returns
58///
59/// Simplified line string with fewer points while maintaining shape
60///
61/// # Errors
62///
63/// Returns error if:
64/// - Line is empty or has fewer than 2 points
65/// - Tolerance is negative
66/// - Algorithm encounters numerical issues
67///
68/// # Examples
69///
70/// ```
71/// # use oxigdal_algorithms::error::Result;
72/// use oxigdal_algorithms::vector::{Coordinate, LineString, simplify_linestring, SimplifyMethod};
73///
74/// # fn main() -> Result<()> {
75/// let coords = vec![
76///     Coordinate::new_2d(0.0, 0.0),
77///     Coordinate::new_2d(1.0, 0.01),
78///     Coordinate::new_2d(2.0, 0.0),
79/// ];
80/// let line = LineString::new(coords)?;
81/// let simplified = simplify_linestring(&line, 0.05, SimplifyMethod::DouglasPeucker)?;
82/// assert_eq!(simplified.len(), 2); // Middle point removed
83/// # Ok(())
84/// # }
85/// ```
86pub fn simplify_linestring(
87    line: &LineString,
88    tolerance: f64,
89    method: SimplifyMethod,
90) -> Result<LineString> {
91    if line.coords.is_empty() {
92        return Err(AlgorithmError::EmptyInput {
93            operation: "simplify_linestring",
94        });
95    }
96
97    if tolerance < 0.0 {
98        return Err(AlgorithmError::InvalidParameter {
99            parameter: "tolerance",
100            message: "tolerance must be non-negative".to_string(),
101        });
102    }
103
104    match method {
105        SimplifyMethod::DouglasPeucker => dp_simplify(line, tolerance),
106        SimplifyMethod::VisvalingamWhyatt => simplify_visvalingam_whyatt(line, tolerance),
107        SimplifyMethod::TopologyPreserving => simplify_topology_preserving(line, tolerance),
108    }
109}
110
111/// Simplifies a polygon using the specified algorithm
112///
113/// # Arguments
114///
115/// * `polygon` - Input polygon to simplify
116/// * `tolerance` - Simplification tolerance
117/// * `method` - Simplification algorithm to use
118///
119/// # Returns
120///
121/// Simplified polygon with exterior and interior rings simplified
122///
123/// # Errors
124///
125/// Returns error if:
126/// - Polygon is invalid
127/// - Tolerance is negative
128/// - Simplified polygon would have fewer than 4 points
129pub fn simplify_polygon(
130    polygon: &Polygon,
131    tolerance: f64,
132    method: SimplifyMethod,
133) -> Result<Polygon> {
134    if polygon.exterior.coords.len() < 4 {
135        return Err(AlgorithmError::InsufficientData {
136            operation: "simplify_polygon",
137            message: "polygon exterior must have at least 4 coordinates".to_string(),
138        });
139    }
140
141    // Simplify exterior ring
142    let simplified_exterior = simplify_linestring(&polygon.exterior, tolerance, method)?;
143
144    // Ensure exterior still has at least 4 points
145    if simplified_exterior.coords.len() < 4 {
146        return Err(AlgorithmError::GeometryError {
147            message: "simplified exterior would have fewer than 4 points".to_string(),
148        });
149    }
150
151    // Simplify interior rings
152    let mut simplified_interiors = Vec::with_capacity(polygon.interiors.len());
153    for interior in &polygon.interiors {
154        let simplified_interior = simplify_linestring(interior, tolerance, method)?;
155
156        // Only keep interior rings that still have at least 4 points
157        if simplified_interior.coords.len() >= 4 {
158            simplified_interiors.push(simplified_interior);
159        }
160    }
161
162    Polygon::new(simplified_exterior, simplified_interiors).map_err(AlgorithmError::Core)
163}
164
165/// Visvalingam-Whyatt simplification algorithm
166///
167/// This algorithm progressively removes vertices that form triangles with
168/// the smallest effective area, continuing until all remaining triangles
169/// exceed the tolerance threshold.
170fn simplify_visvalingam_whyatt(line: &LineString, tolerance: f64) -> Result<LineString> {
171    if line.coords.len() <= 2 {
172        return Ok(line.clone());
173    }
174
175    let n = line.coords.len();
176    let mut keep = vec![true; n];
177    let mut areas = vec![f64::INFINITY; n];
178
179    // Keep endpoints
180    keep[0] = true;
181    keep[n - 1] = true;
182
183    // Calculate initial effective areas
184    for i in 1..n - 1 {
185        areas[i] = triangle_area(&line.coords[i - 1], &line.coords[i], &line.coords[i + 1]);
186    }
187
188    // Progressively remove points with smallest area
189    let mut removed_count = 0;
190    let target_count = n - 2; // Maximum number we can remove
191
192    while removed_count < target_count {
193        // Find point with minimum area that hasn't been removed
194        let mut min_area = f64::INFINITY;
195        let mut min_idx = 0;
196
197        for i in 1..n - 1 {
198            if keep[i] && areas[i] < min_area {
199                min_area = areas[i];
200                min_idx = i;
201            }
202        }
203
204        // If minimum area exceeds tolerance, stop
205        if min_area > tolerance {
206            break;
207        }
208
209        // Remove the point
210        keep[min_idx] = false;
211        removed_count += 1;
212
213        // Recalculate areas for neighboring points
214        let prev = find_prev_kept(&keep, min_idx);
215        let next = find_next_kept(&keep, min_idx);
216
217        if let (Some(p), Some(n)) = (prev, next) {
218            // Update previous point's area
219            if p > 0 {
220                if let Some(pp) = find_prev_kept(&keep, p) {
221                    areas[p] = triangle_area(&line.coords[pp], &line.coords[p], &line.coords[n]);
222                }
223            }
224
225            // Update next point's area
226            if n < line.coords.len() - 1 {
227                if let Some(nn) = find_next_kept(&keep, n) {
228                    areas[n] = triangle_area(&line.coords[p], &line.coords[n], &line.coords[nn]);
229                }
230            }
231        }
232    }
233
234    // Build simplified line
235    let simplified_coords: Vec<Coordinate> = line
236        .coords
237        .iter()
238        .zip(keep.iter())
239        .filter(|&(_, &k)| k)
240        .map(|(c, _)| *c)
241        .collect();
242
243    LineString::new(simplified_coords).map_err(AlgorithmError::Core)
244}
245
246/// Topology-preserving simplification
247///
248/// Ensures that simplification doesn't create self-intersections or
249/// change the topology of the geometry.
250fn simplify_topology_preserving(line: &LineString, tolerance: f64) -> Result<LineString> {
251    // Use Douglas-Peucker as base
252    let simplified = dp_simplify(line, tolerance)?;
253
254    // Verify no self-intersections were created
255    if has_self_intersection(&simplified) {
256        // Fall back to more conservative tolerance
257        let conservative_tolerance = tolerance * 0.5;
258        if conservative_tolerance < 1e-10 {
259            // Can't simplify further without creating issues
260            return Ok(line.clone());
261        }
262        return simplify_topology_preserving(line, conservative_tolerance);
263    }
264
265    Ok(simplified)
266}
267
268/// Calculates the area of a triangle formed by three points
269fn triangle_area(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate) -> f64 {
270    // Use cross product formula: |AB × AC| / 2
271    let ab_x = p2.x - p1.x;
272    let ab_y = p2.y - p1.y;
273    let ac_x = p3.x - p1.x;
274    let ac_y = p3.y - p1.y;
275
276    ((ab_x * ac_y - ab_y * ac_x).abs()) / 2.0
277}
278
279/// Finds the previous kept point index
280fn find_prev_kept(keep: &[bool], from: usize) -> Option<usize> {
281    if from == 0 {
282        return None;
283    }
284
285    (0..from).rev().find(|&i| keep[i])
286}
287
288/// Finds the next kept point index
289fn find_next_kept(keep: &[bool], from: usize) -> Option<usize> {
290    for i in from + 1..keep.len() {
291        if keep[i] {
292            return Some(i);
293        }
294    }
295
296    None
297}
298
299/// Checks if a linestring has self-intersections
300fn has_self_intersection(line: &LineString) -> bool {
301    let n = line.coords.len();
302    if n < 4 {
303        return false; // Can't self-intersect with fewer than 4 points
304    }
305
306    // Check all pairs of non-adjacent segments
307    for i in 0..n - 1 {
308        for j in i + 2..n - 1 {
309            // Skip adjacent segments
310            if j == i + 1 || (i == 0 && j == n - 2) {
311                continue;
312            }
313
314            if segments_intersect(
315                &line.coords[i],
316                &line.coords[i + 1],
317                &line.coords[j],
318                &line.coords[j + 1],
319            ) {
320                return true;
321            }
322        }
323    }
324
325    false
326}
327
328/// Checks if two line segments intersect
329fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
330    let d1 = direction(p3, p4, p1);
331    let d2 = direction(p3, p4, p2);
332    let d3 = direction(p1, p2, p3);
333    let d4 = direction(p1, p2, p4);
334
335    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
336        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
337    {
338        return true;
339    }
340
341    // Check collinear cases
342    if d1.abs() < f64::EPSILON && on_segment(p3, p1, p4) {
343        return true;
344    }
345    if d2.abs() < f64::EPSILON && on_segment(p3, p2, p4) {
346        return true;
347    }
348    if d3.abs() < f64::EPSILON && on_segment(p1, p3, p2) {
349        return true;
350    }
351    if d4.abs() < f64::EPSILON && on_segment(p1, p4, p2) {
352        return true;
353    }
354
355    false
356}
357
358/// Computes the direction/orientation of point p relative to line from a to b
359fn direction(a: &Coordinate, b: &Coordinate, p: &Coordinate) -> f64 {
360    (b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y)
361}
362
363/// Checks if point q lies on segment pr
364fn on_segment(p: &Coordinate, q: &Coordinate, r: &Coordinate) -> bool {
365    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)
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    fn create_zigzag_line() -> Result<LineString> {
373        let coords = vec![
374            Coordinate::new_2d(0.0, 0.0),
375            Coordinate::new_2d(1.0, 1.0),
376            Coordinate::new_2d(2.0, 0.0),
377            Coordinate::new_2d(3.0, 1.0),
378            Coordinate::new_2d(4.0, 0.0),
379        ];
380        LineString::new(coords).map_err(|e| AlgorithmError::Core(e))
381    }
382
383    fn create_square_polygon() -> Result<Polygon> {
384        let coords = vec![
385            Coordinate::new_2d(0.0, 0.0),
386            Coordinate::new_2d(10.0, 0.0),
387            Coordinate::new_2d(10.0, 10.0),
388            Coordinate::new_2d(0.0, 10.0),
389            Coordinate::new_2d(0.0, 0.0),
390        ];
391        let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
392        Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
393    }
394
395    #[test]
396    fn test_simplify_linestring_douglas_peucker() {
397        let line = create_zigzag_line();
398        assert!(line.is_ok());
399
400        if let Ok(l) = line {
401            let result = simplify_linestring(&l, 0.5, SimplifyMethod::DouglasPeucker);
402            assert!(result.is_ok());
403
404            if let Ok(simplified) = result {
405                // Should keep endpoints and some intermediate points
406                assert!(simplified.len() >= 2);
407                assert!(simplified.len() <= l.len());
408            }
409        }
410    }
411
412    #[test]
413    fn test_simplify_linestring_visvalingam() {
414        let line = create_zigzag_line();
415        assert!(line.is_ok());
416
417        if let Ok(l) = line {
418            let result = simplify_linestring(&l, 0.5, SimplifyMethod::VisvalingamWhyatt);
419            assert!(result.is_ok());
420
421            if let Ok(simplified) = result {
422                assert!(simplified.len() >= 2);
423                assert!(simplified.len() <= l.len());
424            }
425        }
426    }
427
428    #[test]
429    fn test_simplify_linestring_topology_preserving() {
430        let line = create_zigzag_line();
431        assert!(line.is_ok());
432
433        if let Ok(l) = line {
434            let result = simplify_linestring(&l, 0.5, SimplifyMethod::TopologyPreserving);
435            assert!(result.is_ok());
436
437            if let Ok(simplified) = result {
438                // Should not have self-intersections
439                assert!(!has_self_intersection(&simplified));
440            }
441        }
442    }
443
444    #[test]
445    fn test_simplify_polygon() {
446        let poly = create_square_polygon();
447        assert!(poly.is_ok());
448
449        if let Ok(p) = poly {
450            let result = simplify_polygon(&p, 0.1, SimplifyMethod::DouglasPeucker);
451            assert!(result.is_ok());
452
453            if let Ok(simplified) = result {
454                // Square should remain a square (4 corners + closing point)
455                assert_eq!(simplified.exterior.len(), 5);
456            }
457        }
458    }
459
460    #[test]
461    fn test_triangle_area() {
462        let p1 = Coordinate::new_2d(0.0, 0.0);
463        let p2 = Coordinate::new_2d(2.0, 0.0);
464        let p3 = Coordinate::new_2d(1.0, 2.0);
465
466        let area = triangle_area(&p1, &p2, &p3);
467        assert!((area - 2.0).abs() < 1e-10); // Area = base * height / 2 = 2 * 2 / 2 = 2
468    }
469
470    #[test]
471    fn test_segments_intersect() {
472        // Intersecting segments
473        let p1 = Coordinate::new_2d(0.0, 0.0);
474        let p2 = Coordinate::new_2d(2.0, 2.0);
475        let p3 = Coordinate::new_2d(0.0, 2.0);
476        let p4 = Coordinate::new_2d(2.0, 0.0);
477
478        assert!(segments_intersect(&p1, &p2, &p3, &p4));
479    }
480
481    #[test]
482    fn test_segments_no_intersect() {
483        // Non-intersecting segments
484        let p1 = Coordinate::new_2d(0.0, 0.0);
485        let p2 = Coordinate::new_2d(1.0, 0.0);
486        let p3 = Coordinate::new_2d(0.0, 1.0);
487        let p4 = Coordinate::new_2d(1.0, 1.0);
488
489        assert!(!segments_intersect(&p1, &p2, &p3, &p4));
490    }
491
492    #[test]
493    fn test_has_self_intersection_false() {
494        let coords = vec![
495            Coordinate::new_2d(0.0, 0.0),
496            Coordinate::new_2d(1.0, 0.0),
497            Coordinate::new_2d(1.0, 1.0),
498            Coordinate::new_2d(0.0, 1.0),
499        ];
500        let line = LineString::new(coords);
501        assert!(line.is_ok());
502
503        if let Ok(l) = line {
504            assert!(!has_self_intersection(&l));
505        }
506    }
507
508    #[test]
509    fn test_simplify_empty_line() {
510        let coords: Vec<Coordinate> = vec![];
511        let line = LineString::new(coords);
512
513        // Empty line can't be created
514        assert!(line.is_err());
515    }
516
517    #[test]
518    fn test_simplify_negative_tolerance() {
519        let line = create_zigzag_line();
520        assert!(line.is_ok());
521
522        if let Ok(l) = line {
523            let result = simplify_linestring(&l, -1.0, SimplifyMethod::DouglasPeucker);
524            assert!(result.is_err());
525        }
526    }
527
528    #[test]
529    fn test_find_prev_next_kept() {
530        let keep = vec![true, false, true, false, true];
531
532        assert_eq!(find_prev_kept(&keep, 2), Some(0));
533        assert_eq!(find_next_kept(&keep, 2), Some(4));
534        assert_eq!(find_prev_kept(&keep, 0), None);
535        assert_eq!(find_next_kept(&keep, 4), None);
536    }
537}