Skip to main content

oxigdal_algorithms/raster/streaming/
chunk.rs

1/// Error type for streaming raster operations.
2#[derive(Debug, Clone, PartialEq)]
3pub enum RasterError {
4    OutOfBounds,
5    InvalidChunkSize,
6    IoError(String),
7    /// Wraps an error from core algorithm operations
8    AlgorithmError(String),
9}
10
11impl std::fmt::Display for RasterError {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Self::OutOfBounds => write!(f, "raster read out of bounds"),
15            Self::InvalidChunkSize => write!(f, "chunk size must be > 0"),
16            Self::IoError(s) => write!(f, "IO error: {}", s),
17            Self::AlgorithmError(s) => write!(f, "algorithm error: {}", s),
18        }
19    }
20}
21
22impl std::error::Error for RasterError {}
23
24/// Trait for raster data sources that can be read in rectangular windows.
25///
26/// Implementations must provide row-major f32 data for any sub-window.
27/// Out-of-bounds accesses (when x + w > width or y + h > height) must be
28/// zero-padded rather than returning an error, to support halo reads at
29/// raster boundaries.
30pub trait RasterSource {
31    /// Total width of the raster in pixels.
32    fn width(&self) -> usize;
33
34    /// Total height of the raster in pixels.
35    fn height(&self) -> usize;
36
37    /// Read a rectangular window of pixels into a `Vec<f32>`, row-major order.
38    ///
39    /// `(x, y)` is the top-left corner of the window.  The returned vector
40    /// always has exactly `w * h` elements.  Pixels that fall outside the
41    /// raster extent are returned as `0.0` (zero-padding for halo support).
42    ///
43    /// Returns `Err(RasterError::OutOfBounds)` when the *origin* `(x, y)` is
44    /// completely outside the raster (i.e. `x >= width || y >= height`).
45    fn read_window(&self, x: usize, y: usize, w: usize, h: usize) -> Result<Vec<f32>, RasterError>;
46}
47
48/// In-memory raster source backed by a `Vec<f32>` for testing and small workloads.
49///
50/// This implementation stores raster data in a flat row-major `Vec<f32>` and
51/// supports efficient sub-window reads with zero-padding for out-of-bounds pixels.
52pub struct InMemoryRasterSource {
53    data: Vec<f32>,
54    width: usize,
55    height: usize,
56}
57
58impl InMemoryRasterSource {
59    /// Creates a new in-memory raster source.
60    ///
61    /// # Panics
62    ///
63    /// Panics if `data.len() != width * height`.
64    pub fn new(data: Vec<f32>, width: usize, height: usize) -> Self {
65        assert_eq!(
66            data.len(),
67            width * height,
68            "data length must equal width * height"
69        );
70        Self {
71            data,
72            width,
73            height,
74        }
75    }
76
77    /// Returns a reference to the underlying data slice.
78    pub fn data(&self) -> &[f32] {
79        &self.data
80    }
81}
82
83impl RasterSource for InMemoryRasterSource {
84    fn width(&self) -> usize {
85        self.width
86    }
87
88    fn height(&self) -> usize {
89        self.height
90    }
91
92    fn read_window(&self, x: usize, y: usize, w: usize, h: usize) -> Result<Vec<f32>, RasterError> {
93        // Reject origins that are entirely outside the raster.
94        if x >= self.width || y >= self.height {
95            return Err(RasterError::OutOfBounds);
96        }
97
98        let mut out = Vec::with_capacity(w * h);
99        for row in 0..h {
100            for col in 0..w {
101                let src_x = x + col;
102                let src_y = y + row;
103                if src_x < self.width && src_y < self.height {
104                    out.push(self.data[src_y * self.width + src_x]);
105                } else {
106                    // Zero-pad pixels outside the raster extent (halo edge handling).
107                    out.push(0.0_f32);
108                }
109            }
110        }
111        Ok(out)
112    }
113}
114
115/// A chunk of raster data extracted from a `RasterSource`, including surrounding
116/// halo (ghost/overlap) cells.
117///
118/// The `data` field holds `(width + 2 * halo) × (height + 2 * halo)` pixels
119/// in row-major order.  Coordinate `(0, 0)` in `data` corresponds to raster
120/// position `(x.saturating_sub(halo), y.saturating_sub(halo))`; pixels that
121/// would fall outside the source raster are zero-padded.
122pub struct Chunk {
123    /// Top-left x of the **core** region (without halo) in source coordinates.
124    pub x: usize,
125    /// Top-left y of the **core** region (without halo) in source coordinates.
126    pub y: usize,
127    /// Width of the **core** region (pixels, without halo columns).
128    pub width: usize,
129    /// Height of the **core** region (pixels, without halo rows).
130    pub height: usize,
131    /// Halo size on each of the four sides (in pixels).
132    pub halo: usize,
133    /// Row-major data of size `(width + 2*halo) × (height + 2*halo)`.
134    ///
135    /// Row 0 and row `height + 2*halo - 1` are halo rows; column 0 and
136    /// column `width + 2*halo - 1` are halo columns.  The origin `(0, 0)`
137    /// maps to source position `(x.saturating_sub(halo), y.saturating_sub(halo))`.
138    pub data: Vec<f32>,
139}
140
141impl Chunk {
142    /// Returns the value at `(col, row)` within the **core** region, applying
143    /// the halo offset transparently.
144    ///
145    /// `col` must be in `0..self.width` and `row` in `0..self.height`.
146    #[inline]
147    pub fn get_core(&self, col: usize, row: usize) -> f32 {
148        let full_w = self.width + 2 * self.halo;
149        self.data[(row + self.halo) * full_w + (col + self.halo)]
150    }
151
152    /// Returns the full width of `data` (core width + 2 * halo).
153    #[inline]
154    pub fn full_width(&self) -> usize {
155        self.width + 2 * self.halo
156    }
157
158    /// Returns the full height of `data` (core height + 2 * halo).
159    #[inline]
160    pub fn full_height(&self) -> usize {
161        self.height + 2 * self.halo
162    }
163}