use arika::earth::eop::{NutationCorrections, PolarMotion, Ut1Offset};
use arika::earth::geodetic::Geodetic;
use arika::epoch::{Epoch, Utc};
use arika::frame::{self, Ecef, Eci, Rotation, Vec3};
pub trait PositionEop: Ut1Offset + PolarMotion + NutationCorrections + Send + Sync {}
impl<T: Ut1Offset + PolarMotion + NutationCorrections + Send + Sync> PositionEop for T {}
pub struct GcrsEopStorage(Box<dyn PositionEop>);
impl GcrsEopStorage {
pub fn new(provider: impl PositionEop + 'static) -> Self {
Self(Box::new(provider))
}
}
impl std::fmt::Debug for GcrsEopStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GcrsEopStorage").finish_non_exhaustive()
}
}
impl Ut1Offset for GcrsEopStorage {
fn dut1(&self, utc_mjd: f64) -> f64 {
self.0.dut1(utc_mjd)
}
}
impl PolarMotion for GcrsEopStorage {
fn x_pole(&self, utc_mjd: f64) -> f64 {
self.0.x_pole(utc_mjd)
}
fn y_pole(&self, utc_mjd: f64) -> f64 {
self.0.y_pole(utc_mjd)
}
}
impl NutationCorrections for GcrsEopStorage {
fn dx(&self, utc_mjd: f64) -> f64 {
self.0.dx(utc_mjd)
}
fn dy(&self, utc_mjd: f64) -> f64 {
self.0.dy(utc_mjd)
}
}
pub trait EarthFrameBridge: Eci + Sized + 'static {
type Fixed: Ecef;
type EopStorage: Send + Sync + 'static;
fn to_geodetic(pos: &Vec3<Self>, utc: &Epoch<Utc>, eop: &Self::EopStorage) -> Geodetic;
fn fixed_to_inertial(utc: &Epoch<Utc>, eop: &Self::EopStorage) -> Rotation<Self::Fixed, Self>;
}
impl EarthFrameBridge for frame::SimpleEci {
type Fixed = frame::SimpleEcef;
type EopStorage = ();
fn to_geodetic(pos: &Vec3<frame::SimpleEci>, utc: &Epoch<Utc>, _eop: &()) -> Geodetic {
let era = utc.to_ut1_naive().era();
let rot = Rotation::<frame::SimpleEci, frame::SimpleEcef>::from_era(era);
rot.transform(pos).to_geodetic()
}
fn fixed_to_inertial(
utc: &Epoch<Utc>,
_eop: &(),
) -> Rotation<frame::SimpleEcef, frame::SimpleEci> {
let era = utc.to_ut1_naive().era();
Rotation::<frame::SimpleEcef, frame::SimpleEci>::from_era(era)
}
}
impl EarthFrameBridge for frame::Gcrs {
type Fixed = frame::Itrs;
type EopStorage = GcrsEopStorage;
fn to_geodetic(pos: &Vec3<frame::Gcrs>, utc: &Epoch<Utc>, eop: &GcrsEopStorage) -> Geodetic {
let rot = Rotation::<frame::Gcrs, frame::Itrs>::iau2006_full_from_utc(utc, eop);
rot.transform(pos).to_geodetic()
}
fn fixed_to_inertial(
utc: &Epoch<Utc>,
eop: &GcrsEopStorage,
) -> Rotation<frame::Itrs, frame::Gcrs> {
Rotation::<frame::Gcrs, frame::Itrs>::iau2006_full_from_utc(utc, eop).inverse()
}
}
#[cfg(test)]
mod tests {
use super::*;
use arika::earth::R as R_EARTH;
use arika::earth::eop::LengthOfDay;
use arika::epoch::Epoch;
struct ZeroEop;
impl Ut1Offset for ZeroEop {
fn dut1(&self, _: f64) -> f64 {
0.0
}
}
impl PolarMotion for ZeroEop {
fn x_pole(&self, _: f64) -> f64 {
0.0
}
fn y_pole(&self, _: f64) -> f64 {
0.0
}
}
impl NutationCorrections for ZeroEop {
fn dx(&self, _: f64) -> f64 {
0.0
}
fn dy(&self, _: f64) -> f64 {
0.0
}
}
impl LengthOfDay for ZeroEop {
fn lod(&self, _: f64) -> f64 {
0.0
}
}
#[test]
fn simple_eci_to_geodetic_altitude() {
let utc = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let alt_km = 400.0;
let pos = Vec3::<frame::SimpleEci>::new(R_EARTH + alt_km, 0.0, 0.0);
let geo = <frame::SimpleEci as EarthFrameBridge>::to_geodetic(&pos, &utc, &());
assert!(
(geo.altitude - alt_km).abs() < 1.0,
"expected ~{alt_km} km, got {}",
geo.altitude
);
}
#[test]
fn gcrs_to_geodetic_altitude() {
let utc = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let alt_km = 400.0;
let pos = Vec3::<frame::Gcrs>::new(R_EARTH + alt_km, 0.0, 0.0);
let eop = GcrsEopStorage::new(ZeroEop);
let geo = <frame::Gcrs as EarthFrameBridge>::to_geodetic(&pos, &utc, &eop);
assert!(
(geo.altitude - alt_km).abs() < 1.0,
"expected ~{alt_km} km, got {}",
geo.altitude
);
}
#[test]
fn simple_eci_fixed_to_inertial_roundtrip() {
let utc = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let v_ecef = Vec3::<frame::SimpleEcef>::new(1.0, 2.0, 3.0);
let rot = <frame::SimpleEci as EarthFrameBridge>::fixed_to_inertial(&utc, &());
let v_eci = rot.transform(&v_ecef);
assert!(
(v_eci.magnitude() - v_ecef.magnitude()).abs() < 1e-14,
"rotation should preserve magnitude"
);
}
#[test]
fn gcrs_fixed_to_inertial_roundtrip() {
let utc = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let v_itrs = Vec3::<frame::Itrs>::new(1.0, 2.0, 3.0);
let eop = GcrsEopStorage::new(ZeroEop);
let rot = <frame::Gcrs as EarthFrameBridge>::fixed_to_inertial(&utc, &eop);
let v_gcrs = rot.transform(&v_itrs);
assert!(
(v_gcrs.magnitude() - v_itrs.magnitude()).abs() < 1e-14,
"rotation should preserve magnitude"
);
}
#[test]
fn simple_and_gcrs_geodetic_close_with_zero_eop() {
let utc = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let alt_km = 400.0;
let pos_simple = Vec3::<frame::SimpleEci>::new(R_EARTH + alt_km, 0.0, 0.0);
let geo_simple =
<frame::SimpleEci as EarthFrameBridge>::to_geodetic(&pos_simple, &utc, &());
let pos_gcrs = Vec3::<frame::Gcrs>::new(R_EARTH + alt_km, 0.0, 0.0);
let eop = GcrsEopStorage::new(ZeroEop);
let geo_gcrs = <frame::Gcrs as EarthFrameBridge>::to_geodetic(&pos_gcrs, &utc, &eop);
assert!(
(geo_simple.altitude - geo_gcrs.altitude).abs() < 5.0,
"simple alt={}, gcrs alt={}",
geo_simple.altitude,
geo_gcrs.altitude
);
}
}