Skip to main content

oxigdal_vrt/
source.rs

1//! VRT source raster references and windowing
2
3use crate::error::{Result, VrtError};
4use oxigdal_core::types::{GeoTransform, NoDataValue, RasterDataType};
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7
8/// Source filename with path resolution
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct SourceFilename {
11    /// Path to source file (can be relative or absolute)
12    pub path: PathBuf,
13    /// Whether the path is relative to the VRT file
14    pub relative_to_vrt: bool,
15    /// Shared flag (for optimization hints)
16    pub shared: bool,
17}
18
19impl SourceFilename {
20    /// Creates a new source filename
21    pub fn new<P: AsRef<Path>>(path: P, relative_to_vrt: bool) -> Self {
22        Self {
23            path: path.as_ref().to_path_buf(),
24            relative_to_vrt,
25            shared: false,
26        }
27    }
28
29    /// Creates a new absolute source filename
30    pub fn absolute<P: AsRef<Path>>(path: P) -> Self {
31        Self::new(path, false)
32    }
33
34    /// Creates a new relative source filename
35    pub fn relative<P: AsRef<Path>>(path: P) -> Self {
36        Self::new(path, true)
37    }
38
39    /// Sets the shared flag
40    pub fn with_shared(mut self, shared: bool) -> Self {
41        self.shared = shared;
42        self
43    }
44
45    /// Resolves the path relative to a VRT file
46    ///
47    /// # Errors
48    /// Returns an error if path resolution fails
49    pub fn resolve<P: AsRef<Path>>(&self, vrt_path: P) -> Result<PathBuf> {
50        if self.relative_to_vrt {
51            let vrt_dir = vrt_path.as_ref().parent().ok_or_else(|| {
52                VrtError::path_resolution(
53                    self.path.display().to_string(),
54                    "VRT path has no parent directory",
55                )
56            })?;
57            Ok(vrt_dir.join(&self.path))
58        } else {
59            Ok(self.path.clone())
60        }
61    }
62}
63
64/// Rectangle in pixel space
65#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
66pub struct PixelRect {
67    /// X offset in pixels
68    pub x_off: u64,
69    /// Y offset in pixels
70    pub y_off: u64,
71    /// Width in pixels
72    pub x_size: u64,
73    /// Height in pixels
74    pub y_size: u64,
75}
76
77impl PixelRect {
78    /// Creates a new pixel rectangle
79    pub fn new(x_off: u64, y_off: u64, x_size: u64, y_size: u64) -> Self {
80        Self {
81            x_off,
82            y_off,
83            x_size,
84            y_size,
85        }
86    }
87
88    /// Checks if the rectangle is valid
89    pub fn is_valid(&self) -> bool {
90        self.x_size > 0 && self.y_size > 0
91    }
92
93    /// Checks if a point is contained in this rectangle
94    pub fn contains(&self, x: u64, y: u64) -> bool {
95        x >= self.x_off
96            && x < self.x_off.saturating_add(self.x_size)
97            && y >= self.y_off
98            && y < self.y_off.saturating_add(self.y_size)
99    }
100
101    /// Computes the intersection with another rectangle
102    pub fn intersect(&self, other: &Self) -> Option<Self> {
103        let x1 = self.x_off.max(other.x_off);
104        let y1 = self.y_off.max(other.y_off);
105        let x2 = (self.x_off + self.x_size).min(other.x_off + other.x_size);
106        let y2 = (self.y_off + self.y_size).min(other.y_off + other.y_size);
107
108        if x2 > x1 && y2 > y1 {
109            Some(Self::new(x1, y1, x2 - x1, y2 - y1))
110        } else {
111            None
112        }
113    }
114
115    /// Checks if this rectangle intersects with another
116    pub fn intersects(&self, other: &Self) -> bool {
117        self.intersect(other).is_some()
118    }
119}
120
121/// Source window configuration
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct SourceWindow {
124    /// Source rectangle (in source pixel space)
125    pub src_rect: PixelRect,
126    /// Destination rectangle (in VRT pixel space)
127    pub dst_rect: PixelRect,
128}
129
130impl SourceWindow {
131    /// Creates a new source window
132    pub fn new(src_rect: PixelRect, dst_rect: PixelRect) -> Self {
133        Self { src_rect, dst_rect }
134    }
135
136    /// Creates a simple identity window
137    pub fn identity(width: u64, height: u64) -> Self {
138        let rect = PixelRect::new(0, 0, width, height);
139        Self::new(rect, rect)
140    }
141
142    /// Validates the window configuration
143    ///
144    /// # Errors
145    /// Returns an error if the window is invalid
146    pub fn validate(&self) -> Result<()> {
147        if !self.src_rect.is_valid() {
148            return Err(VrtError::invalid_window("Source rectangle is invalid"));
149        }
150        if !self.dst_rect.is_valid() {
151            return Err(VrtError::invalid_window("Destination rectangle is invalid"));
152        }
153        Ok(())
154    }
155}
156
157/// VRT source configuration
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct VrtSource {
160    /// Source filename
161    pub filename: SourceFilename,
162    /// Source band (1-based index in source file)
163    pub source_band: usize,
164    /// Source window (optional, defaults to full extent)
165    pub window: Option<SourceWindow>,
166    /// NoData value override
167    pub nodata: Option<NoDataValue>,
168    /// Data type override
169    pub data_type: Option<RasterDataType>,
170    /// Source properties (cached metadata)
171    pub properties: Option<SourceProperties>,
172}
173
174impl VrtSource {
175    /// Creates a new VRT source
176    pub fn new(filename: SourceFilename, source_band: usize) -> Self {
177        Self {
178            filename,
179            source_band,
180            window: None,
181            nodata: None,
182            data_type: None,
183            properties: None,
184        }
185    }
186
187    /// Creates a simple VRT source with default settings
188    pub fn simple<P: AsRef<Path>>(path: P, band: usize) -> Self {
189        Self::new(SourceFilename::absolute(path), band)
190    }
191
192    /// Sets the source window
193    pub fn with_window(mut self, window: SourceWindow) -> Self {
194        self.window = Some(window);
195        self
196    }
197
198    /// Sets the NoData value override
199    pub fn with_nodata(mut self, nodata: NoDataValue) -> Self {
200        self.nodata = Some(nodata);
201        self
202    }
203
204    /// Sets the data type override
205    pub fn with_data_type(mut self, data_type: RasterDataType) -> Self {
206        self.data_type = Some(data_type);
207        self
208    }
209
210    /// Sets the source properties
211    pub fn with_properties(mut self, properties: SourceProperties) -> Self {
212        self.properties = Some(properties);
213        self
214    }
215
216    /// Validates the source configuration
217    ///
218    /// # Errors
219    /// Returns an error if the source is invalid
220    pub fn validate(&self) -> Result<()> {
221        if self.source_band == 0 {
222            return Err(VrtError::invalid_source("Source band must be >= 1"));
223        }
224
225        if let Some(ref window) = self.window {
226            window.validate()?;
227        }
228
229        Ok(())
230    }
231
232    /// Gets the destination rectangle in VRT pixel space
233    pub fn dst_rect(&self) -> Option<PixelRect> {
234        self.window.as_ref().map(|w| w.dst_rect)
235    }
236
237    /// Gets the source rectangle in source pixel space
238    pub fn src_rect(&self) -> Option<PixelRect> {
239        self.window.as_ref().map(|w| w.src_rect)
240    }
241}
242
243/// Cached source properties
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct SourceProperties {
246    /// Raster width
247    pub width: u64,
248    /// Raster height
249    pub height: u64,
250    /// Number of bands
251    pub band_count: usize,
252    /// Data type
253    pub data_type: RasterDataType,
254    /// GeoTransform
255    pub geo_transform: Option<GeoTransform>,
256    /// NoData value
257    pub nodata: NoDataValue,
258}
259
260impl SourceProperties {
261    /// Creates new source properties
262    pub fn new(width: u64, height: u64, band_count: usize, data_type: RasterDataType) -> Self {
263        Self {
264            width,
265            height,
266            band_count,
267            data_type,
268            geo_transform: None,
269            nodata: NoDataValue::None,
270        }
271    }
272
273    /// Sets the GeoTransform
274    pub fn with_geo_transform(mut self, geo_transform: GeoTransform) -> Self {
275        self.geo_transform = Some(geo_transform);
276        self
277    }
278
279    /// Sets the NoData value
280    pub fn with_nodata(mut self, nodata: NoDataValue) -> Self {
281        self.nodata = nodata;
282        self
283    }
284
285    /// Validates that the source band exists
286    ///
287    /// # Errors
288    /// Returns an error if the band is out of range
289    pub fn validate_band(&self, band: usize) -> Result<()> {
290        if band == 0 || band > self.band_count {
291            return Err(VrtError::band_out_of_range(band, self.band_count));
292        }
293        Ok(())
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_source_filename() {
303        let filename = SourceFilename::absolute("/path/to/file.tif");
304        assert_eq!(filename.path, PathBuf::from("/path/to/file.tif"));
305        assert!(!filename.relative_to_vrt);
306
307        let filename = SourceFilename::relative("data/file.tif");
308        assert!(filename.relative_to_vrt);
309    }
310
311    #[test]
312    fn test_pixel_rect() {
313        let rect = PixelRect::new(10, 20, 100, 200);
314        assert!(rect.is_valid());
315        assert!(rect.contains(10, 20));
316        assert!(rect.contains(50, 100));
317        assert!(!rect.contains(5, 20));
318        assert!(!rect.contains(200, 20));
319    }
320
321    #[test]
322    fn test_rect_intersection() {
323        let rect1 = PixelRect::new(0, 0, 100, 100);
324        let rect2 = PixelRect::new(50, 50, 100, 100);
325
326        let intersection = rect1.intersect(&rect2);
327        assert!(intersection.is_some());
328        let inter = intersection.expect("Should have intersection");
329        assert_eq!(inter.x_off, 50);
330        assert_eq!(inter.y_off, 50);
331        assert_eq!(inter.x_size, 50);
332        assert_eq!(inter.y_size, 50);
333
334        let rect3 = PixelRect::new(200, 200, 100, 100);
335        assert!(rect1.intersect(&rect3).is_none());
336    }
337
338    #[test]
339    fn test_source_window() {
340        let src_rect = PixelRect::new(0, 0, 512, 512);
341        let dst_rect = PixelRect::new(100, 100, 512, 512);
342        let window = SourceWindow::new(src_rect, dst_rect);
343
344        assert!(window.validate().is_ok());
345    }
346
347    #[test]
348    fn test_vrt_source() {
349        let source = VrtSource::simple("/path/to/file.tif", 1);
350        assert_eq!(source.source_band, 1);
351        assert!(source.validate().is_ok());
352
353        let invalid_source = VrtSource::simple("/path/to/file.tif", 0);
354        assert!(invalid_source.validate().is_err());
355    }
356
357    #[test]
358    fn test_source_properties() {
359        let props = SourceProperties::new(512, 512, 3, RasterDataType::UInt8);
360        assert_eq!(props.width, 512);
361        assert_eq!(props.band_count, 3);
362
363        assert!(props.validate_band(1).is_ok());
364        assert!(props.validate_band(3).is_ok());
365        assert!(props.validate_band(0).is_err());
366        assert!(props.validate_band(4).is_err());
367    }
368}