Skip to main content

wbraster/
raster.rs

1//! Core `Raster` type — the central data structure for all raster GIS data.
2
3use std::path::Path;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use rayon::prelude::*;
6use wbprojection::Crs;
7use wide::{f64x4, CmpNe};
8
9use crate::error::{Result, RasterError};
10use crate::formats::RasterFormat;
11use crate::crs_info::CrsInfo;
12
13// ─── Data type enum ───────────────────────────────────────────────────────────
14
15/// The underlying numeric data type of raster cells.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum DataType {
18    /// Unsigned 8-bit integer (byte).
19    U8,
20    /// Signed 8-bit integer.
21    I8,
22    /// Unsigned 16-bit integer.
23    U16,
24    /// Signed 16-bit integer.
25    I16,
26    /// Unsigned 32-bit integer.
27    U32,
28    /// Signed 32-bit integer.
29    I32,
30    /// Unsigned 64-bit integer.
31    U64,
32    /// Signed 64-bit integer.
33    I64,
34    /// 32-bit IEEE 754 floating point.
35    #[default]
36    F32,
37    /// 64-bit IEEE 754 floating point.
38    F64,
39}
40
41impl DataType {
42    /// Number of bytes per cell value.
43    pub fn size_bytes(self) -> usize {
44        match self {
45            DataType::U8 | DataType::I8 => 1,
46            DataType::U16 | DataType::I16 => 2,
47            DataType::U32 | DataType::I32 | DataType::F32 => 4,
48            DataType::U64 | DataType::I64 | DataType::F64 => 8,
49        }
50    }
51
52    /// Human-readable name used in header files.
53    pub fn as_str(self) -> &'static str {
54        match self {
55            DataType::U8 => "uint8",
56            DataType::I8 => "int8",
57            DataType::U16 => "uint16",
58            DataType::I16 => "int16",
59            DataType::U32 => "uint32",
60            DataType::I32 => "int32",
61            DataType::U64 => "uint64",
62            DataType::I64 => "int64",
63            DataType::F32 => "float32",
64            DataType::F64 => "float64",
65        }
66    }
67
68    /// Parse from a format string (case-insensitive).
69    #[allow(clippy::should_implement_trait)]
70    pub fn from_str(s: &str) -> Option<Self> {
71        match s.trim().to_ascii_lowercase().as_str() {
72            "uint8" | "u8" | "byte" => Some(DataType::U8),
73            "int8" | "i8" => Some(DataType::I8),
74            "uint16" | "u16" => Some(DataType::U16),
75            "int16" | "i16" | "integer" | "short" => Some(DataType::I16),
76            "uint32" | "u32" => Some(DataType::U32),
77            "int32" | "i32" | "long" => Some(DataType::I32),
78            "uint64" | "u64" => Some(DataType::U64),
79            "int64" | "i64" | "longlong" => Some(DataType::I64),
80            "float32" | "f32" | "float" | "real" => Some(DataType::F32),
81            "float64" | "f64" | "double" => Some(DataType::F64),
82            _ => None,
83        }
84    }
85}
86
87impl std::fmt::Display for DataType {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.write_str(self.as_str())
90    }
91}
92
93/// Typed in-memory pixel buffer.
94#[derive(Debug, Clone)]
95pub enum RasterData {
96    /// Unsigned 8-bit storage.
97    U8(Vec<u8>),
98    /// Signed 8-bit storage.
99    I8(Vec<i8>),
100    /// Unsigned 16-bit storage.
101    U16(Vec<u16>),
102    /// Signed 16-bit storage.
103    I16(Vec<i16>),
104    /// Unsigned 32-bit storage.
105    U32(Vec<u32>),
106    /// Signed 32-bit storage.
107    I32(Vec<i32>),
108    /// Unsigned 64-bit storage.
109    U64(Vec<u64>),
110    /// Signed 64-bit storage.
111    I64(Vec<i64>),
112    /// 32-bit float storage.
113    F32(Vec<f32>),
114    /// 64-bit float storage.
115    F64(Vec<f64>),
116}
117
118/// A mutable typed view of one raster row.
119pub enum RasterRowMut<'a> {
120    /// Unsigned 8-bit row.
121    U8(&'a mut [u8]),
122    /// Signed 8-bit row.
123    I8(&'a mut [i8]),
124    /// Unsigned 16-bit row.
125    U16(&'a mut [u16]),
126    /// Signed 16-bit row.
127    I16(&'a mut [i16]),
128    /// Unsigned 32-bit row.
129    U32(&'a mut [u32]),
130    /// Signed 32-bit row.
131    I32(&'a mut [i32]),
132    /// Unsigned 64-bit row.
133    U64(&'a mut [u64]),
134    /// Signed 64-bit row.
135    I64(&'a mut [i64]),
136    /// 32-bit float row.
137    F32(&'a mut [f32]),
138    /// 64-bit float row.
139    F64(&'a mut [f64]),
140}
141
142/// An immutable typed view of one raster row.
143pub enum RasterRowRef<'a> {
144    /// Unsigned 8-bit row.
145    U8(&'a [u8]),
146    /// Signed 8-bit row.
147    I8(&'a [i8]),
148    /// Unsigned 16-bit row.
149    U16(&'a [u16]),
150    /// Signed 16-bit row.
151    I16(&'a [i16]),
152    /// Unsigned 32-bit row.
153    U32(&'a [u32]),
154    /// Signed 32-bit row.
155    I32(&'a [i32]),
156    /// Unsigned 64-bit row.
157    U64(&'a [u64]),
158    /// Signed 64-bit row.
159    I64(&'a [i64]),
160    /// 32-bit float row.
161    F32(&'a [f32]),
162    /// 64-bit float row.
163    F64(&'a [f64]),
164}
165
166impl RasterData {
167    /// Create a typed data buffer of length `len`, filled with `value` converted
168    /// to the specified data type.
169    pub fn new_filled(data_type: DataType, len: usize, value: f64) -> Self {
170        match data_type {
171            DataType::U8 => Self::U8(vec![value as u8; len]),
172            DataType::I8 => Self::I8(vec![value as i8; len]),
173            DataType::U16 => Self::U16(vec![value as u16; len]),
174            DataType::I16 => Self::I16(vec![value as i16; len]),
175            DataType::U32 => Self::U32(vec![value as u32; len]),
176            DataType::I32 => Self::I32(vec![value as i32; len]),
177            DataType::U64 => Self::U64(vec![value as u64; len]),
178            DataType::I64 => Self::I64(vec![value as i64; len]),
179            DataType::F32 => Self::F32(vec![value as f32; len]),
180            DataType::F64 => Self::F64(vec![value; len]),
181        }
182    }
183
184    /// Convert an `f64` vector into typed storage.
185    pub fn from_f64_vec(data_type: DataType, data: Vec<f64>) -> Self {
186        match data_type {
187            DataType::U8 => Self::U8(data.into_iter().map(|v| v as u8).collect()),
188            DataType::I8 => Self::I8(data.into_iter().map(|v| v as i8).collect()),
189            DataType::U16 => Self::U16(data.into_iter().map(|v| v as u16).collect()),
190            DataType::I16 => Self::I16(data.into_iter().map(|v| v as i16).collect()),
191            DataType::U32 => Self::U32(data.into_iter().map(|v| v as u32).collect()),
192            DataType::I32 => Self::I32(data.into_iter().map(|v| v as i32).collect()),
193            DataType::U64 => Self::U64(data.into_iter().map(|v| v as u64).collect()),
194            DataType::I64 => Self::I64(data.into_iter().map(|v| v as i64).collect()),
195            DataType::F32 => Self::F32(data.into_iter().map(|v| v as f32).collect()),
196            DataType::F64 => Self::F64(data),
197        }
198    }
199
200    /// Number of stored cells.
201    pub fn len(&self) -> usize {
202        match self {
203            Self::U8(v) => v.len(),
204            Self::I8(v) => v.len(),
205            Self::U16(v) => v.len(),
206            Self::I16(v) => v.len(),
207            Self::U32(v) => v.len(),
208            Self::I32(v) => v.len(),
209            Self::U64(v) => v.len(),
210            Self::I64(v) => v.len(),
211            Self::F32(v) => v.len(),
212            Self::F64(v) => v.len(),
213        }
214    }
215
216    /// Returns `true` if no cells are stored.
217    pub fn is_empty(&self) -> bool {
218        self.len() == 0
219    }
220
221    /// Native stored data type.
222    pub fn data_type(&self) -> DataType {
223        match self {
224            Self::U8(_) => DataType::U8,
225            Self::I8(_) => DataType::I8,
226            Self::U16(_) => DataType::U16,
227            Self::I16(_) => DataType::I16,
228            Self::U32(_) => DataType::U32,
229            Self::I32(_) => DataType::I32,
230            Self::U64(_) => DataType::U64,
231            Self::I64(_) => DataType::I64,
232            Self::F32(_) => DataType::F32,
233            Self::F64(_) => DataType::F64,
234        }
235    }
236
237    /// Read one cell as `f64`.
238    pub fn get_f64(&self, idx: usize) -> f64 {
239        match self {
240            Self::U8(v) => v[idx] as f64,
241            Self::I8(v) => v[idx] as f64,
242            Self::U16(v) => v[idx] as f64,
243            Self::I16(v) => v[idx] as f64,
244            Self::U32(v) => v[idx] as f64,
245            Self::I32(v) => v[idx] as f64,
246            Self::U64(v) => v[idx] as f64,
247            Self::I64(v) => v[idx] as f64,
248            Self::F32(v) => v[idx] as f64,
249            Self::F64(v) => v[idx],
250        }
251    }
252
253    /// Set one cell from an `f64` value using native-type conversion.
254    pub fn set_f64(&mut self, idx: usize, value: f64) {
255        match self {
256            Self::U8(v) => v[idx] = value as u8,
257            Self::I8(v) => v[idx] = value as i8,
258            Self::U16(v) => v[idx] = value as u16,
259            Self::I16(v) => v[idx] = value as i16,
260            Self::U32(v) => v[idx] = value as u32,
261            Self::I32(v) => v[idx] = value as i32,
262            Self::U64(v) => v[idx] = value as u64,
263            Self::I64(v) => v[idx] = value as i64,
264            Self::F32(v) => v[idx] = value as f32,
265            Self::F64(v) => v[idx] = value,
266        }
267    }
268
269    /// Iterate over all cells as `f64`.
270    pub fn iter_f64(&self) -> Box<dyn Iterator<Item = f64> + '_> {
271        match self {
272            Self::U8(v) => Box::new(v.iter().copied().map(|x| x as f64)),
273            Self::I8(v) => Box::new(v.iter().copied().map(|x| x as f64)),
274            Self::U16(v) => Box::new(v.iter().copied().map(|x| x as f64)),
275            Self::I16(v) => Box::new(v.iter().copied().map(|x| x as f64)),
276            Self::U32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
277            Self::I32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
278            Self::U64(v) => Box::new(v.iter().copied().map(|x| x as f64)),
279            Self::I64(v) => Box::new(v.iter().copied().map(|x| x as f64)),
280            Self::F32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
281            Self::F64(v) => Box::new(v.iter().copied()),
282        }
283    }
284
285    /// Materialize all cells as `Vec<f64>`.
286    pub fn to_f64_vec(&self) -> Vec<f64> {
287        self.iter_f64().collect()
288    }
289
290    /// Returns data as `u8` slice when storage is `U8`.
291    pub fn as_u8_slice(&self) -> Option<&[u8]> {
292        match self {
293            Self::U8(v) => Some(v.as_slice()),
294            _ => None,
295        }
296    }
297
298    /// Returns data as mutable `u8` slice when storage is `U8`.
299    pub fn as_u8_slice_mut(&mut self) -> Option<&mut [u8]> {
300        match self {
301            Self::U8(v) => Some(v.as_mut_slice()),
302            _ => None,
303        }
304    }
305
306    /// Returns data as `i8` slice when storage is `I8`.
307    pub fn as_i8_slice(&self) -> Option<&[i8]> {
308        match self {
309            Self::I8(v) => Some(v.as_slice()),
310            _ => None,
311        }
312    }
313
314    /// Returns data as mutable `i8` slice when storage is `I8`.
315    pub fn as_i8_slice_mut(&mut self) -> Option<&mut [i8]> {
316        match self {
317            Self::I8(v) => Some(v.as_mut_slice()),
318            _ => None,
319        }
320    }
321
322    /// Returns data as `u16` slice when storage is `U16`.
323    pub fn as_u16_slice(&self) -> Option<&[u16]> {
324        match self {
325            Self::U16(v) => Some(v.as_slice()),
326            _ => None,
327        }
328    }
329
330    /// Returns data as mutable `u16` slice when storage is `U16`.
331    pub fn as_u16_slice_mut(&mut self) -> Option<&mut [u16]> {
332        match self {
333            Self::U16(v) => Some(v.as_mut_slice()),
334            _ => None,
335        }
336    }
337
338    /// Returns data as `i16` slice when storage is `I16`.
339    pub fn as_i16_slice(&self) -> Option<&[i16]> {
340        match self {
341            Self::I16(v) => Some(v.as_slice()),
342            _ => None,
343        }
344    }
345
346    /// Returns data as mutable `i16` slice when storage is `I16`.
347    pub fn as_i16_slice_mut(&mut self) -> Option<&mut [i16]> {
348        match self {
349            Self::I16(v) => Some(v.as_mut_slice()),
350            _ => None,
351        }
352    }
353
354    /// Returns data as `u32` slice when storage is `U32`.
355    pub fn as_u32_slice(&self) -> Option<&[u32]> {
356        match self {
357            Self::U32(v) => Some(v.as_slice()),
358            _ => None,
359        }
360    }
361
362    /// Returns data as mutable `u32` slice when storage is `U32`.
363    pub fn as_u32_slice_mut(&mut self) -> Option<&mut [u32]> {
364        match self {
365            Self::U32(v) => Some(v.as_mut_slice()),
366            _ => None,
367        }
368    }
369
370    /// Returns data as `i32` slice when storage is `I32`.
371    pub fn as_i32_slice(&self) -> Option<&[i32]> {
372        match self {
373            Self::I32(v) => Some(v.as_slice()),
374            _ => None,
375        }
376    }
377
378    /// Returns data as mutable `i32` slice when storage is `I32`.
379    pub fn as_i32_slice_mut(&mut self) -> Option<&mut [i32]> {
380        match self {
381            Self::I32(v) => Some(v.as_mut_slice()),
382            _ => None,
383        }
384    }
385
386    /// Returns data as `u64` slice when storage is `U64`.
387    pub fn as_u64_slice(&self) -> Option<&[u64]> {
388        match self {
389            Self::U64(v) => Some(v.as_slice()),
390            _ => None,
391        }
392    }
393
394    /// Returns data as mutable `u64` slice when storage is `U64`.
395    pub fn as_u64_slice_mut(&mut self) -> Option<&mut [u64]> {
396        match self {
397            Self::U64(v) => Some(v.as_mut_slice()),
398            _ => None,
399        }
400    }
401
402    /// Returns data as `i64` slice when storage is `I64`.
403    pub fn as_i64_slice(&self) -> Option<&[i64]> {
404        match self {
405            Self::I64(v) => Some(v.as_slice()),
406            _ => None,
407        }
408    }
409
410    /// Returns data as mutable `i64` slice when storage is `I64`.
411    pub fn as_i64_slice_mut(&mut self) -> Option<&mut [i64]> {
412        match self {
413            Self::I64(v) => Some(v.as_mut_slice()),
414            _ => None,
415        }
416    }
417
418    /// Returns data as `f32` slice when storage is `F32`.
419    pub fn as_f32_slice(&self) -> Option<&[f32]> {
420        match self {
421            Self::F32(v) => Some(v.as_slice()),
422            _ => None,
423        }
424    }
425
426    /// Returns data as mutable `f32` slice when storage is `F32`.
427    pub fn as_f32_slice_mut(&mut self) -> Option<&mut [f32]> {
428        match self {
429            Self::F32(v) => Some(v.as_mut_slice()),
430            _ => None,
431        }
432    }
433
434    /// Returns data as `f64` slice when storage is `F64`.
435    pub fn as_f64_slice(&self) -> Option<&[f64]> {
436        match self {
437            Self::F64(v) => Some(v.as_slice()),
438            _ => None,
439        }
440    }
441
442    /// Returns data as mutable `f64` slice when storage is `F64`.
443    pub fn as_f64_slice_mut(&mut self) -> Option<&mut [f64]> {
444        match self {
445            Self::F64(v) => Some(v.as_mut_slice()),
446            _ => None,
447        }
448    }
449}
450
451// ─── NoData sentinel ─────────────────────────────────────────────────────────
452
453/// Represents the "no data" sentinel value for a raster layer.
454#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct NoData(pub f64);
456
457impl NoData {
458    /// Common GIS nodata default.
459    pub const COMMON: NoData = NoData(-9999.0);
460    /// IEEE NaN-based nodata (compare with `is_nan()`).
461    pub const NAN: NoData = NoData(f64::NAN);
462
463    /// Returns `true` if `v` represents this nodata value.
464    #[inline]
465    pub fn matches(self, v: f64) -> bool {
466        if self.0.is_nan() {
467            v.is_nan()
468        } else {
469            (v - self.0).abs() < f64::EPSILON * self.0.abs().max(1.0)
470        }
471    }
472}
473
474impl Default for NoData {
475    fn default() -> Self {
476        Self::COMMON
477    }
478}
479
480// ─── Spatial extent ───────────────────────────────────────────────────────────
481
482/// Axis-aligned bounding box for a raster.
483#[derive(Debug, Clone, Copy, PartialEq)]
484pub struct Extent {
485    /// West edge (minimum X / longitude).
486    pub x_min: f64,
487    /// South edge (minimum Y / latitude).
488    pub y_min: f64,
489    /// East edge (maximum X / longitude).
490    pub x_max: f64,
491    /// North edge (maximum Y / latitude).
492    pub y_max: f64,
493}
494
495impl Extent {
496    /// Compute the extent from origin + grid dimensions.
497    pub fn from_origin(x_min: f64, y_min: f64, cols: usize, rows: usize, cell_size: f64) -> Self {
498        Self {
499            x_min,
500            y_min,
501            x_max: x_min + cols as f64 * cell_size,
502            y_max: y_min + rows as f64 * cell_size,
503        }
504    }
505
506    /// Width in spatial units.
507    pub fn width(&self) -> f64 { self.x_max - self.x_min }
508
509    /// Height in spatial units.
510    pub fn height(&self) -> f64 { self.y_max - self.y_min }
511}
512
513// ─── Statistics ───────────────────────────────────────────────────────────────
514
515/// Basic raster statistics.
516#[derive(Debug, Clone, Copy, PartialEq)]
517pub struct Statistics {
518    /// Minimum data value (excluding nodata).
519    pub min: f64,
520    /// Maximum data value (excluding nodata).
521    pub max: f64,
522    /// Mean of data values (excluding nodata).
523    pub mean: f64,
524    /// Standard deviation of data values (excluding nodata).
525    pub std_dev: f64,
526    /// Number of valid (non-nodata) cells.
527    pub valid_count: usize,
528    /// Number of nodata cells.
529    pub nodata_count: usize,
530}
531
532/// Selects the computation path used for raster statistics.
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum StatisticsComputationMode {
535    /// Use the crate's default optimized path.
536    Auto,
537    /// Force the scalar accumulation path.
538    Scalar,
539    /// Force the SIMD accumulation path.
540    Simd,
541}
542
543#[derive(Debug, Clone, Copy)]
544struct StatsAccumulator {
545    min: f64,
546    max: f64,
547    sum: f64,
548    sum_sq: f64,
549    valid_count: usize,
550    nodata_count: usize,
551}
552
553impl Default for StatsAccumulator {
554    fn default() -> Self {
555        Self {
556            min: f64::INFINITY,
557            max: f64::NEG_INFINITY,
558            sum: 0.0,
559            sum_sq: 0.0,
560            valid_count: 0,
561            nodata_count: 0,
562        }
563    }
564}
565
566impl StatsAccumulator {
567    fn merge(&mut self, other: Self) {
568        self.min = self.min.min(other.min);
569        self.max = self.max.max(other.max);
570        self.sum += other.sum;
571        self.sum_sq += other.sum_sq;
572        self.valid_count += other.valid_count;
573        self.nodata_count += other.nodata_count;
574    }
575
576    fn to_statistics(self) -> Statistics {
577        let (mean, std_dev) = if self.valid_count == 0 {
578            (0.0, 0.0)
579        } else {
580            let n = self.valid_count as f64;
581            let mean = self.sum / n;
582            let variance = (self.sum_sq / n - mean * mean).max(0.0);
583            (mean, variance.sqrt())
584        };
585
586        Statistics {
587            min: if self.valid_count == 0 { 0.0 } else { self.min },
588            max: if self.valid_count == 0 { 0.0 } else { self.max },
589            mean,
590            std_dev,
591            valid_count: self.valid_count,
592            nodata_count: self.nodata_count,
593        }
594    }
595}
596
597/// Resampling method used during raster reprojection.
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum ResampleMethod {
600    /// Nearest-neighbor sampling (fast, category-safe).
601    Nearest,
602    /// Bilinear interpolation (continuous surfaces).
603    Bilinear,
604    /// Bicubic interpolation (Catmull-Rom cubic convolution).
605    Cubic,
606    /// Lanczos interpolation (windowed sinc, radius 3).
607    Lanczos,
608    /// 3x3 mean filter around nearest source pixel center.
609    Average,
610    /// 3x3 minimum filter around nearest source pixel center.
611    Min,
612    /// 3x3 maximum filter around nearest source pixel center.
613    Max,
614    /// 3x3 mode filter around nearest source pixel center.
615    Mode,
616    /// 3x3 median filter around nearest source pixel center.
617    Median,
618    /// 3x3 standard-deviation filter around nearest source pixel center.
619    StdDev,
620}
621
622/// Nodata handling policy for interpolation-based reprojection.
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum NodataPolicy {
625    /// Require full valid kernel support; otherwise output nodata.
626    Strict,
627    /// Use available valid kernel samples and renormalize interpolation weights.
628    PartialKernel,
629    /// Try strict interpolation first, then fall back to nearest-neighbor sampling.
630    Fill,
631}
632
633/// Policy controlling longitude bound handling when antimeridian crossing is possible.
634#[derive(Debug, Clone, Copy, PartialEq, Eq)]
635pub enum AntimeridianPolicy {
636    /// Use linear bounds unless wrapped bounds are strictly narrower.
637    Auto,
638    /// Always use linear min/max longitude bounds.
639    Linear,
640    /// Always use wrapped minimal-arc longitude bounds.
641    Wrap,
642}
643
644/// Policy for converting resolution + extent to integer output dimensions.
645#[derive(Debug, Clone, Copy, PartialEq, Eq)]
646pub enum GridSizePolicy {
647    /// Expand outward to fully cover requested extent (uses ceil sizing).
648    Expand,
649    /// Keep grid within requested extent (uses floor sizing, min 1 cell).
650    FitInside,
651}
652
653/// Destination-footprint handling mode during reprojection.
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
655pub enum DestinationFootprint {
656    /// Do not apply a transformed-source footprint mask.
657    None,
658    /// Mask destination cells outside transformed source boundary ring.
659    SourceBoundary,
660}
661
662/// Options for raster reprojection.
663#[derive(Debug, Clone, Copy)]
664pub struct ReprojectOptions {
665    /// Destination CRS EPSG code.
666    pub dst_epsg: u32,
667    /// Resampling method.
668    pub resample: ResampleMethod,
669    /// Optional output column count (defaults to source `cols`).
670    pub cols: Option<usize>,
671    /// Optional output row count (defaults to source `rows`).
672    pub rows: Option<usize>,
673    /// Optional output extent in destination CRS.
674    ///
675    /// Defaults to transformed bounds of source extent corners.
676    pub extent: Option<Extent>,
677    /// Optional output X resolution in destination CRS units/pixel.
678    ///
679    /// Used to derive output `cols` when `cols` is not provided.
680    pub x_res: Option<f64>,
681    /// Optional output Y resolution in destination CRS units/pixel.
682    ///
683    /// Used to derive output `rows` when `rows` is not provided.
684    pub y_res: Option<f64>,
685    /// Optional snap origin X for resolution-derived output grid alignment.
686    ///
687    /// Used only when `x_res` is provided and `cols` is not explicitly set.
688    pub snap_x: Option<f64>,
689    /// Optional snap origin Y for resolution-derived output grid alignment.
690    ///
691    /// Used only when `y_res` is provided and `rows` is not explicitly set.
692    pub snap_y: Option<f64>,
693    /// Nodata policy used by interpolation methods.
694    pub nodata_policy: NodataPolicy,
695    /// Policy controlling antimeridian handling for geographic output bounds.
696    pub antimeridian_policy: AntimeridianPolicy,
697    /// Policy used when deriving integer output size from resolution controls.
698    pub grid_size_policy: GridSizePolicy,
699    /// Destination-footprint handling policy during reprojection.
700    pub destination_footprint: DestinationFootprint,
701}
702
703impl ReprojectOptions {
704    /// Create options with required destination EPSG and resampling method.
705    pub fn new(dst_epsg: u32, resample: ResampleMethod) -> Self {
706        Self {
707            dst_epsg,
708            resample,
709            cols: None,
710            rows: None,
711            extent: None,
712            x_res: None,
713            y_res: None,
714            snap_x: None,
715            snap_y: None,
716            nodata_policy: NodataPolicy::PartialKernel,
717            antimeridian_policy: AntimeridianPolicy::Auto,
718            grid_size_policy: GridSizePolicy::Expand,
719            destination_footprint: DestinationFootprint::None,
720        }
721    }
722
723    /// Set nodata handling policy for interpolation and return updated options.
724    pub fn with_nodata_policy(mut self, nodata_policy: NodataPolicy) -> Self {
725        self.nodata_policy = nodata_policy;
726        self
727    }
728
729    /// Set output raster size (columns, rows) and return updated options.
730    pub fn with_size(mut self, cols: usize, rows: usize) -> Self {
731        self.cols = Some(cols);
732        self.rows = Some(rows);
733        self
734    }
735
736    /// Set output raster extent and return updated options.
737    pub fn with_extent(mut self, extent: Extent) -> Self {
738        self.extent = Some(extent);
739        self
740    }
741
742    /// Set output resolution (`x_res`, `y_res`) and return updated options.
743    ///
744    /// Positive finite values are required and interpreted as destination CRS
745    /// units per pixel.
746    pub fn with_resolution(mut self, x_res: f64, y_res: f64) -> Self {
747        self.x_res = Some(x_res);
748        self.y_res = Some(y_res);
749        self
750    }
751
752    /// Set isotropic output resolution and return updated options.
753    pub fn with_square_resolution(mut self, res: f64) -> Self {
754        self.x_res = Some(res);
755        self.y_res = Some(res);
756        self
757    }
758
759    /// Set snap origin for aligning resolution-derived output grids.
760    ///
761    /// Snap alignment is applied per axis only when that axis uses
762    /// resolution-derived sizing (i.e., no explicit `cols`/`rows`).
763    pub fn with_snap_origin(mut self, snap_x: f64, snap_y: f64) -> Self {
764        self.snap_x = Some(snap_x);
765        self.snap_y = Some(snap_y);
766        self
767    }
768
769    /// Set antimeridian handling policy for geographic output bounds.
770    pub fn with_antimeridian_policy(mut self, policy: AntimeridianPolicy) -> Self {
771        self.antimeridian_policy = policy;
772        self
773    }
774
775    /// Set sizing policy used for resolution-derived output dimensions.
776    pub fn with_grid_size_policy(mut self, policy: GridSizePolicy) -> Self {
777        self.grid_size_policy = policy;
778        self
779    }
780
781    /// Set destination-footprint handling policy for reprojection output.
782    pub fn with_destination_footprint(mut self, footprint: DestinationFootprint) -> Self {
783        self.destination_footprint = footprint;
784        self
785    }
786}
787
788// ─── Configuration / builder ──────────────────────────────────────────────────
789
790/// Parameters used to construct a new `Raster`.
791#[derive(Debug, Clone)]
792pub struct RasterConfig {
793    /// Number of columns (samples).
794    pub cols: usize,
795    /// Number of rows (lines).
796    pub rows: usize,
797    /// Number of bands.
798    pub bands: usize,
799    /// X coordinate of the west edge (left).
800    pub x_min: f64,
801    /// Y coordinate of the south edge (bottom).
802    pub y_min: f64,
803    /// Cell size (assumed square unless `cell_size_y` is set).
804    pub cell_size: f64,
805    /// Optional distinct Y cell size (negative = top-down raster).
806    /// If `None`, `cell_size` is used (positive, bottom-up convention).
807    pub cell_size_y: Option<f64>,
808    /// No-data sentinel value.
809    pub nodata: f64,
810    /// Underlying storage type.
811    pub data_type: DataType,
812    /// Spatial reference system.
813    pub crs: CrsInfo,
814    /// Free-form metadata key/value pairs.
815    pub metadata: Vec<(String, String)>,
816}
817
818impl Default for RasterConfig {
819    fn default() -> Self {
820        Self {
821            cols: 0,
822            rows: 0,
823            bands: 1,
824            x_min: 0.0,
825            y_min: 0.0,
826            cell_size: 1.0,
827            cell_size_y: None,
828            nodata: -9999.0,
829            data_type: DataType::F32,
830            crs: CrsInfo::default(),
831            metadata: Vec::new(),
832        }
833    }
834}
835
836// ─── Main Raster struct ───────────────────────────────────────────────────────
837
838/// A raster grid with one or more bands.
839///
840/// Data is stored in a typed contiguous buffer matching `data_type`.
841/// Conversion to `f64` happens only through accessor helpers when needed.
842///
843/// Layout: band-major, then row-major top-down within each band
844/// (`index = band * rows * cols + row * cols + col`).
845#[derive(Debug, Clone)]
846pub struct Raster {
847    /// Number of columns.
848    pub cols: usize,
849    /// Number of rows.
850    pub rows: usize,
851    /// Number of bands.
852    pub bands: usize,
853    /// West edge X coordinate.
854    pub x_min: f64,
855    /// South edge Y coordinate.
856    pub y_min: f64,
857    /// Cell width in map units (always positive).
858    pub cell_size_x: f64,
859    /// Cell height in map units (always positive, stored as absolute value).
860    pub cell_size_y: f64,
861    /// No-data value.
862    pub nodata: f64,
863    /// On-disk data type (used for writing).
864    pub data_type: DataType,
865    /// Spatial reference.
866    pub crs: CrsInfo,
867    /// Free-form key/value metadata.
868    pub metadata: Vec<(String, String)>,
869    /// Raw data buffer, band-major then row-major top-down. Length = `bands * cols * rows`.
870    pub data: RasterData,
871}
872
873impl Raster {
874    // ─── Construction ──────────────────────────────────────────────────────
875
876    /// Create a new raster from a `RasterConfig`, filling all cells with `nodata`.
877    pub fn new(cfg: RasterConfig) -> Self {
878        let bands = cfg.bands.max(1);
879        let n = cfg.cols * cfg.rows * bands;
880        let cell_size_y = cfg.cell_size_y.map(|v| v.abs()).unwrap_or(cfg.cell_size);
881        Self {
882            cols: cfg.cols,
883            rows: cfg.rows,
884            bands,
885            x_min: cfg.x_min,
886            y_min: cfg.y_min,
887            cell_size_x: cfg.cell_size,
888            cell_size_y,
889            nodata: cfg.nodata,
890            data_type: cfg.data_type,
891            crs: cfg.crs,
892            metadata: cfg.metadata,
893            data: RasterData::new_filled(cfg.data_type, n, cfg.nodata),
894        }
895    }
896
897    /// Create a raster from a raw `f64` data buffer.
898    ///
899    /// # Errors
900    /// Returns [`RasterError::InvalidDimensions`] if `data.len() != cols * rows * bands`.
901    pub fn from_data(cfg: RasterConfig, data: Vec<f64>) -> Result<Self> {
902        let bands = cfg.bands.max(1);
903        if data.len() != cfg.cols * cfg.rows * bands {
904            return Err(RasterError::InvalidDimensions { cols: cfg.cols, rows: cfg.rows });
905        }
906        let dt = cfg.data_type;
907        let mut r = Self::new(cfg);
908        r.data = RasterData::from_f64_vec(dt, data);
909        Ok(r)
910    }
911
912    /// Create a raster from a typed data buffer.
913    ///
914    /// # Errors
915    /// Returns [`RasterError::InvalidDimensions`] if `data.len() != cols * rows * bands`.
916    /// Returns [`RasterError::Other`] if `cfg.data_type != data.data_type()`.
917    pub fn from_data_native(cfg: RasterConfig, data: RasterData) -> Result<Self> {
918        let bands = cfg.bands.max(1);
919        if data.len() != cfg.cols * cfg.rows * bands {
920            return Err(RasterError::InvalidDimensions { cols: cfg.cols, rows: cfg.rows });
921        }
922        if cfg.data_type != data.data_type() {
923            return Err(RasterError::Other(format!(
924                "data_type mismatch: config={}, data={}",
925                cfg.data_type,
926                data.data_type()
927            )));
928        }
929        let mut r = Self::new(cfg);
930        r.data = data;
931        Ok(r)
932    }
933
934    /// Typed fast-path access to `u8` storage.
935    pub fn data_u8(&self) -> Option<&[u8]> { self.data.as_u8_slice() }
936    /// Typed fast-path mutable access to `u8` storage.
937    pub fn data_u8_mut(&mut self) -> Option<&mut [u8]> { self.data.as_u8_slice_mut() }
938
939    /// Typed fast-path access to `i8` storage.
940    pub fn data_i8(&self) -> Option<&[i8]> { self.data.as_i8_slice() }
941    /// Typed fast-path mutable access to `i8` storage.
942    pub fn data_i8_mut(&mut self) -> Option<&mut [i8]> { self.data.as_i8_slice_mut() }
943
944    /// Typed fast-path access to `u16` storage.
945    pub fn data_u16(&self) -> Option<&[u16]> { self.data.as_u16_slice() }
946    /// Typed fast-path mutable access to `u16` storage.
947    pub fn data_u16_mut(&mut self) -> Option<&mut [u16]> { self.data.as_u16_slice_mut() }
948
949    /// Typed fast-path access to `i16` storage.
950    pub fn data_i16(&self) -> Option<&[i16]> { self.data.as_i16_slice() }
951    /// Typed fast-path mutable access to `i16` storage.
952    pub fn data_i16_mut(&mut self) -> Option<&mut [i16]> { self.data.as_i16_slice_mut() }
953
954    /// Typed fast-path access to `u32` storage.
955    pub fn data_u32(&self) -> Option<&[u32]> { self.data.as_u32_slice() }
956    /// Typed fast-path mutable access to `u32` storage.
957    pub fn data_u32_mut(&mut self) -> Option<&mut [u32]> { self.data.as_u32_slice_mut() }
958
959    /// Typed fast-path access to `i32` storage.
960    pub fn data_i32(&self) -> Option<&[i32]> { self.data.as_i32_slice() }
961    /// Typed fast-path mutable access to `i32` storage.
962    pub fn data_i32_mut(&mut self) -> Option<&mut [i32]> { self.data.as_i32_slice_mut() }
963
964    /// Typed fast-path access to `u64` storage.
965    pub fn data_u64(&self) -> Option<&[u64]> { self.data.as_u64_slice() }
966    /// Typed fast-path mutable access to `u64` storage.
967    pub fn data_u64_mut(&mut self) -> Option<&mut [u64]> { self.data.as_u64_slice_mut() }
968
969    /// Typed fast-path access to `i64` storage.
970    pub fn data_i64(&self) -> Option<&[i64]> { self.data.as_i64_slice() }
971    /// Typed fast-path mutable access to `i64` storage.
972    pub fn data_i64_mut(&mut self) -> Option<&mut [i64]> { self.data.as_i64_slice_mut() }
973
974    /// Typed fast-path access to `f32` storage.
975    pub fn data_f32(&self) -> Option<&[f32]> { self.data.as_f32_slice() }
976    /// Typed fast-path mutable access to `f32` storage.
977    pub fn data_f32_mut(&mut self) -> Option<&mut [f32]> { self.data.as_f32_slice_mut() }
978
979    /// Typed fast-path access to `f64` storage.
980    pub fn data_f64(&self) -> Option<&[f64]> { self.data.as_f64_slice() }
981    /// Typed fast-path mutable access to `f64` storage.
982    pub fn data_f64_mut(&mut self) -> Option<&mut [f64]> { self.data.as_f64_slice_mut() }
983
984    // ─── Pixel access ──────────────────────────────────────────────────────
985
986    /// Return the flat buffer index for signed band, row, and column coordinates.
987    /// Returns `None` when coordinates are outside the raster bounds.
988    #[inline]
989    pub fn index(&self, band: isize, row: isize, col: isize) -> Option<usize> {
990        if band < 0
991            || row < 0
992            || col < 0
993            || band >= self.bands as isize
994            || row >= self.rows as isize
995            || col >= self.cols as isize
996        {
997            return None;
998        }
999        let band = band as usize;
1000        let row = row as usize;
1001        let col = col as usize;
1002        let band_stride = self.rows * self.cols;
1003        Some(band * band_stride + row * self.cols + col)
1004    }
1005
1006    /// Get the value at signed pixel coordinates `(band, row, col)`.
1007    ///
1008    /// Returns the raster's numeric `nodata` sentinel when coordinates are
1009    /// out-of-bounds or the stored value is nodata.
1010    #[inline]
1011    pub fn get(&self, band: isize, row: isize, col: isize) -> f64 {
1012        self.get_raw(band, row, col).unwrap_or(self.nodata)
1013    }
1014
1015    /// Get the value at signed pixel coordinates `(band, row, col)` as
1016    /// `Option<f64>`.
1017    ///
1018    /// Returns `None` if coordinates are out-of-bounds or the value is nodata.
1019    #[inline]
1020    pub fn get_opt(&self, band: isize, row: isize, col: isize) -> Option<f64> {
1021        let idx = self.index(band, row, col)?;
1022        let v = self.data.get_f64(idx);
1023        if self.is_nodata(v) { None } else { Some(v) }
1024    }
1025
1026    /// Get the raw value (including nodata) at signed pixel coordinates `(band, row, col)`.
1027    /// Returns `None` only on out-of-bounds.
1028    #[inline]
1029    pub fn get_raw(&self, band: isize, row: isize, col: isize) -> Option<f64> {
1030        let idx = self.index(band, row, col)?;
1031        Some(self.data.get_f64(idx))
1032    }
1033
1034    /// Set the value at signed pixel coordinates `(band, row, col)`.
1035    ///
1036    /// # Errors
1037    /// Returns [`RasterError::OutOfBounds`] if coordinates are outside the grid.
1038    #[inline]
1039    pub fn set(&mut self, band: isize, row: isize, col: isize, value: f64) -> Result<()> {
1040        if band < 0
1041            || row < 0
1042            || col < 0
1043            || band >= self.bands as isize
1044            || row >= self.rows as isize
1045            || col >= self.cols as isize
1046        {
1047            return Err(RasterError::OutOfBounds {
1048                band,
1049                col,
1050                row,
1051                bands: self.bands,
1052                cols: self.cols,
1053                rows: self.rows,
1054            });
1055        }
1056        let idx = self.index(band, row, col).expect("set bounds prechecked");
1057        self.data.set_f64(idx, value);
1058        Ok(())
1059    }
1060
1061    /// Set a value at signed pixel coordinates, panicking on out-of-bounds. Convenience alias.
1062    #[inline]
1063    pub fn set_unchecked(&mut self, band: isize, row: isize, col: isize, value: f64) {
1064        let idx = self
1065            .index(band, row, col)
1066            .expect("set_unchecked requires in-bounds coordinates");
1067        self.data.set_f64(idx, value);
1068    }
1069
1070    /// Returns `true` if `v` equals this raster's nodata sentinel.
1071    #[inline]
1072    pub fn is_nodata(&self, v: f64) -> bool {
1073        if self.nodata.is_nan() {
1074            v.is_nan()
1075        } else {
1076            (v - self.nodata).abs() < 1e-10 * self.nodata.abs().max(1.0)
1077        }
1078    }
1079
1080    // ─── Geometry helpers ─────────────────────────────────────────────────
1081
1082    /// Northern extent (Y max) — top of the grid.
1083    #[inline]
1084    pub fn y_max(&self) -> f64 {
1085        self.y_min + self.rows as f64 * self.cell_size_y
1086    }
1087
1088    /// Eastern extent (X max) — right edge of the grid.
1089    #[inline]
1090    pub fn x_max(&self) -> f64 {
1091        self.x_min + self.cols as f64 * self.cell_size_x
1092    }
1093
1094    /// The geographic extent of the raster.
1095    pub fn extent(&self) -> Extent {
1096        Extent {
1097            x_min: self.x_min,
1098            y_min: self.y_min,
1099            x_max: self.x_max(),
1100            y_max: self.y_max(),
1101        }
1102    }
1103
1104    /// Cell-center X coordinate for signed column index `col`.
1105    #[inline]
1106    pub fn col_center_x(&self, col: isize) -> f64 {
1107        self.x_min + (col as f64 + 0.5) * self.cell_size_x
1108    }
1109
1110    /// Cell-center Y coordinate for signed row index `row` (row 0 = north).
1111    #[inline]
1112    pub fn row_center_y(&self, row: isize) -> f64 {
1113        self.y_max() - (row as f64 + 0.5) * self.cell_size_y
1114    }
1115
1116    /// Convert geographic coordinates `(x, y)` to signed pixel indices `(col, row)`.
1117    /// Returns `None` if the point lies outside the raster extent.
1118    pub fn world_to_pixel(&self, x: f64, y: f64) -> Option<(isize, isize)> {
1119        if x < self.x_min || x >= self.x_max() || y < self.y_min || y >= self.y_max() {
1120            return None;
1121        }
1122        let col = ((x - self.x_min) / self.cell_size_x).floor() as isize;
1123        let row = ((self.y_max() - y) / self.cell_size_y).floor() as isize;
1124        let col = col.min(self.cols as isize - 1);
1125        let row = row.min(self.rows as isize - 1);
1126        Some((col, row))
1127    }
1128
1129    /// Assign a CRS to this raster using an EPSG code.
1130    ///
1131    /// Replaces the entire `crs` struct with a new `CrsInfo` containing only the EPSG code.
1132    /// Any existing `wkt` or `proj4` fields are cleared to ensure CRS consistency.
1133    pub fn assign_crs_epsg(&mut self, epsg: u32) {
1134        self.crs = CrsInfo {
1135            epsg: Some(epsg),
1136            wkt: None,
1137            proj4: None,
1138        };
1139    }
1140
1141    /// Assign a CRS to this raster using WKT text.
1142    ///
1143    /// Replaces the entire `crs` struct with a new `CrsInfo` containing only the WKT definition.
1144    /// Any existing `epsg` or `proj4` fields are cleared to ensure CRS consistency.
1145    pub fn assign_crs_wkt(&mut self, wkt: &str) {
1146        self.crs = CrsInfo {
1147            epsg: None,
1148            wkt: Some(wkt.to_string()),
1149            proj4: None,
1150        };
1151    }
1152
1153    /// Reproject this raster to another EPSG CRS.
1154    ///
1155    /// This MVP implementation uses an auto-derived output extent from transformed
1156    /// sampled source-boundary points (corners + edge densification) and supports
1157    /// nearest, bilinear, cubic, and Lanczos sampling.
1158    ///
1159    /// For explicit output grid controls (`cols`, `rows`, `extent`), use
1160    /// [`Raster::reproject_with_options`].
1161    ///
1162    /// # Errors
1163    /// Returns an error when source/destination EPSG codes are unsupported,
1164    /// source CRS is missing, or transformed extents are invalid.
1165    pub fn reproject_to_epsg(&self, dst_epsg: u32, resample: ResampleMethod) -> Result<Raster> {
1166        self.reproject_with_options(&ReprojectOptions::new(dst_epsg, resample))
1167    }
1168
1169    /// Reproject this raster using detailed output-grid options.
1170    pub fn reproject_with_options(&self, options: &ReprojectOptions) -> Result<Raster> {
1171        let src_epsg = self.crs.epsg.ok_or_else(|| {
1172            RasterError::Other(
1173                "reproject_to_epsg requires source CRS EPSG in raster.crs.epsg".to_string(),
1174            )
1175        })?;
1176        let src_crs = Crs::from_epsg(src_epsg).map_err(|e| {
1177            RasterError::Other(format!("source EPSG {src_epsg} is not supported: {e}"))
1178        })?;
1179        let dst_crs = Crs::from_epsg(options.dst_epsg).map_err(|e| {
1180            RasterError::Other(format!(
1181                "destination EPSG {} is not supported: {e}",
1182                options.dst_epsg
1183            ))
1184        })?;
1185
1186        self.reproject_internal(&src_crs, &dst_crs, options, None)
1187    }
1188
1189    /// Reproject this raster using detailed output-grid options and emit
1190    /// progress updates in the range [0, 1] as destination rows are completed.
1191    pub fn reproject_with_options_and_progress<F>(
1192        &self,
1193        options: &ReprojectOptions,
1194        progress: F,
1195    ) -> Result<Raster>
1196    where
1197        F: Fn(f64) + Send + Sync,
1198    {
1199        let src_epsg = self.crs.epsg.ok_or_else(|| {
1200            RasterError::Other(
1201                "reproject_to_epsg requires source CRS EPSG in raster.crs.epsg".to_string(),
1202            )
1203        })?;
1204        let src_crs = Crs::from_epsg(src_epsg).map_err(|e| {
1205            RasterError::Other(format!("source EPSG {src_epsg} is not supported: {e}"))
1206        })?;
1207        let dst_crs = Crs::from_epsg(options.dst_epsg).map_err(|e| {
1208            RasterError::Other(format!(
1209                "destination EPSG {} is not supported: {e}",
1210                options.dst_epsg
1211            ))
1212        })?;
1213
1214        self.reproject_internal(&src_crs, &dst_crs, options, Some(&progress))
1215    }
1216
1217    /// Reproject this raster using caller-supplied source/destination CRS objects.
1218    ///
1219    /// This advanced path bypasses the requirement that `self.crs.epsg` is set,
1220    /// enabling workflows where CRS definitions are managed externally.
1221    ///
1222    /// Note: `options.dst_epsg` is still used for output `CrsInfo` metadata and
1223    /// EPSG-specific extent behavior (e.g., antimeridian handling for EPSG:4326).
1224    pub fn reproject_with_crs(
1225        &self,
1226        src_crs: &Crs,
1227        dst_crs: &Crs,
1228        options: &ReprojectOptions,
1229    ) -> Result<Raster> {
1230        self.reproject_internal(src_crs, dst_crs, options, None)
1231    }
1232
1233    /// Reproject this raster with caller-supplied CRS objects and emit progress
1234    /// updates in the range [0, 1] as destination rows are completed.
1235    pub fn reproject_with_crs_and_progress<F>(
1236        &self,
1237        src_crs: &Crs,
1238        dst_crs: &Crs,
1239        options: &ReprojectOptions,
1240        progress: F,
1241    ) -> Result<Raster>
1242    where
1243        F: Fn(f64) + Send + Sync,
1244    {
1245        self.reproject_internal(src_crs, dst_crs, options, Some(&progress))
1246    }
1247
1248    /// Convenience helper for nearest-neighbor reprojection.
1249    pub fn reproject_to_epsg_nearest(&self, dst_epsg: u32) -> Result<Raster> {
1250        self.reproject_to_epsg(dst_epsg, ResampleMethod::Nearest)
1251    }
1252
1253    /// Convenience helper for bilinear reprojection.
1254    pub fn reproject_to_epsg_bilinear(&self, dst_epsg: u32) -> Result<Raster> {
1255        self.reproject_to_epsg(dst_epsg, ResampleMethod::Bilinear)
1256    }
1257
1258    /// Convenience helper for cubic reprojection.
1259    pub fn reproject_to_epsg_cubic(&self, dst_epsg: u32) -> Result<Raster> {
1260        self.reproject_to_epsg(dst_epsg, ResampleMethod::Cubic)
1261    }
1262
1263    /// Reproject to destination EPSG using Lanczos interpolation.
1264    pub fn reproject_to_epsg_lanczos(&self, dst_epsg: u32) -> Result<Raster> {
1265        self.reproject_to_epsg(dst_epsg, ResampleMethod::Lanczos)
1266    }
1267
1268    /// Reproject to destination EPSG using 3x3 mean resampling.
1269    pub fn reproject_to_epsg_average(&self, dst_epsg: u32) -> Result<Raster> {
1270        self.reproject_to_epsg(dst_epsg, ResampleMethod::Average)
1271    }
1272
1273    /// Reproject to destination EPSG using 3x3 minimum resampling.
1274    pub fn reproject_to_epsg_min(&self, dst_epsg: u32) -> Result<Raster> {
1275        self.reproject_to_epsg(dst_epsg, ResampleMethod::Min)
1276    }
1277
1278    /// Reproject to destination EPSG using 3x3 maximum resampling.
1279    pub fn reproject_to_epsg_max(&self, dst_epsg: u32) -> Result<Raster> {
1280        self.reproject_to_epsg(dst_epsg, ResampleMethod::Max)
1281    }
1282
1283    /// Reproject to destination EPSG using 3x3 modal resampling.
1284    pub fn reproject_to_epsg_mode(&self, dst_epsg: u32) -> Result<Raster> {
1285        self.reproject_to_epsg(dst_epsg, ResampleMethod::Mode)
1286    }
1287
1288    /// Reproject to destination EPSG using 3x3 median resampling.
1289    pub fn reproject_to_epsg_median(&self, dst_epsg: u32) -> Result<Raster> {
1290        self.reproject_to_epsg(dst_epsg, ResampleMethod::Median)
1291    }
1292
1293    /// Reproject to destination EPSG using 3x3 standard-deviation resampling.
1294    pub fn reproject_to_epsg_stddev(&self, dst_epsg: u32) -> Result<Raster> {
1295        self.reproject_to_epsg(dst_epsg, ResampleMethod::StdDev)
1296    }
1297
1298    /// Reproject this raster to match another raster's grid (CRS, extent, rows, cols).
1299    ///
1300    /// The `target_grid` provides destination EPSG, output extent, and output
1301    /// dimensions. This is useful when aligning products from multiple sources
1302    /// onto a shared reference grid.
1303    ///
1304    /// # Errors
1305    /// Returns an error if `target_grid.crs.epsg` is missing or unsupported.
1306    pub fn reproject_to_match_grid(
1307        &self,
1308        target_grid: &Raster,
1309        resample: ResampleMethod,
1310    ) -> Result<Raster> {
1311        let dst_epsg = target_grid.crs.epsg.ok_or_else(|| {
1312            RasterError::Other(
1313                "reproject_to_match_grid requires target grid CRS EPSG in target_grid.crs.epsg"
1314                    .to_string(),
1315            )
1316        })?;
1317
1318        let opts = ReprojectOptions::new(dst_epsg, resample)
1319            .with_size(target_grid.cols, target_grid.rows)
1320            .with_extent(target_grid.extent());
1321
1322        self.reproject_with_options(&opts)
1323    }
1324
1325    /// Reproject this raster to match another raster's grid while emitting
1326    /// progress updates in the range [0, 1] as destination rows are completed.
1327    pub fn reproject_to_match_grid_and_progress<F>(
1328        &self,
1329        target_grid: &Raster,
1330        resample: ResampleMethod,
1331        progress: F,
1332    ) -> Result<Raster>
1333    where
1334        F: Fn(f64) + Send + Sync,
1335    {
1336        let dst_epsg = target_grid.crs.epsg.ok_or_else(|| {
1337            RasterError::Other(
1338                "reproject_to_match_grid requires target grid CRS EPSG in target_grid.crs.epsg"
1339                    .to_string(),
1340            )
1341        })?;
1342
1343        let opts = ReprojectOptions::new(dst_epsg, resample)
1344            .with_size(target_grid.cols, target_grid.rows)
1345            .with_extent(target_grid.extent());
1346
1347        self.reproject_with_options_and_progress(&opts, progress)
1348    }
1349
1350    /// Reproject this raster using another raster's CRS, resolution, and snap origin.
1351    ///
1352    /// Unlike [`Raster::reproject_to_match_grid`], this keeps the destination
1353    /// extent auto-derived from the transformed source footprint, while aligning
1354    /// that extent to the reference grid's origin and pixel size.
1355    ///
1356    /// # Errors
1357    /// Returns an error if `reference_grid.crs.epsg` is missing or unsupported.
1358    pub fn reproject_to_match_resolution(
1359        &self,
1360        reference_grid: &Raster,
1361        resample: ResampleMethod,
1362    ) -> Result<Raster> {
1363        let dst_epsg = reference_grid.crs.epsg.ok_or_else(|| {
1364            RasterError::Other(
1365                "reproject_to_match_resolution requires reference grid CRS EPSG in reference_grid.crs.epsg"
1366                    .to_string(),
1367            )
1368        })?;
1369
1370        let opts = ReprojectOptions::new(dst_epsg, resample)
1371            .with_resolution(reference_grid.cell_size_x, reference_grid.cell_size_y)
1372            .with_snap_origin(reference_grid.x_min, reference_grid.y_min);
1373
1374        self.reproject_with_options(&opts)
1375    }
1376
1377    /// Reproject this raster while matching a reference raster's resolution
1378    /// and snap origin, emitting progress updates in [0, 1].
1379    pub fn reproject_to_match_resolution_and_progress<F>(
1380        &self,
1381        reference_grid: &Raster,
1382        resample: ResampleMethod,
1383        progress: F,
1384    ) -> Result<Raster>
1385    where
1386        F: Fn(f64) + Send + Sync,
1387    {
1388        let dst_epsg = reference_grid.crs.epsg.ok_or_else(|| {
1389            RasterError::Other(
1390                "reproject_to_match_resolution requires reference grid CRS EPSG in reference_grid.crs.epsg"
1391                    .to_string(),
1392            )
1393        })?;
1394
1395        let opts = ReprojectOptions::new(dst_epsg, resample)
1396            .with_resolution(reference_grid.cell_size_x, reference_grid.cell_size_y)
1397            .with_snap_origin(reference_grid.x_min, reference_grid.y_min);
1398
1399        self.reproject_with_options_and_progress(&opts, progress)
1400    }
1401
1402    /// Reproject this raster to an explicit destination EPSG while matching a
1403    /// reference raster's resolution and snap origin.
1404    ///
1405    /// If `reference_grid` is in a different CRS than `dst_epsg`, the
1406    /// reference snap origin and per-axis cell sizes are transformed to
1407    /// destination CRS using local axis steps at the reference origin.
1408    ///
1409    /// # Errors
1410    /// Returns an error if reference/destination EPSG values are missing or
1411    /// unsupported, or if transformed reference resolution is invalid.
1412    pub fn reproject_to_match_resolution_in_epsg(
1413        &self,
1414        dst_epsg: u32,
1415        reference_grid: &Raster,
1416        resample: ResampleMethod,
1417    ) -> Result<Raster> {
1418        let reference_epsg = reference_grid.crs.epsg.ok_or_else(|| {
1419            RasterError::Other(
1420                "reproject_to_match_resolution_in_epsg requires reference grid CRS EPSG in reference_grid.crs.epsg"
1421                    .to_string(),
1422            )
1423        })?;
1424
1425        let (snap_x, snap_y, x_res, y_res) = if reference_epsg == dst_epsg {
1426            (
1427                reference_grid.x_min,
1428                reference_grid.y_min,
1429                reference_grid.cell_size_x,
1430                reference_grid.cell_size_y,
1431            )
1432        } else {
1433            let ref_crs = Crs::from_epsg(reference_epsg).map_err(|e| {
1434                RasterError::Other(format!(
1435                    "reference EPSG {reference_epsg} is not supported: {e}"
1436                ))
1437            })?;
1438            let dst_crs = Crs::from_epsg(dst_epsg).map_err(|e| {
1439                RasterError::Other(format!(
1440                    "destination EPSG {dst_epsg} is not supported: {e}"
1441                ))
1442            })?;
1443
1444            let ox = reference_grid.x_min;
1445            let oy = reference_grid.y_min;
1446            let (sx, sy) = ref_crs.transform_to(ox, oy, &dst_crs).map_err(|e| {
1447                RasterError::Other(format!(
1448                    "failed to transform reference snap origin to EPSG:{dst_epsg}: {e}"
1449                ))
1450            })?;
1451            let (sx_dx, sy_dx) = ref_crs
1452                .transform_to(ox + reference_grid.cell_size_x, oy, &dst_crs)
1453                .map_err(|e| {
1454                    RasterError::Other(format!(
1455                        "failed to transform reference X-step to EPSG:{dst_epsg}: {e}"
1456                    ))
1457                })?;
1458            let (sx_dy, sy_dy) = ref_crs
1459                .transform_to(ox, oy + reference_grid.cell_size_y, &dst_crs)
1460                .map_err(|e| {
1461                    RasterError::Other(format!(
1462                        "failed to transform reference Y-step to EPSG:{dst_epsg}: {e}"
1463                    ))
1464                })?;
1465
1466            let rx = (sx_dx - sx).hypot(sy_dx - sy);
1467            let ry = (sx_dy - sx).hypot(sy_dy - sy);
1468            if !rx.is_finite() || !ry.is_finite() || rx <= 0.0 || ry <= 0.0 {
1469                return Err(RasterError::Other(
1470                    "invalid transformed reference resolution while matching destination EPSG"
1471                        .to_string(),
1472                ));
1473            }
1474            (sx, sy, rx, ry)
1475        };
1476
1477        let opts = ReprojectOptions::new(dst_epsg, resample)
1478            .with_resolution(x_res, y_res)
1479            .with_snap_origin(snap_x, snap_y);
1480
1481        self.reproject_with_options(&opts)
1482    }
1483
1484    /// Reproject this raster to an explicit destination EPSG while matching a
1485    /// reference raster's transformed resolution/snap, emitting progress in [0, 1].
1486    pub fn reproject_to_match_resolution_in_epsg_and_progress<F>(
1487        &self,
1488        dst_epsg: u32,
1489        reference_grid: &Raster,
1490        resample: ResampleMethod,
1491        progress: F,
1492    ) -> Result<Raster>
1493    where
1494        F: Fn(f64) + Send + Sync,
1495    {
1496        let reference_epsg = reference_grid.crs.epsg.ok_or_else(|| {
1497            RasterError::Other(
1498                "reproject_to_match_resolution_in_epsg requires reference grid CRS EPSG in reference_grid.crs.epsg"
1499                    .to_string(),
1500            )
1501        })?;
1502
1503        let (snap_x, snap_y, x_res, y_res) = if reference_epsg == dst_epsg {
1504            (
1505                reference_grid.x_min,
1506                reference_grid.y_min,
1507                reference_grid.cell_size_x,
1508                reference_grid.cell_size_y,
1509            )
1510        } else {
1511            let ref_crs = Crs::from_epsg(reference_epsg).map_err(|e| {
1512                RasterError::Other(format!(
1513                    "reference EPSG {reference_epsg} is not supported: {e}"
1514                ))
1515            })?;
1516            let dst_crs = Crs::from_epsg(dst_epsg).map_err(|e| {
1517                RasterError::Other(format!(
1518                    "destination EPSG {dst_epsg} is not supported: {e}"
1519                ))
1520            })?;
1521
1522            let ox = reference_grid.x_min;
1523            let oy = reference_grid.y_min;
1524            let (sx, sy) = ref_crs.transform_to(ox, oy, &dst_crs).map_err(|e| {
1525                RasterError::Other(format!(
1526                    "failed to transform reference snap origin to EPSG:{dst_epsg}: {e}"
1527                ))
1528            })?;
1529            let (sx_dx, sy_dx) = ref_crs
1530                .transform_to(ox + reference_grid.cell_size_x, oy, &dst_crs)
1531                .map_err(|e| {
1532                    RasterError::Other(format!(
1533                        "failed to transform reference X-step to EPSG:{dst_epsg}: {e}"
1534                    ))
1535                })?;
1536            let (sx_dy, sy_dy) = ref_crs
1537                .transform_to(ox, oy + reference_grid.cell_size_y, &dst_crs)
1538                .map_err(|e| {
1539                    RasterError::Other(format!(
1540                        "failed to transform reference Y-step to EPSG:{dst_epsg}: {e}"
1541                    ))
1542                })?;
1543
1544            let rx = (sx_dx - sx).hypot(sy_dx - sy);
1545            let ry = (sx_dy - sx).hypot(sy_dy - sy);
1546            if !rx.is_finite() || !ry.is_finite() || rx <= 0.0 || ry <= 0.0 {
1547                return Err(RasterError::Other(
1548                    "invalid transformed reference resolution while matching destination EPSG"
1549                        .to_string(),
1550                ));
1551            }
1552            (sx, sy, rx, ry)
1553        };
1554
1555        let opts = ReprojectOptions::new(dst_epsg, resample)
1556            .with_resolution(x_res, y_res)
1557            .with_snap_origin(snap_x, snap_y);
1558
1559        self.reproject_with_options_and_progress(&opts, progress)
1560    }
1561
1562    fn reproject_internal(
1563        &self,
1564        src_crs: &Crs,
1565        dst_crs: &Crs,
1566        options: &ReprojectOptions,
1567        progress: Option<&(dyn Fn(f64) + Send + Sync)>,
1568    ) -> Result<Raster> {
1569        let src_extent = self.extent();
1570        let samples_per_edge = (self.cols.max(self.rows) / 32).clamp(8, 128);
1571        let base_extent = transformed_extent_from_boundary_samples(
1572            src_crs,
1573            dst_crs,
1574            src_extent,
1575            samples_per_edge,
1576            options.dst_epsg,
1577            options.antimeridian_policy,
1578        )?;
1579        let out_extent = options.extent.unwrap_or(base_extent);
1580        let width = out_extent.x_max - out_extent.x_min;
1581        let height = out_extent.y_max - out_extent.y_min;
1582
1583        if !out_extent.x_min.is_finite()
1584            || !out_extent.x_max.is_finite()
1585            || !out_extent.y_min.is_finite()
1586            || !out_extent.y_max.is_finite()
1587            || width <= 0.0
1588            || height <= 0.0
1589        {
1590            return Err(RasterError::CorruptData(
1591                "invalid transformed extent produced during reprojection".to_string(),
1592            ));
1593        }
1594
1595        let x_res = options.x_res.map(f64::abs);
1596        let y_res = options.y_res.map(f64::abs);
1597        if x_res.is_some_and(|v| !v.is_finite() || v <= 0.0)
1598            || y_res.is_some_and(|v| !v.is_finite() || v <= 0.0)
1599        {
1600            return Err(RasterError::CorruptData(
1601                "invalid reprojection resolution (x_res/y_res must be positive finite values)"
1602                    .to_string(),
1603            ));
1604        }
1605
1606        let mut x_min = out_extent.x_min;
1607        let mut x_max = out_extent.x_max;
1608        let mut y_min = out_extent.y_min;
1609        let mut y_max = out_extent.y_max;
1610
1611        let out_cols = match options.cols {
1612            Some(cols) => cols,
1613            None => match x_res {
1614                Some(rx) => {
1615                    if let Some(sx) = options.snap_x {
1616                        match options.grid_size_policy {
1617                            GridSizePolicy::Expand => {
1618                                x_min = snap_down_to_origin(x_min, sx, rx);
1619                                x_max = snap_up_to_origin(x_max, sx, rx);
1620                            }
1621                            GridSizePolicy::FitInside => {
1622                                x_min = snap_up_to_origin(x_min, sx, rx);
1623                                x_max = snap_down_to_origin(x_max, sx, rx);
1624                            }
1625                        }
1626                    }
1627                    let span = (x_max - x_min).max(0.0);
1628                    let cols = match options.grid_size_policy {
1629                        GridSizePolicy::Expand => (span / rx).ceil().max(1.0) as usize,
1630                        GridSizePolicy::FitInside => (span / rx).floor().max(1.0) as usize,
1631                    };
1632                    x_max = x_min + cols as f64 * rx;
1633                    cols
1634                }
1635                None => self.cols,
1636            },
1637        };
1638        let out_rows = match options.rows {
1639            Some(rows) => rows,
1640            None => match y_res {
1641                Some(ry) => {
1642                    if let Some(sy) = options.snap_y {
1643                        match options.grid_size_policy {
1644                            GridSizePolicy::Expand => {
1645                                y_min = snap_down_to_origin(y_min, sy, ry);
1646                                y_max = snap_up_to_origin(y_max, sy, ry);
1647                            }
1648                            GridSizePolicy::FitInside => {
1649                                y_min = snap_up_to_origin(y_min, sy, ry);
1650                                y_max = snap_down_to_origin(y_max, sy, ry);
1651                            }
1652                        }
1653                    }
1654                    let span = (y_max - y_min).max(0.0);
1655                    let rows = match options.grid_size_policy {
1656                        GridSizePolicy::Expand => (span / ry).ceil().max(1.0) as usize,
1657                        GridSizePolicy::FitInside => (span / ry).floor().max(1.0) as usize,
1658                    };
1659                    y_max = y_min + rows as f64 * ry;
1660                    rows
1661                }
1662                None => self.rows,
1663            },
1664        };
1665
1666        let out_extent = Extent {
1667            x_min,
1668            y_min,
1669            x_max,
1670            y_max,
1671        };
1672
1673        if out_cols == 0 || out_rows == 0 {
1674            return Err(RasterError::InvalidDimensions {
1675                cols: out_cols,
1676                rows: out_rows,
1677            });
1678        }
1679
1680        let cfg = RasterConfig {
1681            cols: out_cols,
1682            rows: out_rows,
1683            bands: self.bands,
1684            x_min: out_extent.x_min,
1685            y_min: out_extent.y_min,
1686            cell_size: (out_extent.x_max - out_extent.x_min) / out_cols as f64,
1687            cell_size_y: Some((out_extent.y_max - out_extent.y_min) / out_rows as f64),
1688            nodata: self.nodata,
1689            data_type: self.data_type,
1690            crs: CrsInfo::from_epsg(options.dst_epsg),
1691            metadata: self.metadata.clone(),
1692        };
1693        let mut out = Raster::new(cfg);
1694
1695        let out_y_max = out.y_max();
1696        let footprint_ring = if options.destination_footprint == DestinationFootprint::SourceBoundary {
1697            let ring = transformed_boundary_ring_samples(
1698                src_crs,
1699                dst_crs,
1700                src_extent,
1701                samples_per_edge,
1702                options.dst_epsg,
1703                options.antimeridian_policy,
1704            )?;
1705            if ring.len() >= 3 {
1706                Some(ring)
1707            } else {
1708                None
1709            }
1710        } else {
1711            None
1712        };
1713
1714        let total_rows = out.rows;
1715        let completed_rows = AtomicUsize::new(0);
1716        let rows_data: Vec<Vec<Option<f64>>> = (0..out.rows as isize)
1717            .into_par_iter()
1718            .map(|row| {
1719                let mut row_values = vec![None; out.cols * out.bands];
1720                let y = out_y_max - (row as f64 + 0.5) * out.cell_size_y;
1721
1722                // Collect per-row coordinates that pass the footprint check, then
1723                // transform them all in one batch call so the SIMD fast paths in
1724                // transform_to_batch can vectorize across the full row width.
1725                let mut batch_coords: Vec<(f64, f64)> = Vec::with_capacity(out.cols);
1726                let mut batch_cols: Vec<usize> = Vec::with_capacity(out.cols);
1727                for col in 0..out.cols as isize {
1728                    let x = out.x_min + (col as f64 + 0.5) * out.cell_size_x;
1729                    if let Some(ring) = &footprint_ring {
1730                        if !point_in_polygon(x, y, ring) {
1731                            continue;
1732                        }
1733                    }
1734                    batch_coords.push((x, y));
1735                    batch_cols.push(col as usize);
1736                }
1737
1738                // Single batch CRS transform for all eligible pixels in this row.
1739                // Successful transforms overwrite batch_coords in-place; errors are
1740                // returned as Some(Err(_)) at the corresponding index.
1741                let errors = dst_crs.transform_to_batch(&mut batch_coords, src_crs);
1742
1743                for (i, &col) in batch_cols.iter().enumerate() {
1744                    if errors[i].is_some() {
1745                        continue;
1746                    }
1747                    let (sx, sy) = batch_coords[i];
1748                    for band in 0..out.bands as isize {
1749                        if let Some(v) = self.sample_world(
1750                            band,
1751                            sx,
1752                            sy,
1753                            options.resample,
1754                            options.nodata_policy,
1755                        ) {
1756                            row_values[band as usize * out.cols + col] = Some(v);
1757                        }
1758                    }
1759                }
1760
1761                if let Some(progress_cb) = progress {
1762                    let done = completed_rows.fetch_add(1, Ordering::Relaxed) + 1;
1763                    progress_cb(done as f64 / total_rows as f64);
1764                }
1765
1766                row_values
1767            })
1768            .collect();
1769
1770        for (row, row_values) in rows_data.into_iter().enumerate() {
1771            let row = row as isize;
1772            for band in 0..out.bands {
1773                for col in 0..out.cols {
1774                    if let Some(v) = row_values[band * out.cols + col] {
1775                        out.set_unchecked(band as isize, row, col as isize, v);
1776                    }
1777                }
1778            }
1779        }
1780
1781        if let Some(progress_cb) = progress {
1782            progress_cb(1.0);
1783        }
1784
1785        Ok(out)
1786    }
1787
1788    /// Sample a raster value at world coordinates using the selected resampling method.
1789    pub fn sample_world(
1790        &self,
1791        band: isize,
1792        x: f64,
1793        y: f64,
1794        method: ResampleMethod,
1795        nodata_policy: NodataPolicy,
1796    ) -> Option<f64> {
1797        let col_f = (x - self.x_min) / self.cell_size_x - 0.5;
1798        let row_f = (self.y_max() - y) / self.cell_size_y - 0.5;
1799        match method {
1800            ResampleMethod::Nearest => self.sample_nearest_pixel(band, col_f, row_f),
1801            ResampleMethod::Bilinear => match nodata_policy {
1802                NodataPolicy::Strict => self.sample_bilinear_strict_pixel(band, col_f, row_f),
1803                NodataPolicy::PartialKernel => {
1804                    self.sample_bilinear_partial_pixel(band, col_f, row_f)
1805                }
1806                NodataPolicy::Fill => self
1807                    .sample_bilinear_strict_pixel(band, col_f, row_f)
1808                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
1809            },
1810            ResampleMethod::Cubic => match nodata_policy {
1811                NodataPolicy::Strict => self.sample_cubic_strict_pixel(band, col_f, row_f),
1812                NodataPolicy::PartialKernel => self.sample_cubic_partial_pixel(band, col_f, row_f),
1813                NodataPolicy::Fill => self
1814                    .sample_cubic_strict_pixel(band, col_f, row_f)
1815                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
1816            },
1817            ResampleMethod::Lanczos => match nodata_policy {
1818                NodataPolicy::Strict => self.sample_lanczos_strict_pixel(band, col_f, row_f),
1819                NodataPolicy::PartialKernel => {
1820                    self.sample_lanczos_partial_pixel(band, col_f, row_f)
1821                }
1822                NodataPolicy::Fill => self
1823                    .sample_lanczos_strict_pixel(band, col_f, row_f)
1824                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
1825            },
1826            ResampleMethod::Average => self.sample_window_stat_pixel(
1827                band,
1828                col_f,
1829                row_f,
1830                WindowStat::Mean,
1831                nodata_policy,
1832            ),
1833            ResampleMethod::Min => self.sample_window_stat_pixel(
1834                band,
1835                col_f,
1836                row_f,
1837                WindowStat::Min,
1838                nodata_policy,
1839            ),
1840            ResampleMethod::Max => self.sample_window_stat_pixel(
1841                band,
1842                col_f,
1843                row_f,
1844                WindowStat::Max,
1845                nodata_policy,
1846            ),
1847            ResampleMethod::Mode => self.sample_window_stat_pixel(
1848                band,
1849                col_f,
1850                row_f,
1851                WindowStat::Mode,
1852                nodata_policy,
1853            ),
1854            ResampleMethod::Median => self.sample_window_stat_pixel(
1855                band,
1856                col_f,
1857                row_f,
1858                WindowStat::Median,
1859                nodata_policy,
1860            ),
1861            ResampleMethod::StdDev => self.sample_window_stat_pixel(
1862                band,
1863                col_f,
1864                row_f,
1865                WindowStat::StdDev,
1866                nodata_policy,
1867            ),
1868        }
1869    }
1870
1871    fn sample_window_stat_pixel(
1872        &self,
1873        band: isize,
1874        col_f: f64,
1875        row_f: f64,
1876        stat: WindowStat,
1877        nodata_policy: NodataPolicy,
1878    ) -> Option<f64> {
1879        if !col_f.is_finite() || !row_f.is_finite() {
1880            return None;
1881        }
1882
1883        let center_col = col_f.round() as isize;
1884        let center_row = row_f.round() as isize;
1885        let mut values = Vec::with_capacity(9);
1886        let mut valid_count = 0usize;
1887
1888        for dy in -1..=1 {
1889            for dx in -1..=1 {
1890                let c = center_col + dx;
1891                let r = center_row + dy;
1892                if c < 0 || r < 0 || c >= self.cols as isize || r >= self.rows as isize {
1893                    continue;
1894                }
1895                if let Some(v) = self.get_opt(band, r, c) {
1896                    values.push(v);
1897                    valid_count += 1;
1898                }
1899            }
1900        }
1901
1902        match nodata_policy {
1903            NodataPolicy::Strict if valid_count < 9 => None,
1904            NodataPolicy::PartialKernel | NodataPolicy::Strict => {
1905                reduce_window_values(&values, stat)
1906            }
1907            NodataPolicy::Fill => reduce_window_values(&values, stat)
1908                .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
1909        }
1910    }
1911
1912    fn sample_nearest_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
1913        if !col_f.is_finite() || !row_f.is_finite() {
1914            return None;
1915        }
1916        let col = col_f.round() as isize;
1917        let row = row_f.round() as isize;
1918        self.get_opt(band, row, col)
1919    }
1920
1921    fn sample_bilinear_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
1922        if !col_f.is_finite() || !row_f.is_finite() {
1923            return None;
1924        }
1925        let c0 = col_f.floor() as isize;
1926        let r0 = row_f.floor() as isize;
1927        let c1 = c0 + 1;
1928        let r1 = r0 + 1;
1929        if c0 < 0 || r0 < 0 || c1 >= self.cols as isize || r1 >= self.rows as isize {
1930            return None;
1931        }
1932
1933        let tx = col_f - c0 as f64;
1934        let ty = row_f - r0 as f64;
1935
1936        if let Some(values) = self.data_f64() {
1937            return self.sample_bilinear_strict_simd_f64(values, band, r0, c0, tx, ty);
1938        }
1939        if let Some(values) = self.data_f32() {
1940            return self.sample_bilinear_strict_simd_f32(values, band, r0, c0, tx, ty);
1941        }
1942
1943        let q00 = self.get_opt(band, r0, c0)?;
1944        let q10 = self.get_opt(band, r0, c1)?;
1945        let q01 = self.get_opt(band, r1, c0)?;
1946        let q11 = self.get_opt(band, r1, c1)?;
1947
1948        let a = q00 * (1.0 - tx) + q10 * tx;
1949        let b = q01 * (1.0 - tx) + q11 * tx;
1950        Some(a * (1.0 - ty) + b * ty)
1951    }
1952
1953    #[inline]
1954    fn sample_bilinear_strict_simd_f64(
1955        &self,
1956        values: &[f64],
1957        band: isize,
1958        r0: isize,
1959        c0: isize,
1960        tx: f64,
1961        ty: f64,
1962    ) -> Option<f64> {
1963        let band_stride = self.rows * self.cols;
1964        let base = band as usize * band_stride + r0 as usize * self.cols + c0 as usize;
1965        let q00 = values[base];
1966        let q10 = values[base + 1];
1967        let q01 = values[base + self.cols];
1968        let q11 = values[base + self.cols + 1];
1969
1970        if self.is_nodata(q00)
1971            || self.is_nodata(q10)
1972            || self.is_nodata(q01)
1973            || self.is_nodata(q11)
1974        {
1975            return None;
1976        }
1977
1978        let weights = f64x4::new([
1979            (1.0 - tx) * (1.0 - ty),
1980            tx * (1.0 - ty),
1981            (1.0 - tx) * ty,
1982            tx * ty,
1983        ]);
1984        let vals = f64x4::new([q00, q10, q01, q11]);
1985        let weighted = <[f64; 4]>::from(vals * weights);
1986        Some(weighted.into_iter().sum())
1987    }
1988
1989    #[inline]
1990    fn sample_bilinear_strict_simd_f32(
1991        &self,
1992        values: &[f32],
1993        band: isize,
1994        r0: isize,
1995        c0: isize,
1996        tx: f64,
1997        ty: f64,
1998    ) -> Option<f64> {
1999        let band_stride = self.rows * self.cols;
2000        let base = band as usize * band_stride + r0 as usize * self.cols + c0 as usize;
2001        let q00 = values[base] as f64;
2002        let q10 = values[base + 1] as f64;
2003        let q01 = values[base + self.cols] as f64;
2004        let q11 = values[base + self.cols + 1] as f64;
2005
2006        if self.is_nodata(q00)
2007            || self.is_nodata(q10)
2008            || self.is_nodata(q01)
2009            || self.is_nodata(q11)
2010        {
2011            return None;
2012        }
2013
2014        let weights = f64x4::new([
2015            (1.0 - tx) * (1.0 - ty),
2016            tx * (1.0 - ty),
2017            (1.0 - tx) * ty,
2018            tx * ty,
2019        ]);
2020        let vals = f64x4::new([q00, q10, q01, q11]);
2021        let weighted = <[f64; 4]>::from(vals * weights);
2022        Some(weighted.into_iter().sum())
2023    }
2024
2025    fn sample_bilinear_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2026        if !col_f.is_finite() || !row_f.is_finite() {
2027            return None;
2028        }
2029        let c0 = col_f.floor() as isize;
2030        let r0 = row_f.floor() as isize;
2031        let c1 = c0 + 1;
2032        let r1 = r0 + 1;
2033
2034        let tx = col_f - c0 as f64;
2035        let ty = row_f - r0 as f64;
2036
2037        let neighbors = [
2038            (c0, r0, (1.0 - tx) * (1.0 - ty)),
2039            (c1, r0, tx * (1.0 - ty)),
2040            (c0, r1, (1.0 - tx) * ty),
2041            (c1, r1, tx * ty),
2042        ];
2043
2044        let mut sum = 0.0;
2045        let mut wsum = 0.0;
2046        for (c, r, w) in neighbors {
2047            if w <= 0.0 || c < 0 || r < 0 || c >= self.cols as isize || r >= self.rows as isize {
2048                continue;
2049            }
2050            if let Some(v) = self.get_opt(band, r, c) {
2051                sum += v * w;
2052                wsum += w;
2053            }
2054        }
2055
2056        if wsum > 0.0 {
2057            Some(sum / wsum)
2058        } else {
2059            None
2060        }
2061    }
2062
2063    fn sample_cubic_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2064        if !col_f.is_finite() || !row_f.is_finite() {
2065            return None;
2066        }
2067        let c1 = col_f.floor() as isize;
2068        let r1 = row_f.floor() as isize;
2069        if c1 - 1 < 0
2070            || r1 - 1 < 0
2071            || c1 + 2 >= self.cols as isize
2072            || r1 + 2 >= self.rows as isize
2073        {
2074            return None;
2075        }
2076
2077        let tx = col_f - c1 as f64;
2078        let ty = row_f - r1 as f64;
2079        let wx = cubic_bspline_weights(tx);
2080        let wy = cubic_bspline_weights(ty);
2081
2082        // Use SIMD-accelerated 4×4 dot product
2083        self.sample_cubic_simd_kernel(&wx, &wy, band, c1 - 1, r1 - 1)
2084    }
2085
2086    /// SIMD-accelerated 4×4 kernel dot product for bicubic resampling.
2087    /// Assumes all 16 pixels are in-bounds and valid.
2088    #[inline]
2089    fn sample_cubic_simd_kernel(
2090        &self,
2091        wx: &[f64; 4],
2092        wy: &[f64; 4],
2093        band: isize,
2094        c_start: isize,
2095        r_start: isize,
2096    ) -> Option<f64> {
2097        // Process 4 rows, 4 pixels per row, computing weighted sum using SIMD for horizontal accumulation.
2098        // Load row-wise and multiply by row weights.
2099        let mut row_sums = [0.0_f64; 4];
2100
2101        for (j, _wyj) in wy.iter().enumerate() {
2102            let rr = r_start + j as isize;
2103            let mut row_sum = 0.0;
2104
2105            // Load 4 pixels in row and compute weighted sum
2106            for (i, wxi) in wx.iter().enumerate() {
2107                let cc = c_start + i as isize;
2108                let v = self.get_opt(band, rr, cc)?;
2109                row_sum += v * *wxi;
2110            }
2111
2112            row_sums[j] = row_sum;
2113        }
2114
2115        // Horizontal reduction: multiply by row weights and sum
2116        let mut sum = 0.0;
2117        for (j, wyj) in wy.iter().enumerate() {
2118            sum += row_sums[j] * *wyj;
2119        }
2120
2121        Some(sum)
2122    }
2123
2124    fn sample_cubic_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2125        if !col_f.is_finite() || !row_f.is_finite() {
2126            return None;
2127        }
2128        let c1 = col_f.floor() as isize;
2129        let r1 = row_f.floor() as isize;
2130        let tx = col_f - c1 as f64;
2131        let ty = row_f - r1 as f64;
2132        let wx = cubic_bspline_weights(tx);
2133        let wy = cubic_bspline_weights(ty);
2134
2135        let mut sum = 0.0;
2136        let mut wsum = 0.0;
2137
2138        for (j, wyj) in wy.iter().enumerate() {
2139            let rr = clamp_isize(r1 + j as isize - 1, 0, self.rows as isize - 1);
2140            if *wyj <= 0.0 {
2141                continue;
2142            }
2143            for (i, wxi) in wx.iter().enumerate() {
2144                let cc = clamp_isize(c1 + i as isize - 1, 0, self.cols as isize - 1);
2145                let w = *wxi * *wyj;
2146                if w <= 0.0 {
2147                    continue;
2148                }
2149                if let Some(v) = self.get_opt(band, rr, cc) {
2150                    sum += v * w;
2151                    wsum += w;
2152                }
2153            }
2154        }
2155
2156        if wsum > 0.0 {
2157            Some(sum / wsum)
2158        } else {
2159            None
2160        }
2161    }
2162
2163    fn sample_lanczos_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2164        if !col_f.is_finite() || !row_f.is_finite() {
2165            return None;
2166        }
2167
2168        let c0 = col_f.floor() as isize;
2169        let r0 = row_f.floor() as isize;
2170        if c0 - 2 < 0
2171            || r0 - 2 < 0
2172            || c0 + 3 >= self.cols as isize
2173            || r0 + 3 >= self.rows as isize
2174        {
2175            return None;
2176        }
2177
2178        let wx = lanczos3_weights(col_f, c0);
2179        let wy = lanczos3_weights(row_f, r0);
2180
2181        if let Some(values) = self.data_f64() {
2182            return self.sample_lanczos_strict_simd_f64(values, band, c0, r0, &wx, &wy);
2183        }
2184        if let Some(values) = self.data_f32() {
2185            return self.sample_lanczos_strict_simd_f32(values, band, c0, r0, &wx, &wy);
2186        }
2187
2188        let mut sum = 0.0;
2189        let mut wsum = 0.0;
2190        for (j, wyj) in wy.iter().enumerate() {
2191            let rr = r0 + j as isize - 2;
2192            for (i, wxi) in wx.iter().enumerate() {
2193                let cc = c0 + i as isize - 2;
2194                let v = self.get_opt(band, rr, cc)?;
2195                let w = *wxi * *wyj;
2196                sum += v * w;
2197                wsum += w;
2198            }
2199        }
2200
2201        if wsum.abs() > 1e-12 {
2202            Some(sum / wsum)
2203        } else {
2204            None
2205        }
2206    }
2207
2208    #[inline]
2209    fn sample_lanczos_strict_simd_f64(
2210        &self,
2211        values: &[f64],
2212        band: isize,
2213        c0: isize,
2214        r0: isize,
2215        wx: &[f64; 6],
2216        wy: &[f64; 6],
2217    ) -> Option<f64> {
2218        let band_stride = self.rows * self.cols;
2219        let base = band as usize * band_stride;
2220
2221        let wx0 = f64x4::new([wx[0], wx[1], wx[2], wx[3]]);
2222        let wx1 = f64x4::new([wx[4], wx[5], 0.0, 0.0]);
2223        let wx_sum: f64 = wx.iter().copied().sum();
2224
2225        let mut sum = 0.0;
2226        let mut wsum = 0.0;
2227        for (j, wyj) in wy.iter().enumerate() {
2228            let rr = (r0 + j as isize - 2) as usize;
2229            let cc = (c0 - 2) as usize;
2230            let row_offset = base + rr * self.cols + cc;
2231
2232            let v = [
2233                values[row_offset],
2234                values[row_offset + 1],
2235                values[row_offset + 2],
2236                values[row_offset + 3],
2237                values[row_offset + 4],
2238                values[row_offset + 5],
2239            ];
2240            if v.into_iter().any(|cell| self.is_nodata(cell)) {
2241                return None;
2242            }
2243
2244            let v0 = f64x4::new([v[0], v[1], v[2], v[3]]);
2245            let v1 = f64x4::new([v[4], v[5], 0.0, 0.0]);
2246            let d0 = <[f64; 4]>::from(v0 * wx0);
2247            let d1 = <[f64; 4]>::from(v1 * wx1);
2248            let row_sum = d0.into_iter().sum::<f64>() + d1.into_iter().sum::<f64>();
2249            sum += row_sum * *wyj;
2250            wsum += wx_sum * *wyj;
2251        }
2252
2253        if wsum.abs() > 1e-12 {
2254            Some(sum / wsum)
2255        } else {
2256            None
2257        }
2258    }
2259
2260    #[inline]
2261    fn sample_lanczos_strict_simd_f32(
2262        &self,
2263        values: &[f32],
2264        band: isize,
2265        c0: isize,
2266        r0: isize,
2267        wx: &[f64; 6],
2268        wy: &[f64; 6],
2269    ) -> Option<f64> {
2270        let band_stride = self.rows * self.cols;
2271        let base = band as usize * band_stride;
2272
2273        let wx0 = f64x4::new([wx[0], wx[1], wx[2], wx[3]]);
2274        let wx1 = f64x4::new([wx[4], wx[5], 0.0, 0.0]);
2275        let wx_sum: f64 = wx.iter().copied().sum();
2276
2277        let mut sum = 0.0;
2278        let mut wsum = 0.0;
2279        for (j, wyj) in wy.iter().enumerate() {
2280            let rr = (r0 + j as isize - 2) as usize;
2281            let cc = (c0 - 2) as usize;
2282            let row_offset = base + rr * self.cols + cc;
2283
2284            let v = [
2285                values[row_offset] as f64,
2286                values[row_offset + 1] as f64,
2287                values[row_offset + 2] as f64,
2288                values[row_offset + 3] as f64,
2289                values[row_offset + 4] as f64,
2290                values[row_offset + 5] as f64,
2291            ];
2292            if v.into_iter().any(|cell| self.is_nodata(cell)) {
2293                return None;
2294            }
2295
2296            let v0 = f64x4::new([v[0], v[1], v[2], v[3]]);
2297            let v1 = f64x4::new([v[4], v[5], 0.0, 0.0]);
2298            let d0 = <[f64; 4]>::from(v0 * wx0);
2299            let d1 = <[f64; 4]>::from(v1 * wx1);
2300            let row_sum = d0.into_iter().sum::<f64>() + d1.into_iter().sum::<f64>();
2301            sum += row_sum * *wyj;
2302            wsum += wx_sum * *wyj;
2303        }
2304
2305        if wsum.abs() > 1e-12 {
2306            Some(sum / wsum)
2307        } else {
2308            None
2309        }
2310    }
2311
2312    fn sample_lanczos_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2313        if !col_f.is_finite() || !row_f.is_finite() {
2314            return None;
2315        }
2316
2317        let c0 = col_f.floor() as isize;
2318        let r0 = row_f.floor() as isize;
2319        let wx = lanczos3_weights(col_f, c0);
2320        let wy = lanczos3_weights(row_f, r0);
2321
2322        let mut sum = 0.0;
2323        let mut wsum = 0.0;
2324
2325        for (j, wyj) in wy.iter().enumerate() {
2326            let rr = clamp_isize(r0 + j as isize - 2, 0, self.rows as isize - 1);
2327            for (i, wxi) in wx.iter().enumerate() {
2328                let cc = clamp_isize(c0 + i as isize - 2, 0, self.cols as isize - 1);
2329                let w = *wxi * *wyj;
2330                if w == 0.0 {
2331                    continue;
2332                }
2333                if let Some(v) = self.get_opt(band, rr, cc) {
2334                    sum += v * w;
2335                    wsum += w;
2336                }
2337            }
2338        }
2339
2340        if wsum.abs() > 1e-12 {
2341            Some(sum / wsum)
2342        } else {
2343            None
2344        }
2345    }
2346
2347    // ─── Statistics ───────────────────────────────────────────────────────
2348
2349    fn stats_accumulator_range_with_mode(
2350        &self,
2351        start: usize,
2352        end: usize,
2353        mode: StatisticsComputationMode,
2354    ) -> StatsAccumulator {
2355        match mode {
2356            StatisticsComputationMode::Auto | StatisticsComputationMode::Simd => {
2357                self.stats_accumulator_range_simd(start, end)
2358            }
2359            StatisticsComputationMode::Scalar => self.stats_accumulator_range_scalar(start, end),
2360        }
2361    }
2362
2363    fn stats_accumulator_range_scalar(&self, start: usize, end: usize) -> StatsAccumulator {
2364        let mut accumulator = StatsAccumulator::default();
2365
2366        for idx in start..end {
2367            let value = self.data.get_f64(idx);
2368            if self.is_nodata(value) {
2369                accumulator.nodata_count += 1;
2370            } else {
2371                accumulator.min = accumulator.min.min(value);
2372                accumulator.max = accumulator.max.max(value);
2373                accumulator.sum += value;
2374                accumulator.sum_sq += value * value;
2375                accumulator.valid_count += 1;
2376            }
2377        }
2378
2379        accumulator
2380    }
2381
2382    /// SIMD-accelerated stats accumulation for a range of indices.
2383    /// Processes groups of 4 values in parallel where possible, falling back to scalar for remainder.
2384    /// This is automatically used by the statistics pipeline for better performance.
2385    fn stats_accumulator_range_simd(&self, start: usize, end: usize) -> StatsAccumulator {
2386        if let Some(values) = self.data_f64() {
2387            return self.stats_accumulator_range_simd_f64(values, start, end);
2388        }
2389        if let Some(values) = self.data_f32() {
2390            return self.stats_accumulator_range_simd_f32(values, start, end);
2391        }
2392
2393        self.stats_accumulator_range_scalar(start, end)
2394    }
2395
2396    fn stats_accumulator_range_simd_f64(
2397        &self,
2398        values: &[f64],
2399        start: usize,
2400        end: usize,
2401    ) -> StatsAccumulator {
2402        let mut accumulator = StatsAccumulator::default();
2403        let nd = self.nodata;
2404
2405        let simd_end = start + ((end - start) / 4) * 4;
2406        let mut simd_min = f64x4::splat(f64::INFINITY);
2407        let mut simd_max = f64x4::splat(f64::NEG_INFINITY);
2408        let mut simd_sum = f64x4::splat(0.0);
2409        let mut simd_sum_sq = f64x4::splat(0.0);
2410        let zero_v = f64x4::splat(0.0);
2411        let nd_v = f64x4::splat(nd);
2412        let inf_v = f64x4::splat(f64::INFINITY);
2413        let neg_inf_v = f64x4::splat(f64::NEG_INFINITY);
2414
2415        let mut idx = start;
2416        while idx < simd_end {
2417            let chunk = &values[idx..idx + 4];
2418            let values = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
2419
2420            let not_nodata = values.simd_ne(nd_v);
2421
2422            let values_for_min = not_nodata.blend(values, inf_v);
2423            let values_for_max = not_nodata.blend(values, neg_inf_v);
2424            simd_min = simd_min.min(values_for_min);
2425            simd_max = simd_max.max(values_for_max);
2426
2427            let masked_values = not_nodata.blend(values, zero_v);
2428            simd_sum = simd_sum + masked_values;
2429            simd_sum_sq = simd_sum_sq + masked_values * masked_values;
2430
2431            for &val in chunk {
2432                if val == nd {
2433                    accumulator.nodata_count += 1;
2434                } else {
2435                    accumulator.valid_count += 1;
2436                }
2437            }
2438
2439            idx += 4;
2440        }
2441
2442        let sum_arr = <[f64; 4]>::from(simd_sum);
2443        let sum_sq_arr = <[f64; 4]>::from(simd_sum_sq);
2444        let min_arr = <[f64; 4]>::from(simd_min);
2445        let max_arr = <[f64; 4]>::from(simd_max);
2446
2447        for i in 0..4 {
2448            accumulator.sum += sum_arr[i];
2449            accumulator.sum_sq += sum_sq_arr[i];
2450            accumulator.min = accumulator.min.min(min_arr[i]);
2451            accumulator.max = accumulator.max.max(max_arr[i]);
2452        }
2453
2454        for &value in &values[simd_end..end] {
2455            if value == nd {
2456                accumulator.nodata_count += 1;
2457            } else {
2458                accumulator.min = accumulator.min.min(value);
2459                accumulator.max = accumulator.max.max(value);
2460                accumulator.sum += value;
2461                accumulator.sum_sq += value * value;
2462                accumulator.valid_count += 1;
2463            }
2464        }
2465
2466        accumulator
2467    }
2468
2469    fn stats_accumulator_range_simd_f32(
2470        &self,
2471        values: &[f32],
2472        start: usize,
2473        end: usize,
2474    ) -> StatsAccumulator {
2475        let mut accumulator = StatsAccumulator::default();
2476        let nd = self.nodata as f32;
2477
2478        let simd_end = start + ((end - start) / 4) * 4;
2479        let mut simd_min = f64x4::splat(f64::INFINITY);
2480        let mut simd_max = f64x4::splat(f64::NEG_INFINITY);
2481        let mut simd_sum = f64x4::splat(0.0);
2482        let mut simd_sum_sq = f64x4::splat(0.0);
2483        let zero_v = f64x4::splat(0.0);
2484        let nd_v = f64x4::splat(nd as f64);
2485        let inf_v = f64x4::splat(f64::INFINITY);
2486        let neg_inf_v = f64x4::splat(f64::NEG_INFINITY);
2487
2488        let mut idx = start;
2489        while idx < simd_end {
2490            let chunk = &values[idx..idx + 4];
2491            let values = f64x4::new([
2492                chunk[0] as f64,
2493                chunk[1] as f64,
2494                chunk[2] as f64,
2495                chunk[3] as f64,
2496            ]);
2497
2498            let not_nodata = values.simd_ne(nd_v);
2499            let values_for_min = not_nodata.blend(values, inf_v);
2500            let values_for_max = not_nodata.blend(values, neg_inf_v);
2501            simd_min = simd_min.min(values_for_min);
2502            simd_max = simd_max.max(values_for_max);
2503
2504            let masked_values = not_nodata.blend(values, zero_v);
2505            simd_sum = simd_sum + masked_values;
2506            simd_sum_sq = simd_sum_sq + masked_values * masked_values;
2507
2508            for &val in chunk {
2509                if val == nd {
2510                    accumulator.nodata_count += 1;
2511                } else {
2512                    accumulator.valid_count += 1;
2513                }
2514            }
2515
2516            idx += 4;
2517        }
2518
2519        let sum_arr = <[f64; 4]>::from(simd_sum);
2520        let sum_sq_arr = <[f64; 4]>::from(simd_sum_sq);
2521        let min_arr = <[f64; 4]>::from(simd_min);
2522        let max_arr = <[f64; 4]>::from(simd_max);
2523
2524        for i in 0..4 {
2525            accumulator.sum += sum_arr[i];
2526            accumulator.sum_sq += sum_sq_arr[i];
2527            accumulator.min = accumulator.min.min(min_arr[i]);
2528            accumulator.max = accumulator.max.max(max_arr[i]);
2529        }
2530
2531        for &value in &values[simd_end..end] {
2532            if value == nd {
2533                accumulator.nodata_count += 1;
2534            } else {
2535                let value = value as f64;
2536                accumulator.min = accumulator.min.min(value);
2537                accumulator.max = accumulator.max.max(value);
2538                accumulator.sum += value;
2539                accumulator.sum_sq += value * value;
2540                accumulator.valid_count += 1;
2541            }
2542        }
2543
2544        accumulator
2545    }
2546
2547    fn statistics_for_index_range_with_mode(
2548        &self,
2549        start: usize,
2550        end: usize,
2551        mode: StatisticsComputationMode,
2552    ) -> Statistics {
2553        const PARALLEL_THRESHOLD: usize = 65_536;
2554        const CHUNK_SIZE: usize = 16_384;
2555
2556        let span = end.saturating_sub(start);
2557        if span == 0 {
2558            return Statistics {
2559                min: 0.0,
2560                max: 0.0,
2561                mean: 0.0,
2562                std_dev: 0.0,
2563                valid_count: 0,
2564                nodata_count: 0,
2565            };
2566        }
2567
2568        let total = if span < PARALLEL_THRESHOLD {
2569            self.stats_accumulator_range_with_mode(start, end, mode)
2570        } else {
2571            let chunk_starts: Vec<usize> = (start..end).step_by(CHUNK_SIZE).collect();
2572            let partials: Vec<StatsAccumulator> = chunk_starts
2573                .into_par_iter()
2574                .map(|chunk_start| {
2575                    let chunk_end = (chunk_start + CHUNK_SIZE).min(end);
2576                    self.stats_accumulator_range_with_mode(chunk_start, chunk_end, mode)
2577                })
2578                .collect();
2579
2580            partials.into_iter().fold(StatsAccumulator::default(), |mut lhs, rhs| {
2581                lhs.merge(rhs);
2582                lhs
2583            })
2584        };
2585
2586        total.to_statistics()
2587    }
2588
2589    /// Compute basic statistics over all valid (non-nodata) cells.
2590    pub fn statistics(&self) -> Statistics {
2591        self.statistics_with_mode(StatisticsComputationMode::Auto)
2592    }
2593
2594    /// Compute basic statistics over all valid (non-nodata) cells using a selected computation path.
2595    pub fn statistics_with_mode(&self, mode: StatisticsComputationMode) -> Statistics {
2596        self.statistics_for_index_range_with_mode(0, self.data.len(), mode)
2597    }
2598
2599    /// Compute basic statistics over all valid (non-nodata) cells in one band.
2600    pub fn statistics_band(&self, band: isize) -> Result<Statistics> {
2601        self.statistics_band_with_mode(band, StatisticsComputationMode::Auto)
2602    }
2603
2604    /// Compute basic statistics over one band using a selected computation path.
2605    pub fn statistics_band_with_mode(
2606        &self,
2607        band: isize,
2608        mode: StatisticsComputationMode,
2609    ) -> Result<Statistics> {
2610        if band < 0 || band >= self.bands as isize {
2611            return Err(RasterError::OutOfBounds {
2612                band,
2613                col: 0,
2614                row: 0,
2615                bands: self.bands,
2616                cols: self.cols,
2617                rows: self.rows,
2618            });
2619        }
2620
2621        let band = band as usize;
2622        let band_stride = self.rows * self.cols;
2623        let start = band * band_stride;
2624        let end = start + band_stride;
2625
2626        Ok(self.statistics_for_index_range_with_mode(start, end, mode))
2627    }
2628
2629    // ─── Row iteration ────────────────────────────────────────────────────
2630
2631    /// Return a copy of one full band as row-major values.
2632    #[inline]
2633    pub fn band_slice(&self, band: isize) -> Vec<f64> {
2634        if band < 0 || band >= self.bands as isize {
2635            return Vec::new();
2636        }
2637        let band_stride = self.rows * self.cols;
2638        let start = band as usize * band_stride;
2639        (start..start + band_stride)
2640            .map(|i| self.data.get_f64(i))
2641            .collect()
2642    }
2643
2644    /// Set one full band from row-major values.
2645    pub fn set_band_slice(&mut self, band: isize, values: &[f64]) -> Result<()> {
2646        let expected = self.rows * self.cols;
2647        if values.len() != expected {
2648            return Err(RasterError::InvalidDimensions {
2649                cols: self.cols,
2650                rows: values.len(),
2651            });
2652        }
2653        if band < 0 || band >= self.bands as isize {
2654            return Err(RasterError::OutOfBounds {
2655                band,
2656                col: 0,
2657                row: 0,
2658                bands: self.bands,
2659                cols: self.cols,
2660                rows: self.rows,
2661            });
2662        }
2663        let band_stride = self.rows * self.cols;
2664        let start = band as usize * band_stride;
2665        for (i, v) in values.iter().copied().enumerate() {
2666            self.data.set_f64(start + i, v);
2667        }
2668        Ok(())
2669    }
2670
2671    /// Return a slice of the raw data for signed `(band, row)`.
2672    #[inline]
2673    pub fn row_slice(&self, band: isize, row: isize) -> Vec<f64> {
2674        if band < 0 || band >= self.bands as isize || row < 0 || row >= self.rows as isize {
2675            return Vec::new();
2676        }
2677        let band_stride = self.rows * self.cols;
2678        let start = band as usize * band_stride + row as usize * self.cols;
2679        (start..start + self.cols).map(|i| self.data.get_f64(i)).collect()
2680    }
2681
2682    /// Set all values in signed `(band, row)` from an `f64` slice.
2683    pub fn set_row_slice(&mut self, band: isize, row: isize, values: &[f64]) -> Result<()> {
2684        if values.len() != self.cols {
2685            return Err(RasterError::InvalidDimensions { cols: values.len(), rows: self.rows });
2686        }
2687        if band < 0 || band >= self.bands as isize || row < 0 || row >= self.rows as isize {
2688            return Err(RasterError::OutOfBounds {
2689                band,
2690                col: 0,
2691                row,
2692                bands: self.bands,
2693                cols: self.cols,
2694                rows: self.rows,
2695            });
2696        }
2697        let band_stride = self.rows * self.cols;
2698        let start = band as usize * band_stride + row as usize * self.cols;
2699        for (i, v) in values.iter().copied().enumerate() {
2700            self.data.set_f64(start + i, v);
2701        }
2702        Ok(())
2703    }
2704
2705    /// Iterate over signed `(band, row, col, value)` for all valid cells.
2706    pub fn iter_valid(&self) -> impl Iterator<Item = (isize, isize, isize, f64)> + '_ {
2707        self.data.iter_f64().enumerate().filter_map(move |(idx, v)| {
2708            if self.is_nodata(v) {
2709                None
2710            } else {
2711                let band_stride = self.rows * self.cols;
2712                let band = idx / band_stride;
2713                let rem = idx % band_stride;
2714                let row = rem / self.cols;
2715                let col = rem % self.cols;
2716                Some((band as isize, row as isize, col as isize, v))
2717            }
2718        })
2719    }
2720
2721    /// Iterate over signed `(row, col, value)` for all valid cells in one band.
2722    pub fn iter_valid_band(
2723        &self,
2724        band: isize,
2725    ) -> Result<Box<dyn Iterator<Item = (isize, isize, f64)> + '_>> {
2726        if band < 0 || band >= self.bands as isize {
2727            return Err(RasterError::OutOfBounds {
2728                band,
2729                col: 0,
2730                row: 0,
2731                bands: self.bands,
2732                cols: self.cols,
2733                rows: self.rows,
2734            });
2735        }
2736
2737        let b = band as usize;
2738        let band_stride = self.rows * self.cols;
2739        let start = b * band_stride;
2740        Ok(Box::new((0..band_stride).filter_map(move |i| {
2741            let v = self.data.get_f64(start + i);
2742            if self.is_nodata(v) {
2743                None
2744            } else {
2745                let row = i / self.cols;
2746                let col = i % self.cols;
2747                Some((row as isize, col as isize, v))
2748            }
2749        })))
2750    }
2751
2752    /// Iterate over row vectors (`Vec<f64>`) for one band from north to south.
2753    pub fn iter_band_rows(&self, band: isize) -> Result<Box<dyn Iterator<Item = Vec<f64>> + '_>> {
2754        if band < 0 || band >= self.bands as isize {
2755            return Err(RasterError::OutOfBounds {
2756                band,
2757                col: 0,
2758                row: 0,
2759                bands: self.bands,
2760                cols: self.cols,
2761                rows: self.rows,
2762            });
2763        }
2764
2765        Ok(Box::new((0..self.rows).map(move |row| self.row_slice(band, row as isize))))
2766    }
2767
2768    /// Traverse mutable native row slices for one band from north to south.
2769    ///
2770    /// This is a zero-allocation fast path for in-place row-wise processing.
2771    /// The callback receives `(row_index, typed_row_slice)`.
2772    pub fn for_each_band_row_mut<F>(&mut self, band: isize, mut f: F) -> Result<()>
2773    where
2774        F: FnMut(isize, RasterRowMut<'_>),
2775    {
2776        if band < 0 || band >= self.bands as isize {
2777            return Err(RasterError::OutOfBounds {
2778                band,
2779                col: 0,
2780                row: 0,
2781                bands: self.bands,
2782                cols: self.cols,
2783                rows: self.rows,
2784            });
2785        }
2786
2787        let b = band as usize;
2788        let band_stride = self.rows * self.cols;
2789        let start = b * band_stride;
2790        let end = start + band_stride;
2791
2792        match &mut self.data {
2793            RasterData::U8(v) => {
2794                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2795                    f(row as isize, RasterRowMut::U8(chunk));
2796                }
2797            }
2798            RasterData::I8(v) => {
2799                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2800                    f(row as isize, RasterRowMut::I8(chunk));
2801                }
2802            }
2803            RasterData::U16(v) => {
2804                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2805                    f(row as isize, RasterRowMut::U16(chunk));
2806                }
2807            }
2808            RasterData::I16(v) => {
2809                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2810                    f(row as isize, RasterRowMut::I16(chunk));
2811                }
2812            }
2813            RasterData::U32(v) => {
2814                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2815                    f(row as isize, RasterRowMut::U32(chunk));
2816                }
2817            }
2818            RasterData::I32(v) => {
2819                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2820                    f(row as isize, RasterRowMut::I32(chunk));
2821                }
2822            }
2823            RasterData::U64(v) => {
2824                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2825                    f(row as isize, RasterRowMut::U64(chunk));
2826                }
2827            }
2828            RasterData::I64(v) => {
2829                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2830                    f(row as isize, RasterRowMut::I64(chunk));
2831                }
2832            }
2833            RasterData::F32(v) => {
2834                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2835                    f(row as isize, RasterRowMut::F32(chunk));
2836                }
2837            }
2838            RasterData::F64(v) => {
2839                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
2840                    f(row as isize, RasterRowMut::F64(chunk));
2841                }
2842            }
2843        }
2844
2845        Ok(())
2846    }
2847
2848    /// Traverse immutable native row slices for one band from north to south.
2849    ///
2850    /// This is a zero-allocation fast path for read-only row-wise processing.
2851    /// The callback receives `(row_index, typed_row_slice)`.
2852    pub fn for_each_band_row<F>(&self, band: isize, mut f: F) -> Result<()>
2853    where
2854        F: FnMut(isize, RasterRowRef<'_>),
2855    {
2856        if band < 0 || band >= self.bands as isize {
2857            return Err(RasterError::OutOfBounds {
2858                band,
2859                col: 0,
2860                row: 0,
2861                bands: self.bands,
2862                cols: self.cols,
2863                rows: self.rows,
2864            });
2865        }
2866
2867        let b = band as usize;
2868        let band_stride = self.rows * self.cols;
2869        let start = b * band_stride;
2870        let end = start + band_stride;
2871
2872        match &self.data {
2873            RasterData::U8(v) => {
2874                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2875                    f(row as isize, RasterRowRef::U8(chunk));
2876                }
2877            }
2878            RasterData::I8(v) => {
2879                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2880                    f(row as isize, RasterRowRef::I8(chunk));
2881                }
2882            }
2883            RasterData::U16(v) => {
2884                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2885                    f(row as isize, RasterRowRef::U16(chunk));
2886                }
2887            }
2888            RasterData::I16(v) => {
2889                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2890                    f(row as isize, RasterRowRef::I16(chunk));
2891                }
2892            }
2893            RasterData::U32(v) => {
2894                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2895                    f(row as isize, RasterRowRef::U32(chunk));
2896                }
2897            }
2898            RasterData::I32(v) => {
2899                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2900                    f(row as isize, RasterRowRef::I32(chunk));
2901                }
2902            }
2903            RasterData::U64(v) => {
2904                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2905                    f(row as isize, RasterRowRef::U64(chunk));
2906                }
2907            }
2908            RasterData::I64(v) => {
2909                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2910                    f(row as isize, RasterRowRef::I64(chunk));
2911                }
2912            }
2913            RasterData::F32(v) => {
2914                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2915                    f(row as isize, RasterRowRef::F32(chunk));
2916                }
2917            }
2918            RasterData::F64(v) => {
2919                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
2920                    f(row as isize, RasterRowRef::F64(chunk));
2921                }
2922            }
2923        }
2924
2925        Ok(())
2926    }
2927
2928    // ─── Transformation ───────────────────────────────────────────────────
2929
2930    /// Fill all cells with `value`.
2931    pub fn fill(&mut self, value: f64) {
2932        for i in 0..self.data.len() {
2933            self.data.set_f64(i, value);
2934        }
2935    }
2936
2937    /// Fill all cells with the nodata value.
2938    pub fn fill_nodata(&mut self) {
2939        let nd = self.nodata;
2940        self.fill(nd);
2941    }
2942
2943    /// Apply a function to every valid (non-nodata) cell value in-place.
2944    pub fn map_valid<F: Fn(f64) -> f64>(&mut self, f: F) {
2945        let nd = self.nodata;
2946        let nodata_nan = nd.is_nan();
2947        for i in 0..self.data.len() {
2948            let v = self.data.get_f64(i);
2949            let is_nd = if nodata_nan { v.is_nan() } else { (v - nd).abs() < 1e-10 * nd.abs().max(1.0) };
2950            if !is_nd {
2951                self.data.set_f64(i, f(v));
2952            }
2953        }
2954    }
2955
2956    /// Apply a function to every valid (non-nodata) cell value in one band in-place.
2957    pub fn map_valid_band<F: Fn(f64) -> f64>(&mut self, band: isize, f: F) -> Result<()> {
2958        if band < 0 || band >= self.bands as isize {
2959            return Err(RasterError::OutOfBounds {
2960                band,
2961                col: 0,
2962                row: 0,
2963                bands: self.bands,
2964                cols: self.cols,
2965                rows: self.rows,
2966            });
2967        }
2968
2969        let nd = self.nodata;
2970        let nodata_nan = nd.is_nan();
2971        let band_stride = self.rows * self.cols;
2972        let start = band as usize * band_stride;
2973        let end = start + band_stride;
2974
2975        for i in start..end {
2976            let v = self.data.get_f64(i);
2977            let is_nd = if nodata_nan {
2978                v.is_nan()
2979            } else {
2980                (v - nd).abs() < 1e-10 * nd.abs().max(1.0)
2981            };
2982            if !is_nd {
2983                self.data.set_f64(i, f(v));
2984            }
2985        }
2986        Ok(())
2987    }
2988
2989    /// Replace all occurrences of `from` with `to` in the data buffer.
2990    pub fn replace(&mut self, from: f64, to: f64) {
2991        for i in 0..self.data.len() {
2992            let v = self.data.get_f64(i);
2993            if (v - from).abs() < f64::EPSILON {
2994                self.data.set_f64(i, to);
2995            }
2996        }
2997    }
2998
2999    /// Replace all occurrences of `from` with `to` in one band.
3000    pub fn replace_band(&mut self, band: isize, from: f64, to: f64) -> Result<()> {
3001        if band < 0 || band >= self.bands as isize {
3002            return Err(RasterError::OutOfBounds {
3003                band,
3004                col: 0,
3005                row: 0,
3006                bands: self.bands,
3007                cols: self.cols,
3008                rows: self.rows,
3009            });
3010        }
3011
3012        let band_stride = self.rows * self.cols;
3013        let start = band as usize * band_stride;
3014        let end = start + band_stride;
3015        for i in start..end {
3016            let v = self.data.get_f64(i);
3017            if (v - from).abs() < f64::EPSILON {
3018                self.data.set_f64(i, to);
3019            }
3020        }
3021        Ok(())
3022    }
3023
3024    // ─── I/O ──────────────────────────────────────────────────────────────
3025
3026    /// Read a raster from `path`, detecting the format automatically.
3027    pub fn read<P: AsRef<Path>>(path: P) -> Result<Self> {
3028        let path = path.as_ref().to_string_lossy().to_string();
3029        let fmt = RasterFormat::detect(&path)?;
3030        fmt.read(&path)
3031    }
3032
3033    /// Read a raster from `path` using the specified format.
3034    pub fn read_with_format<P: AsRef<Path>>(path: P, fmt: RasterFormat) -> Result<Self> {
3035        let path = path.as_ref().to_string_lossy().to_string();
3036        fmt.read(&path)
3037    }
3038
3039    /// Write this raster to `path`, detecting the format from the extension.
3040    pub fn write<P: AsRef<Path>>(&self, path: P, fmt: RasterFormat) -> Result<()> {
3041        let path = path.as_ref().to_string_lossy().to_string();
3042        fmt.write(self, &path)
3043    }
3044
3045    /// Write this raster, auto-detecting the format from the file extension.
3046    pub fn write_auto<P: AsRef<Path>>(&self, path: P) -> Result<()> {
3047        let path = path.as_ref().to_string_lossy().to_string();
3048        let fmt = RasterFormat::detect(&path)?;
3049        fmt.write(self, &path)
3050    }
3051
3052    /// Write this raster as GeoTIFF/BigTIFF/COG using typed options.
3053    pub fn write_geotiff_with_options<P: AsRef<Path>>(
3054        &self,
3055        path: P,
3056        opts: &crate::formats::geotiff::GeoTiffWriteOptions,
3057    ) -> Result<()> {
3058        let path = path.as_ref().to_string_lossy().to_string();
3059        crate::formats::geotiff::write_with_options(self, &path, opts)
3060    }
3061
3062    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using
3063    /// convenience defaults.
3064    ///
3065    /// Defaults:
3066    /// - compression: Deflate
3067    /// - BigTIFF: false
3068    /// - COG tile size: 512
3069    pub fn write_cog<P: AsRef<Path>>(&self, path: P) -> Result<()> {
3070        let opts = crate::formats::geotiff::GeoTiffWriteOptions {
3071            compression: Some(crate::formats::geotiff::GeoTiffCompression::Deflate),
3072            bigtiff: Some(false),
3073            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog { tile_size: 512 }),
3074        };
3075        self.write_geotiff_with_options(path, &opts)
3076    }
3077
3078    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using
3079    /// convenience defaults and a custom tile size.
3080    ///
3081    /// Defaults:
3082    /// - compression: Deflate
3083    /// - BigTIFF: false
3084    /// - COG tile size: `tile_size`
3085    pub fn write_cog_with_tile_size<P: AsRef<Path>>(&self, path: P, tile_size: u32) -> Result<()> {
3086        let opts = crate::formats::geotiff::GeoTiffWriteOptions {
3087            compression: Some(crate::formats::geotiff::GeoTiffCompression::Deflate),
3088            bigtiff: Some(false),
3089            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog { tile_size }),
3090        };
3091        self.write_geotiff_with_options(path, &opts)
3092    }
3093
3094    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using COG-focused
3095    /// typed options.
3096    ///
3097    /// Any option set to `None` uses convenience defaults:
3098    /// - compression: Deflate
3099    /// - BigTIFF: false
3100    /// - COG tile size: 512
3101    pub fn write_cog_with_options<P: AsRef<Path>>(
3102        &self,
3103        path: P,
3104        opts: &crate::formats::geotiff::CogWriteOptions,
3105    ) -> Result<()> {
3106        let geotiff_opts = crate::formats::geotiff::GeoTiffWriteOptions {
3107            compression: Some(
3108                opts.compression
3109                    .unwrap_or(crate::formats::geotiff::GeoTiffCompression::Deflate),
3110            ),
3111            bigtiff: Some(opts.bigtiff.unwrap_or(false)),
3112            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog {
3113                tile_size: opts.tile_size.unwrap_or(512),
3114            }),
3115        };
3116        self.write_geotiff_with_options(path, &geotiff_opts)
3117    }
3118
3119    /// Write this raster as JPEG2000/GeoJP2 using typed options.
3120    pub fn write_jpeg2000_with_options<P: AsRef<Path>>(
3121        &self,
3122        path: P,
3123        opts: &crate::formats::jpeg2000::Jpeg2000WriteOptions,
3124    ) -> Result<()> {
3125        let path = path.as_ref().to_string_lossy().to_string();
3126        crate::formats::jpeg2000::write_with_options(self, &path, opts)
3127    }
3128}
3129
3130fn cubic_bspline_weights(t: f64) -> [f64; 4] {
3131    let t = t.clamp(0.0, 1.0);
3132    let t2 = t * t;
3133    let t3 = t2 * t;
3134    [
3135        ((1.0 - t) * (1.0 - t) * (1.0 - t)) / 6.0,
3136        (3.0 * t3 - 6.0 * t2 + 4.0) / 6.0,
3137        (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) / 6.0,
3138        t3 / 6.0,
3139    ]
3140}
3141
3142fn lanczos_kernel(x: f64, a: f64) -> f64 {
3143    if x.abs() < 1e-12 {
3144        return 1.0;
3145    }
3146    if x.abs() >= a {
3147        return 0.0;
3148    }
3149    let pix = std::f64::consts::PI * x;
3150    let pix_over_a = pix / a;
3151    (pix.sin() / pix) * (pix_over_a.sin() / pix_over_a)
3152}
3153
3154fn lanczos3_weights(sample_f: f64, floor_idx: isize) -> [f64; 6] {
3155    let mut w = [0.0_f64; 6];
3156    for (i, wi) in w.iter_mut().enumerate() {
3157        let idx = floor_idx + i as isize - 2;
3158        let dx = sample_f - idx as f64;
3159        *wi = lanczos_kernel(dx, 3.0);
3160    }
3161    w
3162}
3163
3164fn sample_extent_boundary_points(extent: Extent, samples_per_edge: usize) -> Vec<(f64, f64)> {
3165    let n = samples_per_edge.max(1);
3166    let mut pts = Vec::with_capacity(4 * n);
3167
3168    // Bottom and top edges include corners.
3169    for i in 0..=n {
3170        let t = i as f64 / n as f64;
3171        let x = extent.x_min + t * (extent.x_max - extent.x_min);
3172        pts.push((x, extent.y_min));
3173        pts.push((x, extent.y_max));
3174    }
3175
3176    // Left and right edges exclude corners to avoid duplicate corner points.
3177    for j in 1..n {
3178        let t = j as f64 / n as f64;
3179        let y = extent.y_min + t * (extent.y_max - extent.y_min);
3180        pts.push((extent.x_min, y));
3181        pts.push((extent.x_max, y));
3182    }
3183
3184    pts
3185}
3186
3187fn sample_extent_boundary_ring(extent: Extent, samples_per_edge: usize) -> Vec<(f64, f64)> {
3188    let n = samples_per_edge.max(1);
3189    let mut ring = Vec::with_capacity(n * 4);
3190
3191    for i in 0..n {
3192        let t = if n == 1 {
3193            0.0
3194        } else {
3195            i as f64 / (n - 1) as f64
3196        };
3197        let x = extent.x_min + t * (extent.x_max - extent.x_min);
3198        ring.push((x, extent.y_min));
3199    }
3200
3201    for i in 0..n {
3202        let t = if n == 1 {
3203            0.0
3204        } else {
3205            i as f64 / (n - 1) as f64
3206        };
3207        let y = extent.y_min + t * (extent.y_max - extent.y_min);
3208        ring.push((extent.x_max, y));
3209    }
3210
3211    for i in 0..n {
3212        let t = if n == 1 {
3213            0.0
3214        } else {
3215            i as f64 / (n - 1) as f64
3216        };
3217        let x = extent.x_max - t * (extent.x_max - extent.x_min);
3218        ring.push((x, extent.y_max));
3219    }
3220
3221    for i in 0..n {
3222        let t = if n == 1 {
3223            0.0
3224        } else {
3225            i as f64 / (n - 1) as f64
3226        };
3227        let y = extent.y_max - t * (extent.y_max - extent.y_min);
3228        ring.push((extent.x_min, y));
3229    }
3230
3231    ring
3232}
3233
3234fn transformed_extent_from_boundary_samples(
3235    src_crs: &Crs,
3236    dst_crs: &Crs,
3237    src_extent: Extent,
3238    samples_per_edge: usize,
3239    dst_epsg: u32,
3240    antimeridian_policy: AntimeridianPolicy,
3241) -> Result<Extent> {
3242    let points = sample_extent_boundary_points(src_extent, samples_per_edge);
3243
3244    let mut tx_min = f64::INFINITY;
3245    let mut tx_max = f64::NEG_INFINITY;
3246    let mut ty_min = f64::INFINITY;
3247    let mut ty_max = f64::NEG_INFINITY;
3248    let mut tx_values = Vec::new();
3249    let mut valid = 0usize;
3250
3251    for (x, y) in points {
3252        let Ok((tx, ty)) = src_crs.transform_to(x, y, dst_crs) else {
3253            continue;
3254        };
3255        if !tx.is_finite() || !ty.is_finite() {
3256            continue;
3257        }
3258        tx_min = tx_min.min(tx);
3259        tx_max = tx_max.max(tx);
3260        ty_min = ty_min.min(ty);
3261        ty_max = ty_max.max(ty);
3262        tx_values.push(tx);
3263        valid += 1;
3264    }
3265
3266    if valid == 0 {
3267        return Err(RasterError::Other(format!(
3268            "failed to transform source extent boundary samples to EPSG:{}",
3269            dst_epsg
3270        )));
3271    }
3272
3273    if dst_epsg == 4326 {
3274        if let Some((x0, x1)) = antimeridian_aware_longitude_bounds(
3275            &tx_values,
3276            antimeridian_policy,
3277        ) {
3278            tx_min = x0;
3279            tx_max = x1;
3280        }
3281    }
3282
3283    Ok(Extent {
3284        x_min: tx_min,
3285        y_min: ty_min,
3286        x_max: tx_max,
3287        y_max: ty_max,
3288    })
3289}
3290
3291fn transformed_boundary_ring_samples(
3292    src_crs: &Crs,
3293    dst_crs: &Crs,
3294    src_extent: Extent,
3295    samples_per_edge: usize,
3296    dst_epsg: u32,
3297    antimeridian_policy: AntimeridianPolicy,
3298) -> Result<Vec<(f64, f64)>> {
3299    let ring = sample_extent_boundary_ring(src_extent, samples_per_edge);
3300    let mut transformed = Vec::with_capacity(ring.len());
3301
3302    for (x, y) in ring {
3303        let Ok((tx, ty)) = src_crs.transform_to(x, y, dst_crs) else {
3304            continue;
3305        };
3306        if tx.is_finite() && ty.is_finite() {
3307            transformed.push((tx, ty));
3308        }
3309    }
3310
3311    if transformed.len() < 3 {
3312        return Err(RasterError::Other(format!(
3313            "failed to build transformed boundary ring for EPSG:{}",
3314            dst_epsg
3315        )));
3316    }
3317
3318    if dst_epsg == 4326 && antimeridian_policy != AntimeridianPolicy::Linear {
3319        let lons: Vec<f64> = transformed.iter().map(|(x, _)| *x).collect();
3320        if let Some((base, _)) = minimal_wrapped_longitude_bounds(&lons) {
3321            for (x, _) in &mut transformed {
3322                let mut w = wrap_lon_360(*x);
3323                if w < base {
3324                    w += 360.0;
3325                }
3326                *x = w;
3327            }
3328        }
3329    }
3330
3331    Ok(transformed)
3332}
3333
3334fn snap_down_to_origin(value: f64, origin: f64, step: f64) -> f64 {
3335    origin + ((value - origin) / step).floor() * step
3336}
3337
3338fn snap_up_to_origin(value: f64, origin: f64, step: f64) -> f64 {
3339    origin + ((value - origin) / step).ceil() * step
3340}
3341
3342fn wrap_lon_360(lon: f64) -> f64 {
3343    let mut v = lon % 360.0;
3344    if v < 0.0 {
3345        v += 360.0;
3346    }
3347    v
3348}
3349
3350fn minimal_wrapped_longitude_bounds(longitudes: &[f64]) -> Option<(f64, f64)> {
3351    if longitudes.is_empty() {
3352        return None;
3353    }
3354
3355    if longitudes.len() == 1 {
3356        let v = wrap_lon_360(longitudes[0]);
3357        return Some((v, v));
3358    }
3359
3360    let mut values: Vec<f64> = longitudes.iter().map(|v| wrap_lon_360(*v)).collect();
3361    values.sort_by(f64::total_cmp);
3362
3363    let n = values.len();
3364    let mut max_gap = f64::NEG_INFINITY;
3365    let mut max_gap_idx = 0usize;
3366    for i in 0..n {
3367        let next = if i + 1 < n {
3368            values[i + 1]
3369        } else {
3370            values[0] + 360.0
3371        };
3372        let gap = next - values[i];
3373        if gap > max_gap {
3374            max_gap = gap;
3375            max_gap_idx = i;
3376        }
3377    }
3378
3379    let start = values[(max_gap_idx + 1) % n];
3380    let mut end = values[max_gap_idx];
3381    if end < start {
3382        end += 360.0;
3383    }
3384
3385    Some((start, end))
3386}
3387
3388fn antimeridian_aware_longitude_bounds(
3389    longitudes: &[f64],
3390    policy: AntimeridianPolicy,
3391) -> Option<(f64, f64)> {
3392    if longitudes.is_empty() {
3393        return None;
3394    }
3395
3396    let mut lin_min = f64::INFINITY;
3397    let mut lin_max = f64::NEG_INFINITY;
3398    for lon in longitudes {
3399        if !lon.is_finite() {
3400            continue;
3401        }
3402        lin_min = lin_min.min(*lon);
3403        lin_max = lin_max.max(*lon);
3404    }
3405    if !lin_min.is_finite() || !lin_max.is_finite() {
3406        return None;
3407    }
3408
3409    if policy == AntimeridianPolicy::Linear {
3410        return Some((lin_min, lin_max));
3411    }
3412
3413    let Some((wrap_min, wrap_max)) = minimal_wrapped_longitude_bounds(longitudes) else {
3414        return Some((lin_min, lin_max));
3415    };
3416    if policy == AntimeridianPolicy::Wrap {
3417        return Some((wrap_min, wrap_max));
3418    }
3419
3420    let linear_width = lin_max - lin_min;
3421    let wrapped_width = wrap_max - wrap_min;
3422
3423    if wrapped_width + 1e-9 < linear_width {
3424        Some((wrap_min, wrap_max))
3425    } else {
3426        Some((lin_min, lin_max))
3427    }
3428}
3429
3430fn clamp_isize(v: isize, min_v: isize, max_v: isize) -> isize {
3431    v.max(min_v).min(max_v)
3432}
3433
3434fn point_in_polygon(x: f64, y: f64, polygon: &[(f64, f64)]) -> bool {
3435    if polygon.len() < 3 {
3436        return false;
3437    }
3438    let mut inside = false;
3439    let mut j = polygon.len() - 1;
3440    for i in 0..polygon.len() {
3441        let (xi, yi) = polygon[i];
3442        let (xj, yj) = polygon[j];
3443        let intersects = (yi > y) != (yj > y)
3444            && x < (xj - xi) * (y - yi) / ((yj - yi) + 1e-30) + xi;
3445        if intersects {
3446            inside = !inside;
3447        }
3448        j = i;
3449    }
3450    inside
3451}
3452
3453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3454enum WindowStat {
3455    Mean,
3456    Min,
3457    Max,
3458    Mode,
3459    Median,
3460    StdDev,
3461}
3462
3463fn reduce_window_values(values: &[f64], stat: WindowStat) -> Option<f64> {
3464    if values.is_empty() {
3465        return None;
3466    }
3467
3468    match stat {
3469        WindowStat::Mean => Some(values.iter().sum::<f64>() / values.len() as f64),
3470        WindowStat::Min => values.iter().copied().reduce(f64::min),
3471        WindowStat::Max => values.iter().copied().reduce(f64::max),
3472        WindowStat::Mode => {
3473            let mut pairs: Vec<(f64, usize)> = Vec::new();
3474            for v in values {
3475                if let Some((_, count)) = pairs.iter_mut().find(|(u, _)| *u == *v) {
3476                    *count += 1;
3477                } else {
3478                    pairs.push((*v, 1));
3479                }
3480            }
3481            pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.total_cmp(&b.0)));
3482            pairs.first().map(|(v, _)| *v)
3483        }
3484        WindowStat::Median => {
3485            let mut sorted = values.to_vec();
3486            sorted.sort_by(f64::total_cmp);
3487            let n = sorted.len();
3488            if n % 2 == 1 {
3489                Some(sorted[n / 2])
3490            } else {
3491                Some((sorted[n / 2 - 1] + sorted[n / 2]) / 2.0)
3492            }
3493        }
3494        WindowStat::StdDev => {
3495            let n = values.len() as f64;
3496            let mean = values.iter().sum::<f64>() / n;
3497            let variance = values
3498                .iter()
3499                .map(|v| {
3500                    let d = *v - mean;
3501                    d * d
3502                })
3503                .sum::<f64>()
3504                / n;
3505            Some(variance.max(0.0).sqrt())
3506        }
3507    }
3508}
3509
3510impl std::fmt::Display for Raster {
3511    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3512        write!(
3513            f,
3514            "Raster({}×{}×{}, cell={:.6}, x=[{:.4},{:.4}], y=[{:.4},{:.4}], type={}, nodata={})",
3515            self.bands,
3516            self.cols,
3517            self.rows,
3518            self.cell_size_x,
3519            self.x_min,
3520            self.x_max(),
3521            self.y_min,
3522            self.y_max(),
3523            self.data_type,
3524            self.nodata,
3525        )
3526    }
3527}
3528
3529#[cfg(test)]
3530mod tests {
3531    use super::*;
3532    use std::sync::{Arc, Mutex};
3533
3534    fn make_raster() -> Raster {
3535        let cfg = RasterConfig { cols: 4, rows: 3, cell_size: 10.0, nodata: -9999.0, ..Default::default() };
3536        let mut r = Raster::new(cfg);
3537        for row in 0..3 {
3538            for col in 0..4 {
3539                let _ = r.set(0, row, col, (row * 4 + col) as f64);
3540            }
3541        }
3542        r
3543    }
3544
3545    #[test]
3546    fn get_set() {
3547        let mut r = make_raster();
3548        assert_eq!(r.get(0, 0, 0), 0.0);
3549        assert_eq!(r.get(0, 2, 3), 11.0);
3550        r.set(0, 1, 1, -9999.0).unwrap();
3551        assert!(r.is_nodata(r.get(0, 1, 1))); // nodata
3552        assert_eq!(r.get_opt(0, 1, 1), None); // optional accessor
3553    }
3554
3555    #[test]
3556    fn statistics() {
3557        let r = make_raster();
3558        let s = r.statistics();
3559        assert_eq!(s.valid_count, 12);
3560        assert_eq!(s.min, 0.0);
3561        assert_eq!(s.max, 11.0);
3562        assert!((s.mean - 5.5).abs() < 1e-10);
3563    }
3564
3565    #[test]
3566    fn world_to_pixel() {
3567        let r = make_raster();
3568        // y_max = 30.0, x_max = 40.0
3569        assert_eq!(r.world_to_pixel(5.0, 25.0), Some((0, 0)));
3570        assert_eq!(r.world_to_pixel(35.0, 5.0), Some((3, 2)));
3571        assert_eq!(r.world_to_pixel(-1.0, 0.0), None);
3572    }
3573
3574    #[test]
3575    fn extent() {
3576        let r = make_raster();
3577        let e = r.extent();
3578        assert_eq!(e.x_min, 0.0);
3579        assert_eq!(e.y_min, 0.0);
3580        assert_eq!(e.x_max, 40.0);
3581        assert_eq!(e.y_max, 30.0);
3582    }
3583
3584    #[test]
3585    fn sample_extent_boundary_points_count_and_corners() {
3586        let e = Extent {
3587            x_min: 0.0,
3588            y_min: 0.0,
3589            x_max: 10.0,
3590            y_max: 5.0,
3591        };
3592        let pts = sample_extent_boundary_points(e, 8);
3593        assert_eq!(pts.len(), 32);
3594        assert!(pts.contains(&(0.0, 0.0)));
3595        assert!(pts.contains(&(0.0, 5.0)));
3596        assert!(pts.contains(&(10.0, 0.0)));
3597        assert!(pts.contains(&(10.0, 5.0)));
3598    }
3599
3600    #[test]
3601    fn sample_extent_boundary_points_minimum_sampling_when_zero_requested() {
3602        let e = Extent {
3603            x_min: -1.0,
3604            y_min: -2.0,
3605            x_max: 3.0,
3606            y_max: 4.0,
3607        };
3608        let pts = sample_extent_boundary_points(e, 0);
3609        assert_eq!(pts.len(), 4);
3610        assert!(pts.contains(&(-1.0, -2.0)));
3611        assert!(pts.contains(&(-1.0, 4.0)));
3612        assert!(pts.contains(&(3.0, -2.0)));
3613        assert!(pts.contains(&(3.0, 4.0)));
3614    }
3615
3616    #[test]
3617    fn minimal_wrapped_longitude_bounds_handles_antimeridian_cluster() {
3618        let lons = [179.0, -179.0, 178.0, -178.0];
3619        let (x0, x1) = minimal_wrapped_longitude_bounds(&lons).unwrap();
3620        assert!((x1 - x0) < 6.0);
3621    }
3622
3623    #[test]
3624    fn antimeridian_aware_longitude_bounds_prefers_wrapped_interval() {
3625        let lons = [179.5, -179.5, 179.0, -179.0];
3626        let (x0, x1) =
3627            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
3628        assert!((x1 - x0) < 5.0);
3629        assert!(x0 >= 0.0);
3630    }
3631
3632    #[test]
3633    fn antimeridian_aware_longitude_bounds_keeps_linear_when_better() {
3634        let lons = [-20.0, -10.0, 0.0, 10.0];
3635        let (x0, x1) =
3636            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
3637        assert!((x0 - (-20.0)).abs() < 1e-12);
3638        assert!((x1 - 10.0).abs() < 1e-12);
3639    }
3640
3641    #[test]
3642    fn antimeridian_policy_controls_wrapped_vs_linear_behavior() {
3643        let lons = [179.5, -179.5, 179.0, -179.0];
3644
3645        let (ax0, ax1) =
3646            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
3647        let (lx0, lx1) =
3648            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Linear).unwrap();
3649        let (wx0, wx1) =
3650            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Wrap).unwrap();
3651
3652        assert!((ax1 - ax0) < (lx1 - lx0));
3653        assert!((wx1 - wx0) < (lx1 - lx0));
3654        assert!((wx1 - wx0 - (ax1 - ax0)).abs() < 1e-9);
3655    }
3656
3657    #[test]
3658    fn reproject_antimeridian_policy_linear_vs_wrap_changes_default_extent() {
3659        let cfg = RasterConfig {
3660            cols: 8,
3661            rows: 4,
3662            x_min: 170.0,
3663            y_min: -10.0,
3664            cell_size: 2.5,
3665            nodata: -9999.0,
3666            ..Default::default()
3667        };
3668        let mut r = Raster::new(cfg);
3669        r.crs = CrsInfo::from_epsg(4326);
3670        for row in 0..r.rows {
3671            for col in 0..r.cols {
3672                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
3673                    .unwrap();
3674            }
3675        }
3676
3677        let linear_opts = ReprojectOptions::new(4326, ResampleMethod::Nearest)
3678            .with_antimeridian_policy(AntimeridianPolicy::Linear);
3679        let wrap_opts = ReprojectOptions::new(4326, ResampleMethod::Nearest)
3680            .with_antimeridian_policy(AntimeridianPolicy::Wrap);
3681
3682        let linear = r.reproject_with_options(&linear_opts).unwrap();
3683        let wrap = r.reproject_with_options(&wrap_opts).unwrap();
3684
3685        let linear_width = linear.x_max() - linear.x_min;
3686        let wrap_width = wrap.x_max() - wrap.x_min;
3687
3688        assert!(linear_width.is_finite() && linear_width > 0.0);
3689        assert!(wrap_width.is_finite() && wrap_width > 0.0);
3690        assert!(wrap_width <= linear_width + 1e-9);
3691    }
3692
3693    #[test]
3694    fn reproject_antimeridian_policy_auto_matches_narrower_interval() {
3695        let cfg = RasterConfig {
3696            cols: 8,
3697            rows: 4,
3698            x_min: 170.0,
3699            y_min: -10.0,
3700            cell_size: 2.5,
3701            nodata: -9999.0,
3702            ..Default::default()
3703        };
3704        let mut r = Raster::new(cfg);
3705        r.crs = CrsInfo::from_epsg(4326);
3706        for row in 0..r.rows {
3707            for col in 0..r.cols {
3708                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
3709                    .unwrap();
3710            }
3711        }
3712
3713        let linear = r
3714            .reproject_with_options(
3715                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
3716                    .with_antimeridian_policy(AntimeridianPolicy::Linear),
3717            )
3718            .unwrap();
3719        let wrap = r
3720            .reproject_with_options(
3721                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
3722                    .with_antimeridian_policy(AntimeridianPolicy::Wrap),
3723            )
3724            .unwrap();
3725        let auto = r
3726            .reproject_with_options(
3727                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
3728                    .with_antimeridian_policy(AntimeridianPolicy::Auto),
3729            )
3730            .unwrap();
3731
3732        let linear_width = linear.x_max() - linear.x_min;
3733        let wrap_width = wrap.x_max() - wrap.x_min;
3734        let auto_width = auto.x_max() - auto.x_min;
3735
3736        let expected = linear_width.min(wrap_width);
3737        assert!((auto_width - expected).abs() < 1e-9);
3738    }
3739
3740    #[test]
3741    fn reproject_to_epsg_requires_source_epsg() {
3742        let r = make_raster();
3743        let err = r.reproject_to_epsg_nearest(3857).unwrap_err();
3744        assert!(err
3745            .to_string()
3746            .contains("requires source CRS EPSG in raster.crs.epsg"));
3747    }
3748
3749    #[test]
3750    fn reproject_with_crs_allows_missing_source_epsg() {
3751        let cfg = RasterConfig {
3752            cols: 4,
3753            rows: 4,
3754            x_min: -1.0,
3755            y_min: -1.0,
3756            cell_size: 0.5,
3757            nodata: -9999.0,
3758            ..Default::default()
3759        };
3760        let mut r = Raster::new(cfg);
3761        // Intentionally leave `r.crs.epsg` unset.
3762        for row in 0..4 {
3763            for col in 0..4 {
3764                r.set(0, row, col, (row * 4 + col) as f64).unwrap();
3765            }
3766        }
3767
3768        let src = Crs::from_epsg(4326).unwrap();
3769        let dst = Crs::from_epsg(3857).unwrap();
3770        let opts = ReprojectOptions::new(3857, ResampleMethod::Nearest);
3771
3772        let out = r.reproject_with_crs(&src, &dst, &opts).unwrap();
3773        assert_eq!(out.cols, 4);
3774        assert_eq!(out.rows, 4);
3775        assert_eq!(out.crs.epsg, Some(3857));
3776        assert!(out.statistics().valid_count > 0);
3777    }
3778
3779    #[test]
3780    fn reproject_with_options_and_progress_emits_live_updates() {
3781        let cfg = RasterConfig {
3782            cols: 6,
3783            rows: 5,
3784            x_min: -80.0,
3785            y_min: 40.0,
3786            cell_size: 0.1,
3787            nodata: -9999.0,
3788            ..Default::default()
3789        };
3790        let mut r = Raster::new(cfg);
3791        r.crs = CrsInfo::from_epsg(4326);
3792        for row in 0..r.rows {
3793            for col in 0..r.cols {
3794                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
3795                    .unwrap();
3796            }
3797        }
3798
3799        let progress_values: Arc<Mutex<Vec<f64>>> = Arc::new(Mutex::new(Vec::new()));
3800        let sink = Arc::clone(&progress_values);
3801
3802        let out = r
3803            .reproject_with_options_and_progress(
3804                &ReprojectOptions::new(3857, ResampleMethod::Nearest),
3805                move |pct| {
3806                    sink.lock().unwrap().push(pct);
3807                },
3808            )
3809            .unwrap();
3810
3811        let values = progress_values.lock().unwrap();
3812        assert!(!values.is_empty());
3813        assert_eq!(values.len(), out.rows + 1);
3814        assert!(values.iter().all(|v| v.is_finite() && *v >= 0.0 && *v <= 1.0));
3815        assert!((values.last().copied().unwrap() - 1.0).abs() < 1e-12);
3816    }
3817
3818    #[test]
3819    fn reproject_to_epsg_identity_preserves_data() {
3820        let mut r = make_raster();
3821        r.crs = CrsInfo::from_epsg(4326);
3822
3823        let r2 = r.reproject_to_epsg(4326, ResampleMethod::Nearest).unwrap();
3824        assert_eq!(r2.cols, r.cols);
3825        assert_eq!(r2.rows, r.rows);
3826        assert_eq!(r2.bands, r.bands);
3827        assert_eq!(r2.crs.epsg, Some(4326));
3828        assert_eq!(r2.get(0, 0, 0), r.get(0, 0, 0));
3829        assert_eq!(r2.get(0, 2, 3), r.get(0, 2, 3));
3830    }
3831
3832    #[test]
3833    fn reproject_to_epsg_4326_to_3857_produces_valid_output() {
3834        let cfg = RasterConfig {
3835            cols: 4,
3836            rows: 4,
3837            x_min: -1.0,
3838            y_min: -1.0,
3839            cell_size: 0.5,
3840            nodata: -9999.0,
3841            ..Default::default()
3842        };
3843        let mut r = Raster::new(cfg);
3844        r.crs = CrsInfo::from_epsg(4326);
3845        for row in 0..4 {
3846            for col in 0..4 {
3847                r.set(0, row, col, (row * 4 + col) as f64).unwrap();
3848            }
3849        }
3850
3851        let out = r.reproject_to_epsg_nearest(3857).unwrap();
3852        assert_eq!(out.cols, 4);
3853        assert_eq!(out.rows, 4);
3854        assert_eq!(out.crs.epsg, Some(3857));
3855        assert!(out.x_min.is_finite());
3856        assert!(out.y_min.is_finite());
3857        assert!(out.cell_size_x.is_finite() && out.cell_size_x > 0.0);
3858        assert!(out.cell_size_y.is_finite() && out.cell_size_y > 0.0);
3859        let s = out.statistics();
3860        assert!(s.valid_count > 0);
3861    }
3862
3863    #[test]
3864    fn reproject_to_match_grid_honors_target_grid_definition() {
3865        let src_cfg = RasterConfig {
3866            cols: 6,
3867            rows: 4,
3868            x_min: -3.0,
3869            y_min: -2.0,
3870            cell_size: 1.0,
3871            nodata: -9999.0,
3872            ..Default::default()
3873        };
3874        let mut src = Raster::new(src_cfg);
3875        src.crs = CrsInfo::from_epsg(4326);
3876        for row in 0..src.rows {
3877            for col in 0..src.cols {
3878                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
3879                    .unwrap();
3880            }
3881        }
3882
3883        let target_cfg = RasterConfig {
3884            cols: 12,
3885            rows: 10,
3886            x_min: -500_000.0,
3887            y_min: -400_000.0,
3888            cell_size: 1.0,
3889            cell_size_y: Some(1.0),
3890            nodata: -9999.0,
3891            ..Default::default()
3892        };
3893        let mut target = Raster::new(target_cfg);
3894        target.crs = CrsInfo::from_epsg(3857);
3895        target.cell_size_x = (500_000.0 - (-500_000.0)) / target.cols as f64;
3896        target.cell_size_y = (400_000.0 - (-400_000.0)) / target.rows as f64;
3897
3898        let out = src
3899            .reproject_to_match_grid(&target, ResampleMethod::Bilinear)
3900            .unwrap();
3901
3902        assert_eq!(out.cols, target.cols);
3903        assert_eq!(out.rows, target.rows);
3904        assert_eq!(out.crs.epsg, target.crs.epsg);
3905        assert!((out.x_min - target.x_min).abs() < 1e-9);
3906        assert!((out.y_min - target.y_min).abs() < 1e-9);
3907        assert!((out.x_max() - target.x_max()).abs() < 1e-6);
3908        assert!((out.y_max() - target.y_max()).abs() < 1e-6);
3909        assert!(out.cell_size_x.is_finite() && out.cell_size_x > 0.0);
3910        assert!(out.cell_size_y.is_finite() && out.cell_size_y > 0.0);
3911    }
3912
3913    #[test]
3914    fn reproject_to_match_resolution_honors_reference_cellsize_and_snap() {
3915        let src_cfg = RasterConfig {
3916            cols: 6,
3917            rows: 4,
3918            x_min: -3.0,
3919            y_min: -2.0,
3920            cell_size: 1.0,
3921            nodata: -9999.0,
3922            ..Default::default()
3923        };
3924        let mut src = Raster::new(src_cfg);
3925        src.crs = CrsInfo::from_epsg(4326);
3926        for row in 0..src.rows {
3927            for col in 0..src.cols {
3928                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
3929                    .unwrap();
3930            }
3931        }
3932
3933        let mut reference = Raster::new(RasterConfig {
3934            cols: 4,
3935            rows: 3,
3936            x_min: 50_000.0,
3937            y_min: -75_000.0,
3938            cell_size: 100_000.0,
3939            cell_size_y: Some(80_000.0),
3940            nodata: -9999.0,
3941            ..Default::default()
3942        });
3943        reference.crs = CrsInfo::from_epsg(3857);
3944
3945        let out = src
3946            .reproject_to_match_resolution(&reference, ResampleMethod::Nearest)
3947            .unwrap();
3948
3949        assert_eq!(out.crs.epsg, Some(3857));
3950        assert!((out.cell_size_x - reference.cell_size_x).abs() < 1e-9);
3951        assert!((out.cell_size_y - reference.cell_size_y).abs() < 1e-9);
3952
3953        let kx = ((out.x_min - reference.x_min) / reference.cell_size_x).round();
3954        let ky = ((out.y_min - reference.y_min) / reference.cell_size_y).round();
3955        assert!((out.x_min - (reference.x_min + kx * reference.cell_size_x)).abs() < 1e-6);
3956        assert!((out.y_min - (reference.y_min + ky * reference.cell_size_y)).abs() < 1e-6);
3957        assert!(out.cols > 0);
3958        assert!(out.rows > 0);
3959    }
3960
3961    #[test]
3962    fn reproject_to_match_resolution_in_epsg_same_crs_matches_reference_settings() {
3963        let src_cfg = RasterConfig {
3964            cols: 6,
3965            rows: 4,
3966            x_min: -3.0,
3967            y_min: -2.0,
3968            cell_size: 1.0,
3969            nodata: -9999.0,
3970            ..Default::default()
3971        };
3972        let mut src = Raster::new(src_cfg);
3973        src.crs = CrsInfo::from_epsg(4326);
3974        for row in 0..src.rows {
3975            for col in 0..src.cols {
3976                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
3977                    .unwrap();
3978            }
3979        }
3980
3981        let mut reference = Raster::new(RasterConfig {
3982            cols: 4,
3983            rows: 3,
3984            x_min: -10.0,
3985            y_min: -20.0,
3986            cell_size: 0.5,
3987            cell_size_y: Some(0.25),
3988            nodata: -9999.0,
3989            ..Default::default()
3990        });
3991        reference.crs = CrsInfo::from_epsg(4326);
3992
3993        let out = src
3994            .reproject_to_match_resolution_in_epsg(4326, &reference, ResampleMethod::Nearest)
3995            .unwrap();
3996
3997        assert_eq!(out.crs.epsg, Some(4326));
3998        assert!((out.cell_size_x - reference.cell_size_x).abs() < 1e-12);
3999        assert!((out.cell_size_y - reference.cell_size_y).abs() < 1e-12);
4000    }
4001
4002    #[test]
4003    fn reproject_to_match_resolution_in_epsg_cross_crs_converts_resolution() {
4004        let src_cfg = RasterConfig {
4005            cols: 6,
4006            rows: 4,
4007            x_min: -3.0,
4008            y_min: -2.0,
4009            cell_size: 1.0,
4010            nodata: -9999.0,
4011            ..Default::default()
4012        };
4013        let mut src = Raster::new(src_cfg);
4014        src.crs = CrsInfo::from_epsg(4326);
4015        for row in 0..src.rows {
4016            for col in 0..src.cols {
4017                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
4018                    .unwrap();
4019            }
4020        }
4021
4022        let mut reference = Raster::new(RasterConfig {
4023            cols: 4,
4024            rows: 3,
4025            x_min: 0.0,
4026            y_min: 0.0,
4027            cell_size: 1.0,
4028            cell_size_y: Some(1.0),
4029            nodata: -9999.0,
4030            ..Default::default()
4031        });
4032        reference.crs = CrsInfo::from_epsg(4326);
4033
4034        let out = src
4035            .reproject_to_match_resolution_in_epsg(3857, &reference, ResampleMethod::Nearest)
4036            .unwrap();
4037
4038        assert_eq!(out.crs.epsg, Some(3857));
4039        assert!(out.cell_size_x.is_finite());
4040        assert!(out.cell_size_y.is_finite());
4041        assert!(out.cell_size_x > 0.0);
4042        assert!(out.cell_size_y > 0.0);
4043    }
4044
4045    #[test]
4046    fn reproject_to_epsg_bilinear_cubic_and_lanczos_produce_valid_output() {
4047        let cfg = RasterConfig {
4048            cols: 8,
4049            rows: 8,
4050            x_min: -2.0,
4051            y_min: -2.0,
4052            cell_size: 0.5,
4053            nodata: -9999.0,
4054            ..Default::default()
4055        };
4056        let mut r = Raster::new(cfg);
4057        r.crs = CrsInfo::from_epsg(4326);
4058        for row in 0..8 {
4059            for col in 0..8 {
4060                let val = (col as f64) * 10.0 + row as f64;
4061                r.set(0, row, col, val).unwrap();
4062            }
4063        }
4064
4065        let bilinear = r.reproject_to_epsg_bilinear(3857).unwrap();
4066        let cubic = r.reproject_to_epsg_cubic(3857).unwrap();
4067        let lanczos = r.reproject_to_epsg_lanczos(3857).unwrap();
4068
4069        assert_eq!(bilinear.cols, 8);
4070        assert_eq!(bilinear.rows, 8);
4071        assert_eq!(bilinear.crs.epsg, Some(3857));
4072        assert!(bilinear.statistics().valid_count > 0);
4073
4074        assert_eq!(cubic.cols, 8);
4075        assert_eq!(cubic.rows, 8);
4076        assert_eq!(cubic.crs.epsg, Some(3857));
4077        assert!(cubic.statistics().valid_count > 0);
4078
4079        assert_eq!(lanczos.cols, 8);
4080        assert_eq!(lanczos.rows, 8);
4081        assert_eq!(lanczos.crs.epsg, Some(3857));
4082        assert!(lanczos.statistics().valid_count > 0);
4083    }
4084
4085    #[test]
4086    fn reproject_with_options_honors_grid_controls() {
4087        let cfg = RasterConfig {
4088            cols: 6,
4089            rows: 4,
4090            x_min: -3.0,
4091            y_min: -2.0,
4092            cell_size: 1.0,
4093            nodata: -9999.0,
4094            ..Default::default()
4095        };
4096        let mut r = Raster::new(cfg);
4097        r.crs = CrsInfo::from_epsg(4326);
4098        for row in 0..4 {
4099            for col in 0..6 {
4100                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
4101            }
4102        }
4103
4104        let opts = ReprojectOptions {
4105            dst_epsg: 3857,
4106            resample: ResampleMethod::Bilinear,
4107            cols: Some(12),
4108            rows: Some(10),
4109            extent: Some(Extent {
4110                x_min: -500_000.0,
4111                y_min: -400_000.0,
4112                x_max: 500_000.0,
4113                y_max: 400_000.0,
4114            }),
4115            x_res: None,
4116            y_res: None,
4117            snap_x: None,
4118            snap_y: None,
4119            nodata_policy: NodataPolicy::PartialKernel,
4120            antimeridian_policy: AntimeridianPolicy::Auto,
4121            grid_size_policy: GridSizePolicy::Expand,
4122            destination_footprint: DestinationFootprint::None,
4123        };
4124
4125        let out = r.reproject_with_options(&opts).unwrap();
4126        assert_eq!(out.cols, 12);
4127        assert_eq!(out.rows, 10);
4128        assert!((out.x_min - (-500_000.0)).abs() < 1e-9);
4129        assert!((out.y_min - (-400_000.0)).abs() < 1e-9);
4130        assert!((out.x_max() - 500_000.0).abs() < 1e-6);
4131        assert!((out.y_max() - 400_000.0).abs() < 1e-6);
4132        assert_eq!(out.crs.epsg, Some(3857));
4133    }
4134
4135    #[test]
4136    fn bilinear_partial_kernel_renormalizes_with_nodata() {
4137        let cfg = RasterConfig {
4138            cols: 2,
4139            rows: 2,
4140            x_min: 0.0,
4141            y_min: 0.0,
4142            cell_size: 1.0,
4143            nodata: -9999.0,
4144            ..Default::default()
4145        };
4146        // row-major top-down: [ [1, nodata], [3, 5] ]
4147        let r = Raster::from_data(cfg, vec![1.0, -9999.0, 3.0, 5.0]).unwrap();
4148
4149        let v = r.sample_bilinear_partial_pixel(0, 0.5, 0.5).unwrap();
4150        // weighted average of valid neighbors only: (1 + 3 + 5) / 3 = 3
4151        assert!((v - 3.0).abs() < 1e-9);
4152    }
4153
4154    #[test]
4155    fn bilinear_partial_kernel_handles_edges() {
4156        let cfg = RasterConfig {
4157            cols: 2,
4158            rows: 2,
4159            x_min: 0.0,
4160            y_min: 0.0,
4161            cell_size: 1.0,
4162            nodata: -9999.0,
4163            ..Default::default()
4164        };
4165        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
4166        let v = r.sample_bilinear_partial_pixel(0, -0.2, 0.3).unwrap();
4167        assert!(v.is_finite());
4168    }
4169
4170    #[test]
4171    fn cubic_partial_kernel_handles_edges_and_nodata() {
4172        let cfg = RasterConfig {
4173            cols: 4,
4174            rows: 4,
4175            x_min: 0.0,
4176            y_min: 0.0,
4177            cell_size: 1.0,
4178            nodata: -9999.0,
4179            ..Default::default()
4180        };
4181        let mut data = Vec::new();
4182        for row in 0..4 {
4183            for col in 0..4 {
4184                data.push((row * 4 + col) as f64);
4185            }
4186        }
4187        data[5] = -9999.0; // inject one nodata sample
4188        let r = Raster::from_data(cfg, data).unwrap();
4189
4190        let edge_v = r.sample_cubic_partial_pixel(0, -0.25, 0.2).unwrap();
4191        assert!(edge_v.is_finite());
4192
4193        let nodata_v = r.sample_cubic_partial_pixel(0, 1.25, 1.25).unwrap();
4194        assert!(nodata_v.is_finite());
4195    }
4196
4197    #[test]
4198    fn strict_policy_rejects_incomplete_bilinear_kernel() {
4199        let cfg = RasterConfig {
4200            cols: 2,
4201            rows: 2,
4202            x_min: 0.0,
4203            y_min: 0.0,
4204            cell_size: 1.0,
4205            nodata: -9999.0,
4206            ..Default::default()
4207        };
4208        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
4209
4210        assert!(r.sample_bilinear_strict_pixel(0, -0.2, 0.3).is_none());
4211        assert!(r.sample_bilinear_partial_pixel(0, -0.2, 0.3).is_some());
4212    }
4213
4214    #[test]
4215    fn bilinear_strict_simd_matches_expected_for_f64_and_f32_storage() {
4216        let cfg_f64 = RasterConfig {
4217            cols: 2,
4218            rows: 2,
4219            x_min: 0.0,
4220            y_min: 0.0,
4221            cell_size: 1.0,
4222            nodata: -9999.0,
4223            data_type: DataType::F64,
4224            ..Default::default()
4225        };
4226        let cfg_f32 = RasterConfig {
4227            data_type: DataType::F32,
4228            ..cfg_f64.clone()
4229        };
4230        let data = vec![1.0, 2.0, 3.0, 5.0];
4231        let r64 = Raster::from_data(cfg_f64, data.clone()).unwrap();
4232        let r32 = Raster::from_data(cfg_f32, data).unwrap();
4233
4234        let expected = 2.375;
4235        let v64 = r64.sample_bilinear_strict_pixel(0, 0.25, 0.5).unwrap();
4236        let v32 = r32.sample_bilinear_strict_pixel(0, 0.25, 0.5).unwrap();
4237        assert!((v64 - expected).abs() < 1e-9);
4238        assert!((v32 - expected).abs() < 1e-6);
4239    }
4240
4241    #[test]
4242    fn lanczos_strict_simd_matches_between_f64_and_f32_storage() {
4243        let cfg_f64 = RasterConfig {
4244            cols: 16,
4245            rows: 16,
4246            x_min: 0.0,
4247            y_min: 0.0,
4248            cell_size: 1.0,
4249            nodata: -9999.0,
4250            data_type: DataType::F64,
4251            ..Default::default()
4252        };
4253        let cfg_f32 = RasterConfig {
4254            data_type: DataType::F32,
4255            ..cfg_f64.clone()
4256        };
4257
4258        let mut data = Vec::with_capacity(16 * 16);
4259        for row in 0..16 {
4260            for col in 0..16 {
4261                data.push(((row * 16 + col) as f64).sin() * 100.0 + (row as f64 * 0.5));
4262            }
4263        }
4264
4265        let r64 = Raster::from_data(cfg_f64, data.clone()).unwrap();
4266        let r32 = Raster::from_data(cfg_f32, data).unwrap();
4267
4268        let v64 = r64.sample_lanczos_strict_pixel(0, 7.25, 8.5).unwrap();
4269        let v32 = r32.sample_lanczos_strict_pixel(0, 7.25, 8.5).unwrap();
4270        assert!((v64 - v32).abs() < 1e-4);
4271    }
4272
4273    #[test]
4274    fn fill_policy_falls_back_to_nearest_for_bilinear() {
4275        let cfg = RasterConfig {
4276            cols: 2,
4277            rows: 2,
4278            x_min: 0.0,
4279            y_min: 0.0,
4280            cell_size: 1.0,
4281            nodata: -9999.0,
4282            ..Default::default()
4283        };
4284        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
4285
4286        let v = r.sample_world(0, 0.1, 1.1, ResampleMethod::Bilinear, NodataPolicy::Fill);
4287        assert_eq!(v, Some(10.0));
4288    }
4289
4290    #[test]
4291    fn fill_policy_falls_back_to_nearest_for_lanczos() {
4292        let cfg = RasterConfig {
4293            cols: 2,
4294            rows: 2,
4295            x_min: 0.0,
4296            y_min: 0.0,
4297            cell_size: 1.0,
4298            nodata: -9999.0,
4299            ..Default::default()
4300        };
4301        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
4302
4303        let v = r.sample_world(0, 0.1, 1.1, ResampleMethod::Lanczos, NodataPolicy::Fill);
4304        assert_eq!(v, Some(10.0));
4305    }
4306
4307    #[test]
4308    fn reproject_options_default_to_partial_kernel_policy() {
4309        let opts = ReprojectOptions::new(3857, ResampleMethod::Cubic);
4310        assert_eq!(opts.nodata_policy, NodataPolicy::PartialKernel);
4311        assert_eq!(opts.x_res, None);
4312        assert_eq!(opts.y_res, None);
4313        assert_eq!(opts.snap_x, None);
4314        assert_eq!(opts.snap_y, None);
4315        assert_eq!(opts.antimeridian_policy, AntimeridianPolicy::Auto);
4316        assert_eq!(opts.grid_size_policy, GridSizePolicy::Expand);
4317        assert_eq!(opts.destination_footprint, DestinationFootprint::None);
4318
4319        let strict = opts.with_nodata_policy(NodataPolicy::Strict);
4320        assert_eq!(strict.nodata_policy, NodataPolicy::Strict);
4321
4322        let sized = strict.with_size(321, 123);
4323        assert_eq!(sized.cols, Some(321));
4324        assert_eq!(sized.rows, Some(123));
4325
4326        let e = Extent {
4327            x_min: -10.0,
4328            y_min: -5.0,
4329            x_max: 10.0,
4330            y_max: 5.0,
4331        };
4332        let ext = sized.with_extent(e);
4333        assert_eq!(ext.extent, Some(e));
4334
4335        let res = ext.with_resolution(30.0, 40.0);
4336        assert_eq!(res.x_res, Some(30.0));
4337        assert_eq!(res.y_res, Some(40.0));
4338
4339        let square = res.with_square_resolution(25.0);
4340        assert_eq!(square.x_res, Some(25.0));
4341        assert_eq!(square.y_res, Some(25.0));
4342
4343        let snapped = square.with_snap_origin(0.0, 0.0);
4344        assert_eq!(snapped.snap_x, Some(0.0));
4345        assert_eq!(snapped.snap_y, Some(0.0));
4346
4347        let linear = snapped.with_antimeridian_policy(AntimeridianPolicy::Linear);
4348        assert_eq!(linear.antimeridian_policy, AntimeridianPolicy::Linear);
4349
4350        let fit_inside = linear.with_grid_size_policy(GridSizePolicy::FitInside);
4351        assert_eq!(fit_inside.grid_size_policy, GridSizePolicy::FitInside);
4352
4353        let masked = fit_inside.with_destination_footprint(DestinationFootprint::SourceBoundary);
4354        assert_eq!(masked.destination_footprint, DestinationFootprint::SourceBoundary);
4355    }
4356
4357    #[test]
4358    fn sample_extent_boundary_ring_count_and_corners() {
4359        let e = Extent {
4360            x_min: 0.0,
4361            y_min: 0.0,
4362            x_max: 10.0,
4363            y_max: 5.0,
4364        };
4365        let ring = sample_extent_boundary_ring(e, 8);
4366        assert_eq!(ring.len(), 32);
4367        assert!(ring.contains(&(0.0, 0.0)));
4368        assert!(ring.contains(&(10.0, 0.0)));
4369        assert!(ring.contains(&(10.0, 5.0)));
4370        assert!(ring.contains(&(0.0, 5.0)));
4371    }
4372
4373    #[test]
4374    fn point_in_polygon_identifies_inside_and_outside_points() {
4375        let poly = vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)];
4376        assert!(point_in_polygon(2.0, 2.0, &poly));
4377        assert!(!point_in_polygon(5.0, 2.0, &poly));
4378    }
4379
4380    #[test]
4381    fn thematic_3x3_resamplers_return_expected_statistics() {
4382        let cfg = RasterConfig {
4383            cols: 3,
4384            rows: 3,
4385            x_min: 0.0,
4386            y_min: 0.0,
4387            cell_size: 1.0,
4388            nodata: -9999.0,
4389            ..Default::default()
4390        };
4391        let data = vec![
4392            1.0, 2.0, 2.0,
4393            3.0, 4.0, 4.0,
4394            5.0, 6.0, 6.0,
4395        ];
4396        let expected_mean = data.iter().sum::<f64>() / data.len() as f64;
4397        let expected_stddev = (data
4398            .iter()
4399            .map(|v| {
4400                let d = *v - expected_mean;
4401                d * d
4402            })
4403            .sum::<f64>()
4404            / data.len() as f64)
4405            .sqrt();
4406        let r = Raster::from_data(cfg, data).unwrap();
4407
4408        let x = r.col_center_x(1);
4409        let y = r.row_center_y(1);
4410
4411        let avg = r
4412            .sample_world(0, x, y, ResampleMethod::Average, NodataPolicy::Strict)
4413            .unwrap();
4414        let min = r
4415            .sample_world(0, x, y, ResampleMethod::Min, NodataPolicy::Strict)
4416            .unwrap();
4417        let max = r
4418            .sample_world(0, x, y, ResampleMethod::Max, NodataPolicy::Strict)
4419            .unwrap();
4420        let mode = r
4421            .sample_world(0, x, y, ResampleMethod::Mode, NodataPolicy::Strict)
4422            .unwrap();
4423        let median = r
4424            .sample_world(0, x, y, ResampleMethod::Median, NodataPolicy::Strict)
4425            .unwrap();
4426        let stddev = r
4427            .sample_world(0, x, y, ResampleMethod::StdDev, NodataPolicy::Strict)
4428            .unwrap();
4429
4430        assert!((avg - (33.0 / 9.0)).abs() < 1e-9);
4431        assert_eq!(min, 1.0);
4432        assert_eq!(max, 6.0);
4433        assert_eq!(mode, 2.0);
4434        assert_eq!(median, 4.0);
4435        assert!((stddev - expected_stddev).abs() < 1e-9);
4436    }
4437
4438    #[test]
4439    fn grid_size_policy_fit_inside_reduces_resolution_derived_size() {
4440        let mut r = make_raster();
4441        r.crs = CrsInfo::from_epsg(4326);
4442
4443        let extent = Extent {
4444            x_min: -500_000.0,
4445            y_min: -400_000.0,
4446            x_max: 500_000.0,
4447            y_max: 400_000.0,
4448        };
4449
4450        let expand = r
4451            .reproject_with_options(
4452                &ReprojectOptions::new(3857, ResampleMethod::Nearest)
4453                    .with_extent(extent)
4454                    .with_resolution(300_000.0, 300_000.0)
4455                    .with_grid_size_policy(GridSizePolicy::Expand),
4456            )
4457            .unwrap();
4458
4459        let fit = r
4460            .reproject_with_options(
4461                &ReprojectOptions::new(3857, ResampleMethod::Nearest)
4462                    .with_extent(extent)
4463                    .with_resolution(300_000.0, 300_000.0)
4464                    .with_grid_size_policy(GridSizePolicy::FitInside),
4465            )
4466            .unwrap();
4467
4468        assert!(fit.cols <= expand.cols);
4469        assert!(fit.rows <= expand.rows);
4470    }
4471
4472    #[test]
4473    fn destination_footprint_masks_cells_outside_source_boundary() {
4474        let cfg = RasterConfig {
4475            cols: 4,
4476            rows: 4,
4477            x_min: 0.0,
4478            y_min: 0.0,
4479            cell_size: 1.0,
4480            nodata: -9999.0,
4481            ..Default::default()
4482        };
4483        let mut r = Raster::new(cfg);
4484        r.crs = CrsInfo::from_epsg(4326);
4485        for row in 0..4 {
4486            for col in 0..4 {
4487                r.set(0, row, col, 1.0).unwrap();
4488            }
4489        }
4490
4491        let out = r
4492            .reproject_with_options(
4493                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
4494                    .with_extent(Extent {
4495                        x_min: -1.0,
4496                        y_min: -1.0,
4497                        x_max: 5.0,
4498                        y_max: 5.0,
4499                    })
4500                    .with_size(6, 6)
4501                    .with_destination_footprint(DestinationFootprint::SourceBoundary),
4502            )
4503            .unwrap();
4504
4505        assert!(out.is_nodata(out.get(0, 0, 0)));
4506        assert!(!out.is_nodata(out.get(0, 2, 2)));
4507    }
4508
4509    #[test]
4510    fn reproject_with_options_honors_snap_origin_with_resolution() {
4511        let cfg = RasterConfig {
4512            cols: 6,
4513            rows: 4,
4514            x_min: -3.0,
4515            y_min: -2.0,
4516            cell_size: 1.0,
4517            nodata: -9999.0,
4518            ..Default::default()
4519        };
4520        let mut r = Raster::new(cfg);
4521        r.crs = CrsInfo::from_epsg(4326);
4522        for row in 0..4 {
4523            for col in 0..6 {
4524                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
4525            }
4526        }
4527
4528        let out_extent = Extent {
4529            x_min: -510_000.0,
4530            y_min: -390_000.0,
4531            x_max: 490_000.0,
4532            y_max: 410_000.0,
4533        };
4534        let opts = ReprojectOptions::new(3857, ResampleMethod::Bilinear)
4535            .with_extent(out_extent)
4536            .with_resolution(200_000.0, 160_000.0)
4537            .with_snap_origin(0.0, 0.0);
4538
4539        let out = r.reproject_with_options(&opts).unwrap();
4540        assert!((out.x_min - (-600_000.0)).abs() < 1e-6);
4541        assert!((out.y_min - (-480_000.0)).abs() < 1e-6);
4542        assert!((out.cell_size_x - 200_000.0).abs() < 1e-6);
4543        assert!((out.cell_size_y - 160_000.0).abs() < 1e-6);
4544    }
4545
4546    #[test]
4547    fn reproject_with_options_honors_resolution_controls() {
4548        let cfg = RasterConfig {
4549            cols: 6,
4550            rows: 4,
4551            x_min: -3.0,
4552            y_min: -2.0,
4553            cell_size: 1.0,
4554            nodata: -9999.0,
4555            ..Default::default()
4556        };
4557        let mut r = Raster::new(cfg);
4558        r.crs = CrsInfo::from_epsg(4326);
4559        for row in 0..4 {
4560            for col in 0..6 {
4561                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
4562            }
4563        }
4564
4565        let out_extent = Extent {
4566            x_min: -500_000.0,
4567            y_min: -400_000.0,
4568            x_max: 500_000.0,
4569            y_max: 400_000.0,
4570        };
4571        let opts = ReprojectOptions::new(3857, ResampleMethod::Bilinear)
4572            .with_extent(out_extent)
4573            .with_resolution(200_000.0, 160_000.0);
4574
4575        let out = r.reproject_with_options(&opts).unwrap();
4576        assert_eq!(out.cols, 5);
4577        assert_eq!(out.rows, 5);
4578        assert!((out.cell_size_x - 200_000.0).abs() < 1e-6);
4579        assert!((out.cell_size_y - 160_000.0).abs() < 1e-6);
4580    }
4581
4582    #[test]
4583    fn reproject_with_options_rejects_invalid_resolution_controls() {
4584        let mut r = make_raster();
4585        r.crs = CrsInfo::from_epsg(4326);
4586        let opts = ReprojectOptions::new(3857, ResampleMethod::Nearest)
4587            .with_square_resolution(0.0);
4588        let err = r.reproject_with_options(&opts).unwrap_err();
4589        assert!(err
4590            .to_string()
4591            .contains("invalid reprojection resolution"));
4592    }
4593
4594    #[test]
4595    fn band_helpers() {
4596        let cfg = RasterConfig {
4597            cols: 3,
4598            rows: 2,
4599            bands: 2,
4600            nodata: -9999.0,
4601            ..Default::default()
4602        };
4603        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, -9999.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
4604        let mut r = Raster::from_data(cfg, data).unwrap();
4605
4606        let b0 = r.band_slice(0);
4607        assert_eq!(b0.len(), 6);
4608        assert_eq!(b0[0], 1.0);
4609        assert_eq!(b0[5], -9999.0);
4610
4611        let s0 = r.statistics_band(0).unwrap();
4612        assert_eq!(s0.valid_count, 5);
4613        assert_eq!(s0.nodata_count, 1);
4614        assert_eq!(s0.min, 1.0);
4615        assert_eq!(s0.max, 5.0);
4616
4617        r.set_band_slice(1, &[7.0, 8.0, 9.0, 10.0, 11.0, 12.0]).unwrap();
4618        assert_eq!(r.get_raw(1, 0, 0), Some(7.0));
4619        assert_eq!(r.get_raw(1, 1, 2), Some(12.0));
4620    }
4621
4622    #[test]
4623    fn band_transform_helpers() {
4624        let cfg = RasterConfig {
4625            cols: 2,
4626            rows: 2,
4627            bands: 2,
4628            nodata: -9999.0,
4629            ..Default::default()
4630        };
4631        let data = vec![1.0, 2.0, 3.0, -9999.0, 10.0, 20.0, 30.0, 40.0];
4632        let mut r = Raster::from_data(cfg, data).unwrap();
4633
4634        r.map_valid_band(0, |v| v * 2.0).unwrap();
4635        assert_eq!(r.get_raw(0, 0, 0), Some(2.0));
4636        assert_eq!(r.get_raw(0, 0, 1), Some(4.0));
4637        assert!(r.is_nodata(r.get(0, 1, 1))); // nodata unchanged
4638        assert_eq!(r.get_raw(1, 0, 0), Some(10.0)); // other band unchanged
4639
4640        r.replace_band(1, 20.0, 99.0).unwrap();
4641        assert_eq!(r.get_raw(1, 0, 1), Some(99.0));
4642        assert_eq!(r.get_raw(1, 0, 0), Some(10.0));
4643        assert_eq!(r.get_raw(0, 0, 1), Some(4.0));
4644    }
4645
4646    #[test]
4647    fn band_iterators() {
4648        let cfg = RasterConfig {
4649            cols: 3,
4650            rows: 2,
4651            bands: 2,
4652            nodata: -9999.0,
4653            ..Default::default()
4654        };
4655        let data = vec![1.0, 2.0, -9999.0, 4.0, 5.0, 6.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
4656        let r = Raster::from_data(cfg, data).unwrap();
4657
4658        let valid_b0: Vec<_> = r.iter_valid_band(0).unwrap().collect();
4659        assert_eq!(valid_b0.len(), 5);
4660        assert_eq!(valid_b0[0], (0, 0, 1.0));
4661        assert_eq!(valid_b0[4], (1, 2, 6.0));
4662
4663        let rows_b1: Vec<Vec<f64>> = r.iter_band_rows(1).unwrap().collect();
4664        assert_eq!(rows_b1.len(), 2);
4665        assert_eq!(rows_b1[0], vec![10.0, 20.0, 30.0]);
4666        assert_eq!(rows_b1[1], vec![40.0, 50.0, 60.0]);
4667    }
4668
4669    #[test]
4670    fn mutable_band_rows_native() {
4671        let cfg = RasterConfig {
4672            cols: 3,
4673            rows: 2,
4674            bands: 1,
4675            data_type: DataType::F32,
4676            nodata: -9999.0,
4677            ..Default::default()
4678        };
4679        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
4680        let mut r = Raster::from_data(cfg, data).unwrap();
4681
4682        r.for_each_band_row_mut(0, |row, row_mut| {
4683            if let RasterRowMut::F32(slice) = row_mut {
4684                for v in slice.iter_mut() {
4685                    *v += row as f32;
4686                }
4687            }
4688        })
4689        .unwrap();
4690
4691        assert_eq!(r.get_raw(0, 0, 0), Some(1.0));
4692        assert_eq!(r.get_raw(0, 0, 2), Some(3.0));
4693        assert_eq!(r.get_raw(0, 1, 0), Some(5.0));
4694        assert_eq!(r.get_raw(0, 1, 2), Some(7.0));
4695    }
4696
4697    #[test]
4698    fn immutable_band_rows_native() {
4699        let cfg = RasterConfig {
4700            cols: 3,
4701            rows: 2,
4702            bands: 1,
4703            data_type: DataType::U16,
4704            nodata: 0.0,
4705            ..Default::default()
4706        };
4707        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
4708        let r = Raster::from_data(cfg, data).unwrap();
4709
4710        let mut sums = Vec::new();
4711        r.for_each_band_row(0, |_row, row_ref| {
4712            if let RasterRowRef::U16(slice) = row_ref {
4713                sums.push(slice.iter().map(|v| *v as u64).sum::<u64>());
4714            }
4715        })
4716        .unwrap();
4717
4718        assert_eq!(sums, vec![6, 15]);
4719    }
4720}