use super::{DiagramScale, Point2D};
use crate::{Eulumdat, Symmetry};
use std::f64::consts::FRAC_PI_2;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PolarPoint {
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 PolarCurve {
pub points: Vec<PolarPoint>,
pub c_angle: f64,
pub label: String,
}
impl PolarCurve {
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn max_intensity(&self) -> f64 {
self.points.iter().map(|p| p.intensity).fold(0.0, f64::max)
}
pub fn to_svg_path(&self, center_x: f64, center_y: f64, scale: f64) -> String {
if self.points.is_empty() {
return String::new();
}
let mut path = String::new();
for (i, point) in self.points.iter().enumerate() {
let sx = center_x + point.x / scale;
let sy = center_y + point.y / scale;
if i == 0 {
path.push_str(&format!("M {:.1} {:.1}", sx, sy));
} else {
path.push_str(&format!(" L {:.1} {:.1}", sx, sy));
}
}
path.push_str(" Z");
path
}
pub fn screen_points(&self, center_x: f64, center_y: f64, scale: f64) -> Vec<Point2D> {
self.points
.iter()
.map(|p| Point2D::new(center_x + p.x / scale, center_y + p.y / scale))
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PolarDiagram {
pub c0_c180_curve: PolarCurve,
pub c90_c270_curve: PolarCurve,
pub scale: DiagramScale,
pub symmetry: Symmetry,
}
impl PolarDiagram {
pub fn from_eulumdat(ldt: &Eulumdat) -> Self {
let (c0_c180_points, c90_c270_points, max_intensity) = calculate_vectors(ldt);
let c0_c180_curve = PolarCurve {
points: c0_c180_points,
c_angle: 0.0,
label: "C0-C180".to_string(),
};
let c90_c270_curve = PolarCurve {
points: c90_c270_points,
c_angle: 90.0,
label: "C90-C270".to_string(),
};
let scale = DiagramScale::from_max_intensity(max_intensity, 5);
Self {
c0_c180_curve,
c90_c270_curve,
scale,
symmetry: ldt.symmetry,
}
}
pub fn from_eulumdat_for_plane(ldt: &Eulumdat, c_plane: f64) -> Self {
let opposite = (c_plane + 180.0) % 360.0;
let mut points = Vec::new();
let mut max_intensity: f64 = 0.0;
for &g_angle in &ldt.g_angles {
let intensity = ldt.sample(c_plane, g_angle);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
points.push(PolarPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
for j in (0..ldt.g_angles.len()).rev() {
let g_angle = ldt.g_angles[j];
let intensity = ldt.sample(opposite, g_angle);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
points.push(PolarPoint {
x: -x,
y,
gamma: g_angle,
intensity,
});
}
let label = format!("C{:.0}-C{:.0}", c_plane, opposite);
let curve = PolarCurve {
points,
c_angle: c_plane,
label,
};
let scale = DiagramScale::from_max_intensity(max_intensity, 5);
Self {
c0_c180_curve: curve,
c90_c270_curve: PolarCurve {
points: Vec::new(),
c_angle: 0.0,
label: String::new(),
},
scale,
symmetry: ldt.symmetry,
}
}
pub fn show_c90_c270(&self) -> bool {
self.symmetry != Symmetry::VerticalAxis && !self.c90_c270_curve.is_empty()
}
}
fn calculate_vectors(ldt: &Eulumdat) -> (Vec<PolarPoint>, Vec<PolarPoint>, f64) {
if ldt.intensities.is_empty() || ldt.g_angles.is_empty() {
return (Vec::new(), Vec::new(), 0.0);
}
let mut vector_c0_c180 = Vec::new();
let mut vector_c90_c270 = Vec::new();
let mut max_intensity: f64 = 0.0;
let c_start = match ldt.symmetry {
Symmetry::PlaneC90C270 => ldt.c_angles.iter().position(|&c| c >= 90.0).unwrap_or(0),
_ => 0,
};
let c0_idx = if ldt.symmetry == Symmetry::PlaneC90C270 {
let mc = ldt.intensities.len();
(0..mc)
.find(|&i| {
ldt.c_angles
.get(c_start + i)
.is_some_and(|&a| (a - 180.0).abs() < 0.1)
})
.unwrap_or(mc / 2)
} else {
0
};
if c0_idx < ldt.intensities.len() {
let intensities = &ldt.intensities[c0_idx];
for (j, &g_angle) in ldt.g_angles.iter().enumerate() {
let intensity = intensities.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c0_c180.push(PolarPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
if matches!(
ldt.symmetry,
Symmetry::VerticalAxis | Symmetry::PlaneC90C270 | Symmetry::BothPlanes
) {
for j in (0..ldt.g_angles.len()).rev() {
let point = &vector_c0_c180[j];
vector_c0_c180.push(PolarPoint {
x: -point.x,
y: point.y,
gamma: point.gamma,
intensity: point.intensity,
});
}
} else {
let c180_idx = (0..ldt.intensities.len()).find(|&i| {
ldt.c_angles
.get(i)
.is_some_and(|&a| (a - 180.0).abs() < 0.1)
});
if let Some(idx) = c180_idx {
let intensities_180 = &ldt.intensities[idx];
for j in (0..ldt.g_angles.len()).rev() {
let g_angle = ldt.g_angles[j];
let intensity = intensities_180.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c0_c180.push(PolarPoint {
x: -x,
y,
gamma: g_angle,
intensity,
});
}
}
}
}
if ldt.symmetry == Symmetry::VerticalAxis {
} else if ldt.symmetry == Symmetry::PlaneC90C270 {
let intensities = &ldt.intensities[0];
for (j, &g_angle) in ldt.g_angles.iter().enumerate() {
let intensity = intensities.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c90_c270.push(PolarPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
let mc = ldt.intensities.len();
let c270_idx = (0..mc).find(|&i| {
ldt.c_angles
.get(c_start + i)
.is_some_and(|&a| (a - 270.0).abs() < 0.1)
});
if let Some(idx) = c270_idx {
let intensities_270 = &ldt.intensities[idx];
for j in (0..ldt.g_angles.len()).rev() {
let g_angle = ldt.g_angles[j];
let intensity = intensities_270.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c90_c270.push(PolarPoint {
x: -x,
y,
gamma: g_angle,
intensity,
});
}
}
} else {
let c90_idx = (0..ldt.intensities.len())
.find(|&i| ldt.c_angles.get(i).is_some_and(|&a| (a - 90.0).abs() < 0.1));
if let Some(idx) = c90_idx {
let intensities = &ldt.intensities[idx];
for (j, &g_angle) in ldt.g_angles.iter().enumerate() {
let intensity = intensities.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c90_c270.push(PolarPoint {
x,
y,
gamma: g_angle,
intensity,
});
}
if matches!(ldt.symmetry, Symmetry::PlaneC0C180 | Symmetry::BothPlanes) {
for j in (0..ldt.g_angles.len()).rev() {
let point = &vector_c90_c270[j];
vector_c90_c270.push(PolarPoint {
x: -point.x,
y: point.y,
gamma: point.gamma,
intensity: point.intensity,
});
}
} else if ldt.symmetry == Symmetry::None {
let c270_idx = (0..ldt.intensities.len()).find(|&i| {
ldt.c_angles
.get(i)
.is_some_and(|&a| (a - 270.0).abs() < 0.1)
});
if let Some(idx270) = c270_idx {
let intensities_270 = &ldt.intensities[idx270];
for j in (0..ldt.g_angles.len()).rev() {
let g_angle = ldt.g_angles[j];
let intensity = intensities_270.get(j).copied().unwrap_or(0.0);
max_intensity = max_intensity.max(intensity);
let angle_rad = -g_angle.to_radians() + FRAC_PI_2;
let x = intensity * angle_rad.cos();
let y = intensity * angle_rad.sin();
vector_c90_c270.push(PolarPoint {
x: -x,
y,
gamma: g_angle,
intensity,
});
}
}
}
}
}
(vector_c0_c180, vector_c90_c270, max_intensity)
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::field_reassign_with_default)]
fn create_test_ldt() -> Eulumdat {
let mut ldt = Eulumdat::default();
ldt.symmetry = Symmetry::BothPlanes;
ldt.c_angles = vec![0.0, 30.0, 60.0, 90.0];
ldt.g_angles = vec![0.0, 30.0, 60.0, 90.0];
ldt.intensities = vec![
vec![100.0, 90.0, 70.0, 40.0], vec![95.0, 85.0, 65.0, 35.0], vec![90.0, 80.0, 60.0, 30.0], vec![85.0, 75.0, 55.0, 25.0], ];
ldt
}
#[test]
fn test_polar_diagram_generation() {
let ldt = create_test_ldt();
let polar = PolarDiagram::from_eulumdat(&ldt);
assert!(!polar.c0_c180_curve.is_empty());
assert!(polar.scale.max_intensity > 0.0);
assert!(polar.scale.scale_max >= polar.scale.max_intensity);
}
#[test]
fn test_polar_curve_to_svg() {
let ldt = create_test_ldt();
let polar = PolarDiagram::from_eulumdat(&ldt);
let path = polar.c0_c180_curve.to_svg_path(250.0, 250.0, 1.0);
assert!(path.starts_with("M "));
assert!(path.ends_with(" Z"));
}
#[test]
fn test_symmetry_handling() {
let mut ldt = create_test_ldt();
ldt.symmetry = Symmetry::VerticalAxis;
let polar = PolarDiagram::from_eulumdat(&ldt);
assert!(!polar.show_c90_c270());
}
}