use astrodyn_quantities::frame::{Planet, PlanetFixed, Topocentric};
use astrodyn_quantities::frame_transform::FrameTransform;
use glam::{DMat3, DVec3};
use uom::si::angle::radian;
use crate::geodetic::GeodeticStateTyped;
pub fn topocentric_enu_transform<P: Planet>(
site: &GeodeticStateTyped,
) -> FrameTransform<PlanetFixed<P>, Topocentric<P>> {
let (sinlat, coslat) = site.latitude.get::<radian>().sin_cos();
let (sinlon, coslon) = site.longitude.get::<radian>().sin_cos();
let east = DVec3::new(-sinlon, coslon, 0.0);
let north = DVec3::new(-sinlat * coslon, -sinlat * sinlon, coslat);
let up = DVec3::new(coslat * coslon, coslat * sinlon, sinlat);
let pcpf_to_enu = DMat3::from_cols(east, north, up).transpose();
FrameTransform::from_matrix(pcpf_to_enu)
}
#[cfg(test)]
mod tests {
use super::*;
use astrodyn_quantities::frame::Earth;
use astrodyn_quantities::prelude::{PlanetFixed, Vec3Ext};
use uom::si::angle::degree;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;
fn site(lat_deg: f64, lon_deg: f64) -> GeodeticStateTyped {
GeodeticStateTyped {
latitude: Angle::new::<degree>(lat_deg),
longitude: Angle::new::<degree>(lon_deg),
altitude: Length::new::<meter>(0.0),
}
}
#[test]
fn enu_at_lat0_lon0_matches_known_axes() {
let t = topocentric_enu_transform::<Earth>(&site(0.0, 0.0));
let x = t.apply(DVec3::X.m_at::<PlanetFixed<Earth>>()).raw_si();
let y = t.apply(DVec3::Y.m_at::<PlanetFixed<Earth>>()).raw_si();
let z = t.apply(DVec3::Z.m_at::<PlanetFixed<Earth>>()).raw_si();
assert!(
(x - DVec3::Z).length() < 1e-12,
"PCPF X should map to ENU Up, got {x:?}"
);
assert!(
(y - DVec3::X).length() < 1e-12,
"PCPF Y should map to ENU East, got {y:?}"
);
assert!(
(z - DVec3::Y).length() < 1e-12,
"PCPF Z should map to ENU North, got {z:?}"
);
}
#[test]
fn enu_round_trips_through_inverse() {
let t = topocentric_enu_transform::<Earth>(&site(34.5, -118.25));
let v = DVec3::new(1_234.5, -6_789.0, 4_242.0);
let there = t.apply(v.m_at::<PlanetFixed<Earth>>());
let back = t.inverse().apply(there).raw_si();
assert!(
(back - v).length() < 1e-9,
"round trip drifted: {back:?} vs {v:?}"
);
}
#[test]
fn enu_up_axis_is_geodetic_normal() {
let t = topocentric_enu_transform::<Earth>(&site(90.0, 45.0));
let up_image = t.apply(DVec3::Z.m_at::<PlanetFixed<Earth>>()).raw_si();
assert!(
(up_image - DVec3::Z).length() < 1e-12,
"pole normal should map to ENU Up, got {up_image:?}"
);
}
}