use core::mem::MaybeUninit;
use supernovas_ffi::{
novas_accuracy::{NOVAS_FULL_ACCURACY, NOVAS_REDUCED_ACCURACY},
novas_app_to_hor, novas_frame, novas_make_frame,
novas_reference_system::NOVAS_CIRS,
novas_sky_pos, sky_pos,
};
use crate::{
Angle, CatalogEntry, Horizontal, Observer, Time,
error::{Error, Result},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Accuracy {
Full,
Reduced,
}
impl Accuracy {
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::Parse);
}
Ok(Frame(unsafe { frame.assume_init() }))
}
pub fn accuracy(&self) -> Accuracy {
match self.0.accuracy {
NOVAS_FULL_ACCURACY => Accuracy::Full,
_ => Accuracy::Reduced,
}
}
pub fn observe(&self, source: &CatalogEntry) -> Result<Horizontal> {
use core::mem::MaybeUninit;
let mut sky = MaybeUninit::<sky_pos>::zeroed();
let rc =
unsafe { novas_sky_pos(source.as_object(), &self.0, NOVAS_CIRS, sky.as_mut_ptr()) };
if rc != 0 {
return Err(Error::Parse);
}
let sky = unsafe { sky.assume_init() };
let mut az_deg: f64 = 0.0;
let mut el_deg: f64 = 0.0;
let rc = unsafe {
novas_app_to_hor(
&self.0,
NOVAS_CIRS,
sky.ra,
sky.dec,
None,
&mut az_deg as *mut f64,
&mut el_deg as *mut f64,
)
};
if rc != 0 {
return Err(Error::Parse);
}
Horizontal::from_degrees(az_deg, el_deg)
}
}
#[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}"
);
}
}