use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeologicGridVisualization {
pub cells: Vec<u16>,
pub ages: Vec<f64>,
pub dimensions: [usize; 2],
pub cell_size: f64,
pub rock_types: Vec<String>,
}
impl GeologicGridVisualization {
#[must_use]
pub fn from_grid(grid: &crate::grid::GeologicGrid) -> Self {
let mut rock_types: Vec<String> = vec!["empty".to_string()];
let mut cells = Vec::with_capacity(grid.nx * grid.ny);
let mut ages = Vec::with_capacity(grid.nx * grid.ny);
for y in 0..grid.ny {
for x in 0..grid.nx {
if let Some(unit) = grid.get(x, y) {
let type_idx = rock_types
.iter()
.position(|t| t == &unit.rock_type)
.unwrap_or_else(|| {
rock_types.push(unit.rock_type.clone());
rock_types.len() - 1
});
cells.push(type_idx as u16);
ages.push(unit.age_ma);
} else {
cells.push(0);
ages.push(0.0);
}
}
}
Self {
cells,
ages,
dimensions: [grid.nx, grid.ny],
cell_size: grid.cell_size_m,
rock_types,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrystalVisualization {
pub lattice_vectors: [[f64; 3]; 3],
pub atom_positions: Vec<[f64; 3]>,
pub atom_types: Vec<u8>,
pub system: String,
}
impl CrystalVisualization {
#[must_use]
pub fn from_unit_cell(
cell: &crate::crystallography::UnitCell,
system: crate::crystal::CrystalSystem,
) -> Self {
let a = cell.a;
let b = cell.b;
let c = cell.c;
let beta = cell.beta.to_radians();
let gamma = cell.gamma.to_radians();
let cos_alpha = cell.alpha.to_radians().cos();
let cos_beta = beta.cos();
let cos_gamma = gamma.cos();
let sin_gamma = gamma.sin();
let va = [a, 0.0, 0.0];
let vb = [b * cos_gamma, b * sin_gamma, 0.0];
let cx = c * cos_beta;
let cy = if sin_gamma.abs() > 1e-10 {
c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma
} else {
0.0
};
let cz_sq = c * c - cx * cx - cy * cy;
let cz = if cz_sq > 0.0 { cz_sq.sqrt() } else { 0.0 };
let vc = [cx, cy, cz];
Self {
lattice_vectors: [va, vb, vc],
atom_positions: Vec::new(),
atom_types: Vec::new(),
system: format!("{system:?}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StratColumnVisualization {
pub layers: Vec<StratLayerViz>,
pub total_height: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StratLayerViz {
pub name: String,
pub rock_type: String,
pub thickness: f64,
pub age_base_ma: f64,
pub age_top_ma: f64,
}
impl StratColumnVisualization {
#[must_use]
pub fn from_layers(layers: Vec<StratLayerViz>) -> Self {
let total_height = layers.iter().map(|l| l.thickness).sum();
Self {
layers,
total_height,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StrikeDipMarker {
pub position: [f64; 2],
pub strike_deg: f64,
pub dip_deg: f64,
pub dip_direction_deg: f64,
}
impl StrikeDipMarker {
#[must_use]
pub fn new(position: [f64; 2], sd: &crate::grid::StrikeDip) -> Self {
Self {
position,
strike_deg: sd.strike_deg,
dip_deg: sd.dip_deg,
dip_direction_deg: sd.dip_direction(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn geologic_grid_empty() {
let grid = crate::grid::GeologicGrid::new(3, 3, 10.0);
let viz = GeologicGridVisualization::from_grid(&grid);
assert_eq!(viz.dimensions, [3, 3]);
assert_eq!(viz.cells.len(), 9);
assert!(viz.cells.iter().all(|&c| c == 0));
}
#[test]
fn geologic_grid_with_units() {
let mut grid = crate::grid::GeologicGrid::new(2, 2, 100.0);
grid.set(
0,
0,
crate::grid::GeologicUnit {
name: "Granite".into(),
rock_type: "igneous".into(),
age_ma: 300.0,
},
);
grid.set(
1,
0,
crate::grid::GeologicUnit {
name: "Sandstone".into(),
rock_type: "sedimentary".into(),
age_ma: 100.0,
},
);
let viz = GeologicGridVisualization::from_grid(&grid);
assert_eq!(viz.rock_types.len(), 3); assert!(viz.cells[0] > 0); assert!(viz.cells[1] > 0); assert_eq!(viz.cells[2], 0); }
#[test]
fn crystal_cubic() {
let cell = crate::crystallography::UnitCell {
a: 5.0,
b: 5.0,
c: 5.0,
alpha: 90.0,
beta: 90.0,
gamma: 90.0,
};
let viz = CrystalVisualization::from_unit_cell(&cell, crate::crystal::CrystalSystem::Cubic);
assert!((viz.lattice_vectors[0][0] - 5.0).abs() < 0.01);
assert!(viz.lattice_vectors[0][1].abs() < 0.01);
assert!(viz.system.contains("Cubic"));
}
#[test]
fn strat_column_from_layers() {
let layers = vec![
StratLayerViz {
name: "Limestone".into(),
rock_type: "sedimentary".into(),
thickness: 50.0,
age_base_ma: 350.0,
age_top_ma: 340.0,
},
StratLayerViz {
name: "Shale".into(),
rock_type: "sedimentary".into(),
thickness: 30.0,
age_base_ma: 340.0,
age_top_ma: 330.0,
},
];
let viz = StratColumnVisualization::from_layers(layers);
assert_eq!(viz.layers.len(), 2);
assert!((viz.total_height - 80.0).abs() < 0.01);
}
#[test]
fn strike_dip_marker() {
let sd = crate::grid::StrikeDip {
strike_deg: 45.0,
dip_deg: 30.0,
};
let marker = StrikeDipMarker::new([100.0, 200.0], &sd);
assert!((marker.strike_deg - 45.0).abs() < 0.01);
assert!((marker.dip_direction_deg - 135.0).abs() < 0.01);
}
#[test]
fn geologic_grid_serializes() {
let grid = crate::grid::GeologicGrid::new(2, 2, 10.0);
let viz = GeologicGridVisualization::from_grid(&grid);
let json = serde_json::to_string(&viz);
assert!(json.is_ok());
}
}