use crate::coords::{deg_to_rad, normalize_degrees, rad_to_deg};
const EARTH_RADIUS_KM: f64 = 6378.137;
#[derive(Debug, Clone, Copy)]
pub struct Observer {
pub latitude_deg: f64,
pub longitude_deg: f64,
pub height_m: f64,
}
impl Observer {
pub fn new(latitude_deg: f64, longitude_deg: f64, height_m: f64) -> Self {
Self {
latitude_deg,
longitude_deg,
height_m,
}
}
fn geocentric_params(&self) -> (f64, f64) {
let lat = deg_to_rad(self.latitude_deg);
let u = (0.996_647_19 * lat.tan()).atan(); let height_fraction = self.height_m / 1000.0 / EARTH_RADIUS_KM;
let rho_sin = 0.996_647_19 * u.sin() + height_fraction * lat.sin();
let rho_cos = u.cos() + height_fraction * lat.cos();
(rho_sin, rho_cos)
}
}
pub fn horizontal_parallax(distance_km: f64) -> f64 {
rad_to_deg((EARTH_RADIUS_KM / distance_km).asin())
}
pub fn horizontal_parallax_au(distance_au: f64) -> f64 {
let distance_km = distance_au * 149_597_870.7;
horizontal_parallax(distance_km)
}
#[derive(Debug, Clone, Copy)]
pub struct TopocentricCorrection {
pub longitude_deg: f64,
pub latitude_deg: f64,
}
pub fn topocentric_moon(
geo_lon: f64,
geo_lat: f64,
distance_km: f64,
lst_deg: f64,
obliquity_deg: f64,
observer: &Observer,
) -> TopocentricCorrection {
let (rho_sin, rho_cos) = observer.geocentric_params();
let hp = deg_to_rad(horizontal_parallax(distance_km));
let eps = deg_to_rad(obliquity_deg);
let lst = deg_to_rad(lst_deg);
let lon = deg_to_rad(geo_lon);
let lat = deg_to_rad(geo_lat);
let sin_hp = hp.sin();
let sin_lon = lon.sin();
let cos_lon = lon.cos();
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let sin_eps = eps.sin();
let cos_eps = eps.cos();
let ra = (sin_lon * cos_eps - sin_lat / cos_lat * sin_eps).atan2(cos_lon);
let dec = (sin_lat * cos_eps + cos_lat * sin_eps * sin_lon).asin();
let h = lst - ra;
let cos_dec = dec.cos();
let sin_dec = dec.sin();
let delta_ra = (-rho_cos * sin_hp * h.sin()).atan2(cos_dec - rho_cos * sin_hp * h.cos());
let dec_prime =
((sin_dec - rho_sin * sin_hp) * delta_ra.cos()).atan2(cos_dec - rho_cos * sin_hp * h.cos());
let ra_prime = ra + delta_ra;
let sin_ra_p = ra_prime.sin();
let cos_ra_p = ra_prime.cos();
let sin_dec_p = dec_prime.sin();
let cos_dec_p = dec_prime.cos();
let lon_prime = (sin_ra_p * cos_eps + sin_dec_p / cos_dec_p * sin_eps).atan2(cos_ra_p);
let lat_prime = (sin_dec_p * cos_eps - cos_dec_p * sin_eps * sin_ra_p).asin();
TopocentricCorrection {
longitude_deg: normalize_degrees(rad_to_deg(lon_prime)),
latitude_deg: rad_to_deg(lat_prime),
}
}
pub fn parallax_in_altitude(apparent_alt_deg: f64, horizontal_parallax_deg: f64) -> f64 {
let alt = deg_to_rad(apparent_alt_deg);
let hp = deg_to_rad(horizontal_parallax_deg);
rad_to_deg(hp * alt.cos())
}
pub fn correct_lunar_position(
jd_tt: f64,
observer: &Observer,
) -> crate::error::Result<TopocentricCorrection> {
let lon = crate::moon::lunar_longitude(jd_tt);
let lat = crate::moon::lunar_latitude(jd_tt);
let dist = crate::moon::lunar_distance_km(jd_tt);
let lst = crate::calendar::local_sidereal_time_tt(jd_tt, observer.longitude_deg);
let eps = crate::nutation::true_obliquity(jd_tt);
Ok(topocentric_moon(lon, lat, dist, lst, eps, observer))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn horizontal_parallax_moon() {
let hp = horizontal_parallax(384_400.0);
assert!((hp - 0.95).abs() < 0.05, "got {hp}");
}
#[test]
fn horizontal_parallax_sun() {
let hp = horizontal_parallax_au(1.0);
assert!(
(hp * 3600.0 - 8.794).abs() < 0.1,
"got {} arcsec",
hp * 3600.0
);
}
#[test]
fn observer_geocentric_params() {
let obs = Observer::new(0.0, 0.0, 0.0);
let (rho_sin, rho_cos) = obs.geocentric_params();
assert!(rho_cos > 0.99 && rho_cos < 1.01, "rho_cos = {rho_cos}");
assert!(rho_sin.abs() < 0.01, "rho_sin = {rho_sin}");
}
#[test]
fn observer_at_pole() {
let obs = Observer::new(90.0, 0.0, 0.0);
let (rho_sin, rho_cos) = obs.geocentric_params();
assert!(rho_sin > 0.99, "rho_sin = {rho_sin}");
assert!(rho_cos.abs() < 0.01, "rho_cos = {rho_cos}");
}
#[test]
fn topocentric_moon_correction_nonzero() {
let obs = Observer::new(51.5, -0.1, 0.0);
let result = topocentric_moon(133.162, -3.229, 368_410.0, 197.0, 23.44, &obs);
let lon_diff = (result.longitude_deg - 133.162).abs();
let lat_diff = (result.latitude_deg - (-3.229)).abs();
assert!(
lon_diff > 0.001 || lat_diff > 0.001,
"correction too small: Δlon={lon_diff}, Δlat={lat_diff}"
);
assert!(lon_diff < 2.0, "correction too large: Δlon={lon_diff}");
}
#[test]
fn topocentric_moon_at_geocenter() {
let obs = Observer::new(0.0, 0.0, 0.0);
let result = topocentric_moon(100.0, 0.0, 384_400.0, 100.0, 23.44, &obs);
assert!(result.longitude_deg >= 0.0 && result.longitude_deg < 360.0);
}
#[test]
fn correct_lunar_position_convenience() {
let obs = Observer::new(51.5, -0.1, 0.0);
let corrected = correct_lunar_position(2_451_545.0, &obs).unwrap();
assert!(corrected.longitude_deg >= 0.0 && corrected.longitude_deg < 360.0);
assert!(corrected.latitude_deg.abs() < 10.0);
}
#[test]
fn parallax_in_altitude_horizon() {
let correction = parallax_in_altitude(0.0, 0.95);
assert!((correction - 0.95).abs() < 0.01, "got {correction}");
}
#[test]
fn parallax_in_altitude_zenith() {
let correction = parallax_in_altitude(90.0, 0.95);
assert!(correction.abs() < 0.001, "got {correction}");
}
}