use vedaksha_math::angle::normalize_degrees;
#[derive(Debug, Clone, Copy)]
pub struct CompositePosition {
pub longitude: f64,
pub speed: f64,
}
#[must_use]
pub fn compute_composite(
lons_a: &[f64],
lons_b: &[f64],
speeds_a: &[f64],
speeds_b: &[f64],
) -> Vec<CompositePosition> {
assert_eq!(lons_a.len(), lons_b.len());
assert_eq!(lons_a.len(), speeds_a.len());
assert_eq!(lons_a.len(), speeds_b.len());
lons_a
.iter()
.zip(lons_b.iter())
.zip(speeds_a.iter().zip(speeds_b.iter()))
.map(|((&lon_a, &lon_b), (&spd_a, &spd_b))| {
let longitude = shorter_arc_midpoint(lon_a, lon_b);
let speed = f64::midpoint(spd_a, spd_b);
CompositePosition { longitude, speed }
})
.collect()
}
fn shorter_arc_midpoint(lon_a: f64, lon_b: f64) -> f64 {
let diff = normalize_degrees(lon_b - lon_a);
if diff <= 180.0 {
normalize_degrees(lon_a + diff / 2.0)
} else {
normalize_degrees(lon_a + (diff - 360.0) / 2.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn midpoint_of_10_and_30_is_20() {
let result = shorter_arc_midpoint(10.0, 30.0);
assert!((result - 20.0).abs() < EPS, "Expected 20.0, got {result}");
}
#[test]
fn midpoint_wraps_around_zero() {
let result = shorter_arc_midpoint(350.0, 10.0);
assert!((result - 0.0).abs() < EPS, "Expected 0.0, got {result}");
}
#[test]
fn midpoint_of_0_and_180_is_90() {
let result = shorter_arc_midpoint(0.0, 180.0);
assert!((result - 90.0).abs() < EPS, "Expected 90.0, got {result}");
}
#[test]
fn composite_preserves_planet_count() {
let lons_a = [0.0_f64, 90.0, 180.0];
let lons_b = [30.0_f64, 60.0, 210.0];
let spds_a = [1.0_f64, 0.5, 0.8];
let spds_b = [0.5_f64, 0.3, 0.2];
let composite = compute_composite(&lons_a, &lons_b, &spds_a, &spds_b);
assert_eq!(composite.len(), 3, "Planet count must be preserved");
}
#[test]
fn composite_average_speed_is_correct() {
let lons_a = [0.0_f64];
let lons_b = [60.0_f64];
let spds_a = [2.0_f64];
let spds_b = [1.0_f64];
let composite = compute_composite(&lons_a, &lons_b, &spds_a, &spds_b);
assert_eq!(composite.len(), 1);
assert!(
(composite[0].speed - 1.5).abs() < EPS,
"Expected average speed 1.5, got {}",
composite[0].speed
);
}
#[test]
fn composite_longitude_correct_for_simple_pair() {
let lons_a = [10.0_f64];
let lons_b = [30.0_f64];
let spds_a = [1.0_f64];
let spds_b = [1.0_f64];
let composite = compute_composite(&lons_a, &lons_b, &spds_a, &spds_b);
assert!(
(composite[0].longitude - 20.0).abs() < EPS,
"Expected midpoint 20.0, got {}",
composite[0].longitude
);
}
#[test]
fn composite_wraps_correctly() {
let lons_a = [350.0_f64];
let lons_b = [10.0_f64];
let spds_a = [0.0_f64];
let spds_b = [0.0_f64];
let composite = compute_composite(&lons_a, &lons_b, &spds_a, &spds_b);
assert!(
(composite[0].longitude - 0.0).abs() < EPS,
"Expected wrap-around midpoint 0.0, got {}",
composite[0].longitude
);
}
}