1use std::collections::HashMap;
14
15use oxigdal_core::vector::Coordinate;
16
17use crate::error::{AlgorithmError, Result};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Connectivity {
26 Four,
28 Eight,
30}
31
32impl Default for Connectivity {
33 fn default() -> Self {
34 Self::Eight
35 }
36}
37
38const DIR8_DX: [i32; 8] = [1, 1, 0, -1, -1, -1, 0, 1];
47const DIR8_DY: [i32; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
48
49const DIR4_DX: [i32; 4] = [1, 0, -1, 0];
54const DIR4_DY: [i32; 4] = [0, 1, 0, -1];
55
56#[derive(Debug, Clone)]
62pub(crate) struct PixelRing {
63 pub label: u32,
65 pub coords: Vec<(f64, f64)>,
67 pub signed_area: f64,
69}
70
71impl PixelRing {
72 pub fn is_exterior(&self) -> bool {
74 self.signed_area >= 0.0
75 }
76}
77
78#[derive(Debug, Clone)]
80pub(crate) struct ClassifiedPolygon {
81 pub label: u32,
82 pub exterior: Vec<(f64, f64)>,
83 pub holes: Vec<Vec<(f64, f64)>>,
84}
85
86pub(crate) fn trace_boundaries(
104 labels: &[u32],
105 width: usize,
106 height: usize,
107 connectivity: Connectivity,
108) -> Result<Vec<ClassifiedPolygon>> {
109 if labels.len() != width * height {
110 return Err(AlgorithmError::InvalidInput(
111 "label grid size does not match width*height".to_string(),
112 ));
113 }
114
115 let mut label_set: Vec<u32> = Vec::new();
117 {
118 let mut seen = std::collections::HashSet::new();
119 for &lbl in labels {
120 if lbl != 0 && seen.insert(lbl) {
121 label_set.push(lbl);
122 }
123 }
124 }
125 label_set.sort_unstable();
126
127 let mut all_rings: Vec<PixelRing> = Vec::new();
129
130 let mut visited = vec![false; width * height];
133
134 for &target_label in &label_set {
135 for v in visited.iter_mut() {
137 *v = false;
138 }
139
140 let rings = trace_label_boundaries(
142 labels,
143 width,
144 height,
145 target_label,
146 connectivity,
147 &mut visited,
148 );
149 all_rings.extend(rings);
150 }
151
152 classify_and_assemble(all_rings)
154}
155
156fn trace_label_boundaries(
158 labels: &[u32],
159 width: usize,
160 height: usize,
161 target: u32,
162 connectivity: Connectivity,
163 visited: &mut [bool],
164) -> Vec<PixelRing> {
165 let mut rings = Vec::new();
166
167 for y in 0..height {
171 for x in 0..width {
172 let idx = y * width + x;
173 if labels[idx] != target || visited[idx] {
174 continue;
175 }
176
177 if !is_boundary_pixel(labels, width, height, x, y, target) {
179 continue;
180 }
181
182 if let Some(ring) =
184 trace_single_ring(labels, width, height, x, y, target, connectivity, visited)
185 {
186 if ring.coords.len() >= 4 {
187 rings.push(ring);
188 }
189 }
190 }
191 }
192
193 rings
194}
195
196fn is_boundary_pixel(
198 labels: &[u32],
199 width: usize,
200 height: usize,
201 x: usize,
202 y: usize,
203 target: u32,
204) -> bool {
205 let neighbors: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
207 for (dx, dy) in neighbors {
208 let nx = x as i32 + dx;
209 let ny = y as i32 + dy;
210 if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
211 return true; }
213 if labels[ny as usize * width + nx as usize] != target {
214 return true;
215 }
216 }
217 false
218}
219
220fn trace_single_ring(
226 labels: &[u32],
227 width: usize,
228 height: usize,
229 start_x: usize,
230 start_y: usize,
231 target: u32,
232 connectivity: Connectivity,
233 visited: &mut [bool],
234) -> Option<PixelRing> {
235 let num_dirs = match connectivity {
236 Connectivity::Eight => 8usize,
237 Connectivity::Four => 4usize,
238 };
239
240 let dx_table: &[i32] = match connectivity {
241 Connectivity::Eight => &DIR8_DX,
242 Connectivity::Four => &DIR4_DX,
243 };
244 let dy_table: &[i32] = match connectivity {
245 Connectivity::Eight => &DIR8_DY,
246 Connectivity::Four => &DIR4_DY,
247 };
248
249 let mut coords: Vec<(f64, f64)> = Vec::new();
251 coords.push((start_x as f64 + 0.5, start_y as f64 + 0.5));
252 visited[start_y * width + start_x] = true;
253
254 let mut cx = start_x;
255 let mut cy = start_y;
256
257 let mut dir = 0usize; for d in 0..num_dirs {
263 let nx = cx as i32 + dx_table[d];
264 let ny = cy as i32 + dy_table[d];
265 if !in_bounds_and_target(labels, width, height, nx, ny, target) {
266 dir = (d + num_dirs / 2) % num_dirs;
268 break;
269 }
270 }
271
272 let max_iter = width * height * 4;
274 let mut iter_count = 0;
275
276 loop {
277 iter_count += 1;
278 if iter_count > max_iter {
279 break;
280 }
281
282 let mut found = false;
284 let search_start = (dir + 1) % num_dirs;
285
286 for step in 0..num_dirs {
287 let d = (search_start + step) % num_dirs;
288 let nx = cx as i32 + dx_table[d];
289 let ny = cy as i32 + dy_table[d];
290
291 if in_bounds_and_target(labels, width, height, nx, ny, target) {
292 let nxu = nx as usize;
293 let nyu = ny as usize;
294
295 if nxu == start_x && nyu == start_y && coords.len() > 2 {
297 coords.push((start_x as f64 + 0.5, start_y as f64 + 0.5));
299 let signed_area = compute_signed_area(&coords);
300 return Some(PixelRing {
301 label: target,
302 coords,
303 signed_area,
304 });
305 }
306
307 cx = nxu;
309 cy = nyu;
310 visited[cy * width + cx] = true;
311 coords.push((cx as f64 + 0.5, cy as f64 + 0.5));
312
313 dir = (d + num_dirs / 2) % num_dirs;
315 found = true;
316 break;
317 }
318 }
319
320 if !found {
321 break;
323 }
324 }
325
326 if coords.len() >= 3 {
328 let first = coords[0];
329 let last = coords[coords.len() - 1];
330 if (first.0 - last.0).abs() > f64::EPSILON || (first.1 - last.1).abs() > f64::EPSILON {
331 coords.push(first);
332 }
333 let signed_area = compute_signed_area(&coords);
334 return Some(PixelRing {
335 label: target,
336 coords,
337 signed_area,
338 });
339 }
340
341 if coords.len() <= 2 {
343 let px = start_x as f64;
344 let py = start_y as f64;
345 let square = vec![
346 (px, py),
347 (px + 1.0, py),
348 (px + 1.0, py + 1.0),
349 (px, py + 1.0),
350 (px, py),
351 ];
352 let signed_area = compute_signed_area(&square);
353 return Some(PixelRing {
354 label: target,
355 coords: square,
356 signed_area,
357 });
358 }
359
360 None
361}
362
363#[inline]
365fn in_bounds_and_target(
366 labels: &[u32],
367 width: usize,
368 height: usize,
369 nx: i32,
370 ny: i32,
371 target: u32,
372) -> bool {
373 if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
374 return false;
375 }
376 labels[ny as usize * width + nx as usize] == target
377}
378
379pub(crate) fn compute_signed_area(coords: &[(f64, f64)]) -> f64 {
388 if coords.len() < 3 {
389 return 0.0;
390 }
391 let mut area = 0.0_f64;
392 let n = coords.len();
393 for i in 0..n {
394 let j = (i + 1) % n;
395 area += coords[i].0 * coords[j].1;
396 area -= coords[j].0 * coords[i].1;
397 }
398 area * 0.5
399}
400
401fn classify_and_assemble(rings: Vec<PixelRing>) -> Result<Vec<ClassifiedPolygon>> {
407 let mut by_label: HashMap<u32, Vec<PixelRing>> = HashMap::new();
409 for ring in rings {
410 by_label.entry(ring.label).or_default().push(ring);
411 }
412
413 let mut polygons = Vec::new();
414
415 for (label, mut label_rings) in by_label {
416 let mut exteriors: Vec<Vec<(f64, f64)>> = Vec::new();
418 let mut holes: Vec<Vec<(f64, f64)>> = Vec::new();
419
420 label_rings.sort_by(|a, b| {
422 b.signed_area
423 .abs()
424 .partial_cmp(&a.signed_area.abs())
425 .unwrap_or(std::cmp::Ordering::Equal)
426 });
427
428 for ring in &label_rings {
429 if ring.is_exterior() {
430 let mut coords = ring.coords.clone();
432 if compute_signed_area(&coords) < 0.0 {
433 coords.reverse();
434 }
435 exteriors.push(coords);
436 } else {
437 let mut coords = ring.coords.clone();
439 if compute_signed_area(&coords) > 0.0 {
440 coords.reverse();
441 }
442 holes.push(coords);
443 }
444 }
445
446 if exteriors.is_empty() && !holes.is_empty() {
448 let mut largest = holes.remove(0);
449 largest.reverse(); exteriors.push(largest);
451 }
452
453 if exteriors.len() == 1 {
455 polygons.push(ClassifiedPolygon {
456 label,
457 exterior: exteriors.into_iter().next().unwrap_or_default(),
458 holes,
459 });
460 } else {
461 let mut poly_holes: Vec<Vec<Vec<(f64, f64)>>> = vec![Vec::new(); exteriors.len()];
463
464 for hole in holes {
465 if let Some(test_point) = hole.first() {
467 let mut best_idx = 0usize;
468 let mut best_area = f64::MAX;
469 for (i, ext) in exteriors.iter().enumerate() {
470 if point_in_ring(test_point.0, test_point.1, ext) {
471 let area = compute_signed_area(ext).abs();
472 if area < best_area {
473 best_area = area;
474 best_idx = i;
475 }
476 }
477 }
478 poly_holes[best_idx].push(hole);
479 }
480 }
481
482 for (i, ext) in exteriors.into_iter().enumerate() {
483 let h = if i < poly_holes.len() {
484 std::mem::take(&mut poly_holes[i])
485 } else {
486 Vec::new()
487 };
488 polygons.push(ClassifiedPolygon {
489 label,
490 exterior: ext,
491 holes: h,
492 });
493 }
494 }
495 }
496
497 polygons.sort_by_key(|p| p.label);
499
500 Ok(polygons)
501}
502
503fn point_in_ring(px: f64, py: f64, ring: &[(f64, f64)]) -> bool {
507 if ring.len() < 3 {
508 return false;
509 }
510 let mut inside = false;
511 let n = ring.len();
512 let mut j = n - 1;
513 for i in 0..n {
514 let (xi, yi) = ring[i];
515 let (xj, yj) = ring[j];
516 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
517 inside = !inside;
518 }
519 j = i;
520 }
521 inside
522}
523
524pub(crate) fn extract_pixel_edge_boundaries(
538 labels: &[u32],
539 width: usize,
540 height: usize,
541) -> Result<Vec<ClassifiedPolygon>> {
542 if labels.len() != width * height {
543 return Err(AlgorithmError::InvalidInput(
544 "label grid size does not match width*height".to_string(),
545 ));
546 }
547
548 let mut label_set: Vec<u32> = Vec::new();
550 {
551 let mut seen = std::collections::HashSet::new();
552 for &lbl in labels {
553 if lbl != 0 && seen.insert(lbl) {
554 label_set.push(lbl);
555 }
556 }
557 }
558 label_set.sort_unstable();
559
560 let mut all_polygons = Vec::new();
561
562 for &target in &label_set {
563 let mask: Vec<bool> = labels.iter().map(|&l| l == target).collect();
565
566 let edges = extract_boundary_edges(&mask, width, height);
568
569 let rings = assemble_edge_rings(&edges);
571
572 let mut exteriors: Vec<Vec<(f64, f64)>> = Vec::new();
574 let mut holes: Vec<Vec<(f64, f64)>> = Vec::new();
575
576 for ring in &rings {
577 let area = compute_signed_area(ring);
578 if area >= 0.0 {
579 exteriors.push(ring.clone());
580 } else {
581 holes.push(ring.clone());
582 }
583 }
584
585 if exteriors.is_empty() && !holes.is_empty() {
587 holes.sort_by(|a, b| {
589 compute_signed_area(b)
590 .abs()
591 .partial_cmp(&compute_signed_area(a).abs())
592 .unwrap_or(std::cmp::Ordering::Equal)
593 });
594 let mut largest = holes.remove(0);
595 largest.reverse();
596 exteriors.push(largest);
597 }
598
599 if exteriors.len() == 1 {
600 all_polygons.push(ClassifiedPolygon {
601 label: target,
602 exterior: exteriors.into_iter().next().unwrap_or_default(),
603 holes,
604 });
605 } else {
606 let mut poly_holes: Vec<Vec<Vec<(f64, f64)>>> = vec![Vec::new(); exteriors.len()];
608
609 for hole in holes {
610 if let Some(test_point) = hole.first() {
611 let mut best_idx = 0usize;
612 let mut best_area = f64::MAX;
613 for (i, ext) in exteriors.iter().enumerate() {
614 if point_in_ring(test_point.0, test_point.1, ext) {
615 let area = compute_signed_area(ext).abs();
616 if area < best_area {
617 best_area = area;
618 best_idx = i;
619 }
620 }
621 }
622 poly_holes[best_idx].push(hole);
623 }
624 }
625
626 for (i, ext) in exteriors.into_iter().enumerate() {
627 let h = if i < poly_holes.len() {
628 std::mem::take(&mut poly_holes[i])
629 } else {
630 Vec::new()
631 };
632 all_polygons.push(ClassifiedPolygon {
633 label: target,
634 exterior: ext,
635 holes: h,
636 });
637 }
638 }
639 }
640
641 all_polygons.sort_by_key(|p| p.label);
642 Ok(all_polygons)
643}
644
645#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
647struct EdgeSegment {
648 x0: i32,
650 y0: i32,
651 x1: i32,
653 y1: i32,
654}
655
656fn extract_boundary_edges(mask: &[bool], width: usize, height: usize) -> Vec<EdgeSegment> {
662 let mut edges = Vec::new();
663
664 for y in 0..height {
665 for x in 0..width {
666 if !mask[y * width + x] {
667 continue;
668 }
669
670 if y == 0 || !mask[(y - 1) * width + x] {
672 edges.push(EdgeSegment {
674 x0: x as i32,
675 y0: y as i32,
676 x1: x as i32 + 1,
677 y1: y as i32,
678 });
679 }
680
681 if y == height - 1 || !mask[(y + 1) * width + x] {
683 edges.push(EdgeSegment {
685 x0: x as i32 + 1,
686 y0: y as i32 + 1,
687 x1: x as i32,
688 y1: y as i32 + 1,
689 });
690 }
691
692 if x == 0 || !mask[y * width + x - 1] {
694 edges.push(EdgeSegment {
696 x0: x as i32,
697 y0: y as i32 + 1,
698 x1: x as i32,
699 y1: y as i32,
700 });
701 }
702
703 if x == width - 1 || !mask[y * width + x + 1] {
705 edges.push(EdgeSegment {
707 x0: x as i32 + 1,
708 y0: y as i32,
709 x1: x as i32 + 1,
710 y1: y as i32 + 1,
711 });
712 }
713 }
714 }
715
716 edges
717}
718
719fn assemble_edge_rings(edges: &[EdgeSegment]) -> Vec<Vec<(f64, f64)>> {
721 if edges.is_empty() {
722 return Vec::new();
723 }
724
725 let mut adjacency: HashMap<(i32, i32), Vec<usize>> = HashMap::with_capacity(edges.len());
727 for (i, edge) in edges.iter().enumerate() {
728 adjacency.entry((edge.x0, edge.y0)).or_default().push(i);
729 }
730
731 let mut used = vec![false; edges.len()];
732 let mut rings: Vec<Vec<(f64, f64)>> = Vec::new();
733
734 for start_idx in 0..edges.len() {
735 if used[start_idx] {
736 continue;
737 }
738
739 let mut ring = Vec::new();
740 let mut current_idx = start_idx;
741
742 loop {
743 if used[current_idx] {
744 break;
745 }
746 used[current_idx] = true;
747
748 let edge = &edges[current_idx];
749 ring.push((edge.x0 as f64, edge.y0 as f64));
750
751 let end_point = (edge.x1, edge.y1);
753 let mut found_next = false;
754
755 if let Some(candidates) = adjacency.get(&end_point) {
756 for &cand_idx in candidates {
757 if !used[cand_idx] {
758 current_idx = cand_idx;
759 found_next = true;
760 break;
761 }
762 }
763 }
764
765 if !found_next {
766 break;
767 }
768 }
769
770 if ring.len() >= 3 {
772 let first = ring[0];
773 ring.push(first);
774 rings.push(ring);
775 }
776 }
777
778 rings
779}
780
781pub(crate) fn transform_coords(
787 coords: &[(f64, f64)],
788 gt: &oxigdal_core::types::GeoTransform,
789) -> Vec<Coordinate> {
790 coords
791 .iter()
792 .map(|&(px, py)| {
793 let (wx, wy) = gt.pixel_to_world(px, py);
794 Coordinate::new_2d(wx, wy)
795 })
796 .collect()
797}
798
799pub(crate) fn pixel_coords_to_coordinates(coords: &[(f64, f64)]) -> Vec<Coordinate> {
801 coords
802 .iter()
803 .map(|&(px, py)| Coordinate::new_2d(px, py))
804 .collect()
805}
806
807#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
816 fn test_signed_area_ccw_square() {
817 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)];
819 let area = compute_signed_area(&coords);
820 assert!(
821 area > 0.0,
822 "CCW square should have positive area, got {area}"
823 );
824 assert!((area - 1.0).abs() < 1e-10);
825 }
826
827 #[test]
828 fn test_signed_area_cw_square() {
829 let coords = vec![(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)];
831 let area = compute_signed_area(&coords);
832 assert!(
833 area < 0.0,
834 "CW square should have negative area, got {area}"
835 );
836 assert!((area + 1.0).abs() < 1e-10);
837 }
838
839 #[test]
840 fn test_signed_area_degenerate() {
841 let coords = vec![(0.0, 0.0), (1.0, 0.0)];
842 assert!((compute_signed_area(&coords)).abs() < 1e-10);
843 }
844
845 #[test]
846 fn test_signed_area_empty() {
847 assert!((compute_signed_area(&[])).abs() < 1e-10);
848 }
849
850 #[test]
851 fn test_point_in_ring_inside() {
852 let ring = vec![
853 (0.0, 0.0),
854 (10.0, 0.0),
855 (10.0, 10.0),
856 (0.0, 10.0),
857 (0.0, 0.0),
858 ];
859 assert!(point_in_ring(5.0, 5.0, &ring));
860 }
861
862 #[test]
863 fn test_point_in_ring_outside() {
864 let ring = vec![
865 (0.0, 0.0),
866 (10.0, 0.0),
867 (10.0, 10.0),
868 (0.0, 10.0),
869 (0.0, 0.0),
870 ];
871 assert!(!point_in_ring(15.0, 5.0, &ring));
872 }
873
874 #[test]
875 fn test_point_in_ring_degenerate() {
876 assert!(!point_in_ring(0.0, 0.0, &[]));
877 assert!(!point_in_ring(0.0, 0.0, &[(0.0, 0.0), (1.0, 0.0)]));
878 }
879
880 #[test]
881 fn test_is_boundary_pixel_center() {
882 let labels = vec![1, 1, 1, 1, 1, 1, 1, 1, 1];
884 assert!(!is_boundary_pixel(&labels, 3, 3, 1, 1, 1));
885 }
886
887 #[test]
888 fn test_is_boundary_pixel_edge() {
889 let labels = vec![1, 1, 1, 1, 1, 1, 1, 1, 1];
891 assert!(is_boundary_pixel(&labels, 3, 3, 0, 0, 1));
892 }
893
894 #[test]
895 fn test_is_boundary_pixel_adjacent_to_different() {
896 let labels = vec![1, 2, 1, 1, 1, 1, 1, 1, 1];
897 assert!(is_boundary_pixel(&labels, 3, 3, 0, 0, 1));
899 }
900
901 #[test]
902 fn test_extract_edges_single_pixel() {
903 let mask = vec![true];
904 let edges = extract_boundary_edges(&mask, 1, 1);
905 assert_eq!(edges.len(), 4);
907 }
908
909 #[test]
910 fn test_extract_edges_2x2_solid() {
911 let mask = vec![true, true, true, true];
913 let edges = extract_boundary_edges(&mask, 2, 2);
914 assert_eq!(edges.len(), 8);
916 }
917
918 #[test]
919 fn test_assemble_edge_rings_square() {
920 let edges = vec![
922 EdgeSegment {
923 x0: 0,
924 y0: 0,
925 x1: 1,
926 y1: 0,
927 },
928 EdgeSegment {
929 x0: 1,
930 y0: 0,
931 x1: 1,
932 y1: 1,
933 },
934 EdgeSegment {
935 x0: 1,
936 y0: 1,
937 x1: 0,
938 y1: 1,
939 },
940 EdgeSegment {
941 x0: 0,
942 y0: 1,
943 x1: 0,
944 y1: 0,
945 },
946 ];
947 let rings = assemble_edge_rings(&edges);
948 assert_eq!(rings.len(), 1);
949 assert_eq!(rings[0].len(), 5); }
951
952 #[test]
953 fn test_pixel_edge_single_pixel() {
954 let labels = vec![1u32];
955 let result = extract_pixel_edge_boundaries(&labels, 1, 1);
956 assert!(result.is_ok());
957 let polys = result.ok();
958 assert!(polys.is_some());
959 let polys = polys.as_ref();
960 assert!(polys.is_some_and(|p| p.len() == 1));
961 if let Some(polys) = polys {
962 assert_eq!(polys[0].label, 1);
963 assert!(polys[0].exterior.len() >= 4);
964 }
965 }
966
967 #[test]
968 fn test_pixel_edge_two_regions() {
969 let labels = vec![1u32, 2];
971 let result = extract_pixel_edge_boundaries(&labels, 2, 1);
972 assert!(result.is_ok());
973 let polys = result.ok();
974 assert!(polys.is_some());
975 let polys = polys.as_ref();
976 assert!(polys.is_some_and(|p| p.len() == 2));
977 }
978
979 #[test]
980 fn test_pixel_edge_with_hole() {
981 #[rustfmt::skip]
988 let labels = vec![
989 1, 1, 1, 1, 1,
990 1, 0, 0, 0, 1,
991 1, 0, 0, 0, 1,
992 1, 0, 0, 0, 1,
993 1, 1, 1, 1, 1,
994 ];
995 let result = extract_pixel_edge_boundaries(&labels, 5, 5);
996 assert!(result.is_ok());
997 let polys = result.ok();
998 assert!(polys.is_some());
999 if let Some(polys) = polys {
1000 assert_eq!(polys.len(), 1);
1001 assert_eq!(polys[0].label, 1);
1002 assert!(!polys[0].exterior.is_empty());
1004 assert_eq!(polys[0].holes.len(), 1);
1005 }
1006 }
1007
1008 #[test]
1009 fn test_pixel_edge_empty_grid() {
1010 let labels = vec![0u32; 9];
1011 let result = extract_pixel_edge_boundaries(&labels, 3, 3);
1012 assert!(result.is_ok());
1013 let polys = result.ok();
1014 assert!(polys.is_some_and(|p| p.is_empty()));
1015 }
1016
1017 #[test]
1018 fn test_pixel_edge_size_mismatch() {
1019 let labels = vec![1u32, 2, 3]; let result = extract_pixel_edge_boundaries(&labels, 2, 2); assert!(result.is_err());
1022 }
1023
1024 #[test]
1025 fn test_transform_coords() {
1026 let gt = oxigdal_core::types::GeoTransform::north_up(100.0, 200.0, 10.0, -10.0);
1027 let pixel_coords = vec![(0.0, 0.0), (1.0, 1.0)];
1028 let world_coords = transform_coords(&pixel_coords, >);
1029 assert_eq!(world_coords.len(), 2);
1030 assert!((world_coords[0].x - 100.0).abs() < 1e-10);
1031 assert!((world_coords[0].y - 200.0).abs() < 1e-10);
1032 assert!((world_coords[1].x - 110.0).abs() < 1e-10);
1033 assert!((world_coords[1].y - 190.0).abs() < 1e-10);
1034 }
1035
1036 #[test]
1037 fn test_pixel_coords_to_coordinates() {
1038 let coords = vec![(1.5, 2.5), (3.0, 4.0)];
1039 let result = pixel_coords_to_coordinates(&coords);
1040 assert_eq!(result.len(), 2);
1041 assert!((result[0].x - 1.5).abs() < 1e-10);
1042 assert!((result[0].y - 2.5).abs() < 1e-10);
1043 }
1044
1045 #[test]
1046 fn test_connectivity_default() {
1047 assert_eq!(Connectivity::default(), Connectivity::Eight);
1048 }
1049
1050 #[test]
1051 fn test_trace_boundaries_single_region() {
1052 let labels = vec![1u32; 9];
1054 let result = trace_boundaries(&labels, 3, 3, Connectivity::Eight);
1055 assert!(result.is_ok());
1056 let polys = result.ok();
1057 assert!(polys.is_some());
1058 if let Some(polys) = polys {
1059 assert!(!polys.is_empty());
1060 assert_eq!(polys[0].label, 1);
1061 }
1062 }
1063
1064 #[test]
1065 fn test_trace_boundaries_two_separate_regions() {
1066 let labels = vec![1u32, 1, 0, 2, 2];
1068 let result = trace_boundaries(&labels, 5, 1, Connectivity::Four);
1069 assert!(result.is_ok());
1070 let polys = result.ok();
1071 assert!(polys.is_some());
1072 if let Some(polys) = polys {
1073 assert_eq!(polys.len(), 2);
1074 }
1075 }
1076
1077 #[test]
1078 fn test_trace_boundaries_size_mismatch() {
1079 let labels = vec![1u32; 5];
1080 let result = trace_boundaries(&labels, 3, 3, Connectivity::Eight);
1081 assert!(result.is_err());
1082 }
1083
1084 #[test]
1085 fn test_pixel_ring_is_exterior() {
1086 let ring = PixelRing {
1087 label: 1,
1088 coords: vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)],
1089 signed_area: 1.0,
1090 };
1091 assert!(ring.is_exterior());
1092
1093 let hole = PixelRing {
1094 label: 1,
1095 coords: vec![(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)],
1096 signed_area: -1.0,
1097 };
1098 assert!(!hole.is_exterior());
1099 }
1100}