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