Skip to main content

oxigdal_algorithms/raster/polygonize/
mod.rs

1//! Raster polygonization (raster-to-polygon conversion)
2//!
3//! Converts raster data into vector polygon geometries via:
4//!
5//! 1. **Connected component labeling (CCL):** Two-pass union-find algorithm with
6//!    path compression and union by rank. Configurable 4- or 8-connectivity.
7//!    NoData pixels receive label 0 (background).
8//!
9//! 2. **Boundary extraction:** Pixel-edge contour extraction produces closed
10//!    rectilinear rings along pixel boundaries (like GDAL's `GDALPolygonize`).
11//!    Alternatively, Moore-Neighbor tracing produces pixel-center boundaries.
12//!
13//! 3. **Ring classification:** Signed area via shoelace formula determines ring
14//!    orientation. CCW rings are exteriors; CW rings are holes. Holes are
15//!    assigned to the smallest containing exterior via point-in-polygon tests.
16//!
17//! 4. **GeoTransform:** Optional pixel-to-world coordinate transformation.
18//!
19//! # Algorithm Overview
20//!
21//! The connected component labeling (CCL) uses a classical two-pass algorithm:
22//!
23//! - **Pass 1 (forward scan):** Assign provisional labels. For each pixel, check
24//!   already-labeled neighbors (above / left for 4-conn; above-left, above,
25//!   above-right, left for 8-conn). If no labeled neighbor exists, assign a new
26//!   label. If one or more labeled neighbors exist, take the minimum label and
27//!   record equivalences in the union-find.
28//!
29//! - **Pass 2 (relabel):** Replace each provisional label with its canonical
30//!   representative from the union-find.
31//!
32//! # Examples
33//!
34//! ```
35//! use oxigdal_algorithms::raster::polygonize::{PolygonizeOptions, polygonize};
36//!
37//! // 4x4 grid with two distinct regions
38//! let grid: Vec<f64> = vec![
39//!     1.0, 1.0, 2.0, 2.0,
40//!     1.0, 1.0, 2.0, 2.0,
41//!     3.0, 3.0, 2.0, 2.0,
42//!     3.0, 3.0, 3.0, 3.0,
43//! ];
44//!
45//! let opts = PolygonizeOptions::default();
46//! let result = polygonize(&grid, 4, 4, &opts);
47//! assert!(result.is_ok());
48//! let result = result.as_ref();
49//! assert!(result.is_ok_and(|r| r.polygons.len() == 3));
50//! ```
51
52mod boundary;
53mod union_find;
54
55use std::collections::HashMap;
56
57use oxigdal_core::types::GeoTransform;
58use oxigdal_core::vector::{Coordinate, LineString, MultiPolygon, Polygon};
59
60use crate::error::{AlgorithmError, Result};
61
62pub use boundary::Connectivity;
63use boundary::{
64    ClassifiedPolygon, extract_pixel_edge_boundaries, pixel_coords_to_coordinates,
65    trace_boundaries, transform_coords,
66};
67use union_find::UnionFind;
68
69// ---------------------------------------------------------------------------
70// Public types
71// ---------------------------------------------------------------------------
72
73/// Options controlling raster polygonization behavior.
74#[derive(Debug, Clone)]
75pub struct PolygonizeOptions {
76    /// Pixel connectivity for connected component labeling.
77    /// Default: `Connectivity::Eight`.
78    pub connectivity: Connectivity,
79
80    /// Value treated as nodata (background). Pixels matching this value
81    /// (within `nodata_tolerance`) receive label 0 and are excluded from
82    /// the output polygons. Default: `None` (all values are data).
83    pub nodata: Option<f64>,
84
85    /// Tolerance for comparing pixel values against nodata.
86    /// Default: `1e-10`.
87    pub nodata_tolerance: f64,
88
89    /// Optional GeoTransform for converting pixel coordinates to world
90    /// coordinates. If `None`, output coordinates are in pixel space.
91    pub transform: Option<GeoTransform>,
92
93    /// If `> 0.0`, apply Douglas-Peucker simplification to output polygons
94    /// with this tolerance. Default: `0.0` (disabled).
95    pub simplify_tolerance: f64,
96
97    /// Minimum polygon area (in output coordinate units). Polygons smaller
98    /// than this are dropped. Default: `0.0` (keep all).
99    pub min_area: f64,
100
101    /// Boundary extraction method. Default: `BoundaryMethod::PixelEdge`.
102    pub boundary_method: BoundaryMethod,
103}
104
105/// Method for extracting polygon boundaries from labeled regions.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum BoundaryMethod {
108    /// Extract boundaries along pixel edges (rectilinear). Produces exact
109    /// boundaries aligned to pixel edges, matching GDAL's approach.
110    PixelEdge,
111    /// Moore-Neighbor contour tracing through pixel centers. Produces
112    /// smoother boundaries but may not be pixel-exact.
113    MooreNeighbor,
114}
115
116impl Default for BoundaryMethod {
117    fn default() -> Self {
118        Self::PixelEdge
119    }
120}
121
122impl Default for PolygonizeOptions {
123    fn default() -> Self {
124        Self {
125            connectivity: Connectivity::default(),
126            nodata: None,
127            nodata_tolerance: 1e-10,
128            transform: None,
129            simplify_tolerance: 0.0,
130            min_area: 0.0,
131            boundary_method: BoundaryMethod::default(),
132        }
133    }
134}
135
136/// A single polygonized feature: a value and its polygon geometry.
137#[derive(Debug, Clone)]
138pub struct PolygonFeature {
139    /// The raster pixel value for this polygon.
140    pub value: f64,
141    /// The polygon geometry (exterior ring + interior holes).
142    pub polygon: Polygon,
143}
144
145/// Result of raster polygonization.
146#[derive(Debug, Clone)]
147pub struct PolygonizeResult {
148    /// Extracted polygon features, one per connected component.
149    pub polygons: Vec<PolygonFeature>,
150    /// Number of connected components found (excluding nodata).
151    pub num_components: usize,
152    /// Grid width in pixels.
153    pub width: usize,
154    /// Grid height in pixels.
155    pub height: usize,
156}
157
158impl PolygonizeResult {
159    /// Get all polygons for a specific pixel value.
160    pub fn polygons_for_value(&self, value: f64, tolerance: f64) -> Vec<&PolygonFeature> {
161        self.polygons
162            .iter()
163            .filter(|f| (f.value - value).abs() <= tolerance)
164            .collect()
165    }
166
167    /// Collect all polygons into a `MultiPolygon`.
168    pub fn to_multipolygon(&self) -> MultiPolygon {
169        let polys: Vec<Polygon> = self.polygons.iter().map(|f| f.polygon.clone()).collect();
170        MultiPolygon::new(polys)
171    }
172
173    /// Get unique pixel values present in the result.
174    pub fn unique_values(&self) -> Vec<f64> {
175        let mut values: Vec<f64> = Vec::new();
176        for feat in &self.polygons {
177            if !values
178                .iter()
179                .any(|v| (*v - feat.value).abs() < f64::EPSILON)
180            {
181                values.push(feat.value);
182            }
183        }
184        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
185        values
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Connected Component Labeling
191// ---------------------------------------------------------------------------
192
193/// Two-pass connected component labeling on a quantized grid.
194///
195/// Pixel values are quantized to integer keys for comparison (grouping pixels
196/// with the same value). Returns a label grid where each connected region of
197/// identical values has a unique label. Label 0 = nodata/background.
198fn connected_component_label(
199    grid: &[f64],
200    width: usize,
201    height: usize,
202    connectivity: Connectivity,
203    nodata: Option<f64>,
204    nodata_tolerance: f64,
205) -> Result<(Vec<u32>, u32, HashMap<u32, f64>)> {
206    let n = width * height;
207    if grid.len() != n {
208        return Err(AlgorithmError::InvalidInput(
209            "grid size does not match width*height".to_string(),
210        ));
211    }
212
213    let mut labels = vec![0u32; n];
214    let mut next_label = 1u32;
215    let mut uf = UnionFind::with_capacity(256); // will grow as needed
216    let mut label_values: HashMap<u32, f64> = HashMap::new();
217
218    // Pass 1: forward scan, assign provisional labels
219    for y in 0..height {
220        for x in 0..width {
221            let idx = y * width + x;
222            let val = grid[idx];
223
224            // Skip nodata pixels
225            if is_nodata_value(val, nodata, nodata_tolerance) {
226                continue;
227            }
228
229            // Collect labels of same-value neighbors that have already been labeled
230            let neighbor_labels = get_neighbor_labels(
231                grid,
232                &labels,
233                width,
234                height,
235                x,
236                y,
237                val,
238                connectivity,
239                nodata,
240                nodata_tolerance,
241            );
242
243            if neighbor_labels.is_empty() {
244                // New component
245                uf.ensure_label(next_label);
246                labels[idx] = next_label;
247                label_values.insert(next_label, val);
248                next_label = next_label.saturating_add(1);
249            } else {
250                // Take the minimum label
251                let mut min_label = neighbor_labels[0];
252                for &nl in &neighbor_labels[1..] {
253                    if nl < min_label {
254                        min_label = nl;
255                    }
256                }
257                labels[idx] = min_label;
258
259                // Union all neighbor labels together
260                for &nl in &neighbor_labels {
261                    if nl != min_label {
262                        uf.ensure_label(nl.max(min_label));
263                        uf.union(min_label, nl);
264                    }
265                }
266            }
267        }
268    }
269
270    // Pass 2: relabel using canonical representatives
271    let mut canonical_map: HashMap<u32, u32> = HashMap::new();
272    let mut remap_next = 1u32;
273
274    for label in labels.iter_mut() {
275        if *label == 0 {
276            continue;
277        }
278        let root = uf.find(*label);
279        let canonical = *canonical_map.entry(root).or_insert_with(|| {
280            let c = remap_next;
281            remap_next = remap_next.saturating_add(1);
282            c
283        });
284        *label = canonical;
285    }
286
287    // Remap label_values to use canonical labels
288    let mut new_label_values: HashMap<u32, f64> = HashMap::new();
289    for (&orig_label, &val) in &label_values {
290        let root = uf.find(orig_label);
291        if let Some(&canonical) = canonical_map.get(&root) {
292            new_label_values.entry(canonical).or_insert(val);
293        }
294    }
295
296    let num_components = remap_next.saturating_sub(1);
297    Ok((labels, num_components, new_label_values))
298}
299
300/// Check whether a value is nodata.
301#[inline]
302fn is_nodata_value(value: f64, nodata: Option<f64>, tolerance: f64) -> bool {
303    if value.is_nan() {
304        return true;
305    }
306    if let Some(nd) = nodata {
307        (value - nd).abs() <= tolerance
308    } else {
309        false
310    }
311}
312
313/// Check whether two pixel values are "same" for CCL purposes.
314#[inline]
315fn same_value(a: f64, b: f64) -> bool {
316    // Use exact equality for integer-like values, tolerance for floating point
317    (a - b).abs() < f64::EPSILON * 100.0 || (a == b)
318}
319
320/// Get already-labeled neighbors with the same pixel value.
321fn get_neighbor_labels(
322    grid: &[f64],
323    labels: &[u32],
324    width: usize,
325    height: usize,
326    x: usize,
327    y: usize,
328    value: f64,
329    connectivity: Connectivity,
330    nodata: Option<f64>,
331    nodata_tolerance: f64,
332) -> Vec<u32> {
333    let mut result = Vec::with_capacity(4);
334
335    // For forward-scan CCL, we only look at already-visited neighbors:
336    // - 4-conn: above (N) and left (W)
337    // - 8-conn: above-left (NW), above (N), above-right (NE), left (W)
338
339    // Above (N): y-1, x
340    if y > 0 {
341        let nidx = (y - 1) * width + x;
342        let nl = labels[nidx];
343        let nv = grid[nidx];
344        if nl != 0 && !is_nodata_value(nv, nodata, nodata_tolerance) && same_value(value, nv) {
345            result.push(nl);
346        }
347    }
348
349    // Left (W): y, x-1
350    if x > 0 {
351        let nidx = y * width + (x - 1);
352        let nl = labels[nidx];
353        let nv = grid[nidx];
354        if nl != 0 && !is_nodata_value(nv, nodata, nodata_tolerance) && same_value(value, nv) {
355            if !result.contains(&nl) {
356                result.push(nl);
357            }
358        }
359    }
360
361    if connectivity == Connectivity::Eight {
362        // Above-left (NW): y-1, x-1
363        if y > 0 && x > 0 {
364            let nidx = (y - 1) * width + (x - 1);
365            let nl = labels[nidx];
366            let nv = grid[nidx];
367            if nl != 0
368                && !is_nodata_value(nv, nodata, nodata_tolerance)
369                && same_value(value, nv)
370                && !result.contains(&nl)
371            {
372                result.push(nl);
373            }
374        }
375
376        // Above-right (NE): y-1, x+1
377        if y > 0 && x + 1 < width {
378            let nidx = (y - 1) * width + (x + 1);
379            let nl = labels[nidx];
380            let nv = grid[nidx];
381            if nl != 0
382                && !is_nodata_value(nv, nodata, nodata_tolerance)
383                && same_value(value, nv)
384                && !result.contains(&nl)
385            {
386                result.push(nl);
387            }
388        }
389    }
390
391    result
392}
393
394// ---------------------------------------------------------------------------
395// Simplification (Douglas-Peucker)
396// ---------------------------------------------------------------------------
397
398/// Simplify a coordinate ring using Douglas-Peucker.
399fn simplify_ring(coords: &[(f64, f64)], tolerance: f64) -> Vec<(f64, f64)> {
400    if coords.len() <= 4 || tolerance <= 0.0 {
401        return coords.to_vec();
402    }
403    let tol_sq = tolerance * tolerance;
404    let mut keep = vec![false; coords.len()];
405    keep[0] = true;
406    keep[coords.len() - 1] = true;
407    dp_simplify(coords, 0, coords.len() - 1, tol_sq, &mut keep);
408
409    let result: Vec<(f64, f64)> = coords
410        .iter()
411        .enumerate()
412        .filter(|(i, _)| keep[*i])
413        .map(|(_, c)| *c)
414        .collect();
415
416    // Ensure ring closure
417    if result.len() >= 3 {
418        result
419    } else {
420        coords.to_vec() // Don't simplify below minimum ring size
421    }
422}
423
424/// Douglas-Peucker recursive helper.
425fn dp_simplify(coords: &[(f64, f64)], start: usize, end: usize, tol_sq: f64, keep: &mut [bool]) {
426    if end <= start + 1 {
427        return;
428    }
429    let (sx, sy) = coords[start];
430    let (ex, ey) = coords[end];
431    let dx = ex - sx;
432    let dy = ey - sy;
433    let len_sq = dx.mul_add(dx, dy * dy);
434
435    let mut max_dist_sq = 0.0_f64;
436    let mut max_idx = start;
437
438    for i in (start + 1)..end {
439        let (px, py) = coords[i];
440        let dist_sq = if len_sq < f64::EPSILON {
441            let dpx = px - sx;
442            let dpy = py - sy;
443            dpx.mul_add(dpx, dpy * dpy)
444        } else {
445            let t = ((px - sx).mul_add(dx, (py - sy) * dy) / len_sq).clamp(0.0, 1.0);
446            let proj_x = t.mul_add(dx, sx);
447            let proj_y = t.mul_add(dy, sy);
448            let dpx = px - proj_x;
449            let dpy = py - proj_y;
450            dpx.mul_add(dpx, dpy * dpy)
451        };
452
453        if dist_sq > max_dist_sq {
454            max_dist_sq = dist_sq;
455            max_idx = i;
456        }
457    }
458
459    if max_dist_sq > tol_sq {
460        keep[max_idx] = true;
461        dp_simplify(coords, start, max_idx, tol_sq, keep);
462        dp_simplify(coords, max_idx, end, tol_sq, keep);
463    }
464}
465
466// ---------------------------------------------------------------------------
467// Public API
468// ---------------------------------------------------------------------------
469
470/// Convert a raster grid to vector polygon geometries.
471///
472/// This is the main entry point for raster polygonization, equivalent to
473/// GDAL's `GDALPolygonize()`. Each connected region of identical pixel values
474/// becomes a separate polygon feature.
475///
476/// # Arguments
477///
478/// * `grid` - Flat row-major array of pixel values.
479/// * `width` - Grid width in pixels.
480/// * `height` - Grid height in pixels.
481/// * `options` - Polygonization options (connectivity, nodata, transform, etc.).
482///
483/// # Returns
484///
485/// A `PolygonizeResult` containing polygon features with their associated pixel
486/// values, or an error if the input is invalid.
487///
488/// # Errors
489///
490/// Returns an error if:
491/// - `grid.len() != width * height`
492/// - `width` or `height` is zero
493/// - Internal geometry construction fails
494///
495/// # Examples
496///
497/// ```
498/// use oxigdal_algorithms::raster::polygonize::{PolygonizeOptions, polygonize};
499///
500/// let grid = vec![1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 3.0];
501/// let result = polygonize(&grid, 4, 3, &PolygonizeOptions::default());
502/// assert!(result.is_ok());
503/// ```
504pub fn polygonize(
505    grid: &[f64],
506    width: usize,
507    height: usize,
508    options: &PolygonizeOptions,
509) -> Result<PolygonizeResult> {
510    // Validate inputs
511    if width == 0 || height == 0 {
512        return Err(AlgorithmError::InvalidDimensions {
513            message: "grid dimensions must be positive",
514            actual: width.min(height),
515            expected: 1,
516        });
517    }
518    if grid.len() != width * height {
519        return Err(AlgorithmError::InvalidInput(format!(
520            "grid size ({}) does not match width*height ({}*{}={})",
521            grid.len(),
522            width,
523            height,
524            width * height
525        )));
526    }
527
528    // Phase 1: Connected component labeling
529    let (labels, num_components, label_values) = connected_component_label(
530        grid,
531        width,
532        height,
533        options.connectivity,
534        options.nodata,
535        options.nodata_tolerance,
536    )?;
537
538    if num_components == 0 {
539        return Ok(PolygonizeResult {
540            polygons: Vec::new(),
541            num_components: 0,
542            width,
543            height,
544        });
545    }
546
547    // Phase 2: Boundary extraction
548    let classified_polys = match options.boundary_method {
549        BoundaryMethod::PixelEdge => extract_pixel_edge_boundaries(&labels, width, height)?,
550        BoundaryMethod::MooreNeighbor => {
551            trace_boundaries(&labels, width, height, options.connectivity)?
552        }
553    };
554
555    // Phase 3: Convert to output Polygon features
556    let mut features = Vec::with_capacity(classified_polys.len());
557
558    for cpoly in classified_polys {
559        // Look up the pixel value for this label
560        let value = label_values.get(&cpoly.label).copied().unwrap_or(0.0);
561
562        // Apply simplification if requested
563        let exterior_coords = if options.simplify_tolerance > 0.0 {
564            simplify_ring(&cpoly.exterior, options.simplify_tolerance)
565        } else {
566            cpoly.exterior.clone()
567        };
568
569        let hole_coords: Vec<Vec<(f64, f64)>> = if options.simplify_tolerance > 0.0 {
570            cpoly
571                .holes
572                .iter()
573                .map(|h| simplify_ring(h, options.simplify_tolerance))
574                .collect()
575        } else {
576            cpoly.holes.clone()
577        };
578
579        // Compute area for min_area filtering
580        let area = boundary::compute_signed_area(&exterior_coords).abs();
581        if options.min_area > 0.0 && area < options.min_area {
582            continue;
583        }
584
585        // Convert to world coordinates if transform is provided
586        let ext_world_coords = match &options.transform {
587            Some(gt) => transform_coords(&exterior_coords, gt),
588            None => pixel_coords_to_coordinates(&exterior_coords),
589        };
590
591        let hole_world_coords: Vec<Vec<Coordinate>> = hole_coords
592            .iter()
593            .map(|h| match &options.transform {
594                Some(gt) => transform_coords(h, gt),
595                None => pixel_coords_to_coordinates(h),
596            })
597            .collect();
598
599        // Build the Polygon geometry
600        let polygon = build_polygon(ext_world_coords, hole_world_coords)?;
601
602        if let Some(poly) = polygon {
603            features.push(PolygonFeature {
604                value,
605                polygon: poly,
606            });
607        }
608    }
609
610    Ok(PolygonizeResult {
611        polygons: features,
612        num_components: num_components as usize,
613        width,
614        height,
615    })
616}
617
618/// Polygonize with a `RasterBuffer` from `oxigdal-core`.
619///
620/// Convenience wrapper that reads pixel values from a `RasterBuffer` and
621/// calls [`polygonize`].
622///
623/// # Errors
624///
625/// Returns an error if pixel access fails or polygonization fails.
626pub fn polygonize_raster(
627    raster: &oxigdal_core::buffer::RasterBuffer,
628    options: &PolygonizeOptions,
629) -> Result<PolygonizeResult> {
630    let width = raster.width() as usize;
631    let height = raster.height() as usize;
632
633    let mut grid = vec![0.0_f64; width * height];
634    for y in 0..height {
635        for x in 0..width {
636            grid[y * width + x] = raster
637                .get_pixel(x as u64, y as u64)
638                .map_err(AlgorithmError::Core)?;
639        }
640    }
641
642    polygonize(&grid, width, height, options)
643}
644
645/// Build a `Polygon` from coordinate vectors, handling edge cases.
646fn build_polygon(
647    exterior: Vec<Coordinate>,
648    holes: Vec<Vec<Coordinate>>,
649) -> Result<Option<Polygon>> {
650    if exterior.len() < 4 {
651        return Ok(None);
652    }
653
654    // Ensure the exterior ring is closed
655    let mut ext = exterior;
656    let first = ext[0];
657    let last = ext[ext.len() - 1];
658    if (first.x - last.x).abs() > f64::EPSILON || (first.y - last.y).abs() > f64::EPSILON {
659        ext.push(first);
660    }
661
662    if ext.len() < 4 {
663        return Ok(None);
664    }
665
666    let ext_ring = LineString::new(ext).map_err(|e| AlgorithmError::Core(e))?;
667
668    // Build interior rings
669    let mut interior_rings = Vec::new();
670    for hole in holes {
671        if hole.len() < 4 {
672            continue;
673        }
674        let mut h = hole;
675        let hfirst = h[0];
676        let hlast = h[h.len() - 1];
677        if (hfirst.x - hlast.x).abs() > f64::EPSILON || (hfirst.y - hlast.y).abs() > f64::EPSILON {
678            h.push(hfirst);
679        }
680        if h.len() >= 4 {
681            if let Ok(ring) = LineString::new(h) {
682                interior_rings.push(ring);
683            }
684        }
685    }
686
687    match Polygon::new(ext_ring, interior_rings) {
688        Ok(poly) => Ok(Some(poly)),
689        Err(_) => Ok(None), // Skip invalid polygons rather than failing
690    }
691}
692
693// ---------------------------------------------------------------------------
694// Tests
695// ---------------------------------------------------------------------------
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use oxigdal_core::buffer::RasterBuffer;
701    use oxigdal_core::types::RasterDataType;
702
703    /// Helper: create a RasterBuffer from a flat f64 array.
704    fn make_raster(width: usize, height: usize, values: &[f64]) -> RasterBuffer {
705        let mut buf = RasterBuffer::zeros(width as u64, height as u64, RasterDataType::Float64);
706        for row in 0..height {
707            for col in 0..width {
708                let _ = buf.set_pixel(col as u64, row as u64, values[row * width + col]);
709            }
710        }
711        buf
712    }
713
714    // --- Basic CCL tests ---
715
716    #[test]
717    fn test_ccl_single_value() {
718        let grid = vec![1.0; 9];
719        let (labels, num, values) =
720            connected_component_label(&grid, 3, 3, Connectivity::Eight, None, 1e-10)
721                .expect("CCL should succeed");
722        assert_eq!(num, 1);
723        assert!(values.values().any(|&v| (v - 1.0).abs() < f64::EPSILON));
724        // All labels should be the same non-zero value
725        let first_label = labels[0];
726        assert!(first_label > 0);
727        for &l in &labels {
728            assert_eq!(l, first_label);
729        }
730    }
731
732    #[test]
733    fn test_ccl_two_regions() {
734        #[rustfmt::skip]
735        let grid = vec![
736            1.0, 1.0, 2.0,
737            1.0, 1.0, 2.0,
738            2.0, 2.0, 2.0,
739        ];
740        let (labels, num, _) =
741            connected_component_label(&grid, 3, 3, Connectivity::Four, None, 1e-10)
742                .expect("CCL should succeed");
743        assert_eq!(num, 2);
744        // Check that the 1-region and 2-region have different labels
745        assert_ne!(labels[0], labels[2]);
746    }
747
748    #[test]
749    fn test_ccl_nodata() {
750        let grid = vec![1.0, f64::NAN, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
751        let (labels, _, _) =
752            connected_component_label(&grid, 3, 3, Connectivity::Four, None, 1e-10)
753                .expect("CCL should succeed");
754        // NaN pixel should have label 0
755        assert_eq!(labels[1], 0);
756    }
757
758    #[test]
759    fn test_ccl_custom_nodata() {
760        let grid = vec![1.0, -9999.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
761        let (labels, _, _) =
762            connected_component_label(&grid, 3, 3, Connectivity::Four, Some(-9999.0), 1e-10)
763                .expect("CCL should succeed");
764        assert_eq!(labels[1], 0);
765    }
766
767    #[test]
768    fn test_ccl_four_vs_eight_connectivity() {
769        // Diagonal region: with 8-conn, diagonals connect; with 4-conn they don't
770        #[rustfmt::skip]
771        let grid = vec![
772            1.0, 0.0,
773            0.0, 1.0,
774        ];
775
776        let (_, num_4, _) =
777            connected_component_label(&grid, 2, 2, Connectivity::Four, Some(0.0), 1e-10)
778                .expect("CCL should succeed");
779        let (_, num_8, _) =
780            connected_component_label(&grid, 2, 2, Connectivity::Eight, Some(0.0), 1e-10)
781                .expect("CCL should succeed");
782
783        // 4-conn: two separate components; 8-conn: one component
784        assert_eq!(num_4, 2);
785        assert_eq!(num_8, 1);
786    }
787
788    #[test]
789    fn test_ccl_empty_grid() {
790        let grid = vec![f64::NAN; 4];
791        let (labels, num, _) =
792            connected_component_label(&grid, 2, 2, Connectivity::Eight, None, 1e-10)
793                .expect("CCL should succeed");
794        assert_eq!(num, 0);
795        for &l in &labels {
796            assert_eq!(l, 0);
797        }
798    }
799
800    #[test]
801    fn test_ccl_size_mismatch() {
802        let grid = vec![1.0; 5];
803        let result = connected_component_label(&grid, 2, 2, Connectivity::Eight, None, 1e-10);
804        assert!(result.is_err());
805    }
806
807    // --- Polygonize API tests ---
808
809    #[test]
810    fn test_polygonize_basic() {
811        #[rustfmt::skip]
812        let grid = vec![
813            1.0, 1.0, 2.0, 2.0,
814            1.0, 1.0, 2.0, 2.0,
815            3.0, 3.0, 2.0, 2.0,
816            3.0, 3.0, 3.0, 3.0,
817        ];
818        let opts = PolygonizeOptions::default();
819        let result = polygonize(&grid, 4, 4, &opts);
820        assert!(result.is_ok());
821        let result = result.expect("polygonize should succeed");
822        assert_eq!(result.num_components, 3);
823        assert_eq!(result.polygons.len(), 3);
824        assert_eq!(result.width, 4);
825        assert_eq!(result.height, 4);
826    }
827
828    #[test]
829    fn test_polygonize_single_value() {
830        let grid = vec![5.0; 16];
831        let opts = PolygonizeOptions::default();
832        let result = polygonize(&grid, 4, 4, &opts);
833        assert!(result.is_ok());
834        let result = result.expect("polygonize should succeed");
835        assert_eq!(result.num_components, 1);
836        assert_eq!(result.polygons.len(), 1);
837        assert!((result.polygons[0].value - 5.0).abs() < f64::EPSILON);
838    }
839
840    #[test]
841    fn test_polygonize_with_nodata() {
842        #[rustfmt::skip]
843        let grid = vec![
844            1.0, 1.0, 0.0,
845            1.0, 0.0, 2.0,
846            0.0, 2.0, 2.0,
847        ];
848        let mut opts = PolygonizeOptions::default();
849        opts.nodata = Some(0.0);
850        let result = polygonize(&grid, 3, 3, &opts);
851        assert!(result.is_ok());
852        let result = result.expect("polygonize should succeed");
853        // With nodata=0, should have region-1 and region-2
854        assert!(result.num_components >= 2);
855        // No polygon should have value 0.0
856        for feat in &result.polygons {
857            assert!(
858                (feat.value).abs() > f64::EPSILON,
859                "nodata polygons should be excluded"
860            );
861        }
862    }
863
864    #[test]
865    fn test_polygonize_with_transform() {
866        let grid = vec![1.0; 4];
867        let mut opts = PolygonizeOptions::default();
868        opts.transform = Some(GeoTransform::north_up(100.0, 200.0, 10.0, -10.0));
869        let result = polygonize(&grid, 2, 2, &opts);
870        assert!(result.is_ok());
871        let result = result.expect("polygonize should succeed");
872        assert_eq!(result.polygons.len(), 1);
873
874        // Verify coordinates are in world space
875        let poly = &result.polygons[0].polygon;
876        for coord in &poly.exterior.coords {
877            // World X should be >= 100 (origin)
878            assert!(
879                coord.x >= 99.0,
880                "world x should be near origin, got {}",
881                coord.x
882            );
883        }
884    }
885
886    #[test]
887    fn test_polygonize_min_area_filter() {
888        #[rustfmt::skip]
889        let grid = vec![
890            1.0, 1.0, 1.0, 1.0,
891            1.0, 1.0, 1.0, 1.0,
892            1.0, 1.0, 1.0, 2.0,
893            1.0, 1.0, 1.0, 1.0,
894        ];
895        let mut opts = PolygonizeOptions::default();
896        opts.min_area = 2.0; // Filter out the single-pixel "2" region
897        let result = polygonize(&grid, 4, 4, &opts);
898        assert!(result.is_ok());
899        let result = result.expect("polygonize should succeed");
900        // The small "2" region should be filtered out
901        for feat in &result.polygons {
902            assert!(
903                (feat.value - 2.0).abs() > f64::EPSILON,
904                "small region should be filtered"
905            );
906        }
907    }
908
909    #[test]
910    fn test_polygonize_simplify() {
911        let grid = vec![1.0; 100]; // 10x10
912        let mut opts = PolygonizeOptions::default();
913        opts.simplify_tolerance = 2.0;
914        let result = polygonize(&grid, 10, 10, &opts);
915        assert!(result.is_ok());
916        let result = result.expect("polygonize should succeed");
917        assert!(!result.polygons.is_empty());
918    }
919
920    #[test]
921    fn test_polygonize_zero_dimensions() {
922        let result = polygonize(&[], 0, 0, &PolygonizeOptions::default());
923        assert!(result.is_err());
924    }
925
926    #[test]
927    fn test_polygonize_size_mismatch() {
928        let result = polygonize(&[1.0; 5], 2, 2, &PolygonizeOptions::default());
929        assert!(result.is_err());
930    }
931
932    #[test]
933    fn test_polygonize_all_nodata() {
934        let grid = vec![f64::NAN; 9];
935        let result = polygonize(&grid, 3, 3, &PolygonizeOptions::default());
936        assert!(result.is_ok());
937        let result = result.expect("polygonize should succeed");
938        assert_eq!(result.num_components, 0);
939        assert!(result.polygons.is_empty());
940    }
941
942    #[test]
943    fn test_polygonize_moore_neighbor_method() {
944        let grid = vec![1.0; 9];
945        let mut opts = PolygonizeOptions::default();
946        opts.boundary_method = BoundaryMethod::MooreNeighbor;
947        let result = polygonize(&grid, 3, 3, &opts);
948        assert!(result.is_ok());
949        let result = result.expect("polygonize should succeed");
950        assert!(!result.polygons.is_empty());
951    }
952
953    #[test]
954    fn test_polygonize_four_connectivity() {
955        let grid = vec![1.0; 9];
956        let mut opts = PolygonizeOptions::default();
957        opts.connectivity = Connectivity::Four;
958        let result = polygonize(&grid, 3, 3, &opts);
959        assert!(result.is_ok());
960        let result = result.expect("polygonize should succeed");
961        assert_eq!(result.num_components, 1);
962    }
963
964    // --- PolygonizeResult methods ---
965
966    #[test]
967    fn test_polygonize_result_for_value() {
968        #[rustfmt::skip]
969        let grid = vec![
970            1.0, 1.0, 2.0,
971            1.0, 1.0, 2.0,
972            2.0, 2.0, 2.0,
973        ];
974        let result = polygonize(&grid, 3, 3, &PolygonizeOptions::default());
975        assert!(result.is_ok());
976        let result = result.expect("polygonize should succeed");
977
978        let ones = result.polygons_for_value(1.0, f64::EPSILON);
979        assert_eq!(ones.len(), 1);
980        let twos = result.polygons_for_value(2.0, f64::EPSILON);
981        assert_eq!(twos.len(), 1);
982        let threes = result.polygons_for_value(3.0, f64::EPSILON);
983        assert!(threes.is_empty());
984    }
985
986    #[test]
987    fn test_polygonize_result_to_multipolygon() {
988        let grid = vec![1.0; 4];
989        let result = polygonize(&grid, 2, 2, &PolygonizeOptions::default());
990        assert!(result.is_ok());
991        let result = result.expect("polygonize should succeed");
992        let mp = result.to_multipolygon();
993        assert_eq!(mp.polygons.len(), 1);
994    }
995
996    #[test]
997    fn test_polygonize_result_unique_values() {
998        #[rustfmt::skip]
999        let grid = vec![
1000            1.0, 2.0,
1001            3.0, 4.0,
1002        ];
1003        let result = polygonize(&grid, 2, 2, &PolygonizeOptions::default());
1004        assert!(result.is_ok());
1005        let result = result.expect("polygonize should succeed");
1006        let values = result.unique_values();
1007        assert_eq!(values.len(), 4);
1008    }
1009
1010    // --- RasterBuffer integration ---
1011
1012    #[test]
1013    fn test_polygonize_raster_buffer() {
1014        let raster = make_raster(3, 3, &[1.0; 9]);
1015        let result = polygonize_raster(&raster, &PolygonizeOptions::default());
1016        assert!(result.is_ok());
1017        let result = result.expect("polygonize_raster should succeed");
1018        assert_eq!(result.num_components, 1);
1019        assert_eq!(result.polygons.len(), 1);
1020    }
1021
1022    #[test]
1023    fn test_polygonize_raster_with_mixed_values() {
1024        #[rustfmt::skip]
1025        let values = vec![
1026            1.0, 1.0, 2.0, 2.0,
1027            1.0, 1.0, 2.0, 2.0,
1028            3.0, 3.0, 3.0, 3.0,
1029        ];
1030        let raster = make_raster(4, 3, &values);
1031        let result = polygonize_raster(&raster, &PolygonizeOptions::default());
1032        assert!(result.is_ok());
1033        let result = result.expect("polygonize_raster should succeed");
1034        assert_eq!(result.num_components, 3);
1035    }
1036
1037    // --- Checkerboard pattern ---
1038
1039    #[test]
1040    fn test_polygonize_checkerboard_4conn() {
1041        // Checkerboard: with 4-connectivity, each cell is isolated
1042        #[rustfmt::skip]
1043        let grid = vec![
1044            1.0, 2.0, 1.0,
1045            2.0, 1.0, 2.0,
1046            1.0, 2.0, 1.0,
1047        ];
1048        let mut opts = PolygonizeOptions::default();
1049        opts.connectivity = Connectivity::Four;
1050        let result = polygonize(&grid, 3, 3, &opts);
1051        assert!(result.is_ok());
1052        let result = result.expect("polygonize should succeed");
1053        // With 4-conn, each 1 and each 2 is separate
1054        // 5 ones + 4 twos = 9 components
1055        assert_eq!(result.num_components, 9);
1056    }
1057
1058    #[test]
1059    fn test_polygonize_checkerboard_8conn() {
1060        // Checkerboard: with 8-connectivity, diagonals connect
1061        #[rustfmt::skip]
1062        let grid = vec![
1063            1.0, 2.0, 1.0,
1064            2.0, 1.0, 2.0,
1065            1.0, 2.0, 1.0,
1066        ];
1067        let mut opts = PolygonizeOptions::default();
1068        opts.connectivity = Connectivity::Eight;
1069        let result = polygonize(&grid, 3, 3, &opts);
1070        assert!(result.is_ok());
1071        let result = result.expect("polygonize should succeed");
1072        // With 8-conn, all 1s connect (5 pixels) and all 2s connect (4 pixels)
1073        assert_eq!(result.num_components, 2);
1074    }
1075
1076    // --- Large grid stress test ---
1077
1078    #[test]
1079    fn test_polygonize_large_grid() {
1080        // 50x50 grid with a radial pattern (multiple concentric regions)
1081        let width = 50;
1082        let height = 50;
1083        let mut grid = vec![0.0_f64; width * height];
1084        let cx = 25.0_f64;
1085        let cy = 25.0_f64;
1086        for y in 0..height {
1087            for x in 0..width {
1088                let dx = x as f64 - cx;
1089                let dy = y as f64 - cy;
1090                let dist = (dx.mul_add(dx, dy * dy)).sqrt();
1091                // Quantize distance into bands
1092                grid[y * width + x] = (dist / 5.0).floor();
1093            }
1094        }
1095        let result = polygonize(&grid, width, height, &PolygonizeOptions::default());
1096        assert!(result.is_ok());
1097        let result = result.expect("polygonize should succeed");
1098        assert!(result.num_components > 1);
1099        assert!(!result.polygons.is_empty());
1100    }
1101
1102    #[test]
1103    fn test_polygonize_stress_500x500() {
1104        // 500x500 stress test: checkerboard-like quantized radial + stripes to
1105        // exercise CCL and boundary extraction at scale. The quantization here
1106        // produces a bounded number of unique values, keeping component count
1107        // tractable while still exercising a 250,000-pixel grid.
1108        let width = 500;
1109        let height = 500;
1110        let mut grid = vec![0.0_f64; width * height];
1111        for y in 0..height {
1112            for x in 0..width {
1113                // Coarse bands + column stripes produce several components
1114                let band = (y / 100) as f64;
1115                let stripe = (x / 125) as f64;
1116                grid[y * width + x] = band * 4.0 + stripe;
1117            }
1118        }
1119        let result = polygonize(&grid, width, height, &PolygonizeOptions::default());
1120        assert!(result.is_ok());
1121        let result = result.expect("polygonize should succeed");
1122        // Expect bounded number of components: at most 5 bands * 4 stripes + slack.
1123        assert!(result.num_components >= 5);
1124        assert!(result.num_components <= 64);
1125        assert!(!result.polygons.is_empty());
1126        assert_eq!(result.width, 500);
1127        assert_eq!(result.height, 500);
1128    }
1129
1130    // --- Donut / hole integration tests ---
1131
1132    #[test]
1133    fn test_polygonize_donut_with_hole() {
1134        // 5x5 grid: outer ring of 1s, inner 3x3 of nodata (0.0).
1135        #[rustfmt::skip]
1136        let grid = vec![
1137            1.0, 1.0, 1.0, 1.0, 1.0,
1138            1.0, 0.0, 0.0, 0.0, 1.0,
1139            1.0, 0.0, 0.0, 0.0, 1.0,
1140            1.0, 0.0, 0.0, 0.0, 1.0,
1141            1.0, 1.0, 1.0, 1.0, 1.0,
1142        ];
1143        let mut opts = PolygonizeOptions::default();
1144        opts.nodata = Some(0.0);
1145        let result = polygonize(&grid, 5, 5, &opts);
1146        assert!(result.is_ok());
1147        let result = result.expect("polygonize should succeed");
1148        // Exactly one polygon for the donut region; it must have one hole.
1149        assert_eq!(result.polygons.len(), 1);
1150        let feat = &result.polygons[0];
1151        assert!((feat.value - 1.0).abs() < f64::EPSILON);
1152        assert_eq!(
1153            feat.polygon.interiors.len(),
1154            1,
1155            "donut polygon should have exactly one interior hole"
1156        );
1157    }
1158
1159    #[test]
1160    fn test_polygonize_multiple_disjoint_components() {
1161        // 5x1 grid with two components of value=1 separated by nodata.
1162        #[rustfmt::skip]
1163        let grid = vec![
1164            1.0, 1.0, 0.0, 1.0, 1.0,
1165        ];
1166        let mut opts = PolygonizeOptions::default();
1167        opts.nodata = Some(0.0);
1168        let result = polygonize(&grid, 5, 1, &opts);
1169        assert!(result.is_ok());
1170        let result = result.expect("polygonize should succeed");
1171        // Two separate components of value 1
1172        assert_eq!(result.num_components, 2);
1173    }
1174
1175    // --- Options defaults ---
1176
1177    #[test]
1178    fn test_options_default() {
1179        let opts = PolygonizeOptions::default();
1180        assert_eq!(opts.connectivity, Connectivity::Eight);
1181        assert!(opts.nodata.is_none());
1182        assert!((opts.nodata_tolerance - 1e-10).abs() < f64::EPSILON);
1183        assert!(opts.transform.is_none());
1184        assert!((opts.simplify_tolerance).abs() < f64::EPSILON);
1185        assert!((opts.min_area).abs() < f64::EPSILON);
1186        assert_eq!(opts.boundary_method, BoundaryMethod::PixelEdge);
1187    }
1188
1189    #[test]
1190    fn test_boundary_method_default() {
1191        assert_eq!(BoundaryMethod::default(), BoundaryMethod::PixelEdge);
1192    }
1193
1194    // --- Edge cases ---
1195
1196    #[test]
1197    fn test_polygonize_1x1_grid() {
1198        let grid = vec![1.0];
1199        let result = polygonize(&grid, 1, 1, &PolygonizeOptions::default());
1200        assert!(result.is_ok());
1201        let result = result.expect("polygonize should succeed");
1202        assert_eq!(result.num_components, 1);
1203    }
1204
1205    #[test]
1206    fn test_polygonize_1xn_grid() {
1207        let grid = vec![1.0, 2.0, 1.0, 2.0];
1208        let result = polygonize(&grid, 4, 1, &PolygonizeOptions::default());
1209        assert!(result.is_ok());
1210    }
1211
1212    #[test]
1213    fn test_polygonize_nx1_grid() {
1214        let grid = vec![1.0, 2.0, 1.0, 2.0];
1215        let result = polygonize(&grid, 1, 4, &PolygonizeOptions::default());
1216        assert!(result.is_ok());
1217    }
1218
1219    #[test]
1220    fn test_polygonize_with_negative_values() {
1221        let grid = vec![-1.0, -2.0, -1.0, -2.0];
1222        let result = polygonize(&grid, 2, 2, &PolygonizeOptions::default());
1223        assert!(result.is_ok());
1224    }
1225
1226    #[test]
1227    fn test_same_value_function() {
1228        assert!(same_value(1.0, 1.0));
1229        assert!(same_value(0.0, 0.0));
1230        assert!(!same_value(1.0, 2.0));
1231    }
1232
1233    #[test]
1234    fn test_is_nodata_value() {
1235        assert!(is_nodata_value(f64::NAN, None, 1e-10));
1236        assert!(is_nodata_value(-9999.0, Some(-9999.0), 1e-10));
1237        assert!(!is_nodata_value(1.0, Some(-9999.0), 1e-10));
1238        assert!(!is_nodata_value(1.0, None, 1e-10));
1239    }
1240
1241    #[test]
1242    fn test_simplify_ring_passthrough() {
1243        let ring = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)];
1244        let result = simplify_ring(&ring, 0.0);
1245        assert_eq!(result.len(), ring.len());
1246    }
1247
1248    #[test]
1249    fn test_simplify_ring_with_tolerance() {
1250        let ring = vec![
1251            (0.0, 0.0),
1252            (0.5, 0.001),
1253            (1.0, 0.0),
1254            (1.0, 0.5),
1255            (1.0, 1.0),
1256            (0.5, 1.001),
1257            (0.0, 1.0),
1258            (0.0, 0.5),
1259            (0.0, 0.0),
1260        ];
1261        let result = simplify_ring(&ring, 0.1);
1262        assert!(result.len() <= ring.len());
1263    }
1264
1265    #[test]
1266    fn test_build_polygon_valid() {
1267        let ext = vec![
1268            Coordinate::new_2d(0.0, 0.0),
1269            Coordinate::new_2d(1.0, 0.0),
1270            Coordinate::new_2d(1.0, 1.0),
1271            Coordinate::new_2d(0.0, 1.0),
1272            Coordinate::new_2d(0.0, 0.0),
1273        ];
1274        let result = build_polygon(ext, vec![]);
1275        assert!(result.is_ok());
1276        assert!(result.expect("should succeed").is_some());
1277    }
1278
1279    #[test]
1280    fn test_build_polygon_too_few_coords() {
1281        let ext = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 0.0)];
1282        let result = build_polygon(ext, vec![]);
1283        assert!(result.is_ok());
1284        assert!(result.expect("should succeed").is_none());
1285    }
1286
1287    #[test]
1288    fn test_build_polygon_auto_close() {
1289        let ext = vec![
1290            Coordinate::new_2d(0.0, 0.0),
1291            Coordinate::new_2d(1.0, 0.0),
1292            Coordinate::new_2d(1.0, 1.0),
1293            Coordinate::new_2d(0.0, 1.0),
1294            // Not closed
1295        ];
1296        let result = build_polygon(ext, vec![]);
1297        assert!(result.is_ok());
1298        // Should auto-close and succeed
1299        let poly = result.expect("should succeed");
1300        assert!(poly.is_some());
1301    }
1302
1303    // --- Integration: pixel-edge boundaries produce valid polygons ---
1304
1305    #[test]
1306    fn test_pixel_edge_produces_valid_polygons() {
1307        #[rustfmt::skip]
1308        let grid = vec![
1309            1.0, 1.0, 2.0,
1310            1.0, 2.0, 2.0,
1311            2.0, 2.0, 2.0,
1312        ];
1313        let result = polygonize(&grid, 3, 3, &PolygonizeOptions::default());
1314        assert!(result.is_ok());
1315        let result = result.expect("polygonize should succeed");
1316        for feat in &result.polygons {
1317            // Each polygon should have at least 4 exterior coordinates (closed ring)
1318            assert!(
1319                feat.polygon.exterior.coords.len() >= 4,
1320                "polygon for value {} has too few coords: {}",
1321                feat.value,
1322                feat.polygon.exterior.coords.len()
1323            );
1324        }
1325    }
1326}