use crate::error::{Result, validate_ra, validate_dec};
use crate::time::j2000_days;
use chrono::{DateTime, Utc};
pub fn apply_proper_motion(
ra_j2000: f64,
dec_j2000: f64,
pm_ra_cosdec: f64, pm_dec: f64, target_epoch: DateTime<Utc>,
) -> Result<(f64, f64)> {
validate_ra(ra_j2000)?;
validate_dec(dec_j2000)?;
let dt_years = j2000_days(target_epoch) / 365.25;
let pm_ra_deg = pm_ra_cosdec / 3_600_000.0; let pm_dec_deg = pm_dec / 3_600_000.0;
let mut ra = ra_j2000 + pm_ra_deg * dt_years;
let dec = dec_j2000 + pm_dec_deg * dt_years;
while ra < 0.0 {
ra += 360.0;
}
while ra >= 360.0 {
ra -= 360.0;
}
validate_dec(dec)?;
Ok((ra, dec))
}
pub fn apply_proper_motion_rigorous(
ra_j2000: f64,
dec_j2000: f64,
pm_ra_cosdec: f64,
pm_dec: f64,
parallax: f64,
radial_velocity: f64,
target_epoch: DateTime<Utc>,
) -> Result<(f64, f64, f64)> {
use crate::error::AstroError;
validate_ra(ra_j2000)?;
validate_dec(dec_j2000)?;
if parallax <= 0.0 {
return Err(AstroError::OutOfRange {
parameter: "parallax",
value: parallax,
min: 0.0,
max: f64::INFINITY,
});
}
let t = j2000_days(target_epoch) / 365.25;
let ra_rad = ra_j2000.to_radians();
let dec_rad = dec_j2000.to_radians();
let dist_pc = 1000.0 / parallax;
let _pm_ra_rad = pm_ra_cosdec * std::f64::consts::PI / (180.0 * 3_600_000.0);
let _pm_dec_rad = pm_dec * std::f64::consts::PI / (180.0 * 3_600_000.0);
let vt_ra = 4.74047 * pm_ra_cosdec * dist_pc / 1000.0;
let vt_dec = 4.74047 * pm_dec * dist_pc / 1000.0;
let x0 = dist_pc * dec_rad.cos() * ra_rad.cos();
let y0 = dist_pc * dec_rad.cos() * ra_rad.sin();
let z0 = dist_pc * dec_rad.sin();
let vx = -vt_ra * ra_rad.sin() - vt_dec * dec_rad.sin() * ra_rad.cos() + radial_velocity * dec_rad.cos() * ra_rad.cos();
let vy = vt_ra * ra_rad.cos() - vt_dec * dec_rad.sin() * ra_rad.sin() + radial_velocity * dec_rad.cos() * ra_rad.sin();
let vz = vt_dec * dec_rad.cos() + radial_velocity * dec_rad.sin();
let k = 0.000977792;
let x = x0 + vx * k * t;
let y = y0 + vy * k * t;
let z = z0 + vz * k * t;
let dist_new = (x*x + y*y + z*z).sqrt();
let ra_new = y.atan2(x);
let dec_new = (z / dist_new).asin();
let parallax_new = 1000.0 / dist_new;
let mut ra_deg = ra_new.to_degrees();
if ra_deg < 0.0 {
ra_deg += 360.0;
}
let dec_deg = dec_new.to_degrees();
Ok((ra_deg, dec_deg, parallax_new))
}
pub fn total_proper_motion(pm_ra_cosdec: f64, pm_dec: f64) -> f64 {
(pm_ra_cosdec * pm_ra_cosdec + pm_dec * pm_dec).sqrt()
}
pub fn proper_motion_position_angle(pm_ra_cosdec: f64, pm_dec: f64) -> f64 {
let pa_rad = pm_ra_cosdec.atan2(pm_dec);
let mut pa_deg = pa_rad.to_degrees();
if pa_deg < 0.0 {
pa_deg += 360.0;
}
pa_deg
}
pub fn pm_ra_to_pm_ra_cosdec(pm_ra: f64, dec: f64) -> f64 {
pm_ra * dec.to_radians().cos()
}
pub fn pm_ra_cosdec_to_pm_ra(pm_ra_cosdec: f64, dec: f64) -> f64 {
pm_ra_cosdec / dec.to_radians().cos()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_no_proper_motion() {
let epoch = Utc.with_ymd_and_hms(2050, 1, 1, 0, 0, 0).unwrap();
let (ra, dec) = apply_proper_motion(100.0, 25.0, 0.0, 0.0, epoch).unwrap();
assert!((ra - 100.0).abs() < 1e-10);
assert!((dec - 25.0).abs() < 1e-10);
}
#[test]
fn test_barnards_star() {
let ra_2000 = 269.454;
let dec_2000 = 4.668;
let pm_ra = -797.84; let pm_dec = 10326.93;
let epoch = Utc.with_ymd_and_hms(2050, 1, 1, 0, 0, 0).unwrap();
let (_ra, dec) = apply_proper_motion(ra_2000, dec_2000, pm_ra, pm_dec, epoch).unwrap();
assert!((dec - dec_2000) > 0.14 && (dec - dec_2000) < 0.15);
}
#[test]
fn test_total_proper_motion_calculation() {
let total = total_proper_motion(3.0, 4.0);
assert!((total - 5.0).abs() < 1e-10);
let total_barnards = total_proper_motion(-797.84, 10326.93);
assert!((total_barnards - 10357.72).abs() < 0.1);
}
#[test]
fn test_position_angle() {
let pa = proper_motion_position_angle(0.0, 1.0);
assert!((pa - 0.0).abs() < 1e-10);
let pa = proper_motion_position_angle(1.0, 0.0);
assert!((pa - 90.0).abs() < 1e-10);
let pa = proper_motion_position_angle(0.0, -1.0);
assert!((pa - 180.0).abs() < 1e-10);
let pa = proper_motion_position_angle(-1.0, 0.0);
assert!((pa - 270.0).abs() < 1e-10);
}
}