oxigdal_algorithms/vector/
generalization.rs1use oxigdal_core::vector::{Coordinate, LineString, Polygon};
30
31#[derive(Debug, Clone)]
35pub struct CollapseOptions {
36 pub min_polygon_area: f64,
40
41 pub min_linestring_length: Option<f64>,
45}
46
47impl Default for CollapseOptions {
48 fn default() -> Self {
49 Self {
50 min_polygon_area: 0.0,
51 min_linestring_length: None,
52 }
53 }
54}
55
56impl CollapseOptions {
57 pub fn new(min_area: f64) -> Self {
59 Self {
60 min_polygon_area: min_area,
61 min_linestring_length: None,
62 }
63 }
64
65 pub fn with_min_linestring_length(mut self, min_len: f64) -> Self {
67 self.min_linestring_length = Some(min_len);
68 self
69 }
70}
71
72pub fn should_collapse_polygon(poly: &Polygon, options: &CollapseOptions) -> bool {
80 polygon_area_abs(poly) < options.min_polygon_area
81}
82
83pub fn collapse_polygon_to_point(poly: &Polygon, options: &CollapseOptions) -> Option<Coordinate> {
93 if should_collapse_polygon(poly, options) {
94 Some(polygon_centroid_2d(poly))
95 } else {
96 None
97 }
98}
99
100pub fn collapse_linestring_to_point(
110 line: &LineString,
111 options: &CollapseOptions,
112) -> Option<Coordinate> {
113 if let Some(min_len) = options.min_linestring_length {
114 let len = linestring_length(line);
115 if len < min_len {
116 return Some(linestring_midpoint(line));
117 }
118 }
119 None
120}
121
122#[derive(Debug, Clone)]
126pub enum ExaggerateAnchor {
127 Centroid,
129 BoundingBoxCenter,
131 Custom(Coordinate),
133}
134
135#[derive(Debug, Clone)]
137pub struct ExaggerateOptions {
138 pub scale_factor: f64,
144
145 pub anchor: ExaggerateAnchor,
147}
148
149impl Default for ExaggerateOptions {
150 fn default() -> Self {
151 Self {
152 scale_factor: 1.0,
153 anchor: ExaggerateAnchor::Centroid,
154 }
155 }
156}
157
158pub fn exaggerate_coord(point: &Coordinate, anchor: &Coordinate, scale: f64) -> Coordinate {
168 Coordinate {
169 x: anchor.x + scale * (point.x - anchor.x),
170 y: anchor.y + scale * (point.y - anchor.y),
171 z: match (point.z, anchor.z) {
173 (Some(pz), Some(az)) => Some(az + scale * (pz - az)),
174 (Some(pz), None) => Some(scale * pz),
175 _ => None,
176 },
177 m: point.m,
178 }
179}
180
181pub fn exaggerate_linestring(line: &LineString, options: &ExaggerateOptions) -> LineString {
193 let anchor = resolve_linestring_anchor(line, options);
194 let new_coords: Vec<Coordinate> = line
195 .coords()
196 .iter()
197 .map(|c| exaggerate_coord(c, &anchor, options.scale_factor))
198 .collect();
199 LineString::new(new_coords).unwrap_or_else(|_| line.clone())
201}
202
203pub fn exaggerate_polygon(poly: &Polygon, options: &ExaggerateOptions) -> Polygon {
212 let anchor = resolve_polygon_anchor(poly, options);
213
214 let new_exterior = exaggerate_ring(&poly.exterior, &anchor, options.scale_factor);
215 let new_holes: Vec<LineString> = poly
216 .interiors
217 .iter()
218 .map(|ring| exaggerate_ring(ring, &anchor, options.scale_factor))
219 .collect();
220
221 Polygon::new(new_exterior, new_holes).unwrap_or_else(|_| poly.clone())
222}
223
224#[derive(Debug, Clone)]
228pub struct DisplaceOptions {
229 pub min_distance: f64,
231
232 pub max_iterations: usize,
234
235 pub damping: f64,
239
240 pub convergence_tol: f64,
243}
244
245impl Default for DisplaceOptions {
246 fn default() -> Self {
247 Self {
248 min_distance: 1.0,
249 max_iterations: 50,
250 damping: 0.5,
251 convergence_tol: 1e-6,
252 }
253 }
254}
255
256#[derive(Debug, Clone)]
258pub struct DisplaceStats {
259 pub iterations: usize,
261
262 pub final_max_displacement: f64,
264
265 pub converged: bool,
267}
268
269pub fn displace_points(positions: &mut [Coordinate], options: &DisplaceOptions) -> DisplaceStats {
289 if positions.len() < 2 {
290 return DisplaceStats {
291 iterations: 0,
292 final_max_displacement: 0.0,
293 converged: true,
294 };
295 }
296
297 let mut max_disp = 0.0_f64;
298 let mut iter = 0_usize;
299
300 for _ in 0..options.max_iterations {
301 iter += 1;
302 let n = positions.len();
303 let mut deltas = vec![(0.0_f64, 0.0_f64); n];
304
305 for i in 0..n {
307 for j in (i + 1)..n {
308 let dx = positions[j].x - positions[i].x;
309 let dy = positions[j].y - positions[i].y;
310 let dist_sq = dx * dx + dy * dy;
311 let dist = dist_sq.sqrt();
312
313 if dist < options.min_distance && dist > 1e-12 {
314 let deficit = options.min_distance - dist;
315 let push = deficit * 0.5 * options.damping;
317 let ux = dx / dist;
318 let uy = dy / dist;
319
320 deltas[i].0 -= push * ux;
321 deltas[i].1 -= push * uy;
322 deltas[j].0 += push * ux;
323 deltas[j].1 += push * uy;
324 }
325 }
326 }
327
328 let mut step_max = 0.0_f64;
330 for (pos, (ddx, ddy)) in positions.iter_mut().zip(deltas.iter()) {
331 pos.x += ddx;
332 pos.y += ddy;
333 let step = (ddx * ddx + ddy * ddy).sqrt();
334 if step > step_max {
335 step_max = step;
336 }
337 }
338 max_disp = step_max;
339
340 if max_disp < options.convergence_tol {
341 break;
342 }
343 }
344
345 DisplaceStats {
346 iterations: iter,
347 final_max_displacement: max_disp,
348 converged: max_disp < options.convergence_tol,
349 }
350}
351
352pub fn displace_polygons_by_centroid(
371 polys: &mut [Polygon],
372 options: &DisplaceOptions,
373) -> DisplaceStats {
374 if polys.is_empty() {
375 return DisplaceStats {
376 iterations: 0,
377 final_max_displacement: 0.0,
378 converged: true,
379 };
380 }
381
382 let mut centroids: Vec<Coordinate> = polys.iter().map(|p| polygon_centroid_2d(p)).collect();
384 let original_centroids = centroids.clone();
385
386 let stats = displace_points(&mut centroids, options);
387
388 for (poly, (orig, new_c)) in polys
390 .iter_mut()
391 .zip(original_centroids.iter().zip(centroids.iter()))
392 {
393 let dx = new_c.x - orig.x;
394 let dy = new_c.y - orig.y;
395 *poly = translate_polygon(poly, dx, dy);
396 }
397
398 stats
399}
400
401fn polygon_area_abs(poly: &Polygon) -> f64 {
406 let exterior_area = ring_signed_area(&poly.exterior.coords).abs();
407 let holes_area: f64 = poly
408 .interiors
409 .iter()
410 .map(|h| ring_signed_area(&h.coords).abs())
411 .sum();
412 (exterior_area - holes_area).abs()
414}
415
416fn ring_signed_area(coords: &[Coordinate]) -> f64 {
420 if coords.len() < 3 {
421 return 0.0;
422 }
423 let n = coords.len();
424 let mut acc = 0.0_f64;
425 for i in 0..n {
426 let j = (i + 1) % n;
427 acc += coords[i].x * coords[j].y;
428 acc -= coords[j].x * coords[i].y;
429 }
430 acc * 0.5
431}
432
433fn polygon_centroid_2d(poly: &Polygon) -> Coordinate {
439 let ext_coords = &poly.exterior.coords;
441 let (ext_area, ext_cx, ext_cy) = ring_centroid_parts(ext_coords);
442
443 if ext_area.abs() < f64::EPSILON {
444 return coordinate_average(ext_coords);
446 }
447
448 let mut total_area = ext_area;
449 let mut wx = ext_cx * ext_area;
450 let mut wy = ext_cy * ext_area;
451
452 for hole in &poly.interiors {
454 let (h_area, h_cx, h_cy) = ring_centroid_parts(&hole.coords);
455 total_area -= h_area;
456 wx -= h_cx * h_area;
457 wy -= h_cy * h_area;
458 }
459
460 if total_area.abs() < f64::EPSILON {
461 return coordinate_average(ext_coords);
462 }
463
464 Coordinate::new_2d(wx / total_area, wy / total_area)
465}
466
467fn ring_centroid_parts(coords: &[Coordinate]) -> (f64, f64, f64) {
470 if coords.len() < 3 {
471 return (0.0, 0.0, 0.0);
472 }
473 let n = coords.len();
474 let mut area = 0.0_f64;
475 let mut cx = 0.0_f64;
476 let mut cy = 0.0_f64;
477
478 for i in 0..n {
479 let j = (i + 1) % n;
480 let cross = coords[i].x * coords[j].y - coords[j].x * coords[i].y;
481 area += cross;
482 cx += (coords[i].x + coords[j].x) * cross;
483 cy += (coords[i].y + coords[j].y) * cross;
484 }
485
486 area *= 0.5;
487
488 if area.abs() < f64::EPSILON {
489 let avg = coordinate_average(coords);
491 return (0.0, avg.x, avg.y);
492 }
493
494 cx /= 6.0 * area;
495 cy /= 6.0 * area;
496
497 (area, cx, cy)
498}
499
500fn coordinate_average(coords: &[Coordinate]) -> Coordinate {
502 if coords.is_empty() {
503 return Coordinate::new_2d(0.0, 0.0);
504 }
505 let n = coords.len() as f64;
506 let sum_x: f64 = coords.iter().map(|c| c.x).sum();
507 let sum_y: f64 = coords.iter().map(|c| c.y).sum();
508 Coordinate::new_2d(sum_x / n, sum_y / n)
509}
510
511fn linestring_length(line: &LineString) -> f64 {
513 let coords = &line.coords;
514 let n = coords.len();
515 if n < 2 {
516 return 0.0;
517 }
518 let mut len = 0.0_f64;
519 for i in 0..(n - 1) {
520 let dx = coords[i + 1].x - coords[i].x;
521 let dy = coords[i + 1].y - coords[i].y;
522 len += (dx * dx + dy * dy).sqrt();
523 }
524 len
525}
526
527fn linestring_midpoint(line: &LineString) -> Coordinate {
532 let coords = &line.coords;
533 let n = coords.len();
534
535 if n == 0 {
536 return Coordinate::new_2d(0.0, 0.0);
537 }
538 if n == 1 {
539 return coords[0];
540 }
541
542 let total = linestring_length(line);
543 if total < f64::EPSILON {
544 return coords[0];
546 }
547
548 let half = total * 0.5;
549 let mut accumulated = 0.0_f64;
550
551 for i in 0..(n - 1) {
552 let dx = coords[i + 1].x - coords[i].x;
553 let dy = coords[i + 1].y - coords[i].y;
554 let seg_len = (dx * dx + dy * dy).sqrt();
555
556 if accumulated + seg_len >= half {
557 let t = if seg_len > f64::EPSILON {
559 (half - accumulated) / seg_len
560 } else {
561 0.0
562 };
563 return Coordinate::new_2d(coords[i].x + t * dx, coords[i].y + t * dy);
564 }
565 accumulated += seg_len;
566 }
567
568 coords[n - 1]
570}
571
572fn exaggerate_ring(ring: &LineString, anchor: &Coordinate, scale: f64) -> LineString {
578 let new_coords: Vec<Coordinate> = ring
579 .coords
580 .iter()
581 .map(|c| exaggerate_coord(c, anchor, scale))
582 .collect();
583 LineString::new(new_coords).unwrap_or_else(|_| ring.clone())
585}
586
587fn resolve_polygon_anchor(poly: &Polygon, options: &ExaggerateOptions) -> Coordinate {
589 match &options.anchor {
590 ExaggerateAnchor::Centroid => polygon_centroid_2d(poly),
591 ExaggerateAnchor::BoundingBoxCenter => {
592 polygon_bbox_center(poly).unwrap_or_else(|| polygon_centroid_2d(poly))
593 }
594 ExaggerateAnchor::Custom(c) => *c,
595 }
596}
597
598fn resolve_linestring_anchor(line: &LineString, options: &ExaggerateOptions) -> Coordinate {
600 match &options.anchor {
601 ExaggerateAnchor::Centroid => linestring_arc_centroid(line),
602 ExaggerateAnchor::BoundingBoxCenter => {
603 linestring_bbox_center(line).unwrap_or_else(|| linestring_arc_centroid(line))
604 }
605 ExaggerateAnchor::Custom(c) => *c,
606 }
607}
608
609fn polygon_bbox_center(poly: &Polygon) -> Option<Coordinate> {
612 if poly.exterior.coords.is_empty() {
613 return None;
614 }
615 let mut min_x = f64::INFINITY;
616 let mut min_y = f64::INFINITY;
617 let mut max_x = f64::NEG_INFINITY;
618 let mut max_y = f64::NEG_INFINITY;
619
620 for c in &poly.exterior.coords {
621 if c.x < min_x {
622 min_x = c.x;
623 }
624 if c.x > max_x {
625 max_x = c.x;
626 }
627 if c.y < min_y {
628 min_y = c.y;
629 }
630 if c.y > max_y {
631 max_y = c.y;
632 }
633 }
634 Some(Coordinate::new_2d(
635 (min_x + max_x) * 0.5,
636 (min_y + max_y) * 0.5,
637 ))
638}
639
640fn linestring_bbox_center(line: &LineString) -> Option<Coordinate> {
643 if line.coords.is_empty() {
644 return None;
645 }
646 let mut min_x = f64::INFINITY;
647 let mut min_y = f64::INFINITY;
648 let mut max_x = f64::NEG_INFINITY;
649 let mut max_y = f64::NEG_INFINITY;
650
651 for c in &line.coords {
652 if c.x < min_x {
653 min_x = c.x;
654 }
655 if c.x > max_x {
656 max_x = c.x;
657 }
658 if c.y < min_y {
659 min_y = c.y;
660 }
661 if c.y > max_y {
662 max_y = c.y;
663 }
664 }
665 Some(Coordinate::new_2d(
666 (min_x + max_x) * 0.5,
667 (min_y + max_y) * 0.5,
668 ))
669}
670
671fn linestring_arc_centroid(line: &LineString) -> Coordinate {
675 let coords = &line.coords;
676 let n = coords.len();
677
678 if n == 0 {
679 return Coordinate::new_2d(0.0, 0.0);
680 }
681 if n == 1 {
682 return coords[0];
683 }
684
685 let mut total_len = 0.0_f64;
686 let mut wx = 0.0_f64;
687 let mut wy = 0.0_f64;
688
689 for i in 0..(n - 1) {
690 let dx = coords[i + 1].x - coords[i].x;
691 let dy = coords[i + 1].y - coords[i].y;
692 let seg_len = (dx * dx + dy * dy).sqrt();
693
694 if seg_len > f64::EPSILON {
695 let mid_x = (coords[i].x + coords[i + 1].x) * 0.5;
696 let mid_y = (coords[i].y + coords[i + 1].y) * 0.5;
697 wx += mid_x * seg_len;
698 wy += mid_y * seg_len;
699 total_len += seg_len;
700 }
701 }
702
703 if total_len < f64::EPSILON {
704 return coords[0];
705 }
706
707 Coordinate::new_2d(wx / total_len, wy / total_len)
708}
709
710fn translate_polygon(poly: &Polygon, dx: f64, dy: f64) -> Polygon {
716 let new_exterior = translate_ring(&poly.exterior, dx, dy);
717 let new_holes: Vec<LineString> = poly
718 .interiors
719 .iter()
720 .map(|h| translate_ring(h, dx, dy))
721 .collect();
722 Polygon::new(new_exterior, new_holes).unwrap_or_else(|_| poly.clone())
723}
724
725fn translate_ring(ring: &LineString, dx: f64, dy: f64) -> LineString {
727 let new_coords: Vec<Coordinate> = ring
728 .coords
729 .iter()
730 .map(|c| Coordinate {
731 x: c.x + dx,
732 y: c.y + dy,
733 z: c.z,
734 m: c.m,
735 })
736 .collect();
737 LineString::new(new_coords).unwrap_or_else(|_| ring.clone())
738}
739
740#[cfg(test)]
743mod tests {
744 use super::*;
745
746 fn make_square(x0: f64, y0: f64, side: f64) -> Polygon {
747 let coords = vec![
748 Coordinate::new_2d(x0, y0),
749 Coordinate::new_2d(x0 + side, y0),
750 Coordinate::new_2d(x0 + side, y0 + side),
751 Coordinate::new_2d(x0, y0 + side),
752 Coordinate::new_2d(x0, y0),
753 ];
754 let ext = LineString::new(coords).expect("valid square ring");
755 Polygon::new(ext, vec![]).expect("valid square polygon")
756 }
757
758 #[test]
759 fn test_polygon_area_abs_unit_square() {
760 let poly = make_square(0.0, 0.0, 1.0);
761 let area = polygon_area_abs(&poly);
762 assert!((area - 1.0).abs() < 1e-12);
763 }
764
765 #[test]
766 fn test_polygon_centroid_2d_unit_square() {
767 let poly = make_square(0.0, 0.0, 1.0);
768 let c = polygon_centroid_2d(&poly);
769 assert!((c.x - 0.5).abs() < 1e-12);
770 assert!((c.y - 0.5).abs() < 1e-12);
771 }
772
773 #[test]
774 fn test_linestring_length_horizontal() {
775 let coords = vec![
776 Coordinate::new_2d(0.0, 0.0),
777 Coordinate::new_2d(3.0, 0.0),
778 Coordinate::new_2d(3.0, 4.0),
779 ];
780 let line = LineString::new(coords).expect("valid");
781 assert!((linestring_length(&line) - 7.0).abs() < 1e-12);
782 }
783
784 #[test]
785 fn test_linestring_midpoint_two_points() {
786 let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(4.0, 0.0)];
787 let line = LineString::new(coords).expect("valid");
788 let mid = linestring_midpoint(&line);
789 assert!((mid.x - 2.0).abs() < 1e-12);
790 assert!((mid.y - 0.0).abs() < 1e-12);
791 }
792
793 #[test]
794 fn test_exaggerate_coord_doubles_from_origin() {
795 let anchor = Coordinate::new_2d(0.0, 0.0);
796 let point = Coordinate::new_2d(1.0, 2.0);
797 let result = exaggerate_coord(&point, &anchor, 2.0);
798 assert!((result.x - 2.0).abs() < 1e-12);
799 assert!((result.y - 4.0).abs() < 1e-12);
800 }
801
802 #[test]
803 fn test_translate_polygon_moves_by_delta() {
804 let poly = make_square(0.0, 0.0, 1.0);
805 let moved = translate_polygon(&poly, 5.0, 3.0);
806 let c = polygon_centroid_2d(&moved);
807 assert!((c.x - 5.5).abs() < 1e-12);
808 assert!((c.y - 3.5).abs() < 1e-12);
809 }
810}