use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GrowthVisualization {
pub height: f32,
pub max_height: f32,
pub stage: String,
pub diameter: f32,
pub leaf_area: f32,
pub maturity: f32,
}
impl GrowthVisualization {
#[must_use]
pub fn from_model(model: &crate::growth::GrowthModel, day: f32) -> Self {
let height = model.height_at_day(day);
let stage = crate::growth::growth_stage(height, model.max_height);
let diameter = crate::biomass::height_to_diameter(height, 0.05);
let leaf_area = crate::biomass::height_to_leaf_area(height, 0.3);
let maturity = if model.max_height > 0.0 {
(height / model.max_height).clamp(0.0, 1.0)
} else {
0.0
};
Self {
height,
max_height: model.max_height,
stage: format!("{stage:?}"),
diameter,
leaf_area,
maturity,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RootVisualization {
pub root_type: String,
pub max_depth: f32,
pub spread_radius: f32,
pub water_uptake_rate: f32,
pub stabilization: f32,
}
impl RootVisualization {
#[must_use]
pub fn from_root_system(root: &crate::root::RootSystem) -> Self {
Self {
root_type: format!("{:?}", root.root_type),
max_depth: root.max_depth_m,
spread_radius: root.spread_radius_m,
water_uptake_rate: root.water_uptake_rate,
stabilization: root.stabilization_factor(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EcosystemMap {
pub density: Vec<f64>,
pub dimensions: [usize; 2],
pub cell_size: f64,
pub species: String,
pub max_density: f64,
}
impl EcosystemMap {
#[must_use]
pub fn uniform(
nx: usize,
ny: usize,
cell_size: f64,
species: &str,
density_value: f64,
) -> Self {
Self {
density: vec![density_value; nx * ny],
dimensions: [nx, ny],
cell_size,
species: species.to_string(),
max_density: density_value,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SeasonalColor {
pub season: crate::season::Season,
pub foliage_rgb: [f32; 3],
pub growth_modifier: f32,
pub daylight_hours: f32,
}
impl SeasonalColor {
#[must_use]
pub fn at_day(day_of_year: u16, latitude_deg: f32) -> Self {
let season = crate::season::Season::from_day(day_of_year);
let growth = crate::season::growth_modifier_at(day_of_year, latitude_deg);
let daylight = crate::season::daylight_hours_at(day_of_year, latitude_deg);
let foliage_rgb = match season {
crate::season::Season::Spring => [0.4, 0.8, 0.3], crate::season::Season::Summer => [0.2, 0.6, 0.15], crate::season::Season::Autumn => [0.8, 0.5, 0.15], crate::season::Season::Winter => [0.5, 0.45, 0.35], };
Self {
season,
foliage_rgb,
growth_modifier: growth,
daylight_hours: daylight,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn growth_viz_oak() {
let model = crate::growth::GrowthModel::oak();
let viz = GrowthVisualization::from_model(&model, 365.0 * 5.0);
assert!(viz.height > 0.0);
assert!(viz.diameter > 0.0);
assert!(viz.maturity > 0.0 && viz.maturity <= 1.0);
}
#[test]
fn growth_viz_day_zero() {
let model = crate::growth::GrowthModel::oak();
let viz = GrowthVisualization::from_model(&model, 0.0);
assert!(viz.height > 0.0); assert!(viz.maturity < 0.1);
}
#[test]
fn root_viz_oak() {
let root = crate::root::RootSystem::oak();
let viz = RootVisualization::from_root_system(&root);
assert!(viz.max_depth > 1.0);
assert!(viz.spread_radius > 0.0);
assert!(viz.stabilization > 0.0);
assert!(viz.root_type.contains("Taproot"));
}
#[test]
fn root_viz_grass() {
let root = crate::root::RootSystem::grass();
let viz = RootVisualization::from_root_system(&root);
assert!(viz.root_type.contains("Fibrous"));
}
#[test]
fn ecosystem_map_uniform() {
let map = EcosystemMap::uniform(5, 5, 10.0, "Oak", 100.0);
assert_eq!(map.density.len(), 25);
assert!((map.max_density - 100.0).abs() < 0.01);
}
#[test]
fn seasonal_color_summer() {
let sc = SeasonalColor::at_day(180, 45.0);
assert!(matches!(sc.season, crate::season::Season::Summer));
assert!(sc.growth_modifier > 0.5);
assert!(sc.daylight_hours > 14.0);
assert!(sc.foliage_rgb[1] > sc.foliage_rgb[0]);
}
#[test]
fn seasonal_color_winter() {
let sc = SeasonalColor::at_day(15, 45.0);
assert!(matches!(sc.season, crate::season::Season::Winter));
assert!(sc.growth_modifier < 0.3);
}
#[test]
fn seasonal_color_serializes() {
let sc = SeasonalColor::at_day(100, 30.0);
let json = serde_json::to_string(&sc);
assert!(json.is_ok());
}
}