Skip to main content

rustial_engine/visualization/
grid_scalar_layer.rs

1//! Flat georeferenced colour grid overlay layer.
2
3use std::any::Any;
4
5use super::{ColorRamp, GeoGrid, ScalarField2D};
6use crate::layer::{Layer, LayerId, LayerKind};
7
8/// A flat georeferenced colour grid overlay.
9///
10/// Each grid cell is coloured by evaluating the [`ColorRamp`] at the
11/// cell's normalised [`ScalarField2D`] value.
12///
13/// This layer participates in the standard `MapState::pick` system:
14/// a geographic pick query resolves to the grid cell via
15/// [`GeoGrid::cell_at_geo`] and returns the cell index and scalar
16/// value in the [`PickHit`](crate::picking::PickHit) result.
17#[derive(Debug, Clone)]
18pub struct GridScalarLayer {
19    id: LayerId,
20    name: String,
21    visible: bool,
22    opacity: f32,
23    z_index: i32,
24    /// Grid geometry.
25    pub grid: GeoGrid,
26    /// Scalar field values.
27    pub field: ScalarField2D,
28    /// Colour transfer function.
29    pub ramp: ColorRamp,
30}
31
32impl GridScalarLayer {
33    /// Create a new grid scalar layer.
34    pub fn new(
35        name: impl Into<String>,
36        grid: GeoGrid,
37        field: ScalarField2D,
38        ramp: ColorRamp,
39    ) -> Self {
40        Self {
41            id: LayerId::next(),
42            name: name.into(),
43            visible: true,
44            opacity: 1.0,
45            z_index: 0,
46            grid,
47            field,
48            ramp,
49        }
50    }
51
52    /// Replace the scalar field values (value-only update).
53    pub fn update_field(&mut self, data: Vec<f32>) {
54        self.field.update_values(data);
55    }
56}
57
58impl Layer for GridScalarLayer {
59    fn id(&self) -> LayerId {
60        self.id
61    }
62
63    fn name(&self) -> &str {
64        &self.name
65    }
66
67    fn kind(&self) -> LayerKind {
68        LayerKind::Visualization
69    }
70
71    fn visible(&self) -> bool {
72        self.visible
73    }
74
75    fn set_visible(&mut self, visible: bool) {
76        self.visible = visible;
77    }
78
79    fn opacity(&self) -> f32 {
80        self.opacity
81    }
82
83    fn set_opacity(&mut self, opacity: f32) {
84        self.opacity = opacity.clamp(0.0, 1.0);
85    }
86
87    fn z_index(&self) -> i32 {
88        self.z_index
89    }
90
91    fn as_any(&self) -> &dyn Any {
92        self
93    }
94
95    fn as_any_mut(&mut self) -> &mut dyn Any {
96        self
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::visualization::ColorStop;
104    use rustial_math::GeoCoord;
105
106    fn test_ramp() -> ColorRamp {
107        ColorRamp::new(vec![
108            ColorStop {
109                value: 0.0,
110                color: [0.0, 0.0, 1.0, 1.0],
111            },
112            ColorStop {
113                value: 1.0,
114                color: [1.0, 0.0, 0.0, 1.0],
115            },
116        ])
117    }
118
119    #[test]
120    fn grid_scalar_layer_basics() {
121        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 4, 4, 100.0, 100.0);
122        let field = ScalarField2D::from_data(4, 4, vec![0.0; 16]);
123        let layer = GridScalarLayer::new("density", grid, field, test_ramp());
124        assert_eq!(layer.name(), "density");
125        assert!(layer.visible());
126        assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
127        assert_eq!(layer.kind(), LayerKind::Visualization);
128    }
129
130    #[test]
131    fn grid_scalar_layer_id_stable() {
132        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
133        let field = ScalarField2D::from_data(2, 2, vec![1.0; 4]);
134        let layer = GridScalarLayer::new("test", grid, field, test_ramp());
135        assert_eq!(layer.id(), layer.id());
136    }
137
138    #[test]
139    fn grid_scalar_layer_downcast() {
140        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
141        let field = ScalarField2D::from_data(2, 2, vec![1.0; 4]);
142        let layer: Box<dyn Layer> =
143            Box::new(GridScalarLayer::new("test", grid, field, test_ramp()));
144        assert!(layer.as_any().downcast_ref::<GridScalarLayer>().is_some());
145    }
146
147    #[test]
148    fn update_field_bumps_value_generation() {
149        let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 10.0, 10.0);
150        let field = ScalarField2D::from_data(2, 2, vec![0.0; 4]);
151        let mut layer = GridScalarLayer::new("test", grid, field, test_ramp());
152        assert_eq!(layer.field.value_generation, 0);
153        layer.update_field(vec![1.0; 4]);
154        assert_eq!(layer.field.value_generation, 1);
155    }
156}