Skip to main content

oxigdal_streaming/raster/
stream.rs

1//! Core raster streaming types and implementations.
2
3use crate::core::StreamElement;
4use crate::error::{Result, StreamingError};
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use oxigdal_core::{
8    buffer::RasterBuffer,
9    types::{BoundingBox, GeoTransform, RasterMetadata},
10};
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13use tokio::sync::{RwLock, mpsc};
14use tracing::{info, warn};
15
16/// Configuration for raster streaming.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RasterStreamConfig {
19    /// Chunk size in pixels (width, height)
20    pub chunk_size: (usize, usize),
21
22    /// Overlap between chunks in pixels
23    pub overlap: usize,
24
25    /// Buffer size for the stream
26    pub buffer_size: usize,
27
28    /// Whether to enable compression
29    pub compression: bool,
30
31    /// Compression level (1-9)
32    pub compression_level: u8,
33
34    /// Maximum memory usage in bytes
35    pub max_memory_bytes: usize,
36
37    /// Number of prefetch chunks
38    pub prefetch_count: usize,
39
40    /// Enable parallel chunk processing
41    pub parallel: bool,
42
43    /// Number of parallel workers
44    pub num_workers: usize,
45}
46
47impl Default for RasterStreamConfig {
48    fn default() -> Self {
49        Self {
50            chunk_size: (512, 512),
51            overlap: 0,
52            buffer_size: 100,
53            compression: false,
54            compression_level: 6,
55            max_memory_bytes: 1024 * 1024 * 1024, // 1GB
56            prefetch_count: 2,
57            parallel: true,
58            num_workers: std::thread::available_parallelism()
59                .map(|n| n.get())
60                .unwrap_or(4),
61        }
62    }
63}
64
65impl RasterStreamConfig {
66    /// Create a new configuration with default values.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Set the chunk size.
72    pub fn with_chunk_size(mut self, width: usize, height: usize) -> Self {
73        self.chunk_size = (width, height);
74        self
75    }
76
77    /// Set the overlap size.
78    pub fn with_overlap(mut self, overlap: usize) -> Self {
79        self.overlap = overlap;
80        self
81    }
82
83    /// Enable compression.
84    pub fn with_compression(mut self, level: u8) -> Self {
85        self.compression = true;
86        self.compression_level = level;
87        self
88    }
89
90    /// Set the maximum memory usage.
91    pub fn with_max_memory(mut self, bytes: usize) -> Self {
92        self.max_memory_bytes = bytes;
93        self
94    }
95
96    /// Set the number of prefetch chunks.
97    pub fn with_prefetch(mut self, count: usize) -> Self {
98        self.prefetch_count = count;
99        self
100    }
101
102    /// Enable or disable parallel processing.
103    pub fn with_parallel(mut self, parallel: bool, num_workers: usize) -> Self {
104        self.parallel = parallel;
105        self.num_workers = num_workers;
106        self
107    }
108}
109
110/// A chunk of raster data with spatial metadata.
111#[derive(Debug, Clone)]
112pub struct RasterChunk {
113    /// The raster data buffer
114    pub buffer: RasterBuffer,
115
116    /// Bounding box of this chunk
117    pub bbox: BoundingBox,
118
119    /// Geotransform for this chunk
120    pub geotransform: GeoTransform,
121
122    /// Chunk indices (row, col)
123    pub indices: (usize, usize),
124
125    /// Timestamp when chunk was created
126    pub timestamp: DateTime<Utc>,
127
128    /// Chunk metadata
129    pub metadata: ChunkMetadata,
130}
131
132impl RasterChunk {
133    /// Create a new raster chunk.
134    pub fn new(
135        buffer: RasterBuffer,
136        bbox: BoundingBox,
137        geotransform: GeoTransform,
138        indices: (usize, usize),
139    ) -> Self {
140        Self {
141            buffer,
142            bbox,
143            geotransform,
144            indices,
145            timestamp: Utc::now(),
146            metadata: ChunkMetadata::default(),
147        }
148    }
149
150    /// Get the size in bytes of this chunk.
151    pub fn size_bytes(&self) -> usize {
152        self.buffer.as_bytes().len()
153    }
154
155    /// Check if this chunk overlaps with another.
156    pub fn overlaps_with(&self, other: &RasterChunk) -> bool {
157        self.bbox.intersects(&other.bbox)
158    }
159
160    /// Get the overlap region with another chunk.
161    pub fn overlap_region(&self, other: &RasterChunk) -> Option<BoundingBox> {
162        self.bbox.intersection(&other.bbox)
163    }
164
165    /// Convert to a stream element using JSON serialization of spatial metadata
166    /// and a raw bytes payload for the raster data.
167    pub fn to_stream_element(&self) -> Result<StreamElement> {
168        // Encode spatial metadata as JSON, then append raw pixel bytes.
169        // Layout: [8 bytes: json_len][json_len bytes: JSON][remaining: raw pixels]
170        let meta = serde_json::json!({
171            "bbox": {
172                "min_x": self.bbox.min_x(),
173                "min_y": self.bbox.min_y(),
174                "max_x": self.bbox.max_x(),
175                "max_y": self.bbox.max_y(),
176            },
177            "gt": {
178                "origin_x": self.geotransform.origin_x,
179                "origin_y": self.geotransform.origin_y,
180                "pixel_width": self.geotransform.pixel_width,
181                "pixel_height": self.geotransform.pixel_height,
182                "row_rotation": self.geotransform.row_rotation,
183                "col_rotation": self.geotransform.col_rotation,
184            },
185            "row": self.indices.0,
186            "col": self.indices.1,
187            "width": self.buffer.width(),
188            "height": self.buffer.height(),
189            "data_type": format!("{:?}", self.buffer.data_type()),
190        });
191        let json_bytes = serde_json::to_vec(&meta)
192            .map_err(|e| StreamingError::SerializationError(e.to_string()))?;
193        let json_len = json_bytes.len() as u64;
194        let pixel_bytes = self.buffer.as_bytes();
195
196        let mut data = Vec::with_capacity(8 + json_bytes.len() + pixel_bytes.len());
197        data.extend_from_slice(&json_len.to_le_bytes());
198        data.extend_from_slice(&json_bytes);
199        data.extend_from_slice(pixel_bytes);
200
201        Ok(StreamElement::new(data, self.timestamp))
202    }
203
204    /// Reconstruct from a stream element.  Only the raw pixel buffer and
205    /// spatial metadata are restored; band-level metadata is left at defaults.
206    pub fn from_stream_element(element: &StreamElement) -> Result<Self> {
207        use oxigdal_core::types::{BoundingBox, NoDataValue, RasterDataType};
208
209        let data = &element.data;
210        if data.len() < 8 {
211            return Err(StreamingError::DeserializationError(
212                "stream element too short to contain header".to_string(),
213            ));
214        }
215        let json_len = u64::from_le_bytes(
216            data[..8]
217                .try_into()
218                .map_err(|_| StreamingError::DeserializationError("bad header".to_string()))?,
219        ) as usize;
220        if data.len() < 8 + json_len {
221            return Err(StreamingError::DeserializationError(
222                "stream element truncated: JSON metadata missing".to_string(),
223            ));
224        }
225        let meta: serde_json::Value = serde_json::from_slice(&data[8..8 + json_len])
226            .map_err(|e| StreamingError::DeserializationError(e.to_string()))?;
227
228        let pixel_bytes = data[8 + json_len..].to_vec();
229
230        let width = meta["width"]
231            .as_u64()
232            .ok_or_else(|| StreamingError::DeserializationError("missing width".to_string()))?;
233        let height = meta["height"]
234            .as_u64()
235            .ok_or_else(|| StreamingError::DeserializationError("missing height".to_string()))?;
236        let row = meta["row"]
237            .as_u64()
238            .ok_or_else(|| StreamingError::DeserializationError("missing row index".to_string()))?
239            as usize;
240        let col = meta["col"]
241            .as_u64()
242            .ok_or_else(|| StreamingError::DeserializationError("missing col index".to_string()))?
243            as usize;
244
245        let gt = GeoTransform {
246            origin_x: meta["gt"]["origin_x"].as_f64().unwrap_or(0.0),
247            origin_y: meta["gt"]["origin_y"].as_f64().unwrap_or(0.0),
248            pixel_width: meta["gt"]["pixel_width"].as_f64().unwrap_or(1.0),
249            pixel_height: meta["gt"]["pixel_height"].as_f64().unwrap_or(-1.0),
250            row_rotation: meta["gt"]["row_rotation"].as_f64().unwrap_or(0.0),
251            col_rotation: meta["gt"]["col_rotation"].as_f64().unwrap_or(0.0),
252        };
253
254        let min_x = meta["bbox"]["min_x"].as_f64().unwrap_or(0.0);
255        let min_y = meta["bbox"]["min_y"].as_f64().unwrap_or(0.0);
256        let max_x = meta["bbox"]["max_x"].as_f64().unwrap_or(0.0);
257        let max_y = meta["bbox"]["max_y"].as_f64().unwrap_or(0.0);
258        let bbox = BoundingBox::new(min_x, min_y, max_x, max_y).map_err(StreamingError::Core)?;
259
260        let buffer = RasterBuffer::new(
261            pixel_bytes,
262            width,
263            height,
264            RasterDataType::UInt8, // fallback; full round-trip requires type registry
265            NoDataValue::None,
266        )
267        .map_err(|e| StreamingError::DeserializationError(e.to_string()))?;
268
269        Ok(Self {
270            buffer,
271            bbox,
272            geotransform: gt,
273            indices: (row, col),
274            timestamp: element.event_time,
275            metadata: ChunkMetadata::default(),
276        })
277    }
278}
279
280/// Metadata for a raster chunk.
281#[derive(Debug, Clone, Default, Serialize, Deserialize)]
282pub struct ChunkMetadata {
283    /// Band indices included in this chunk
284    pub bands: Vec<usize>,
285
286    /// Compression codec used
287    pub compression: Option<String>,
288
289    /// Original size in bytes (before compression)
290    pub original_size: Option<usize>,
291
292    /// Compressed size in bytes
293    pub compressed_size: Option<usize>,
294
295    /// Checksum of the data
296    pub checksum: Option<String>,
297
298    /// Custom attributes
299    pub attributes: std::collections::HashMap<String, String>,
300}
301
302/// Async trait for raster streaming.
303#[async_trait]
304pub trait RasterStreaming: Send + Sync {
305    /// Get the next chunk from the stream.
306    async fn next_chunk(&mut self) -> Result<Option<RasterChunk>>;
307
308    /// Get multiple chunks in parallel.
309    async fn next_chunks(&mut self, count: usize) -> Result<Vec<RasterChunk>>;
310
311    /// Skip to a specific chunk by index.
312    async fn seek_to_chunk(&mut self, row: usize, col: usize) -> Result<()>;
313
314    /// Get the total number of chunks.
315    fn total_chunks(&self) -> (usize, usize);
316
317    /// Get the current position in the stream.
318    fn current_position(&self) -> (usize, usize);
319
320    /// Check if the stream has more chunks.
321    fn has_more_chunks(&self) -> bool;
322}
323
324/// A stream of raster chunks.
325pub struct RasterStream {
326    /// Stream configuration
327    config: RasterStreamConfig,
328
329    /// Raster metadata
330    metadata: RasterMetadata,
331
332    /// Total number of chunks (rows, cols)
333    total_chunks: (usize, usize),
334
335    /// Current chunk position
336    current_position: Arc<RwLock<(usize, usize)>>,
337
338    /// Channel for receiving chunks
339    receiver: mpsc::Receiver<RasterChunk>,
340
341    /// Sender for prefetch requests
342    prefetch_sender: Option<mpsc::Sender<(usize, usize)>>,
343
344    /// Current memory usage in bytes
345    memory_usage: Arc<RwLock<usize>>,
346}
347
348impl RasterStream {
349    /// Create a new raster stream.
350    pub fn new(config: RasterStreamConfig, metadata: RasterMetadata) -> Result<Self> {
351        // Calculate total number of chunks
352        let total_chunks = Self::calculate_chunks(
353            metadata.width as usize,
354            metadata.height as usize,
355            config.chunk_size.0,
356            config.chunk_size.1,
357            config.overlap,
358        );
359
360        let (_sender, receiver) = mpsc::channel(config.buffer_size);
361
362        info!(
363            "Created raster stream with {} x {} chunks",
364            total_chunks.0, total_chunks.1
365        );
366
367        Ok(Self {
368            config,
369            metadata,
370            total_chunks,
371            current_position: Arc::new(RwLock::new((0, 0))),
372            receiver,
373            prefetch_sender: None,
374            memory_usage: Arc::new(RwLock::new(0)),
375        })
376    }
377
378    /// Calculate the number of chunks needed.
379    pub fn calculate_chunks(
380        width: usize,
381        height: usize,
382        chunk_width: usize,
383        chunk_height: usize,
384        overlap: usize,
385    ) -> (usize, usize) {
386        let effective_chunk_width = chunk_width - overlap;
387        let effective_chunk_height = chunk_height - overlap;
388
389        let num_cols = width.div_ceil(effective_chunk_width);
390        let num_rows = height.div_ceil(effective_chunk_height);
391
392        (num_rows, num_cols)
393    }
394
395    /// Get the bounding box for a specific chunk.
396    pub fn chunk_bbox(&self, row: usize, col: usize) -> Result<BoundingBox> {
397        if row >= self.total_chunks.0 || col >= self.total_chunks.1 {
398            return Err(StreamingError::InvalidOperation(format!(
399                "Chunk ({}, {}) out of bounds",
400                row, col
401            )));
402        }
403
404        let chunk_width = self.config.chunk_size.0;
405        let chunk_height = self.config.chunk_size.1;
406        let overlap = self.config.overlap;
407
408        let effective_width = chunk_width - overlap;
409        let effective_height = chunk_height - overlap;
410
411        let x_start = col * effective_width;
412        let y_start = row * effective_height;
413        let x_end = (x_start + chunk_width).min(self.metadata.width as usize);
414        let y_end = (y_start + chunk_height).min(self.metadata.height as usize);
415
416        // Convert pixel coordinates to geographic coordinates
417        let gt =
418            self.metadata.geo_transform.as_ref().ok_or_else(|| {
419                StreamingError::InvalidState("No geotransform available".to_string())
420            })?;
421
422        let min_x = gt.origin_x + (x_start as f64) * gt.pixel_width;
423        let max_y = gt.origin_y + (y_start as f64) * gt.pixel_height;
424        let max_x = gt.origin_x + (x_end as f64) * gt.pixel_width;
425        let min_y = gt.origin_y + (y_end as f64) * gt.pixel_height;
426
427        BoundingBox::new(min_x, min_y, max_x, max_y).map_err(StreamingError::Core)
428    }
429
430    /// Get the geotransform for a specific chunk.
431    pub fn chunk_geotransform(&self, row: usize, col: usize) -> Result<GeoTransform> {
432        let gt =
433            self.metadata.geo_transform.as_ref().ok_or_else(|| {
434                StreamingError::InvalidState("No geotransform available".to_string())
435            })?;
436
437        let chunk_width = self.config.chunk_size.0;
438        let chunk_height = self.config.chunk_size.1;
439        let overlap = self.config.overlap;
440
441        let effective_width = chunk_width - overlap;
442        let effective_height = chunk_height - overlap;
443
444        let x_start = col * effective_width;
445        let y_start = row * effective_height;
446
447        let origin_x = gt.origin_x + (x_start as f64) * gt.pixel_width;
448        let origin_y = gt.origin_y + (y_start as f64) * gt.pixel_height;
449
450        Ok(GeoTransform {
451            origin_x,
452            origin_y,
453            pixel_width: gt.pixel_width,
454            pixel_height: gt.pixel_height,
455            row_rotation: gt.row_rotation,
456            col_rotation: gt.col_rotation,
457        })
458    }
459
460    /// Get memory usage statistics.
461    pub async fn memory_stats(&self) -> MemoryStats {
462        let current = *self.memory_usage.read().await;
463        MemoryStats {
464            current_bytes: current,
465            max_bytes: self.config.max_memory_bytes,
466            utilization: (current as f64) / (self.config.max_memory_bytes as f64),
467        }
468    }
469
470    /// Update memory usage.
471    #[allow(dead_code)]
472    async fn update_memory(&self, delta: isize) -> Result<()> {
473        let mut usage = self.memory_usage.write().await;
474        if delta > 0 {
475            let new_usage = *usage + delta as usize;
476            if new_usage > self.config.max_memory_bytes {
477                return Err(StreamingError::Other("Memory limit exceeded".to_string()));
478            }
479            *usage = new_usage;
480        } else {
481            *usage = usage.saturating_sub((-delta) as usize);
482        }
483        Ok(())
484    }
485}
486
487#[async_trait]
488impl RasterStreaming for RasterStream {
489    async fn next_chunk(&mut self) -> Result<Option<RasterChunk>> {
490        match self.receiver.recv().await {
491            Some(chunk) => {
492                // Update position
493                let mut pos = self.current_position.write().await;
494                pos.1 += 1;
495                if pos.1 >= self.total_chunks.1 {
496                    pos.1 = 0;
497                    pos.0 += 1;
498                }
499
500                Ok(Some(chunk))
501            }
502            None => Ok(None),
503        }
504    }
505
506    async fn next_chunks(&mut self, count: usize) -> Result<Vec<RasterChunk>> {
507        let mut chunks = Vec::with_capacity(count);
508        for _ in 0..count {
509            match self.next_chunk().await? {
510                Some(chunk) => chunks.push(chunk),
511                None => break,
512            }
513        }
514        Ok(chunks)
515    }
516
517    async fn seek_to_chunk(&mut self, row: usize, col: usize) -> Result<()> {
518        if row >= self.total_chunks.0 || col >= self.total_chunks.1 {
519            return Err(StreamingError::InvalidOperation(format!(
520                "Chunk ({}, {}) out of bounds",
521                row, col
522            )));
523        }
524
525        let mut pos = self.current_position.write().await;
526        *pos = (row, col);
527
528        // Send prefetch request if enabled
529        if let Some(sender) = &self.prefetch_sender
530            && sender.try_send((row, col)).is_err()
531        {
532            warn!("Failed to send prefetch request");
533        }
534
535        Ok(())
536    }
537
538    fn total_chunks(&self) -> (usize, usize) {
539        self.total_chunks
540    }
541
542    fn current_position(&self) -> (usize, usize) {
543        // Note: This is a blocking call on async lock, but it's okay for a quick read
544        // In production, you might want to use a different synchronization primitive
545        match self.current_position.try_read() {
546            Ok(pos) => *pos,
547            Err(_) => (0, 0),
548        }
549    }
550
551    fn has_more_chunks(&self) -> bool {
552        let pos = self.current_position();
553        pos.0 < self.total_chunks.0
554    }
555}
556
557/// Memory usage statistics.
558#[derive(Debug, Clone)]
559pub struct MemoryStats {
560    /// Current memory usage in bytes
561    pub current_bytes: usize,
562
563    /// Maximum allowed memory in bytes
564    pub max_bytes: usize,
565
566    /// Memory utilization (0.0 to 1.0)
567    pub utilization: f64,
568}
569
570/// Chunk processing statistics.
571#[derive(Debug, Clone, Default)]
572pub struct ChunkStats {
573    /// Number of chunks processed
574    pub chunks_processed: usize,
575
576    /// Number of chunks failed
577    pub chunks_failed: usize,
578
579    /// Total bytes processed
580    pub bytes_processed: usize,
581
582    /// Total processing time in milliseconds
583    pub processing_time_ms: u64,
584
585    /// Average chunk processing time in milliseconds
586    pub avg_chunk_time_ms: f64,
587}
588
589impl ChunkStats {
590    /// Create new statistics.
591    pub fn new() -> Self {
592        Self::default()
593    }
594
595    /// Update statistics with a processed chunk.
596    pub fn record_chunk(&mut self, size_bytes: usize, time_ms: u64) {
597        self.chunks_processed += 1;
598        self.bytes_processed += size_bytes;
599        self.processing_time_ms += time_ms;
600        self.avg_chunk_time_ms = (self.processing_time_ms as f64) / (self.chunks_processed as f64);
601    }
602
603    /// Record a failed chunk.
604    pub fn record_failure(&mut self) {
605        self.chunks_failed += 1;
606    }
607
608    /// Get the throughput in MB/s.
609    pub fn throughput_mbps(&self) -> f64 {
610        if self.processing_time_ms == 0 {
611            return 0.0;
612        }
613        let mb = (self.bytes_processed as f64) / (1024.0 * 1024.0);
614        let seconds = (self.processing_time_ms as f64) / 1000.0;
615        mb / seconds
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn test_chunk_calculation() {
625        let chunks = RasterStream::calculate_chunks(1024, 1024, 256, 256, 0);
626        assert_eq!(chunks, (4, 4));
627
628        let chunks = RasterStream::calculate_chunks(1000, 1000, 256, 256, 0);
629        assert_eq!(chunks, (4, 4));
630
631        let chunks = RasterStream::calculate_chunks(1024, 1024, 256, 256, 16);
632        assert_eq!(chunks, (5, 5));
633    }
634
635    #[test]
636    fn test_config_builder() {
637        let config = RasterStreamConfig::new()
638            .with_chunk_size(1024, 1024)
639            .with_overlap(32)
640            .with_compression(9)
641            .with_prefetch(4);
642
643        assert_eq!(config.chunk_size, (1024, 1024));
644        assert_eq!(config.overlap, 32);
645        assert!(config.compression);
646        assert_eq!(config.compression_level, 9);
647        assert_eq!(config.prefetch_count, 4);
648    }
649
650    #[test]
651    fn test_chunk_stats() {
652        let mut stats = ChunkStats::new();
653        stats.record_chunk(1024 * 1024, 100);
654        stats.record_chunk(1024 * 1024, 150);
655
656        assert_eq!(stats.chunks_processed, 2);
657        assert_eq!(stats.bytes_processed, 2 * 1024 * 1024);
658        assert_eq!(stats.avg_chunk_time_ms, 125.0);
659        assert!(stats.throughput_mbps() > 0.0);
660    }
661}