rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Flat georeferenced colour grid overlay layer.

use std::any::Any;

use super::{ColorRamp, GeoGrid, ScalarField2D};
use crate::layer::{Layer, LayerId, LayerKind};

/// A flat georeferenced colour grid overlay.
///
/// Each grid cell is coloured by evaluating the [`ColorRamp`] at the
/// cell's normalised [`ScalarField2D`] value.
///
/// This layer participates in the standard `MapState::pick` system:
/// a geographic pick query resolves to the grid cell via
/// [`GeoGrid::cell_at_geo`] and returns the cell index and scalar
/// value in the [`PickHit`](crate::picking::PickHit) result.
#[derive(Debug, Clone)]
pub struct GridScalarLayer {
    id: LayerId,
    name: String,
    visible: bool,
    opacity: f32,
    z_index: i32,
    /// Grid geometry.
    pub grid: GeoGrid,
    /// Scalar field values.
    pub field: ScalarField2D,
    /// Colour transfer function.
    pub ramp: ColorRamp,
}

impl GridScalarLayer {
    /// Create a new grid scalar layer.
    pub fn new(
        name: impl Into<String>,
        grid: GeoGrid,
        field: ScalarField2D,
        ramp: ColorRamp,
    ) -> Self {
        Self {
            id: LayerId::next(),
            name: name.into(),
            visible: true,
            opacity: 1.0,
            z_index: 0,
            grid,
            field,
            ramp,
        }
    }

    /// Replace the scalar field values (value-only update).
    pub fn update_field(&mut self, data: Vec<f32>) {
        self.field.update_values(data);
    }
}

impl Layer for GridScalarLayer {
    fn id(&self) -> LayerId {
        self.id
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn kind(&self) -> LayerKind {
        LayerKind::Visualization
    }

    fn visible(&self) -> bool {
        self.visible
    }

    fn set_visible(&mut self, visible: bool) {
        self.visible = visible;
    }

    fn opacity(&self) -> f32 {
        self.opacity
    }

    fn set_opacity(&mut self, opacity: f32) {
        self.opacity = opacity.clamp(0.0, 1.0);
    }

    fn z_index(&self) -> i32 {
        self.z_index
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::visualization::ColorStop;
    use rustial_math::GeoCoord;

    fn test_ramp() -> ColorRamp {
        ColorRamp::new(vec![
            ColorStop {
                value: 0.0,
                color: [0.0, 0.0, 1.0, 1.0],
            },
            ColorStop {
                value: 1.0,
                color: [1.0, 0.0, 0.0, 1.0],
            },
        ])
    }

    #[test]
    fn grid_scalar_layer_basics() {
        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 4, 4, 100.0, 100.0);
        let field = ScalarField2D::from_data(4, 4, vec![0.0; 16]);
        let layer = GridScalarLayer::new("density", grid, field, test_ramp());
        assert_eq!(layer.name(), "density");
        assert!(layer.visible());
        assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
        assert_eq!(layer.kind(), LayerKind::Visualization);
    }

    #[test]
    fn grid_scalar_layer_id_stable() {
        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
        let field = ScalarField2D::from_data(2, 2, vec![1.0; 4]);
        let layer = GridScalarLayer::new("test", grid, field, test_ramp());
        assert_eq!(layer.id(), layer.id());
    }

    #[test]
    fn grid_scalar_layer_downcast() {
        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
        let field = ScalarField2D::from_data(2, 2, vec![1.0; 4]);
        let layer: Box<dyn Layer> =
            Box::new(GridScalarLayer::new("test", grid, field, test_ramp()));
        assert!(layer.as_any().downcast_ref::<GridScalarLayer>().is_some());
    }

    #[test]
    fn update_field_bumps_value_generation() {
        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
        let field = ScalarField2D::from_data(2, 2, vec![0.0; 4]);
        let mut layer = GridScalarLayer::new("test", grid, field, test_ramp());
        assert_eq!(layer.field.value_generation, 0);
        layer.update_field(vec![1.0; 4]);
        assert_eq!(layer.field.value_generation, 1);
    }
}