use super::{ChunkStats, RasterChunk, RasterStreamConfig};
use crate::error::{Result, StreamingError};
use oxigdal_core::types::RasterMetadata;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use tracing::info;
pub struct RasterStreamWriter {
path: PathBuf,
config: RasterStreamConfig,
metadata: RasterMetadata,
stats: Arc<RwLock<ChunkStats>>,
chunk_store: Arc<RwLock<HashMap<(usize, usize), RasterChunk>>>,
total_chunks: (usize, usize),
}
impl RasterStreamWriter {
pub async fn new<P: AsRef<Path>>(
path: P,
metadata: RasterMetadata,
config: RasterStreamConfig,
) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let total_chunks = Self::calculate_chunks(
metadata.width as usize,
metadata.height as usize,
config.chunk_size.0,
config.chunk_size.1,
config.overlap,
);
info!(
"Created raster stream writer for {}x{} raster ({} x {} chunks)",
metadata.width, metadata.height, total_chunks.0, total_chunks.1
);
Ok(Self {
path,
config,
metadata,
stats: Arc::new(RwLock::new(ChunkStats::new())),
chunk_store: Arc::new(RwLock::new(HashMap::new())),
total_chunks,
})
}
pub fn calculate_chunks(
width: usize,
height: usize,
chunk_width: usize,
chunk_height: usize,
overlap: usize,
) -> (usize, usize) {
let effective_chunk_width = chunk_width - overlap;
let effective_chunk_height = chunk_height - overlap;
let num_cols = width.div_ceil(effective_chunk_width);
let num_rows = height.div_ceil(effective_chunk_height);
(num_rows, num_cols)
}
pub async fn write_chunk(&self, chunk: RasterChunk) -> Result<()> {
let start = Instant::now();
let size = chunk.size_bytes();
let indices = chunk.indices;
let mut store = self.chunk_store.write().await;
store.insert(indices, chunk);
drop(store);
let elapsed = start.elapsed().as_millis() as u64;
let mut stats_guard = self.stats.write().await;
stats_guard.record_chunk(size, elapsed);
Ok(())
}
pub async fn write_chunks(&self, chunks: Vec<RasterChunk>) -> Result<()> {
for chunk in chunks {
self.write_chunk(chunk).await?;
}
Ok(())
}
pub async fn stats(&self) -> ChunkStats {
self.stats.read().await.clone()
}
pub async fn flush(&self) -> Result<()> {
Ok(())
}
pub async fn finalize(&self) -> Result<()> {
let img_width = self.metadata.width as usize;
let img_height = self.metadata.height as usize;
let band_count = self.metadata.band_count as usize;
let data_type = self.metadata.data_type;
let bytes_per_sample = data_type.size_bytes();
let bytes_per_pixel = bytes_per_sample * band_count;
let total_bytes = img_width * img_height * bytes_per_pixel;
let mut image_data = vec![0u8; total_bytes];
let chunk_store = self.chunk_store.read().await;
let chunk_w = self.config.chunk_size.0;
let chunk_h = self.config.chunk_size.1;
let overlap = self.config.overlap;
let effective_w = chunk_w - overlap;
let effective_h = chunk_h - overlap;
for (&(row, col), chunk) in chunk_store.iter() {
let x_start = col * effective_w;
let y_start = row * effective_h;
let src_width = chunk.buffer.width() as usize / band_count; let src_height = chunk.buffer.height() as usize;
let src_bytes = chunk.buffer.as_bytes();
for cy in 0..src_height {
let dst_y = y_start + cy;
if dst_y >= img_height {
break;
}
for cx in 0..src_width {
let dst_x = x_start + cx;
if dst_x >= img_width {
break;
}
let src_offset = (cy * src_width + cx) * bytes_per_pixel;
let dst_offset = (dst_y * img_width + dst_x) * bytes_per_pixel;
if src_offset + bytes_per_pixel <= src_bytes.len()
&& dst_offset + bytes_per_pixel <= image_data.len()
{
image_data[dst_offset..dst_offset + bytes_per_pixel]
.copy_from_slice(&src_bytes[src_offset..src_offset + bytes_per_pixel]);
}
}
}
}
drop(chunk_store);
let path = self.path.clone();
let meta = self.metadata.clone();
tokio::task::spawn_blocking(move || Self::write_geotiff_blocking(&path, &meta, &image_data))
.await
.map_err(|e| StreamingError::Other(format!("finalize task failed: {}", e)))?
}
fn write_geotiff_blocking(path: &Path, metadata: &RasterMetadata, data: &[u8]) -> Result<()> {
use oxigdal_geotiff::tiff::{Compression, PhotometricInterpretation, Predictor};
use oxigdal_geotiff::writer::{GeoTiffWriter, GeoTiffWriterOptions, WriterConfig};
let photometric = if metadata.band_count >= 3 {
PhotometricInterpretation::Rgb
} else {
PhotometricInterpretation::BlackIsZero
};
let mut config = WriterConfig::new(
metadata.width,
metadata.height,
metadata.band_count as u16,
metadata.data_type,
)
.with_compression(Compression::Lzw)
.with_predictor(Predictor::HorizontalDifferencing)
.with_tile_size(256, 256)
.with_photometric(photometric)
.with_nodata(metadata.nodata)
.with_overviews(false, oxigdal_geotiff::OverviewResampling::Average);
if let Some(gt) = metadata.geo_transform {
config = config.with_geo_transform(gt);
}
let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
.map_err(|e| {
StreamingError::Other(format!(
"Failed to create GeoTIFF writer for '{}': {}",
path.display(),
e
))
})?;
writer.write(data).map_err(|e| {
StreamingError::Other(format!(
"Failed to write GeoTIFF data to '{}': {}",
path.display(),
e
))
})?;
info!("Finalized GeoTIFF: {}", path.display());
Ok(())
}
pub fn total_chunks(&self) -> (usize, usize) {
self.total_chunks
}
pub async fn current_position(&self) -> (usize, usize) {
let store = self.chunk_store.read().await;
let count = store.len();
drop(store);
let cols = self.total_chunks.1.max(1);
(count / cols, count % cols)
}
}
pub struct RasterStreamWriterBuilder {
path: PathBuf,
metadata: RasterMetadata,
config: RasterStreamConfig,
}
impl RasterStreamWriterBuilder {
pub fn new<P: AsRef<Path>>(path: P, metadata: RasterMetadata) -> Self {
Self {
path: path.as_ref().to_path_buf(),
metadata,
config: RasterStreamConfig::default(),
}
}
pub fn chunk_size(mut self, width: usize, height: usize) -> Self {
self.config = self.config.with_chunk_size(width, height);
self
}
pub fn overlap(mut self, overlap: usize) -> Self {
self.config = self.config.with_overlap(overlap);
self
}
pub fn compression(mut self, level: u8) -> Self {
self.config = self.config.with_compression(level);
self
}
pub fn parallel(mut self, num_workers: usize) -> Self {
self.config = self.config.with_parallel(true, num_workers);
self
}
pub async fn build(self) -> Result<RasterStreamWriter> {
RasterStreamWriter::new(self.path, self.metadata, self.config).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxigdal_core::types::{GeoTransform, NoDataValue, PixelLayout, RasterDataType};
use std::env;
#[tokio::test]
async fn test_writer_creation() {
let temp_dir = env::temp_dir();
let test_path = temp_dir.join("test_output.tif");
let metadata = RasterMetadata {
width: 1024,
height: 1024,
band_count: 1,
data_type: RasterDataType::Float32,
geo_transform: Some(GeoTransform {
origin_x: 0.0,
origin_y: 0.0,
pixel_width: 1.0,
pixel_height: -1.0,
row_rotation: 0.0,
col_rotation: 0.0,
}),
crs_wkt: None,
nodata: NoDataValue::None,
color_interpretation: Vec::new(),
layout: PixelLayout::default(),
driver_metadata: Vec::new(),
statistics: None,
};
let result = RasterStreamWriterBuilder::new(&test_path, metadata)
.chunk_size(256, 256)
.compression(6)
.parallel(4)
.build()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chunk_calculation() {
let chunks = RasterStreamWriter::calculate_chunks(1024, 1024, 256, 256, 0);
assert_eq!(chunks, (4, 4));
let chunks = RasterStreamWriter::calculate_chunks(1000, 1000, 256, 256, 0);
assert_eq!(chunks, (4, 4));
}
}