Skip to main content

oxigdal_vrt/
builder.rs

1//! VRT builder API for fluent VRT creation
2
3use crate::band::VrtBand;
4use crate::dataset::VrtDataset;
5use crate::error::{Result, VrtError};
6use crate::source::{PixelRect, SourceWindow, VrtSource};
7use crate::xml::VrtXmlWriter;
8use oxigdal_core::types::{GeoTransform, RasterDataType};
9use std::path::{Path, PathBuf};
10
11/// VRT builder for creating VRT datasets
12pub struct VrtBuilder {
13    dataset: VrtDataset,
14    auto_calculate_extent: bool,
15    vrt_path: Option<PathBuf>,
16}
17
18impl VrtBuilder {
19    /// Creates a new VRT builder
20    pub fn new() -> Self {
21        Self {
22            dataset: VrtDataset::new(0, 0),
23            auto_calculate_extent: true,
24            vrt_path: None,
25        }
26    }
27
28    /// Creates a VRT builder with specified dimensions
29    pub fn with_size(width: u64, height: u64) -> Self {
30        Self {
31            dataset: VrtDataset::new(width, height),
32            auto_calculate_extent: false,
33            vrt_path: None,
34        }
35    }
36
37    /// Sets the VRT file path (for resolving relative source paths)
38    pub fn with_vrt_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
39        self.vrt_path = Some(path.into());
40        self
41    }
42
43    /// Sets the spatial reference system
44    pub fn with_srs<S: Into<String>>(mut self, srs: S) -> Self {
45        self.dataset = self.dataset.with_srs(srs);
46        self
47    }
48
49    /// Sets the GeoTransform
50    pub fn with_geo_transform(mut self, geo_transform: GeoTransform) -> Self {
51        self.dataset = self.dataset.with_geo_transform(geo_transform);
52        self
53    }
54
55    /// Sets the block size
56    pub fn with_block_size(mut self, width: u32, height: u32) -> Self {
57        self.dataset = self.dataset.with_block_size(width, height);
58        self
59    }
60
61    /// Adds a simple source to a band
62    ///
63    /// # Errors
64    /// Returns an error if the source configuration is invalid
65    pub fn add_source<P: AsRef<Path>>(
66        mut self,
67        path: P,
68        band_num: usize,
69        source_band: usize,
70    ) -> Result<Self> {
71        let source = VrtSource::simple(path.as_ref(), source_band);
72        self.add_source_to_band(band_num, source)?;
73        Ok(self)
74    }
75
76    /// Adds a source with a window to a band
77    ///
78    /// # Errors
79    /// Returns an error if the source configuration is invalid
80    pub fn add_source_with_window<P: AsRef<Path>>(
81        mut self,
82        path: P,
83        band_num: usize,
84        source_band: usize,
85        src_rect: PixelRect,
86        dst_rect: PixelRect,
87    ) -> Result<Self> {
88        let window = SourceWindow::new(src_rect, dst_rect);
89        let source = VrtSource::simple(path.as_ref(), source_band).with_window(window);
90        self.add_source_to_band(band_num, source)?;
91        Ok(self)
92    }
93
94    /// Adds a mosaic tile at a specific position
95    ///
96    /// # Errors
97    /// Returns an error if the tile configuration is invalid
98    pub fn add_tile<P: AsRef<Path>>(
99        mut self,
100        path: P,
101        x_off: u64,
102        y_off: u64,
103        width: u64,
104        height: u64,
105    ) -> Result<Self> {
106        let src_rect = PixelRect::new(0, 0, width, height);
107        let dst_rect = PixelRect::new(x_off, y_off, width, height);
108        let window = SourceWindow::new(src_rect, dst_rect);
109
110        let source = VrtSource::simple(path.as_ref(), 1).with_window(window);
111        self.add_source_to_band(1, source)?;
112        Ok(self)
113    }
114
115    /// Adds a mosaic tile grid (NxM tiles)
116    ///
117    /// # Errors
118    /// Returns an error if the tile configuration is invalid
119    pub fn add_tile_grid<P>(
120        mut self,
121        paths: &[P],
122        tile_width: u64,
123        tile_height: u64,
124        cols: usize,
125    ) -> Result<Self>
126    where
127        P: AsRef<Path>,
128    {
129        for (idx, path) in paths.iter().enumerate() {
130            let col = idx % cols;
131            let row = idx / cols;
132            let x_off = col as u64 * tile_width;
133            let y_off = row as u64 * tile_height;
134
135            let src_rect = PixelRect::new(0, 0, tile_width, tile_height);
136            let dst_rect = PixelRect::new(x_off, y_off, tile_width, tile_height);
137            let window = SourceWindow::new(src_rect, dst_rect);
138
139            let source = VrtSource::simple(path.as_ref(), 1).with_window(window);
140            self.add_source_to_band(1, source)?;
141        }
142
143        Ok(self)
144    }
145
146    /// Adds a full band configuration
147    ///
148    /// # Errors
149    /// Returns an error if the band is invalid
150    pub fn add_band(mut self, band: VrtBand) -> Result<Self> {
151        band.validate()?;
152        self.dataset.add_band(band);
153        Ok(self)
154    }
155
156    /// Sets the dataset dimensions explicitly
157    pub fn set_dimensions(mut self, width: u64, height: u64) -> Self {
158        self.dataset.raster_x_size = width;
159        self.dataset.raster_y_size = height;
160        self.auto_calculate_extent = false;
161        self
162    }
163
164    /// Builds the VRT dataset
165    ///
166    /// # Errors
167    /// Returns an error if the dataset configuration is invalid
168    pub fn build(mut self) -> Result<VrtDataset> {
169        // Auto-calculate extent if needed
170        if self.auto_calculate_extent && self.dataset.raster_x_size == 0 {
171            self.calculate_extent()?;
172        }
173
174        // Set VRT path if provided
175        if let Some(path) = self.vrt_path {
176            self.dataset.vrt_path = Some(path);
177        }
178
179        // Validate the dataset
180        self.dataset.validate()?;
181
182        Ok(self.dataset)
183    }
184
185    /// Builds and writes the VRT to a file
186    ///
187    /// # Errors
188    /// Returns an error if building or writing fails
189    pub fn build_file<P: AsRef<Path>>(mut self, path: P) -> Result<VrtDataset> {
190        // Set VRT path for relative source resolution
191        let path = path.as_ref();
192        self.vrt_path = Some(path.to_path_buf());
193
194        let dataset = self.build()?;
195        VrtXmlWriter::write_file(&dataset, path)?;
196        Ok(dataset)
197    }
198
199    /// Helper to add a source to a specific band
200    fn add_source_to_band(&mut self, band_num: usize, source: VrtSource) -> Result<()> {
201        source.validate()?;
202
203        // Find or create the band
204        let band_idx = band_num - 1;
205        while self.dataset.bands.len() <= band_idx {
206            let new_band_num = self.dataset.bands.len() + 1;
207            let data_type = source
208                .data_type
209                .or_else(|| source.properties.as_ref().map(|p| p.data_type))
210                .unwrap_or(RasterDataType::UInt8);
211            self.dataset
212                .bands
213                .push(VrtBand::new(new_band_num, data_type));
214        }
215
216        if let Some(band) = self.dataset.get_band_mut(band_idx) {
217            band.add_source(source);
218        }
219
220        Ok(())
221    }
222
223    /// Calculates extent from sources
224    fn calculate_extent(&mut self) -> Result<()> {
225        if self.dataset.bands.is_empty() {
226            return Err(VrtError::invalid_structure(
227                "No bands to calculate extent from",
228            ));
229        }
230
231        let mut max_x = 0u64;
232        let mut max_y = 0u64;
233
234        for band in &self.dataset.bands {
235            for source in &band.sources {
236                if let Some(dst_rect) = source.dst_rect() {
237                    max_x = max_x.max(dst_rect.x_off + dst_rect.x_size);
238                    max_y = max_y.max(dst_rect.y_off + dst_rect.y_size);
239                } else if let Some(ref props) = source.properties {
240                    max_x = max_x.max(props.width);
241                    max_y = max_y.max(props.height);
242                }
243            }
244        }
245
246        if max_x == 0 || max_y == 0 {
247            return Err(VrtError::invalid_structure(
248                "Cannot calculate extent from sources",
249            ));
250        }
251
252        self.dataset.raster_x_size = max_x;
253        self.dataset.raster_y_size = max_y;
254
255        Ok(())
256    }
257}
258
259impl Default for VrtBuilder {
260    fn default() -> Self {
261        Self::new()
262    }
263}
264
265/// Convenience builder for simple mosaic VRTs
266pub struct MosaicBuilder {
267    builder: VrtBuilder,
268    tile_width: u64,
269    tile_height: u64,
270    current_x: u64,
271    current_y: u64,
272    max_x: u64,
273    max_y: u64,
274}
275
276impl MosaicBuilder {
277    /// Creates a new mosaic builder
278    pub fn new(tile_width: u64, tile_height: u64) -> Self {
279        Self {
280            builder: VrtBuilder::new(),
281            tile_width,
282            tile_height,
283            current_x: 0,
284            current_y: 0,
285            max_x: 0,
286            max_y: 0,
287        }
288    }
289
290    /// Adds a tile at the current position and advances
291    ///
292    /// # Errors
293    /// Returns an error if the tile cannot be added
294    pub fn add_tile<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
295        let src_rect = PixelRect::new(0, 0, self.tile_width, self.tile_height);
296        let dst_rect = PixelRect::new(
297            self.current_x,
298            self.current_y,
299            self.tile_width,
300            self.tile_height,
301        );
302        let window = SourceWindow::new(src_rect, dst_rect);
303
304        let source = VrtSource::simple(path.as_ref(), 1).with_window(window);
305        self.builder.add_source_to_band(1, source)?;
306
307        // Update extent tracking
308        self.max_x = self.max_x.max(self.current_x + self.tile_width);
309        self.max_y = self.max_y.max(self.current_y + self.tile_height);
310
311        Ok(self)
312    }
313
314    /// Adds a tile at a specific position
315    ///
316    /// # Errors
317    /// Returns an error if the tile cannot be added
318    pub fn add_tile_at<P: AsRef<Path>>(mut self, path: P, x: u64, y: u64) -> Result<Self> {
319        let src_rect = PixelRect::new(0, 0, self.tile_width, self.tile_height);
320        let dst_rect = PixelRect::new(x, y, self.tile_width, self.tile_height);
321        let window = SourceWindow::new(src_rect, dst_rect);
322
323        let source = VrtSource::simple(path.as_ref(), 1).with_window(window);
324        self.builder.add_source_to_band(1, source)?;
325
326        // Update extent tracking
327        self.max_x = self.max_x.max(x + self.tile_width);
328        self.max_y = self.max_y.max(y + self.tile_height);
329
330        Ok(self)
331    }
332
333    /// Moves to the next column
334    pub fn next_column(mut self) -> Self {
335        self.current_x += self.tile_width;
336        self
337    }
338
339    /// Moves to the next row
340    pub fn next_row(mut self) -> Self {
341        self.current_x = 0;
342        self.current_y += self.tile_height;
343        self
344    }
345
346    /// Sets the current position
347    pub fn at(mut self, x: u64, y: u64) -> Self {
348        self.current_x = x;
349        self.current_y = y;
350        self
351    }
352
353    /// Sets the spatial reference system
354    pub fn with_srs<S: Into<String>>(mut self, srs: S) -> Self {
355        self.builder = self.builder.with_srs(srs);
356        self
357    }
358
359    /// Sets the GeoTransform
360    pub fn with_geo_transform(mut self, geo_transform: GeoTransform) -> Self {
361        self.builder = self.builder.with_geo_transform(geo_transform);
362        self
363    }
364
365    /// Builds the mosaic VRT
366    ///
367    /// # Errors
368    /// Returns an error if building fails
369    pub fn build(mut self) -> Result<VrtDataset> {
370        // Set dimensions based on extent
371        self.builder = self.builder.set_dimensions(self.max_x, self.max_y);
372        self.builder.build()
373    }
374
375    /// Builds and writes the mosaic VRT to a file
376    ///
377    /// # Errors
378    /// Returns an error if building or writing fails
379    pub fn build_file<P: AsRef<Path>>(mut self, path: P) -> Result<VrtDataset> {
380        // Set dimensions based on extent
381        self.builder = self.builder.set_dimensions(self.max_x, self.max_y);
382        self.builder.build_file(path)
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_vrt_builder() {
392        let builder = VrtBuilder::with_size(512, 512);
393        let result = builder.add_source("/test1.tif", 1, 1);
394
395        assert!(result.is_ok());
396        let builder = result.expect("Should add source");
397        let dataset = builder.build();
398        assert!(dataset.is_ok());
399    }
400
401    #[test]
402    fn test_mosaic_builder() {
403        let builder = MosaicBuilder::new(256, 256);
404        let result = builder.add_tile("/tile1.tif");
405
406        assert!(result.is_ok());
407        let builder = result.expect("Should add tile");
408        let result = builder.next_column().add_tile("/tile2.tif");
409        assert!(result.is_ok());
410
411        let builder = result.expect("Should add tile");
412        let dataset = builder.build();
413        assert!(dataset.is_ok());
414        let ds = dataset.expect("Should build");
415        assert_eq!(ds.band_count(), 1);
416    }
417
418    #[test]
419    fn test_tile_grid() {
420        let paths = vec!["/tile1.tif", "/tile2.tif", "/tile3.tif", "/tile4.tif"];
421        let builder = VrtBuilder::new();
422        let result = builder.add_tile_grid(&paths, 256, 256, 2);
423
424        assert!(result.is_ok());
425        let builder = result.expect("Should add tiles");
426        let result = builder.set_dimensions(512, 512).build();
427        assert!(result.is_ok());
428    }
429
430    #[test]
431    fn test_geo_transform() {
432        let gt = GeoTransform {
433            origin_x: 0.0,
434            pixel_width: 1.0,
435            row_rotation: 0.0,
436            origin_y: 0.0,
437            col_rotation: 0.0,
438            pixel_height: -1.0,
439        };
440
441        let builder = VrtBuilder::with_size(512, 512);
442        let result = builder
443            .with_geo_transform(gt)
444            .with_srs("EPSG:4326")
445            .add_source("/test.tif", 1, 1);
446
447        assert!(result.is_ok());
448        let builder = result.expect("Should configure");
449        let dataset = builder.build();
450        assert!(dataset.is_ok());
451        let ds = dataset.expect("Should build");
452        assert!(ds.geo_transform.is_some());
453        assert!(ds.srs.is_some());
454    }
455}