oxigdal_streaming/raster/
writer.rs1use 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
16pub struct RasterStreamWriter {
21 path: PathBuf,
23
24 config: RasterStreamConfig,
26
27 metadata: RasterMetadata,
29
30 stats: Arc<RwLock<ChunkStats>>,
32
33 chunk_store: Arc<RwLock<HashMap<(usize, usize), RasterChunk>>>,
35
36 total_chunks: (usize, usize),
38}
39
40impl RasterStreamWriter {
41 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 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 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 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 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 pub async fn stats(&self) -> ChunkStats {
119 self.stats.read().await.clone()
120 }
121
122 pub async fn flush(&self) -> Result<()> {
124 Ok(())
125 }
126
127 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 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; 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 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 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 pub fn total_chunks(&self) -> (usize, usize) {
240 self.total_chunks
241 }
242
243 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 let cols = self.total_chunks.1.max(1);
250 (count / cols, count % cols)
251 }
252}
253
254pub struct RasterStreamWriterBuilder {
256 path: PathBuf,
257 metadata: RasterMetadata,
258 config: RasterStreamConfig,
259}
260
261impl RasterStreamWriterBuilder {
262 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 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 pub fn overlap(mut self, overlap: usize) -> Self {
279 self.config = self.config.with_overlap(overlap);
280 self
281 }
282
283 pub fn compression(mut self, level: u8) -> Self {
285 self.config = self.config.with_compression(level);
286 self
287 }
288
289 pub fn parallel(mut self, num_workers: usize) -> Self {
291 self.config = self.config.with_parallel(true, num_workers);
292 self
293 }
294
295 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}