Skip to main content

wbraster/
raster.rs

1//! Core `Raster` type — the central data structure for all raster GIS data.
2
3use rayon::prelude::*;
4use std::path::Path;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use wbprojection::{from_proj_string, Crs, CrsTransformPolicy, EpochPolicy, EpochTransformOptions};
7use wide::{f64x4, CmpNe};
8
9use crate::crs_info::CrsInfo;
10use crate::error::{RasterError, Result};
11use crate::formats::RasterFormat;
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() {
137            v.is_nan()
138        } else {
139            v == self.nodata
140        }
141    }
142
143    /// Direct reference to the underlying flat buffer (`row * cols + col` indexing).
144    /// Length is `rows as usize * cols as usize`.
145    #[inline]
146    pub fn as_slice(&self) -> &[f64] {
147        &self.data
148    }
149}
150
151// SAFETY: BandView contains only Vec<f64>, isize, and f64 — all Send and Sync.
152unsafe impl Send for BandView {}
153unsafe impl Sync for BandView {}
154
155/// Typed in-memory pixel buffer.
156#[derive(Debug, Clone)]
157pub enum RasterData {
158    /// Unsigned 8-bit storage.
159    U8(Vec<u8>),
160    /// Signed 8-bit storage.
161    I8(Vec<i8>),
162    /// Unsigned 16-bit storage.
163    U16(Vec<u16>),
164    /// Signed 16-bit storage.
165    I16(Vec<i16>),
166    /// Unsigned 32-bit storage.
167    U32(Vec<u32>),
168    /// Signed 32-bit storage.
169    I32(Vec<i32>),
170    /// Unsigned 64-bit storage.
171    U64(Vec<u64>),
172    /// Signed 64-bit storage.
173    I64(Vec<i64>),
174    /// 32-bit float storage.
175    F32(Vec<f32>),
176    /// 64-bit float storage.
177    F64(Vec<f64>),
178}
179
180/// A mutable typed view of one raster row.
181pub enum RasterRowMut<'a> {
182    /// Unsigned 8-bit row.
183    U8(&'a mut [u8]),
184    /// Signed 8-bit row.
185    I8(&'a mut [i8]),
186    /// Unsigned 16-bit row.
187    U16(&'a mut [u16]),
188    /// Signed 16-bit row.
189    I16(&'a mut [i16]),
190    /// Unsigned 32-bit row.
191    U32(&'a mut [u32]),
192    /// Signed 32-bit row.
193    I32(&'a mut [i32]),
194    /// Unsigned 64-bit row.
195    U64(&'a mut [u64]),
196    /// Signed 64-bit row.
197    I64(&'a mut [i64]),
198    /// 32-bit float row.
199    F32(&'a mut [f32]),
200    /// 64-bit float row.
201    F64(&'a mut [f64]),
202}
203
204/// An immutable typed view of one raster row.
205pub enum RasterRowRef<'a> {
206    /// Unsigned 8-bit row.
207    U8(&'a [u8]),
208    /// Signed 8-bit row.
209    I8(&'a [i8]),
210    /// Unsigned 16-bit row.
211    U16(&'a [u16]),
212    /// Signed 16-bit row.
213    I16(&'a [i16]),
214    /// Unsigned 32-bit row.
215    U32(&'a [u32]),
216    /// Signed 32-bit row.
217    I32(&'a [i32]),
218    /// Unsigned 64-bit row.
219    U64(&'a [u64]),
220    /// Signed 64-bit row.
221    I64(&'a [i64]),
222    /// 32-bit float row.
223    F32(&'a [f32]),
224    /// 64-bit float row.
225    F64(&'a [f64]),
226}
227
228impl RasterData {
229    /// Create a typed data buffer of length `len`, filled with `value` converted
230    /// to the specified data type.
231    pub fn new_filled(data_type: DataType, len: usize, value: f64) -> Self {
232        match data_type {
233            DataType::U8 => Self::U8(vec![value as u8; len]),
234            DataType::I8 => Self::I8(vec![value as i8; len]),
235            DataType::U16 => Self::U16(vec![value as u16; len]),
236            DataType::I16 => Self::I16(vec![value as i16; len]),
237            DataType::U32 => Self::U32(vec![value as u32; len]),
238            DataType::I32 => Self::I32(vec![value as i32; len]),
239            DataType::U64 => Self::U64(vec![value as u64; len]),
240            DataType::I64 => Self::I64(vec![value as i64; len]),
241            DataType::F32 => Self::F32(vec![value as f32; len]),
242            DataType::F64 => Self::F64(vec![value; len]),
243        }
244    }
245
246    /// Create a typed data buffer of length `len` with **uninitialized** contents.
247    ///
248    /// # Safety
249    /// Every element must be written before any read. This is intended as a
250    /// performance fast-path for callers that immediately overwrite every cell
251    /// (e.g. `par_fill_with`), avoiding the redundant nodata-fill of `new_filled`.
252    pub fn new_uninit(data_type: DataType, len: usize) -> Self {
253        fn uninit_vec<T>(len: usize) -> Vec<T> {
254            let mut v = Vec::with_capacity(len);
255            // SAFETY: capacity == len, all elements are written by the caller
256            // before any read occurs.
257            unsafe { v.set_len(len) };
258            v
259        }
260        match data_type {
261            DataType::U8 => Self::U8(uninit_vec(len)),
262            DataType::I8 => Self::I8(uninit_vec(len)),
263            DataType::U16 => Self::U16(uninit_vec(len)),
264            DataType::I16 => Self::I16(uninit_vec(len)),
265            DataType::U32 => Self::U32(uninit_vec(len)),
266            DataType::I32 => Self::I32(uninit_vec(len)),
267            DataType::U64 => Self::U64(uninit_vec(len)),
268            DataType::I64 => Self::I64(uninit_vec(len)),
269            DataType::F32 => Self::F32(uninit_vec(len)),
270            DataType::F64 => Self::F64(uninit_vec(len)),
271        }
272    }
273
274    /// Convert an `f64` vector into typed storage.
275    pub fn from_f64_vec(data_type: DataType, data: Vec<f64>) -> Self {
276        match data_type {
277            DataType::U8 => Self::U8(data.into_iter().map(|v| v as u8).collect()),
278            DataType::I8 => Self::I8(data.into_iter().map(|v| v as i8).collect()),
279            DataType::U16 => Self::U16(data.into_iter().map(|v| v as u16).collect()),
280            DataType::I16 => Self::I16(data.into_iter().map(|v| v as i16).collect()),
281            DataType::U32 => Self::U32(data.into_iter().map(|v| v as u32).collect()),
282            DataType::I32 => Self::I32(data.into_iter().map(|v| v as i32).collect()),
283            DataType::U64 => Self::U64(data.into_iter().map(|v| v as u64).collect()),
284            DataType::I64 => Self::I64(data.into_iter().map(|v| v as i64).collect()),
285            DataType::F32 => Self::F32(data.into_iter().map(|v| v as f32).collect()),
286            DataType::F64 => Self::F64(data),
287        }
288    }
289
290    /// Number of stored cells.
291    pub fn len(&self) -> usize {
292        match self {
293            Self::U8(v) => v.len(),
294            Self::I8(v) => v.len(),
295            Self::U16(v) => v.len(),
296            Self::I16(v) => v.len(),
297            Self::U32(v) => v.len(),
298            Self::I32(v) => v.len(),
299            Self::U64(v) => v.len(),
300            Self::I64(v) => v.len(),
301            Self::F32(v) => v.len(),
302            Self::F64(v) => v.len(),
303        }
304    }
305
306    /// Returns `true` if no cells are stored.
307    pub fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310
311    /// Native stored data type.
312    pub fn data_type(&self) -> DataType {
313        match self {
314            Self::U8(_) => DataType::U8,
315            Self::I8(_) => DataType::I8,
316            Self::U16(_) => DataType::U16,
317            Self::I16(_) => DataType::I16,
318            Self::U32(_) => DataType::U32,
319            Self::I32(_) => DataType::I32,
320            Self::U64(_) => DataType::U64,
321            Self::I64(_) => DataType::I64,
322            Self::F32(_) => DataType::F32,
323            Self::F64(_) => DataType::F64,
324        }
325    }
326
327    /// Read one cell as `f64`.
328    pub fn get_f64(&self, idx: usize) -> f64 {
329        match self {
330            Self::U8(v) => v[idx] as f64,
331            Self::I8(v) => v[idx] as f64,
332            Self::U16(v) => v[idx] as f64,
333            Self::I16(v) => v[idx] as f64,
334            Self::U32(v) => v[idx] as f64,
335            Self::I32(v) => v[idx] as f64,
336            Self::U64(v) => v[idx] as f64,
337            Self::I64(v) => v[idx] as f64,
338            Self::F32(v) => v[idx] as f64,
339            Self::F64(v) => v[idx],
340        }
341    }
342
343    /// Set one cell from an `f64` value using native-type conversion.
344    pub fn set_f64(&mut self, idx: usize, value: f64) {
345        match self {
346            Self::U8(v) => v[idx] = value as u8,
347            Self::I8(v) => v[idx] = value as i8,
348            Self::U16(v) => v[idx] = value as u16,
349            Self::I16(v) => v[idx] = value as i16,
350            Self::U32(v) => v[idx] = value as u32,
351            Self::I32(v) => v[idx] = value as i32,
352            Self::U64(v) => v[idx] = value as u64,
353            Self::I64(v) => v[idx] = value as i64,
354            Self::F32(v) => v[idx] = value as f32,
355            Self::F64(v) => v[idx] = value,
356        }
357    }
358
359    /// Iterate over all cells as `f64`.
360    pub fn iter_f64(&self) -> Box<dyn Iterator<Item = f64> + '_> {
361        match self {
362            Self::U8(v) => Box::new(v.iter().copied().map(|x| x as f64)),
363            Self::I8(v) => Box::new(v.iter().copied().map(|x| x as f64)),
364            Self::U16(v) => Box::new(v.iter().copied().map(|x| x as f64)),
365            Self::I16(v) => Box::new(v.iter().copied().map(|x| x as f64)),
366            Self::U32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
367            Self::I32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
368            Self::U64(v) => Box::new(v.iter().copied().map(|x| x as f64)),
369            Self::I64(v) => Box::new(v.iter().copied().map(|x| x as f64)),
370            Self::F32(v) => Box::new(v.iter().copied().map(|x| x as f64)),
371            Self::F64(v) => Box::new(v.iter().copied()),
372        }
373    }
374
375    /// Materialize all cells as `Vec<f64>`.
376    pub fn to_f64_vec(&self) -> Vec<f64> {
377        self.iter_f64().collect()
378    }
379
380    /// Returns data as `u8` slice when storage is `U8`.
381    pub fn as_u8_slice(&self) -> Option<&[u8]> {
382        match self {
383            Self::U8(v) => Some(v.as_slice()),
384            _ => None,
385        }
386    }
387
388    /// Returns data as mutable `u8` slice when storage is `U8`.
389    pub fn as_u8_slice_mut(&mut self) -> Option<&mut [u8]> {
390        match self {
391            Self::U8(v) => Some(v.as_mut_slice()),
392            _ => None,
393        }
394    }
395
396    /// Returns data as `i8` slice when storage is `I8`.
397    pub fn as_i8_slice(&self) -> Option<&[i8]> {
398        match self {
399            Self::I8(v) => Some(v.as_slice()),
400            _ => None,
401        }
402    }
403
404    /// Returns data as mutable `i8` slice when storage is `I8`.
405    pub fn as_i8_slice_mut(&mut self) -> Option<&mut [i8]> {
406        match self {
407            Self::I8(v) => Some(v.as_mut_slice()),
408            _ => None,
409        }
410    }
411
412    /// Returns data as `u16` slice when storage is `U16`.
413    pub fn as_u16_slice(&self) -> Option<&[u16]> {
414        match self {
415            Self::U16(v) => Some(v.as_slice()),
416            _ => None,
417        }
418    }
419
420    /// Returns data as mutable `u16` slice when storage is `U16`.
421    pub fn as_u16_slice_mut(&mut self) -> Option<&mut [u16]> {
422        match self {
423            Self::U16(v) => Some(v.as_mut_slice()),
424            _ => None,
425        }
426    }
427
428    /// Returns data as `i16` slice when storage is `I16`.
429    pub fn as_i16_slice(&self) -> Option<&[i16]> {
430        match self {
431            Self::I16(v) => Some(v.as_slice()),
432            _ => None,
433        }
434    }
435
436    /// Returns data as mutable `i16` slice when storage is `I16`.
437    pub fn as_i16_slice_mut(&mut self) -> Option<&mut [i16]> {
438        match self {
439            Self::I16(v) => Some(v.as_mut_slice()),
440            _ => None,
441        }
442    }
443
444    /// Returns data as `u32` slice when storage is `U32`.
445    pub fn as_u32_slice(&self) -> Option<&[u32]> {
446        match self {
447            Self::U32(v) => Some(v.as_slice()),
448            _ => None,
449        }
450    }
451
452    /// Returns data as mutable `u32` slice when storage is `U32`.
453    pub fn as_u32_slice_mut(&mut self) -> Option<&mut [u32]> {
454        match self {
455            Self::U32(v) => Some(v.as_mut_slice()),
456            _ => None,
457        }
458    }
459
460    /// Returns data as `i32` slice when storage is `I32`.
461    pub fn as_i32_slice(&self) -> Option<&[i32]> {
462        match self {
463            Self::I32(v) => Some(v.as_slice()),
464            _ => None,
465        }
466    }
467
468    /// Returns data as mutable `i32` slice when storage is `I32`.
469    pub fn as_i32_slice_mut(&mut self) -> Option<&mut [i32]> {
470        match self {
471            Self::I32(v) => Some(v.as_mut_slice()),
472            _ => None,
473        }
474    }
475
476    /// Returns data as `u64` slice when storage is `U64`.
477    pub fn as_u64_slice(&self) -> Option<&[u64]> {
478        match self {
479            Self::U64(v) => Some(v.as_slice()),
480            _ => None,
481        }
482    }
483
484    /// Returns data as mutable `u64` slice when storage is `U64`.
485    pub fn as_u64_slice_mut(&mut self) -> Option<&mut [u64]> {
486        match self {
487            Self::U64(v) => Some(v.as_mut_slice()),
488            _ => None,
489        }
490    }
491
492    /// Returns data as `i64` slice when storage is `I64`.
493    pub fn as_i64_slice(&self) -> Option<&[i64]> {
494        match self {
495            Self::I64(v) => Some(v.as_slice()),
496            _ => None,
497        }
498    }
499
500    /// Returns data as mutable `i64` slice when storage is `I64`.
501    pub fn as_i64_slice_mut(&mut self) -> Option<&mut [i64]> {
502        match self {
503            Self::I64(v) => Some(v.as_mut_slice()),
504            _ => None,
505        }
506    }
507
508    /// Returns data as `f32` slice when storage is `F32`.
509    pub fn as_f32_slice(&self) -> Option<&[f32]> {
510        match self {
511            Self::F32(v) => Some(v.as_slice()),
512            _ => None,
513        }
514    }
515
516    /// Returns data as mutable `f32` slice when storage is `F32`.
517    pub fn as_f32_slice_mut(&mut self) -> Option<&mut [f32]> {
518        match self {
519            Self::F32(v) => Some(v.as_mut_slice()),
520            _ => None,
521        }
522    }
523
524    /// Returns data as `f64` slice when storage is `F64`.
525    pub fn as_f64_slice(&self) -> Option<&[f64]> {
526        match self {
527            Self::F64(v) => Some(v.as_slice()),
528            _ => None,
529        }
530    }
531
532    /// Returns data as mutable `f64` slice when storage is `F64`.
533    pub fn as_f64_slice_mut(&mut self) -> Option<&mut [f64]> {
534        match self {
535            Self::F64(v) => Some(v.as_mut_slice()),
536            _ => None,
537        }
538    }
539
540    /// Fill all cells in-place using a parallel closure `f(index) -> f64`.
541    ///
542    /// The index is the flat band-major, row-major cell index. For typed
543    /// storage other than `F64`, the returned `f64` is converted to the
544    /// native stored type before writing. The closure must be `Send + Sync`
545    /// so that Rayon can dispatch it across threads.
546    pub fn par_fill_with<F>(&mut self, f: F)
547    where
548        F: Fn(usize) -> f64 + Send + Sync,
549    {
550        match self {
551            Self::U8(v) => v
552                .par_iter_mut()
553                .enumerate()
554                .for_each(|(i, c)| *c = f(i) as u8),
555            Self::I8(v) => v
556                .par_iter_mut()
557                .enumerate()
558                .for_each(|(i, c)| *c = f(i) as i8),
559            Self::U16(v) => v
560                .par_iter_mut()
561                .enumerate()
562                .for_each(|(i, c)| *c = f(i) as u16),
563            Self::I16(v) => v
564                .par_iter_mut()
565                .enumerate()
566                .for_each(|(i, c)| *c = f(i) as i16),
567            Self::U32(v) => v
568                .par_iter_mut()
569                .enumerate()
570                .for_each(|(i, c)| *c = f(i) as u32),
571            Self::I32(v) => v
572                .par_iter_mut()
573                .enumerate()
574                .for_each(|(i, c)| *c = f(i) as i32),
575            Self::U64(v) => v
576                .par_iter_mut()
577                .enumerate()
578                .for_each(|(i, c)| *c = f(i) as u64),
579            Self::I64(v) => v
580                .par_iter_mut()
581                .enumerate()
582                .for_each(|(i, c)| *c = f(i) as i64),
583            Self::F32(v) => v
584                .par_iter_mut()
585                .enumerate()
586                .for_each(|(i, c)| *c = f(i) as f32),
587            Self::F64(v) => v.par_iter_mut().enumerate().for_each(|(i, c)| *c = f(i)),
588        }
589    }
590}
591
592// ─── NoData sentinel ─────────────────────────────────────────────────────────
593
594/// Represents the "no data" sentinel value for a raster layer.
595#[derive(Debug, Clone, Copy, PartialEq)]
596pub struct NoData(pub f64);
597
598impl NoData {
599    /// Common GIS nodata default.
600    pub const COMMON: NoData = NoData(-9999.0);
601    /// IEEE NaN-based nodata (compare with `is_nan()`).
602    pub const NAN: NoData = NoData(f64::NAN);
603
604    /// Returns `true` if `v` represents this nodata value.
605    #[inline]
606    pub fn matches(self, v: f64) -> bool {
607        if self.0.is_nan() {
608            v.is_nan()
609        } else {
610            (v - self.0).abs() < f64::EPSILON * self.0.abs().max(1.0)
611        }
612    }
613}
614
615impl Default for NoData {
616    fn default() -> Self {
617        Self::COMMON
618    }
619}
620
621// ─── Spatial extent ───────────────────────────────────────────────────────────
622
623/// Axis-aligned bounding box for a raster.
624#[derive(Debug, Clone, Copy, PartialEq)]
625pub struct Extent {
626    /// West edge (minimum X / longitude).
627    pub x_min: f64,
628    /// South edge (minimum Y / latitude).
629    pub y_min: f64,
630    /// East edge (maximum X / longitude).
631    pub x_max: f64,
632    /// North edge (maximum Y / latitude).
633    pub y_max: f64,
634}
635
636impl Extent {
637    /// Compute the extent from origin + grid dimensions.
638    pub fn from_origin(x_min: f64, y_min: f64, cols: usize, rows: usize, cell_size: f64) -> Self {
639        Self {
640            x_min,
641            y_min,
642            x_max: x_min + cols as f64 * cell_size,
643            y_max: y_min + rows as f64 * cell_size,
644        }
645    }
646
647    /// Width in spatial units.
648    pub fn width(&self) -> f64 {
649        self.x_max - self.x_min
650    }
651
652    /// Height in spatial units.
653    pub fn height(&self) -> f64 {
654        self.y_max - self.y_min
655    }
656}
657
658// ─── Statistics ───────────────────────────────────────────────────────────────
659
660/// Basic raster statistics.
661#[derive(Debug, Clone, Copy, PartialEq)]
662pub struct Statistics {
663    /// Minimum data value (excluding nodata).
664    pub min: f64,
665    /// Maximum data value (excluding nodata).
666    pub max: f64,
667    /// Mean of data values (excluding nodata).
668    pub mean: f64,
669    /// Standard deviation of data values (excluding nodata).
670    pub std_dev: f64,
671    /// Number of valid (non-nodata) cells.
672    pub valid_count: usize,
673    /// Number of nodata cells.
674    pub nodata_count: usize,
675}
676
677/// Selects the computation path used for raster statistics.
678#[derive(Debug, Clone, Copy, PartialEq, Eq)]
679pub enum StatisticsComputationMode {
680    /// Use the crate's default optimized path.
681    Auto,
682    /// Force the scalar accumulation path.
683    Scalar,
684    /// Force the SIMD accumulation path.
685    Simd,
686}
687
688#[derive(Debug, Clone, Copy)]
689struct StatsAccumulator {
690    min: f64,
691    max: f64,
692    sum: f64,
693    sum_sq: f64,
694    valid_count: usize,
695    nodata_count: usize,
696}
697
698impl Default for StatsAccumulator {
699    fn default() -> Self {
700        Self {
701            min: f64::INFINITY,
702            max: f64::NEG_INFINITY,
703            sum: 0.0,
704            sum_sq: 0.0,
705            valid_count: 0,
706            nodata_count: 0,
707        }
708    }
709}
710
711impl StatsAccumulator {
712    fn merge(&mut self, other: Self) {
713        self.min = self.min.min(other.min);
714        self.max = self.max.max(other.max);
715        self.sum += other.sum;
716        self.sum_sq += other.sum_sq;
717        self.valid_count += other.valid_count;
718        self.nodata_count += other.nodata_count;
719    }
720
721    fn to_statistics(self) -> Statistics {
722        let (mean, std_dev) = if self.valid_count == 0 {
723            (0.0, 0.0)
724        } else {
725            let n = self.valid_count as f64;
726            let mean = self.sum / n;
727            let variance = (self.sum_sq / n - mean * mean).max(0.0);
728            (mean, variance.sqrt())
729        };
730
731        Statistics {
732            min: if self.valid_count == 0 { 0.0 } else { self.min },
733            max: if self.valid_count == 0 { 0.0 } else { self.max },
734            mean,
735            std_dev,
736            valid_count: self.valid_count,
737            nodata_count: self.nodata_count,
738        }
739    }
740}
741
742/// Resampling method used during raster reprojection.
743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum ResampleMethod {
745    /// Nearest-neighbor sampling (fast, category-safe).
746    Nearest,
747    /// Bilinear interpolation (continuous surfaces).
748    Bilinear,
749    /// Bicubic interpolation (Catmull-Rom cubic convolution).
750    Cubic,
751    /// Lanczos interpolation (windowed sinc, radius 3).
752    Lanczos,
753    /// 3x3 mean filter around nearest source pixel center.
754    Average,
755    /// 3x3 minimum filter around nearest source pixel center.
756    Min,
757    /// 3x3 maximum filter around nearest source pixel center.
758    Max,
759    /// 3x3 mode filter around nearest source pixel center.
760    Mode,
761    /// 3x3 median filter around nearest source pixel center.
762    Median,
763    /// 3x3 standard-deviation filter around nearest source pixel center.
764    StdDev,
765}
766
767/// Nodata handling policy for interpolation-based reprojection.
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub enum NodataPolicy {
770    /// Require full valid kernel support; otherwise output nodata.
771    Strict,
772    /// Use available valid kernel samples and renormalize interpolation weights.
773    PartialKernel,
774    /// Try strict interpolation first, then fall back to nearest-neighbor sampling.
775    Fill,
776}
777
778/// Policy controlling longitude bound handling when antimeridian crossing is possible.
779#[derive(Debug, Clone, Copy, PartialEq, Eq)]
780pub enum AntimeridianPolicy {
781    /// Use linear bounds unless wrapped bounds are strictly narrower.
782    Auto,
783    /// Always use linear min/max longitude bounds.
784    Linear,
785    /// Always use wrapped minimal-arc longitude bounds.
786    Wrap,
787}
788
789/// Policy for converting resolution + extent to integer output dimensions.
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub enum GridSizePolicy {
792    /// Expand outward to fully cover requested extent (uses ceil sizing).
793    Expand,
794    /// Keep grid within requested extent (uses floor sizing, min 1 cell).
795    FitInside,
796}
797
798/// Destination-footprint handling mode during reprojection.
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub enum DestinationFootprint {
801    /// Do not apply a transformed-source footprint mask.
802    None,
803    /// Mask destination cells outside transformed source boundary ring.
804    SourceBoundary,
805}
806
807/// Options for raster reprojection.
808#[derive(Debug, Clone, Copy)]
809pub struct ReprojectOptions {
810    /// Destination CRS EPSG code.
811    pub dst_epsg: u32,
812    /// Resampling method.
813    pub resample: ResampleMethod,
814    /// Optional output column count (defaults to source `cols`).
815    pub cols: Option<usize>,
816    /// Optional output row count (defaults to source `rows`).
817    pub rows: Option<usize>,
818    /// Optional output extent in destination CRS.
819    ///
820    /// Defaults to transformed bounds of source extent corners.
821    pub extent: Option<Extent>,
822    /// Optional output X resolution in destination CRS units/pixel.
823    ///
824    /// Used to derive output `cols` when `cols` is not provided.
825    pub x_res: Option<f64>,
826    /// Optional output Y resolution in destination CRS units/pixel.
827    ///
828    /// Used to derive output `rows` when `rows` is not provided.
829    pub y_res: Option<f64>,
830    /// Optional snap origin X for resolution-derived output grid alignment.
831    ///
832    /// Used only when `x_res` is provided and `cols` is not explicitly set.
833    pub snap_x: Option<f64>,
834    /// Optional snap origin Y for resolution-derived output grid alignment.
835    ///
836    /// Used only when `y_res` is provided and `rows` is not explicitly set.
837    pub snap_y: Option<f64>,
838    /// Nodata policy used by interpolation methods.
839    pub nodata_policy: NodataPolicy,
840    /// Policy controlling antimeridian handling for geographic output bounds.
841    pub antimeridian_policy: AntimeridianPolicy,
842    /// Policy used when deriving integer output size from resolution controls.
843    pub grid_size_policy: GridSizePolicy,
844    /// Destination-footprint handling policy during reprojection.
845    pub destination_footprint: DestinationFootprint,
846    /// Emit non-fatal warnings when sampled source raster points appear outside
847    /// the declared area of use of source and/or destination CRS definitions.
848    pub warn_on_area_of_use_mismatch: bool,
849    /// Optional epoch-aware transform routing options.
850    pub epoch_transform: EpochTransformOptions,
851}
852
853impl ReprojectOptions {
854    /// Create options with required destination EPSG and resampling method.
855    pub fn new(dst_epsg: u32, resample: ResampleMethod) -> Self {
856        Self {
857            dst_epsg,
858            resample,
859            cols: None,
860            rows: None,
861            extent: None,
862            x_res: None,
863            y_res: None,
864            snap_x: None,
865            snap_y: None,
866            nodata_policy: NodataPolicy::PartialKernel,
867            antimeridian_policy: AntimeridianPolicy::Auto,
868            grid_size_policy: GridSizePolicy::Expand,
869            destination_footprint: DestinationFootprint::None,
870            warn_on_area_of_use_mismatch: false,
871            epoch_transform: EpochTransformOptions::default(),
872        }
873    }
874
875    /// Set nodata handling policy for interpolation and return updated options.
876    pub fn with_nodata_policy(mut self, nodata_policy: NodataPolicy) -> Self {
877        self.nodata_policy = nodata_policy;
878        self
879    }
880
881    /// Set output raster size (columns, rows) and return updated options.
882    pub fn with_size(mut self, cols: usize, rows: usize) -> Self {
883        self.cols = Some(cols);
884        self.rows = Some(rows);
885        self
886    }
887
888    /// Set output raster extent and return updated options.
889    pub fn with_extent(mut self, extent: Extent) -> Self {
890        self.extent = Some(extent);
891        self
892    }
893
894    /// Set output resolution (`x_res`, `y_res`) and return updated options.
895    ///
896    /// Positive finite values are required and interpreted as destination CRS
897    /// units per pixel.
898    pub fn with_resolution(mut self, x_res: f64, y_res: f64) -> Self {
899        self.x_res = Some(x_res);
900        self.y_res = Some(y_res);
901        self
902    }
903
904    /// Set isotropic output resolution and return updated options.
905    pub fn with_square_resolution(mut self, res: f64) -> Self {
906        self.x_res = Some(res);
907        self.y_res = Some(res);
908        self
909    }
910
911    /// Set snap origin for aligning resolution-derived output grids.
912    ///
913    /// Snap alignment is applied per axis only when that axis uses
914    /// resolution-derived sizing (i.e., no explicit `cols`/`rows`).
915    pub fn with_snap_origin(mut self, snap_x: f64, snap_y: f64) -> Self {
916        self.snap_x = Some(snap_x);
917        self.snap_y = Some(snap_y);
918        self
919    }
920
921    /// Set antimeridian handling policy for geographic output bounds.
922    pub fn with_antimeridian_policy(mut self, policy: AntimeridianPolicy) -> Self {
923        self.antimeridian_policy = policy;
924        self
925    }
926
927    /// Set sizing policy used for resolution-derived output dimensions.
928    pub fn with_grid_size_policy(mut self, policy: GridSizePolicy) -> Self {
929        self.grid_size_policy = policy;
930        self
931    }
932
933    /// Set destination-footprint handling policy for reprojection output.
934    pub fn with_destination_footprint(mut self, footprint: DestinationFootprint) -> Self {
935        self.destination_footprint = footprint;
936        self
937    }
938
939    /// Enable/disable non-fatal area-of-use mismatch warnings.
940    pub fn with_area_of_use_warning(mut self, enabled: bool) -> Self {
941        self.warn_on_area_of_use_mismatch = enabled;
942        self
943    }
944
945    /// Set epoch-aware transform routing options.
946    pub fn with_epoch_transform_options(mut self, epoch_transform: EpochTransformOptions) -> Self {
947        self.epoch_transform = epoch_transform;
948        self
949    }
950}
951
952// ─── Configuration / builder ──────────────────────────────────────────────────
953
954/// Parameters used to construct a new `Raster`.
955#[derive(Debug, Clone)]
956pub struct RasterConfig {
957    /// Number of columns (samples).
958    pub cols: usize,
959    /// Number of rows (lines).
960    pub rows: usize,
961    /// Number of bands.
962    pub bands: usize,
963    /// X coordinate of the west edge (left).
964    pub x_min: f64,
965    /// Y coordinate of the south edge (bottom).
966    pub y_min: f64,
967    /// Cell size (assumed square unless `cell_size_y` is set).
968    pub cell_size: f64,
969    /// Optional distinct Y cell size (negative = top-down raster).
970    /// If `None`, `cell_size` is used (positive, bottom-up convention).
971    pub cell_size_y: Option<f64>,
972    /// No-data sentinel value.
973    pub nodata: f64,
974    /// Underlying storage type.
975    pub data_type: DataType,
976    /// Spatial reference system.
977    pub crs: CrsInfo,
978    /// Free-form metadata key/value pairs.
979    pub metadata: Vec<(String, String)>,
980}
981
982impl Default for RasterConfig {
983    fn default() -> Self {
984        Self {
985            cols: 0,
986            rows: 0,
987            bands: 1,
988            x_min: 0.0,
989            y_min: 0.0,
990            cell_size: 1.0,
991            cell_size_y: None,
992            nodata: -9999.0,
993            data_type: DataType::F32,
994            crs: CrsInfo::default(),
995            metadata: Vec::new(),
996        }
997    }
998}
999
1000// ─── Main Raster struct ───────────────────────────────────────────────────────
1001
1002/// A raster grid with one or more bands.
1003///
1004/// Data is stored in a typed contiguous buffer matching `data_type`.
1005/// Conversion to `f64` happens only through accessor helpers when needed.
1006///
1007/// Layout: band-major, then row-major top-down within each band
1008/// (`index = band * rows * cols + row * cols + col`).
1009#[derive(Debug, Clone)]
1010pub struct Raster {
1011    /// Number of columns.
1012    pub cols: usize,
1013    /// Number of rows.
1014    pub rows: usize,
1015    /// Number of bands.
1016    pub bands: usize,
1017    /// West edge X coordinate.
1018    pub x_min: f64,
1019    /// South edge Y coordinate.
1020    pub y_min: f64,
1021    /// Cell width in map units (always positive).
1022    pub cell_size_x: f64,
1023    /// Cell height in map units (always positive, stored as absolute value).
1024    pub cell_size_y: f64,
1025    /// No-data value.
1026    pub nodata: f64,
1027    /// On-disk data type (used for writing).
1028    pub data_type: DataType,
1029    /// Spatial reference.
1030    pub crs: CrsInfo,
1031    /// Free-form key/value metadata.
1032    pub metadata: Vec<(String, String)>,
1033    /// Raw data buffer, band-major then row-major top-down. Length = `bands * cols * rows`.
1034    pub data: RasterData,
1035}
1036
1037impl Raster {
1038    // ─── Construction ──────────────────────────────────────────────────────
1039
1040    /// Create a new raster from a `RasterConfig`, filling all cells with `nodata`.
1041    pub fn new(cfg: RasterConfig) -> Self {
1042        let bands = cfg.bands.max(1);
1043        let n = cfg.cols * cfg.rows * bands;
1044        let cell_size_y = cfg.cell_size_y.map(|v| v.abs()).unwrap_or(cfg.cell_size);
1045        Self {
1046            cols: cfg.cols,
1047            rows: cfg.rows,
1048            bands,
1049            x_min: cfg.x_min,
1050            y_min: cfg.y_min,
1051            cell_size_x: cfg.cell_size,
1052            cell_size_y,
1053            nodata: cfg.nodata,
1054            data_type: cfg.data_type,
1055            crs: cfg.crs,
1056            metadata: cfg.metadata,
1057            data: RasterData::new_filled(cfg.data_type, n, cfg.nodata),
1058        }
1059    }
1060
1061    /// Create a raster from a raw `f64` data buffer.
1062    ///
1063    /// # Errors
1064    /// Returns [`RasterError::InvalidDimensions`] if `data.len() != cols * rows * bands`.
1065    pub fn from_data(cfg: RasterConfig, data: Vec<f64>) -> Result<Self> {
1066        let bands = cfg.bands.max(1);
1067        if data.len() != cfg.cols * cfg.rows * bands {
1068            return Err(RasterError::InvalidDimensions {
1069                cols: cfg.cols,
1070                rows: cfg.rows,
1071            });
1072        }
1073        let dt = cfg.data_type;
1074        let mut r = Self::new(cfg);
1075        r.data = RasterData::from_f64_vec(dt, data);
1076        Ok(r)
1077    }
1078
1079    /// Create a raster from a typed data buffer.
1080    ///
1081    /// # Errors
1082    /// Returns [`RasterError::InvalidDimensions`] if `data.len() != cols * rows * bands`.
1083    /// Returns [`RasterError::Other`] if `cfg.data_type != data.data_type()`.
1084    pub fn from_data_native(cfg: RasterConfig, data: RasterData) -> Result<Self> {
1085        let bands = cfg.bands.max(1);
1086        if data.len() != cfg.cols * cfg.rows * bands {
1087            return Err(RasterError::InvalidDimensions {
1088                cols: cfg.cols,
1089                rows: cfg.rows,
1090            });
1091        }
1092        if cfg.data_type != data.data_type() {
1093            return Err(RasterError::Other(format!(
1094                "data_type mismatch: config={}, data={}",
1095                cfg.data_type,
1096                data.data_type()
1097            )));
1098        }
1099        let mut r = Self::new(cfg);
1100        r.data = data;
1101        Ok(r)
1102    }
1103
1104    /// Create a new raster that reuses spatial metadata and layout from `template`.
1105    ///
1106    /// Cell values are initialized to the template's nodata value.
1107    pub fn new_like(template: &Raster) -> Self {
1108        Self::new(RasterConfig {
1109            cols: template.cols,
1110            rows: template.rows,
1111            bands: template.bands,
1112            x_min: template.x_min,
1113            y_min: template.y_min,
1114            cell_size: template.cell_size_x,
1115            cell_size_y: Some(template.cell_size_y),
1116            nodata: template.nodata,
1117            data_type: template.data_type,
1118            crs: template.crs.clone(),
1119            metadata: template.metadata.clone(),
1120        })
1121    }
1122
1123    /// Like [`new_like`] but skips initializing the data buffer.
1124    ///
1125    /// Use this when every cell will be written before any read (e.g. immediately
1126    /// followed by `par_fill_with`). Avoids a redundant full-buffer nodata write.
1127    pub fn new_like_uninit(template: &Raster) -> Self {
1128        let bands = template.bands.max(1);
1129        let n = template.cols * template.rows * bands;
1130        let cell_size_y = template.cell_size_y;
1131        Self {
1132            cols: template.cols,
1133            rows: template.rows,
1134            bands,
1135            x_min: template.x_min,
1136            y_min: template.y_min,
1137            cell_size_x: template.cell_size_x,
1138            cell_size_y,
1139            nodata: template.nodata,
1140            data_type: template.data_type,
1141            crs: template.crs.clone(),
1142            metadata: template.metadata.clone(),
1143            data: RasterData::new_uninit(template.data_type, n),
1144        }
1145    }
1146
1147    /// Typed fast-path access to `u8` storage.
1148    pub fn data_u8(&self) -> Option<&[u8]> {
1149        self.data.as_u8_slice()
1150    }
1151    /// Typed fast-path mutable access to `u8` storage.
1152    pub fn data_u8_mut(&mut self) -> Option<&mut [u8]> {
1153        self.data.as_u8_slice_mut()
1154    }
1155
1156    /// Typed fast-path access to `i8` storage.
1157    pub fn data_i8(&self) -> Option<&[i8]> {
1158        self.data.as_i8_slice()
1159    }
1160    /// Typed fast-path mutable access to `i8` storage.
1161    pub fn data_i8_mut(&mut self) -> Option<&mut [i8]> {
1162        self.data.as_i8_slice_mut()
1163    }
1164
1165    /// Typed fast-path access to `u16` storage.
1166    pub fn data_u16(&self) -> Option<&[u16]> {
1167        self.data.as_u16_slice()
1168    }
1169    /// Typed fast-path mutable access to `u16` storage.
1170    pub fn data_u16_mut(&mut self) -> Option<&mut [u16]> {
1171        self.data.as_u16_slice_mut()
1172    }
1173
1174    /// Typed fast-path access to `i16` storage.
1175    pub fn data_i16(&self) -> Option<&[i16]> {
1176        self.data.as_i16_slice()
1177    }
1178    /// Typed fast-path mutable access to `i16` storage.
1179    pub fn data_i16_mut(&mut self) -> Option<&mut [i16]> {
1180        self.data.as_i16_slice_mut()
1181    }
1182
1183    /// Typed fast-path access to `u32` storage.
1184    pub fn data_u32(&self) -> Option<&[u32]> {
1185        self.data.as_u32_slice()
1186    }
1187    /// Typed fast-path mutable access to `u32` storage.
1188    pub fn data_u32_mut(&mut self) -> Option<&mut [u32]> {
1189        self.data.as_u32_slice_mut()
1190    }
1191
1192    /// Typed fast-path access to `i32` storage.
1193    pub fn data_i32(&self) -> Option<&[i32]> {
1194        self.data.as_i32_slice()
1195    }
1196    /// Typed fast-path mutable access to `i32` storage.
1197    pub fn data_i32_mut(&mut self) -> Option<&mut [i32]> {
1198        self.data.as_i32_slice_mut()
1199    }
1200
1201    /// Typed fast-path access to `u64` storage.
1202    pub fn data_u64(&self) -> Option<&[u64]> {
1203        self.data.as_u64_slice()
1204    }
1205    /// Typed fast-path mutable access to `u64` storage.
1206    pub fn data_u64_mut(&mut self) -> Option<&mut [u64]> {
1207        self.data.as_u64_slice_mut()
1208    }
1209
1210    /// Typed fast-path access to `i64` storage.
1211    pub fn data_i64(&self) -> Option<&[i64]> {
1212        self.data.as_i64_slice()
1213    }
1214    /// Typed fast-path mutable access to `i64` storage.
1215    pub fn data_i64_mut(&mut self) -> Option<&mut [i64]> {
1216        self.data.as_i64_slice_mut()
1217    }
1218
1219    /// Typed fast-path access to `f32` storage.
1220    pub fn data_f32(&self) -> Option<&[f32]> {
1221        self.data.as_f32_slice()
1222    }
1223    /// Typed fast-path mutable access to `f32` storage.
1224    pub fn data_f32_mut(&mut self) -> Option<&mut [f32]> {
1225        self.data.as_f32_slice_mut()
1226    }
1227
1228    /// Typed fast-path access to `f64` storage.
1229    pub fn data_f64(&self) -> Option<&[f64]> {
1230        self.data.as_f64_slice()
1231    }
1232    /// Typed fast-path mutable access to `f64` storage.
1233    pub fn data_f64_mut(&mut self) -> Option<&mut [f64]> {
1234        self.data.as_f64_slice_mut()
1235    }
1236
1237    /// Materialize one band (zero-based) as a [`BandView`].
1238    ///
1239    /// This is the canonical input path for tool kernels that need per-cell read
1240    /// access without explicit bounds checks or type dispatch at every call site.
1241    /// Call once at tool entry, wrap in `Arc`, share across worker threads, and
1242    /// call `view.get(row, col)` in the hot loop.
1243    ///
1244    /// For `F64` rasters the internal buffer is a direct subslice clone (one
1245    /// allocation). For all other storage types each cell is converted once.
1246    pub fn band_view(&self, band: usize) -> BandView {
1247        BandView {
1248            data: self.band_to_vec_f64(band),
1249            rows: self.rows as isize,
1250            cols: self.cols as isize,
1251            nodata: self.nodata,
1252        }
1253    }
1254
1255    /// Returns a direct reference to the raw `f64` storage for a single band (zero-based)
1256    /// when the raster's native storage type is `F64`, otherwise returns `None`.
1257    ///
1258    /// The slice has length `rows * cols` and is indexed as `row * cols + col`.
1259    ///
1260    /// Use this as a zero-copy fast path before spawning worker threads on `F64` rasters
1261    /// (e.g. DEMs). For non-`F64` rasters, call [`band_to_vec_f64`] instead to get a
1262    /// converted, owned buffer with the same indexing convention.
1263    #[inline]
1264    pub fn band_as_f64_slice(&self, band: usize) -> Option<&[f64]> {
1265        let stride = self.rows * self.cols;
1266        let start = band * stride;
1267        self.data.as_f64_slice()?.get(start..start + stride)
1268    }
1269
1270    /// Materializes one band (zero-based) as a flat, row-major `Vec<f64>`.
1271    ///
1272    /// For `F64` rasters this clones the band's subslice directly (one allocation,
1273    /// no per-cell conversion). For all other storage types each cell is converted
1274    /// from the native type in a single pass.
1275    ///
1276    /// The returned buffer has length `rows * cols` and is indexed as
1277    /// `row * cols + col`. Out-of-bounds values are the caller's responsibility;
1278    /// respect `raster.rows` and `raster.cols`.
1279    ///
1280    /// **Use this once before spawning worker threads** so that hot kernels can index
1281    /// a plain `Vec` directly rather than going through the generic [`get`] accessor
1282    /// on every cell access.
1283    ///
1284    /// ```rust
1285    /// let buf = raster.band_to_vec_f64(0);
1286    /// let z = buf[row * cols + col];  // no per-cell dispatch overhead
1287    /// ```
1288    pub fn band_to_vec_f64(&self, band: usize) -> Vec<f64> {
1289        let stride = self.rows * self.cols;
1290        let start = band * stride;
1291        let end = start + stride;
1292        match &self.data {
1293            RasterData::F64(v) => v[start..end].to_vec(),
1294            RasterData::F32(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1295            RasterData::U8(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1296            RasterData::I8(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1297            RasterData::U16(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1298            RasterData::I16(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1299            RasterData::U32(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1300            RasterData::I32(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1301            RasterData::U64(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1302            RasterData::I64(v) => v[start..end].iter().map(|&x| x as f64).collect(),
1303        }
1304    }
1305
1306    /// Fill all cells in-place using a parallel closure `f(index) -> f64`.
1307    ///
1308    /// The index is the flat band-major, row-major cell index. For typed
1309    /// storage other than `F64`, the returned `f64` is down-cast to the
1310    /// native stored type before writing.
1311    pub fn par_fill_with<F>(&mut self, f: F)
1312    where
1313        F: Fn(usize) -> f64 + Send + Sync,
1314    {
1315        self.data.par_fill_with(f);
1316    }
1317
1318    /// Apply a unary math operation to selected bands (or all bands if `target_bands` is `None`).
1319    ///
1320    /// Operates **in-place** on `self`. For floating-point rasters (F32/F64), uses direct typed
1321    /// slice access + SIMD-friendly iteration. For integer rasters, falls back to per-band copy-loop.
1322    ///
1323    /// If `target_bands` is `None`, operates on the entire flat buffer with fast F32/F64 paths.
1324    /// If `target_bands` is `Some(vec)`, operates only on those band indices via per-band slicing.
1325    ///
1326    /// # Errors
1327    /// Returns an error if a band index is out of bounds (only when `target_bands` is `Some`).
1328    pub fn apply_unary_math<F>(&mut self, f: F, target_bands: Option<Vec<usize>>) -> Result<()>
1329    where
1330        F: Fn(f64) -> f64 + Send + Sync,
1331    {
1332        let nodata = self.nodata;
1333        let nodata_is_nan = nodata.is_nan();
1334
1335        match target_bands {
1336            None => {
1337                // Fast path: operate on all data via flat buffer (most common for tools).
1338                // F32 direct access with no enum dispatch.
1339                if let Some(data) = self.data.as_f32_slice_mut() {
1340                    let nodata_f32 = nodata as f32;
1341                    data.par_iter_mut().for_each(|v| {
1342                        let zf = *v as f64;
1343                        let is_nd = if nodata_is_nan {
1344                            zf.is_nan()
1345                        } else {
1346                            *v == nodata_f32
1347                        };
1348                        if !is_nd {
1349                            *v = f(zf) as f32;
1350                        }
1351                    });
1352                // F64 direct access.
1353                } else if let Some(data) = self.data.as_f64_slice_mut() {
1354                    data.par_iter_mut().for_each(|v| {
1355                        let is_nd = if nodata_is_nan {
1356                            v.is_nan()
1357                        } else {
1358                            *v == nodata
1359                        };
1360                        if !is_nd {
1361                            *v = f(*v);
1362                        }
1363                    });
1364                // Generic fallback for integer types.
1365                } else {
1366                    let len = self.data.len();
1367                    for i in 0..len {
1368                        let z = self.data.get_f64(i);
1369                        if nodata_is_nan {
1370                            if !z.is_nan() {
1371                                self.data.set_f64(i, f(z));
1372                            }
1373                        } else if z != nodata {
1374                            self.data.set_f64(i, f(z));
1375                        }
1376                    }
1377                }
1378            }
1379            Some(bands) => {
1380                // Per-band operation for selective application.
1381                for band in bands {
1382                    if band >= self.bands {
1383                        return Err(RasterError::OutOfBounds {
1384                            band: band as isize,
1385                            row: 0,
1386                            col: 0,
1387                            bands: self.bands,
1388                            cols: self.cols,
1389                            rows: self.rows,
1390                        });
1391                    }
1392                    let mut vals = self.band_slice(band as isize);
1393                    vals.par_iter_mut().for_each(|v| {
1394                        let is_nd = if nodata_is_nan {
1395                            v.is_nan()
1396                        } else {
1397                            *v == nodata
1398                        };
1399                        if !is_nd {
1400                            *v = f(*v);
1401                        }
1402                    });
1403                    self.set_band_slice(band as isize, &vals)?;
1404                }
1405            }
1406        }
1407
1408        Ok(())
1409    }
1410
1411    /// Apply a unary math operation reading from `src` and writing to `self`.
1412    ///
1413    /// Copies each cell value from `src`, applies the operation, and stores in `self`.
1414    /// Uses direct typed slice access (F32/F64) for performance.
1415    ///
1416    /// # Errors
1417    /// Returns an error if rasters have different dimensions.
1418    pub fn apply_unary_math_from<F>(&mut self, f: F, src: &Raster) -> Result<()>
1419    where
1420        F: Fn(f64) -> f64 + Send + Sync,
1421    {
1422        if self.rows != src.rows || self.cols != src.cols || self.bands != src.bands {
1423            return Err(RasterError::Other(format!(
1424                "raster dimension mismatch: self {}×{}×{} != src {}×{}×{}",
1425                self.bands, self.rows, self.cols, src.bands, src.rows, src.cols
1426            )));
1427        }
1428
1429        let nodata = src.nodata;
1430        let nodata_is_nan = nodata.is_nan();
1431
1432        // Fast path: F32 input/output.
1433        if let (Some(src_data), Some(dst_data)) =
1434            (src.data.as_f32_slice(), self.data.as_f32_slice_mut())
1435        {
1436            let nodata_f32 = nodata as f32;
1437            dst_data
1438                .par_iter_mut()
1439                .zip(src_data.par_iter())
1440                .for_each(|(out, &z)| {
1441                    let zf = z as f64;
1442                    let is_nd = if nodata_is_nan {
1443                        zf.is_nan()
1444                    } else {
1445                        z == nodata_f32
1446                    };
1447                    *out = if is_nd { nodata_f32 } else { f(zf) as f32 };
1448                });
1449        // Fast path: F64 input/output.
1450        } else if let (Some(src_data), Some(dst_data)) =
1451            (src.data.as_f64_slice(), self.data.as_f64_slice_mut())
1452        {
1453            dst_data
1454                .par_iter_mut()
1455                .zip(src_data.par_iter())
1456                .for_each(|(out, &z)| {
1457                    let is_nd = if nodata_is_nan {
1458                        z.is_nan()
1459                    } else {
1460                        z == nodata
1461                    };
1462                    *out = if is_nd { nodata } else { f(z) };
1463                });
1464        // Generic fallback for mixed or integer types.
1465        } else {
1466            let len = self.data.len();
1467            for i in 0..len {
1468                let z = src.data.get_f64(i);
1469                let result = if nodata_is_nan {
1470                    if z.is_nan() {
1471                        nodata
1472                    } else {
1473                        f(z)
1474                    }
1475                } else if z == nodata {
1476                    nodata
1477                } else {
1478                    f(z)
1479                };
1480                self.data.set_f64(i, result);
1481            }
1482        }
1483
1484        Ok(())
1485    }
1486
1487    /// Apply a binary math operation from two source rasters, writing results into `self`.
1488    ///
1489    /// `self` must be a freshly-allocated output (e.g. from `Raster::new_like`). The closure
1490    /// `f(z1, z2) -> f64` is called only for cell pairs where neither source is nodata.
1491    /// When either source is nodata the output cell is set to `self.nodata`.
1492    ///
1493    /// Fast paths: when `self`, `src1`, and `src2` are all F32 or all F64 the typed slices
1494    /// are zipped in parallel without any enum dispatch per cell.
1495    ///
1496    /// # Errors
1497    /// Returns an error if the raster dimensions do not all match.
1498    pub fn apply_binary_math_from<F>(&mut self, f: F, src1: &Raster, src2: &Raster) -> Result<()>
1499    where
1500        F: Fn(f64, f64) -> f64 + Send + Sync,
1501    {
1502        if self.rows != src1.rows
1503            || self.cols != src1.cols
1504            || self.bands != src1.bands
1505            || src1.rows != src2.rows
1506            || src1.cols != src2.cols
1507            || src1.bands != src2.bands
1508        {
1509            return Err(RasterError::Other(format!(
1510                "raster dimension mismatch: self {}×{}×{}, src1 {}×{}×{}, src2 {}×{}×{}",
1511                self.bands,
1512                self.rows,
1513                self.cols,
1514                src1.bands,
1515                src1.rows,
1516                src1.cols,
1517                src2.bands,
1518                src2.rows,
1519                src2.cols,
1520            )));
1521        }
1522
1523        let nd1 = src1.nodata;
1524        let nd1_is_nan = nd1.is_nan();
1525        let nd2 = src2.nodata;
1526        let nd2_is_nan = nd2.is_nan();
1527        let nd_out = self.nodata;
1528
1529        let is_nd1 = |v: f32| -> bool {
1530            if nd1_is_nan {
1531                (v as f64).is_nan()
1532            } else {
1533                v == nd1 as f32
1534            }
1535        };
1536        let is_nd2 = |v: f32| -> bool {
1537            if nd2_is_nan {
1538                (v as f64).is_nan()
1539            } else {
1540                v == nd2 as f32
1541            }
1542        };
1543        let is_nd1_f64 = |v: f64| -> bool {
1544            if nd1_is_nan {
1545                v.is_nan()
1546            } else {
1547                v == nd1
1548            }
1549        };
1550        let is_nd2_f64 = |v: f64| -> bool {
1551            if nd2_is_nan {
1552                v.is_nan()
1553            } else {
1554                v == nd2
1555            }
1556        };
1557
1558        // Fast path family: destination + src1 are F32, src2 may be any native storage.
1559        if let (Some(d1), Some(dst)) = (src1.data.as_f32_slice(), self.data.as_f32_slice_mut()) {
1560            let nd_out_f32 = nd_out as f32;
1561
1562            if let Some(d2) = src2.data.as_f32_slice() {
1563                dst.par_iter_mut()
1564                    .zip(d1.par_iter())
1565                    .zip(d2.par_iter())
1566                    .for_each(|((out, &z1), &z2)| {
1567                        if is_nd1(z1) || is_nd2(z2) {
1568                            *out = nd_out_f32;
1569                        } else {
1570                            *out = f(z1 as f64, z2 as f64) as f32;
1571                        }
1572                    });
1573                return Ok(());
1574            }
1575
1576            macro_rules! run_f32_rhs_typed {
1577                ($rhs:expr) => {{
1578                    dst.par_iter_mut()
1579                        .zip(d1.par_iter())
1580                        .zip($rhs.par_iter())
1581                        .for_each(|((out, &z1), &z2)| {
1582                            let z2f = z2 as f64;
1583                            let z2_is_nd = if nd2_is_nan { z2f.is_nan() } else { z2f == nd2 };
1584                            if is_nd1(z1) || z2_is_nd {
1585                                *out = nd_out_f32;
1586                            } else {
1587                                *out = f(z1 as f64, z2f) as f32;
1588                            }
1589                        });
1590                    return Ok(());
1591                }};
1592            }
1593
1594            if let Some(d2) = src2.data.as_f64_slice() {
1595                dst.par_iter_mut()
1596                    .zip(d1.par_iter())
1597                    .zip(d2.par_iter())
1598                    .for_each(|((out, &z1), &z2)| {
1599                        if is_nd1(z1) || is_nd2_f64(z2) {
1600                            *out = nd_out_f32;
1601                        } else {
1602                            *out = f(z1 as f64, z2) as f32;
1603                        }
1604                    });
1605                return Ok(());
1606            }
1607            if let Some(d2) = src2.data.as_u8_slice() {
1608                run_f32_rhs_typed!(d2);
1609            }
1610            if let Some(d2) = src2.data.as_i8_slice() {
1611                run_f32_rhs_typed!(d2);
1612            }
1613            if let Some(d2) = src2.data.as_u16_slice() {
1614                run_f32_rhs_typed!(d2);
1615            }
1616            if let Some(d2) = src2.data.as_i16_slice() {
1617                run_f32_rhs_typed!(d2);
1618            }
1619            if let Some(d2) = src2.data.as_u32_slice() {
1620                run_f32_rhs_typed!(d2);
1621            }
1622            if let Some(d2) = src2.data.as_i32_slice() {
1623                run_f32_rhs_typed!(d2);
1624            }
1625            if let Some(d2) = src2.data.as_u64_slice() {
1626                run_f32_rhs_typed!(d2);
1627            }
1628            if let Some(d2) = src2.data.as_i64_slice() {
1629                run_f32_rhs_typed!(d2);
1630            }
1631        }
1632
1633        // Fast path family: destination + src1 are F64, src2 may be any native storage.
1634        if let (Some(d1), Some(dst)) = (src1.data.as_f64_slice(), self.data.as_f64_slice_mut()) {
1635            if let Some(d2) = src2.data.as_f64_slice() {
1636                dst.par_iter_mut()
1637                    .zip(d1.par_iter())
1638                    .zip(d2.par_iter())
1639                    .for_each(|((out, &z1), &z2)| {
1640                        if is_nd1_f64(z1) || is_nd2_f64(z2) {
1641                            *out = nd_out;
1642                        } else {
1643                            *out = f(z1, z2);
1644                        }
1645                    });
1646                return Ok(());
1647            }
1648
1649            macro_rules! run_f64_rhs_typed {
1650                ($rhs:expr) => {{
1651                    dst.par_iter_mut()
1652                        .zip(d1.par_iter())
1653                        .zip($rhs.par_iter())
1654                        .for_each(|((out, &z1), &z2)| {
1655                            let z2f = z2 as f64;
1656                            let z2_is_nd = if nd2_is_nan { z2f.is_nan() } else { z2f == nd2 };
1657                            if is_nd1_f64(z1) || z2_is_nd {
1658                                *out = nd_out;
1659                            } else {
1660                                *out = f(z1, z2f);
1661                            }
1662                        });
1663                    return Ok(());
1664                }};
1665            }
1666
1667            if let Some(d2) = src2.data.as_f32_slice() {
1668                dst.par_iter_mut()
1669                    .zip(d1.par_iter())
1670                    .zip(d2.par_iter())
1671                    .for_each(|((out, &z1), &z2)| {
1672                        if is_nd1_f64(z1) || is_nd2(z2) {
1673                            *out = nd_out;
1674                        } else {
1675                            *out = f(z1, z2 as f64);
1676                        }
1677                    });
1678                return Ok(());
1679            }
1680            if let Some(d2) = src2.data.as_u8_slice() {
1681                run_f64_rhs_typed!(d2);
1682            }
1683            if let Some(d2) = src2.data.as_i8_slice() {
1684                run_f64_rhs_typed!(d2);
1685            }
1686            if let Some(d2) = src2.data.as_u16_slice() {
1687                run_f64_rhs_typed!(d2);
1688            }
1689            if let Some(d2) = src2.data.as_i16_slice() {
1690                run_f64_rhs_typed!(d2);
1691            }
1692            if let Some(d2) = src2.data.as_u32_slice() {
1693                run_f64_rhs_typed!(d2);
1694            }
1695            if let Some(d2) = src2.data.as_i32_slice() {
1696                run_f64_rhs_typed!(d2);
1697            }
1698            if let Some(d2) = src2.data.as_u64_slice() {
1699                run_f64_rhs_typed!(d2);
1700            }
1701            if let Some(d2) = src2.data.as_i64_slice() {
1702                run_f64_rhs_typed!(d2);
1703            }
1704        }
1705
1706        // Fast path family: destination + src1 are I16, src2 may be any native storage.
1707        // Covers D8 flow-pointer rasters and other signed-16-bit outputs.
1708        if let (Some(d1), Some(dst)) = (src1.data.as_i16_slice(), self.data.as_i16_slice_mut()) {
1709            let nd1_i16 = nd1 as i16;
1710            let nd_out_i16 = nd_out as i16;
1711            let chk1_i16 = |v: i16| v == nd1_i16;
1712
1713            macro_rules! run_i16_rhs_typed {
1714                ($rhs:expr) => {{
1715                    dst.par_iter_mut()
1716                        .zip(d1.par_iter())
1717                        .zip($rhs.par_iter())
1718                        .for_each(|((out, &z1), &z2)| {
1719                            let z2f = z2 as f64;
1720                            let z2_is_nd = if nd2_is_nan { z2f.is_nan() } else { z2f == nd2 };
1721                            *out = if chk1_i16(z1) || z2_is_nd {
1722                                nd_out_i16
1723                            } else {
1724                                f(z1 as f64, z2f) as i16
1725                            };
1726                        });
1727                    return Ok(());
1728                }};
1729            }
1730
1731            if let Some(d2) = src2.data.as_f32_slice() {
1732                dst.par_iter_mut()
1733                    .zip(d1.par_iter())
1734                    .zip(d2.par_iter())
1735                    .for_each(|((out, &z1), &z2)| {
1736                        *out = if chk1_i16(z1) || is_nd2(z2) {
1737                            nd_out_i16
1738                        } else {
1739                            f(z1 as f64, z2 as f64) as i16
1740                        };
1741                    });
1742                return Ok(());
1743            }
1744            if let Some(d2) = src2.data.as_f64_slice() {
1745                run_i16_rhs_typed!(d2);
1746            }
1747            if let Some(d2) = src2.data.as_u8_slice() {
1748                run_i16_rhs_typed!(d2);
1749            }
1750            if let Some(d2) = src2.data.as_i8_slice() {
1751                run_i16_rhs_typed!(d2);
1752            }
1753            if let Some(d2) = src2.data.as_u16_slice() {
1754                run_i16_rhs_typed!(d2);
1755            }
1756            if let Some(d2) = src2.data.as_i16_slice() {
1757                run_i16_rhs_typed!(d2);
1758            }
1759            if let Some(d2) = src2.data.as_u32_slice() {
1760                run_i16_rhs_typed!(d2);
1761            }
1762            if let Some(d2) = src2.data.as_i32_slice() {
1763                run_i16_rhs_typed!(d2);
1764            }
1765            if let Some(d2) = src2.data.as_u64_slice() {
1766                run_i16_rhs_typed!(d2);
1767            }
1768            if let Some(d2) = src2.data.as_i64_slice() {
1769                run_i16_rhs_typed!(d2);
1770            }
1771        }
1772
1773        // Fast path family: destination + src1 are U8, src2 may be any native storage.
1774        // Covers binary classification masks and other unsigned-8-bit outputs.
1775        if let (Some(d1), Some(dst)) = (src1.data.as_u8_slice(), self.data.as_u8_slice_mut()) {
1776            let nd1_u8 = nd1 as u8;
1777            let nd_out_u8 = nd_out as u8;
1778            let chk1_u8 = |v: u8| v == nd1_u8;
1779
1780            macro_rules! run_u8_rhs_typed {
1781                ($rhs:expr) => {{
1782                    dst.par_iter_mut()
1783                        .zip(d1.par_iter())
1784                        .zip($rhs.par_iter())
1785                        .for_each(|((out, &z1), &z2)| {
1786                            let z2f = z2 as f64;
1787                            let z2_is_nd = if nd2_is_nan { z2f.is_nan() } else { z2f == nd2 };
1788                            *out = if chk1_u8(z1) || z2_is_nd {
1789                                nd_out_u8
1790                            } else {
1791                                f(z1 as f64, z2f) as u8
1792                            };
1793                        });
1794                    return Ok(());
1795                }};
1796            }
1797
1798            if let Some(d2) = src2.data.as_f32_slice() {
1799                dst.par_iter_mut()
1800                    .zip(d1.par_iter())
1801                    .zip(d2.par_iter())
1802                    .for_each(|((out, &z1), &z2)| {
1803                        *out = if chk1_u8(z1) || is_nd2(z2) {
1804                            nd_out_u8
1805                        } else {
1806                            f(z1 as f64, z2 as f64) as u8
1807                        };
1808                    });
1809                return Ok(());
1810            }
1811            if let Some(d2) = src2.data.as_f64_slice() {
1812                run_u8_rhs_typed!(d2);
1813            }
1814            if let Some(d2) = src2.data.as_u8_slice() {
1815                run_u8_rhs_typed!(d2);
1816            }
1817            if let Some(d2) = src2.data.as_i8_slice() {
1818                run_u8_rhs_typed!(d2);
1819            }
1820            if let Some(d2) = src2.data.as_u16_slice() {
1821                run_u8_rhs_typed!(d2);
1822            }
1823            if let Some(d2) = src2.data.as_i16_slice() {
1824                run_u8_rhs_typed!(d2);
1825            }
1826            if let Some(d2) = src2.data.as_u32_slice() {
1827                run_u8_rhs_typed!(d2);
1828            }
1829            if let Some(d2) = src2.data.as_i32_slice() {
1830                run_u8_rhs_typed!(d2);
1831            }
1832            if let Some(d2) = src2.data.as_u64_slice() {
1833                run_u8_rhs_typed!(d2);
1834            }
1835            if let Some(d2) = src2.data.as_i64_slice() {
1836                run_u8_rhs_typed!(d2);
1837            }
1838        }
1839
1840        // Generic parallel fallback for any remaining type combinations
1841        // (e.g. I32/U16/U32/I64/U64 as dst+src1, or cross-typed mixed pairs).
1842        // Uses par_fill_with for parallelism; get_f64 dispatch is slightly
1843        // heavier than the typed fast paths but still fully parallel.
1844        self.data.par_fill_with(|i| {
1845            let z1 = src1.data.get_f64(i);
1846            let z2 = src2.data.get_f64(i);
1847            if is_nd1_f64(z1) || is_nd2_f64(z2) {
1848                nd_out
1849            } else {
1850                f(z1, z2)
1851            }
1852        });
1853
1854        Ok(())
1855    }
1856
1857    /// Add a scalar constant to every non-nodata cell, reading from `src`, writing to `self`.
1858    ///
1859    /// Equivalent to `apply_unary_math_from(|z| z + scalar, src)` but expresses intent clearly
1860    /// and is the canonical kernel shared by the `increment` tool.
1861    ///
1862    /// # Errors
1863    /// Returns an error if raster dimensions do not match.
1864    pub fn apply_scalar_add(&mut self, src: &Raster, scalar: f64) -> Result<()> {
1865        self.apply_unary_math_from(|z| z + scalar, src)
1866    }
1867
1868    /// Subtract a scalar constant from every non-nodata cell, reading from `src`, writing to `self`.
1869    ///
1870    /// Equivalent to `apply_unary_math_from(|z| z - scalar, src)` but expresses intent clearly
1871    /// and is the canonical kernel shared by the `decrement` tool.
1872    ///
1873    /// # Errors
1874    /// Returns an error if raster dimensions do not match.
1875    pub fn apply_scalar_sub(&mut self, src: &Raster, scalar: f64) -> Result<()> {
1876        self.apply_unary_math_from(|z| z - scalar, src)
1877    }
1878
1879    // ─── Pixel access ──────────────────────────────────────────────────────
1880
1881    /// Return the flat buffer index for signed band, row, and column coordinates.
1882    /// Returns `None` when coordinates are outside the raster bounds.
1883    #[inline]
1884    pub fn index(&self, band: isize, row: isize, col: isize) -> Option<usize> {
1885        if band < 0
1886            || row < 0
1887            || col < 0
1888            || band >= self.bands as isize
1889            || row >= self.rows as isize
1890            || col >= self.cols as isize
1891        {
1892            return None;
1893        }
1894        let band = band as usize;
1895        let row = row as usize;
1896        let col = col as usize;
1897        let band_stride = self.rows * self.cols;
1898        Some(band * band_stride + row * self.cols + col)
1899    }
1900
1901    /// Get the value at signed pixel coordinates `(band, row, col)`.
1902    ///
1903    /// Returns the raster's numeric `nodata` sentinel when coordinates are
1904    /// out-of-bounds or the stored value is nodata.
1905    #[inline]
1906    pub fn get(&self, band: isize, row: isize, col: isize) -> f64 {
1907        self.get_raw(band, row, col).unwrap_or(self.nodata)
1908    }
1909
1910    /// Get the value at signed pixel coordinates `(band, row, col)` as
1911    /// `Option<f64>`.
1912    ///
1913    /// Returns `None` if coordinates are out-of-bounds or the value is nodata.
1914    #[inline]
1915    pub fn get_opt(&self, band: isize, row: isize, col: isize) -> Option<f64> {
1916        let idx = self.index(band, row, col)?;
1917        let v = self.data.get_f64(idx);
1918        if self.is_nodata(v) {
1919            None
1920        } else {
1921            Some(v)
1922        }
1923    }
1924
1925    /// Get the raw value (including nodata) at signed pixel coordinates `(band, row, col)`.
1926    /// Returns `None` only on out-of-bounds.
1927    #[inline]
1928    pub fn get_raw(&self, band: isize, row: isize, col: isize) -> Option<f64> {
1929        let idx = self.index(band, row, col)?;
1930        Some(self.data.get_f64(idx))
1931    }
1932
1933    /// Set the value at signed pixel coordinates `(band, row, col)`.
1934    ///
1935    /// # Errors
1936    /// Returns [`RasterError::OutOfBounds`] if coordinates are outside the grid.
1937    #[inline]
1938    pub fn set(&mut self, band: isize, row: isize, col: isize, value: f64) -> Result<()> {
1939        if band < 0
1940            || row < 0
1941            || col < 0
1942            || band >= self.bands as isize
1943            || row >= self.rows as isize
1944            || col >= self.cols as isize
1945        {
1946            return Err(RasterError::OutOfBounds {
1947                band,
1948                col,
1949                row,
1950                bands: self.bands,
1951                cols: self.cols,
1952                rows: self.rows,
1953            });
1954        }
1955        let idx = self.index(band, row, col).expect("set bounds prechecked");
1956        self.data.set_f64(idx, value);
1957        Ok(())
1958    }
1959
1960    /// Set a value at signed pixel coordinates, panicking on out-of-bounds. Convenience alias.
1961    #[inline]
1962    pub fn set_unchecked(&mut self, band: isize, row: isize, col: isize, value: f64) {
1963        let idx = self
1964            .index(band, row, col)
1965            .expect("set_unchecked requires in-bounds coordinates");
1966        self.data.set_f64(idx, value);
1967    }
1968
1969    /// Returns `true` if `v` equals this raster's nodata sentinel.
1970    #[inline]
1971    pub fn is_nodata(&self, v: f64) -> bool {
1972        if self.nodata.is_nan() {
1973            v.is_nan()
1974        } else {
1975            (v - self.nodata).abs() < 1e-10 * self.nodata.abs().max(1.0)
1976        }
1977    }
1978
1979    // ─── Geometry helpers ─────────────────────────────────────────────────
1980
1981    /// Northern extent (Y max) — top of the grid.
1982    #[inline]
1983    pub fn y_max(&self) -> f64 {
1984        self.y_min + self.rows as f64 * self.cell_size_y
1985    }
1986
1987    /// Eastern extent (X max) — right edge of the grid.
1988    #[inline]
1989    pub fn x_max(&self) -> f64 {
1990        self.x_min + self.cols as f64 * self.cell_size_x
1991    }
1992
1993    /// The geographic extent of the raster.
1994    pub fn extent(&self) -> Extent {
1995        Extent {
1996            x_min: self.x_min,
1997            y_min: self.y_min,
1998            x_max: self.x_max(),
1999            y_max: self.y_max(),
2000        }
2001    }
2002
2003    /// Cell-center X coordinate for signed column index `col`.
2004    #[inline]
2005    pub fn col_center_x(&self, col: isize) -> f64 {
2006        self.x_min + (col as f64 + 0.5) * self.cell_size_x
2007    }
2008
2009    /// Cell-center Y coordinate for signed row index `row` (row 0 = north).
2010    #[inline]
2011    pub fn row_center_y(&self, row: isize) -> f64 {
2012        self.y_max() - (row as f64 + 0.5) * self.cell_size_y
2013    }
2014
2015    /// Convert geographic coordinates `(x, y)` to signed pixel indices `(col, row)`.
2016    /// Returns `None` if the point lies outside the raster extent.
2017    pub fn world_to_pixel(&self, x: f64, y: f64) -> Option<(isize, isize)> {
2018        if x < self.x_min || x >= self.x_max() || y < self.y_min || y >= self.y_max() {
2019            return None;
2020        }
2021        let col = ((x - self.x_min) / self.cell_size_x).floor() as isize;
2022        let row = ((self.y_max() - y) / self.cell_size_y).floor() as isize;
2023        let col = col.min(self.cols as isize - 1);
2024        let row = row.min(self.rows as isize - 1);
2025        Some((col, row))
2026    }
2027
2028    /// Assign a CRS to this raster using an EPSG code.
2029    ///
2030    /// Replaces the entire `crs` struct with a new `CrsInfo` containing only the EPSG code.
2031    /// Any existing `wkt` or `proj4` fields are cleared to ensure CRS consistency.
2032    pub fn assign_crs_epsg(&mut self, epsg: u32) {
2033        self.crs = CrsInfo {
2034            epsg: Some(epsg),
2035            wkt: None,
2036            proj4: None,
2037        };
2038    }
2039
2040    /// Assign a CRS to this raster using WKT text.
2041    ///
2042    /// Replaces the entire `crs` struct with a new `CrsInfo` containing only the WKT definition.
2043    /// Any existing `epsg` or `proj4` fields are cleared to ensure CRS consistency.
2044    pub fn assign_crs_wkt(&mut self, wkt: &str) {
2045        self.crs = CrsInfo {
2046            epsg: None,
2047            wkt: Some(wkt.to_string()),
2048            proj4: None,
2049        };
2050    }
2051
2052    /// Reproject this raster to another EPSG CRS.
2053    ///
2054    /// This MVP implementation uses an auto-derived output extent from transformed
2055    /// sampled source-boundary points (corners + edge densification) and supports
2056    /// nearest, bilinear, cubic, and Lanczos sampling.
2057    ///
2058    /// For explicit output grid controls (`cols`, `rows`, `extent`), use
2059    /// [`Raster::reproject_with_options`].
2060    ///
2061    /// # Errors
2062    /// Returns an error when source/destination EPSG codes are unsupported,
2063    /// source CRS metadata (EPSG/WKT/PROJ) is missing or invalid, or transformed
2064    /// extents are invalid.
2065    pub fn reproject_to_epsg(&self, dst_epsg: u32, resample: ResampleMethod) -> Result<Raster> {
2066        self.reproject_with_options(&ReprojectOptions::new(dst_epsg, resample))
2067    }
2068
2069    /// Reproject this raster using detailed output-grid options.
2070    pub fn reproject_with_options(&self, options: &ReprojectOptions) -> Result<Raster> {
2071        let src_crs = self.source_crs_for_reprojection()?;
2072        let dst_crs = Crs::from_epsg(options.dst_epsg).map_err(|e| {
2073            RasterError::Other(format!(
2074                "destination EPSG {} is not supported: {e}",
2075                options.dst_epsg
2076            ))
2077        })?;
2078
2079        self.reproject_internal(&src_crs, &dst_crs, options, None)
2080    }
2081
2082    /// Reproject this raster using detailed output-grid options and emit
2083    /// progress updates in the range [0, 1] as destination rows are completed.
2084    pub fn reproject_with_options_and_progress<F>(
2085        &self,
2086        options: &ReprojectOptions,
2087        progress: F,
2088    ) -> Result<Raster>
2089    where
2090        F: Fn(f64) + Send + Sync,
2091    {
2092        let src_crs = self.source_crs_for_reprojection()?;
2093        let dst_crs = Crs::from_epsg(options.dst_epsg).map_err(|e| {
2094            RasterError::Other(format!(
2095                "destination EPSG {} is not supported: {e}",
2096                options.dst_epsg
2097            ))
2098        })?;
2099
2100        self.reproject_internal(&src_crs, &dst_crs, options, Some(&progress))
2101    }
2102
2103    /// Reproject this raster using caller-supplied source/destination CRS objects.
2104    ///
2105    /// This advanced path bypasses source CRS metadata parsing, enabling
2106    /// workflows where CRS definitions are managed externally.
2107    ///
2108    /// Note: `options.dst_epsg` is still used for output `CrsInfo` metadata and
2109    /// EPSG-specific extent behavior (e.g., antimeridian handling for EPSG:4326).
2110    pub fn reproject_with_crs(
2111        &self,
2112        src_crs: &Crs,
2113        dst_crs: &Crs,
2114        options: &ReprojectOptions,
2115    ) -> Result<Raster> {
2116        self.reproject_internal(src_crs, dst_crs, options, None)
2117    }
2118
2119    /// Reproject this raster with caller-supplied CRS objects and emit progress
2120    /// updates in the range [0, 1] as destination rows are completed.
2121    pub fn reproject_with_crs_and_progress<F>(
2122        &self,
2123        src_crs: &Crs,
2124        dst_crs: &Crs,
2125        options: &ReprojectOptions,
2126        progress: F,
2127    ) -> Result<Raster>
2128    where
2129        F: Fn(f64) + Send + Sync,
2130    {
2131        self.reproject_internal(src_crs, dst_crs, options, Some(&progress))
2132    }
2133
2134    /// Convenience helper for nearest-neighbor reprojection.
2135    pub fn reproject_to_epsg_nearest(&self, dst_epsg: u32) -> Result<Raster> {
2136        self.reproject_to_epsg(dst_epsg, ResampleMethod::Nearest)
2137    }
2138
2139    /// Convenience helper for bilinear reprojection.
2140    pub fn reproject_to_epsg_bilinear(&self, dst_epsg: u32) -> Result<Raster> {
2141        self.reproject_to_epsg(dst_epsg, ResampleMethod::Bilinear)
2142    }
2143
2144    /// Convenience helper for cubic reprojection.
2145    pub fn reproject_to_epsg_cubic(&self, dst_epsg: u32) -> Result<Raster> {
2146        self.reproject_to_epsg(dst_epsg, ResampleMethod::Cubic)
2147    }
2148
2149    /// Reproject to destination EPSG using Lanczos interpolation.
2150    pub fn reproject_to_epsg_lanczos(&self, dst_epsg: u32) -> Result<Raster> {
2151        self.reproject_to_epsg(dst_epsg, ResampleMethod::Lanczos)
2152    }
2153
2154    /// Reproject to destination EPSG using 3x3 mean resampling.
2155    pub fn reproject_to_epsg_average(&self, dst_epsg: u32) -> Result<Raster> {
2156        self.reproject_to_epsg(dst_epsg, ResampleMethod::Average)
2157    }
2158
2159    /// Reproject to destination EPSG using 3x3 minimum resampling.
2160    pub fn reproject_to_epsg_min(&self, dst_epsg: u32) -> Result<Raster> {
2161        self.reproject_to_epsg(dst_epsg, ResampleMethod::Min)
2162    }
2163
2164    /// Reproject to destination EPSG using 3x3 maximum resampling.
2165    pub fn reproject_to_epsg_max(&self, dst_epsg: u32) -> Result<Raster> {
2166        self.reproject_to_epsg(dst_epsg, ResampleMethod::Max)
2167    }
2168
2169    /// Reproject to destination EPSG using 3x3 modal resampling.
2170    pub fn reproject_to_epsg_mode(&self, dst_epsg: u32) -> Result<Raster> {
2171        self.reproject_to_epsg(dst_epsg, ResampleMethod::Mode)
2172    }
2173
2174    /// Reproject to destination EPSG using 3x3 median resampling.
2175    pub fn reproject_to_epsg_median(&self, dst_epsg: u32) -> Result<Raster> {
2176        self.reproject_to_epsg(dst_epsg, ResampleMethod::Median)
2177    }
2178
2179    /// Reproject to destination EPSG using 3x3 standard-deviation resampling.
2180    pub fn reproject_to_epsg_stddev(&self, dst_epsg: u32) -> Result<Raster> {
2181        self.reproject_to_epsg(dst_epsg, ResampleMethod::StdDev)
2182    }
2183
2184    /// Reproject this raster to match another raster's grid (CRS, extent, rows, cols).
2185    ///
2186    /// The `target_grid` provides destination EPSG, output extent, and output
2187    /// dimensions. This is useful when aligning products from multiple sources
2188    /// onto a shared reference grid.
2189    ///
2190    /// # Errors
2191    /// Returns an error if `target_grid.crs.epsg` is missing or unsupported.
2192    pub fn reproject_to_match_grid(
2193        &self,
2194        target_grid: &Raster,
2195        resample: ResampleMethod,
2196    ) -> Result<Raster> {
2197        let dst_epsg = target_grid.crs.epsg.ok_or_else(|| {
2198            RasterError::Other(
2199                "reproject_to_match_grid requires target grid CRS EPSG in target_grid.crs.epsg"
2200                    .to_string(),
2201            )
2202        })?;
2203
2204        let opts = ReprojectOptions::new(dst_epsg, resample)
2205            .with_size(target_grid.cols, target_grid.rows)
2206            .with_extent(target_grid.extent());
2207
2208        self.reproject_with_options(&opts)
2209    }
2210
2211    /// Reproject this raster to match another raster's grid while emitting
2212    /// progress updates in the range [0, 1] as destination rows are completed.
2213    pub fn reproject_to_match_grid_and_progress<F>(
2214        &self,
2215        target_grid: &Raster,
2216        resample: ResampleMethod,
2217        progress: F,
2218    ) -> Result<Raster>
2219    where
2220        F: Fn(f64) + Send + Sync,
2221    {
2222        let dst_epsg = target_grid.crs.epsg.ok_or_else(|| {
2223            RasterError::Other(
2224                "reproject_to_match_grid requires target grid CRS EPSG in target_grid.crs.epsg"
2225                    .to_string(),
2226            )
2227        })?;
2228
2229        let opts = ReprojectOptions::new(dst_epsg, resample)
2230            .with_size(target_grid.cols, target_grid.rows)
2231            .with_extent(target_grid.extent());
2232
2233        self.reproject_with_options_and_progress(&opts, progress)
2234    }
2235
2236    /// Reproject this raster using another raster's CRS, resolution, and snap origin.
2237    ///
2238    /// Unlike [`Raster::reproject_to_match_grid`], this keeps the destination
2239    /// extent auto-derived from the transformed source footprint, while aligning
2240    /// that extent to the reference grid's origin and pixel size.
2241    ///
2242    /// # Errors
2243    /// Returns an error if `reference_grid.crs.epsg` is missing or unsupported.
2244    pub fn reproject_to_match_resolution(
2245        &self,
2246        reference_grid: &Raster,
2247        resample: ResampleMethod,
2248    ) -> Result<Raster> {
2249        let dst_epsg = reference_grid.crs.epsg.ok_or_else(|| {
2250            RasterError::Other(
2251                "reproject_to_match_resolution requires reference grid CRS EPSG in reference_grid.crs.epsg"
2252                    .to_string(),
2253            )
2254        })?;
2255
2256        let opts = ReprojectOptions::new(dst_epsg, resample)
2257            .with_resolution(reference_grid.cell_size_x, reference_grid.cell_size_y)
2258            .with_snap_origin(reference_grid.x_min, reference_grid.y_min);
2259
2260        self.reproject_with_options(&opts)
2261    }
2262
2263    /// Reproject this raster while matching a reference raster's resolution
2264    /// and snap origin, emitting progress updates in [0, 1].
2265    pub fn reproject_to_match_resolution_and_progress<F>(
2266        &self,
2267        reference_grid: &Raster,
2268        resample: ResampleMethod,
2269        progress: F,
2270    ) -> Result<Raster>
2271    where
2272        F: Fn(f64) + Send + Sync,
2273    {
2274        let dst_epsg = reference_grid.crs.epsg.ok_or_else(|| {
2275            RasterError::Other(
2276                "reproject_to_match_resolution requires reference grid CRS EPSG in reference_grid.crs.epsg"
2277                    .to_string(),
2278            )
2279        })?;
2280
2281        let opts = ReprojectOptions::new(dst_epsg, resample)
2282            .with_resolution(reference_grid.cell_size_x, reference_grid.cell_size_y)
2283            .with_snap_origin(reference_grid.x_min, reference_grid.y_min);
2284
2285        self.reproject_with_options_and_progress(&opts, progress)
2286    }
2287
2288    /// Reproject this raster to an explicit destination EPSG while matching a
2289    /// reference raster's resolution and snap origin.
2290    ///
2291    /// If `reference_grid` is in a different CRS than `dst_epsg`, the
2292    /// reference snap origin and per-axis cell sizes are transformed to
2293    /// destination CRS using local axis steps at the reference origin.
2294    ///
2295    /// # Errors
2296    /// Returns an error if reference/destination EPSG values are missing or
2297    /// unsupported, or if transformed reference resolution is invalid.
2298    pub fn reproject_to_match_resolution_in_epsg(
2299        &self,
2300        dst_epsg: u32,
2301        reference_grid: &Raster,
2302        resample: ResampleMethod,
2303    ) -> Result<Raster> {
2304        let reference_epsg = reference_grid.crs.epsg.ok_or_else(|| {
2305            RasterError::Other(
2306                "reproject_to_match_resolution_in_epsg requires reference grid CRS EPSG in reference_grid.crs.epsg"
2307                    .to_string(),
2308            )
2309        })?;
2310
2311        let (snap_x, snap_y, x_res, y_res) = if reference_epsg == dst_epsg {
2312            (
2313                reference_grid.x_min,
2314                reference_grid.y_min,
2315                reference_grid.cell_size_x,
2316                reference_grid.cell_size_y,
2317            )
2318        } else {
2319            let ref_crs = Crs::from_epsg(reference_epsg).map_err(|e| {
2320                RasterError::Other(format!(
2321                    "reference EPSG {reference_epsg} is not supported: {e}"
2322                ))
2323            })?;
2324            let dst_crs = Crs::from_epsg(dst_epsg).map_err(|e| {
2325                RasterError::Other(format!("destination EPSG {dst_epsg} is not supported: {e}"))
2326            })?;
2327
2328            let ox = reference_grid.x_min;
2329            let oy = reference_grid.y_min;
2330            let (sx, sy) = ref_crs.transform_to(ox, oy, &dst_crs).map_err(|e| {
2331                RasterError::Other(format!(
2332                    "failed to transform reference snap origin to EPSG:{dst_epsg}: {e}"
2333                ))
2334            })?;
2335            let (sx_dx, sy_dx) = ref_crs
2336                .transform_to(ox + reference_grid.cell_size_x, oy, &dst_crs)
2337                .map_err(|e| {
2338                    RasterError::Other(format!(
2339                        "failed to transform reference X-step to EPSG:{dst_epsg}: {e}"
2340                    ))
2341                })?;
2342            let (sx_dy, sy_dy) = ref_crs
2343                .transform_to(ox, oy + reference_grid.cell_size_y, &dst_crs)
2344                .map_err(|e| {
2345                    RasterError::Other(format!(
2346                        "failed to transform reference Y-step to EPSG:{dst_epsg}: {e}"
2347                    ))
2348                })?;
2349
2350            let rx = (sx_dx - sx).hypot(sy_dx - sy);
2351            let ry = (sx_dy - sx).hypot(sy_dy - sy);
2352            if !rx.is_finite() || !ry.is_finite() || rx <= 0.0 || ry <= 0.0 {
2353                return Err(RasterError::Other(
2354                    "invalid transformed reference resolution while matching destination EPSG"
2355                        .to_string(),
2356                ));
2357            }
2358            (sx, sy, rx, ry)
2359        };
2360
2361        let opts = ReprojectOptions::new(dst_epsg, resample)
2362            .with_resolution(x_res, y_res)
2363            .with_snap_origin(snap_x, snap_y);
2364
2365        self.reproject_with_options(&opts)
2366    }
2367
2368    /// Reproject this raster to an explicit destination EPSG while matching a
2369    /// reference raster's transformed resolution/snap, emitting progress in [0, 1].
2370    pub fn reproject_to_match_resolution_in_epsg_and_progress<F>(
2371        &self,
2372        dst_epsg: u32,
2373        reference_grid: &Raster,
2374        resample: ResampleMethod,
2375        progress: F,
2376    ) -> Result<Raster>
2377    where
2378        F: Fn(f64) + Send + Sync,
2379    {
2380        let reference_epsg = reference_grid.crs.epsg.ok_or_else(|| {
2381            RasterError::Other(
2382                "reproject_to_match_resolution_in_epsg requires reference grid CRS EPSG in reference_grid.crs.epsg"
2383                    .to_string(),
2384            )
2385        })?;
2386
2387        let (snap_x, snap_y, x_res, y_res) = if reference_epsg == dst_epsg {
2388            (
2389                reference_grid.x_min,
2390                reference_grid.y_min,
2391                reference_grid.cell_size_x,
2392                reference_grid.cell_size_y,
2393            )
2394        } else {
2395            let ref_crs = Crs::from_epsg(reference_epsg).map_err(|e| {
2396                RasterError::Other(format!(
2397                    "reference EPSG {reference_epsg} is not supported: {e}"
2398                ))
2399            })?;
2400            let dst_crs = Crs::from_epsg(dst_epsg).map_err(|e| {
2401                RasterError::Other(format!("destination EPSG {dst_epsg} is not supported: {e}"))
2402            })?;
2403
2404            let ox = reference_grid.x_min;
2405            let oy = reference_grid.y_min;
2406            let (sx, sy) = ref_crs.transform_to(ox, oy, &dst_crs).map_err(|e| {
2407                RasterError::Other(format!(
2408                    "failed to transform reference snap origin to EPSG:{dst_epsg}: {e}"
2409                ))
2410            })?;
2411            let (sx_dx, sy_dx) = ref_crs
2412                .transform_to(ox + reference_grid.cell_size_x, oy, &dst_crs)
2413                .map_err(|e| {
2414                    RasterError::Other(format!(
2415                        "failed to transform reference X-step to EPSG:{dst_epsg}: {e}"
2416                    ))
2417                })?;
2418            let (sx_dy, sy_dy) = ref_crs
2419                .transform_to(ox, oy + reference_grid.cell_size_y, &dst_crs)
2420                .map_err(|e| {
2421                    RasterError::Other(format!(
2422                        "failed to transform reference Y-step to EPSG:{dst_epsg}: {e}"
2423                    ))
2424                })?;
2425
2426            let rx = (sx_dx - sx).hypot(sy_dx - sy);
2427            let ry = (sx_dy - sx).hypot(sy_dy - sy);
2428            if !rx.is_finite() || !ry.is_finite() || rx <= 0.0 || ry <= 0.0 {
2429                return Err(RasterError::Other(
2430                    "invalid transformed reference resolution while matching destination EPSG"
2431                        .to_string(),
2432                ));
2433            }
2434            (sx, sy, rx, ry)
2435        };
2436
2437        let opts = ReprojectOptions::new(dst_epsg, resample)
2438            .with_resolution(x_res, y_res)
2439            .with_snap_origin(snap_x, snap_y);
2440
2441        self.reproject_with_options_and_progress(&opts, progress)
2442    }
2443
2444    fn reproject_internal(
2445        &self,
2446        src_crs: &Crs,
2447        dst_crs: &Crs,
2448        options: &ReprojectOptions,
2449        progress: Option<&(dyn Fn(f64) + Send + Sync)>,
2450    ) -> Result<Raster> {
2451        maybe_warn_area_of_use_mismatch(
2452            src_crs,
2453            dst_crs,
2454            self.extent(),
2455            options.warn_on_area_of_use_mismatch,
2456        );
2457
2458        let src_extent = self.extent();
2459        let samples_per_edge = (self.cols.max(self.rows) / 32).clamp(8, 128);
2460        let base_extent = transformed_extent_from_boundary_samples(
2461            src_crs,
2462            dst_crs,
2463            src_extent,
2464            samples_per_edge,
2465            options.dst_epsg,
2466            options.antimeridian_policy,
2467            &options.epoch_transform,
2468        )?;
2469        let out_extent = options.extent.unwrap_or(base_extent);
2470        let width = out_extent.x_max - out_extent.x_min;
2471        let height = out_extent.y_max - out_extent.y_min;
2472
2473        if !out_extent.x_min.is_finite()
2474            || !out_extent.x_max.is_finite()
2475            || !out_extent.y_min.is_finite()
2476            || !out_extent.y_max.is_finite()
2477            || width <= 0.0
2478            || height <= 0.0
2479        {
2480            return Err(RasterError::CorruptData(
2481                "invalid transformed extent produced during reprojection".to_string(),
2482            ));
2483        }
2484
2485        let x_res = options.x_res.map(f64::abs);
2486        let y_res = options.y_res.map(f64::abs);
2487        if x_res.is_some_and(|v| !v.is_finite() || v <= 0.0)
2488            || y_res.is_some_and(|v| !v.is_finite() || v <= 0.0)
2489        {
2490            return Err(RasterError::CorruptData(
2491                "invalid reprojection resolution (x_res/y_res must be positive finite values)"
2492                    .to_string(),
2493            ));
2494        }
2495
2496        let mut x_min = out_extent.x_min;
2497        let mut x_max = out_extent.x_max;
2498        let mut y_min = out_extent.y_min;
2499        let mut y_max = out_extent.y_max;
2500
2501        let out_cols = match options.cols {
2502            Some(cols) => cols,
2503            None => match x_res {
2504                Some(rx) => {
2505                    if let Some(sx) = options.snap_x {
2506                        match options.grid_size_policy {
2507                            GridSizePolicy::Expand => {
2508                                x_min = snap_down_to_origin(x_min, sx, rx);
2509                                x_max = snap_up_to_origin(x_max, sx, rx);
2510                            }
2511                            GridSizePolicy::FitInside => {
2512                                x_min = snap_up_to_origin(x_min, sx, rx);
2513                                x_max = snap_down_to_origin(x_max, sx, rx);
2514                            }
2515                        }
2516                    }
2517                    let span = (x_max - x_min).max(0.0);
2518                    let cols = match options.grid_size_policy {
2519                        GridSizePolicy::Expand => (span / rx).ceil().max(1.0) as usize,
2520                        GridSizePolicy::FitInside => (span / rx).floor().max(1.0) as usize,
2521                    };
2522                    x_max = x_min + cols as f64 * rx;
2523                    cols
2524                }
2525                None => self.cols,
2526            },
2527        };
2528        let out_rows = match options.rows {
2529            Some(rows) => rows,
2530            None => match y_res {
2531                Some(ry) => {
2532                    if let Some(sy) = options.snap_y {
2533                        match options.grid_size_policy {
2534                            GridSizePolicy::Expand => {
2535                                y_min = snap_down_to_origin(y_min, sy, ry);
2536                                y_max = snap_up_to_origin(y_max, sy, ry);
2537                            }
2538                            GridSizePolicy::FitInside => {
2539                                y_min = snap_up_to_origin(y_min, sy, ry);
2540                                y_max = snap_down_to_origin(y_max, sy, ry);
2541                            }
2542                        }
2543                    }
2544                    let span = (y_max - y_min).max(0.0);
2545                    let rows = match options.grid_size_policy {
2546                        GridSizePolicy::Expand => (span / ry).ceil().max(1.0) as usize,
2547                        GridSizePolicy::FitInside => (span / ry).floor().max(1.0) as usize,
2548                    };
2549                    y_max = y_min + rows as f64 * ry;
2550                    rows
2551                }
2552                None => self.rows,
2553            },
2554        };
2555
2556        let out_extent = Extent {
2557            x_min,
2558            y_min,
2559            x_max,
2560            y_max,
2561        };
2562
2563        if out_cols == 0 || out_rows == 0 {
2564            return Err(RasterError::InvalidDimensions {
2565                cols: out_cols,
2566                rows: out_rows,
2567            });
2568        }
2569
2570        let cfg = RasterConfig {
2571            cols: out_cols,
2572            rows: out_rows,
2573            bands: self.bands,
2574            x_min: out_extent.x_min,
2575            y_min: out_extent.y_min,
2576            cell_size: (out_extent.x_max - out_extent.x_min) / out_cols as f64,
2577            cell_size_y: Some((out_extent.y_max - out_extent.y_min) / out_rows as f64),
2578            nodata: self.nodata,
2579            data_type: self.data_type,
2580            crs: CrsInfo::from_epsg(options.dst_epsg),
2581            metadata: self.metadata.clone(),
2582        };
2583        let mut out = Raster::new(cfg);
2584
2585        let out_y_max = out.y_max();
2586        let footprint_ring =
2587            if options.destination_footprint == DestinationFootprint::SourceBoundary {
2588                let ring = transformed_boundary_ring_samples(
2589                    src_crs,
2590                    dst_crs,
2591                    src_extent,
2592                    samples_per_edge,
2593                    options.dst_epsg,
2594                    options.antimeridian_policy,
2595                    &options.epoch_transform,
2596                )?;
2597                if ring.len() >= 3 {
2598                    Some(ring)
2599                } else {
2600                    None
2601                }
2602            } else {
2603                None
2604            };
2605
2606        let total_rows = out.rows;
2607        let completed_rows = AtomicUsize::new(0);
2608        let rows_data: Vec<Vec<Option<f64>>> = (0..out.rows as isize)
2609            .into_par_iter()
2610            .map(|row| {
2611                let mut row_values = vec![None; out.cols * out.bands];
2612                let y = out_y_max - (row as f64 + 0.5) * out.cell_size_y;
2613
2614                // Collect per-row coordinates that pass the footprint check, then
2615                // transform them all in one batch call so the SIMD fast paths in
2616                // transform_to_batch can vectorize across the full row width.
2617                let mut batch_coords: Vec<(f64, f64)> = Vec::with_capacity(out.cols);
2618                let mut batch_cols: Vec<usize> = Vec::with_capacity(out.cols);
2619                for col in 0..out.cols as isize {
2620                    let x = out.x_min + (col as f64 + 0.5) * out.cell_size_x;
2621                    if let Some(ring) = &footprint_ring {
2622                        if !point_in_polygon(x, y, ring) {
2623                            continue;
2624                        }
2625                    }
2626                    batch_coords.push((x, y));
2627                    batch_cols.push(col as usize);
2628                }
2629
2630                let epoch_routing_requested = options
2631                    .epoch_transform
2632                    .coordinate_epoch_decimal_year
2633                    .is_some()
2634                    || options
2635                        .epoch_transform
2636                        .source_reference_epoch_decimal_year
2637                        .is_some()
2638                    || options
2639                        .epoch_transform
2640                        .target_reference_epoch_decimal_year
2641                        .is_some()
2642                    || options.epoch_transform.operation_code.is_some()
2643                    || !options.epoch_transform.prefer_official_operation
2644                    || matches!(
2645                        options.epoch_transform.epoch_policy,
2646                        EpochPolicy::AllowStaticFallback
2647                    );
2648
2649                if !epoch_routing_requested {
2650                    // Single batch CRS transform for all eligible pixels in this row.
2651                    // Successful transforms overwrite batch_coords in-place; errors are
2652                    // returned as Some(Err(_)) at the corresponding index.
2653                    let errors = dst_crs.transform_to_batch(&mut batch_coords, src_crs);
2654
2655                    for (i, &col) in batch_cols.iter().enumerate() {
2656                        if errors[i].is_some() {
2657                            continue;
2658                        }
2659                        let (sx, sy) = batch_coords[i];
2660                        for band in 0..out.bands as isize {
2661                            if let Some(v) = self.sample_world(
2662                                band,
2663                                sx,
2664                                sy,
2665                                options.resample,
2666                                options.nodata_policy,
2667                            ) {
2668                                row_values[band as usize * out.cols + col] = Some(v);
2669                            }
2670                        }
2671                    }
2672                } else {
2673                    for (i, &col) in batch_cols.iter().enumerate() {
2674                        let Ok((sx, sy)) = transform_xy_with_epoch_options(
2675                            src_crs,
2676                            dst_crs,
2677                            batch_coords[i].0,
2678                            batch_coords[i].1,
2679                            &options.epoch_transform,
2680                        ) else {
2681                            continue;
2682                        };
2683                        for band in 0..out.bands as isize {
2684                            if let Some(v) = self.sample_world(
2685                                band,
2686                                sx,
2687                                sy,
2688                                options.resample,
2689                                options.nodata_policy,
2690                            ) {
2691                                row_values[band as usize * out.cols + col] = Some(v);
2692                            }
2693                        }
2694                    }
2695                }
2696
2697                if let Some(progress_cb) = progress {
2698                    let done = completed_rows.fetch_add(1, Ordering::Relaxed) + 1;
2699                    progress_cb(done as f64 / total_rows as f64);
2700                }
2701
2702                row_values
2703            })
2704            .collect();
2705
2706        for (row, row_values) in rows_data.into_iter().enumerate() {
2707            let row = row as isize;
2708            for band in 0..out.bands {
2709                for col in 0..out.cols {
2710                    if let Some(v) = row_values[band * out.cols + col] {
2711                        out.set_unchecked(band as isize, row, col as isize, v);
2712                    }
2713                }
2714            }
2715        }
2716
2717        if let Some(progress_cb) = progress {
2718            progress_cb(1.0);
2719        }
2720
2721        Ok(out)
2722    }
2723
2724    fn source_crs_for_reprojection(&self) -> Result<Crs> {
2725        if let Some(src_epsg) = self.crs.epsg {
2726            return Crs::from_epsg(src_epsg).map_err(|e| {
2727                RasterError::Other(format!("source EPSG {src_epsg} is not supported: {e}"))
2728            });
2729        }
2730
2731        if let Some(wkt) = self.crs.wkt.as_deref() {
2732            let trimmed = wkt.trim();
2733            if !trimmed.is_empty() {
2734                return wbprojection::from_wkt(trimmed).map_err(|e| {
2735                    RasterError::Other(format!("source CRS WKT is not supported: {e}"))
2736                });
2737            }
2738        }
2739
2740        if let Some(proj) = self.crs.proj4.as_deref() {
2741            let trimmed = proj.trim();
2742            if !trimmed.is_empty() {
2743                return from_proj_string(trimmed).map_err(|e| {
2744                    RasterError::Other(format!("source CRS PROJ string is not supported: {e}"))
2745                });
2746            }
2747        }
2748
2749        Err(RasterError::Other(
2750            "reproject_to_epsg requires source CRS metadata (EPSG, WKT, or PROJ string)"
2751                .to_string(),
2752        ))
2753    }
2754
2755    /// Sample a raster value at world coordinates using the selected resampling method.
2756    pub fn sample_world(
2757        &self,
2758        band: isize,
2759        x: f64,
2760        y: f64,
2761        method: ResampleMethod,
2762        nodata_policy: NodataPolicy,
2763    ) -> Option<f64> {
2764        let col_f = (x - self.x_min) / self.cell_size_x - 0.5;
2765        let row_f = (self.y_max() - y) / self.cell_size_y - 0.5;
2766        match method {
2767            ResampleMethod::Nearest => self.sample_nearest_pixel(band, col_f, row_f),
2768            ResampleMethod::Bilinear => match nodata_policy {
2769                NodataPolicy::Strict => self.sample_bilinear_strict_pixel(band, col_f, row_f),
2770                NodataPolicy::PartialKernel => {
2771                    self.sample_bilinear_partial_pixel(band, col_f, row_f)
2772                }
2773                NodataPolicy::Fill => self
2774                    .sample_bilinear_strict_pixel(band, col_f, row_f)
2775                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
2776            },
2777            ResampleMethod::Cubic => match nodata_policy {
2778                NodataPolicy::Strict => self.sample_cubic_strict_pixel(band, col_f, row_f),
2779                NodataPolicy::PartialKernel => self.sample_cubic_partial_pixel(band, col_f, row_f),
2780                NodataPolicy::Fill => self
2781                    .sample_cubic_strict_pixel(band, col_f, row_f)
2782                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
2783            },
2784            ResampleMethod::Lanczos => match nodata_policy {
2785                NodataPolicy::Strict => self.sample_lanczos_strict_pixel(band, col_f, row_f),
2786                NodataPolicy::PartialKernel => {
2787                    self.sample_lanczos_partial_pixel(band, col_f, row_f)
2788                }
2789                NodataPolicy::Fill => self
2790                    .sample_lanczos_strict_pixel(band, col_f, row_f)
2791                    .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
2792            },
2793            ResampleMethod::Average => {
2794                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::Mean, nodata_policy)
2795            }
2796            ResampleMethod::Min => {
2797                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::Min, nodata_policy)
2798            }
2799            ResampleMethod::Max => {
2800                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::Max, nodata_policy)
2801            }
2802            ResampleMethod::Mode => {
2803                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::Mode, nodata_policy)
2804            }
2805            ResampleMethod::Median => {
2806                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::Median, nodata_policy)
2807            }
2808            ResampleMethod::StdDev => {
2809                self.sample_window_stat_pixel(band, col_f, row_f, WindowStat::StdDev, nodata_policy)
2810            }
2811        }
2812    }
2813
2814    fn sample_window_stat_pixel(
2815        &self,
2816        band: isize,
2817        col_f: f64,
2818        row_f: f64,
2819        stat: WindowStat,
2820        nodata_policy: NodataPolicy,
2821    ) -> Option<f64> {
2822        if !col_f.is_finite() || !row_f.is_finite() {
2823            return None;
2824        }
2825
2826        let center_col = col_f.round() as isize;
2827        let center_row = row_f.round() as isize;
2828        let mut values = Vec::with_capacity(9);
2829        let mut valid_count = 0usize;
2830
2831        for dy in -1..=1 {
2832            for dx in -1..=1 {
2833                let c = center_col + dx;
2834                let r = center_row + dy;
2835                if c < 0 || r < 0 || c >= self.cols as isize || r >= self.rows as isize {
2836                    continue;
2837                }
2838                if let Some(v) = self.get_opt(band, r, c) {
2839                    values.push(v);
2840                    valid_count += 1;
2841                }
2842            }
2843        }
2844
2845        match nodata_policy {
2846            NodataPolicy::Strict if valid_count < 9 => None,
2847            NodataPolicy::PartialKernel | NodataPolicy::Strict => {
2848                reduce_window_values(&values, stat)
2849            }
2850            NodataPolicy::Fill => reduce_window_values(&values, stat)
2851                .or_else(|| self.sample_nearest_pixel(band, col_f, row_f)),
2852        }
2853    }
2854
2855    fn sample_nearest_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2856        if !col_f.is_finite() || !row_f.is_finite() {
2857            return None;
2858        }
2859        let col = col_f.round() as isize;
2860        let row = row_f.round() as isize;
2861        self.get_opt(band, row, col)
2862    }
2863
2864    fn sample_bilinear_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2865        if !col_f.is_finite() || !row_f.is_finite() {
2866            return None;
2867        }
2868        let c0 = col_f.floor() as isize;
2869        let r0 = row_f.floor() as isize;
2870        let c1 = c0 + 1;
2871        let r1 = r0 + 1;
2872        if c0 < 0 || r0 < 0 || c1 >= self.cols as isize || r1 >= self.rows as isize {
2873            return None;
2874        }
2875
2876        let tx = col_f - c0 as f64;
2877        let ty = row_f - r0 as f64;
2878
2879        if let Some(values) = self.data_f64() {
2880            return self.sample_bilinear_strict_simd_f64(values, band, r0, c0, tx, ty);
2881        }
2882        if let Some(values) = self.data_f32() {
2883            return self.sample_bilinear_strict_simd_f32(values, band, r0, c0, tx, ty);
2884        }
2885
2886        let q00 = self.get_opt(band, r0, c0)?;
2887        let q10 = self.get_opt(band, r0, c1)?;
2888        let q01 = self.get_opt(band, r1, c0)?;
2889        let q11 = self.get_opt(band, r1, c1)?;
2890
2891        let a = q00 * (1.0 - tx) + q10 * tx;
2892        let b = q01 * (1.0 - tx) + q11 * tx;
2893        Some(a * (1.0 - ty) + b * ty)
2894    }
2895
2896    #[inline]
2897    fn sample_bilinear_strict_simd_f64(
2898        &self,
2899        values: &[f64],
2900        band: isize,
2901        r0: isize,
2902        c0: isize,
2903        tx: f64,
2904        ty: f64,
2905    ) -> Option<f64> {
2906        let band_stride = self.rows * self.cols;
2907        let base = band as usize * band_stride + r0 as usize * self.cols + c0 as usize;
2908        let q00 = values[base];
2909        let q10 = values[base + 1];
2910        let q01 = values[base + self.cols];
2911        let q11 = values[base + self.cols + 1];
2912
2913        if self.is_nodata(q00) || self.is_nodata(q10) || self.is_nodata(q01) || self.is_nodata(q11)
2914        {
2915            return None;
2916        }
2917
2918        let weights = f64x4::new([
2919            (1.0 - tx) * (1.0 - ty),
2920            tx * (1.0 - ty),
2921            (1.0 - tx) * ty,
2922            tx * ty,
2923        ]);
2924        let vals = f64x4::new([q00, q10, q01, q11]);
2925        let weighted = <[f64; 4]>::from(vals * weights);
2926        Some(weighted.into_iter().sum())
2927    }
2928
2929    #[inline]
2930    fn sample_bilinear_strict_simd_f32(
2931        &self,
2932        values: &[f32],
2933        band: isize,
2934        r0: isize,
2935        c0: isize,
2936        tx: f64,
2937        ty: f64,
2938    ) -> Option<f64> {
2939        let band_stride = self.rows * self.cols;
2940        let base = band as usize * band_stride + r0 as usize * self.cols + c0 as usize;
2941        let q00 = values[base] as f64;
2942        let q10 = values[base + 1] as f64;
2943        let q01 = values[base + self.cols] as f64;
2944        let q11 = values[base + self.cols + 1] as f64;
2945
2946        if self.is_nodata(q00) || self.is_nodata(q10) || self.is_nodata(q01) || self.is_nodata(q11)
2947        {
2948            return None;
2949        }
2950
2951        let weights = f64x4::new([
2952            (1.0 - tx) * (1.0 - ty),
2953            tx * (1.0 - ty),
2954            (1.0 - tx) * ty,
2955            tx * ty,
2956        ]);
2957        let vals = f64x4::new([q00, q10, q01, q11]);
2958        let weighted = <[f64; 4]>::from(vals * weights);
2959        Some(weighted.into_iter().sum())
2960    }
2961
2962    fn sample_bilinear_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
2963        if !col_f.is_finite() || !row_f.is_finite() {
2964            return None;
2965        }
2966        let c0 = col_f.floor() as isize;
2967        let r0 = row_f.floor() as isize;
2968        let c1 = c0 + 1;
2969        let r1 = r0 + 1;
2970
2971        let tx = col_f - c0 as f64;
2972        let ty = row_f - r0 as f64;
2973
2974        let neighbors = [
2975            (c0, r0, (1.0 - tx) * (1.0 - ty)),
2976            (c1, r0, tx * (1.0 - ty)),
2977            (c0, r1, (1.0 - tx) * ty),
2978            (c1, r1, tx * ty),
2979        ];
2980
2981        let mut sum = 0.0;
2982        let mut wsum = 0.0;
2983        for (c, r, w) in neighbors {
2984            if w <= 0.0 || c < 0 || r < 0 || c >= self.cols as isize || r >= self.rows as isize {
2985                continue;
2986            }
2987            if let Some(v) = self.get_opt(band, r, c) {
2988                sum += v * w;
2989                wsum += w;
2990            }
2991        }
2992
2993        if wsum > 0.0 {
2994            Some(sum / wsum)
2995        } else {
2996            None
2997        }
2998    }
2999
3000    fn sample_cubic_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
3001        if !col_f.is_finite() || !row_f.is_finite() {
3002            return None;
3003        }
3004        let c1 = col_f.floor() as isize;
3005        let r1 = row_f.floor() as isize;
3006        if c1 - 1 < 0 || r1 - 1 < 0 || c1 + 2 >= self.cols as isize || r1 + 2 >= self.rows as isize
3007        {
3008            return None;
3009        }
3010
3011        let tx = col_f - c1 as f64;
3012        let ty = row_f - r1 as f64;
3013        let wx = cubic_bspline_weights(tx);
3014        let wy = cubic_bspline_weights(ty);
3015
3016        // Use SIMD-accelerated 4×4 dot product
3017        self.sample_cubic_simd_kernel(&wx, &wy, band, c1 - 1, r1 - 1)
3018    }
3019
3020    /// SIMD-accelerated 4×4 kernel dot product for bicubic resampling.
3021    /// Assumes all 16 pixels are in-bounds and valid.
3022    #[inline]
3023    fn sample_cubic_simd_kernel(
3024        &self,
3025        wx: &[f64; 4],
3026        wy: &[f64; 4],
3027        band: isize,
3028        c_start: isize,
3029        r_start: isize,
3030    ) -> Option<f64> {
3031        // Process 4 rows, 4 pixels per row, computing weighted sum using SIMD for horizontal accumulation.
3032        // Load row-wise and multiply by row weights.
3033        let mut row_sums = [0.0_f64; 4];
3034
3035        for (j, _wyj) in wy.iter().enumerate() {
3036            let rr = r_start + j as isize;
3037            let mut row_sum = 0.0;
3038
3039            // Load 4 pixels in row and compute weighted sum
3040            for (i, wxi) in wx.iter().enumerate() {
3041                let cc = c_start + i as isize;
3042                let v = self.get_opt(band, rr, cc)?;
3043                row_sum += v * *wxi;
3044            }
3045
3046            row_sums[j] = row_sum;
3047        }
3048
3049        // Horizontal reduction: multiply by row weights and sum
3050        let mut sum = 0.0;
3051        for (j, wyj) in wy.iter().enumerate() {
3052            sum += row_sums[j] * *wyj;
3053        }
3054
3055        Some(sum)
3056    }
3057
3058    fn sample_cubic_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
3059        if !col_f.is_finite() || !row_f.is_finite() {
3060            return None;
3061        }
3062        let c1 = col_f.floor() as isize;
3063        let r1 = row_f.floor() as isize;
3064        let tx = col_f - c1 as f64;
3065        let ty = row_f - r1 as f64;
3066        let wx = cubic_bspline_weights(tx);
3067        let wy = cubic_bspline_weights(ty);
3068
3069        let mut sum = 0.0;
3070        let mut wsum = 0.0;
3071
3072        for (j, wyj) in wy.iter().enumerate() {
3073            let rr = clamp_isize(r1 + j as isize - 1, 0, self.rows as isize - 1);
3074            if *wyj <= 0.0 {
3075                continue;
3076            }
3077            for (i, wxi) in wx.iter().enumerate() {
3078                let cc = clamp_isize(c1 + i as isize - 1, 0, self.cols as isize - 1);
3079                let w = *wxi * *wyj;
3080                if w <= 0.0 {
3081                    continue;
3082                }
3083                if let Some(v) = self.get_opt(band, rr, cc) {
3084                    sum += v * w;
3085                    wsum += w;
3086                }
3087            }
3088        }
3089
3090        if wsum > 0.0 {
3091            Some(sum / wsum)
3092        } else {
3093            None
3094        }
3095    }
3096
3097    fn sample_lanczos_strict_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
3098        if !col_f.is_finite() || !row_f.is_finite() {
3099            return None;
3100        }
3101
3102        let c0 = col_f.floor() as isize;
3103        let r0 = row_f.floor() as isize;
3104        if c0 - 2 < 0 || r0 - 2 < 0 || c0 + 3 >= self.cols as isize || r0 + 3 >= self.rows as isize
3105        {
3106            return None;
3107        }
3108
3109        let wx = lanczos3_weights(col_f, c0);
3110        let wy = lanczos3_weights(row_f, r0);
3111
3112        if let Some(values) = self.data_f64() {
3113            return self.sample_lanczos_strict_simd_f64(values, band, c0, r0, &wx, &wy);
3114        }
3115        if let Some(values) = self.data_f32() {
3116            return self.sample_lanczos_strict_simd_f32(values, band, c0, r0, &wx, &wy);
3117        }
3118
3119        let mut sum = 0.0;
3120        let mut wsum = 0.0;
3121        for (j, wyj) in wy.iter().enumerate() {
3122            let rr = r0 + j as isize - 2;
3123            for (i, wxi) in wx.iter().enumerate() {
3124                let cc = c0 + i as isize - 2;
3125                let v = self.get_opt(band, rr, cc)?;
3126                let w = *wxi * *wyj;
3127                sum += v * w;
3128                wsum += w;
3129            }
3130        }
3131
3132        if wsum.abs() > 1e-12 {
3133            Some(sum / wsum)
3134        } else {
3135            None
3136        }
3137    }
3138
3139    #[inline]
3140    fn sample_lanczos_strict_simd_f64(
3141        &self,
3142        values: &[f64],
3143        band: isize,
3144        c0: isize,
3145        r0: isize,
3146        wx: &[f64; 6],
3147        wy: &[f64; 6],
3148    ) -> Option<f64> {
3149        let band_stride = self.rows * self.cols;
3150        let base = band as usize * band_stride;
3151
3152        let wx0 = f64x4::new([wx[0], wx[1], wx[2], wx[3]]);
3153        let wx1 = f64x4::new([wx[4], wx[5], 0.0, 0.0]);
3154        let wx_sum: f64 = wx.iter().copied().sum();
3155
3156        let mut sum = 0.0;
3157        let mut wsum = 0.0;
3158        for (j, wyj) in wy.iter().enumerate() {
3159            let rr = (r0 + j as isize - 2) as usize;
3160            let cc = (c0 - 2) as usize;
3161            let row_offset = base + rr * self.cols + cc;
3162
3163            let v = [
3164                values[row_offset],
3165                values[row_offset + 1],
3166                values[row_offset + 2],
3167                values[row_offset + 3],
3168                values[row_offset + 4],
3169                values[row_offset + 5],
3170            ];
3171            if v.into_iter().any(|cell| self.is_nodata(cell)) {
3172                return None;
3173            }
3174
3175            let v0 = f64x4::new([v[0], v[1], v[2], v[3]]);
3176            let v1 = f64x4::new([v[4], v[5], 0.0, 0.0]);
3177            let d0 = <[f64; 4]>::from(v0 * wx0);
3178            let d1 = <[f64; 4]>::from(v1 * wx1);
3179            let row_sum = d0.into_iter().sum::<f64>() + d1.into_iter().sum::<f64>();
3180            sum += row_sum * *wyj;
3181            wsum += wx_sum * *wyj;
3182        }
3183
3184        if wsum.abs() > 1e-12 {
3185            Some(sum / wsum)
3186        } else {
3187            None
3188        }
3189    }
3190
3191    #[inline]
3192    fn sample_lanczos_strict_simd_f32(
3193        &self,
3194        values: &[f32],
3195        band: isize,
3196        c0: isize,
3197        r0: isize,
3198        wx: &[f64; 6],
3199        wy: &[f64; 6],
3200    ) -> Option<f64> {
3201        let band_stride = self.rows * self.cols;
3202        let base = band as usize * band_stride;
3203
3204        let wx0 = f64x4::new([wx[0], wx[1], wx[2], wx[3]]);
3205        let wx1 = f64x4::new([wx[4], wx[5], 0.0, 0.0]);
3206        let wx_sum: f64 = wx.iter().copied().sum();
3207
3208        let mut sum = 0.0;
3209        let mut wsum = 0.0;
3210        for (j, wyj) in wy.iter().enumerate() {
3211            let rr = (r0 + j as isize - 2) as usize;
3212            let cc = (c0 - 2) as usize;
3213            let row_offset = base + rr * self.cols + cc;
3214
3215            let v = [
3216                values[row_offset] as f64,
3217                values[row_offset + 1] as f64,
3218                values[row_offset + 2] as f64,
3219                values[row_offset + 3] as f64,
3220                values[row_offset + 4] as f64,
3221                values[row_offset + 5] as f64,
3222            ];
3223            if v.into_iter().any(|cell| self.is_nodata(cell)) {
3224                return None;
3225            }
3226
3227            let v0 = f64x4::new([v[0], v[1], v[2], v[3]]);
3228            let v1 = f64x4::new([v[4], v[5], 0.0, 0.0]);
3229            let d0 = <[f64; 4]>::from(v0 * wx0);
3230            let d1 = <[f64; 4]>::from(v1 * wx1);
3231            let row_sum = d0.into_iter().sum::<f64>() + d1.into_iter().sum::<f64>();
3232            sum += row_sum * *wyj;
3233            wsum += wx_sum * *wyj;
3234        }
3235
3236        if wsum.abs() > 1e-12 {
3237            Some(sum / wsum)
3238        } else {
3239            None
3240        }
3241    }
3242
3243    fn sample_lanczos_partial_pixel(&self, band: isize, col_f: f64, row_f: f64) -> Option<f64> {
3244        if !col_f.is_finite() || !row_f.is_finite() {
3245            return None;
3246        }
3247
3248        let c0 = col_f.floor() as isize;
3249        let r0 = row_f.floor() as isize;
3250        let wx = lanczos3_weights(col_f, c0);
3251        let wy = lanczos3_weights(row_f, r0);
3252
3253        let mut sum = 0.0;
3254        let mut wsum = 0.0;
3255
3256        for (j, wyj) in wy.iter().enumerate() {
3257            let rr = clamp_isize(r0 + j as isize - 2, 0, self.rows as isize - 1);
3258            for (i, wxi) in wx.iter().enumerate() {
3259                let cc = clamp_isize(c0 + i as isize - 2, 0, self.cols as isize - 1);
3260                let w = *wxi * *wyj;
3261                if w == 0.0 {
3262                    continue;
3263                }
3264                if let Some(v) = self.get_opt(band, rr, cc) {
3265                    sum += v * w;
3266                    wsum += w;
3267                }
3268            }
3269        }
3270
3271        if wsum.abs() > 1e-12 {
3272            Some(sum / wsum)
3273        } else {
3274            None
3275        }
3276    }
3277
3278    // ─── Statistics ───────────────────────────────────────────────────────
3279
3280    fn stats_accumulator_range_with_mode(
3281        &self,
3282        start: usize,
3283        end: usize,
3284        mode: StatisticsComputationMode,
3285    ) -> StatsAccumulator {
3286        match mode {
3287            StatisticsComputationMode::Auto | StatisticsComputationMode::Simd => {
3288                self.stats_accumulator_range_simd(start, end)
3289            }
3290            StatisticsComputationMode::Scalar => self.stats_accumulator_range_scalar(start, end),
3291        }
3292    }
3293
3294    fn stats_accumulator_range_scalar(&self, start: usize, end: usize) -> StatsAccumulator {
3295        let mut accumulator = StatsAccumulator::default();
3296
3297        for idx in start..end {
3298            let value = self.data.get_f64(idx);
3299            if self.is_nodata(value) {
3300                accumulator.nodata_count += 1;
3301            } else {
3302                accumulator.min = accumulator.min.min(value);
3303                accumulator.max = accumulator.max.max(value);
3304                accumulator.sum += value;
3305                accumulator.sum_sq += value * value;
3306                accumulator.valid_count += 1;
3307            }
3308        }
3309
3310        accumulator
3311    }
3312
3313    /// SIMD-accelerated stats accumulation for a range of indices.
3314    /// Processes groups of 4 values in parallel where possible, falling back to scalar for remainder.
3315    /// This is automatically used by the statistics pipeline for better performance.
3316    fn stats_accumulator_range_simd(&self, start: usize, end: usize) -> StatsAccumulator {
3317        if let Some(values) = self.data_f64() {
3318            return self.stats_accumulator_range_simd_f64(values, start, end);
3319        }
3320        if let Some(values) = self.data_f32() {
3321            return self.stats_accumulator_range_simd_f32(values, start, end);
3322        }
3323
3324        self.stats_accumulator_range_scalar(start, end)
3325    }
3326
3327    fn stats_accumulator_range_simd_f64(
3328        &self,
3329        values: &[f64],
3330        start: usize,
3331        end: usize,
3332    ) -> StatsAccumulator {
3333        let mut accumulator = StatsAccumulator::default();
3334        let nd = self.nodata;
3335
3336        let simd_end = start + ((end - start) / 4) * 4;
3337        let mut simd_min = f64x4::splat(f64::INFINITY);
3338        let mut simd_max = f64x4::splat(f64::NEG_INFINITY);
3339        let mut simd_sum = f64x4::splat(0.0);
3340        let mut simd_sum_sq = f64x4::splat(0.0);
3341        let zero_v = f64x4::splat(0.0);
3342        let nd_v = f64x4::splat(nd);
3343        let inf_v = f64x4::splat(f64::INFINITY);
3344        let neg_inf_v = f64x4::splat(f64::NEG_INFINITY);
3345
3346        let mut idx = start;
3347        while idx < simd_end {
3348            let chunk = &values[idx..idx + 4];
3349            let values = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
3350
3351            let not_nodata = values.simd_ne(nd_v);
3352
3353            let values_for_min = not_nodata.blend(values, inf_v);
3354            let values_for_max = not_nodata.blend(values, neg_inf_v);
3355            simd_min = simd_min.min(values_for_min);
3356            simd_max = simd_max.max(values_for_max);
3357
3358            let masked_values = not_nodata.blend(values, zero_v);
3359            simd_sum = simd_sum + masked_values;
3360            simd_sum_sq = simd_sum_sq + masked_values * masked_values;
3361
3362            for &val in chunk {
3363                if val == nd {
3364                    accumulator.nodata_count += 1;
3365                } else {
3366                    accumulator.valid_count += 1;
3367                }
3368            }
3369
3370            idx += 4;
3371        }
3372
3373        let sum_arr = <[f64; 4]>::from(simd_sum);
3374        let sum_sq_arr = <[f64; 4]>::from(simd_sum_sq);
3375        let min_arr = <[f64; 4]>::from(simd_min);
3376        let max_arr = <[f64; 4]>::from(simd_max);
3377
3378        for i in 0..4 {
3379            accumulator.sum += sum_arr[i];
3380            accumulator.sum_sq += sum_sq_arr[i];
3381            accumulator.min = accumulator.min.min(min_arr[i]);
3382            accumulator.max = accumulator.max.max(max_arr[i]);
3383        }
3384
3385        for &value in &values[simd_end..end] {
3386            if value == nd {
3387                accumulator.nodata_count += 1;
3388            } else {
3389                accumulator.min = accumulator.min.min(value);
3390                accumulator.max = accumulator.max.max(value);
3391                accumulator.sum += value;
3392                accumulator.sum_sq += value * value;
3393                accumulator.valid_count += 1;
3394            }
3395        }
3396
3397        accumulator
3398    }
3399
3400    fn stats_accumulator_range_simd_f32(
3401        &self,
3402        values: &[f32],
3403        start: usize,
3404        end: usize,
3405    ) -> StatsAccumulator {
3406        let mut accumulator = StatsAccumulator::default();
3407        let nd = self.nodata as f32;
3408
3409        let simd_end = start + ((end - start) / 4) * 4;
3410        let mut simd_min = f64x4::splat(f64::INFINITY);
3411        let mut simd_max = f64x4::splat(f64::NEG_INFINITY);
3412        let mut simd_sum = f64x4::splat(0.0);
3413        let mut simd_sum_sq = f64x4::splat(0.0);
3414        let zero_v = f64x4::splat(0.0);
3415        let nd_v = f64x4::splat(nd as f64);
3416        let inf_v = f64x4::splat(f64::INFINITY);
3417        let neg_inf_v = f64x4::splat(f64::NEG_INFINITY);
3418
3419        let mut idx = start;
3420        while idx < simd_end {
3421            let chunk = &values[idx..idx + 4];
3422            let values = f64x4::new([
3423                chunk[0] as f64,
3424                chunk[1] as f64,
3425                chunk[2] as f64,
3426                chunk[3] as f64,
3427            ]);
3428
3429            let not_nodata = values.simd_ne(nd_v);
3430            let values_for_min = not_nodata.blend(values, inf_v);
3431            let values_for_max = not_nodata.blend(values, neg_inf_v);
3432            simd_min = simd_min.min(values_for_min);
3433            simd_max = simd_max.max(values_for_max);
3434
3435            let masked_values = not_nodata.blend(values, zero_v);
3436            simd_sum = simd_sum + masked_values;
3437            simd_sum_sq = simd_sum_sq + masked_values * masked_values;
3438
3439            for &val in chunk {
3440                if val == nd {
3441                    accumulator.nodata_count += 1;
3442                } else {
3443                    accumulator.valid_count += 1;
3444                }
3445            }
3446
3447            idx += 4;
3448        }
3449
3450        let sum_arr = <[f64; 4]>::from(simd_sum);
3451        let sum_sq_arr = <[f64; 4]>::from(simd_sum_sq);
3452        let min_arr = <[f64; 4]>::from(simd_min);
3453        let max_arr = <[f64; 4]>::from(simd_max);
3454
3455        for i in 0..4 {
3456            accumulator.sum += sum_arr[i];
3457            accumulator.sum_sq += sum_sq_arr[i];
3458            accumulator.min = accumulator.min.min(min_arr[i]);
3459            accumulator.max = accumulator.max.max(max_arr[i]);
3460        }
3461
3462        for &value in &values[simd_end..end] {
3463            if value == nd {
3464                accumulator.nodata_count += 1;
3465            } else {
3466                let value = value as f64;
3467                accumulator.min = accumulator.min.min(value);
3468                accumulator.max = accumulator.max.max(value);
3469                accumulator.sum += value;
3470                accumulator.sum_sq += value * value;
3471                accumulator.valid_count += 1;
3472            }
3473        }
3474
3475        accumulator
3476    }
3477
3478    fn statistics_for_index_range_with_mode(
3479        &self,
3480        start: usize,
3481        end: usize,
3482        mode: StatisticsComputationMode,
3483    ) -> Statistics {
3484        const PARALLEL_THRESHOLD: usize = 65_536;
3485        const CHUNK_SIZE: usize = 16_384;
3486
3487        let span = end.saturating_sub(start);
3488        if span == 0 {
3489            return Statistics {
3490                min: 0.0,
3491                max: 0.0,
3492                mean: 0.0,
3493                std_dev: 0.0,
3494                valid_count: 0,
3495                nodata_count: 0,
3496            };
3497        }
3498
3499        let total = if span < PARALLEL_THRESHOLD {
3500            self.stats_accumulator_range_with_mode(start, end, mode)
3501        } else {
3502            let chunk_starts: Vec<usize> = (start..end).step_by(CHUNK_SIZE).collect();
3503            let partials: Vec<StatsAccumulator> = chunk_starts
3504                .into_par_iter()
3505                .map(|chunk_start| {
3506                    let chunk_end = (chunk_start + CHUNK_SIZE).min(end);
3507                    self.stats_accumulator_range_with_mode(chunk_start, chunk_end, mode)
3508                })
3509                .collect();
3510
3511            partials
3512                .into_iter()
3513                .fold(StatsAccumulator::default(), |mut lhs, rhs| {
3514                    lhs.merge(rhs);
3515                    lhs
3516                })
3517        };
3518
3519        total.to_statistics()
3520    }
3521
3522    /// Compute basic statistics over all valid (non-nodata) cells.
3523    pub fn statistics(&self) -> Statistics {
3524        self.statistics_with_mode(StatisticsComputationMode::Auto)
3525    }
3526
3527    /// Compute basic statistics over all valid (non-nodata) cells using a selected computation path.
3528    pub fn statistics_with_mode(&self, mode: StatisticsComputationMode) -> Statistics {
3529        self.statistics_for_index_range_with_mode(0, self.data.len(), mode)
3530    }
3531
3532    /// Compute basic statistics over all valid (non-nodata) cells in one band.
3533    pub fn statistics_band(&self, band: isize) -> Result<Statistics> {
3534        self.statistics_band_with_mode(band, StatisticsComputationMode::Auto)
3535    }
3536
3537    /// Compute basic statistics over one band using a selected computation path.
3538    pub fn statistics_band_with_mode(
3539        &self,
3540        band: isize,
3541        mode: StatisticsComputationMode,
3542    ) -> Result<Statistics> {
3543        if band < 0 || band >= self.bands as isize {
3544            return Err(RasterError::OutOfBounds {
3545                band,
3546                col: 0,
3547                row: 0,
3548                bands: self.bands,
3549                cols: self.cols,
3550                rows: self.rows,
3551            });
3552        }
3553
3554        let band = band as usize;
3555        let band_stride = self.rows * self.cols;
3556        let start = band * band_stride;
3557        let end = start + band_stride;
3558
3559        Ok(self.statistics_for_index_range_with_mode(start, end, mode))
3560    }
3561
3562    // ─── Row iteration ────────────────────────────────────────────────────
3563
3564    /// Return a copy of one full band as row-major values.
3565    #[inline]
3566    pub fn band_slice(&self, band: isize) -> Vec<f64> {
3567        if band < 0 || band >= self.bands as isize {
3568            return Vec::new();
3569        }
3570        let band_stride = self.rows * self.cols;
3571        let start = band as usize * band_stride;
3572        (start..start + band_stride)
3573            .map(|i| self.data.get_f64(i))
3574            .collect()
3575    }
3576
3577    /// Set one full band from row-major values.
3578    pub fn set_band_slice(&mut self, band: isize, values: &[f64]) -> Result<()> {
3579        let expected = self.rows * self.cols;
3580        if values.len() != expected {
3581            return Err(RasterError::InvalidDimensions {
3582                cols: self.cols,
3583                rows: values.len(),
3584            });
3585        }
3586        if band < 0 || band >= self.bands as isize {
3587            return Err(RasterError::OutOfBounds {
3588                band,
3589                col: 0,
3590                row: 0,
3591                bands: self.bands,
3592                cols: self.cols,
3593                rows: self.rows,
3594            });
3595        }
3596        let band_stride = self.rows * self.cols;
3597        let start = band as usize * band_stride;
3598        for (i, v) in values.iter().copied().enumerate() {
3599            self.data.set_f64(start + i, v);
3600        }
3601        Ok(())
3602    }
3603
3604    /// Return a slice of the raw data for signed `(band, row)`.
3605    #[inline]
3606    pub fn row_slice(&self, band: isize, row: isize) -> Vec<f64> {
3607        if band < 0 || band >= self.bands as isize || row < 0 || row >= self.rows as isize {
3608            return Vec::new();
3609        }
3610        let band_stride = self.rows * self.cols;
3611        let start = band as usize * band_stride + row as usize * self.cols;
3612        (start..start + self.cols)
3613            .map(|i| self.data.get_f64(i))
3614            .collect()
3615    }
3616
3617    /// Set all values in signed `(band, row)` from an `f64` slice.
3618    pub fn set_row_slice(&mut self, band: isize, row: isize, values: &[f64]) -> Result<()> {
3619        if values.len() != self.cols {
3620            return Err(RasterError::InvalidDimensions {
3621                cols: values.len(),
3622                rows: self.rows,
3623            });
3624        }
3625        if band < 0 || band >= self.bands as isize || row < 0 || row >= self.rows as isize {
3626            return Err(RasterError::OutOfBounds {
3627                band,
3628                col: 0,
3629                row,
3630                bands: self.bands,
3631                cols: self.cols,
3632                rows: self.rows,
3633            });
3634        }
3635        let band_stride = self.rows * self.cols;
3636        let start = band as usize * band_stride + row as usize * self.cols;
3637        for (i, v) in values.iter().copied().enumerate() {
3638            self.data.set_f64(start + i, v);
3639        }
3640        Ok(())
3641    }
3642
3643    /// Iterate over signed `(band, row, col, value)` for all valid cells.
3644    pub fn iter_valid(&self) -> impl Iterator<Item = (isize, isize, isize, f64)> + '_ {
3645        self.data
3646            .iter_f64()
3647            .enumerate()
3648            .filter_map(move |(idx, v)| {
3649                if self.is_nodata(v) {
3650                    None
3651                } else {
3652                    let band_stride = self.rows * self.cols;
3653                    let band = idx / band_stride;
3654                    let rem = idx % band_stride;
3655                    let row = rem / self.cols;
3656                    let col = rem % self.cols;
3657                    Some((band as isize, row as isize, col as isize, v))
3658                }
3659            })
3660    }
3661
3662    /// Iterate over signed `(row, col, value)` for all valid cells in one band.
3663    pub fn iter_valid_band(
3664        &self,
3665        band: isize,
3666    ) -> Result<Box<dyn Iterator<Item = (isize, isize, f64)> + '_>> {
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 b = band as usize;
3679        let band_stride = self.rows * self.cols;
3680        let start = b * band_stride;
3681        Ok(Box::new((0..band_stride).filter_map(move |i| {
3682            let v = self.data.get_f64(start + i);
3683            if self.is_nodata(v) {
3684                None
3685            } else {
3686                let row = i / self.cols;
3687                let col = i % self.cols;
3688                Some((row as isize, col as isize, v))
3689            }
3690        })))
3691    }
3692
3693    /// Iterate over row vectors (`Vec<f64>`) for one band from north to south.
3694    pub fn iter_band_rows(&self, band: isize) -> Result<Box<dyn Iterator<Item = Vec<f64>> + '_>> {
3695        if band < 0 || band >= self.bands as isize {
3696            return Err(RasterError::OutOfBounds {
3697                band,
3698                col: 0,
3699                row: 0,
3700                bands: self.bands,
3701                cols: self.cols,
3702                rows: self.rows,
3703            });
3704        }
3705
3706        Ok(Box::new(
3707            (0..self.rows).map(move |row| self.row_slice(band, row as isize)),
3708        ))
3709    }
3710
3711    /// Traverse mutable native row slices for one band from north to south.
3712    ///
3713    /// This is a zero-allocation fast path for in-place row-wise processing.
3714    /// The callback receives `(row_index, typed_row_slice)`.
3715    pub fn for_each_band_row_mut<F>(&mut self, band: isize, mut f: F) -> Result<()>
3716    where
3717        F: FnMut(isize, RasterRowMut<'_>),
3718    {
3719        if band < 0 || band >= self.bands as isize {
3720            return Err(RasterError::OutOfBounds {
3721                band,
3722                col: 0,
3723                row: 0,
3724                bands: self.bands,
3725                cols: self.cols,
3726                rows: self.rows,
3727            });
3728        }
3729
3730        let b = band as usize;
3731        let band_stride = self.rows * self.cols;
3732        let start = b * band_stride;
3733        let end = start + band_stride;
3734
3735        match &mut self.data {
3736            RasterData::U8(v) => {
3737                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3738                    f(row as isize, RasterRowMut::U8(chunk));
3739                }
3740            }
3741            RasterData::I8(v) => {
3742                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3743                    f(row as isize, RasterRowMut::I8(chunk));
3744                }
3745            }
3746            RasterData::U16(v) => {
3747                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3748                    f(row as isize, RasterRowMut::U16(chunk));
3749                }
3750            }
3751            RasterData::I16(v) => {
3752                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3753                    f(row as isize, RasterRowMut::I16(chunk));
3754                }
3755            }
3756            RasterData::U32(v) => {
3757                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3758                    f(row as isize, RasterRowMut::U32(chunk));
3759                }
3760            }
3761            RasterData::I32(v) => {
3762                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3763                    f(row as isize, RasterRowMut::I32(chunk));
3764                }
3765            }
3766            RasterData::U64(v) => {
3767                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3768                    f(row as isize, RasterRowMut::U64(chunk));
3769                }
3770            }
3771            RasterData::I64(v) => {
3772                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3773                    f(row as isize, RasterRowMut::I64(chunk));
3774                }
3775            }
3776            RasterData::F32(v) => {
3777                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3778                    f(row as isize, RasterRowMut::F32(chunk));
3779                }
3780            }
3781            RasterData::F64(v) => {
3782                for (row, chunk) in v[start..end].chunks_mut(self.cols).enumerate() {
3783                    f(row as isize, RasterRowMut::F64(chunk));
3784                }
3785            }
3786        }
3787
3788        Ok(())
3789    }
3790
3791    /// Traverse immutable native row slices for one band from north to south.
3792    ///
3793    /// This is a zero-allocation fast path for read-only row-wise processing.
3794    /// The callback receives `(row_index, typed_row_slice)`.
3795    pub fn for_each_band_row<F>(&self, band: isize, mut f: F) -> Result<()>
3796    where
3797        F: FnMut(isize, RasterRowRef<'_>),
3798    {
3799        if band < 0 || band >= self.bands as isize {
3800            return Err(RasterError::OutOfBounds {
3801                band,
3802                col: 0,
3803                row: 0,
3804                bands: self.bands,
3805                cols: self.cols,
3806                rows: self.rows,
3807            });
3808        }
3809
3810        let b = band as usize;
3811        let band_stride = self.rows * self.cols;
3812        let start = b * band_stride;
3813        let end = start + band_stride;
3814
3815        match &self.data {
3816            RasterData::U8(v) => {
3817                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3818                    f(row as isize, RasterRowRef::U8(chunk));
3819                }
3820            }
3821            RasterData::I8(v) => {
3822                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3823                    f(row as isize, RasterRowRef::I8(chunk));
3824                }
3825            }
3826            RasterData::U16(v) => {
3827                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3828                    f(row as isize, RasterRowRef::U16(chunk));
3829                }
3830            }
3831            RasterData::I16(v) => {
3832                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3833                    f(row as isize, RasterRowRef::I16(chunk));
3834                }
3835            }
3836            RasterData::U32(v) => {
3837                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3838                    f(row as isize, RasterRowRef::U32(chunk));
3839                }
3840            }
3841            RasterData::I32(v) => {
3842                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3843                    f(row as isize, RasterRowRef::I32(chunk));
3844                }
3845            }
3846            RasterData::U64(v) => {
3847                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3848                    f(row as isize, RasterRowRef::U64(chunk));
3849                }
3850            }
3851            RasterData::I64(v) => {
3852                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3853                    f(row as isize, RasterRowRef::I64(chunk));
3854                }
3855            }
3856            RasterData::F32(v) => {
3857                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3858                    f(row as isize, RasterRowRef::F32(chunk));
3859                }
3860            }
3861            RasterData::F64(v) => {
3862                for (row, chunk) in v[start..end].chunks(self.cols).enumerate() {
3863                    f(row as isize, RasterRowRef::F64(chunk));
3864                }
3865            }
3866        }
3867
3868        Ok(())
3869    }
3870
3871    // ─── Transformation ───────────────────────────────────────────────────
3872
3873    /// Fill all cells with `value`.
3874    pub fn fill(&mut self, value: f64) {
3875        for i in 0..self.data.len() {
3876            self.data.set_f64(i, value);
3877        }
3878    }
3879
3880    /// Fill all cells with the nodata value.
3881    pub fn fill_nodata(&mut self) {
3882        let nd = self.nodata;
3883        self.fill(nd);
3884    }
3885
3886    /// Apply a function to every valid (non-nodata) cell value in-place.
3887    pub fn map_valid<F: Fn(f64) -> f64>(&mut self, f: F) {
3888        let nd = self.nodata;
3889        let nodata_nan = nd.is_nan();
3890        for i in 0..self.data.len() {
3891            let v = self.data.get_f64(i);
3892            let is_nd = if nodata_nan {
3893                v.is_nan()
3894            } else {
3895                (v - nd).abs() < 1e-10 * nd.abs().max(1.0)
3896            };
3897            if !is_nd {
3898                self.data.set_f64(i, f(v));
3899            }
3900        }
3901    }
3902
3903    /// Apply a function to every valid (non-nodata) cell value in one band in-place.
3904    pub fn map_valid_band<F: Fn(f64) -> f64>(&mut self, band: isize, f: F) -> Result<()> {
3905        if band < 0 || band >= self.bands as isize {
3906            return Err(RasterError::OutOfBounds {
3907                band,
3908                col: 0,
3909                row: 0,
3910                bands: self.bands,
3911                cols: self.cols,
3912                rows: self.rows,
3913            });
3914        }
3915
3916        let nd = self.nodata;
3917        let nodata_nan = nd.is_nan();
3918        let band_stride = self.rows * self.cols;
3919        let start = band as usize * band_stride;
3920        let end = start + band_stride;
3921
3922        for i in start..end {
3923            let v = self.data.get_f64(i);
3924            let is_nd = if nodata_nan {
3925                v.is_nan()
3926            } else {
3927                (v - nd).abs() < 1e-10 * nd.abs().max(1.0)
3928            };
3929            if !is_nd {
3930                self.data.set_f64(i, f(v));
3931            }
3932        }
3933        Ok(())
3934    }
3935
3936    /// Replace all occurrences of `from` with `to` in the data buffer.
3937    pub fn replace(&mut self, from: f64, to: f64) {
3938        for i in 0..self.data.len() {
3939            let v = self.data.get_f64(i);
3940            if (v - from).abs() < f64::EPSILON {
3941                self.data.set_f64(i, to);
3942            }
3943        }
3944    }
3945
3946    /// Replace all occurrences of `from` with `to` in one band.
3947    pub fn replace_band(&mut self, band: isize, from: f64, to: f64) -> Result<()> {
3948        if band < 0 || band >= self.bands as isize {
3949            return Err(RasterError::OutOfBounds {
3950                band,
3951                col: 0,
3952                row: 0,
3953                bands: self.bands,
3954                cols: self.cols,
3955                rows: self.rows,
3956            });
3957        }
3958
3959        let band_stride = self.rows * self.cols;
3960        let start = band as usize * band_stride;
3961        let end = start + band_stride;
3962        for i in start..end {
3963            let v = self.data.get_f64(i);
3964            if (v - from).abs() < f64::EPSILON {
3965                self.data.set_f64(i, to);
3966            }
3967        }
3968        Ok(())
3969    }
3970
3971    // ─── I/O ──────────────────────────────────────────────────────────────
3972
3973    /// Read a raster from `path`, detecting the format automatically.
3974    pub fn read<P: AsRef<Path>>(path: P) -> Result<Self> {
3975        let path = path.as_ref().to_string_lossy().to_string();
3976        if crate::formats::is_hdf_dataset_uri(&path) {
3977            return crate::formats::read_hdf_dataset_uri(&path);
3978        }
3979        let fmt = RasterFormat::detect(&path)?;
3980        fmt.read(&path)
3981    }
3982
3983    /// Read a raster from `path` using the specified format.
3984    pub fn read_with_format<P: AsRef<Path>>(path: P, fmt: RasterFormat) -> Result<Self> {
3985        let path = path.as_ref().to_string_lossy().to_string();
3986        fmt.read(&path)
3987    }
3988
3989    /// Write this raster to `path`, detecting the format from the extension.
3990    pub fn write<P: AsRef<Path>>(&self, path: P, fmt: RasterFormat) -> Result<()> {
3991        let path = path.as_ref().to_string_lossy().to_string();
3992        fmt.write(self, &path)
3993    }
3994
3995    /// Write this raster, auto-detecting the format from the file extension.
3996    pub fn write_auto<P: AsRef<Path>>(&self, path: P) -> Result<()> {
3997        let path = path.as_ref().to_string_lossy().to_string();
3998        let fmt = RasterFormat::detect(&path)?;
3999        fmt.write(self, &path)
4000    }
4001
4002    /// Write this raster as GeoTIFF/BigTIFF/COG using typed options.
4003    pub fn write_geotiff_with_options<P: AsRef<Path>>(
4004        &self,
4005        path: P,
4006        opts: &crate::formats::geotiff::GeoTiffWriteOptions,
4007    ) -> Result<()> {
4008        let path = path.as_ref().to_string_lossy().to_string();
4009        crate::formats::geotiff::write_with_options(self, &path, opts)
4010    }
4011
4012    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using
4013    /// convenience defaults.
4014    ///
4015    /// Defaults:
4016    /// - compression: Deflate
4017    /// - BigTIFF: false
4018    /// - COG tile size: 512
4019    pub fn write_cog<P: AsRef<Path>>(&self, path: P) -> Result<()> {
4020        let opts = crate::formats::geotiff::GeoTiffWriteOptions {
4021            compression: Some(crate::formats::geotiff::GeoTiffCompression::Deflate),
4022            bigtiff: Some(false),
4023            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog { tile_size: 512 }),
4024        };
4025        self.write_geotiff_with_options(path, &opts)
4026    }
4027
4028    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using
4029    /// convenience defaults and a custom tile size.
4030    ///
4031    /// Defaults:
4032    /// - compression: Deflate
4033    /// - BigTIFF: false
4034    /// - COG tile size: `tile_size`
4035    pub fn write_cog_with_tile_size<P: AsRef<Path>>(&self, path: P, tile_size: u32) -> Result<()> {
4036        let opts = crate::formats::geotiff::GeoTiffWriteOptions {
4037            compression: Some(crate::formats::geotiff::GeoTiffCompression::Deflate),
4038            bigtiff: Some(false),
4039            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog { tile_size }),
4040        };
4041        self.write_geotiff_with_options(path, &opts)
4042    }
4043
4044    /// Write this raster as a Cloud-Optimized GeoTIFF (COG) using COG-focused
4045    /// typed options.
4046    ///
4047    /// Any option set to `None` uses convenience defaults:
4048    /// - compression: Deflate
4049    /// - BigTIFF: false
4050    /// - COG tile size: 512
4051    pub fn write_cog_with_options<P: AsRef<Path>>(
4052        &self,
4053        path: P,
4054        opts: &crate::formats::geotiff::CogWriteOptions,
4055    ) -> Result<()> {
4056        let geotiff_opts = crate::formats::geotiff::GeoTiffWriteOptions {
4057            compression: Some(
4058                opts.compression
4059                    .unwrap_or(crate::formats::geotiff::GeoTiffCompression::Deflate),
4060            ),
4061            bigtiff: Some(opts.bigtiff.unwrap_or(false)),
4062            layout: Some(crate::formats::geotiff::GeoTiffLayout::Cog {
4063                tile_size: opts.tile_size.unwrap_or(512),
4064            }),
4065        };
4066        self.write_geotiff_with_options(path, &geotiff_opts)
4067    }
4068
4069    /// Write this raster as JPEG2000/GeoJP2 using typed options.
4070    pub fn write_jpeg2000_with_options<P: AsRef<Path>>(
4071        &self,
4072        path: P,
4073        opts: &crate::formats::jpeg2000::Jpeg2000WriteOptions,
4074    ) -> Result<()> {
4075        let path = path.as_ref().to_string_lossy().to_string();
4076        crate::formats::jpeg2000::write_with_options(self, &path, opts)
4077    }
4078}
4079
4080fn maybe_warn_area_of_use_mismatch(src: &Crs, dst: &Crs, src_extent: Extent, enabled: bool) {
4081    if !enabled {
4082        return;
4083    }
4084
4085    let src_area = src.area_of_use();
4086    let dst_area = dst.area_of_use();
4087    if src_area.is_none() && dst_area.is_none() {
4088        return;
4089    }
4090
4091    let Ok(wgs84) = Crs::from_epsg(4326) else {
4092        return;
4093    };
4094
4095    let sample_points = [
4096        (src_extent.x_min, src_extent.y_min),
4097        (src_extent.x_min, src_extent.y_max),
4098        (src_extent.x_max, src_extent.y_min),
4099        (src_extent.x_max, src_extent.y_max),
4100        (
4101            0.5 * (src_extent.x_min + src_extent.x_max),
4102            0.5 * (src_extent.y_min + src_extent.y_max),
4103        ),
4104    ];
4105
4106    let mut src_outside = 0usize;
4107    let mut dst_outside = 0usize;
4108    let mut checked = 0usize;
4109
4110    for (x, y) in sample_points {
4111        let Ok((lon, lat)) = src.transform_to(x, y, &wgs84) else {
4112            continue;
4113        };
4114        checked += 1;
4115
4116        if let Some(bb) = &src_area {
4117            if !bb.contains_geographic(lon, lat) {
4118                src_outside += 1;
4119            }
4120        }
4121        if let Some(bb) = &dst_area {
4122            if !bb.contains_geographic(lon, lat) {
4123                dst_outside += 1;
4124            }
4125        }
4126    }
4127
4128    if checked == 0 {
4129        return;
4130    }
4131
4132    if src_outside > 0 || dst_outside > 0 {
4133        eprintln!(
4134            "wbraster reprojection warning: sampled source extent appears outside CRS area of use (src outside: {src_outside}/{checked}, dst outside: {dst_outside}/{checked})"
4135        );
4136    }
4137}
4138
4139fn cubic_bspline_weights(t: f64) -> [f64; 4] {
4140    let t = t.clamp(0.0, 1.0);
4141    let t2 = t * t;
4142    let t3 = t2 * t;
4143    [
4144        ((1.0 - t) * (1.0 - t) * (1.0 - t)) / 6.0,
4145        (3.0 * t3 - 6.0 * t2 + 4.0) / 6.0,
4146        (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) / 6.0,
4147        t3 / 6.0,
4148    ]
4149}
4150
4151fn lanczos_kernel(x: f64, a: f64) -> f64 {
4152    if x.abs() < 1e-12 {
4153        return 1.0;
4154    }
4155    if x.abs() >= a {
4156        return 0.0;
4157    }
4158    let pix = std::f64::consts::PI * x;
4159    let pix_over_a = pix / a;
4160    (pix.sin() / pix) * (pix_over_a.sin() / pix_over_a)
4161}
4162
4163fn lanczos3_weights(sample_f: f64, floor_idx: isize) -> [f64; 6] {
4164    let mut w = [0.0_f64; 6];
4165    for (i, wi) in w.iter_mut().enumerate() {
4166        let idx = floor_idx + i as isize - 2;
4167        let dx = sample_f - idx as f64;
4168        *wi = lanczos_kernel(dx, 3.0);
4169    }
4170    w
4171}
4172
4173fn sample_extent_boundary_points(extent: Extent, samples_per_edge: usize) -> Vec<(f64, f64)> {
4174    let n = samples_per_edge.max(1);
4175    let mut pts = Vec::with_capacity(4 * n);
4176
4177    // Bottom and top edges include corners.
4178    for i in 0..=n {
4179        let t = i as f64 / n as f64;
4180        let x = extent.x_min + t * (extent.x_max - extent.x_min);
4181        pts.push((x, extent.y_min));
4182        pts.push((x, extent.y_max));
4183    }
4184
4185    // Left and right edges exclude corners to avoid duplicate corner points.
4186    for j in 1..n {
4187        let t = j as f64 / n as f64;
4188        let y = extent.y_min + t * (extent.y_max - extent.y_min);
4189        pts.push((extent.x_min, y));
4190        pts.push((extent.x_max, y));
4191    }
4192
4193    pts
4194}
4195
4196fn sample_extent_boundary_ring(extent: Extent, samples_per_edge: usize) -> Vec<(f64, f64)> {
4197    let n = samples_per_edge.max(1);
4198    let mut ring = Vec::with_capacity(n * 4);
4199
4200    for i in 0..n {
4201        let t = if n == 1 {
4202            0.0
4203        } else {
4204            i as f64 / (n - 1) as f64
4205        };
4206        let x = extent.x_min + t * (extent.x_max - extent.x_min);
4207        ring.push((x, extent.y_min));
4208    }
4209
4210    for i in 0..n {
4211        let t = if n == 1 {
4212            0.0
4213        } else {
4214            i as f64 / (n - 1) as f64
4215        };
4216        let y = extent.y_min + t * (extent.y_max - extent.y_min);
4217        ring.push((extent.x_max, y));
4218    }
4219
4220    for i in 0..n {
4221        let t = if n == 1 {
4222            0.0
4223        } else {
4224            i as f64 / (n - 1) as f64
4225        };
4226        let x = extent.x_max - t * (extent.x_max - extent.x_min);
4227        ring.push((x, extent.y_max));
4228    }
4229
4230    for i in 0..n {
4231        let t = if n == 1 {
4232            0.0
4233        } else {
4234            i as f64 / (n - 1) as f64
4235        };
4236        let y = extent.y_max - t * (extent.y_max - extent.y_min);
4237        ring.push((extent.x_min, y));
4238    }
4239
4240    ring
4241}
4242
4243fn transformed_extent_from_boundary_samples(
4244    src_crs: &Crs,
4245    dst_crs: &Crs,
4246    src_extent: Extent,
4247    samples_per_edge: usize,
4248    dst_epsg: u32,
4249    antimeridian_policy: AntimeridianPolicy,
4250    epoch_transform: &EpochTransformOptions,
4251) -> Result<Extent> {
4252    let points = sample_extent_boundary_points(src_extent, samples_per_edge);
4253
4254    let mut tx_min = f64::INFINITY;
4255    let mut tx_max = f64::NEG_INFINITY;
4256    let mut ty_min = f64::INFINITY;
4257    let mut ty_max = f64::NEG_INFINITY;
4258    let mut tx_values = Vec::new();
4259    let mut valid = 0usize;
4260
4261    for (x, y) in points {
4262        let Ok((tx, ty)) = transform_xy_with_epoch_options(src_crs, dst_crs, x, y, epoch_transform)
4263        else {
4264            continue;
4265        };
4266        if !tx.is_finite() || !ty.is_finite() {
4267            continue;
4268        }
4269        tx_min = tx_min.min(tx);
4270        tx_max = tx_max.max(tx);
4271        ty_min = ty_min.min(ty);
4272        ty_max = ty_max.max(ty);
4273        tx_values.push(tx);
4274        valid += 1;
4275    }
4276
4277    if valid == 0 {
4278        return Err(RasterError::Other(format!(
4279            "failed to transform source extent boundary samples to EPSG:{}",
4280            dst_epsg
4281        )));
4282    }
4283
4284    if dst_epsg == 4326 {
4285        if let Some((x0, x1)) = antimeridian_aware_longitude_bounds(&tx_values, antimeridian_policy)
4286        {
4287            tx_min = x0;
4288            tx_max = x1;
4289        }
4290    }
4291
4292    Ok(Extent {
4293        x_min: tx_min,
4294        y_min: ty_min,
4295        x_max: tx_max,
4296        y_max: ty_max,
4297    })
4298}
4299
4300fn transformed_boundary_ring_samples(
4301    src_crs: &Crs,
4302    dst_crs: &Crs,
4303    src_extent: Extent,
4304    samples_per_edge: usize,
4305    dst_epsg: u32,
4306    antimeridian_policy: AntimeridianPolicy,
4307    epoch_transform: &EpochTransformOptions,
4308) -> Result<Vec<(f64, f64)>> {
4309    let ring = sample_extent_boundary_ring(src_extent, samples_per_edge);
4310    let mut transformed = Vec::with_capacity(ring.len());
4311
4312    for (x, y) in ring {
4313        let Ok((tx, ty)) = transform_xy_with_epoch_options(src_crs, dst_crs, x, y, epoch_transform)
4314        else {
4315            continue;
4316        };
4317        if tx.is_finite() && ty.is_finite() {
4318            transformed.push((tx, ty));
4319        }
4320    }
4321
4322    if transformed.len() < 3 {
4323        return Err(RasterError::Other(format!(
4324            "failed to build transformed boundary ring for EPSG:{}",
4325            dst_epsg
4326        )));
4327    }
4328
4329    if dst_epsg == 4326 && antimeridian_policy != AntimeridianPolicy::Linear {
4330        let lons: Vec<f64> = transformed.iter().map(|(x, _)| *x).collect();
4331        if let Some((base, _)) = minimal_wrapped_longitude_bounds(&lons) {
4332            for (x, _) in &mut transformed {
4333                let mut w = wrap_lon_360(*x);
4334                if w < base {
4335                    w += 360.0;
4336                }
4337                *x = w;
4338            }
4339        }
4340    }
4341
4342    Ok(transformed)
4343}
4344
4345fn snap_down_to_origin(value: f64, origin: f64, step: f64) -> f64 {
4346    origin + ((value - origin) / step).floor() * step
4347}
4348
4349fn snap_up_to_origin(value: f64, origin: f64, step: f64) -> f64 {
4350    origin + ((value - origin) / step).ceil() * step
4351}
4352
4353fn transform_xy_with_epoch_options(
4354    src: &Crs,
4355    dst: &Crs,
4356    x: f64,
4357    y: f64,
4358    options: &EpochTransformOptions,
4359) -> Result<(f64, f64)> {
4360    options
4361        .validate()
4362        .map_err(|e| RasterError::Other(format!("invalid epoch transform options: {e}")))?;
4363    let ctx = options
4364        .build_context()
4365        .map_err(|e| RasterError::Other(format!("invalid epoch transform options: {e}")))?;
4366    let epoch_routing_requested = options.coordinate_epoch_decimal_year.is_some()
4367        || options.source_reference_epoch_decimal_year.is_some()
4368        || options.target_reference_epoch_decimal_year.is_some()
4369        || options.operation_code.is_some()
4370        || !options.prefer_official_operation
4371        || matches!(options.epoch_policy, EpochPolicy::AllowStaticFallback);
4372
4373    if !epoch_routing_requested {
4374        return src
4375            .transform_to(x, y, dst)
4376            .map_err(|err| RasterError::Other(format!("epoch-aware transform failed: {err}")));
4377    }
4378
4379    let result = if let Some(operation_code) = options.operation_code {
4380        src.transform_to_with_operation(x, y, dst, operation_code, ctx)
4381    } else if options.prefer_official_operation {
4382        src.transform_to_with_preferred_operation(x, y, dst, ctx)
4383    } else if let Some(epoch_ctx) = ctx {
4384        src.transform_to_with_context(x, y, dst, epoch_ctx)
4385    } else {
4386        src.transform_to_with_policy(x, y, dst, CrsTransformPolicy::Auto)
4387    };
4388
4389    match result {
4390        Ok(v) => Ok(v),
4391        Err(err) if matches!(options.epoch_policy, EpochPolicy::AllowStaticFallback) => src
4392            .transform_to_with_policy(x, y, dst, CrsTransformPolicy::Auto)
4393            .map_err(|fallback_err| {
4394                RasterError::Other(format!(
4395                    "epoch-aware transform failed ({err}); static fallback failed ({fallback_err})"
4396                ))
4397            }),
4398        Err(err) => Err(RasterError::Other(format!(
4399            "epoch-aware transform failed: {err}"
4400        ))),
4401    }
4402}
4403
4404fn wrap_lon_360(lon: f64) -> f64 {
4405    let mut v = lon % 360.0;
4406    if v < 0.0 {
4407        v += 360.0;
4408    }
4409    v
4410}
4411
4412fn minimal_wrapped_longitude_bounds(longitudes: &[f64]) -> Option<(f64, f64)> {
4413    if longitudes.is_empty() {
4414        return None;
4415    }
4416
4417    if longitudes.len() == 1 {
4418        let v = wrap_lon_360(longitudes[0]);
4419        return Some((v, v));
4420    }
4421
4422    let mut values: Vec<f64> = longitudes.iter().map(|v| wrap_lon_360(*v)).collect();
4423    values.sort_by(f64::total_cmp);
4424
4425    let n = values.len();
4426    let mut max_gap = f64::NEG_INFINITY;
4427    let mut max_gap_idx = 0usize;
4428    for i in 0..n {
4429        let next = if i + 1 < n {
4430            values[i + 1]
4431        } else {
4432            values[0] + 360.0
4433        };
4434        let gap = next - values[i];
4435        if gap > max_gap {
4436            max_gap = gap;
4437            max_gap_idx = i;
4438        }
4439    }
4440
4441    let start = values[(max_gap_idx + 1) % n];
4442    let mut end = values[max_gap_idx];
4443    if end < start {
4444        end += 360.0;
4445    }
4446
4447    Some((start, end))
4448}
4449
4450fn antimeridian_aware_longitude_bounds(
4451    longitudes: &[f64],
4452    policy: AntimeridianPolicy,
4453) -> Option<(f64, f64)> {
4454    if longitudes.is_empty() {
4455        return None;
4456    }
4457
4458    let mut lin_min = f64::INFINITY;
4459    let mut lin_max = f64::NEG_INFINITY;
4460    for lon in longitudes {
4461        if !lon.is_finite() {
4462            continue;
4463        }
4464        lin_min = lin_min.min(*lon);
4465        lin_max = lin_max.max(*lon);
4466    }
4467    if !lin_min.is_finite() || !lin_max.is_finite() {
4468        return None;
4469    }
4470
4471    if policy == AntimeridianPolicy::Linear {
4472        return Some((lin_min, lin_max));
4473    }
4474
4475    let Some((wrap_min, wrap_max)) = minimal_wrapped_longitude_bounds(longitudes) else {
4476        return Some((lin_min, lin_max));
4477    };
4478    if policy == AntimeridianPolicy::Wrap {
4479        return Some((wrap_min, wrap_max));
4480    }
4481
4482    let linear_width = lin_max - lin_min;
4483    let wrapped_width = wrap_max - wrap_min;
4484
4485    if wrapped_width + 1e-9 < linear_width {
4486        Some((wrap_min, wrap_max))
4487    } else {
4488        Some((lin_min, lin_max))
4489    }
4490}
4491
4492fn clamp_isize(v: isize, min_v: isize, max_v: isize) -> isize {
4493    v.max(min_v).min(max_v)
4494}
4495
4496fn point_in_polygon(x: f64, y: f64, polygon: &[(f64, f64)]) -> bool {
4497    if polygon.len() < 3 {
4498        return false;
4499    }
4500    let mut inside = false;
4501    let mut j = polygon.len() - 1;
4502    for i in 0..polygon.len() {
4503        let (xi, yi) = polygon[i];
4504        let (xj, yj) = polygon[j];
4505        let intersects =
4506            (yi > y) != (yj > y) && x < (xj - xi) * (y - yi) / ((yj - yi) + 1e-30) + xi;
4507        if intersects {
4508            inside = !inside;
4509        }
4510        j = i;
4511    }
4512    inside
4513}
4514
4515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4516enum WindowStat {
4517    Mean,
4518    Min,
4519    Max,
4520    Mode,
4521    Median,
4522    StdDev,
4523}
4524
4525fn reduce_window_values(values: &[f64], stat: WindowStat) -> Option<f64> {
4526    if values.is_empty() {
4527        return None;
4528    }
4529
4530    match stat {
4531        WindowStat::Mean => Some(values.iter().sum::<f64>() / values.len() as f64),
4532        WindowStat::Min => values.iter().copied().reduce(f64::min),
4533        WindowStat::Max => values.iter().copied().reduce(f64::max),
4534        WindowStat::Mode => {
4535            let mut pairs: Vec<(f64, usize)> = Vec::new();
4536            for v in values {
4537                if let Some((_, count)) = pairs.iter_mut().find(|(u, _)| *u == *v) {
4538                    *count += 1;
4539                } else {
4540                    pairs.push((*v, 1));
4541                }
4542            }
4543            pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.total_cmp(&b.0)));
4544            pairs.first().map(|(v, _)| *v)
4545        }
4546        WindowStat::Median => {
4547            let mut sorted = values.to_vec();
4548            sorted.sort_by(f64::total_cmp);
4549            let n = sorted.len();
4550            if n % 2 == 1 {
4551                Some(sorted[n / 2])
4552            } else {
4553                Some((sorted[n / 2 - 1] + sorted[n / 2]) / 2.0)
4554            }
4555        }
4556        WindowStat::StdDev => {
4557            let n = values.len() as f64;
4558            let mean = values.iter().sum::<f64>() / n;
4559            let variance = values
4560                .iter()
4561                .map(|v| {
4562                    let d = *v - mean;
4563                    d * d
4564                })
4565                .sum::<f64>()
4566                / n;
4567            Some(variance.max(0.0).sqrt())
4568        }
4569    }
4570}
4571
4572impl std::fmt::Display for Raster {
4573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4574        write!(
4575            f,
4576            "Raster({}×{}×{}, cell={:.6}, x=[{:.4},{:.4}], y=[{:.4},{:.4}], type={}, nodata={})",
4577            self.bands,
4578            self.cols,
4579            self.rows,
4580            self.cell_size_x,
4581            self.x_min,
4582            self.x_max(),
4583            self.y_min,
4584            self.y_max(),
4585            self.data_type,
4586            self.nodata,
4587        )
4588    }
4589}
4590
4591#[cfg(test)]
4592mod tests {
4593    use super::*;
4594    use flate2::{write::ZlibEncoder, Compression};
4595    use std::io::Write;
4596    use std::sync::{Arc, Mutex};
4597
4598    fn make_raster() -> Raster {
4599        let cfg = RasterConfig {
4600            cols: 4,
4601            rows: 3,
4602            cell_size: 10.0,
4603            nodata: -9999.0,
4604            ..Default::default()
4605        };
4606        let mut r = Raster::new(cfg);
4607        for row in 0..3 {
4608            for col in 0..4 {
4609                let _ = r.set(0, row, col, (row * 4 + col) as f64);
4610            }
4611        }
4612        r
4613    }
4614
4615    fn write_synthetic_hdf4_i16_fixture(file: &mut tempfile::NamedTempFile) {
4616        let mut bytes = vec![0x0E, 0x03, 0x13, 0x01];
4617        bytes.extend_from_slice(&[0x00, 0x01]);
4618        bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
4619        bytes.extend_from_slice(&[
4620            0x02, 0xBE, 0x00, 0x03, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x10,
4621        ]);
4622        bytes.resize(0x90, 0);
4623        bytes[0x80..0x90].copy_from_slice(&[
4624            0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00,
4625            0x08, 0x00,
4626        ]);
4627        bytes.extend_from_slice(
4628            b"\nStructMetadata.0\nGridName=\"GridA\"\nXDim=4\nYDim=2\nUpperLeftPointMtrs=(0,10)\nLowerRightMtrs=(20,0)\nDataFieldName=\"FieldA\"\nDataType=DFNT_INT16\nDimList=(\"YDim\",\"XDim\")\n",
4629        );
4630        file.write_all(&bytes)
4631            .expect("synthetic HDF4 fixture should be writable");
4632    }
4633
4634    fn write_synthetic_hdf5_contiguous_fixture_header(
4635        bytes: &mut [u8],
4636        dataset_path: &str,
4637        payload_offset: usize,
4638        element_count: usize,
4639        bytes_per_value: usize,
4640    ) {
4641        const HEADER_OFFSET: usize = 256;
4642        const CONTINUATION_OFFSET: usize = 512;
4643        const CONTINUATION_SIZE: usize = 26;
4644
4645        let marker = dataset_path.as_bytes();
4646        bytes[64..64 + marker.len()].copy_from_slice(marker);
4647
4648        let payload_size = element_count * bytes_per_value;
4649
4650        let mut cursor = HEADER_OFFSET;
4651        bytes[cursor..cursor + 4].copy_from_slice(b"OHDR");
4652        cursor += 4;
4653        bytes[cursor] = 2;
4654        cursor += 1;
4655        bytes[cursor] = 0;
4656        cursor += 1;
4657        bytes[cursor] = 0x34;
4658        cursor += 1;
4659
4660        bytes[cursor..cursor + 4].copy_from_slice(&[0x01, 0x10, 0x00, 0x00]);
4661        cursor += 4;
4662        bytes[cursor..cursor + 8].copy_from_slice(&[0x02, 0x01, 0x00, 0x00, 0, 0, 0, 0]);
4663        cursor += 8;
4664        bytes[cursor..cursor + 8].copy_from_slice(&(element_count as u64).to_le_bytes());
4665        cursor += 8;
4666
4667        bytes[cursor..cursor + 4].copy_from_slice(&[0x03, 0x08, 0x00, 0x00]);
4668        cursor += 4;
4669        bytes[cursor] = 0x13;
4670        bytes[cursor + 1] = 0x00;
4671        bytes[cursor + 2] = 0x00;
4672        bytes[cursor + 3] = 0x00;
4673        bytes[cursor + 4..cursor + 8].copy_from_slice(&(bytes_per_value as u32).to_le_bytes());
4674        cursor += 8;
4675
4676        bytes[cursor..cursor + 4].copy_from_slice(&[0x10, 0x10, 0x00, 0x00]);
4677        cursor += 4;
4678        bytes[cursor..cursor + 8].copy_from_slice(&(CONTINUATION_OFFSET as u64).to_le_bytes());
4679        cursor += 8;
4680        bytes[cursor..cursor + 8].copy_from_slice(&(CONTINUATION_SIZE as u64).to_le_bytes());
4681
4682        let mut continuation_cursor = CONTINUATION_OFFSET;
4683        bytes[continuation_cursor..continuation_cursor + 4].copy_from_slice(b"OCHK");
4684        continuation_cursor += 4;
4685        bytes[continuation_cursor..continuation_cursor + 4]
4686            .copy_from_slice(&[0x08, 0x12, 0x00, 0x00]);
4687        continuation_cursor += 4;
4688        bytes[continuation_cursor] = 0x03;
4689        bytes[continuation_cursor + 1] = 0x01;
4690        continuation_cursor += 2;
4691        bytes[continuation_cursor..continuation_cursor + 8]
4692            .copy_from_slice(&(payload_offset as u64).to_le_bytes());
4693        continuation_cursor += 8;
4694        bytes[continuation_cursor..continuation_cursor + 8]
4695            .copy_from_slice(&(payload_size as u64).to_le_bytes());
4696    }
4697
4698    fn write_synthetic_hdf5_gedi_contiguous_fixture(file: &mut tempfile::NamedTempFile) {
4699        const OFFSET: usize = 321_111;
4700        const ELEMENTS: usize = 89_634;
4701
4702        let mut bytes = vec![0u8; OFFSET + ELEMENTS * 4];
4703        write_synthetic_hdf5_contiguous_fixture_header(
4704            &mut bytes,
4705            "/BEAM0000/elev_lowestmode",
4706            OFFSET,
4707            ELEMENTS,
4708            4,
4709        );
4710
4711        for i in 0..ELEMENTS {
4712            let v = i as f32;
4713            let start = OFFSET + i * 4;
4714            bytes[start..start + 4].copy_from_slice(&v.to_le_bytes());
4715        }
4716        file.write_all(&bytes)
4717            .expect("synthetic HDF5 fixture should be writable");
4718    }
4719
4720    fn write_synthetic_hdf5_viirs_xdim_fixture(file: &mut tempfile::NamedTempFile) {
4721        const OFFSET: usize = 45_321;
4722        const ELEMENTS: usize = 2_400;
4723
4724        let mut bytes = vec![0u8; OFFSET + ELEMENTS * 8];
4725        write_synthetic_hdf5_contiguous_fixture_header(
4726            &mut bytes,
4727            "/HDFEOS/GRIDS/VIIRS_Grid_8Day_VI_500m/XDim",
4728            OFFSET,
4729            ELEMENTS,
4730            8,
4731        );
4732
4733        for i in 0..ELEMENTS {
4734            let v = i as f64 + 0.5;
4735            let start = OFFSET + i * 8;
4736            bytes[start..start + 8].copy_from_slice(&v.to_le_bytes());
4737        }
4738        file.write_all(&bytes)
4739            .expect("synthetic HDF5 VIIRS fixture should be writable");
4740    }
4741
4742    fn write_synthetic_hdf5_generic_contiguous_f32_fixture(file: &mut tempfile::NamedTempFile) {
4743        const OFFSET: usize = 62_222;
4744        const ELEMENTS: usize = 12;
4745        const DATASET_PATH: &str = "/ScienceData/NDVI";
4746
4747        let mut bytes = vec![0u8; OFFSET + ELEMENTS * 4];
4748        write_synthetic_hdf5_contiguous_fixture_header(
4749            &mut bytes,
4750            DATASET_PATH,
4751            OFFSET,
4752            ELEMENTS,
4753            4,
4754        );
4755
4756        for i in 0..ELEMENTS {
4757            let v = (i as f32) * 0.25;
4758            let start = OFFSET + i * 4;
4759            bytes[start..start + 4].copy_from_slice(&v.to_le_bytes());
4760        }
4761        file.write_all(&bytes)
4762            .expect("synthetic HDF5 generic fixture should be writable");
4763    }
4764
4765    fn write_synthetic_hdf5_chunked_single_chunk_f32_fixture(file: &mut tempfile::NamedTempFile) {
4766        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked";
4767        const HEADER_OFFSET: usize = 1024;
4768        const CHUNK_INDEX_OFFSET: usize = 4096;
4769        const PAYLOAD_OFFSET: usize = 8192;
4770        const ROWS: usize = 2;
4771        const COLS: usize = 3;
4772
4773        let values = [0.5f32, 1.5, 2.5, 3.5, 4.5, 5.5];
4774        let mut raw_payload = Vec::<u8>::new();
4775        for value in values {
4776            raw_payload.extend_from_slice(&value.to_le_bytes());
4777        }
4778
4779        let total_len = PAYLOAD_OFFSET + raw_payload.len();
4780        let mut bytes = vec![0u8; total_len];
4781
4782        let marker = DATASET_PATH.as_bytes();
4783        bytes[64..64 + marker.len()].copy_from_slice(marker);
4784
4785        // v1 object header prefix.
4786        let mut cursor = HEADER_OFFSET;
4787        bytes[cursor] = 1;
4788        bytes[cursor + 1] = 0;
4789        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
4790        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
4791        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
4792        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
4793        cursor += 16;
4794
4795        // Dataspace message (rank=2, dims=2x3).
4796        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
4797        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
4798        bytes[cursor + 4] = 0;
4799        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4800        cursor += 8;
4801        bytes[cursor] = 2;
4802        bytes[cursor + 1] = 2;
4803        bytes[cursor + 2] = 0;
4804        bytes[cursor + 3] = 0;
4805        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
4806        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
4807        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
4808        cursor += 24;
4809
4810        // Datatype message (size=4).
4811        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
4812        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
4813        bytes[cursor + 4] = 0;
4814        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4815        cursor += 8;
4816        bytes[cursor] = 0x13;
4817        bytes[cursor + 1] = 0;
4818        bytes[cursor + 2] = 0;
4819        bytes[cursor + 3] = 0;
4820        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
4821        cursor += 8;
4822
4823        // Chunked layout message (num_dimensions=2, chunk dims=2x3).
4824        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
4825        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
4826        bytes[cursor + 4] = 0;
4827        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4828        cursor += 8;
4829        bytes[cursor] = 3;
4830        bytes[cursor + 1] = 2;
4831        bytes[cursor + 2] = 2;
4832        bytes[cursor + 3..cursor + 11].copy_from_slice(&(CHUNK_INDEX_OFFSET as u64).to_le_bytes());
4833        bytes[cursor + 11..cursor + 15].copy_from_slice(&(ROWS as u32).to_le_bytes());
4834        bytes[cursor + 15..cursor + 19].copy_from_slice(&(COLS as u32).to_le_bytes());
4835
4836        // Chunk index node (first leaf record only, little-endian fields per current bounded parser).
4837        let mut node_cursor = CHUNK_INDEX_OFFSET;
4838        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
4839        bytes[node_cursor + 4] = 1;
4840        bytes[node_cursor + 5] = 0;
4841        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
4842        node_cursor += 24;
4843        bytes[node_cursor..node_cursor + 8]
4844            .copy_from_slice(&(raw_payload.len() as u64).to_le_bytes());
4845        node_cursor += 8;
4846        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
4847        node_cursor += 8;
4848        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
4849        node_cursor += 8;
4850        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(PAYLOAD_OFFSET as u64).to_le_bytes());
4851
4852        // Chunk payload bytes.
4853        bytes[PAYLOAD_OFFSET..PAYLOAD_OFFSET + raw_payload.len()].copy_from_slice(&raw_payload);
4854
4855        file.write_all(&bytes)
4856            .expect("synthetic HDF5 chunked fixture should be writable");
4857    }
4858
4859    fn write_synthetic_hdf5_chunked_two_chunk_f32_fixture(file: &mut tempfile::NamedTempFile) {
4860        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_Two";
4861        const HEADER_OFFSET: usize = 1024;
4862        const CHUNK_INDEX_OFFSET: usize = 4096;
4863        const PAYLOAD_A_OFFSET: usize = 8192;
4864        const PAYLOAD_B_OFFSET: usize = 8208;
4865        const ROWS: usize = 2;
4866        const COLS: usize = 4;
4867        const CHUNK_ROWS: usize = 2;
4868        const CHUNK_COLS: usize = 2;
4869
4870        let values_a = [0.5f32, 1.5, 4.5, 5.5];
4871        let values_b = [2.5f32, 3.5, 6.5, 7.5];
4872        let mut payload_a = Vec::<u8>::new();
4873        let mut payload_b = Vec::<u8>::new();
4874        for value in values_a {
4875            payload_a.extend_from_slice(&value.to_le_bytes());
4876        }
4877        for value in values_b {
4878            payload_b.extend_from_slice(&value.to_le_bytes());
4879        }
4880
4881        let total_len = PAYLOAD_B_OFFSET + payload_b.len();
4882        let mut bytes = vec![0u8; total_len];
4883        let marker = DATASET_PATH.as_bytes();
4884        bytes[64..64 + marker.len()].copy_from_slice(marker);
4885
4886        let mut cursor = HEADER_OFFSET;
4887        bytes[cursor] = 1;
4888        bytes[cursor + 1] = 0;
4889        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
4890        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
4891        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
4892        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
4893        cursor += 16;
4894
4895        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
4896        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
4897        bytes[cursor + 4] = 0;
4898        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4899        cursor += 8;
4900        bytes[cursor] = 2;
4901        bytes[cursor + 1] = 2;
4902        bytes[cursor + 2] = 0;
4903        bytes[cursor + 3] = 0;
4904        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
4905        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
4906        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
4907        cursor += 24;
4908
4909        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
4910        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
4911        bytes[cursor + 4] = 0;
4912        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4913        cursor += 8;
4914        bytes[cursor] = 0x13;
4915        bytes[cursor + 1] = 0;
4916        bytes[cursor + 2] = 0;
4917        bytes[cursor + 3] = 0;
4918        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
4919        cursor += 8;
4920
4921        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
4922        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
4923        bytes[cursor + 4] = 0;
4924        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
4925        cursor += 8;
4926        bytes[cursor] = 3;
4927        bytes[cursor + 1] = 2;
4928        bytes[cursor + 2] = 2;
4929        bytes[cursor + 3..cursor + 11].copy_from_slice(&(CHUNK_INDEX_OFFSET as u64).to_le_bytes());
4930        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
4931        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
4932
4933        let mut node_cursor = CHUNK_INDEX_OFFSET;
4934        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
4935        bytes[node_cursor + 4] = 1;
4936        bytes[node_cursor + 5] = 0;
4937        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(2u16).to_le_bytes());
4938        node_cursor += 24;
4939
4940        bytes[node_cursor..node_cursor + 8]
4941            .copy_from_slice(&(payload_a.len() as u64).to_le_bytes());
4942        node_cursor += 8;
4943        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
4944        node_cursor += 8;
4945        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
4946        node_cursor += 8;
4947        bytes[node_cursor..node_cursor + 8]
4948            .copy_from_slice(&(PAYLOAD_A_OFFSET as u64).to_le_bytes());
4949        node_cursor += 8;
4950
4951        bytes[node_cursor..node_cursor + 8]
4952            .copy_from_slice(&(payload_b.len() as u64).to_le_bytes());
4953        node_cursor += 8;
4954        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
4955        node_cursor += 8;
4956        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
4957        node_cursor += 8;
4958        bytes[node_cursor..node_cursor + 8]
4959            .copy_from_slice(&(PAYLOAD_B_OFFSET as u64).to_le_bytes());
4960
4961        bytes[PAYLOAD_A_OFFSET..PAYLOAD_A_OFFSET + payload_a.len()].copy_from_slice(&payload_a);
4962        bytes[PAYLOAD_B_OFFSET..PAYLOAD_B_OFFSET + payload_b.len()].copy_from_slice(&payload_b);
4963
4964        file.write_all(&bytes)
4965            .expect("synthetic HDF5 two-chunk fixture should be writable");
4966    }
4967
4968    fn write_synthetic_hdf5_chunked_two_chunk_f32_deflate_fixture(
4969        file: &mut tempfile::NamedTempFile,
4970    ) {
4971        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_Two_Deflate";
4972        const HEADER_OFFSET: usize = 1024;
4973        const CHUNK_INDEX_OFFSET: usize = 4096;
4974        const PAYLOAD_A_OFFSET: usize = 8192;
4975        const ROWS: usize = 2;
4976        const COLS: usize = 4;
4977        const CHUNK_ROWS: usize = 2;
4978        const CHUNK_COLS: usize = 2;
4979
4980        let values_a = [0.5f32, 1.5, 4.5, 5.5];
4981        let values_b = [2.5f32, 3.5, 6.5, 7.5];
4982        let payload_a = encode_f32_values_as_zlib(&values_a);
4983        let payload_b = encode_f32_values_as_zlib(&values_b);
4984        let payload_b_offset = PAYLOAD_A_OFFSET + payload_a.len();
4985
4986        let total_len = payload_b_offset + payload_b.len();
4987        let mut bytes = vec![0u8; total_len];
4988        let marker = DATASET_PATH.as_bytes();
4989        bytes[64..64 + marker.len()].copy_from_slice(marker);
4990
4991        let mut cursor = HEADER_OFFSET;
4992        bytes[cursor] = 1;
4993        bytes[cursor + 1] = 0;
4994        bytes[cursor + 2..cursor + 4].copy_from_slice(&(4u16).to_le_bytes());
4995        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
4996        bytes[cursor + 8..cursor + 12].copy_from_slice(&(121u32).to_le_bytes());
4997        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
4998        cursor += 16;
4999
5000        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5001        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5002        bytes[cursor + 4] = 0;
5003        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5004        cursor += 8;
5005        bytes[cursor] = 2;
5006        bytes[cursor + 1] = 2;
5007        bytes[cursor + 2] = 0;
5008        bytes[cursor + 3] = 0;
5009        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5010        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5011        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5012        cursor += 24;
5013
5014        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5015        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5016        bytes[cursor + 4] = 0;
5017        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5018        cursor += 8;
5019        bytes[cursor] = 0x13;
5020        bytes[cursor + 1] = 0;
5021        bytes[cursor + 2] = 0;
5022        bytes[cursor + 3] = 0;
5023        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5024        cursor += 8;
5025
5026        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5027        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5028        bytes[cursor + 4] = 0;
5029        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5030        cursor += 8;
5031        bytes[cursor] = 3;
5032        bytes[cursor + 1] = 2;
5033        bytes[cursor + 2] = 2;
5034        bytes[cursor + 3..cursor + 11].copy_from_slice(&(CHUNK_INDEX_OFFSET as u64).to_le_bytes());
5035        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5036        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5037        cursor += 19;
5038
5039        bytes[cursor..cursor + 2].copy_from_slice(&(0x000bu16).to_le_bytes());
5040        bytes[cursor + 2..cursor + 4].copy_from_slice(&(16u16).to_le_bytes());
5041        bytes[cursor + 4] = 0;
5042        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5043        cursor += 8;
5044        bytes[cursor] = 1;
5045        bytes[cursor + 1] = 1;
5046        bytes[cursor + 2..cursor + 8].copy_from_slice(&[0, 0, 0, 0, 0, 0]);
5047        cursor += 8;
5048        bytes[cursor..cursor + 2].copy_from_slice(&(1u16).to_le_bytes());
5049        bytes[cursor + 2..cursor + 4].copy_from_slice(&(0u16).to_le_bytes());
5050        bytes[cursor + 4..cursor + 6].copy_from_slice(&(0u16).to_le_bytes());
5051        bytes[cursor + 6..cursor + 8].copy_from_slice(&(0u16).to_le_bytes());
5052
5053        let mut node_cursor = CHUNK_INDEX_OFFSET;
5054        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5055        bytes[node_cursor + 4] = 1;
5056        bytes[node_cursor + 5] = 0;
5057        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(2u16).to_le_bytes());
5058        node_cursor += 24;
5059
5060        bytes[node_cursor..node_cursor + 8]
5061            .copy_from_slice(&(payload_a.len() as u64).to_le_bytes());
5062        node_cursor += 8;
5063        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5064        node_cursor += 8;
5065        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5066        node_cursor += 8;
5067        bytes[node_cursor..node_cursor + 8]
5068            .copy_from_slice(&(PAYLOAD_A_OFFSET as u64).to_le_bytes());
5069        node_cursor += 8;
5070
5071        bytes[node_cursor..node_cursor + 8]
5072            .copy_from_slice(&(payload_b.len() as u64).to_le_bytes());
5073        node_cursor += 8;
5074        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5075        node_cursor += 8;
5076        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5077        node_cursor += 8;
5078        bytes[node_cursor..node_cursor + 8]
5079            .copy_from_slice(&(payload_b_offset as u64).to_le_bytes());
5080
5081        bytes[PAYLOAD_A_OFFSET..PAYLOAD_A_OFFSET + payload_a.len()].copy_from_slice(&payload_a);
5082        bytes[payload_b_offset..payload_b_offset + payload_b.len()].copy_from_slice(&payload_b);
5083
5084        file.write_all(&bytes)
5085            .expect("synthetic HDF5 deflate two-chunk fixture should be writable");
5086    }
5087
5088    fn write_synthetic_hdf5_chunked_two_leaf_f32_fixture(file: &mut tempfile::NamedTempFile) {
5089        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_Two_Leaf";
5090        const HEADER_OFFSET: usize = 1024;
5091        const FIRST_LEAF_OFFSET: usize = 4096;
5092        const SECOND_LEAF_OFFSET: usize = 4184;
5093        const PAYLOAD_A_OFFSET: usize = 8192;
5094        const PAYLOAD_B_OFFSET: usize = 8208;
5095        const ROWS: usize = 2;
5096        const COLS: usize = 4;
5097        const CHUNK_ROWS: usize = 2;
5098        const CHUNK_COLS: usize = 2;
5099
5100        let values_a = [0.5f32, 1.5, 4.5, 5.5];
5101        let values_b = [2.5f32, 3.5, 6.5, 7.5];
5102        let mut payload_a = Vec::<u8>::new();
5103        let mut payload_b = Vec::<u8>::new();
5104        for value in values_a {
5105            payload_a.extend_from_slice(&value.to_le_bytes());
5106        }
5107        for value in values_b {
5108            payload_b.extend_from_slice(&value.to_le_bytes());
5109        }
5110
5111        let total_len = PAYLOAD_B_OFFSET + payload_b.len();
5112        let mut bytes = vec![0u8; total_len];
5113        let marker = DATASET_PATH.as_bytes();
5114        bytes[64..64 + marker.len()].copy_from_slice(marker);
5115
5116        let mut cursor = HEADER_OFFSET;
5117        bytes[cursor] = 1;
5118        bytes[cursor + 1] = 0;
5119        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
5120        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5121        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
5122        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
5123        cursor += 16;
5124
5125        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5126        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5127        bytes[cursor + 4] = 0;
5128        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5129        cursor += 8;
5130        bytes[cursor] = 2;
5131        bytes[cursor + 1] = 2;
5132        bytes[cursor + 2] = 0;
5133        bytes[cursor + 3] = 0;
5134        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5135        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5136        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5137        cursor += 24;
5138
5139        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5140        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5141        bytes[cursor + 4] = 0;
5142        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5143        cursor += 8;
5144        bytes[cursor] = 0x13;
5145        bytes[cursor + 1] = 0;
5146        bytes[cursor + 2] = 0;
5147        bytes[cursor + 3] = 0;
5148        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5149        cursor += 8;
5150
5151        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5152        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5153        bytes[cursor + 4] = 0;
5154        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5155        cursor += 8;
5156        bytes[cursor] = 3;
5157        bytes[cursor + 1] = 2;
5158        bytes[cursor + 2] = 2;
5159        bytes[cursor + 3..cursor + 11].copy_from_slice(&(FIRST_LEAF_OFFSET as u64).to_le_bytes());
5160        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5161        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5162
5163        let mut node_cursor = FIRST_LEAF_OFFSET;
5164        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5165        bytes[node_cursor + 4] = 1;
5166        bytes[node_cursor + 5] = 0;
5167        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5168        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5169        bytes[node_cursor + 16..node_cursor + 24]
5170            .copy_from_slice(&(SECOND_LEAF_OFFSET as u64).to_le_bytes());
5171        node_cursor += 24;
5172        bytes[node_cursor..node_cursor + 8]
5173            .copy_from_slice(&(payload_a.len() as u64).to_le_bytes());
5174        node_cursor += 8;
5175        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5176        node_cursor += 8;
5177        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5178        node_cursor += 8;
5179        bytes[node_cursor..node_cursor + 8]
5180            .copy_from_slice(&(PAYLOAD_A_OFFSET as u64).to_le_bytes());
5181
5182        let mut node_cursor = SECOND_LEAF_OFFSET;
5183        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5184        bytes[node_cursor + 4] = 1;
5185        bytes[node_cursor + 5] = 0;
5186        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5187        bytes[node_cursor + 8..node_cursor + 16]
5188            .copy_from_slice(&(FIRST_LEAF_OFFSET as u64).to_le_bytes());
5189        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5190        node_cursor += 24;
5191        bytes[node_cursor..node_cursor + 8]
5192            .copy_from_slice(&(payload_b.len() as u64).to_le_bytes());
5193        node_cursor += 8;
5194        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5195        node_cursor += 8;
5196        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5197        node_cursor += 8;
5198        bytes[node_cursor..node_cursor + 8]
5199            .copy_from_slice(&(PAYLOAD_B_OFFSET as u64).to_le_bytes());
5200
5201        bytes[PAYLOAD_A_OFFSET..PAYLOAD_A_OFFSET + payload_a.len()].copy_from_slice(&payload_a);
5202        bytes[PAYLOAD_B_OFFSET..PAYLOAD_B_OFFSET + payload_b.len()].copy_from_slice(&payload_b);
5203
5204        file.write_all(&bytes)
5205            .expect("synthetic HDF5 two-leaf fixture should be writable");
5206    }
5207
5208    fn write_synthetic_hdf5_chunked_internal_root_fixture(file: &mut tempfile::NamedTempFile) {
5209        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_InternalRoot";
5210        const HEADER_OFFSET: usize = 1024;
5211        const INTERNAL_ROOT_OFFSET: usize = 4096;
5212        const FIRST_LEAF_OFFSET: usize = 4168;
5213        const SECOND_LEAF_OFFSET: usize = 4256;
5214        const PAYLOAD_A_OFFSET: usize = 8192;
5215        const PAYLOAD_B_OFFSET: usize = 8208;
5216        const ROWS: usize = 2;
5217        const COLS: usize = 4;
5218        const CHUNK_ROWS: usize = 2;
5219        const CHUNK_COLS: usize = 2;
5220
5221        let values_a = [0.5f32, 1.5, 4.5, 5.5];
5222        let values_b = [2.5f32, 3.5, 6.5, 7.5];
5223        let mut payload_a = Vec::<u8>::new();
5224        let mut payload_b = Vec::<u8>::new();
5225        for value in values_a {
5226            payload_a.extend_from_slice(&value.to_le_bytes());
5227        }
5228        for value in values_b {
5229            payload_b.extend_from_slice(&value.to_le_bytes());
5230        }
5231
5232        let total_len = PAYLOAD_B_OFFSET + payload_b.len();
5233        let mut bytes = vec![0u8; total_len];
5234        let marker = DATASET_PATH.as_bytes();
5235        bytes[64..64 + marker.len()].copy_from_slice(marker);
5236
5237        let mut cursor = HEADER_OFFSET;
5238        bytes[cursor] = 1;
5239        bytes[cursor + 1] = 0;
5240        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
5241        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5242        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
5243        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
5244        cursor += 16;
5245
5246        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5247        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5248        bytes[cursor + 4] = 0;
5249        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5250        cursor += 8;
5251        bytes[cursor] = 2;
5252        bytes[cursor + 1] = 2;
5253        bytes[cursor + 2] = 0;
5254        bytes[cursor + 3] = 0;
5255        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5256        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5257        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5258        cursor += 24;
5259
5260        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5261        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5262        bytes[cursor + 4] = 0;
5263        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5264        cursor += 8;
5265        bytes[cursor] = 0x13;
5266        bytes[cursor + 1] = 0;
5267        bytes[cursor + 2] = 0;
5268        bytes[cursor + 3] = 0;
5269        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5270        cursor += 8;
5271
5272        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5273        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5274        bytes[cursor + 4] = 0;
5275        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5276        cursor += 8;
5277        bytes[cursor] = 3;
5278        bytes[cursor + 1] = 2;
5279        bytes[cursor + 2] = 2;
5280        bytes[cursor + 3..cursor + 11]
5281            .copy_from_slice(&(INTERNAL_ROOT_OFFSET as u64).to_le_bytes());
5282        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5283        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5284
5285        bytes[INTERNAL_ROOT_OFFSET..INTERNAL_ROOT_OFFSET + 4].copy_from_slice(b"TREE");
5286        bytes[INTERNAL_ROOT_OFFSET + 4] = 1;
5287        bytes[INTERNAL_ROOT_OFFSET + 5] = 1;
5288        bytes[INTERNAL_ROOT_OFFSET + 6..INTERNAL_ROOT_OFFSET + 8]
5289            .copy_from_slice(&(2u16).to_le_bytes());
5290        bytes[INTERNAL_ROOT_OFFSET + 8..INTERNAL_ROOT_OFFSET + 16]
5291            .copy_from_slice(&u64::MAX.to_le_bytes());
5292        bytes[INTERNAL_ROOT_OFFSET + 16..INTERNAL_ROOT_OFFSET + 24]
5293            .copy_from_slice(&u64::MAX.to_le_bytes());
5294
5295        let mut node_cursor = INTERNAL_ROOT_OFFSET + 24;
5296        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5297        node_cursor += 8;
5298        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5299        node_cursor += 8;
5300        bytes[node_cursor..node_cursor + 8]
5301            .copy_from_slice(&(FIRST_LEAF_OFFSET as u64).to_le_bytes());
5302        node_cursor += 8;
5303        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5304        node_cursor += 8;
5305        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(4u64).to_le_bytes());
5306        node_cursor += 8;
5307        bytes[node_cursor..node_cursor + 8]
5308            .copy_from_slice(&(SECOND_LEAF_OFFSET as u64).to_le_bytes());
5309
5310        let mut node_cursor = FIRST_LEAF_OFFSET;
5311        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5312        bytes[node_cursor + 4] = 1;
5313        bytes[node_cursor + 5] = 0;
5314        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5315        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5316        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5317        node_cursor += 24;
5318        bytes[node_cursor..node_cursor + 8]
5319            .copy_from_slice(&(payload_a.len() as u64).to_le_bytes());
5320        node_cursor += 8;
5321        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5322        node_cursor += 8;
5323        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5324        node_cursor += 8;
5325        bytes[node_cursor..node_cursor + 8]
5326            .copy_from_slice(&(PAYLOAD_A_OFFSET as u64).to_le_bytes());
5327
5328        let mut node_cursor = SECOND_LEAF_OFFSET;
5329        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5330        bytes[node_cursor + 4] = 1;
5331        bytes[node_cursor + 5] = 0;
5332        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5333        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5334        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5335        node_cursor += 24;
5336        bytes[node_cursor..node_cursor + 8]
5337            .copy_from_slice(&(payload_b.len() as u64).to_le_bytes());
5338        node_cursor += 8;
5339        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5340        node_cursor += 8;
5341        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5342        node_cursor += 8;
5343        bytes[node_cursor..node_cursor + 8]
5344            .copy_from_slice(&(PAYLOAD_B_OFFSET as u64).to_le_bytes());
5345
5346        bytes[PAYLOAD_A_OFFSET..PAYLOAD_A_OFFSET + payload_a.len()].copy_from_slice(&payload_a);
5347        bytes[PAYLOAD_B_OFFSET..PAYLOAD_B_OFFSET + payload_b.len()].copy_from_slice(&payload_b);
5348
5349        file.write_all(&bytes)
5350            .expect("synthetic HDF5 internal-root fixture should be writable");
5351    }
5352
5353    fn write_synthetic_hdf5_chunked_multilevel_root_fixture(file: &mut tempfile::NamedTempFile) {
5354        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_MultiLevelRoot";
5355        const HEADER_OFFSET: usize = 1024;
5356        const ROOT_OFFSET: usize = 4096;
5357        const FIRST_INTERNAL_OFFSET: usize = 4168;
5358        const SECOND_INTERNAL_OFFSET: usize = 4256;
5359        const FIRST_LEAF_OFFSET: usize = 4344;
5360        const SECOND_LEAF_OFFSET: usize = 4432;
5361        const THIRD_LEAF_OFFSET: usize = 4520;
5362        const PAYLOAD_A_OFFSET: usize = 8192;
5363        const PAYLOAD_B_OFFSET: usize = 8208;
5364        const PAYLOAD_C_OFFSET: usize = 8224;
5365        const ROWS: usize = 2;
5366        const COLS: usize = 6;
5367        const CHUNK_ROWS: usize = 2;
5368        const CHUNK_COLS: usize = 2;
5369
5370        let values_a = [0.5f32, 1.5, 4.5, 5.5];
5371        let values_b = [2.5f32, 3.5, 6.5, 7.5];
5372        let values_c = [8.5f32, 9.5, 10.5, 11.5];
5373        let mut payload_a = Vec::<u8>::new();
5374        let mut payload_b = Vec::<u8>::new();
5375        let mut payload_c = Vec::<u8>::new();
5376        for value in values_a {
5377            payload_a.extend_from_slice(&value.to_le_bytes());
5378        }
5379        for value in values_b {
5380            payload_b.extend_from_slice(&value.to_le_bytes());
5381        }
5382        for value in values_c {
5383            payload_c.extend_from_slice(&value.to_le_bytes());
5384        }
5385
5386        let total_len = PAYLOAD_C_OFFSET + payload_c.len();
5387        let mut bytes = vec![0u8; total_len];
5388        let marker = DATASET_PATH.as_bytes();
5389        bytes[64..64 + marker.len()].copy_from_slice(marker);
5390
5391        let mut cursor = HEADER_OFFSET;
5392        bytes[cursor] = 1;
5393        bytes[cursor + 1] = 0;
5394        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
5395        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5396        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
5397        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
5398        cursor += 16;
5399
5400        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5401        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5402        bytes[cursor + 4] = 0;
5403        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5404        cursor += 8;
5405        bytes[cursor] = 2;
5406        bytes[cursor + 1] = 2;
5407        bytes[cursor + 2] = 0;
5408        bytes[cursor + 3] = 0;
5409        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5410        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5411        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5412        cursor += 24;
5413
5414        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5415        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5416        bytes[cursor + 4] = 0;
5417        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5418        cursor += 8;
5419        bytes[cursor] = 0x13;
5420        bytes[cursor + 1] = 0;
5421        bytes[cursor + 2] = 0;
5422        bytes[cursor + 3] = 0;
5423        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5424        cursor += 8;
5425
5426        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5427        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5428        bytes[cursor + 4] = 0;
5429        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5430        cursor += 8;
5431        bytes[cursor] = 3;
5432        bytes[cursor + 1] = 2;
5433        bytes[cursor + 2] = 2;
5434        bytes[cursor + 3..cursor + 11].copy_from_slice(&(ROOT_OFFSET as u64).to_le_bytes());
5435        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5436        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5437
5438        bytes[ROOT_OFFSET..ROOT_OFFSET + 4].copy_from_slice(b"TREE");
5439        bytes[ROOT_OFFSET + 4] = 1;
5440        bytes[ROOT_OFFSET + 5] = 2;
5441        bytes[ROOT_OFFSET + 6..ROOT_OFFSET + 8].copy_from_slice(&(2u16).to_le_bytes());
5442        bytes[ROOT_OFFSET + 8..ROOT_OFFSET + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5443        bytes[ROOT_OFFSET + 16..ROOT_OFFSET + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5444
5445        let mut node_cursor = ROOT_OFFSET + 24;
5446        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5447        node_cursor += 8;
5448        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5449        node_cursor += 8;
5450        bytes[node_cursor..node_cursor + 8]
5451            .copy_from_slice(&(FIRST_INTERNAL_OFFSET as u64).to_le_bytes());
5452        node_cursor += 8;
5453        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5454        node_cursor += 8;
5455        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(6u64).to_le_bytes());
5456        node_cursor += 8;
5457        bytes[node_cursor..node_cursor + 8]
5458            .copy_from_slice(&(SECOND_INTERNAL_OFFSET as u64).to_le_bytes());
5459
5460        bytes[FIRST_INTERNAL_OFFSET..FIRST_INTERNAL_OFFSET + 4].copy_from_slice(b"TREE");
5461        bytes[FIRST_INTERNAL_OFFSET + 4] = 1;
5462        bytes[FIRST_INTERNAL_OFFSET + 5] = 1;
5463        bytes[FIRST_INTERNAL_OFFSET + 6..FIRST_INTERNAL_OFFSET + 8]
5464            .copy_from_slice(&(2u16).to_le_bytes());
5465        bytes[FIRST_INTERNAL_OFFSET + 8..FIRST_INTERNAL_OFFSET + 16]
5466            .copy_from_slice(&u64::MAX.to_le_bytes());
5467        bytes[FIRST_INTERNAL_OFFSET + 16..FIRST_INTERNAL_OFFSET + 24]
5468            .copy_from_slice(&u64::MAX.to_le_bytes());
5469        let mut node_cursor = FIRST_INTERNAL_OFFSET + 24;
5470        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5471        node_cursor += 8;
5472        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5473        node_cursor += 8;
5474        bytes[node_cursor..node_cursor + 8]
5475            .copy_from_slice(&(FIRST_LEAF_OFFSET as u64).to_le_bytes());
5476        node_cursor += 8;
5477        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5478        node_cursor += 8;
5479        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(4u64).to_le_bytes());
5480        node_cursor += 8;
5481        bytes[node_cursor..node_cursor + 8]
5482            .copy_from_slice(&(SECOND_LEAF_OFFSET as u64).to_le_bytes());
5483
5484        bytes[SECOND_INTERNAL_OFFSET..SECOND_INTERNAL_OFFSET + 4].copy_from_slice(b"TREE");
5485        bytes[SECOND_INTERNAL_OFFSET + 4] = 1;
5486        bytes[SECOND_INTERNAL_OFFSET + 5] = 1;
5487        bytes[SECOND_INTERNAL_OFFSET + 6..SECOND_INTERNAL_OFFSET + 8]
5488            .copy_from_slice(&(1u16).to_le_bytes());
5489        bytes[SECOND_INTERNAL_OFFSET + 8..SECOND_INTERNAL_OFFSET + 16]
5490            .copy_from_slice(&u64::MAX.to_le_bytes());
5491        bytes[SECOND_INTERNAL_OFFSET + 16..SECOND_INTERNAL_OFFSET + 24]
5492            .copy_from_slice(&u64::MAX.to_le_bytes());
5493        let mut node_cursor = SECOND_INTERNAL_OFFSET + 24;
5494        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5495        node_cursor += 8;
5496        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(6u64).to_le_bytes());
5497        node_cursor += 8;
5498        bytes[node_cursor..node_cursor + 8]
5499            .copy_from_slice(&(THIRD_LEAF_OFFSET as u64).to_le_bytes());
5500
5501        let mut node_cursor = FIRST_LEAF_OFFSET;
5502        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5503        bytes[node_cursor + 4] = 1;
5504        bytes[node_cursor + 5] = 0;
5505        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5506        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5507        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5508        node_cursor += 24;
5509        bytes[node_cursor..node_cursor + 8]
5510            .copy_from_slice(&(payload_a.len() as u64).to_le_bytes());
5511        node_cursor += 8;
5512        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5513        node_cursor += 8;
5514        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5515        node_cursor += 8;
5516        bytes[node_cursor..node_cursor + 8]
5517            .copy_from_slice(&(PAYLOAD_A_OFFSET as u64).to_le_bytes());
5518
5519        let mut node_cursor = SECOND_LEAF_OFFSET;
5520        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5521        bytes[node_cursor + 4] = 1;
5522        bytes[node_cursor + 5] = 0;
5523        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5524        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5525        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5526        node_cursor += 24;
5527        bytes[node_cursor..node_cursor + 8]
5528            .copy_from_slice(&(payload_b.len() as u64).to_le_bytes());
5529        node_cursor += 8;
5530        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5531        node_cursor += 8;
5532        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5533        node_cursor += 8;
5534        bytes[node_cursor..node_cursor + 8]
5535            .copy_from_slice(&(PAYLOAD_B_OFFSET as u64).to_le_bytes());
5536
5537        let mut node_cursor = THIRD_LEAF_OFFSET;
5538        bytes[node_cursor..node_cursor + 4].copy_from_slice(b"TREE");
5539        bytes[node_cursor + 4] = 1;
5540        bytes[node_cursor + 5] = 0;
5541        bytes[node_cursor + 6..node_cursor + 8].copy_from_slice(&(1u16).to_le_bytes());
5542        bytes[node_cursor + 8..node_cursor + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5543        bytes[node_cursor + 16..node_cursor + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5544        node_cursor += 24;
5545        bytes[node_cursor..node_cursor + 8]
5546            .copy_from_slice(&(payload_c.len() as u64).to_le_bytes());
5547        node_cursor += 8;
5548        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5549        node_cursor += 8;
5550        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(4u64).to_le_bytes());
5551        node_cursor += 8;
5552        bytes[node_cursor..node_cursor + 8]
5553            .copy_from_slice(&(PAYLOAD_C_OFFSET as u64).to_le_bytes());
5554
5555        bytes[PAYLOAD_A_OFFSET..PAYLOAD_A_OFFSET + payload_a.len()].copy_from_slice(&payload_a);
5556        bytes[PAYLOAD_B_OFFSET..PAYLOAD_B_OFFSET + payload_b.len()].copy_from_slice(&payload_b);
5557        bytes[PAYLOAD_C_OFFSET..PAYLOAD_C_OFFSET + payload_c.len()].copy_from_slice(&payload_c);
5558
5559        file.write_all(&bytes)
5560            .expect("synthetic HDF5 multilevel-root fixture should be writable");
5561    }
5562
5563    fn write_synthetic_hdf5_chunked_malformed_multilevel_root_fixture(
5564        file: &mut tempfile::NamedTempFile,
5565    ) {
5566        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_MalformedMultiLevel";
5567        const HEADER_OFFSET: usize = 1024;
5568        const ROOT_OFFSET: usize = 4096;
5569        const ROWS: usize = 2;
5570        const COLS: usize = 4;
5571        const CHUNK_ROWS: usize = 2;
5572        const CHUNK_COLS: usize = 2;
5573
5574        let total_len = ROOT_OFFSET + 64;
5575        let mut bytes = vec![0u8; total_len];
5576        let marker = DATASET_PATH.as_bytes();
5577        bytes[64..64 + marker.len()].copy_from_slice(marker);
5578
5579        let mut cursor = HEADER_OFFSET;
5580        bytes[cursor] = 1;
5581        bytes[cursor + 1] = 0;
5582        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
5583        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5584        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
5585        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
5586        cursor += 16;
5587
5588        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5589        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5590        bytes[cursor + 4] = 0;
5591        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5592        cursor += 8;
5593        bytes[cursor] = 2;
5594        bytes[cursor + 1] = 2;
5595        bytes[cursor + 2] = 0;
5596        bytes[cursor + 3] = 0;
5597        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5598        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5599        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5600        cursor += 24;
5601
5602        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5603        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5604        bytes[cursor + 4] = 0;
5605        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5606        cursor += 8;
5607        bytes[cursor] = 0x13;
5608        bytes[cursor + 1] = 0;
5609        bytes[cursor + 2] = 0;
5610        bytes[cursor + 3] = 0;
5611        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5612        cursor += 8;
5613
5614        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5615        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5616        bytes[cursor + 4] = 0;
5617        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5618        cursor += 8;
5619        bytes[cursor] = 3;
5620        bytes[cursor + 1] = 2;
5621        bytes[cursor + 2] = 2;
5622        bytes[cursor + 3..cursor + 11].copy_from_slice(&(ROOT_OFFSET as u64).to_le_bytes());
5623        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5624        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5625
5626        bytes[ROOT_OFFSET..ROOT_OFFSET + 4].copy_from_slice(b"TREE");
5627        bytes[ROOT_OFFSET + 4] = 1;
5628        bytes[ROOT_OFFSET + 5] = 2;
5629        bytes[ROOT_OFFSET + 6..ROOT_OFFSET + 8].copy_from_slice(&(1u16).to_le_bytes());
5630        bytes[ROOT_OFFSET + 8..ROOT_OFFSET + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5631        bytes[ROOT_OFFSET + 16..ROOT_OFFSET + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5632
5633        file.write_all(&bytes)
5634            .expect("synthetic malformed multilevel-root fixture should be writable");
5635    }
5636
5637    fn write_synthetic_hdf5_chunked_malformed_multilevel_fanout_fixture(
5638        file: &mut tempfile::NamedTempFile,
5639    ) {
5640        const DATASET_PATH: &str = "/ScienceData/NDVI_Chunked_MalformedMultiLevelFanout";
5641        const HEADER_OFFSET: usize = 1024;
5642        const ROOT_OFFSET: usize = 4096;
5643        const FIRST_INTERNAL_OFFSET: usize = 4168;
5644        const SECOND_INTERNAL_OFFSET: usize = 4256;
5645        const FIRST_LEAF_OFFSET: usize = 4344;
5646        const SECOND_LEAF_OFFSET: usize = 4432;
5647        const ROWS: usize = 2;
5648        const COLS: usize = 6;
5649        const CHUNK_ROWS: usize = 2;
5650        const CHUNK_COLS: usize = 2;
5651
5652        let total_len = SECOND_LEAF_OFFSET + 64;
5653        let mut bytes = vec![0u8; total_len];
5654        let marker = DATASET_PATH.as_bytes();
5655        bytes[64..64 + marker.len()].copy_from_slice(marker);
5656
5657        let mut cursor = HEADER_OFFSET;
5658        bytes[cursor] = 1;
5659        bytes[cursor + 1] = 0;
5660        bytes[cursor + 2..cursor + 4].copy_from_slice(&(3u16).to_le_bytes());
5661        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5662        bytes[cursor + 8..cursor + 12].copy_from_slice(&(95u32).to_le_bytes());
5663        bytes[cursor + 12..cursor + 16].copy_from_slice(&0u32.to_le_bytes());
5664        cursor += 16;
5665
5666        bytes[cursor..cursor + 2].copy_from_slice(&(0x0001u16).to_le_bytes());
5667        bytes[cursor + 2..cursor + 4].copy_from_slice(&(24u16).to_le_bytes());
5668        bytes[cursor + 4] = 0;
5669        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5670        cursor += 8;
5671        bytes[cursor] = 2;
5672        bytes[cursor + 1] = 2;
5673        bytes[cursor + 2] = 0;
5674        bytes[cursor + 3] = 0;
5675        bytes[cursor + 4..cursor + 8].copy_from_slice(&0u32.to_le_bytes());
5676        bytes[cursor + 8..cursor + 16].copy_from_slice(&(ROWS as u64).to_le_bytes());
5677        bytes[cursor + 16..cursor + 24].copy_from_slice(&(COLS as u64).to_le_bytes());
5678        cursor += 24;
5679
5680        bytes[cursor..cursor + 2].copy_from_slice(&(0x0003u16).to_le_bytes());
5681        bytes[cursor + 2..cursor + 4].copy_from_slice(&(8u16).to_le_bytes());
5682        bytes[cursor + 4] = 0;
5683        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5684        cursor += 8;
5685        bytes[cursor] = 0x13;
5686        bytes[cursor + 1] = 0;
5687        bytes[cursor + 2] = 0;
5688        bytes[cursor + 3] = 0;
5689        bytes[cursor + 4..cursor + 8].copy_from_slice(&(4u32).to_le_bytes());
5690        cursor += 8;
5691
5692        bytes[cursor..cursor + 2].copy_from_slice(&(0x0008u16).to_le_bytes());
5693        bytes[cursor + 2..cursor + 4].copy_from_slice(&(19u16).to_le_bytes());
5694        bytes[cursor + 4] = 0;
5695        bytes[cursor + 5..cursor + 8].copy_from_slice(&[0, 0, 0]);
5696        cursor += 8;
5697        bytes[cursor] = 3;
5698        bytes[cursor + 1] = 2;
5699        bytes[cursor + 2] = 2;
5700        bytes[cursor + 3..cursor + 11].copy_from_slice(&(ROOT_OFFSET as u64).to_le_bytes());
5701        bytes[cursor + 11..cursor + 15].copy_from_slice(&(CHUNK_ROWS as u32).to_le_bytes());
5702        bytes[cursor + 15..cursor + 19].copy_from_slice(&(CHUNK_COLS as u32).to_le_bytes());
5703
5704        bytes[ROOT_OFFSET..ROOT_OFFSET + 4].copy_from_slice(b"TREE");
5705        bytes[ROOT_OFFSET + 4] = 1;
5706        bytes[ROOT_OFFSET + 5] = 2;
5707        bytes[ROOT_OFFSET + 6..ROOT_OFFSET + 8].copy_from_slice(&(2u16).to_le_bytes());
5708        bytes[ROOT_OFFSET + 8..ROOT_OFFSET + 16].copy_from_slice(&u64::MAX.to_le_bytes());
5709        bytes[ROOT_OFFSET + 16..ROOT_OFFSET + 24].copy_from_slice(&u64::MAX.to_le_bytes());
5710        let mut node_cursor = ROOT_OFFSET + 24;
5711        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5712        node_cursor += 8;
5713        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5714        node_cursor += 8;
5715        bytes[node_cursor..node_cursor + 8]
5716            .copy_from_slice(&(FIRST_INTERNAL_OFFSET as u64).to_le_bytes());
5717        node_cursor += 8;
5718        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5719        node_cursor += 8;
5720        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(6u64).to_le_bytes());
5721        node_cursor += 8;
5722        bytes[node_cursor..node_cursor + 8]
5723            .copy_from_slice(&(SECOND_INTERNAL_OFFSET as u64).to_le_bytes());
5724
5725        bytes[FIRST_INTERNAL_OFFSET..FIRST_INTERNAL_OFFSET + 4].copy_from_slice(b"TREE");
5726        bytes[FIRST_INTERNAL_OFFSET + 4] = 1;
5727        bytes[FIRST_INTERNAL_OFFSET + 5] = 1;
5728        bytes[FIRST_INTERNAL_OFFSET + 6..FIRST_INTERNAL_OFFSET + 8]
5729            .copy_from_slice(&(2u16).to_le_bytes());
5730        bytes[FIRST_INTERNAL_OFFSET + 8..FIRST_INTERNAL_OFFSET + 16]
5731            .copy_from_slice(&u64::MAX.to_le_bytes());
5732        bytes[FIRST_INTERNAL_OFFSET + 16..FIRST_INTERNAL_OFFSET + 24]
5733            .copy_from_slice(&u64::MAX.to_le_bytes());
5734        let mut node_cursor = FIRST_INTERNAL_OFFSET + 24;
5735        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5736        node_cursor += 8;
5737        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5738        node_cursor += 8;
5739        bytes[node_cursor..node_cursor + 8]
5740            .copy_from_slice(&(FIRST_LEAF_OFFSET as u64).to_le_bytes());
5741        node_cursor += 8;
5742        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5743        node_cursor += 8;
5744        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(4u64).to_le_bytes());
5745        node_cursor += 8;
5746        bytes[node_cursor..node_cursor + 8]
5747            .copy_from_slice(&(SECOND_LEAF_OFFSET as u64).to_le_bytes());
5748
5749        bytes[FIRST_LEAF_OFFSET..FIRST_LEAF_OFFSET + 4].copy_from_slice(b"TREE");
5750        bytes[FIRST_LEAF_OFFSET + 4] = 1;
5751        bytes[FIRST_LEAF_OFFSET + 5] = 0;
5752        bytes[FIRST_LEAF_OFFSET + 6..FIRST_LEAF_OFFSET + 8].copy_from_slice(&(1u16).to_le_bytes());
5753        bytes[FIRST_LEAF_OFFSET + 8..FIRST_LEAF_OFFSET + 16]
5754            .copy_from_slice(&u64::MAX.to_le_bytes());
5755        bytes[FIRST_LEAF_OFFSET + 16..FIRST_LEAF_OFFSET + 24]
5756            .copy_from_slice(&u64::MAX.to_le_bytes());
5757        node_cursor = FIRST_LEAF_OFFSET + 24;
5758        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(16u64).to_le_bytes());
5759        node_cursor += 8;
5760        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5761        node_cursor += 8;
5762        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5763        node_cursor += 8;
5764        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(8192u64).to_le_bytes());
5765
5766        bytes[SECOND_LEAF_OFFSET..SECOND_LEAF_OFFSET + 4].copy_from_slice(b"TREE");
5767        bytes[SECOND_LEAF_OFFSET + 4] = 1;
5768        bytes[SECOND_LEAF_OFFSET + 5] = 0;
5769        bytes[SECOND_LEAF_OFFSET + 6..SECOND_LEAF_OFFSET + 8]
5770            .copy_from_slice(&(1u16).to_le_bytes());
5771        bytes[SECOND_LEAF_OFFSET + 8..SECOND_LEAF_OFFSET + 16]
5772            .copy_from_slice(&u64::MAX.to_le_bytes());
5773        bytes[SECOND_LEAF_OFFSET + 16..SECOND_LEAF_OFFSET + 24]
5774            .copy_from_slice(&u64::MAX.to_le_bytes());
5775        node_cursor = SECOND_LEAF_OFFSET + 24;
5776        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(16u64).to_le_bytes());
5777        node_cursor += 8;
5778        bytes[node_cursor..node_cursor + 8].copy_from_slice(&0u64.to_le_bytes());
5779        node_cursor += 8;
5780        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(2u64).to_le_bytes());
5781        node_cursor += 8;
5782        bytes[node_cursor..node_cursor + 8].copy_from_slice(&(8208u64).to_le_bytes());
5783
5784        file.write_all(&bytes)
5785            .expect("synthetic malformed multilevel-fanout fixture should be writable");
5786    }
5787
5788    fn encode_f32_values_as_zlib(values: &[f32]) -> Vec<u8> {
5789        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
5790        for value in values {
5791            encoder
5792                .write_all(&value.to_le_bytes())
5793                .expect("zlib encoder should accept f32 bytes");
5794        }
5795        encoder
5796            .finish()
5797            .expect("zlib encoder should finish synthetic payload")
5798    }
5799
5800    #[test]
5801    fn raster_read_hdf4_dataset_uri_reads_supported_layout() {
5802        let mut tmp = tempfile::Builder::new()
5803            .suffix(".hdf")
5804            .tempfile()
5805            .expect("temp file should be created");
5806        write_synthetic_hdf4_i16_fixture(&mut tmp);
5807
5808        let uri = format!("{}:///GridA/FieldA", tmp.path().to_string_lossy());
5809        let raster = Raster::read(&uri).expect("HDF4 dataset URI should decode to raster");
5810
5811        assert_eq!(raster.rows, 2);
5812        assert_eq!(raster.cols, 4);
5813        assert_eq!(raster.bands, 1);
5814        assert_eq!(raster.data_type, DataType::I16);
5815        assert_eq!(raster.get(0, 0, 0), 1.0);
5816        assert_eq!(raster.get(0, 1, 3), 8.0);
5817    }
5818
5819    #[test]
5820    fn raster_read_hdf4_dataset_uri_reads_supported_layout_with_canonical_selector() {
5821        let mut tmp = tempfile::Builder::new()
5822            .suffix(".hdf")
5823            .tempfile()
5824            .expect("temp file should be created");
5825        write_synthetic_hdf4_i16_fixture(&mut tmp);
5826
5827        let uri = format!("{}#dataset=/GridA/FieldA", tmp.path().to_string_lossy());
5828        let raster =
5829            Raster::read(&uri).expect("canonical HDF4 dataset URI should decode to raster");
5830
5831        assert_eq!(raster.rows, 2);
5832        assert_eq!(raster.cols, 4);
5833        assert_eq!(raster.get(0, 0, 0), 1.0);
5834        assert_eq!(raster.get(0, 1, 3), 8.0);
5835    }
5836
5837    #[test]
5838    fn raster_read_hdf_dataset_uri_reports_missing_dataset_path() {
5839        let mut tmp = tempfile::Builder::new()
5840            .suffix(".hdf")
5841            .tempfile()
5842            .expect("temp file should be created");
5843        write_synthetic_hdf4_i16_fixture(&mut tmp);
5844
5845        let uri = format!("{}:///GridA/DoesNotExist", tmp.path().to_string_lossy());
5846        let err = Raster::read(&uri).expect_err("missing dataset path should fail");
5847        assert!(err.to_string().contains("dataset path resolution failed"));
5848    }
5849
5850    #[test]
5851    fn raster_read_hdf5_dataset_uri_reads_supported_gedi_path() {
5852        let mut tmp = tempfile::Builder::new()
5853            .suffix(".h5")
5854            .tempfile()
5855            .expect("temp file should be created");
5856        write_synthetic_hdf5_gedi_contiguous_fixture(&mut tmp);
5857
5858        let uri = format!(
5859            "{}:///BEAM0000/elev_lowestmode",
5860            tmp.path().to_string_lossy()
5861        );
5862        let raster = Raster::read(&uri).expect("HDF5 GEDI URI should materialize to raster");
5863
5864        assert_eq!(raster.rows, 1);
5865        assert_eq!(raster.cols, 89_634);
5866        assert_eq!(raster.bands, 1);
5867        assert_eq!(raster.data_type, DataType::F32);
5868        assert_eq!(raster.get(0, 0, 0), 0.0);
5869        assert_eq!(raster.get(0, 0, 12_345), 12_345.0);
5870        assert_eq!(raster.get(0, 0, 89_633), 89_633.0);
5871    }
5872
5873    #[test]
5874    fn raster_read_hdf5_dataset_uri_reads_supported_gedi_path_with_canonical_selector() {
5875        let mut tmp = tempfile::Builder::new()
5876            .suffix(".h5")
5877            .tempfile()
5878            .expect("temp file should be created");
5879        write_synthetic_hdf5_gedi_contiguous_fixture(&mut tmp);
5880
5881        let uri = format!(
5882            "{}#dataset=/BEAM0000/elev_lowestmode",
5883            tmp.path().to_string_lossy()
5884        );
5885        let raster =
5886            Raster::read(&uri).expect("canonical HDF5 GEDI URI should materialize to raster");
5887
5888        assert_eq!(raster.rows, 1);
5889        assert_eq!(raster.cols, 89_634);
5890        assert_eq!(raster.get(0, 0, 0), 0.0);
5891        assert_eq!(raster.get(0, 0, 12_345), 12_345.0);
5892    }
5893
5894    #[test]
5895    fn raster_read_hdf5_dataset_uri_reads_supported_viirs_xdim_path() {
5896        let mut tmp = tempfile::Builder::new()
5897            .suffix(".h5")
5898            .tempfile()
5899            .expect("temp file should be created");
5900        write_synthetic_hdf5_viirs_xdim_fixture(&mut tmp);
5901
5902        let uri = format!(
5903            "{}:///HDFEOS/GRIDS/VIIRS_Grid_8Day_VI_500m/XDim",
5904            tmp.path().to_string_lossy()
5905        );
5906        let raster = Raster::read(&uri).expect("HDF5 VIIRS URI should materialize to raster");
5907
5908        assert_eq!(raster.rows, 1);
5909        assert_eq!(raster.cols, 2_400);
5910        assert_eq!(raster.bands, 1);
5911        assert_eq!(raster.data_type, DataType::F64);
5912        assert_eq!(raster.get(0, 0, 0), 0.5);
5913        assert_eq!(raster.get(0, 0, 1_234), 1_234.5);
5914        assert_eq!(raster.get(0, 0, 2_399), 2_399.5);
5915    }
5916
5917    #[test]
5918    fn raster_read_hdf5_dataset_uri_reads_generic_contiguous_f32_path() {
5919        let mut tmp = tempfile::Builder::new()
5920            .suffix(".h5")
5921            .tempfile()
5922            .expect("temp file should be created");
5923        write_synthetic_hdf5_generic_contiguous_f32_fixture(&mut tmp);
5924
5925        let uri = format!("{}#dataset=/ScienceData/NDVI", tmp.path().to_string_lossy());
5926        let raster = Raster::read(&uri).expect("generic contiguous HDF5 URI should materialize");
5927
5928        assert_eq!(raster.rows, 1);
5929        assert_eq!(raster.cols, 12);
5930        assert_eq!(raster.bands, 1);
5931        assert_eq!(raster.data_type, DataType::F32);
5932        assert_eq!(raster.get(0, 0, 0), 0.0);
5933        assert_eq!(raster.get(0, 0, 4), 1.0);
5934        assert_eq!(raster.get(0, 0, 11), 2.75);
5935    }
5936
5937    #[test]
5938    fn raster_read_hdf5_dataset_uri_reads_chunked_single_chunk_f32_path() {
5939        let mut tmp = tempfile::Builder::new()
5940            .suffix(".h5")
5941            .tempfile()
5942            .expect("temp file should be created");
5943        write_synthetic_hdf5_chunked_single_chunk_f32_fixture(&mut tmp);
5944
5945        let uri = format!(
5946            "{}#dataset=/ScienceData/NDVI_Chunked",
5947            tmp.path().to_string_lossy()
5948        );
5949        let raster = Raster::read(&uri).expect("chunked single-chunk HDF5 URI should materialize");
5950
5951        assert_eq!(raster.rows, 2);
5952        assert_eq!(raster.cols, 3);
5953        assert_eq!(raster.bands, 1);
5954        assert_eq!(raster.data_type, DataType::F32);
5955        assert_eq!(raster.get(0, 0, 0), 0.5);
5956        assert_eq!(raster.get(0, 0, 2), 2.5);
5957        assert_eq!(raster.get(0, 1, 2), 5.5);
5958    }
5959
5960    #[test]
5961    fn raster_read_hdf5_dataset_uri_reads_chunked_two_chunk_f32_path() {
5962        let mut tmp = tempfile::Builder::new()
5963            .suffix(".h5")
5964            .tempfile()
5965            .expect("temp file should be created");
5966        write_synthetic_hdf5_chunked_two_chunk_f32_fixture(&mut tmp);
5967
5968        let uri = format!(
5969            "{}#dataset=/ScienceData/NDVI_Chunked_Two",
5970            tmp.path().to_string_lossy()
5971        );
5972        let raster = Raster::read(&uri).expect("chunked two-chunk HDF5 URI should materialize");
5973
5974        assert_eq!(raster.rows, 2);
5975        assert_eq!(raster.cols, 4);
5976        assert_eq!(raster.bands, 1);
5977        assert_eq!(raster.data_type, DataType::F32);
5978        assert_eq!(raster.get(0, 0, 0), 0.5);
5979        assert_eq!(raster.get(0, 0, 3), 3.5);
5980        assert_eq!(raster.get(0, 1, 0), 4.5);
5981        assert_eq!(raster.get(0, 1, 3), 7.5);
5982    }
5983
5984    #[test]
5985    fn raster_read_hdf5_dataset_uri_reads_chunked_two_chunk_f32_deflate_path() {
5986        let mut tmp = tempfile::Builder::new()
5987            .suffix(".h5")
5988            .tempfile()
5989            .expect("temp file should be created");
5990        write_synthetic_hdf5_chunked_two_chunk_f32_deflate_fixture(&mut tmp);
5991
5992        let uri = format!(
5993            "{}#dataset=/ScienceData/NDVI_Chunked_Two_Deflate",
5994            tmp.path().to_string_lossy()
5995        );
5996        let raster =
5997            Raster::read(&uri).expect("chunked deflate two-chunk HDF5 URI should materialize");
5998
5999        assert_eq!(raster.rows, 2);
6000        assert_eq!(raster.cols, 4);
6001        assert_eq!(raster.bands, 1);
6002        assert_eq!(raster.data_type, DataType::F32);
6003        assert_eq!(raster.get(0, 0, 0), 0.5);
6004        assert_eq!(raster.get(0, 0, 3), 3.5);
6005        assert_eq!(raster.get(0, 1, 0), 4.5);
6006        assert_eq!(raster.get(0, 1, 3), 7.5);
6007    }
6008
6009    #[test]
6010    fn raster_read_hdf5_dataset_uri_reads_chunked_two_leaf_f32_path() {
6011        let mut tmp = tempfile::Builder::new()
6012            .suffix(".h5")
6013            .tempfile()
6014            .expect("temp file should be created");
6015        write_synthetic_hdf5_chunked_two_leaf_f32_fixture(&mut tmp);
6016
6017        let uri = format!(
6018            "{}#dataset=/ScienceData/NDVI_Chunked_Two_Leaf",
6019            tmp.path().to_string_lossy()
6020        );
6021        let raster = Raster::read(&uri).expect("chunked two-leaf HDF5 URI should materialize");
6022
6023        assert_eq!(raster.rows, 2);
6024        assert_eq!(raster.cols, 4);
6025        assert_eq!(raster.bands, 1);
6026        assert_eq!(raster.data_type, DataType::F32);
6027        assert_eq!(raster.get(0, 0, 0), 0.5);
6028        assert_eq!(raster.get(0, 0, 3), 3.5);
6029        assert_eq!(raster.get(0, 1, 0), 4.5);
6030        assert_eq!(raster.get(0, 1, 3), 7.5);
6031    }
6032
6033    #[test]
6034    fn raster_read_hdf5_dataset_uri_reads_chunked_internal_root_f32_path() {
6035        let mut tmp = tempfile::Builder::new()
6036            .suffix(".h5")
6037            .tempfile()
6038            .expect("temp file should be created");
6039        write_synthetic_hdf5_chunked_internal_root_fixture(&mut tmp);
6040
6041        let uri = format!(
6042            "{}#dataset=/ScienceData/NDVI_Chunked_InternalRoot",
6043            tmp.path().to_string_lossy()
6044        );
6045        let raster = Raster::read(&uri).expect("internal-root chunk index should materialize");
6046        assert_eq!(raster.rows, 2);
6047        assert_eq!(raster.cols, 4);
6048        assert_eq!(raster.bands, 1);
6049        assert_eq!(raster.data_type, DataType::F32);
6050        assert_eq!(raster.get(0, 0, 0), 0.5);
6051        assert_eq!(raster.get(0, 0, 3), 3.5);
6052        assert_eq!(raster.get(0, 1, 0), 4.5);
6053        assert_eq!(raster.get(0, 1, 3), 7.5);
6054    }
6055
6056    #[test]
6057    fn raster_read_hdf5_dataset_uri_reads_chunked_multilevel_root_f32_path() {
6058        let mut tmp = tempfile::Builder::new()
6059            .suffix(".h5")
6060            .tempfile()
6061            .expect("temp file should be created");
6062        write_synthetic_hdf5_chunked_multilevel_root_fixture(&mut tmp);
6063
6064        let uri = format!(
6065            "{}#dataset=/ScienceData/NDVI_Chunked_MultiLevelRoot",
6066            tmp.path().to_string_lossy()
6067        );
6068        let raster = Raster::read(&uri).expect("multilevel root chunk index should materialize");
6069        assert_eq!(raster.rows, 2);
6070        assert_eq!(raster.cols, 6);
6071        assert_eq!(raster.bands, 1);
6072        assert_eq!(raster.data_type, DataType::F32);
6073        assert_eq!(raster.get(0, 0, 0), 0.5);
6074        assert_eq!(raster.get(0, 0, 3), 3.5);
6075        assert_eq!(raster.get(0, 0, 5), 9.5);
6076        assert_eq!(raster.get(0, 1, 0), 4.5);
6077        assert_eq!(raster.get(0, 1, 3), 7.5);
6078        assert_eq!(raster.get(0, 1, 5), 11.5);
6079    }
6080
6081    #[test]
6082    fn raster_read_hdf5_dataset_uri_reports_malformed_multilevel_root_as_unsupported() {
6083        let mut tmp = tempfile::Builder::new()
6084            .suffix(".h5")
6085            .tempfile()
6086            .expect("temp file should be created");
6087        write_synthetic_hdf5_chunked_malformed_multilevel_root_fixture(&mut tmp);
6088
6089        let uri = format!(
6090            "{}#dataset=/ScienceData/NDVI_Chunked_MalformedMultiLevel",
6091            tmp.path().to_string_lossy()
6092        );
6093        let err = Raster::read(&uri).expect_err("malformed multilevel root should fail explicitly");
6094        let msg = err.to_string();
6095        assert!(
6096            msg.contains("B-tree node is missing TREE signature")
6097                || msg.contains("exhausted internal-level budget")
6098                || msg.contains("internal-node cycle")
6099                || msg.contains("invalid child address"),
6100            "unexpected malformed-root diagnostic: {msg}"
6101        );
6102    }
6103
6104    #[test]
6105    fn raster_read_hdf5_dataset_uri_reports_malformed_multilevel_fanout_as_unsupported() {
6106        let mut tmp = tempfile::Builder::new()
6107            .suffix(".h5")
6108            .tempfile()
6109            .expect("temp file should be created");
6110        write_synthetic_hdf5_chunked_malformed_multilevel_fanout_fixture(&mut tmp);
6111
6112        let uri = format!(
6113            "{}#dataset=/ScienceData/NDVI_Chunked_MalformedMultiLevelFanout",
6114            tmp.path().to_string_lossy()
6115        );
6116        let err =
6117            Raster::read(&uri).expect_err("malformed multilevel fanout should fail explicitly");
6118        let msg = err.to_string();
6119        assert!(
6120            msg.contains("B-tree node is missing TREE signature")
6121                || msg.contains("exhausted internal-level budget")
6122                || msg.contains("internal-node cycle")
6123                || msg.contains("invalid child address"),
6124            "unexpected malformed-fanout diagnostic: {msg}"
6125        );
6126    }
6127
6128    #[test]
6129    fn raster_read_hdf5_dataset_uri_reports_unimplemented_materialization() {
6130        let err = Raster::read("mock_scene.h5:///ScienceData/DoesNotExist")
6131            .expect_err("HDF5 dataset URI should currently fail explicitly");
6132        assert!(err
6133            .to_string()
6134            .contains("HDF5 dataset path resolution failed"));
6135    }
6136
6137    #[test]
6138    fn get_set() {
6139        let mut r = make_raster();
6140        assert_eq!(r.get(0, 0, 0), 0.0);
6141        assert_eq!(r.get(0, 2, 3), 11.0);
6142        r.set(0, 1, 1, -9999.0).unwrap();
6143        assert!(r.is_nodata(r.get(0, 1, 1))); // nodata
6144        assert_eq!(r.get_opt(0, 1, 1), None); // optional accessor
6145    }
6146
6147    #[test]
6148    fn statistics() {
6149        let r = make_raster();
6150        let s = r.statistics();
6151        assert_eq!(s.valid_count, 12);
6152        assert_eq!(s.min, 0.0);
6153        assert_eq!(s.max, 11.0);
6154        assert!((s.mean - 5.5).abs() < 1e-10);
6155    }
6156
6157    #[test]
6158    fn world_to_pixel() {
6159        let r = make_raster();
6160        // y_max = 30.0, x_max = 40.0
6161        assert_eq!(r.world_to_pixel(5.0, 25.0), Some((0, 0)));
6162        assert_eq!(r.world_to_pixel(35.0, 5.0), Some((3, 2)));
6163        assert_eq!(r.world_to_pixel(-1.0, 0.0), None);
6164    }
6165
6166    #[test]
6167    fn extent() {
6168        let r = make_raster();
6169        let e = r.extent();
6170        assert_eq!(e.x_min, 0.0);
6171        assert_eq!(e.y_min, 0.0);
6172        assert_eq!(e.x_max, 40.0);
6173        assert_eq!(e.y_max, 30.0);
6174    }
6175
6176    #[test]
6177    fn sample_extent_boundary_points_count_and_corners() {
6178        let e = Extent {
6179            x_min: 0.0,
6180            y_min: 0.0,
6181            x_max: 10.0,
6182            y_max: 5.0,
6183        };
6184        let pts = sample_extent_boundary_points(e, 8);
6185        assert_eq!(pts.len(), 32);
6186        assert!(pts.contains(&(0.0, 0.0)));
6187        assert!(pts.contains(&(0.0, 5.0)));
6188        assert!(pts.contains(&(10.0, 0.0)));
6189        assert!(pts.contains(&(10.0, 5.0)));
6190    }
6191
6192    #[test]
6193    fn sample_extent_boundary_points_minimum_sampling_when_zero_requested() {
6194        let e = Extent {
6195            x_min: -1.0,
6196            y_min: -2.0,
6197            x_max: 3.0,
6198            y_max: 4.0,
6199        };
6200        let pts = sample_extent_boundary_points(e, 0);
6201        assert_eq!(pts.len(), 4);
6202        assert!(pts.contains(&(-1.0, -2.0)));
6203        assert!(pts.contains(&(-1.0, 4.0)));
6204        assert!(pts.contains(&(3.0, -2.0)));
6205        assert!(pts.contains(&(3.0, 4.0)));
6206    }
6207
6208    #[test]
6209    fn minimal_wrapped_longitude_bounds_handles_antimeridian_cluster() {
6210        let lons = [179.0, -179.0, 178.0, -178.0];
6211        let (x0, x1) = minimal_wrapped_longitude_bounds(&lons).unwrap();
6212        assert!((x1 - x0) < 6.0);
6213    }
6214
6215    #[test]
6216    fn antimeridian_aware_longitude_bounds_prefers_wrapped_interval() {
6217        let lons = [179.5, -179.5, 179.0, -179.0];
6218        let (x0, x1) =
6219            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
6220        assert!((x1 - x0) < 5.0);
6221        assert!(x0 >= 0.0);
6222    }
6223
6224    #[test]
6225    fn antimeridian_aware_longitude_bounds_keeps_linear_when_better() {
6226        let lons = [-20.0, -10.0, 0.0, 10.0];
6227        let (x0, x1) =
6228            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
6229        assert!((x0 - (-20.0)).abs() < 1e-12);
6230        assert!((x1 - 10.0).abs() < 1e-12);
6231    }
6232
6233    #[test]
6234    fn antimeridian_policy_controls_wrapped_vs_linear_behavior() {
6235        let lons = [179.5, -179.5, 179.0, -179.0];
6236
6237        let (ax0, ax1) =
6238            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Auto).unwrap();
6239        let (lx0, lx1) =
6240            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Linear).unwrap();
6241        let (wx0, wx1) =
6242            antimeridian_aware_longitude_bounds(&lons, AntimeridianPolicy::Wrap).unwrap();
6243
6244        assert!((ax1 - ax0) < (lx1 - lx0));
6245        assert!((wx1 - wx0) < (lx1 - lx0));
6246        assert!((wx1 - wx0 - (ax1 - ax0)).abs() < 1e-9);
6247    }
6248
6249    #[test]
6250    fn reproject_antimeridian_policy_linear_vs_wrap_changes_default_extent() {
6251        let cfg = RasterConfig {
6252            cols: 8,
6253            rows: 4,
6254            x_min: 170.0,
6255            y_min: -10.0,
6256            cell_size: 2.5,
6257            nodata: -9999.0,
6258            ..Default::default()
6259        };
6260        let mut r = Raster::new(cfg);
6261        r.crs = CrsInfo::from_epsg(4326);
6262        for row in 0..r.rows {
6263            for col in 0..r.cols {
6264                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
6265                    .unwrap();
6266            }
6267        }
6268
6269        let linear_opts = ReprojectOptions::new(4326, ResampleMethod::Nearest)
6270            .with_antimeridian_policy(AntimeridianPolicy::Linear);
6271        let wrap_opts = ReprojectOptions::new(4326, ResampleMethod::Nearest)
6272            .with_antimeridian_policy(AntimeridianPolicy::Wrap);
6273
6274        let linear = r.reproject_with_options(&linear_opts).unwrap();
6275        let wrap = r.reproject_with_options(&wrap_opts).unwrap();
6276
6277        let linear_width = linear.x_max() - linear.x_min;
6278        let wrap_width = wrap.x_max() - wrap.x_min;
6279
6280        assert!(linear_width.is_finite() && linear_width > 0.0);
6281        assert!(wrap_width.is_finite() && wrap_width > 0.0);
6282        assert!(wrap_width <= linear_width + 1e-9);
6283    }
6284
6285    #[test]
6286    fn reproject_antimeridian_policy_auto_matches_narrower_interval() {
6287        let cfg = RasterConfig {
6288            cols: 8,
6289            rows: 4,
6290            x_min: 170.0,
6291            y_min: -10.0,
6292            cell_size: 2.5,
6293            nodata: -9999.0,
6294            ..Default::default()
6295        };
6296        let mut r = Raster::new(cfg);
6297        r.crs = CrsInfo::from_epsg(4326);
6298        for row in 0..r.rows {
6299            for col in 0..r.cols {
6300                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
6301                    .unwrap();
6302            }
6303        }
6304
6305        let linear = r
6306            .reproject_with_options(
6307                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
6308                    .with_antimeridian_policy(AntimeridianPolicy::Linear),
6309            )
6310            .unwrap();
6311        let wrap = r
6312            .reproject_with_options(
6313                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
6314                    .with_antimeridian_policy(AntimeridianPolicy::Wrap),
6315            )
6316            .unwrap();
6317        let auto = r
6318            .reproject_with_options(
6319                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
6320                    .with_antimeridian_policy(AntimeridianPolicy::Auto),
6321            )
6322            .unwrap();
6323
6324        let linear_width = linear.x_max() - linear.x_min;
6325        let wrap_width = wrap.x_max() - wrap.x_min;
6326        let auto_width = auto.x_max() - auto.x_min;
6327
6328        let expected = linear_width.min(wrap_width);
6329        assert!((auto_width - expected).abs() < 1e-9);
6330    }
6331
6332    #[test]
6333    fn reproject_to_epsg_requires_source_epsg() {
6334        let r = make_raster();
6335        let err = r.reproject_to_epsg_nearest(3857).unwrap_err();
6336        assert!(err
6337            .to_string()
6338            .contains("requires source CRS EPSG in raster.crs.epsg"));
6339    }
6340
6341    #[test]
6342    fn reproject_with_crs_allows_missing_source_epsg() {
6343        let cfg = RasterConfig {
6344            cols: 4,
6345            rows: 4,
6346            x_min: -1.0,
6347            y_min: -1.0,
6348            cell_size: 0.5,
6349            nodata: -9999.0,
6350            ..Default::default()
6351        };
6352        let mut r = Raster::new(cfg);
6353        // Intentionally leave `r.crs.epsg` unset.
6354        for row in 0..4 {
6355            for col in 0..4 {
6356                r.set(0, row, col, (row * 4 + col) as f64).unwrap();
6357            }
6358        }
6359
6360        let src = Crs::from_epsg(4326).unwrap();
6361        let dst = Crs::from_epsg(3857).unwrap();
6362        let opts = ReprojectOptions::new(3857, ResampleMethod::Nearest);
6363
6364        let out = r.reproject_with_crs(&src, &dst, &opts).unwrap();
6365        assert_eq!(out.cols, 4);
6366        assert_eq!(out.rows, 4);
6367        assert_eq!(out.crs.epsg, Some(3857));
6368        assert!(out.statistics().valid_count > 0);
6369    }
6370
6371    #[test]
6372    fn reproject_with_options_and_progress_emits_live_updates() {
6373        let cfg = RasterConfig {
6374            cols: 6,
6375            rows: 5,
6376            x_min: -80.0,
6377            y_min: 40.0,
6378            cell_size: 0.1,
6379            nodata: -9999.0,
6380            ..Default::default()
6381        };
6382        let mut r = Raster::new(cfg);
6383        r.crs = CrsInfo::from_epsg(4326);
6384        for row in 0..r.rows {
6385            for col in 0..r.cols {
6386                r.set(0, row as isize, col as isize, (row * r.cols + col) as f64)
6387                    .unwrap();
6388            }
6389        }
6390
6391        let progress_values: Arc<Mutex<Vec<f64>>> = Arc::new(Mutex::new(Vec::new()));
6392        let sink = Arc::clone(&progress_values);
6393
6394        let out = r
6395            .reproject_with_options_and_progress(
6396                &ReprojectOptions::new(3857, ResampleMethod::Nearest),
6397                move |pct| {
6398                    sink.lock().unwrap().push(pct);
6399                },
6400            )
6401            .unwrap();
6402
6403        let values = progress_values.lock().unwrap();
6404        assert!(!values.is_empty());
6405        assert_eq!(values.len(), out.rows + 1);
6406        assert!(values
6407            .iter()
6408            .all(|v| v.is_finite() && *v >= 0.0 && *v <= 1.0));
6409        assert!((values.last().copied().unwrap() - 1.0).abs() < 1e-12);
6410    }
6411
6412    #[test]
6413    fn reproject_to_epsg_identity_preserves_data() {
6414        let mut r = make_raster();
6415        r.crs = CrsInfo::from_epsg(4326);
6416
6417        let r2 = r.reproject_to_epsg(4326, ResampleMethod::Nearest).unwrap();
6418        assert_eq!(r2.cols, r.cols);
6419        assert_eq!(r2.rows, r.rows);
6420        assert_eq!(r2.bands, r.bands);
6421        assert_eq!(r2.crs.epsg, Some(4326));
6422        assert_eq!(r2.get(0, 0, 0), r.get(0, 0, 0));
6423        assert_eq!(r2.get(0, 2, 3), r.get(0, 2, 3));
6424    }
6425
6426    #[test]
6427    fn reproject_to_epsg_4326_to_3857_produces_valid_output() {
6428        let cfg = RasterConfig {
6429            cols: 4,
6430            rows: 4,
6431            x_min: -1.0,
6432            y_min: -1.0,
6433            cell_size: 0.5,
6434            nodata: -9999.0,
6435            ..Default::default()
6436        };
6437        let mut r = Raster::new(cfg);
6438        r.crs = CrsInfo::from_epsg(4326);
6439        for row in 0..4 {
6440            for col in 0..4 {
6441                r.set(0, row, col, (row * 4 + col) as f64).unwrap();
6442            }
6443        }
6444
6445        let out = r.reproject_to_epsg_nearest(3857).unwrap();
6446        assert_eq!(out.cols, 4);
6447        assert_eq!(out.rows, 4);
6448        assert_eq!(out.crs.epsg, Some(3857));
6449        assert!(out.x_min.is_finite());
6450        assert!(out.y_min.is_finite());
6451        assert!(out.cell_size_x.is_finite() && out.cell_size_x > 0.0);
6452        assert!(out.cell_size_y.is_finite() && out.cell_size_y > 0.0);
6453        let s = out.statistics();
6454        assert!(s.valid_count > 0);
6455    }
6456
6457    #[test]
6458    fn reproject_to_match_grid_honors_target_grid_definition() {
6459        let src_cfg = RasterConfig {
6460            cols: 6,
6461            rows: 4,
6462            x_min: -3.0,
6463            y_min: -2.0,
6464            cell_size: 1.0,
6465            nodata: -9999.0,
6466            ..Default::default()
6467        };
6468        let mut src = Raster::new(src_cfg);
6469        src.crs = CrsInfo::from_epsg(4326);
6470        for row in 0..src.rows {
6471            for col in 0..src.cols {
6472                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
6473                    .unwrap();
6474            }
6475        }
6476
6477        let target_cfg = RasterConfig {
6478            cols: 12,
6479            rows: 10,
6480            x_min: -500_000.0,
6481            y_min: -400_000.0,
6482            cell_size: 1.0,
6483            cell_size_y: Some(1.0),
6484            nodata: -9999.0,
6485            ..Default::default()
6486        };
6487        let mut target = Raster::new(target_cfg);
6488        target.crs = CrsInfo::from_epsg(3857);
6489        target.cell_size_x = (500_000.0 - (-500_000.0)) / target.cols as f64;
6490        target.cell_size_y = (400_000.0 - (-400_000.0)) / target.rows as f64;
6491
6492        let out = src
6493            .reproject_to_match_grid(&target, ResampleMethod::Bilinear)
6494            .unwrap();
6495
6496        assert_eq!(out.cols, target.cols);
6497        assert_eq!(out.rows, target.rows);
6498        assert_eq!(out.crs.epsg, target.crs.epsg);
6499        assert!((out.x_min - target.x_min).abs() < 1e-9);
6500        assert!((out.y_min - target.y_min).abs() < 1e-9);
6501        assert!((out.x_max() - target.x_max()).abs() < 1e-6);
6502        assert!((out.y_max() - target.y_max()).abs() < 1e-6);
6503        assert!(out.cell_size_x.is_finite() && out.cell_size_x > 0.0);
6504        assert!(out.cell_size_y.is_finite() && out.cell_size_y > 0.0);
6505    }
6506
6507    #[test]
6508    fn reproject_to_match_resolution_honors_reference_cellsize_and_snap() {
6509        let src_cfg = RasterConfig {
6510            cols: 6,
6511            rows: 4,
6512            x_min: -3.0,
6513            y_min: -2.0,
6514            cell_size: 1.0,
6515            nodata: -9999.0,
6516            ..Default::default()
6517        };
6518        let mut src = Raster::new(src_cfg);
6519        src.crs = CrsInfo::from_epsg(4326);
6520        for row in 0..src.rows {
6521            for col in 0..src.cols {
6522                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
6523                    .unwrap();
6524            }
6525        }
6526
6527        let mut reference = Raster::new(RasterConfig {
6528            cols: 4,
6529            rows: 3,
6530            x_min: 50_000.0,
6531            y_min: -75_000.0,
6532            cell_size: 100_000.0,
6533            cell_size_y: Some(80_000.0),
6534            nodata: -9999.0,
6535            ..Default::default()
6536        });
6537        reference.crs = CrsInfo::from_epsg(3857);
6538
6539        let out = src
6540            .reproject_to_match_resolution(&reference, ResampleMethod::Nearest)
6541            .unwrap();
6542
6543        assert_eq!(out.crs.epsg, Some(3857));
6544        assert!((out.cell_size_x - reference.cell_size_x).abs() < 1e-9);
6545        assert!((out.cell_size_y - reference.cell_size_y).abs() < 1e-9);
6546
6547        let kx = ((out.x_min - reference.x_min) / reference.cell_size_x).round();
6548        let ky = ((out.y_min - reference.y_min) / reference.cell_size_y).round();
6549        assert!((out.x_min - (reference.x_min + kx * reference.cell_size_x)).abs() < 1e-6);
6550        assert!((out.y_min - (reference.y_min + ky * reference.cell_size_y)).abs() < 1e-6);
6551        assert!(out.cols > 0);
6552        assert!(out.rows > 0);
6553    }
6554
6555    #[test]
6556    fn reproject_to_match_resolution_in_epsg_same_crs_matches_reference_settings() {
6557        let src_cfg = RasterConfig {
6558            cols: 6,
6559            rows: 4,
6560            x_min: -3.0,
6561            y_min: -2.0,
6562            cell_size: 1.0,
6563            nodata: -9999.0,
6564            ..Default::default()
6565        };
6566        let mut src = Raster::new(src_cfg);
6567        src.crs = CrsInfo::from_epsg(4326);
6568        for row in 0..src.rows {
6569            for col in 0..src.cols {
6570                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
6571                    .unwrap();
6572            }
6573        }
6574
6575        let mut reference = Raster::new(RasterConfig {
6576            cols: 4,
6577            rows: 3,
6578            x_min: -10.0,
6579            y_min: -20.0,
6580            cell_size: 0.5,
6581            cell_size_y: Some(0.25),
6582            nodata: -9999.0,
6583            ..Default::default()
6584        });
6585        reference.crs = CrsInfo::from_epsg(4326);
6586
6587        let out = src
6588            .reproject_to_match_resolution_in_epsg(4326, &reference, ResampleMethod::Nearest)
6589            .unwrap();
6590
6591        assert_eq!(out.crs.epsg, Some(4326));
6592        assert!((out.cell_size_x - reference.cell_size_x).abs() < 1e-12);
6593        assert!((out.cell_size_y - reference.cell_size_y).abs() < 1e-12);
6594    }
6595
6596    #[test]
6597    fn reproject_to_match_resolution_in_epsg_cross_crs_converts_resolution() {
6598        let src_cfg = RasterConfig {
6599            cols: 6,
6600            rows: 4,
6601            x_min: -3.0,
6602            y_min: -2.0,
6603            cell_size: 1.0,
6604            nodata: -9999.0,
6605            ..Default::default()
6606        };
6607        let mut src = Raster::new(src_cfg);
6608        src.crs = CrsInfo::from_epsg(4326);
6609        for row in 0..src.rows {
6610            for col in 0..src.cols {
6611                src.set(0, row as isize, col as isize, (row * src.cols + col) as f64)
6612                    .unwrap();
6613            }
6614        }
6615
6616        let mut reference = Raster::new(RasterConfig {
6617            cols: 4,
6618            rows: 3,
6619            x_min: 0.0,
6620            y_min: 0.0,
6621            cell_size: 1.0,
6622            cell_size_y: Some(1.0),
6623            nodata: -9999.0,
6624            ..Default::default()
6625        });
6626        reference.crs = CrsInfo::from_epsg(4326);
6627
6628        let out = src
6629            .reproject_to_match_resolution_in_epsg(3857, &reference, ResampleMethod::Nearest)
6630            .unwrap();
6631
6632        assert_eq!(out.crs.epsg, Some(3857));
6633        assert!(out.cell_size_x.is_finite());
6634        assert!(out.cell_size_y.is_finite());
6635        assert!(out.cell_size_x > 0.0);
6636        assert!(out.cell_size_y > 0.0);
6637    }
6638
6639    #[test]
6640    fn reproject_to_epsg_bilinear_cubic_and_lanczos_produce_valid_output() {
6641        let cfg = RasterConfig {
6642            cols: 8,
6643            rows: 8,
6644            x_min: -2.0,
6645            y_min: -2.0,
6646            cell_size: 0.5,
6647            nodata: -9999.0,
6648            ..Default::default()
6649        };
6650        let mut r = Raster::new(cfg);
6651        r.crs = CrsInfo::from_epsg(4326);
6652        for row in 0..8 {
6653            for col in 0..8 {
6654                let val = (col as f64) * 10.0 + row as f64;
6655                r.set(0, row, col, val).unwrap();
6656            }
6657        }
6658
6659        let bilinear = r.reproject_to_epsg_bilinear(3857).unwrap();
6660        let cubic = r.reproject_to_epsg_cubic(3857).unwrap();
6661        let lanczos = r.reproject_to_epsg_lanczos(3857).unwrap();
6662
6663        assert_eq!(bilinear.cols, 8);
6664        assert_eq!(bilinear.rows, 8);
6665        assert_eq!(bilinear.crs.epsg, Some(3857));
6666        assert!(bilinear.statistics().valid_count > 0);
6667
6668        assert_eq!(cubic.cols, 8);
6669        assert_eq!(cubic.rows, 8);
6670        assert_eq!(cubic.crs.epsg, Some(3857));
6671        assert!(cubic.statistics().valid_count > 0);
6672
6673        assert_eq!(lanczos.cols, 8);
6674        assert_eq!(lanczos.rows, 8);
6675        assert_eq!(lanczos.crs.epsg, Some(3857));
6676        assert!(lanczos.statistics().valid_count > 0);
6677    }
6678
6679    #[test]
6680    fn reproject_with_options_honors_grid_controls() {
6681        let cfg = RasterConfig {
6682            cols: 6,
6683            rows: 4,
6684            x_min: -3.0,
6685            y_min: -2.0,
6686            cell_size: 1.0,
6687            nodata: -9999.0,
6688            ..Default::default()
6689        };
6690        let mut r = Raster::new(cfg);
6691        r.crs = CrsInfo::from_epsg(4326);
6692        for row in 0..4 {
6693            for col in 0..6 {
6694                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
6695            }
6696        }
6697
6698        let opts = ReprojectOptions {
6699            dst_epsg: 3857,
6700            resample: ResampleMethod::Bilinear,
6701            cols: Some(12),
6702            rows: Some(10),
6703            extent: Some(Extent {
6704                x_min: -500_000.0,
6705                y_min: -400_000.0,
6706                x_max: 500_000.0,
6707                y_max: 400_000.0,
6708            }),
6709            x_res: None,
6710            y_res: None,
6711            snap_x: None,
6712            snap_y: None,
6713            nodata_policy: NodataPolicy::PartialKernel,
6714            antimeridian_policy: AntimeridianPolicy::Auto,
6715            grid_size_policy: GridSizePolicy::Expand,
6716            destination_footprint: DestinationFootprint::None,
6717            warn_on_area_of_use_mismatch: false,
6718            epoch_transform: EpochTransformOptions::default(),
6719        };
6720
6721        let out = r.reproject_with_options(&opts).unwrap();
6722        assert_eq!(out.cols, 12);
6723        assert_eq!(out.rows, 10);
6724        assert!((out.x_min - (-500_000.0)).abs() < 1e-9);
6725        assert!((out.y_min - (-400_000.0)).abs() < 1e-9);
6726        assert!((out.x_max() - 500_000.0).abs() < 1e-6);
6727        assert!((out.y_max() - 400_000.0).abs() < 1e-6);
6728        assert_eq!(out.crs.epsg, Some(3857));
6729    }
6730
6731    #[test]
6732    fn bilinear_partial_kernel_renormalizes_with_nodata() {
6733        let cfg = RasterConfig {
6734            cols: 2,
6735            rows: 2,
6736            x_min: 0.0,
6737            y_min: 0.0,
6738            cell_size: 1.0,
6739            nodata: -9999.0,
6740            ..Default::default()
6741        };
6742        // row-major top-down: [ [1, nodata], [3, 5] ]
6743        let r = Raster::from_data(cfg, vec![1.0, -9999.0, 3.0, 5.0]).unwrap();
6744
6745        let v = r.sample_bilinear_partial_pixel(0, 0.5, 0.5).unwrap();
6746        // weighted average of valid neighbors only: (1 + 3 + 5) / 3 = 3
6747        assert!((v - 3.0).abs() < 1e-9);
6748    }
6749
6750    #[test]
6751    fn bilinear_partial_kernel_handles_edges() {
6752        let cfg = RasterConfig {
6753            cols: 2,
6754            rows: 2,
6755            x_min: 0.0,
6756            y_min: 0.0,
6757            cell_size: 1.0,
6758            nodata: -9999.0,
6759            ..Default::default()
6760        };
6761        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
6762        let v = r.sample_bilinear_partial_pixel(0, -0.2, 0.3).unwrap();
6763        assert!(v.is_finite());
6764    }
6765
6766    #[test]
6767    fn cubic_partial_kernel_handles_edges_and_nodata() {
6768        let cfg = RasterConfig {
6769            cols: 4,
6770            rows: 4,
6771            x_min: 0.0,
6772            y_min: 0.0,
6773            cell_size: 1.0,
6774            nodata: -9999.0,
6775            ..Default::default()
6776        };
6777        let mut data = Vec::new();
6778        for row in 0..4 {
6779            for col in 0..4 {
6780                data.push((row * 4 + col) as f64);
6781            }
6782        }
6783        data[5] = -9999.0; // inject one nodata sample
6784        let r = Raster::from_data(cfg, data).unwrap();
6785
6786        let edge_v = r.sample_cubic_partial_pixel(0, -0.25, 0.2).unwrap();
6787        assert!(edge_v.is_finite());
6788
6789        let nodata_v = r.sample_cubic_partial_pixel(0, 1.25, 1.25).unwrap();
6790        assert!(nodata_v.is_finite());
6791    }
6792
6793    #[test]
6794    fn strict_policy_rejects_incomplete_bilinear_kernel() {
6795        let cfg = RasterConfig {
6796            cols: 2,
6797            rows: 2,
6798            x_min: 0.0,
6799            y_min: 0.0,
6800            cell_size: 1.0,
6801            nodata: -9999.0,
6802            ..Default::default()
6803        };
6804        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
6805
6806        assert!(r.sample_bilinear_strict_pixel(0, -0.2, 0.3).is_none());
6807        assert!(r.sample_bilinear_partial_pixel(0, -0.2, 0.3).is_some());
6808    }
6809
6810    #[test]
6811    fn bilinear_strict_simd_matches_expected_for_f64_and_f32_storage() {
6812        let cfg_f64 = RasterConfig {
6813            cols: 2,
6814            rows: 2,
6815            x_min: 0.0,
6816            y_min: 0.0,
6817            cell_size: 1.0,
6818            nodata: -9999.0,
6819            data_type: DataType::F64,
6820            ..Default::default()
6821        };
6822        let cfg_f32 = RasterConfig {
6823            data_type: DataType::F32,
6824            ..cfg_f64.clone()
6825        };
6826        let data = vec![1.0, 2.0, 3.0, 5.0];
6827        let r64 = Raster::from_data(cfg_f64, data.clone()).unwrap();
6828        let r32 = Raster::from_data(cfg_f32, data).unwrap();
6829
6830        let expected = 2.375;
6831        let v64 = r64.sample_bilinear_strict_pixel(0, 0.25, 0.5).unwrap();
6832        let v32 = r32.sample_bilinear_strict_pixel(0, 0.25, 0.5).unwrap();
6833        assert!((v64 - expected).abs() < 1e-9);
6834        assert!((v32 - expected).abs() < 1e-6);
6835    }
6836
6837    #[test]
6838    fn lanczos_strict_simd_matches_between_f64_and_f32_storage() {
6839        let cfg_f64 = RasterConfig {
6840            cols: 16,
6841            rows: 16,
6842            x_min: 0.0,
6843            y_min: 0.0,
6844            cell_size: 1.0,
6845            nodata: -9999.0,
6846            data_type: DataType::F64,
6847            ..Default::default()
6848        };
6849        let cfg_f32 = RasterConfig {
6850            data_type: DataType::F32,
6851            ..cfg_f64.clone()
6852        };
6853
6854        let mut data = Vec::with_capacity(16 * 16);
6855        for row in 0..16 {
6856            for col in 0..16 {
6857                data.push(((row * 16 + col) as f64).sin() * 100.0 + (row as f64 * 0.5));
6858            }
6859        }
6860
6861        let r64 = Raster::from_data(cfg_f64, data.clone()).unwrap();
6862        let r32 = Raster::from_data(cfg_f32, data).unwrap();
6863
6864        let v64 = r64.sample_lanczos_strict_pixel(0, 7.25, 8.5).unwrap();
6865        let v32 = r32.sample_lanczos_strict_pixel(0, 7.25, 8.5).unwrap();
6866        assert!((v64 - v32).abs() < 1e-4);
6867    }
6868
6869    #[test]
6870    fn fill_policy_falls_back_to_nearest_for_bilinear() {
6871        let cfg = RasterConfig {
6872            cols: 2,
6873            rows: 2,
6874            x_min: 0.0,
6875            y_min: 0.0,
6876            cell_size: 1.0,
6877            nodata: -9999.0,
6878            ..Default::default()
6879        };
6880        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
6881
6882        let v = r.sample_world(0, 0.1, 1.1, ResampleMethod::Bilinear, NodataPolicy::Fill);
6883        assert_eq!(v, Some(10.0));
6884    }
6885
6886    #[test]
6887    fn fill_policy_falls_back_to_nearest_for_lanczos() {
6888        let cfg = RasterConfig {
6889            cols: 2,
6890            rows: 2,
6891            x_min: 0.0,
6892            y_min: 0.0,
6893            cell_size: 1.0,
6894            nodata: -9999.0,
6895            ..Default::default()
6896        };
6897        let r = Raster::from_data(cfg, vec![10.0, 20.0, 30.0, 40.0]).unwrap();
6898
6899        let v = r.sample_world(0, 0.1, 1.1, ResampleMethod::Lanczos, NodataPolicy::Fill);
6900        assert_eq!(v, Some(10.0));
6901    }
6902
6903    #[test]
6904    fn reproject_options_default_to_partial_kernel_policy() {
6905        let opts = ReprojectOptions::new(3857, ResampleMethod::Cubic);
6906        assert_eq!(opts.nodata_policy, NodataPolicy::PartialKernel);
6907        assert_eq!(opts.x_res, None);
6908        assert_eq!(opts.y_res, None);
6909        assert_eq!(opts.snap_x, None);
6910        assert_eq!(opts.snap_y, None);
6911        assert_eq!(opts.antimeridian_policy, AntimeridianPolicy::Auto);
6912        assert_eq!(opts.grid_size_policy, GridSizePolicy::Expand);
6913        assert_eq!(opts.destination_footprint, DestinationFootprint::None);
6914
6915        let strict = opts.with_nodata_policy(NodataPolicy::Strict);
6916        assert_eq!(strict.nodata_policy, NodataPolicy::Strict);
6917
6918        let sized = strict.with_size(321, 123);
6919        assert_eq!(sized.cols, Some(321));
6920        assert_eq!(sized.rows, Some(123));
6921
6922        let e = Extent {
6923            x_min: -10.0,
6924            y_min: -5.0,
6925            x_max: 10.0,
6926            y_max: 5.0,
6927        };
6928        let ext = sized.with_extent(e);
6929        assert_eq!(ext.extent, Some(e));
6930
6931        let res = ext.with_resolution(30.0, 40.0);
6932        assert_eq!(res.x_res, Some(30.0));
6933        assert_eq!(res.y_res, Some(40.0));
6934
6935        let square = res.with_square_resolution(25.0);
6936        assert_eq!(square.x_res, Some(25.0));
6937        assert_eq!(square.y_res, Some(25.0));
6938
6939        let snapped = square.with_snap_origin(0.0, 0.0);
6940        assert_eq!(snapped.snap_x, Some(0.0));
6941        assert_eq!(snapped.snap_y, Some(0.0));
6942
6943        let linear = snapped.with_antimeridian_policy(AntimeridianPolicy::Linear);
6944        assert_eq!(linear.antimeridian_policy, AntimeridianPolicy::Linear);
6945
6946        let fit_inside = linear.with_grid_size_policy(GridSizePolicy::FitInside);
6947        assert_eq!(fit_inside.grid_size_policy, GridSizePolicy::FitInside);
6948
6949        let masked = fit_inside.with_destination_footprint(DestinationFootprint::SourceBoundary);
6950        assert_eq!(
6951            masked.destination_footprint,
6952            DestinationFootprint::SourceBoundary
6953        );
6954    }
6955
6956    #[test]
6957    fn sample_extent_boundary_ring_count_and_corners() {
6958        let e = Extent {
6959            x_min: 0.0,
6960            y_min: 0.0,
6961            x_max: 10.0,
6962            y_max: 5.0,
6963        };
6964        let ring = sample_extent_boundary_ring(e, 8);
6965        assert_eq!(ring.len(), 32);
6966        assert!(ring.contains(&(0.0, 0.0)));
6967        assert!(ring.contains(&(10.0, 0.0)));
6968        assert!(ring.contains(&(10.0, 5.0)));
6969        assert!(ring.contains(&(0.0, 5.0)));
6970    }
6971
6972    #[test]
6973    fn point_in_polygon_identifies_inside_and_outside_points() {
6974        let poly = vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)];
6975        assert!(point_in_polygon(2.0, 2.0, &poly));
6976        assert!(!point_in_polygon(5.0, 2.0, &poly));
6977    }
6978
6979    #[test]
6980    fn thematic_3x3_resamplers_return_expected_statistics() {
6981        let cfg = RasterConfig {
6982            cols: 3,
6983            rows: 3,
6984            x_min: 0.0,
6985            y_min: 0.0,
6986            cell_size: 1.0,
6987            nodata: -9999.0,
6988            ..Default::default()
6989        };
6990        let data = vec![1.0, 2.0, 2.0, 3.0, 4.0, 4.0, 5.0, 6.0, 6.0];
6991        let expected_mean = data.iter().sum::<f64>() / data.len() as f64;
6992        let expected_stddev = (data
6993            .iter()
6994            .map(|v| {
6995                let d = *v - expected_mean;
6996                d * d
6997            })
6998            .sum::<f64>()
6999            / data.len() as f64)
7000            .sqrt();
7001        let r = Raster::from_data(cfg, data).unwrap();
7002
7003        let x = r.col_center_x(1);
7004        let y = r.row_center_y(1);
7005
7006        let avg = r
7007            .sample_world(0, x, y, ResampleMethod::Average, NodataPolicy::Strict)
7008            .unwrap();
7009        let min = r
7010            .sample_world(0, x, y, ResampleMethod::Min, NodataPolicy::Strict)
7011            .unwrap();
7012        let max = r
7013            .sample_world(0, x, y, ResampleMethod::Max, NodataPolicy::Strict)
7014            .unwrap();
7015        let mode = r
7016            .sample_world(0, x, y, ResampleMethod::Mode, NodataPolicy::Strict)
7017            .unwrap();
7018        let median = r
7019            .sample_world(0, x, y, ResampleMethod::Median, NodataPolicy::Strict)
7020            .unwrap();
7021        let stddev = r
7022            .sample_world(0, x, y, ResampleMethod::StdDev, NodataPolicy::Strict)
7023            .unwrap();
7024
7025        assert!((avg - (33.0 / 9.0)).abs() < 1e-9);
7026        assert_eq!(min, 1.0);
7027        assert_eq!(max, 6.0);
7028        assert_eq!(mode, 2.0);
7029        assert_eq!(median, 4.0);
7030        assert!((stddev - expected_stddev).abs() < 1e-9);
7031    }
7032
7033    #[test]
7034    fn grid_size_policy_fit_inside_reduces_resolution_derived_size() {
7035        let mut r = make_raster();
7036        r.crs = CrsInfo::from_epsg(4326);
7037
7038        let extent = Extent {
7039            x_min: -500_000.0,
7040            y_min: -400_000.0,
7041            x_max: 500_000.0,
7042            y_max: 400_000.0,
7043        };
7044
7045        let expand = r
7046            .reproject_with_options(
7047                &ReprojectOptions::new(3857, ResampleMethod::Nearest)
7048                    .with_extent(extent)
7049                    .with_resolution(300_000.0, 300_000.0)
7050                    .with_grid_size_policy(GridSizePolicy::Expand),
7051            )
7052            .unwrap();
7053
7054        let fit = r
7055            .reproject_with_options(
7056                &ReprojectOptions::new(3857, ResampleMethod::Nearest)
7057                    .with_extent(extent)
7058                    .with_resolution(300_000.0, 300_000.0)
7059                    .with_grid_size_policy(GridSizePolicy::FitInside),
7060            )
7061            .unwrap();
7062
7063        assert!(fit.cols <= expand.cols);
7064        assert!(fit.rows <= expand.rows);
7065    }
7066
7067    #[test]
7068    fn destination_footprint_masks_cells_outside_source_boundary() {
7069        let cfg = RasterConfig {
7070            cols: 4,
7071            rows: 4,
7072            x_min: 0.0,
7073            y_min: 0.0,
7074            cell_size: 1.0,
7075            nodata: -9999.0,
7076            ..Default::default()
7077        };
7078        let mut r = Raster::new(cfg);
7079        r.crs = CrsInfo::from_epsg(4326);
7080        for row in 0..4 {
7081            for col in 0..4 {
7082                r.set(0, row, col, 1.0).unwrap();
7083            }
7084        }
7085
7086        let out = r
7087            .reproject_with_options(
7088                &ReprojectOptions::new(4326, ResampleMethod::Nearest)
7089                    .with_extent(Extent {
7090                        x_min: -1.0,
7091                        y_min: -1.0,
7092                        x_max: 5.0,
7093                        y_max: 5.0,
7094                    })
7095                    .with_size(6, 6)
7096                    .with_destination_footprint(DestinationFootprint::SourceBoundary),
7097            )
7098            .unwrap();
7099
7100        assert!(out.is_nodata(out.get(0, 0, 0)));
7101        assert!(!out.is_nodata(out.get(0, 2, 2)));
7102    }
7103
7104    #[test]
7105    fn reproject_with_options_honors_snap_origin_with_resolution() {
7106        let cfg = RasterConfig {
7107            cols: 6,
7108            rows: 4,
7109            x_min: -3.0,
7110            y_min: -2.0,
7111            cell_size: 1.0,
7112            nodata: -9999.0,
7113            ..Default::default()
7114        };
7115        let mut r = Raster::new(cfg);
7116        r.crs = CrsInfo::from_epsg(4326);
7117        for row in 0..4 {
7118            for col in 0..6 {
7119                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
7120            }
7121        }
7122
7123        let out_extent = Extent {
7124            x_min: -510_000.0,
7125            y_min: -390_000.0,
7126            x_max: 490_000.0,
7127            y_max: 410_000.0,
7128        };
7129        let opts = ReprojectOptions::new(3857, ResampleMethod::Bilinear)
7130            .with_extent(out_extent)
7131            .with_resolution(200_000.0, 160_000.0)
7132            .with_snap_origin(0.0, 0.0);
7133
7134        let out = r.reproject_with_options(&opts).unwrap();
7135        assert!((out.x_min - (-600_000.0)).abs() < 1e-6);
7136        assert!((out.y_min - (-480_000.0)).abs() < 1e-6);
7137        assert!((out.cell_size_x - 200_000.0).abs() < 1e-6);
7138        assert!((out.cell_size_y - 160_000.0).abs() < 1e-6);
7139    }
7140
7141    #[test]
7142    fn reproject_with_options_honors_resolution_controls() {
7143        let cfg = RasterConfig {
7144            cols: 6,
7145            rows: 4,
7146            x_min: -3.0,
7147            y_min: -2.0,
7148            cell_size: 1.0,
7149            nodata: -9999.0,
7150            ..Default::default()
7151        };
7152        let mut r = Raster::new(cfg);
7153        r.crs = CrsInfo::from_epsg(4326);
7154        for row in 0..4 {
7155            for col in 0..6 {
7156                r.set(0, row, col, (row * 6 + col) as f64).unwrap();
7157            }
7158        }
7159
7160        let out_extent = Extent {
7161            x_min: -500_000.0,
7162            y_min: -400_000.0,
7163            x_max: 500_000.0,
7164            y_max: 400_000.0,
7165        };
7166        let opts = ReprojectOptions::new(3857, ResampleMethod::Bilinear)
7167            .with_extent(out_extent)
7168            .with_resolution(200_000.0, 160_000.0);
7169
7170        let out = r.reproject_with_options(&opts).unwrap();
7171        assert_eq!(out.cols, 5);
7172        assert_eq!(out.rows, 5);
7173        assert!((out.cell_size_x - 200_000.0).abs() < 1e-6);
7174        assert!((out.cell_size_y - 160_000.0).abs() < 1e-6);
7175    }
7176
7177    #[test]
7178    fn reproject_with_options_rejects_invalid_resolution_controls() {
7179        let mut r = make_raster();
7180        r.crs = CrsInfo::from_epsg(4326);
7181        let opts = ReprojectOptions::new(3857, ResampleMethod::Nearest).with_square_resolution(0.0);
7182        let err = r.reproject_with_options(&opts).unwrap_err();
7183        assert!(err.to_string().contains("invalid reprojection resolution"));
7184    }
7185
7186    #[test]
7187    fn band_helpers() {
7188        let cfg = RasterConfig {
7189            cols: 3,
7190            rows: 2,
7191            bands: 2,
7192            nodata: -9999.0,
7193            ..Default::default()
7194        };
7195        let data = vec![
7196            1.0, 2.0, 3.0, 4.0, 5.0, -9999.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0,
7197        ];
7198        let mut r = Raster::from_data(cfg, data).unwrap();
7199
7200        let b0 = r.band_slice(0);
7201        assert_eq!(b0.len(), 6);
7202        assert_eq!(b0[0], 1.0);
7203        assert_eq!(b0[5], -9999.0);
7204
7205        let s0 = r.statistics_band(0).unwrap();
7206        assert_eq!(s0.valid_count, 5);
7207        assert_eq!(s0.nodata_count, 1);
7208        assert_eq!(s0.min, 1.0);
7209        assert_eq!(s0.max, 5.0);
7210
7211        r.set_band_slice(1, &[7.0, 8.0, 9.0, 10.0, 11.0, 12.0])
7212            .unwrap();
7213        assert_eq!(r.get_raw(1, 0, 0), Some(7.0));
7214        assert_eq!(r.get_raw(1, 1, 2), Some(12.0));
7215    }
7216
7217    #[test]
7218    fn band_transform_helpers() {
7219        let cfg = RasterConfig {
7220            cols: 2,
7221            rows: 2,
7222            bands: 2,
7223            nodata: -9999.0,
7224            ..Default::default()
7225        };
7226        let data = vec![1.0, 2.0, 3.0, -9999.0, 10.0, 20.0, 30.0, 40.0];
7227        let mut r = Raster::from_data(cfg, data).unwrap();
7228
7229        r.map_valid_band(0, |v| v * 2.0).unwrap();
7230        assert_eq!(r.get_raw(0, 0, 0), Some(2.0));
7231        assert_eq!(r.get_raw(0, 0, 1), Some(4.0));
7232        assert!(r.is_nodata(r.get(0, 1, 1))); // nodata unchanged
7233        assert_eq!(r.get_raw(1, 0, 0), Some(10.0)); // other band unchanged
7234
7235        r.replace_band(1, 20.0, 99.0).unwrap();
7236        assert_eq!(r.get_raw(1, 0, 1), Some(99.0));
7237        assert_eq!(r.get_raw(1, 0, 0), Some(10.0));
7238        assert_eq!(r.get_raw(0, 0, 1), Some(4.0));
7239    }
7240
7241    #[test]
7242    fn band_iterators() {
7243        let cfg = RasterConfig {
7244            cols: 3,
7245            rows: 2,
7246            bands: 2,
7247            nodata: -9999.0,
7248            ..Default::default()
7249        };
7250        let data = vec![
7251            1.0, 2.0, -9999.0, 4.0, 5.0, 6.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0,
7252        ];
7253        let r = Raster::from_data(cfg, data).unwrap();
7254
7255        let valid_b0: Vec<_> = r.iter_valid_band(0).unwrap().collect();
7256        assert_eq!(valid_b0.len(), 5);
7257        assert_eq!(valid_b0[0], (0, 0, 1.0));
7258        assert_eq!(valid_b0[4], (1, 2, 6.0));
7259
7260        let rows_b1: Vec<Vec<f64>> = r.iter_band_rows(1).unwrap().collect();
7261        assert_eq!(rows_b1.len(), 2);
7262        assert_eq!(rows_b1[0], vec![10.0, 20.0, 30.0]);
7263        assert_eq!(rows_b1[1], vec![40.0, 50.0, 60.0]);
7264    }
7265
7266    #[test]
7267    fn mutable_band_rows_native() {
7268        let cfg = RasterConfig {
7269            cols: 3,
7270            rows: 2,
7271            bands: 1,
7272            data_type: DataType::F32,
7273            nodata: -9999.0,
7274            ..Default::default()
7275        };
7276        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
7277        let mut r = Raster::from_data(cfg, data).unwrap();
7278
7279        r.for_each_band_row_mut(0, |row, row_mut| {
7280            if let RasterRowMut::F32(slice) = row_mut {
7281                for v in slice.iter_mut() {
7282                    *v += row as f32;
7283                }
7284            }
7285        })
7286        .unwrap();
7287
7288        assert_eq!(r.get_raw(0, 0, 0), Some(1.0));
7289        assert_eq!(r.get_raw(0, 0, 2), Some(3.0));
7290        assert_eq!(r.get_raw(0, 1, 0), Some(5.0));
7291        assert_eq!(r.get_raw(0, 1, 2), Some(7.0));
7292    }
7293
7294    #[test]
7295    fn immutable_band_rows_native() {
7296        let cfg = RasterConfig {
7297            cols: 3,
7298            rows: 2,
7299            bands: 1,
7300            data_type: DataType::U16,
7301            nodata: 0.0,
7302            ..Default::default()
7303        };
7304        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
7305        let r = Raster::from_data(cfg, data).unwrap();
7306
7307        let mut sums = Vec::new();
7308        r.for_each_band_row(0, |_row, row_ref| {
7309            if let RasterRowRef::U16(slice) = row_ref {
7310                sums.push(slice.iter().map(|v| *v as u64).sum::<u64>());
7311            }
7312        })
7313        .unwrap();
7314
7315        assert_eq!(sums, vec![6, 15]);
7316    }
7317}