eorst 1.0.1

Earth Observation and Remote Sensing Toolkit - library for raster processing pipelines
//! Metadata types for raster datasets.
//!
//! This module provides types for describing the geographic extent, layers,
//! and metadata of raster datasets.

use crate::core_types::{RasterData, RasterType};
use crate::data_sources::DateType;
use crate::types::{BlockSize, GeoTransform, RasterDataShape};
use anyhow::Result;
use num_traits::NumCast;
use std::path::PathBuf;
use std::str::FromStr;

/// Geographic extent defined by bounding box coordinates.
#[derive(Debug, PartialEq, Clone)]
pub struct Extent {
    /// Minimum x coordinate (longitude/west)
    pub xmin: f64,
    /// Minimum y coordinate (latitude/south)
    pub ymin: f64,
    /// Maximum x coordinate (longitude/east)
    pub xmax: f64,
    /// Maximum y coordinate (latitude/north)
    pub ymax: f64,
}

impl FromStr for Extent {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.split(',').collect();
        if parts.len() != 4 {
            return Err("Extent must be in the format xmin,ymin,xmax,ymax".to_string());
        }

        let xmin = parts[0].parse().map_err(|_| "Invalid xmin")?;
        let ymin = parts[1].parse().map_err(|_| "Invalid ymin")?;
        let xmax = parts[2].parse().map_err(|_| "Invalid xmax")?;
        let ymax = parts[3].parse().map_err(|_| "Invalid ymax")?;

        Ok(Extent {
            xmin,
            ymin,
            xmax,
            ymax,
        })
    }
}

impl Extent {
    /// Round extent coordinates outward so they align with a resolution grid.
    ///
    /// `xmin`/`ymin` are floored, `xmax`/`ymax` are ceiled, ensuring the
    /// snapped extent fully covers the original extent.
    pub fn snap_to_grid(&self, resolution: f64) -> Self {
        Extent {
            xmin: (self.xmin / resolution).floor() * resolution,
            ymin: (self.ymin / resolution).floor() * resolution,
            xmax: (self.xmax / resolution).ceil() * resolution,
            ymax: (self.ymax / resolution).ceil() * resolution,
        }
    }

    /// Expand this extent to include another extent (union).
    pub fn union(&self, other: &Self) -> Self {
        Extent {
            xmin: self.xmin.min(other.xmin),
            ymin: self.ymin.min(other.ymin),
            xmax: self.xmax.max(other.xmax),
            ymax: self.ymax.max(other.ymax),
        }
    }
}

/// Represents a layer (band) within a raster dataset.
///
/// Each layer corresponds to a specific band or variable in the data.
#[derive(Debug, Clone, PartialEq)]
pub struct Layer {
    pub source: PathBuf,
    pub(crate) layer_pos: usize,
    pub(crate) time_pos: usize,
}

impl Layer {
    pub fn new(source: PathBuf, layer_pos: usize, time_pos: usize) -> Self {
        Layer {
            source,
            layer_pos,
            time_pos,
        }
    }

    /// Updates the layer position after stacking.
    pub fn stack_position(
        &mut self,
        original_layer: Layer,
        dimension_to_stack: crate::types::Dimension,
        max_dim: usize,
    ) -> &mut Layer {
        match dimension_to_stack {
            crate::types::Dimension::Layer => self.layer_pos = original_layer.layer_pos + max_dim,
            crate::types::Dimension::Time => self.time_pos = original_layer.time_pos + max_dim,
        }
        self
    }
}

/// Metadata describing a raster dataset's structure, CRS, and band configuration.
#[derive(Debug, Clone, PartialEq)]
pub struct RasterMetadata<U>
where
    U: RasterType,
{
    /// Layers (bands) in this dataset
    pub layers: Vec<Layer>,
    /// Shape of the raster data (times, layers, rows, cols)
    pub shape: RasterDataShape,
    pub(crate) block_size: BlockSize,
    /// EPSG coordinate reference system code
    pub epsg_code: u32,
    /// Geographic transformation parameters
    pub geo_transform: GeoTransform,
    /// Size of overlap between blocks
    pub overlap_size: usize,
    /// Date indices for time-series data
    pub date_indices: Vec<DateType>,
    /// Layer identifiers
    pub layer_indices: Vec<String>,
    /// No-data value
    pub na_value: U,
}

impl<U> Default for RasterMetadata<U>
where
    U: RasterType,
{
    fn default() -> Self {
        Self::new()
    }
}

impl<U> RasterMetadata<U>
where
    U: RasterType,
{
    /// Creates a new empty RasterMetadata.
    pub fn new() -> Self {
        let layers = Vec::new();
        let shape = RasterDataShape {
            times: 0,
            layers: 0,
            rows: 0,
            cols: 0,
        };
        let block_size = BlockSize { rows: 0, cols: 0 };
        let epsg_code = 4326;
        let geo_transform = GeoTransform {
            x_ul: 0.,
            x_res: 0.,
            x_rot: 0.,
            y_ul: 0.,
            y_rot: 0.,
            y_res: 0.,
        };
        let overlap_size = 0;
        let date_indices = Vec::new();
        let layer_indices = Vec::new();
        let na_value = U::zero();

        RasterMetadata {
            layers,
            shape,
            block_size,
            epsg_code,
            geo_transform,
            overlap_size,
            date_indices,
            layer_indices,
            na_value: NumCast::from(na_value).unwrap(),
        }
    }
}

/// A block of raster data with its associated metadata and no-data value.
#[derive(Debug, Clone, PartialEq)]
pub struct RasterDataBlock<T>
where
    T: RasterType,
{
    /// The raster data array
    pub data: RasterData<T>,
    /// Metadata associated with this block
    pub metadata: RasterMetadata<T>,
    /// No-data value for this block
    pub no_data: T,
}