use crate::qtty::*;
use crate::time::JulianDate;
#[derive(Debug, Clone, Copy)]
pub struct EopValues {
pub dut1: Seconds,
pub xp: Arcseconds,
pub yp: Arcseconds,
pub dx: MilliArcseconds,
pub dy: MilliArcseconds,
}
impl Default for EopValues {
fn default() -> Self {
Self {
dut1: Seconds::new(0.0),
xp: Arcseconds::new(0.0),
yp: Arcseconds::new(0.0),
dx: MilliArcseconds::new(0.0),
dy: MilliArcseconds::new(0.0),
}
}
}
impl EopValues {
#[inline]
pub fn jd_ut1(&self, jd_utc: JulianDate) -> JulianDate {
JulianDate::new(jd_utc.jd_value() + self.dut1.to::<Day>().value())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EopError {
NoData {
jd_utc: f64,
mjd_utc: f64,
},
}
impl core::fmt::Display for EopError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::NoData { jd_utc, mjd_utc } => write!(
f,
"no Earth Orientation Parameters cover UTC JD {jd_utc} (MJD {mjd_utc})"
),
}
}
}
impl std::error::Error for EopError {}
pub trait EopProvider {
#[inline]
fn try_eop_at(&self, jd_utc: JulianDate) -> Result<EopValues, EopError> {
Ok(self.eop_at(jd_utc))
}
fn eop_at(&self, jd_utc: JulianDate) -> EopValues;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NullEop;
impl EopProvider for NullEop {
#[inline]
fn try_eop_at(&self, _jd_utc: JulianDate) -> Result<EopValues, EopError> {
Ok(EopValues::default())
}
#[inline]
fn eop_at(&self, _jd_utc: JulianDate) -> EopValues {
EopValues::default()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IersEop;
impl IersEop {
#[inline]
pub fn new() -> Self {
Self
}
pub fn mjd_range(&self) -> Option<(f64, f64)> {
Some((tempoch::EOP_START_MJD.value(), tempoch::EOP_END_MJD.value()))
}
#[inline]
pub fn len(&self) -> usize {
((tempoch::EOP_END_MJD - tempoch::EOP_START_MJD)
.value()
.floor() as usize)
+ 1
}
#[inline]
pub fn is_empty(&self) -> bool {
false
}
}
impl EopProvider for IersEop {
fn try_eop_at(&self, jd_utc: JulianDate) -> Result<EopValues, EopError> {
let mjd = Days::new(jd_utc.jd_value() - 2_400_000.5);
let e = tempoch::eop::builtin_eop_at(mjd).ok_or(EopError::NoData {
jd_utc: jd_utc.jd_value(),
mjd_utc: mjd.value(),
})?;
Ok(EopValues {
dut1: e.ut1_minus_utc,
xp: Arcseconds::new(e.pm_xp_arcsec.unwrap_or(0.0)),
yp: Arcseconds::new(e.pm_yp_arcsec.unwrap_or(0.0)),
dx: MilliArcseconds::new(e.dx_milliarcsec.unwrap_or(0.0)),
dy: MilliArcseconds::new(e.dy_milliarcsec.unwrap_or(0.0)),
})
}
fn eop_at(&self, jd_utc: JulianDate) -> EopValues {
self.try_eop_at(jd_utc)
.expect("IERS EOP requested outside bundled EOP coverage")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_eop_returns_zeros() {
let eop = NullEop;
let vals = eop.eop_at(JulianDate::J2000);
assert_eq!(vals.dut1.value(), 0.0);
assert_eq!(vals.xp.value(), 0.0);
assert_eq!(vals.yp.value(), 0.0);
assert_eq!(vals.dx.value(), 0.0);
assert_eq!(vals.dy.value(), 0.0);
}
#[test]
fn eop_jd_ut1_conversion() {
let vals = EopValues {
dut1: Seconds::new(0.35),
..Default::default()
};
let jd_utc = JulianDate::J2000;
let jd_ut1 = vals.jd_ut1(jd_utc);
let diff_days = (jd_ut1 - jd_utc).value();
let diff_s = diff_days * 86400.0;
assert!(
(diff_s - 0.35).abs() < 1e-3,
"UT1-UTC = {}s, expected 0.35s",
diff_s
);
}
#[test]
fn eop_unit_conversions() {
let as2rad = std::f64::consts::PI / (180.0 * 3600.0);
let vals = EopValues {
xp: Arcseconds::new(0.1),
yp: Arcseconds::new(0.2),
dx: MilliArcseconds::new(0.3),
dy: MilliArcseconds::new(0.4),
..Default::default()
};
assert!((vals.xp.to::<Radian>().value() - 0.1 * as2rad).abs() < 1e-20);
assert!((vals.yp.to::<Radian>().value() - 0.2 * as2rad).abs() < 1e-20);
assert!((vals.dx.to::<Radian>().value() - 0.3e-3 * as2rad).abs() < 1e-20);
assert!((vals.dy.to::<Radian>().value() - 0.4e-3 * as2rad).abs() < 1e-20);
}
#[test]
fn iers_eop_rejects_out_of_range_instead_of_zeroing() {
let eop = IersEop::new();
let (first, _) = eop.mjd_range().expect("compiled EOP range");
let before = JulianDate::new(first + 2_400_000.5 - 1.0);
let err = eop.try_eop_at(before).expect_err("before range must fail");
assert!(matches!(err, EopError::NoData { .. }));
}
#[test]
fn iers_eop_uses_tempoch_builtin_data() {
let eop = IersEop::new();
let (first, _) = eop.mjd_range().expect("compiled EOP range");
let jd = JulianDate::new(first + 2_400_000.5);
let vals = eop.try_eop_at(jd).expect("range start must be covered");
let raw = tempoch::eop::builtin_eop_at(Days::new(first)).expect("tempoch EOP");
assert!((vals.dut1.value() - raw.ut1_minus_utc.value()).abs() < 1e-12);
}
}