use super::ObserverState;
use crate::astro::aberration::{
apply_aberration_to_direction_with_velocity, remove_aberration_from_direction_with_velocity,
};
use crate::coordinates::cartesian;
use crate::coordinates::frames::{EquatorialMeanJ2000, MutableFrame};
use crate::coordinates::spherical;
use crate::coordinates::transform::TransformFrame;
#[derive(Debug, Clone, Copy)]
pub struct Astrometric<D> {
direction: D,
}
impl<D> Astrometric<D> {
pub fn new(direction: D) -> Self {
Self { direction }
}
pub fn direction(&self) -> &D {
&self.direction
}
pub fn into_inner(self) -> D {
self.direction
}
}
impl<F: MutableFrame> Astrometric<spherical::Direction<F>> {
pub fn to_apparent(self, obs: &ObserverState) -> Apparent<spherical::Direction<F>>
where
cartesian::Direction<EquatorialMeanJ2000>: TransformFrame<cartesian::Direction<F>>,
cartesian::Direction<F>: TransformFrame<cartesian::Direction<EquatorialMeanJ2000>>,
{
let cart_dir = self.direction.to_cartesian();
let cart_eq: cartesian::Direction<EquatorialMeanJ2000> = cart_dir.to_frame();
let vel = obs.velocity();
let apparent_eq = apply_aberration_to_direction_with_velocity(cart_eq, vel);
let apparent_cart: cartesian::Direction<F> = apparent_eq.to_frame();
let apparent_sph = apparent_cart.to_spherical();
Apparent::new(apparent_sph)
}
}
impl<F: MutableFrame> Astrometric<cartesian::Direction<F>> {
pub fn to_apparent(self, obs: &ObserverState) -> Apparent<cartesian::Direction<F>>
where
cartesian::Direction<EquatorialMeanJ2000>: TransformFrame<cartesian::Direction<F>>,
cartesian::Direction<F>: TransformFrame<cartesian::Direction<EquatorialMeanJ2000>>,
{
let cart_eq: cartesian::Direction<EquatorialMeanJ2000> = self.direction.to_frame();
let vel = obs.velocity();
let apparent_eq = apply_aberration_to_direction_with_velocity(cart_eq, vel);
let apparent_cart: cartesian::Direction<F> = apparent_eq.to_frame();
Apparent::new(apparent_cart)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Apparent<D> {
direction: D,
}
impl<D> Apparent<D> {
pub fn new(direction: D) -> Self {
Self { direction }
}
pub fn direction(&self) -> &D {
&self.direction
}
pub fn into_inner(self) -> D {
self.direction
}
}
impl<F: MutableFrame> Apparent<spherical::Direction<F>> {
pub fn to_astrometric(self, obs: &ObserverState) -> Astrometric<spherical::Direction<F>>
where
cartesian::Direction<EquatorialMeanJ2000>: TransformFrame<cartesian::Direction<F>>,
cartesian::Direction<F>: TransformFrame<cartesian::Direction<EquatorialMeanJ2000>>,
{
let cart_dir = self.direction.to_cartesian();
let cart_eq: cartesian::Direction<EquatorialMeanJ2000> = cart_dir.to_frame();
let vel = obs.velocity();
let astrometric_eq = remove_aberration_from_direction_with_velocity(cart_eq, vel);
let astrometric_cart: cartesian::Direction<F> = astrometric_eq.to_frame();
let astrometric_sph = astrometric_cart.to_spherical();
Astrometric::new(astrometric_sph)
}
}
impl<F: MutableFrame> Apparent<cartesian::Direction<F>> {
pub fn to_astrometric(self, obs: &ObserverState) -> Astrometric<cartesian::Direction<F>>
where
cartesian::Direction<EquatorialMeanJ2000>: TransformFrame<cartesian::Direction<F>>,
cartesian::Direction<F>: TransformFrame<cartesian::Direction<EquatorialMeanJ2000>>,
{
let cart_eq: cartesian::Direction<EquatorialMeanJ2000> = self.direction.to_frame();
let vel = obs.velocity();
let astrometric_eq = remove_aberration_from_direction_with_velocity(cart_eq, vel);
let astrometric_cart: cartesian::Direction<F> = astrometric_eq.to_frame();
Astrometric::new(astrometric_cart)
}
}
impl<D: std::fmt::Display> std::fmt::Display for Astrometric<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Astrometric({})", self.direction)
}
}
impl<D: std::fmt::Display> std::fmt::Display for Apparent<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Apparent({})", self.direction)
}
}
impl<D: std::fmt::LowerExp> std::fmt::LowerExp for Astrometric<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Astrometric(")?;
std::fmt::LowerExp::fmt(&self.direction, f)?;
write!(f, ")")
}
}
impl<D: std::fmt::LowerExp> std::fmt::LowerExp for Apparent<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Apparent(")?;
std::fmt::LowerExp::fmt(&self.direction, f)?;
write!(f, ")")
}
}
impl<D: std::fmt::UpperExp> std::fmt::UpperExp for Astrometric<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Astrometric(")?;
std::fmt::UpperExp::fmt(&self.direction, f)?;
write!(f, ")")
}
}
impl<D: std::fmt::UpperExp> std::fmt::UpperExp for Apparent<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Apparent(")?;
std::fmt::UpperExp::fmt(&self.direction, f)?;
write!(f, ")")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::JulianDate;
use qtty::*;
#[test]
fn test_astrometric_to_apparent_introduces_shift() {
let jd = JulianDate::J2000;
let obs = ObserverState::geocentric(jd);
let astrometric = Astrometric::new(spherical::direction::EquatorialMeanJ2000::new(
0.0 * DEG,
0.0 * DEG,
));
let apparent: Apparent<spherical::direction::EquatorialMeanJ2000> =
astrometric.to_apparent(&obs);
let original = astrometric.direction();
let shifted = apparent.direction();
let mut delta_ra = (shifted.azimuth - original.azimuth).abs();
if delta_ra > 180.0 {
delta_ra = Degrees::new(360.0) - delta_ra;
}
let delta_dec = (shifted.polar - original.polar).abs();
assert!(
delta_ra > 0.0 || delta_dec > 0.0,
"Expected aberration to introduce a shift"
);
assert!(
delta_ra < 0.1 && delta_dec < 0.1,
"Aberration shift too large: dRA={}, dDec={}",
delta_ra,
delta_dec
);
}
#[test]
fn test_roundtrip_preserves_direction() {
let jd = JulianDate::J2000;
let obs = ObserverState::geocentric(jd);
let original = Astrometric::new(spherical::direction::EquatorialMeanJ2000::new(
45.0 * DEG,
30.0 * DEG,
));
let apparent = original.to_apparent(&obs);
let recovered: Astrometric<spherical::direction::EquatorialMeanJ2000> =
apparent.to_astrometric(&obs);
let orig_dir = original.direction();
let rec_dir = recovered.direction();
let delta_ra = (rec_dir.azimuth - orig_dir.azimuth).abs();
let delta_dec = (rec_dir.polar - orig_dir.polar).abs();
assert!(
delta_ra < 1e-6,
"RA not preserved in roundtrip: delta = {}",
delta_ra
);
assert!(
delta_dec < 1e-6,
"Dec not preserved in roundtrip: delta = {}",
delta_dec
);
}
#[test]
fn test_apparent_at_pole() {
let jd = JulianDate::J2000;
let obs = ObserverState::geocentric(jd);
let astrometric = Astrometric::new(spherical::direction::EquatorialMeanJ2000::new(
0.0 * DEG,
90.0 * DEG,
));
let apparent = astrometric.to_apparent(&obs);
assert!(
apparent.direction().polar < 90.0,
"Declination at pole should decrease due to aberration"
);
}
#[test]
fn test_type_safety() {
let jd = JulianDate::J2000;
let obs = ObserverState::geocentric(jd);
let astrometric = Astrometric::new(spherical::direction::EquatorialMeanJ2000::new(
10.0 * DEG,
20.0 * DEG,
));
let apparent = astrometric.to_apparent(&obs);
let _astrometric_inner: &spherical::direction::EquatorialMeanJ2000 =
astrometric.direction();
let _apparent_inner: &spherical::direction::EquatorialMeanJ2000 = apparent.direction();
assert!(format!("{}", astrometric).contains("Astrometric"));
assert!(format!("{}", apparent).contains("Apparent"));
}
}