Skip to main content

oxigdal_algorithms/vector/
topology_simplify.rs

1//! Topology-preserving simplification for polygon collections
2//!
3//! Shared edges between adjacent polygons are simplified once so that both polygons
4//! receive the identical simplified edge. Junction vertices (where three or more
5//! polygons meet or chain boundaries) are always preserved.
6//!
7//! # Algorithm
8//!
9//! 1. **Edge decomposition** -- every directed segment of every ring is assigned a
10//!    canonical `EdgeKey` (quantized, ordered endpoint pair). Each key maps to a
11//!    list of `EdgeRecord`s that identify which polygon/ring/segment produced it.
12//! 2. **Chain building** -- segments within a ring are grouped into maximal
13//!    contiguous chains that share the same neighbour polygon (or are unshared).
14//! 3. **Simplification** -- shared chains are Douglas-Peucker simplified once,
15//!    with endpoints locked. Non-shared chains are simplified independently.
16//! 4. **Reassembly** -- rings are reconstructed from simplified chains, reversing
17//!    coordinate order where the original edge winding was opposite.
18//! 5. **Validation** -- a self-intersection check is run on every ring. If any
19//!    ring fails, non-shared chains are re-simplified at half tolerance.
20//!
21//! # Examples
22//!
23//! ```
24//! # use oxigdal_algorithms::error::Result;
25//! use oxigdal_algorithms::vector::{Coordinate, LineString, Polygon};
26//! use oxigdal_algorithms::vector::{simplify_topology, TopologySimplifyOptions};
27//!
28//! # fn main() -> Result<()> {
29//! // Two adjacent squares sharing edge at x=1
30//! let sq_a = Polygon::new(
31//!     LineString::new(vec![
32//!         Coordinate::new_2d(0.0, 0.0),
33//!         Coordinate::new_2d(1.0, 0.0),
34//!         Coordinate::new_2d(1.0, 1.0),
35//!         Coordinate::new_2d(0.0, 1.0),
36//!         Coordinate::new_2d(0.0, 0.0),
37//!     ])?,
38//!     vec![],
39//! )?;
40//! let sq_b = Polygon::new(
41//!     LineString::new(vec![
42//!         Coordinate::new_2d(1.0, 0.0),
43//!         Coordinate::new_2d(2.0, 0.0),
44//!         Coordinate::new_2d(2.0, 1.0),
45//!         Coordinate::new_2d(1.0, 1.0),
46//!         Coordinate::new_2d(1.0, 0.0),
47//!     ])?,
48//!     vec![],
49//! )?;
50//! let simplified = simplify_topology(&[sq_a, sq_b], 0.1)?;
51//! assert_eq!(simplified.len(), 2);
52//! # Ok(())
53//! # }
54//! ```
55
56use crate::error::{AlgorithmError, Result};
57use oxigdal_core::vector::{Coordinate, LineString, Polygon};
58use std::collections::HashMap;
59
60// ---------------------------------------------------------------------------
61// Constants
62// ---------------------------------------------------------------------------
63
64/// Quantisation factor for snapping endpoints to a discrete grid.
65/// Coordinates are multiplied by this before rounding to i64.
66const QUANTIZE_FACTOR: f64 = 1e6;
67
68// ---------------------------------------------------------------------------
69// Public API types
70// ---------------------------------------------------------------------------
71
72/// Options for topology-preserving simplification.
73#[derive(Debug, Clone)]
74pub struct TopologySimplifyOptions {
75    /// Douglas-Peucker tolerance.
76    pub tolerance: f64,
77    /// Maximum retry attempts when self-intersection is detected.
78    pub max_retries: usize,
79    /// Tolerance reduction factor per retry (applied to non-shared chains).
80    pub retry_factor: f64,
81}
82
83impl Default for TopologySimplifyOptions {
84    fn default() -> Self {
85        Self {
86            tolerance: 1.0,
87            max_retries: 5,
88            retry_factor: 0.5,
89        }
90    }
91}
92
93impl TopologySimplifyOptions {
94    /// Create options with only a tolerance, using defaults for the rest.
95    #[must_use]
96    pub fn with_tolerance(tolerance: f64) -> Self {
97        Self {
98            tolerance,
99            ..Self::default()
100        }
101    }
102}
103
104// ---------------------------------------------------------------------------
105// Public API
106// ---------------------------------------------------------------------------
107
108/// Simplify a collection of polygons while preserving shared-edge topology.
109///
110/// Shared edges between adjacent polygons are simplified exactly once so that
111/// both polygons receive the identical simplified result. Junction vertices
112/// (chain boundaries) are always preserved.
113///
114/// # Errors
115///
116/// Returns an error if tolerance is negative or reassembled rings are degenerate.
117pub fn simplify_topology(polygons: &[Polygon], tolerance: f64) -> Result<Vec<Polygon>> {
118    let opts = TopologySimplifyOptions::with_tolerance(tolerance);
119    simplify_topology_with_options(polygons, &opts)
120}
121
122/// Simplify a collection of polygons with detailed options.
123///
124/// See [`simplify_topology`] for the simpler interface.
125///
126/// # Errors
127///
128/// Returns an error if tolerance is negative or reassembled rings are degenerate.
129pub fn simplify_topology_with_options(
130    polygons: &[Polygon],
131    options: &TopologySimplifyOptions,
132) -> Result<Vec<Polygon>> {
133    if options.tolerance < 0.0 {
134        return Err(AlgorithmError::InvalidParameter {
135            parameter: "tolerance",
136            message: "tolerance must be non-negative".to_string(),
137        });
138    }
139    if polygons.is_empty() {
140        return Ok(Vec::new());
141    }
142    // tolerance == 0 means no simplification
143    if options.tolerance < f64::EPSILON {
144        return Ok(polygons.to_vec());
145    }
146
147    // -- Step 1: edge decomposition ----------------------------------------
148    let edge_map = build_edge_map(polygons);
149
150    // -- Step 2+3+4: per-ring chain building, simplification, reassembly ---
151    let mut result_polygons = Vec::with_capacity(polygons.len());
152
153    // Shared-chain cache: key = (canonical chain key), value = simplified coords
154    let mut shared_cache: HashMap<SharedChainKey, Vec<Coordinate>> = HashMap::new();
155
156    for (poly_idx, polygon) in polygons.iter().enumerate() {
157        let simplified_exterior = simplify_ring(
158            &polygon.exterior,
159            poly_idx,
160            0, // ring_idx 0 = exterior
161            &edge_map,
162            &mut shared_cache,
163            options,
164        )?;
165
166        let mut simplified_interiors = Vec::with_capacity(polygon.interiors.len());
167        for (hole_idx, interior) in polygon.interiors.iter().enumerate() {
168            let simplified_hole = simplify_ring(
169                interior,
170                poly_idx,
171                hole_idx + 1, // ring_idx > 0 = interior
172                &edge_map,
173                &mut shared_cache,
174                options,
175            )?;
176            // Only keep holes that survive simplification
177            if simplified_hole.coords.len() >= 4 {
178                simplified_interiors.push(simplified_hole);
179            }
180        }
181
182        // Ensure exterior still valid
183        if simplified_exterior.coords.len() < 4 {
184            return Err(AlgorithmError::GeometryError {
185                message: format!(
186                    "simplified exterior of polygon {poly_idx} has fewer than 4 points"
187                ),
188            });
189        }
190
191        let poly = Polygon::new(simplified_exterior, simplified_interiors)
192            .map_err(AlgorithmError::Core)?;
193        result_polygons.push(poly);
194    }
195
196    Ok(result_polygons)
197}
198
199// ---------------------------------------------------------------------------
200// Internal types
201// ---------------------------------------------------------------------------
202
203/// Quantized 2D point (i64 pair).
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
205struct QPoint(i64, i64);
206
207/// Canonical edge key: ordered pair of quantized endpoints so that the
208/// "smaller" endpoint comes first.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210struct EdgeKey {
211    a: QPoint,
212    b: QPoint,
213}
214
215/// Record of one directed segment contributing to an EdgeKey.
216#[derive(Debug, Clone)]
217struct EdgeRecord {
218    poly_idx: usize,
219    ring_idx: usize,
220    seg_idx: usize,
221    /// True if the segment direction (start->end) was reversed to form the canonical key.
222    is_reversed: bool,
223}
224
225/// Key for caching shared-chain simplification results.
226/// (poly_a, poly_b, chain start QPoint, chain end QPoint)
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228struct SharedChainKey {
229    poly_a: usize,
230    poly_b: usize,
231    start: QPoint,
232    end: QPoint,
233}
234
235/// Describes one contiguous run of segments within a ring.
236#[derive(Debug)]
237struct Chain {
238    /// Starting segment index in the ring.
239    seg_start: usize,
240    /// One-past-the-end segment index.
241    seg_end: usize,
242    /// If Some, this chain is shared with the given polygon index.
243    shared_with: Option<usize>,
244}
245
246// ---------------------------------------------------------------------------
247// Quantization helpers
248// ---------------------------------------------------------------------------
249
250fn quantize(c: &Coordinate) -> QPoint {
251    QPoint(
252        (c.x * QUANTIZE_FACTOR).round() as i64,
253        (c.y * QUANTIZE_FACTOR).round() as i64,
254    )
255}
256
257fn make_edge_key(a: &Coordinate, b: &Coordinate) -> (EdgeKey, bool) {
258    let qa = quantize(a);
259    let qb = quantize(b);
260    if (qa.0, qa.1) <= (qb.0, qb.1) {
261        (EdgeKey { a: qa, b: qb }, false)
262    } else {
263        (EdgeKey { a: qb, b: qa }, true)
264    }
265}
266
267// ---------------------------------------------------------------------------
268// Step 1: edge map
269// ---------------------------------------------------------------------------
270
271type EdgeMap = HashMap<EdgeKey, Vec<EdgeRecord>>;
272
273fn build_edge_map(polygons: &[Polygon]) -> EdgeMap {
274    let mut map: EdgeMap = HashMap::new();
275
276    for (poly_idx, polygon) in polygons.iter().enumerate() {
277        insert_ring_edges(&mut map, &polygon.exterior, poly_idx, 0);
278        for (hole_idx, interior) in polygon.interiors.iter().enumerate() {
279            insert_ring_edges(&mut map, interior, poly_idx, hole_idx + 1);
280        }
281    }
282
283    map
284}
285
286fn insert_ring_edges(map: &mut EdgeMap, ring: &LineString, poly_idx: usize, ring_idx: usize) {
287    let n = ring.coords.len();
288    if n < 2 {
289        return;
290    }
291    // A closed ring has n-1 segments (the last coord == first coord).
292    let seg_count = n - 1;
293    for seg_idx in 0..seg_count {
294        let (key, is_reversed) = make_edge_key(&ring.coords[seg_idx], &ring.coords[seg_idx + 1]);
295        // Skip zero-length edges after quantization
296        if key.a == key.b {
297            continue;
298        }
299        map.entry(key).or_default().push(EdgeRecord {
300            poly_idx,
301            ring_idx,
302            seg_idx,
303            is_reversed,
304        });
305    }
306}
307
308// ---------------------------------------------------------------------------
309// Step 2: chain building within a ring
310// ---------------------------------------------------------------------------
311
312/// For a given ring in a given polygon, classify each segment as shared-with-X
313/// or non-shared, then merge adjacent segments with the same classification
314/// into chains.
315fn build_chains(
316    ring: &LineString,
317    poly_idx: usize,
318    ring_idx: usize,
319    edge_map: &EdgeMap,
320) -> Vec<Chain> {
321    let n = ring.coords.len();
322    if n < 2 {
323        return Vec::new();
324    }
325    let seg_count = n - 1;
326
327    // Classify each segment
328    let mut seg_class: Vec<Option<usize>> = Vec::with_capacity(seg_count);
329    for seg_idx in 0..seg_count {
330        let (key, _) = make_edge_key(&ring.coords[seg_idx], &ring.coords[seg_idx + 1]);
331        if key.a == key.b {
332            // zero-length after quantization, treat as non-shared
333            seg_class.push(None);
334            continue;
335        }
336        let neighbour = edge_map.get(&key).and_then(|records| {
337            // Find a record from a *different* polygon
338            records.iter().find_map(|r| {
339                if r.poly_idx != poly_idx {
340                    Some(r.poly_idx)
341                } else {
342                    None
343                }
344            })
345        });
346        seg_class.push(neighbour);
347    }
348
349    // Merge contiguous segments with same classification into chains
350    let mut chains = Vec::new();
351    let mut i = 0;
352    while i < seg_count {
353        let cls = seg_class[i];
354        let start = i;
355        while i < seg_count && seg_class[i] == cls {
356            i += 1;
357        }
358        chains.push(Chain {
359            seg_start: start,
360            seg_end: i,
361            shared_with: cls,
362        });
363    }
364
365    chains
366}
367
368// ---------------------------------------------------------------------------
369// Step 3+4: simplify chains and reassemble
370// ---------------------------------------------------------------------------
371
372fn simplify_ring(
373    ring: &LineString,
374    poly_idx: usize,
375    ring_idx: usize,
376    edge_map: &EdgeMap,
377    shared_cache: &mut HashMap<SharedChainKey, Vec<Coordinate>>,
378    options: &TopologySimplifyOptions,
379) -> Result<LineString> {
380    let chains = build_chains(ring, poly_idx, ring_idx, edge_map);
381    if chains.is_empty() {
382        return Ok(ring.clone());
383    }
384
385    let mut result_coords: Vec<Coordinate> = Vec::new();
386
387    for chain in &chains {
388        // Extract the coordinate sub-sequence for this chain.
389        // A chain of segments [seg_start..seg_end) covers coords[seg_start..=seg_end].
390        let chain_coords = &ring.coords[chain.seg_start..=chain.seg_end];
391
392        let simplified = match chain.shared_with {
393            Some(other_poly) => simplify_shared_chain(
394                chain_coords,
395                poly_idx,
396                other_poly,
397                shared_cache,
398                options.tolerance,
399            ),
400            None => dp_simplify_coords(chain_coords, options.tolerance),
401        };
402
403        // Append simplified coords, skipping the first if it duplicates the
404        // last coordinate already in result_coords.
405        if result_coords.is_empty() {
406            result_coords.extend_from_slice(&simplified);
407        } else if let Some(first) = simplified.first() {
408            if let Some(last) = result_coords.last() {
409                if coords_close(last, first) {
410                    // skip duplicate junction vertex
411                    if simplified.len() > 1 {
412                        result_coords.extend_from_slice(&simplified[1..]);
413                    }
414                } else {
415                    result_coords.extend_from_slice(&simplified);
416                }
417            } else {
418                result_coords.extend_from_slice(&simplified);
419            }
420        }
421    }
422
423    // Close the ring: ensure last == first (exact copy)
424    if result_coords.len() >= 2 {
425        let first = result_coords[0];
426        let last_idx = result_coords.len() - 1;
427        if !coords_exact(&result_coords[last_idx], &first) {
428            if coords_close(&result_coords[last_idx], &first) {
429                // snap to exact
430                result_coords[last_idx] = first;
431            } else {
432                result_coords.push(first);
433            }
434        }
435    }
436
437    // Self-intersection validation with retry
438    if result_coords.len() >= 4 {
439        let test_ls = LineString::new(result_coords.clone()).map_err(AlgorithmError::Core)?;
440        if has_ring_self_intersection(&test_ls) {
441            // Retry: re-simplify non-shared chains at reduced tolerance
442            let reduced = attempt_retry_simplification(
443                ring,
444                poly_idx,
445                ring_idx,
446                edge_map,
447                shared_cache,
448                options,
449            )?;
450            return Ok(reduced);
451        }
452    }
453
454    LineString::new(result_coords).map_err(AlgorithmError::Core)
455}
456
457/// Attempt re-simplification with progressively halved tolerance on non-shared
458/// chains until the ring has no self-intersections or retries are exhausted.
459fn attempt_retry_simplification(
460    ring: &LineString,
461    poly_idx: usize,
462    ring_idx: usize,
463    edge_map: &EdgeMap,
464    shared_cache: &mut HashMap<SharedChainKey, Vec<Coordinate>>,
465    options: &TopologySimplifyOptions,
466) -> Result<LineString> {
467    let chains = build_chains(ring, poly_idx, ring_idx, edge_map);
468    let mut current_tol = options.tolerance * options.retry_factor;
469
470    for _ in 0..options.max_retries {
471        let mut result_coords: Vec<Coordinate> = Vec::new();
472
473        for chain in &chains {
474            let chain_coords = &ring.coords[chain.seg_start..=chain.seg_end];
475
476            let simplified = match chain.shared_with {
477                Some(other_poly) => {
478                    // Shared chains keep original tolerance (they must stay consistent)
479                    simplify_shared_chain(
480                        chain_coords,
481                        poly_idx,
482                        other_poly,
483                        shared_cache,
484                        options.tolerance,
485                    )
486                }
487                None => dp_simplify_coords(chain_coords, current_tol),
488            };
489
490            if result_coords.is_empty() {
491                result_coords.extend_from_slice(&simplified);
492            } else if let Some(first) = simplified.first() {
493                if let Some(last) = result_coords.last() {
494                    if coords_close(last, first) {
495                        if simplified.len() > 1 {
496                            result_coords.extend_from_slice(&simplified[1..]);
497                        }
498                    } else {
499                        result_coords.extend_from_slice(&simplified);
500                    }
501                } else {
502                    result_coords.extend_from_slice(&simplified);
503                }
504            }
505        }
506
507        // Close ring
508        if result_coords.len() >= 2 {
509            let first = result_coords[0];
510            let last_idx = result_coords.len() - 1;
511            if !coords_exact(&result_coords[last_idx], &first) {
512                if coords_close(&result_coords[last_idx], &first) {
513                    result_coords[last_idx] = first;
514                } else {
515                    result_coords.push(first);
516                }
517            }
518        }
519
520        if result_coords.len() >= 4 {
521            let test_ls = LineString::new(result_coords.clone()).map_err(AlgorithmError::Core)?;
522            if !has_ring_self_intersection(&test_ls) {
523                return LineString::new(result_coords).map_err(AlgorithmError::Core);
524            }
525        }
526
527        current_tol *= options.retry_factor;
528        if current_tol < 1e-12 {
529            break;
530        }
531    }
532
533    // All retries exhausted, return original ring
534    Ok(ring.clone())
535}
536
537/// Simplify a shared chain. Uses a cache so that the same chain between two
538/// polygons is simplified exactly once.
539fn simplify_shared_chain(
540    chain_coords: &[Coordinate],
541    poly_a: usize,
542    poly_b: usize,
543    cache: &mut HashMap<SharedChainKey, Vec<Coordinate>>,
544    tolerance: f64,
545) -> Vec<Coordinate> {
546    // Canonical ordering: smaller polygon index first
547    let (ca, cb) = if poly_a <= poly_b {
548        (poly_a, poly_b)
549    } else {
550        (poly_b, poly_a)
551    };
552
553    let start_q = quantize(&chain_coords[0]);
554    let end_q = quantize(&chain_coords[chain_coords.len() - 1]);
555
556    // Build a canonical key: endpoints ordered by the canonical polygon order.
557    // If poly_a == ca, the chain_coords are in "forward" order for the canonical key.
558    // If poly_a != ca, they're reversed.
559    let is_reversed = poly_a != ca;
560
561    let (key_start, key_end) = if is_reversed {
562        (end_q, start_q)
563    } else {
564        (start_q, end_q)
565    };
566
567    let key = SharedChainKey {
568        poly_a: ca,
569        poly_b: cb,
570        start: key_start,
571        end: key_end,
572    };
573
574    if let Some(cached) = cache.get(&key) {
575        // Return in the caller's winding order
576        if is_reversed {
577            let mut rev = cached.clone();
578            rev.reverse();
579            return rev;
580        }
581        return cached.clone();
582    }
583
584    // Simplify in canonical (forward for poly_a=ca) direction
585    let canonical_coords: Vec<Coordinate> = if is_reversed {
586        chain_coords.iter().rev().copied().collect()
587    } else {
588        chain_coords.to_vec()
589    };
590
591    let simplified = dp_simplify_coords(&canonical_coords, tolerance);
592    cache.insert(key, simplified.clone());
593
594    if is_reversed {
595        let mut rev = simplified;
596        rev.reverse();
597        rev
598    } else {
599        simplified
600    }
601}
602
603// ---------------------------------------------------------------------------
604// Douglas-Peucker on raw coordinate slices
605// ---------------------------------------------------------------------------
606
607/// Douglas-Peucker simplification on a coordinate slice.
608/// Endpoints are always preserved.
609fn dp_simplify_coords(coords: &[Coordinate], tolerance: f64) -> Vec<Coordinate> {
610    if coords.len() <= 2 {
611        return coords.to_vec();
612    }
613
614    let n = coords.len();
615    let mut keep = vec![false; n];
616    keep[0] = true;
617    keep[n - 1] = true;
618
619    dp_recursive(coords, &mut keep, 0, n - 1, tolerance);
620
621    coords
622        .iter()
623        .zip(keep.iter())
624        .filter(|&(_, &k)| k)
625        .map(|(c, _)| *c)
626        .collect()
627}
628
629fn dp_recursive(coords: &[Coordinate], keep: &mut [bool], start: usize, end: usize, tol: f64) {
630    if end <= start + 1 {
631        return;
632    }
633
634    let mut max_dist: f64 = 0.0;
635    let mut max_idx = start;
636
637    for i in (start + 1)..end {
638        let d = perpendicular_distance(&coords[i], &coords[start], &coords[end]);
639        if d > max_dist {
640            max_dist = d;
641            max_idx = i;
642        }
643    }
644
645    if max_dist > tol {
646        keep[max_idx] = true;
647        dp_recursive(coords, keep, start, max_idx, tol);
648        dp_recursive(coords, keep, max_idx, end, tol);
649    }
650}
651
652fn perpendicular_distance(point: &Coordinate, a: &Coordinate, b: &Coordinate) -> f64 {
653    let dx = b.x - a.x;
654    let dy = b.y - a.y;
655    let len_sq = dx * dx + dy * dy;
656
657    if len_sq < f64::EPSILON * f64::EPSILON {
658        let ex = point.x - a.x;
659        let ey = point.y - a.y;
660        return (ex * ex + ey * ey).sqrt();
661    }
662
663    let numerator = (dy * point.x - dx * point.y + b.x * a.y - b.y * a.x).abs();
664    let denominator = len_sq.sqrt();
665    numerator / denominator
666}
667
668// ---------------------------------------------------------------------------
669// Self-intersection detection (local implementation)
670// ---------------------------------------------------------------------------
671
672/// Check if a closed ring has self-intersections (ignoring the closing edge
673/// that shares endpoints with adjacent edges).
674fn has_ring_self_intersection(ring: &LineString) -> bool {
675    let n = ring.coords.len();
676    if n < 4 {
677        return false;
678    }
679
680    let seg_count = n - 1;
681    for i in 0..seg_count {
682        // Only test non-adjacent segment pairs
683        let j_start = if i == 0 { i + 2 } else { i + 2 };
684        for j in j_start..seg_count {
685            // Skip the pair formed by the last and first segment of a closed ring
686            if i == 0 && j == seg_count - 1 {
687                continue;
688            }
689            if segments_intersect(
690                &ring.coords[i],
691                &ring.coords[i + 1],
692                &ring.coords[j],
693                &ring.coords[j + 1],
694            ) {
695                return true;
696            }
697        }
698    }
699
700    false
701}
702
703fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
704    let d1 = cross_direction(p3, p4, p1);
705    let d2 = cross_direction(p3, p4, p2);
706    let d3 = cross_direction(p1, p2, p3);
707    let d4 = cross_direction(p1, p2, p4);
708
709    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
710        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
711    {
712        return true;
713    }
714
715    // Collinear cases
716    if d1.abs() < f64::EPSILON && on_segment(p3, p1, p4) {
717        return true;
718    }
719    if d2.abs() < f64::EPSILON && on_segment(p3, p2, p4) {
720        return true;
721    }
722    if d3.abs() < f64::EPSILON && on_segment(p1, p3, p2) {
723        return true;
724    }
725    if d4.abs() < f64::EPSILON && on_segment(p1, p4, p2) {
726        return true;
727    }
728
729    false
730}
731
732fn cross_direction(a: &Coordinate, b: &Coordinate, p: &Coordinate) -> f64 {
733    (b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y)
734}
735
736fn on_segment(p: &Coordinate, q: &Coordinate, r: &Coordinate) -> bool {
737    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)
738}
739
740// ---------------------------------------------------------------------------
741// Coordinate comparison helpers
742// ---------------------------------------------------------------------------
743
744fn coords_close(a: &Coordinate, b: &Coordinate) -> bool {
745    (a.x - b.x).abs() < 1e-10 && (a.y - b.y).abs() < 1e-10
746}
747
748fn coords_exact(a: &Coordinate, b: &Coordinate) -> bool {
749    a.x == b.x && a.y == b.y
750}
751
752// ---------------------------------------------------------------------------
753// Tests
754// ---------------------------------------------------------------------------
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759
760    // -----------------------------------------------------------------------
761    // Helpers
762    // -----------------------------------------------------------------------
763
764    fn make_polygon(coords: Vec<(f64, f64)>) -> Polygon {
765        let cs: Vec<Coordinate> = coords
766            .iter()
767            .map(|&(x, y)| Coordinate::new_2d(x, y))
768            .collect();
769        let exterior = LineString::new(cs).expect("valid exterior");
770        Polygon::new(exterior, vec![]).expect("valid polygon")
771    }
772
773    fn make_polygon_with_holes(
774        exterior_coords: Vec<(f64, f64)>,
775        holes: Vec<Vec<(f64, f64)>>,
776    ) -> Polygon {
777        let ext: Vec<Coordinate> = exterior_coords
778            .iter()
779            .map(|&(x, y)| Coordinate::new_2d(x, y))
780            .collect();
781        let exterior = LineString::new(ext).expect("valid exterior");
782        let interiors: Vec<LineString> = holes
783            .into_iter()
784            .map(|h| {
785                let cs: Vec<Coordinate> =
786                    h.iter().map(|&(x, y)| Coordinate::new_2d(x, y)).collect();
787                LineString::new(cs).expect("valid hole")
788            })
789            .collect();
790        Polygon::new(exterior, interiors).expect("valid polygon with holes")
791    }
792
793    // -----------------------------------------------------------------------
794    // 1. Adjacent squares — shared edge at x=1
795    // -----------------------------------------------------------------------
796
797    #[test]
798    fn test_adjacent_squares_basic() {
799        let sq_a = make_polygon(vec![
800            (0.0, 0.0),
801            (1.0, 0.0),
802            (1.0, 1.0),
803            (0.0, 1.0),
804            (0.0, 0.0),
805        ]);
806        let sq_b = make_polygon(vec![
807            (1.0, 0.0),
808            (2.0, 0.0),
809            (2.0, 1.0),
810            (1.0, 1.0),
811            (1.0, 0.0),
812        ]);
813
814        let result = simplify_topology(&[sq_a, sq_b], 0.1);
815        assert!(result.is_ok());
816        let polygons = result.expect("simplify should succeed");
817        assert_eq!(polygons.len(), 2);
818
819        // Both polygons must have valid, closed rings
820        for p in &polygons {
821            assert!(p.exterior.coords.len() >= 4);
822            let first = &p.exterior.coords[0];
823            let last = &p.exterior.coords[p.exterior.coords.len() - 1];
824            assert!(coords_exact(first, last), "ring must be closed");
825        }
826    }
827
828    // -----------------------------------------------------------------------
829    // 2. Adjacent squares with jagged shared edge — simplification removes jag
830    // -----------------------------------------------------------------------
831
832    #[test]
833    fn test_adjacent_squares_jagged_shared_edge() {
834        // Square A: left side, with jagged shared edge at x~1
835        let sq_a = make_polygon(vec![
836            (0.0, 0.0),
837            (1.0, 0.0),
838            (1.0, 0.25),
839            (1.01, 0.5), // jag
840            (1.0, 0.75),
841            (1.0, 1.0),
842            (0.0, 1.0),
843            (0.0, 0.0),
844        ]);
845        // Square B: right side, same jagged shared edge (reversed winding)
846        let sq_b = make_polygon(vec![
847            (1.0, 0.0),
848            (2.0, 0.0),
849            (2.0, 1.0),
850            (1.0, 1.0),
851            (1.0, 0.75),
852            (1.01, 0.5), // same jag
853            (1.0, 0.25),
854            (1.0, 0.0),
855        ]);
856
857        let result = simplify_topology(&[sq_a, sq_b], 0.05);
858        assert!(result.is_ok());
859        let polygons = result.expect("simplify should succeed");
860        assert_eq!(polygons.len(), 2);
861
862        // Extract the shared edge coords from each polygon (those near x=1.0)
863        let shared_a: Vec<(f64, f64)> = polygons[0]
864            .exterior
865            .coords
866            .iter()
867            .filter(|c| (c.x - 1.0).abs() < 0.02)
868            .map(|c| (c.x, c.y))
869            .collect();
870
871        let shared_b: Vec<(f64, f64)> = polygons[1]
872            .exterior
873            .coords
874            .iter()
875            .filter(|c| (c.x - 1.0).abs() < 0.02)
876            .map(|c| (c.x, c.y))
877            .collect();
878
879        // Shared edges should contain matching coordinates (possibly reversed).
880        // A closed ring repeats its first/last vertex; if the ring starts at a
881        // junction on the shared edge, that vertex appears twice in the coord
882        // list. Deduplicate consecutive duplicates after sorting so the
883        // comparison reflects the *set* of shared-edge vertices, not the ring
884        // closure artifact.
885        let mut sorted_a = shared_a.clone();
886        sorted_a.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
887        sorted_a.dedup();
888        let mut sorted_b = shared_b.clone();
889        sorted_b.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
890        sorted_b.dedup();
891        assert_eq!(
892            sorted_a, sorted_b,
893            "shared edge vertices must match between adjacent polygons"
894        );
895    }
896
897    // -----------------------------------------------------------------------
898    // 3. 2x2 grid — four squares, shared edges, junction at center
899    // -----------------------------------------------------------------------
900
901    #[test]
902    fn test_2x2_grid() {
903        let polys = vec![
904            // bottom-left
905            make_polygon(vec![
906                (0.0, 0.0),
907                (1.0, 0.0),
908                (1.0, 1.0),
909                (0.0, 1.0),
910                (0.0, 0.0),
911            ]),
912            // bottom-right
913            make_polygon(vec![
914                (1.0, 0.0),
915                (2.0, 0.0),
916                (2.0, 1.0),
917                (1.0, 1.0),
918                (1.0, 0.0),
919            ]),
920            // top-left
921            make_polygon(vec![
922                (0.0, 1.0),
923                (1.0, 1.0),
924                (1.0, 2.0),
925                (0.0, 2.0),
926                (0.0, 1.0),
927            ]),
928            // top-right
929            make_polygon(vec![
930                (1.0, 1.0),
931                (2.0, 1.0),
932                (2.0, 2.0),
933                (1.0, 2.0),
934                (1.0, 1.0),
935            ]),
936        ];
937
938        let result = simplify_topology(&polys, 0.1);
939        assert!(result.is_ok());
940        let simplified = result.expect("simplify should succeed");
941        assert_eq!(simplified.len(), 4);
942
943        // Every polygon must remain valid
944        for (i, p) in simplified.iter().enumerate() {
945            assert!(
946                p.exterior.coords.len() >= 4,
947                "polygon {i} has too few exterior coords"
948            );
949        }
950
951        // Junction vertex (1,1) must be preserved in all four polygons
952        for (i, p) in simplified.iter().enumerate() {
953            let has_junction = p
954                .exterior
955                .coords
956                .iter()
957                .any(|c| (c.x - 1.0).abs() < 1e-10 && (c.y - 1.0).abs() < 1e-10);
958            assert!(
959                has_junction,
960                "polygon {i} must preserve junction vertex (1,1)"
961            );
962        }
963    }
964
965    // -----------------------------------------------------------------------
966    // 4. Non-adjacent polygons — no shared edges, each simplified independently
967    // -----------------------------------------------------------------------
968
969    #[test]
970    fn test_non_adjacent_polygons() {
971        let sq_a = make_polygon(vec![
972            (0.0, 0.0),
973            (1.0, 0.0),
974            (1.0, 1.0),
975            (0.0, 1.0),
976            (0.0, 0.0),
977        ]);
978        let sq_b = make_polygon(vec![
979            (5.0, 5.0),
980            (6.0, 5.0),
981            (6.0, 6.0),
982            (5.0, 6.0),
983            (5.0, 5.0),
984        ]);
985
986        let result = simplify_topology(&[sq_a.clone(), sq_b.clone()], 0.1);
987        assert!(result.is_ok());
988        let simplified = result.expect("simplify should succeed");
989        assert_eq!(simplified.len(), 2);
990
991        // Squares with no intermediate points should remain unchanged
992        assert_eq!(simplified[0].exterior.coords.len(), 5);
993        assert_eq!(simplified[1].exterior.coords.len(), 5);
994    }
995
996    // -----------------------------------------------------------------------
997    // 5. Polygon with holes
998    // -----------------------------------------------------------------------
999
1000    #[test]
1001    fn test_polygon_with_hole() {
1002        let outer = vec![
1003            (0.0, 0.0),
1004            (10.0, 0.0),
1005            (10.0, 10.0),
1006            (0.0, 10.0),
1007            (0.0, 0.0),
1008        ];
1009        let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0), (2.0, 2.0)];
1010        let poly = make_polygon_with_holes(outer, vec![hole]);
1011
1012        let result = simplify_topology(&[poly], 0.5);
1013        assert!(result.is_ok());
1014        let simplified = result.expect("simplify should succeed");
1015        assert_eq!(simplified.len(), 1);
1016        assert_eq!(simplified[0].interiors.len(), 1);
1017        assert!(simplified[0].exterior.coords.len() >= 4);
1018        assert!(simplified[0].interiors[0].coords.len() >= 4);
1019    }
1020
1021    // -----------------------------------------------------------------------
1022    // 6. Junction preservation — three polygons meeting at a single vertex
1023    // -----------------------------------------------------------------------
1024
1025    #[test]
1026    fn test_junction_preservation() {
1027        // Three triangles meeting at (0,0)
1028        let t1 = make_polygon(vec![(0.0, 0.0), (2.0, 0.0), (1.0, 2.0), (0.0, 0.0)]);
1029        let t2 = make_polygon(vec![(0.0, 0.0), (1.0, 2.0), (-1.0, 2.0), (0.0, 0.0)]);
1030        let t3 = make_polygon(vec![(0.0, 0.0), (-1.0, 2.0), (-2.0, 0.0), (0.0, 0.0)]);
1031
1032        let result = simplify_topology(&[t1, t2, t3], 0.1);
1033        assert!(result.is_ok());
1034        let simplified = result.expect("simplify should succeed");
1035        assert_eq!(simplified.len(), 3);
1036
1037        // The junction vertex (0,0) must be present in all three
1038        for (i, p) in simplified.iter().enumerate() {
1039            let has_origin = p
1040                .exterior
1041                .coords
1042                .iter()
1043                .any(|c| c.x.abs() < 1e-10 && c.y.abs() < 1e-10);
1044            assert!(
1045                has_origin,
1046                "polygon {i} must preserve junction vertex (0,0)"
1047            );
1048        }
1049    }
1050
1051    // -----------------------------------------------------------------------
1052    // 7. Self-intersection prevention
1053    // -----------------------------------------------------------------------
1054
1055    #[test]
1056    fn test_self_intersection_prevention() {
1057        // A polygon with many intermediate points on each edge that should
1058        // simplify cleanly.
1059        let mut coords = Vec::new();
1060        // Bottom edge
1061        for i in 0..=20 {
1062            let x = i as f64 * 0.5;
1063            let y = if i % 2 == 1 { 0.01 } else { 0.0 };
1064            coords.push((x, y));
1065        }
1066        // Right edge
1067        coords.push((10.0, 10.0));
1068        // Top edge
1069        coords.push((0.0, 10.0));
1070        // Close
1071        coords.push((0.0, 0.0));
1072
1073        let poly = make_polygon(coords);
1074        let result = simplify_topology(&[poly], 0.5);
1075        assert!(result.is_ok());
1076        let simplified = result.expect("simplify should succeed");
1077        assert_eq!(simplified.len(), 1);
1078        let ring = &simplified[0].exterior;
1079        assert!(!has_ring_self_intersection(ring));
1080    }
1081
1082    // -----------------------------------------------------------------------
1083    // 8. Tolerance = 0 is a no-op
1084    // -----------------------------------------------------------------------
1085
1086    #[test]
1087    fn test_tolerance_zero_noop() {
1088        let sq = make_polygon(vec![
1089            (0.0, 0.0),
1090            (1.0, 0.0),
1091            (1.0, 0.5),
1092            (1.0, 1.0),
1093            (0.0, 1.0),
1094            (0.0, 0.0),
1095        ]);
1096        let original_len = sq.exterior.coords.len();
1097
1098        let result = simplify_topology(std::slice::from_ref(&sq), 0.0);
1099        assert!(result.is_ok());
1100        let simplified = result.expect("simplify should succeed");
1101        assert_eq!(simplified[0].exterior.coords.len(), original_len);
1102        assert_eq!(simplified[0].exterior.coords, sq.exterior.coords);
1103    }
1104
1105    // -----------------------------------------------------------------------
1106    // 9. Negative tolerance is an error
1107    // -----------------------------------------------------------------------
1108
1109    #[test]
1110    fn test_negative_tolerance_error() {
1111        let sq = make_polygon(vec![
1112            (0.0, 0.0),
1113            (1.0, 0.0),
1114            (1.0, 1.0),
1115            (0.0, 1.0),
1116            (0.0, 0.0),
1117        ]);
1118        let result = simplify_topology(&[sq], -1.0);
1119        assert!(result.is_err());
1120    }
1121
1122    // -----------------------------------------------------------------------
1123    // 10. Empty input
1124    // -----------------------------------------------------------------------
1125
1126    #[test]
1127    fn test_empty_input() {
1128        let result = simplify_topology(&[], 1.0);
1129        assert!(result.is_ok());
1130        let simplified = result.expect("simplify should succeed");
1131        assert!(simplified.is_empty());
1132    }
1133
1134    // -----------------------------------------------------------------------
1135    // 11. Single polygon — no shared edges
1136    // -----------------------------------------------------------------------
1137
1138    #[test]
1139    fn test_single_polygon() {
1140        // Pentagon with intermediate collinear-ish points on one edge
1141        let poly = make_polygon(vec![
1142            (0.0, 0.0),
1143            (5.0, 0.0),
1144            (5.0, 2.5),
1145            (5.0, 5.0),
1146            (2.5, 5.0),
1147            (0.0, 5.0),
1148            (0.0, 0.0),
1149        ]);
1150
1151        let result = simplify_topology(&[poly], 0.5);
1152        assert!(result.is_ok());
1153        let simplified = result.expect("simplify should succeed");
1154        assert_eq!(simplified.len(), 1);
1155        // The collinear intermediate points should be removed
1156        assert!(simplified[0].exterior.coords.len() <= 6);
1157        assert!(simplified[0].exterior.coords.len() >= 4);
1158    }
1159
1160    // -----------------------------------------------------------------------
1161    // 12. Options — custom retry settings
1162    // -----------------------------------------------------------------------
1163
1164    #[test]
1165    fn test_custom_options() {
1166        let sq_a = make_polygon(vec![
1167            (0.0, 0.0),
1168            (1.0, 0.0),
1169            (1.0, 1.0),
1170            (0.0, 1.0),
1171            (0.0, 0.0),
1172        ]);
1173
1174        let opts = TopologySimplifyOptions {
1175            tolerance: 0.1,
1176            max_retries: 10,
1177            retry_factor: 0.25,
1178        };
1179        let result = simplify_topology_with_options(&[sq_a], &opts);
1180        assert!(result.is_ok());
1181        let simplified = result.expect("simplify should succeed");
1182        assert_eq!(simplified.len(), 1);
1183    }
1184
1185    // -----------------------------------------------------------------------
1186    // 13. Shared edge consistency — identical simplified coords
1187    // -----------------------------------------------------------------------
1188
1189    #[test]
1190    fn test_shared_edge_consistency() {
1191        // Two rectangles sharing a wavy edge at x=5.
1192        // Each has the same intermediate vertices along the shared edge.
1193        let mut left_coords = vec![(0.0, 0.0), (5.0, 0.0)];
1194        let mut right_coords = vec![(5.0, 0.0), (10.0, 0.0), (10.0, 10.0), (5.0, 10.0)];
1195
1196        // Add wavy shared-edge points going up
1197        for i in 1..10 {
1198            let y = i as f64;
1199            let x = 5.0 + 0.01 * (i as f64 * 0.7).sin();
1200            left_coords.push((x, y));
1201        }
1202        left_coords.push((5.0, 10.0));
1203        left_coords.push((0.0, 10.0));
1204        left_coords.push((0.0, 0.0));
1205
1206        // Right polygon has the same points in reverse
1207        for i in (1..10).rev() {
1208            let y = i as f64;
1209            let x = 5.0 + 0.01 * (i as f64 * 0.7).sin();
1210            right_coords.push((x, y));
1211        }
1212        right_coords.push((5.0, 0.0));
1213
1214        let left = make_polygon(left_coords);
1215        let right = make_polygon(right_coords);
1216
1217        let result = simplify_topology(&[left, right], 0.05);
1218        assert!(result.is_ok());
1219        let simplified = result.expect("simplify should succeed");
1220
1221        // Gather shared-edge coords from each polygon
1222        let near_five_left: Vec<(f64, f64)> = simplified[0]
1223            .exterior
1224            .coords
1225            .iter()
1226            .filter(|c| (c.x - 5.0).abs() < 0.02)
1227            .map(|c| (c.x, c.y))
1228            .collect();
1229        let near_five_right: Vec<(f64, f64)> = simplified[1]
1230            .exterior
1231            .coords
1232            .iter()
1233            .filter(|c| (c.x - 5.0).abs() < 0.02)
1234            .map(|c| (c.x, c.y))
1235            .collect();
1236
1237        // Sort by y to compare. Rings close by repeating the first coord; if
1238        // the repeated coord sits on the shared edge, dedup() collapses that
1239        // closure artifact so we compare the true set of shared vertices.
1240        let mut sorted_l = near_five_left;
1241        sorted_l.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1242        sorted_l.dedup();
1243        let mut sorted_r = near_five_right;
1244        sorted_r.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1245        sorted_r.dedup();
1246
1247        assert_eq!(
1248            sorted_l, sorted_r,
1249            "shared edge must have identical simplified vertices"
1250        );
1251    }
1252
1253    // -----------------------------------------------------------------------
1254    // 14. Large polygon with many points
1255    // -----------------------------------------------------------------------
1256
1257    #[test]
1258    fn test_large_polygon_simplification() {
1259        // Circle approximation with 100 points
1260        let n = 100;
1261        let mut coords = Vec::with_capacity(n + 1);
1262        for i in 0..n {
1263            let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
1264            coords.push((10.0 * angle.cos(), 10.0 * angle.sin()));
1265        }
1266        coords.push(coords[0]); // close
1267        let poly = make_polygon(coords);
1268
1269        let result = simplify_topology(&[poly], 0.5);
1270        assert!(result.is_ok());
1271        let simplified = result.expect("simplify should succeed");
1272        assert_eq!(simplified.len(), 1);
1273        // Should have significantly fewer points
1274        assert!(simplified[0].exterior.coords.len() < 80);
1275        assert!(simplified[0].exterior.coords.len() >= 4);
1276        assert!(!has_ring_self_intersection(&simplified[0].exterior));
1277    }
1278
1279    // -----------------------------------------------------------------------
1280    // 15. Internal DP on coords
1281    // -----------------------------------------------------------------------
1282
1283    #[test]
1284    fn test_dp_simplify_coords_straight() {
1285        let coords = vec![
1286            Coordinate::new_2d(0.0, 0.0),
1287            Coordinate::new_2d(1.0, 0.0),
1288            Coordinate::new_2d(2.0, 0.0),
1289            Coordinate::new_2d(3.0, 0.0),
1290        ];
1291        let simplified = dp_simplify_coords(&coords, 0.1);
1292        // Straight line => only endpoints
1293        assert_eq!(simplified.len(), 2);
1294        assert!(coords_exact(&simplified[0], &coords[0]));
1295        assert!(coords_exact(&simplified[1], &coords[3]));
1296    }
1297
1298    #[test]
1299    fn test_dp_simplify_coords_zigzag() {
1300        let coords = vec![
1301            Coordinate::new_2d(0.0, 0.0),
1302            Coordinate::new_2d(1.0, 1.0),
1303            Coordinate::new_2d(2.0, 0.0),
1304            Coordinate::new_2d(3.0, 1.0),
1305            Coordinate::new_2d(4.0, 0.0),
1306        ];
1307        // Small tolerance keeps all points
1308        let simplified = dp_simplify_coords(&coords, 0.01);
1309        assert_eq!(simplified.len(), 5);
1310    }
1311
1312    // -----------------------------------------------------------------------
1313    // 16. Edge key ordering
1314    // -----------------------------------------------------------------------
1315
1316    #[test]
1317    fn test_edge_key_canonical() {
1318        let a = Coordinate::new_2d(1.0, 2.0);
1319        let b = Coordinate::new_2d(3.0, 4.0);
1320
1321        let (key_ab, rev_ab) = make_edge_key(&a, &b);
1322        let (key_ba, rev_ba) = make_edge_key(&b, &a);
1323
1324        assert_eq!(
1325            key_ab, key_ba,
1326            "canonical keys must match regardless of direction"
1327        );
1328        assert_ne!(rev_ab, rev_ba, "reversal flags must differ");
1329    }
1330
1331    // -----------------------------------------------------------------------
1332    // 17. Quantize round-trip
1333    // -----------------------------------------------------------------------
1334
1335    #[test]
1336    fn test_quantize() {
1337        let c = Coordinate::new_2d(1.23456789, 9.87654321);
1338        let q = quantize(&c);
1339        assert_eq!(q, QPoint(1234568, 9876543));
1340    }
1341
1342    // -----------------------------------------------------------------------
1343    // 18. Perpendicular distance
1344    // -----------------------------------------------------------------------
1345
1346    #[test]
1347    fn test_perpendicular_distance_basic() {
1348        let p = Coordinate::new_2d(1.0, 1.0);
1349        let a = Coordinate::new_2d(0.0, 0.0);
1350        let b = Coordinate::new_2d(2.0, 0.0);
1351        let d = perpendicular_distance(&p, &a, &b);
1352        assert!((d - 1.0).abs() < 1e-10);
1353    }
1354
1355    #[test]
1356    fn test_perpendicular_distance_zero_length_segment() {
1357        let p = Coordinate::new_2d(3.0, 4.0);
1358        let a = Coordinate::new_2d(0.0, 0.0);
1359        let d = perpendicular_distance(&p, &a, &a);
1360        assert!((d - 5.0).abs() < 1e-10);
1361    }
1362
1363    // -----------------------------------------------------------------------
1364    // 19. has_ring_self_intersection
1365    // -----------------------------------------------------------------------
1366
1367    #[test]
1368    fn test_no_self_intersection_square() {
1369        let coords = vec![
1370            Coordinate::new_2d(0.0, 0.0),
1371            Coordinate::new_2d(1.0, 0.0),
1372            Coordinate::new_2d(1.0, 1.0),
1373            Coordinate::new_2d(0.0, 1.0),
1374            Coordinate::new_2d(0.0, 0.0),
1375        ];
1376        let ring = LineString::new(coords).expect("valid ring");
1377        assert!(!has_ring_self_intersection(&ring));
1378    }
1379
1380    #[test]
1381    fn test_self_intersection_bowtie() {
1382        // Bowtie shape: crosses itself
1383        let coords = vec![
1384            Coordinate::new_2d(0.0, 0.0),
1385            Coordinate::new_2d(2.0, 2.0),
1386            Coordinate::new_2d(2.0, 0.0),
1387            Coordinate::new_2d(0.0, 2.0),
1388            Coordinate::new_2d(0.0, 0.0),
1389        ];
1390        let ring = LineString::new(coords).expect("valid ring");
1391        assert!(has_ring_self_intersection(&ring));
1392    }
1393
1394    // -----------------------------------------------------------------------
1395    // 20. Default options
1396    // -----------------------------------------------------------------------
1397
1398    #[test]
1399    fn test_default_options() {
1400        let opts = TopologySimplifyOptions::default();
1401        assert!((opts.tolerance - 1.0).abs() < f64::EPSILON);
1402        assert_eq!(opts.max_retries, 5);
1403        assert!((opts.retry_factor - 0.5).abs() < f64::EPSILON);
1404    }
1405
1406    #[test]
1407    fn test_options_with_tolerance() {
1408        let opts = TopologySimplifyOptions::with_tolerance(2.5);
1409        assert!((opts.tolerance - 2.5).abs() < f64::EPSILON);
1410        assert_eq!(opts.max_retries, 5);
1411    }
1412}