Skip to main content

oxigdal_streaming/raster/
writer.rs

1//! Async raster stream writer for large datasets.
2//!
3//! Writes GeoTIFF files by accumulating chunk data and flushing the complete
4//! image on [`RasterStreamWriter::finalize`].
5
6use super::{ChunkStats, RasterChunk, RasterStreamConfig};
7use crate::error::{Result, StreamingError};
8use oxigdal_core::types::RasterMetadata;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::time::Instant;
13use tokio::sync::RwLock;
14use tracing::info;
15
16/// Async raster stream writer.
17///
18/// Accepts chunks via [`write_chunk`](Self::write_chunk) and writes a complete
19/// GeoTIFF when [`finalize`](Self::finalize) is called.
20pub struct RasterStreamWriter {
21    /// Path to the output raster file
22    path: PathBuf,
23
24    /// Stream configuration
25    config: RasterStreamConfig,
26
27    /// Raster metadata
28    metadata: RasterMetadata,
29
30    /// Statistics
31    stats: Arc<RwLock<ChunkStats>>,
32
33    /// Accumulated chunk data indexed by (row, col)
34    chunk_store: Arc<RwLock<HashMap<(usize, usize), RasterChunk>>>,
35
36    /// Total chunks
37    total_chunks: (usize, usize),
38}
39
40impl RasterStreamWriter {
41    /// Create a new raster stream writer.
42    pub async fn new<P: AsRef<Path>>(
43        path: P,
44        metadata: RasterMetadata,
45        config: RasterStreamConfig,
46    ) -> Result<Self> {
47        let path = path.as_ref().to_path_buf();
48
49        // Calculate total chunks
50        let total_chunks = Self::calculate_chunks(
51            metadata.width as usize,
52            metadata.height as usize,
53            config.chunk_size.0,
54            config.chunk_size.1,
55            config.overlap,
56        );
57
58        info!(
59            "Created raster stream writer for {}x{} raster ({} x {} chunks)",
60            metadata.width, metadata.height, total_chunks.0, total_chunks.1
61        );
62
63        Ok(Self {
64            path,
65            config,
66            metadata,
67            stats: Arc::new(RwLock::new(ChunkStats::new())),
68            chunk_store: Arc::new(RwLock::new(HashMap::new())),
69            total_chunks,
70        })
71    }
72
73    /// Calculate the number of chunks needed.
74    pub fn calculate_chunks(
75        width: usize,
76        height: usize,
77        chunk_width: usize,
78        chunk_height: usize,
79        overlap: usize,
80    ) -> (usize, usize) {
81        let effective_chunk_width = chunk_width - overlap;
82        let effective_chunk_height = chunk_height - overlap;
83
84        let num_cols = width.div_ceil(effective_chunk_width);
85        let num_rows = height.div_ceil(effective_chunk_height);
86
87        (num_rows, num_cols)
88    }
89
90    /// Write a chunk to the raster.
91    ///
92    /// Chunks are stored in memory and written to disk on [`finalize`](Self::finalize).
93    pub async fn write_chunk(&self, chunk: RasterChunk) -> Result<()> {
94        let start = Instant::now();
95        let size = chunk.size_bytes();
96        let indices = chunk.indices;
97
98        let mut store = self.chunk_store.write().await;
99        store.insert(indices, chunk);
100        drop(store);
101
102        let elapsed = start.elapsed().as_millis() as u64;
103        let mut stats_guard = self.stats.write().await;
104        stats_guard.record_chunk(size, elapsed);
105
106        Ok(())
107    }
108
109    /// Write multiple chunks.
110    pub async fn write_chunks(&self, chunks: Vec<RasterChunk>) -> Result<()> {
111        for chunk in chunks {
112            self.write_chunk(chunk).await?;
113        }
114        Ok(())
115    }
116
117    /// Get statistics for written chunks.
118    pub async fn stats(&self) -> ChunkStats {
119        self.stats.read().await.clone()
120    }
121
122    /// Flush — no-op for the accumulation model (all data is held in memory).
123    pub async fn flush(&self) -> Result<()> {
124        Ok(())
125    }
126
127    /// Finalize the raster file.
128    ///
129    /// Assembles all accumulated chunks into a contiguous pixel buffer and
130    /// writes a GeoTIFF using the `oxigdal-geotiff` driver.
131    pub async fn finalize(&self) -> Result<()> {
132        let img_width = self.metadata.width as usize;
133        let img_height = self.metadata.height as usize;
134        let band_count = self.metadata.band_count as usize;
135        let data_type = self.metadata.data_type;
136        let bytes_per_sample = data_type.size_bytes();
137        let bytes_per_pixel = bytes_per_sample * band_count;
138        let total_bytes = img_width * img_height * bytes_per_pixel;
139
140        // Assemble chunks into a contiguous image buffer
141        let mut image_data = vec![0u8; total_bytes];
142        let chunk_store = self.chunk_store.read().await;
143
144        let chunk_w = self.config.chunk_size.0;
145        let chunk_h = self.config.chunk_size.1;
146        let overlap = self.config.overlap;
147        let effective_w = chunk_w - overlap;
148        let effective_h = chunk_h - overlap;
149
150        for (&(row, col), chunk) in chunk_store.iter() {
151            let x_start = col * effective_w;
152            let y_start = row * effective_h;
153            let src_width = chunk.buffer.width() as usize / band_count; // actual pixel width
154            let src_height = chunk.buffer.height() as usize;
155            let src_bytes = chunk.buffer.as_bytes();
156
157            for cy in 0..src_height {
158                let dst_y = y_start + cy;
159                if dst_y >= img_height {
160                    break;
161                }
162                for cx in 0..src_width {
163                    let dst_x = x_start + cx;
164                    if dst_x >= img_width {
165                        break;
166                    }
167                    let src_offset = (cy * src_width + cx) * bytes_per_pixel;
168                    let dst_offset = (dst_y * img_width + dst_x) * bytes_per_pixel;
169
170                    if src_offset + bytes_per_pixel <= src_bytes.len()
171                        && dst_offset + bytes_per_pixel <= image_data.len()
172                    {
173                        image_data[dst_offset..dst_offset + bytes_per_pixel]
174                            .copy_from_slice(&src_bytes[src_offset..src_offset + bytes_per_pixel]);
175                    }
176                }
177            }
178        }
179        drop(chunk_store);
180
181        // Write the assembled image to disk
182        let path = self.path.clone();
183        let meta = self.metadata.clone();
184        tokio::task::spawn_blocking(move || Self::write_geotiff_blocking(&path, &meta, &image_data))
185            .await
186            .map_err(|e| StreamingError::Other(format!("finalize task failed: {}", e)))?
187    }
188
189    /// Write a complete image to a GeoTIFF file.
190    fn write_geotiff_blocking(path: &Path, metadata: &RasterMetadata, data: &[u8]) -> Result<()> {
191        use oxigdal_geotiff::tiff::{Compression, PhotometricInterpretation, Predictor};
192        use oxigdal_geotiff::writer::{GeoTiffWriter, GeoTiffWriterOptions, WriterConfig};
193
194        let photometric = if metadata.band_count >= 3 {
195            PhotometricInterpretation::Rgb
196        } else {
197            PhotometricInterpretation::BlackIsZero
198        };
199
200        let mut config = WriterConfig::new(
201            metadata.width,
202            metadata.height,
203            metadata.band_count as u16,
204            metadata.data_type,
205        )
206        .with_compression(Compression::Lzw)
207        .with_predictor(Predictor::HorizontalDifferencing)
208        .with_tile_size(256, 256)
209        .with_photometric(photometric)
210        .with_nodata(metadata.nodata)
211        .with_overviews(false, oxigdal_geotiff::OverviewResampling::Average);
212
213        if let Some(gt) = metadata.geo_transform {
214            config = config.with_geo_transform(gt);
215        }
216
217        let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
218            .map_err(|e| {
219                StreamingError::Other(format!(
220                    "Failed to create GeoTIFF writer for '{}': {}",
221                    path.display(),
222                    e
223                ))
224            })?;
225
226        writer.write(data).map_err(|e| {
227            StreamingError::Other(format!(
228                "Failed to write GeoTIFF data to '{}': {}",
229                path.display(),
230                e
231            ))
232        })?;
233
234        info!("Finalized GeoTIFF: {}", path.display());
235        Ok(())
236    }
237
238    /// Get the total number of chunks.
239    pub fn total_chunks(&self) -> (usize, usize) {
240        self.total_chunks
241    }
242
243    /// Get the current write position (number of chunks written so far).
244    pub async fn current_position(&self) -> (usize, usize) {
245        let store = self.chunk_store.read().await;
246        let count = store.len();
247        drop(store);
248        // Convert linear count to (row, col) for compatibility
249        let cols = self.total_chunks.1.max(1);
250        (count / cols, count % cols)
251    }
252}
253
254/// Builder for configuring a raster stream writer.
255pub struct RasterStreamWriterBuilder {
256    path: PathBuf,
257    metadata: RasterMetadata,
258    config: RasterStreamConfig,
259}
260
261impl RasterStreamWriterBuilder {
262    /// Create a new builder.
263    pub fn new<P: AsRef<Path>>(path: P, metadata: RasterMetadata) -> Self {
264        Self {
265            path: path.as_ref().to_path_buf(),
266            metadata,
267            config: RasterStreamConfig::default(),
268        }
269    }
270
271    /// Set the chunk size.
272    pub fn chunk_size(mut self, width: usize, height: usize) -> Self {
273        self.config = self.config.with_chunk_size(width, height);
274        self
275    }
276
277    /// Set the overlap size.
278    pub fn overlap(mut self, overlap: usize) -> Self {
279        self.config = self.config.with_overlap(overlap);
280        self
281    }
282
283    /// Enable compression.
284    pub fn compression(mut self, level: u8) -> Self {
285        self.config = self.config.with_compression(level);
286        self
287    }
288
289    /// Set the number of parallel workers.
290    pub fn parallel(mut self, num_workers: usize) -> Self {
291        self.config = self.config.with_parallel(true, num_workers);
292        self
293    }
294
295    /// Build the writer.
296    pub async fn build(self) -> Result<RasterStreamWriter> {
297        RasterStreamWriter::new(self.path, self.metadata, self.config).await
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use oxigdal_core::types::{GeoTransform, NoDataValue, PixelLayout, RasterDataType};
305    use std::env;
306
307    #[tokio::test]
308    async fn test_writer_creation() {
309        let temp_dir = env::temp_dir();
310        let test_path = temp_dir.join("test_output.tif");
311
312        let metadata = RasterMetadata {
313            width: 1024,
314            height: 1024,
315            band_count: 1,
316            data_type: RasterDataType::Float32,
317            geo_transform: Some(GeoTransform {
318                origin_x: 0.0,
319                origin_y: 0.0,
320                pixel_width: 1.0,
321                pixel_height: -1.0,
322                row_rotation: 0.0,
323                col_rotation: 0.0,
324            }),
325            crs_wkt: None,
326            nodata: NoDataValue::None,
327            color_interpretation: Vec::new(),
328            layout: PixelLayout::default(),
329            driver_metadata: Vec::new(),
330            statistics: None,
331        };
332
333        let result = RasterStreamWriterBuilder::new(&test_path, metadata)
334            .chunk_size(256, 256)
335            .compression(6)
336            .parallel(4)
337            .build()
338            .await;
339
340        assert!(result.is_ok());
341    }
342
343    #[tokio::test]
344    async fn test_chunk_calculation() {
345        let chunks = RasterStreamWriter::calculate_chunks(1024, 1024, 256, 256, 0);
346        assert_eq!(chunks, (4, 4));
347
348        let chunks = RasterStreamWriter::calculate_chunks(1000, 1000, 256, 256, 0);
349        assert_eq!(chunks, (4, 4));
350    }
351}