Skip to main content

oxigdal_streaming/raster/reader/
mod.rs

1//! Async raster stream reader for large datasets.
2//!
3//! Reads real GeoTIFF files via the `oxigdal-geotiff` driver, streaming
4//! data in configurable chunks.
5
6use super::{RasterChunk, RasterStream, RasterStreamConfig, RasterStreaming};
7use crate::error::{Result, StreamingError};
8use async_trait::async_trait;
9use oxigdal_core::{
10    buffer::RasterBuffer,
11    io::FileDataSource,
12    types::{BoundingBox, GeoTransform, RasterMetadata},
13};
14use oxigdal_geotiff::GeoTiffReader;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use tokio::sync::Semaphore;
18use tokio::task;
19use tracing::{debug, error, info};
20
21/// Supported raster formats detected from file extension or magic bytes.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RasterFormat {
24    /// GeoTIFF / Cloud Optimized GeoTIFF
25    GeoTiff,
26}
27
28/// Detects the raster format from file extension.
29pub(crate) fn detect_format_from_extension(path: &Path) -> Option<RasterFormat> {
30    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
31    match ext.as_str() {
32        "tif" | "tiff" | "geotiff" | "gtiff" => Some(RasterFormat::GeoTiff),
33        _ => None,
34    }
35}
36
37/// Detects the raster format from magic bytes.
38pub(crate) fn detect_format_from_magic(path: &Path) -> Option<RasterFormat> {
39    let data = std::fs::read(path).ok()?;
40    if data.len() >= 4 && oxigdal_geotiff::is_tiff(&data[..4.min(data.len())]) {
41        return Some(RasterFormat::GeoTiff);
42    }
43    None
44}
45
46/// Detects the raster format from a file path using both extension and magic bytes.
47pub(crate) fn detect_format(path: &Path) -> Result<RasterFormat> {
48    if let Some(fmt) = detect_format_from_extension(path) {
49        return Ok(fmt);
50    }
51    if let Some(fmt) = detect_format_from_magic(path) {
52        return Ok(fmt);
53    }
54    Err(StreamingError::Other(format!(
55        "Unsupported raster format for file: {}",
56        path.display()
57    )))
58}
59
60/// Async raster stream reader.
61///
62/// Reads real GeoTIFF files, extracting metadata from IFD tags and
63/// reading actual tile/strip data for each chunk.
64pub struct RasterStreamReader {
65    /// Path to the raster file
66    path: PathBuf,
67
68    /// Stream configuration
69    config: RasterStreamConfig,
70
71    /// Raster metadata
72    metadata: RasterMetadata,
73
74    /// The underlying stream
75    stream: Option<RasterStream>,
76
77    /// Prefetch semaphore for limiting concurrent operations
78    prefetch_semaphore: Arc<Semaphore>,
79
80    /// Band indices to read
81    bands: Vec<usize>,
82
83    /// Detected file format
84    format: RasterFormat,
85}
86
87impl RasterStreamReader {
88    /// Create a new raster stream reader.
89    ///
90    /// Opens the GeoTIFF file and reads its metadata (dimensions, data type,
91    /// geotransform, CRS, nodata value) from the TIFF IFD.
92    pub async fn new<P: AsRef<Path>>(path: P, config: RasterStreamConfig) -> Result<Self> {
93        let path = path.as_ref().to_path_buf();
94
95        // Validate file exists
96        if !path.exists() {
97            return Err(StreamingError::Io(std::io::Error::new(
98                std::io::ErrorKind::NotFound,
99                format!("File not found: {}", path.display()),
100            )));
101        }
102
103        // Detect format
104        let format = detect_format(&path)?;
105
106        // Read metadata from the file
107        let metadata = Self::read_metadata_from_file(&path, format).await?;
108
109        info!(
110            "Created raster stream reader for {}x{} raster with {} bands ({})",
111            metadata.width,
112            metadata.height,
113            metadata.band_count,
114            path.display()
115        );
116
117        let prefetch_semaphore = Arc::new(Semaphore::new(config.prefetch_count));
118
119        Ok(Self {
120            path,
121            config,
122            metadata,
123            stream: None,
124            prefetch_semaphore,
125            bands: vec![0], // Default to first band
126            format,
127        })
128    }
129
130    /// Read metadata from a raster file using the GeoTIFF driver.
131    async fn read_metadata_from_file(path: &Path, format: RasterFormat) -> Result<RasterMetadata> {
132        let path = path.to_path_buf();
133        task::spawn_blocking(move || match format {
134            RasterFormat::GeoTiff => Self::read_geotiff_metadata(&path),
135        })
136        .await
137        .map_err(|e| StreamingError::Other(format!("Task join error: {}", e)))?
138    }
139
140    /// Read metadata from a GeoTIFF file using the oxigdal-geotiff driver.
141    fn read_geotiff_metadata(path: &Path) -> Result<RasterMetadata> {
142        let source = FileDataSource::open(path).map_err(|e| {
143            StreamingError::Other(format!(
144                "Failed to open GeoTIFF file '{}': {}",
145                path.display(),
146                e
147            ))
148        })?;
149
150        let reader = GeoTiffReader::open(source).map_err(|e| {
151            StreamingError::Other(format!(
152                "Failed to parse GeoTIFF '{}': {}",
153                path.display(),
154                e
155            ))
156        })?;
157
158        Ok(reader.metadata())
159    }
160
161    /// Set which bands to read.
162    pub fn with_bands(mut self, bands: Vec<usize>) -> Self {
163        self.bands = bands;
164        self
165    }
166
167    /// Start the streaming process.
168    pub async fn start(&mut self) -> Result<()> {
169        let stream = RasterStream::new(self.config.clone(), self.metadata.clone())?;
170
171        // Start prefetch workers if enabled
172        if self.config.parallel {
173            self.start_prefetch_workers().await?;
174        }
175
176        self.stream = Some(stream);
177        Ok(())
178    }
179
180    /// Start prefetch workers for parallel chunk loading.
181    async fn start_prefetch_workers(&self) -> Result<()> {
182        let num_workers = self.config.num_workers;
183
184        for worker_id in 0..num_workers {
185            let _path = self.path.clone();
186            let _config = self.config.clone();
187            let _metadata = self.metadata.clone();
188            let _bands = self.bands.clone();
189            let _semaphore = Arc::clone(&self.prefetch_semaphore);
190
191            tokio::spawn(async move {
192                debug!("Prefetch worker {} started", worker_id);
193                // Workers acquire semaphore permits before reading chunks
194                debug!("Prefetch worker {} finished", worker_id);
195            });
196        }
197
198        Ok(())
199    }
200
201    /// Read a specific chunk from the raster.
202    ///
203    /// This actually reads tile/strip data from the GeoTIFF file for the
204    /// given chunk coordinates.
205    pub async fn read_chunk(&self, row: usize, col: usize) -> Result<RasterChunk> {
206        let _permit = self
207            .prefetch_semaphore
208            .acquire()
209            .await
210            .map_err(|e| StreamingError::Other(e.to_string()))?;
211
212        let path = self.path.clone();
213        let config = self.config.clone();
214        let metadata = self.metadata.clone();
215        let bands = self.bands.clone();
216        let format = self.format;
217
218        task::spawn_blocking(move || {
219            Self::read_chunk_blocking(path, row, col, config, metadata, bands, format)
220        })
221        .await
222        .map_err(|e| StreamingError::Other(e.to_string()))?
223    }
224
225    /// Read a chunk in blocking mode using the real GeoTIFF driver.
226    fn read_chunk_blocking(
227        path: PathBuf,
228        row: usize,
229        col: usize,
230        config: RasterStreamConfig,
231        metadata: RasterMetadata,
232        _bands: Vec<usize>,
233        format: RasterFormat,
234    ) -> Result<RasterChunk> {
235        let chunk_width = config.chunk_size.0;
236        let chunk_height = config.chunk_size.1;
237        let overlap = config.overlap;
238
239        let effective_width = chunk_width.saturating_sub(overlap).max(1);
240        let effective_height = chunk_height.saturating_sub(overlap).max(1);
241
242        let x_start = col * effective_width;
243        let y_start = row * effective_height;
244        let x_end = (x_start + chunk_width).min(metadata.width as usize);
245        let y_end = (y_start + chunk_height).min(metadata.height as usize);
246
247        let actual_width = x_end.saturating_sub(x_start);
248        let actual_height = y_end.saturating_sub(y_start);
249
250        if actual_width == 0 || actual_height == 0 {
251            return Err(StreamingError::InvalidOperation(format!(
252                "Empty chunk at ({}, {}): {}x{}",
253                row, col, actual_width, actual_height
254            )));
255        }
256
257        // Read actual data from the file
258        let buffer = match format {
259            RasterFormat::GeoTiff => Self::read_geotiff_chunk(
260                &path,
261                &metadata,
262                x_start,
263                y_start,
264                actual_width,
265                actual_height,
266            )?,
267        };
268
269        // Calculate bounding box
270        let gt = metadata
271            .geo_transform
272            .as_ref()
273            .ok_or_else(|| StreamingError::InvalidState("No geotransform available".to_string()))?;
274
275        let min_x = gt.origin_x + (x_start as f64) * gt.pixel_width;
276        let max_y = gt.origin_y + (y_start as f64) * gt.pixel_height;
277        let max_x = gt.origin_x + (x_end as f64) * gt.pixel_width;
278        let min_y = gt.origin_y + (y_end as f64) * gt.pixel_height;
279
280        let bbox = BoundingBox::new(min_x, min_y, max_x, max_y).map_err(StreamingError::Core)?;
281
282        // Calculate chunk geotransform
283        let chunk_gt = GeoTransform {
284            origin_x: min_x,
285            origin_y: max_y,
286            pixel_width: gt.pixel_width,
287            pixel_height: gt.pixel_height,
288            row_rotation: gt.row_rotation,
289            col_rotation: gt.col_rotation,
290        };
291
292        Ok(RasterChunk::new(buffer, bbox, chunk_gt, (row, col)))
293    }
294
295    /// Read a rectangular region from a GeoTIFF file by reading relevant
296    /// tiles/strips and extracting the overlapping pixels.
297    fn read_geotiff_chunk(
298        path: &Path,
299        metadata: &RasterMetadata,
300        x_start: usize,
301        y_start: usize,
302        width: usize,
303        height: usize,
304    ) -> Result<RasterBuffer> {
305        let source = FileDataSource::open(path).map_err(|e| {
306            StreamingError::Other(format!("Failed to open GeoTIFF for chunk read: {}", e))
307        })?;
308
309        let reader = GeoTiffReader::open(source).map_err(|e| {
310            StreamingError::Other(format!("Failed to parse GeoTIFF for chunk read: {}", e))
311        })?;
312
313        let info = reader.metadata();
314        let data_type = info.data_type;
315        let bytes_per_pixel = data_type.size_bytes() * info.band_count as usize;
316        let img_width = info.width as usize;
317        let img_height = info.height as usize;
318
319        // Determine tile/strip layout
320        let geotiff_info = reader.metadata();
321        let (tile_w, tile_h) = match geotiff_info.layout {
322            oxigdal_core::types::PixelLayout::Tiled {
323                tile_width,
324                tile_height,
325            } => (tile_width as usize, tile_height as usize),
326            _ => {
327                // Striped layout: treat as tiles of (img_width x rows_per_strip)
328                // We read the whole band and extract
329                return Self::read_geotiff_chunk_full_band(
330                    path, metadata, x_start, y_start, width, height,
331                );
332            }
333        };
334
335        // Allocate output buffer
336        let out_size = width * height * bytes_per_pixel;
337        let mut out_data = vec![0u8; out_size];
338
339        // Calculate which tiles overlap our window
340        let tile_col_start = x_start / tile_w;
341        let tile_col_end = (x_start + width).min(img_width).div_ceil(tile_w);
342        let tile_row_start = y_start / tile_h;
343        let tile_row_end = (y_start + height).min(img_height).div_ceil(tile_h);
344
345        for ty in tile_row_start..tile_row_end {
346            for tx in tile_col_start..tile_col_end {
347                // Read tile data
348                let tile_data = reader.read_tile(0, tx as u32, ty as u32).map_err(|e| {
349                    StreamingError::Other(format!("Failed to read tile ({}, {}): {}", tx, ty, e))
350                })?;
351
352                // Calculate overlap between tile and our window
353                let tile_x0 = tx * tile_w;
354                let tile_y0 = ty * tile_h;
355                let tile_x1 = (tile_x0 + tile_w).min(img_width);
356                let tile_y1 = (tile_y0 + tile_h).min(img_height);
357
358                let overlap_x0 = x_start.max(tile_x0);
359                let overlap_y0 = y_start.max(tile_y0);
360                let overlap_x1 = (x_start + width).min(tile_x1);
361                let overlap_y1 = (y_start + height).min(tile_y1);
362
363                if overlap_x0 >= overlap_x1 || overlap_y0 >= overlap_y1 {
364                    continue;
365                }
366
367                // Copy overlapping region
368                let copy_width = overlap_x1 - overlap_x0;
369                for row_idx in overlap_y0..overlap_y1 {
370                    let src_row_in_tile = row_idx - tile_y0;
371                    let src_col_in_tile = overlap_x0 - tile_x0;
372                    let src_offset = (src_row_in_tile * tile_w + src_col_in_tile) * bytes_per_pixel;
373
374                    let dst_row = row_idx - y_start;
375                    let dst_col = overlap_x0 - x_start;
376                    let dst_offset = (dst_row * width + dst_col) * bytes_per_pixel;
377
378                    let copy_bytes = copy_width * bytes_per_pixel;
379
380                    if src_offset + copy_bytes <= tile_data.len()
381                        && dst_offset + copy_bytes <= out_data.len()
382                    {
383                        out_data[dst_offset..dst_offset + copy_bytes]
384                            .copy_from_slice(&tile_data[src_offset..src_offset + copy_bytes]);
385                    }
386                }
387            }
388        }
389
390        // RasterBuffer validates size as width * height * data_type.size_bytes().
391        // For multi-band interleaved data, the total size is width * height * bytes_per_pixel,
392        // where bytes_per_pixel = data_type.size_bytes() * band_count.
393        // We encode the effective width as width * band_count so that the buffer can hold
394        // all interleaved band data correctly.
395        let band_count = metadata.band_count as u64;
396        let effective_width = width as u64 * band_count;
397        RasterBuffer::new(
398            out_data,
399            effective_width,
400            height as u64,
401            data_type,
402            metadata.nodata,
403        )
404        .map_err(|e| StreamingError::Other(format!("Failed to create RasterBuffer: {}", e)))
405    }
406
407    /// Fallback for striped GeoTIFFs: read the full band and extract the window.
408    fn read_geotiff_chunk_full_band(
409        path: &Path,
410        metadata: &RasterMetadata,
411        x_start: usize,
412        y_start: usize,
413        width: usize,
414        height: usize,
415    ) -> Result<RasterBuffer> {
416        let source = FileDataSource::open(path).map_err(|e| {
417            StreamingError::Other(format!("Failed to open GeoTIFF for band read: {}", e))
418        })?;
419
420        let reader = GeoTiffReader::open(source).map_err(|e| {
421            StreamingError::Other(format!("Failed to parse GeoTIFF for band read: {}", e))
422        })?;
423
424        let info = reader.metadata();
425        let data_type = info.data_type;
426        let bytes_per_pixel = data_type.size_bytes() * info.band_count as usize;
427        let img_width = info.width as usize;
428
429        // Read the entire band
430        let band_data = reader
431            .read_band(0, 0)
432            .map_err(|e| StreamingError::Other(format!("Failed to read band: {}", e)))?;
433
434        // Extract the window
435        let out_size = width * height * bytes_per_pixel;
436        let mut out_data = vec![0u8; out_size];
437
438        for row_idx in 0..height {
439            let src_y = y_start + row_idx;
440            if src_y >= info.height as usize {
441                break;
442            }
443            let src_offset = (src_y * img_width + x_start) * bytes_per_pixel;
444            let dst_offset = row_idx * width * bytes_per_pixel;
445            let copy_width = width.min(img_width.saturating_sub(x_start));
446            let copy_bytes = copy_width * bytes_per_pixel;
447
448            if src_offset + copy_bytes <= band_data.len()
449                && dst_offset + copy_bytes <= out_data.len()
450            {
451                out_data[dst_offset..dst_offset + copy_bytes]
452                    .copy_from_slice(&band_data[src_offset..src_offset + copy_bytes]);
453            }
454        }
455
456        let band_count = metadata.band_count as u64;
457        let effective_width = width as u64 * band_count;
458        RasterBuffer::new(
459            out_data,
460            effective_width,
461            height as u64,
462            data_type,
463            metadata.nodata,
464        )
465        .map_err(|e| StreamingError::Other(format!("Failed to create RasterBuffer: {}", e)))
466    }
467
468    /// Read multiple chunks in parallel.
469    pub async fn read_chunks(&self, chunks: Vec<(usize, usize)>) -> Result<Vec<RasterChunk>> {
470        let mut handles = Vec::with_capacity(chunks.len());
471
472        for (row, col) in chunks {
473            let path = self.path.clone();
474            let config = self.config.clone();
475            let metadata = self.metadata.clone();
476            let bands = self.bands.clone();
477            let semaphore = Arc::clone(&self.prefetch_semaphore);
478            let format = self.format;
479
480            let handle = tokio::spawn(async move {
481                let _permit = semaphore
482                    .acquire()
483                    .await
484                    .map_err(|e| StreamingError::Other(e.to_string()))?;
485
486                task::spawn_blocking(move || {
487                    Self::read_chunk_blocking(path, row, col, config, metadata, bands, format)
488                })
489                .await
490                .map_err(|e| StreamingError::Other(e.to_string()))?
491            });
492
493            handles.push(handle);
494        }
495
496        let mut results = Vec::with_capacity(handles.len());
497        for handle in handles {
498            match handle.await {
499                Ok(Ok(chunk)) => results.push(chunk),
500                Ok(Err(e)) => {
501                    error!("Failed to read chunk: {}", e);
502                    return Err(e);
503                }
504                Err(e) => {
505                    error!("Task panicked: {}", e);
506                    return Err(StreamingError::Other(e.to_string()));
507                }
508            }
509        }
510
511        Ok(results)
512    }
513
514    /// Get the metadata for this raster.
515    pub fn metadata(&self) -> &RasterMetadata {
516        &self.metadata
517    }
518
519    /// Get the stream configuration.
520    pub fn config(&self) -> &RasterStreamConfig {
521        &self.config
522    }
523
524    /// Get the detected file format.
525    pub fn format(&self) -> RasterFormat {
526        self.format
527    }
528}
529
530#[async_trait]
531impl RasterStreaming for RasterStreamReader {
532    async fn next_chunk(&mut self) -> Result<Option<RasterChunk>> {
533        let stream = self
534            .stream
535            .as_mut()
536            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
537        stream.next_chunk().await
538    }
539
540    async fn next_chunks(&mut self, count: usize) -> Result<Vec<RasterChunk>> {
541        let stream = self
542            .stream
543            .as_mut()
544            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
545        stream.next_chunks(count).await
546    }
547
548    async fn seek_to_chunk(&mut self, row: usize, col: usize) -> Result<()> {
549        let stream = self
550            .stream
551            .as_mut()
552            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
553        stream.seek_to_chunk(row, col).await
554    }
555
556    fn total_chunks(&self) -> (usize, usize) {
557        self.stream
558            .as_ref()
559            .map(|s| s.total_chunks())
560            .unwrap_or((0, 0))
561    }
562
563    fn current_position(&self) -> (usize, usize) {
564        self.stream
565            .as_ref()
566            .map(|s| s.current_position())
567            .unwrap_or((0, 0))
568    }
569
570    fn has_more_chunks(&self) -> bool {
571        self.stream
572            .as_ref()
573            .map(|s| s.has_more_chunks())
574            .unwrap_or(false)
575    }
576}
577
578/// Builder for configuring a raster stream reader.
579pub struct RasterStreamReaderBuilder {
580    path: PathBuf,
581    config: RasterStreamConfig,
582    bands: Vec<usize>,
583}
584
585impl RasterStreamReaderBuilder {
586    /// Create a new builder.
587    pub fn new<P: AsRef<Path>>(path: P) -> Self {
588        Self {
589            path: path.as_ref().to_path_buf(),
590            config: RasterStreamConfig::default(),
591            bands: vec![0],
592        }
593    }
594
595    /// Set the chunk size.
596    pub fn chunk_size(mut self, width: usize, height: usize) -> Self {
597        self.config = self.config.with_chunk_size(width, height);
598        self
599    }
600
601    /// Set the overlap size.
602    pub fn overlap(mut self, overlap: usize) -> Self {
603        self.config = self.config.with_overlap(overlap);
604        self
605    }
606
607    /// Enable compression.
608    pub fn compression(mut self, level: u8) -> Self {
609        self.config = self.config.with_compression(level);
610        self
611    }
612
613    /// Set the bands to read.
614    pub fn bands(mut self, bands: Vec<usize>) -> Self {
615        self.bands = bands;
616        self
617    }
618
619    /// Set the number of parallel workers.
620    pub fn parallel(mut self, num_workers: usize) -> Self {
621        self.config = self.config.with_parallel(true, num_workers);
622        self
623    }
624
625    /// Build the reader.
626    pub async fn build(self) -> Result<RasterStreamReader> {
627        let reader = RasterStreamReader::new(self.path, self.config).await?;
628        Ok(reader.with_bands(self.bands))
629    }
630}
631
632#[cfg(test)]
633mod tests;