use super::color::{Color, ColorPalette};
use super::{DiagramScale, Point2D};
use crate::Eulumdat;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CartesianPoint {
pub x: f64,
pub y: f64,
pub gamma: f64,
pub intensity: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CartesianCurve {
pub points: Vec<CartesianPoint>,
pub c_angle: f64,
pub color: Color,
pub label: String,
}
impl CartesianCurve {
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn to_svg_path(&self) -> String {
if self.points.is_empty() {
return String::new();
}
let mut path = String::new();
for (i, point) in self.points.iter().enumerate() {
if i == 0 {
path.push_str(&format!("M {:.1} {:.1}", point.x, point.y));
} else {
path.push_str(&format!(" L {:.1} {:.1}", point.x, point.y));
}
}
path
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CartesianDiagram {
pub curves: Vec<CartesianCurve>,
pub x_ticks: Vec<f64>,
pub y_ticks: Vec<f64>,
pub scale: DiagramScale,
pub max_gamma: f64,
pub plot_width: f64,
pub plot_height: f64,
pub margin_left: f64,
pub margin_top: f64,
}
impl CartesianDiagram {
pub fn from_eulumdat(ldt: &Eulumdat, width: f64, height: f64, max_curves: usize) -> Self {
let margin_left = 60.0;
let margin_right = 25.0;
let margin_top = 35.0;
let margin_bottom = 50.0;
let plot_width = width - margin_left - margin_right;
let plot_height = height - margin_top - margin_bottom;
let max_intensity = ldt.max_intensity();
let max_gamma = ldt.g_angles.last().copied().unwrap_or(90.0);
let y_ticks = if max_intensity > 0.0 {
let step = DiagramScale::nice_step(max_intensity, 5);
let mut ticks = Vec::new();
let mut v = 0.0;
while v <= max_intensity * 1.05 {
ticks.push(v);
v += step;
}
ticks
} else {
vec![0.0, 25.0, 50.0, 75.0, 100.0]
};
let y_max = y_ticks.last().copied().unwrap_or(100.0);
let x_ticks = {
let step = if max_gamma <= 90.0 { 15.0 } else { 30.0 };
let mut ticks = Vec::new();
let mut v = 0.0;
while v <= max_gamma {
ticks.push(v);
v += step;
}
ticks
};
let scale = DiagramScale {
max_intensity,
scale_max: y_max,
grid_values: y_ticks.clone(),
};
let palette = ColorPalette::default();
let curves = generate_curves(
ldt,
margin_left,
margin_top,
plot_width,
plot_height,
y_max,
max_gamma,
max_curves,
&palette,
);
Self {
curves,
x_ticks,
y_ticks,
scale,
max_gamma,
plot_width,
plot_height,
margin_left,
margin_top,
}
}
pub fn from_eulumdat_for_plane(ldt: &Eulumdat, c_plane: f64, width: f64, height: f64) -> Self {
let margin_left = 60.0;
let margin_right = 25.0;
let margin_top = 35.0;
let margin_bottom = 50.0;
let plot_width = width - margin_left - margin_right;
let plot_height = height - margin_top - margin_bottom;
let max_gamma = ldt.g_angles.last().copied().unwrap_or(90.0);
let sampled: Vec<f64> = ldt
.g_angles
.iter()
.map(|&g| ldt.sample(c_plane, g))
.collect();
let max_intensity = sampled.iter().copied().fold(0.0_f64, f64::max);
let y_ticks = if max_intensity > 0.0 {
let step = DiagramScale::nice_step(max_intensity, 5);
let mut ticks = Vec::new();
let mut v = 0.0;
while v <= max_intensity * 1.05 {
ticks.push(v);
v += step;
}
ticks
} else {
vec![0.0, 25.0, 50.0, 75.0, 100.0]
};
let y_max = y_ticks.last().copied().unwrap_or(100.0);
let x_ticks = {
let step = if max_gamma <= 90.0 { 15.0 } else { 30.0 };
let mut ticks = Vec::new();
let mut v = 0.0;
while v <= max_gamma {
ticks.push(v);
v += step;
}
ticks
};
let scale = DiagramScale {
max_intensity,
scale_max: y_max,
grid_values: y_ticks.clone(),
};
let palette = ColorPalette::default();
let mut points = Vec::new();
for (&g_angle, &intensity) in ldt.g_angles.iter().zip(sampled.iter()) {
let x = margin_left + plot_width * (g_angle / max_gamma);
let y = margin_top + plot_height * (1.0 - intensity / y_max);
points.push(CartesianPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
let curve = CartesianCurve {
points,
c_angle: c_plane,
color: palette.color_at(0),
label: format!("C{:.0}°", c_plane),
};
Self {
curves: vec![curve],
x_ticks,
y_ticks,
scale,
max_gamma,
plot_width,
plot_height,
margin_left,
margin_top,
}
}
pub fn all_data_points(&self) -> Vec<(&CartesianCurve, Vec<Point2D>)> {
self.curves
.iter()
.map(|curve| {
let points: Vec<Point2D> = curve
.points
.iter()
.map(|p| Point2D::new(p.x, p.y))
.collect();
(curve, points)
})
.collect()
}
}
#[allow(clippy::too_many_arguments)]
fn generate_curves(
ldt: &Eulumdat,
margin_left: f64,
margin_top: f64,
plot_width: f64,
plot_height: f64,
y_max: f64,
max_gamma: f64,
max_curves: usize,
palette: &ColorPalette,
) -> Vec<CartesianCurve> {
if ldt.intensities.is_empty() || ldt.g_angles.is_empty() || y_max <= 0.0 {
return Vec::new();
}
let mut curves = Vec::new();
let num_curves = ldt.intensities.len().min(max_curves);
for (c_idx, intensities) in ldt.intensities.iter().take(num_curves).enumerate() {
let mut points = Vec::new();
for (i, (&g_angle, &intensity)) in ldt.g_angles.iter().zip(intensities.iter()).enumerate() {
let _ = i; let x = margin_left + plot_width * (g_angle / max_gamma);
let y = margin_top + plot_height * (1.0 - intensity / y_max);
points.push(CartesianPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
let color = palette.color_at(c_idx);
let label = if c_idx < ldt.c_angles.len() {
format!("C{:.0}°", ldt.c_angles[c_idx])
} else {
format!("C{}", c_idx)
};
curves.push(CartesianCurve {
points,
c_angle: ldt.c_angles.get(c_idx).copied().unwrap_or(0.0),
color,
label,
});
}
curves
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::field_reassign_with_default)]
fn create_test_ldt() -> Eulumdat {
let mut ldt = Eulumdat::default();
ldt.c_angles = vec![0.0, 90.0, 180.0, 270.0];
ldt.g_angles = vec![0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 90.0];
ldt.intensities = vec![
vec![100.0, 95.0, 85.0, 70.0, 50.0, 25.0, 10.0],
vec![90.0, 88.0, 80.0, 65.0, 45.0, 22.0, 8.0],
vec![100.0, 95.0, 85.0, 70.0, 50.0, 25.0, 10.0],
vec![90.0, 88.0, 80.0, 65.0, 45.0, 22.0, 8.0],
];
ldt
}
#[test]
fn test_cartesian_diagram_generation() {
let ldt = create_test_ldt();
let diagram = CartesianDiagram::from_eulumdat(&ldt, 500.0, 380.0, 8);
assert_eq!(diagram.curves.len(), 4);
for curve in &diagram.curves {
assert_eq!(curve.points.len(), ldt.g_angles.len());
}
assert!(!diagram.x_ticks.is_empty());
assert!(!diagram.y_ticks.is_empty());
}
#[test]
fn test_cartesian_curve_to_svg() {
let ldt = create_test_ldt();
let diagram = CartesianDiagram::from_eulumdat(&ldt, 500.0, 380.0, 8);
let path = diagram.curves[0].to_svg_path();
assert!(path.starts_with("M "));
assert!(!path.ends_with(" Z")); }
#[test]
fn test_max_curves_limit() {
let ldt = create_test_ldt();
let diagram = CartesianDiagram::from_eulumdat(&ldt, 500.0, 380.0, 2);
assert_eq!(diagram.curves.len(), 2);
}
#[test]
fn test_nice_step() {
assert!((DiagramScale::nice_step(100.0, 5) - 20.0).abs() < 0.01);
assert!((DiagramScale::nice_step(47.0, 5) - 10.0).abs() < 0.01);
assert!((DiagramScale::nice_step(1000.0, 5) - 200.0).abs() < 0.01);
}
}