1use crate::error::{AlgorithmError, Result};
57use oxigdal_core::vector::{Coordinate, LineString, Polygon};
58use std::collections::HashMap;
59
60const QUANTIZE_FACTOR: f64 = 1e6;
67
68#[derive(Debug, Clone)]
74pub struct TopologySimplifyOptions {
75 pub tolerance: f64,
77 pub max_retries: usize,
79 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 #[must_use]
96 pub fn with_tolerance(tolerance: f64) -> Self {
97 Self {
98 tolerance,
99 ..Self::default()
100 }
101 }
102}
103
104pub 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
122pub 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 if options.tolerance < f64::EPSILON {
144 return Ok(polygons.to_vec());
145 }
146
147 let edge_map = build_edge_map(polygons);
149
150 let mut result_polygons = Vec::with_capacity(polygons.len());
152
153 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, &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, &edge_map,
173 &mut shared_cache,
174 options,
175 )?;
176 if simplified_hole.coords.len() >= 4 {
178 simplified_interiors.push(simplified_hole);
179 }
180 }
181
182 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
205struct QPoint(i64, i64);
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210struct EdgeKey {
211 a: QPoint,
212 b: QPoint,
213}
214
215#[derive(Debug, Clone)]
217struct EdgeRecord {
218 poly_idx: usize,
219 ring_idx: usize,
220 seg_idx: usize,
221 is_reversed: bool,
223}
224
225#[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#[derive(Debug)]
237struct Chain {
238 seg_start: usize,
240 seg_end: usize,
242 shared_with: Option<usize>,
244}
245
246fn 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
267type 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 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 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
308fn 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 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 seg_class.push(None);
334 continue;
335 }
336 let neighbour = edge_map.get(&key).and_then(|records| {
337 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 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
368fn 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 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 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 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 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 result_coords[last_idx] = first;
431 } else {
432 result_coords.push(first);
433 }
434 }
435 }
436
437 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 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
457fn 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 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 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 Ok(ring.clone())
535}
536
537fn 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 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 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 if is_reversed {
577 let mut rev = cached.clone();
578 rev.reverse();
579 return rev;
580 }
581 return cached.clone();
582 }
583
584 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
603fn 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
668fn 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 let j_start = if i == 0 { i + 2 } else { i + 2 };
684 for j in j_start..seg_count {
685 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 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
740fn 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#[cfg(test)]
757mod tests {
758 use super::*;
759
760 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 #[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 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 #[test]
833 fn test_adjacent_squares_jagged_shared_edge() {
834 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), (1.0, 0.75),
841 (1.0, 1.0),
842 (0.0, 1.0),
843 (0.0, 0.0),
844 ]);
845 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), (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 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 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 #[test]
902 fn test_2x2_grid() {
903 let polys = vec![
904 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 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 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 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 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 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 #[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 assert_eq!(simplified[0].exterior.coords.len(), 5);
993 assert_eq!(simplified[1].exterior.coords.len(), 5);
994 }
995
996 #[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 #[test]
1026 fn test_junction_preservation() {
1027 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 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 #[test]
1056 fn test_self_intersection_prevention() {
1057 let mut coords = Vec::new();
1060 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 coords.push((10.0, 10.0));
1068 coords.push((0.0, 10.0));
1070 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 #[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 #[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 #[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 #[test]
1139 fn test_single_polygon() {
1140 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 assert!(simplified[0].exterior.coords.len() <= 6);
1157 assert!(simplified[0].exterior.coords.len() >= 4);
1158 }
1159
1160 #[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 #[test]
1190 fn test_shared_edge_consistency() {
1191 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 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 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 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 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 #[test]
1258 fn test_large_polygon_simplification() {
1259 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]); 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 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 #[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 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 let simplified = dp_simplify_coords(&coords, 0.01);
1309 assert_eq!(simplified.len(), 5);
1310 }
1311
1312 #[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 #[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 #[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 #[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 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 #[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}