use core::mem::MaybeUninit;
use supernovas_ffi::{
novas_accuracy::{NOVAS_FULL_ACCURACY, NOVAS_REDUCED_ACCURACY},
novas_frame, novas_make_frame,
};
use crate::{
Angle, Horizontal, Observer, Time,
apparent::ReferenceSystem,
error::{Error, Result},
source::Source,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Accuracy {
Full,
Reduced,
}
impl Accuracy {
pub(crate) fn to_sys(self) -> supernovas_ffi::novas_accuracy {
match self {
Accuracy::Full => NOVAS_FULL_ACCURACY,
Accuracy::Reduced => NOVAS_REDUCED_ACCURACY,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Frame(novas_frame);
impl Frame {
pub fn new(accuracy: Accuracy, observer: &Observer, time: &Time) -> Result<Self> {
Self::with_polar_motion_mas(accuracy, observer, time, 0.0, 0.0)
}
pub fn with_polar_motion(
accuracy: Accuracy,
observer: &Observer,
time: &Time,
xp: Angle,
yp: Angle,
) -> Result<Self> {
Self::with_polar_motion_mas(accuracy, observer, time, xp.mas(), yp.mas())
}
fn with_polar_motion_mas(
accuracy: Accuracy,
observer: &Observer,
time: &Time,
xp_mas: f64,
yp_mas: f64,
) -> Result<Self> {
if !xp_mas.is_finite() || !yp_mas.is_finite() {
return Err(Error::NotFinite);
}
let obs = observer.as_novas_observer()?;
let mut frame = MaybeUninit::<novas_frame>::zeroed();
let rc = unsafe {
novas_make_frame(
accuracy.to_sys(),
&obs,
time.as_timespec(),
xp_mas,
yp_mas,
frame.as_mut_ptr(),
)
};
if rc != 0 {
return Err(Error::Ffi);
}
Ok(Frame(unsafe { frame.assume_init() }))
}
pub fn accuracy(&self) -> Accuracy {
match self.0.accuracy {
NOVAS_FULL_ACCURACY => Accuracy::Full,
_ => Accuracy::Reduced,
}
}
pub fn tt_jd(&self) -> f64 {
#[allow(clippy::useless_conversion)]
let ijd: i64 = i64::from(self.0.time.ijd_tt);
ijd as f64 + self.0.time.fjd_tt
}
pub(crate) fn as_novas_frame(&self) -> &novas_frame {
&self.0
}
pub fn observe(&self, source: &impl Source) -> Result<Horizontal> {
source
.apparent_in(self, ReferenceSystem::Cirs)?
.to_horizontal()
}
}
#[cfg(test)]
mod tests {
use supernovas_ffi::novas_timescale::NOVAS_TT;
use super::*;
fn j2000() -> Time {
Time::from_jd(NOVAS_TT, 2_451_545.0, 32, 0.0).unwrap()
}
#[test]
fn build_geocentric_frame() {
let obs = Observer::Geocenter;
let t = j2000();
let f = Frame::new(Accuracy::Reduced, &obs, &t).unwrap();
assert_eq!(f.accuracy(), Accuracy::Reduced);
}
#[test]
fn build_geodetic_frame() {
let obs = Observer::geodetic(34.0, -118.0, 100.0).unwrap();
let t = j2000();
let f = Frame::new(Accuracy::Reduced, &obs, &t).unwrap();
assert_eq!(f.accuracy(), Accuracy::Reduced);
}
#[test]
fn with_polar_motion_is_finite_only() {
let obs = Observer::Geocenter;
let t = j2000();
let xp = Angle::from_mas(120.5).unwrap();
let yp = Angle::from_mas(-85.3).unwrap();
let _ = Frame::with_polar_motion(Accuracy::Reduced, &obs, &t, xp, yp).unwrap();
}
#[test]
fn polaris_elevation_matches_observer_latitude() {
let lat_deg = 34.0;
let polaris = crate::CatalogEntry::icrs(
"Polaris",
crate::TimeAngle::from_hours(2.530_301_5).unwrap(),
Angle::from_degrees(89.264_109).unwrap(),
)
.unwrap();
let obs = Observer::geodetic(lat_deg, 0.0, 0.0).unwrap();
let t = Time::from_utc_jd(2_460_676.5, 37, 0.0).unwrap();
let frame = Frame::new(Accuracy::Reduced, &obs, &t).unwrap();
let horizontal = frame.observe(&polaris).unwrap();
let el = horizontal.elevation().deg();
assert!(
(el - lat_deg).abs() < 1.0,
"Polaris elevation {el} should be within 1° of latitude {lat_deg}"
);
}
}