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}