use std::{
ffi::{CStr, CString},
mem::MaybeUninit,
path::Path,
};
use supernovas_ffi::{
novas_diurnal_eop_at_time, novas_eop, novas_eop_series, novas_fetch_eop, novas_fetch_eop_unix,
novas_get_eop_itrf_year, novas_get_eop_url, novas_is_auto_fetch_eop, novas_itrf_transform_eop,
novas_reset_eop, novas_set_auto_fetch_eop, novas_set_eop_url, novas_set_leap_list,
};
use crate::{
Angle, Time,
error::{Error, Result},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EopSeries {
LeapList,
RapidIau2000,
C04Iau2000,
C01Iau2000,
}
impl EopSeries {
fn to_raw(self) -> novas_eop_series {
match self {
Self::LeapList => novas_eop_series::EOP_LEAP_LIST,
Self::RapidIau2000 => novas_eop_series::EOP_RAPID_IAU2000,
Self::C04Iau2000 => novas_eop_series::EOP_C04_IAU2000_0UTC,
Self::C01Iau2000 => novas_eop_series::EOP_C01_IAU2000,
}
}
fn from_raw(r: novas_eop_series) -> Self {
match r {
novas_eop_series::EOP_LEAP_LIST => Self::LeapList,
novas_eop_series::EOP_RAPID_IAU2000 => Self::RapidIau2000,
novas_eop_series::EOP_C04_IAU2000_0UTC => Self::C04Iau2000,
novas_eop_series::EOP_C01_IAU2000 => Self::C01Iau2000,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Eop(novas_eop);
impl Eop {
fn from_raw(r: novas_eop) -> Self {
Eop(r)
}
#[must_use]
pub fn series(&self) -> EopSeries {
EopSeries::from_raw(self.0.series)
}
#[must_use]
pub fn jd(&self) -> f64 {
self.0.jd
}
#[must_use]
pub fn leap(&self) -> i32 {
self.0.leap
}
#[must_use]
pub fn dut1(&self) -> f32 {
self.0.dut1
}
#[must_use]
pub fn dut1_err(&self) -> f32 {
self.0.dut1_err
}
#[must_use]
pub fn xp(&self) -> Angle {
Angle::from_arcsec(f64::from(self.0.xp)).expect("EOP xp from IERS is always finite")
}
#[must_use]
pub fn xp_err(&self) -> Angle {
Angle::from_arcsec(f64::from(self.0.xp_err)).expect("EOP xp_err from IERS is always finite")
}
#[must_use]
pub fn yp(&self) -> Angle {
Angle::from_arcsec(f64::from(self.0.yp)).expect("EOP yp from IERS is always finite")
}
#[must_use]
pub fn yp_err(&self) -> Angle {
Angle::from_arcsec(f64::from(self.0.yp_err)).expect("EOP yp_err from IERS is always finite")
}
#[must_use]
pub fn lod(&self) -> f32 {
self.0.lod
}
#[must_use]
pub fn lod_err(&self) -> f32 {
self.0.lod_err
}
}
pub fn fetch_eop(jd: f64, timeout_ms: i64) -> Result<Eop> {
let mut out = MaybeUninit::<novas_eop>::zeroed();
let rc = unsafe { novas_fetch_eop(jd, timeout_ms as _, out.as_mut_ptr()) };
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok(Eop::from_raw(unsafe { out.assume_init() }))
}
pub fn fetch_eop_unix(unix_t: i64, timeout_ms: i64) -> Result<Eop> {
let mut out = MaybeUninit::<novas_eop>::zeroed();
let rc = unsafe { novas_fetch_eop_unix(unix_t as _, timeout_ms as _, out.as_mut_ptr()) };
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok(Eop::from_raw(unsafe { out.assume_init() }))
}
pub fn reset_eop() {
unsafe { novas_reset_eop() };
}
pub fn set_auto_fetch_eop(enabled: bool) -> Result<()> {
let rc = unsafe { novas_set_auto_fetch_eop(enabled.into()) };
if rc != 0 { Err(Error::ffi(rc)) } else { Ok(()) }
}
#[must_use]
pub fn is_auto_fetch_eop() -> bool {
unsafe { novas_is_auto_fetch_eop() != 0 }
}
pub fn set_eop_url(series: EopSeries, itrf_year: i32, url: &str) -> Result<()> {
let c_url = CString::new(url).map_err(|_| Error::OutOfRange("url contains interior NUL"))?;
let rc = unsafe { novas_set_eop_url(series.to_raw(), itrf_year, c_url.as_ptr()) };
if rc != 0 { Err(Error::ffi(rc)) } else { Ok(()) }
}
pub fn set_eop_file(series: EopSeries, itrf_year: i32, path: &Path) -> Result<()> {
let abs = path
.canonicalize()
.map_err(|_| Error::OutOfRange("EOP file path could not be resolved"))?;
let url = std::format!("file://{}", abs.display());
set_eop_url(series, itrf_year, &url)
}
#[must_use]
pub fn get_eop_url(series: EopSeries) -> Option<&'static CStr> {
let ptr = unsafe { novas_get_eop_url(series.to_raw()) };
if ptr.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(ptr) })
}
}
#[must_use]
pub fn get_eop_itrf_year(series: EopSeries) -> i32 {
unsafe { novas_get_eop_itrf_year(series.to_raw()) }
}
pub fn diurnal_eop_at_time(time: &Time) -> Result<(Angle, Angle, f64)> {
let mut dxp = 0.0_f64;
let mut dyp = 0.0_f64;
let mut dut1 = 0.0_f64;
let rc = unsafe {
novas_diurnal_eop_at_time(
time.as_timespec(),
&raw mut dxp,
&raw mut dyp,
&raw mut dut1,
)
};
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok((Angle::from_arcsec(dxp)?, Angle::from_arcsec(dyp)?, dut1))
}
#[allow(clippy::similar_names)]
pub fn itrf_transform_eop(
from_year: i32,
from_xp: Angle,
from_yp: Angle,
from_dut1: f64,
to_year: i32,
) -> Result<(Angle, Angle, f64)> {
let mut to_xp = 0.0_f64;
let mut to_yp = 0.0_f64;
let mut to_dut1 = 0.0_f64;
let rc = unsafe {
novas_itrf_transform_eop(
from_year,
from_xp.arcsec(),
from_yp.arcsec(),
from_dut1,
to_year,
&raw mut to_xp,
&raw mut to_yp,
&raw mut to_dut1,
)
};
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok((
Angle::from_arcsec(to_xp)?,
Angle::from_arcsec(to_yp)?,
to_dut1,
))
}
pub fn set_leap_list(path: &Path) -> Result<()> {
let bytes = path.as_os_str().as_encoded_bytes();
let c_path = CString::new(bytes).map_err(|_| Error::OutOfRange("path contains NUL"))?;
let rc = unsafe { novas_set_leap_list(c_path.as_ptr()) };
if rc != 0 { Err(Error::ffi(rc)) } else { Ok(()) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eop_series_round_trips() {
for s in [
EopSeries::LeapList,
EopSeries::RapidIau2000,
EopSeries::C04Iau2000,
EopSeries::C01Iau2000,
] {
assert_eq!(EopSeries::from_raw(s.to_raw()), s);
}
}
#[test]
fn get_eop_itrf_year_returns_integer() {
let year = get_eop_itrf_year(EopSeries::RapidIau2000);
assert!(year >= 0, "expected non-negative ITRF year, got {year}");
}
#[test]
fn is_auto_fetch_eop_does_not_panic() {
let _ = is_auto_fetch_eop();
}
#[test]
fn diurnal_eop_at_time_returns_finite() {
let t = crate::Time::from_tt_jd(2_460_676.5, 37, 0.0).unwrap();
let (dxp, dyp, dut1) = diurnal_eop_at_time(&t).unwrap();
assert!(dxp.arcsec().is_finite() && dyp.arcsec().is_finite() && dut1.is_finite());
}
#[test]
#[allow(clippy::similar_names)]
fn itrf_transform_eop_identity_same_year() {
let xp = Angle::from_arcsec(0.1).unwrap();
let yp = Angle::from_arcsec(0.2).unwrap();
let (to_xp, to_yp, to_dut1) = itrf_transform_eop(2020, xp, yp, 0.05, 2020).unwrap();
assert!((to_xp.arcsec() - 0.1).abs() < 1e-9);
assert!((to_yp.arcsec() - 0.2).abs() < 1e-9);
assert!((to_dut1 - 0.05).abs() < 1e-9);
}
}