use crate::consts;
use crate::TimeLike;
use crate::TimeScale;
use crate::mathtypes::*;
pub fn ecliptic_longitude<T: TimeLike>(time: &T) -> f64 {
let time = time.as_instant();
let t: f64 = (time.as_jd_with_scale(TimeScale::TDB) - 2451545.0) / 36525.0;
#[allow(non_upper_case_globals)]
const deg2rad: f64 = std::f64::consts::PI / 180.;
let lon = deg2rad
* 0.11f64.mul_add(
-f64::sin(deg2rad * 966404.05f64.mul_add(t, 186.6)),
0.19f64.mul_add(
-f64::sin(deg2rad * 35999.05f64.mul_add(t, 357.5)),
0.21f64.mul_add(
f64::sin(deg2rad * 954397.70f64.mul_add(t, 269.9)),
0.66f64.mul_add(
f64::sin(deg2rad * 890534.23f64.mul_add(t, 235.7)),
1.27f64.mul_add(
-f64::sin(deg2rad * 413335.38f64.mul_add(-t, 259.2)),
6.29f64.mul_add(
f64::sin(deg2rad * 477198.85f64.mul_add(t, 134.9)),
481267.8813f64.mul_add(t, 218.32),
),
),
),
),
),
);
lon % (2.0 * std::f64::consts::PI)
}
pub fn phase<T: TimeLike>(time: &T) -> f64 {
let time = time.as_instant();
let lambda_moon = ecliptic_longitude(&time);
let lambda_sun = crate::lpephem::sun::ecliptic_longitude(&time);
let phase = (lambda_moon - lambda_sun) % (2.0 * std::f64::consts::PI);
if phase < 0.0 {
phase + 2.0 * std::f64::consts::PI
} else if phase > 2.0 * std::f64::consts::PI {
phase - 2.0 * std::f64::consts::PI
} else {
phase
}
}
pub fn illumination<T: TimeLike>(time: &T) -> f64 {
let time = time.as_instant();
let phase = phase(&time);
0.5 * (1.0 - f64::cos(phase))
}
#[derive(Debug, Clone, Copy, std::cmp::PartialEq, std::cmp::Eq)]
pub enum MoonPhase {
NewMoon,
WaxingCrescent,
FirstQuarter,
WaxingGibbous,
FullMoon,
WaningGibbous,
LastQuarter,
WaningCrescent,
}
impl MoonPhase {
pub fn name(&self) -> &'static str {
match self {
Self::NewMoon => "New Moon",
Self::WaxingCrescent => "Waxing Crescent",
Self::FirstQuarter => "First Quarter",
Self::WaxingGibbous => "Waxing Gibbous",
Self::FullMoon => "Full Moon",
Self::WaningGibbous => "Waning Gibbous",
Self::LastQuarter => "Last Quarter",
Self::WaningCrescent => "Waning Crescent",
}
}
}
pub fn phase_name<T: TimeLike>(time: &T) -> MoonPhase {
let time = time.as_instant();
let phase_rad = phase(&time);
let phase_deg = phase_rad.to_degrees();
let phase_deg = if phase_deg < 0.0 {
phase_deg + 360.0
} else if phase_deg >= 360.0 {
phase_deg - 360.0
} else {
phase_deg
};
match phase_deg {
p if p < 22.5 => MoonPhase::NewMoon,
p if p < 67.5 => MoonPhase::WaxingCrescent,
p if p < 112.5 => MoonPhase::FirstQuarter,
p if p < 157.5 => MoonPhase::WaxingGibbous,
p if p < 202.5 => MoonPhase::FullMoon,
p if p < 247.5 => MoonPhase::WaningGibbous,
p if p < 292.5 => MoonPhase::LastQuarter,
p if p < 337.5 => MoonPhase::WaningCrescent,
_ => MoonPhase::NewMoon, }
}
pub fn pos_gcrf<T: TimeLike>(time: &T) -> Vector3 {
let time = time.as_instant();
let t: f64 = (time.as_jd_with_scale(TimeScale::TDB) - 2451545.0) / 36525.0;
#[allow(non_upper_case_globals)]
const deg2rad: f64 = std::f64::consts::PI / 180.;
let lambda_ecliptic: f64 = deg2rad
* 0.11f64.mul_add(
-f64::sin(deg2rad * 966404.05f64.mul_add(t, 186.6)),
0.19f64.mul_add(
-f64::sin(deg2rad * 35999.05f64.mul_add(t, 357.5)),
0.21f64.mul_add(
f64::sin(deg2rad * 954397.70f64.mul_add(t, 269.9)),
0.66f64.mul_add(
f64::sin(deg2rad * 890534.23f64.mul_add(t, 235.7)),
1.27f64.mul_add(
-f64::sin(deg2rad * 413335.38f64.mul_add(-t, 259.2)),
6.29f64.mul_add(
f64::sin(deg2rad * 477198.85f64.mul_add(t, 134.9)),
481267.8813f64.mul_add(t, 218.32),
),
),
),
),
),
);
let phi_ecliptic: f64 = deg2rad
* 0.17f64.mul_add(
-f64::sin(deg2rad * 407332.20f64.mul_add(-t, 217.6)),
0.28f64.mul_add(
-f64::sin(deg2rad * 6003.18f64.mul_add(t, 318.3)),
5.13f64.mul_add(
f64::sin(deg2rad * 483202.03f64.mul_add(t, 93.3)),
0.28 * f64::sin(deg2rad * 960400.87f64.mul_add(t, 228.2)),
),
),
);
let hparallax: f64 = deg2rad
* 0.0028f64.mul_add(
f64::cos(deg2rad * 954397.70f64.mul_add(t, 269.9)),
0.0078f64.mul_add(
f64::cos(deg2rad * 890534.23f64.mul_add(t, 235.7)),
0.0095f64.mul_add(
f64::cos(deg2rad * 413335.38f64.mul_add(-t, 259.2)),
0.0518f64.mul_add(f64::cos(deg2rad * 477198.85f64.mul_add(t, 134.9)), 0.9508),
),
),
);
let epsilon: f64 = deg2rad
* (5.04E-7 * t * t).mul_add(
t,
(1.64e-7 * t).mul_add(-t, 0.0130042f64.mul_add(-t, 23.439291)),
);
let rmag: f64 = consts::EARTH_RADIUS / f64::sin(hparallax);
rmag * numeris::vector![
f64::cos(phi_ecliptic) * f64::cos(lambda_ecliptic),
(f64::cos(epsilon) * f64::cos(phi_ecliptic)).mul_add(
f64::sin(lambda_ecliptic),
-(f64::sin(epsilon) * f64::sin(phi_ecliptic)),
),
(f64::sin(epsilon) * f64::cos(phi_ecliptic)).mul_add(
f64::sin(lambda_ecliptic),
f64::cos(epsilon) * f64::sin(phi_ecliptic),
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Instant;
#[test]
fn moonpos() {
let t0 = Instant::from_date(1994, 4, 28).unwrap();
let t = Instant::from_mjd_with_scale(t0.as_mjd_with_scale(TimeScale::UTC), TimeScale::TDB);
let pos = pos_gcrf(&t);
let ref_pos = [-134240.626E3, -311571.590E3, -126693.785E3];
for idx in 0..3 {
let err = f64::abs(pos[idx] / ref_pos[idx] - 1.0);
assert!(err < 1.0e-6);
}
}
#[test]
fn test_moon_ecliptic() {
let time = Instant::from_datetime(1998, 8, 21, 0, 12, 0.0).unwrap();
let jd = time.as_jd_with_scale(TimeScale::TDB);
println!("JD: {}", jd);
let lambda = ecliptic_longitude(&time);
let lambda_deg = lambda.to_degrees();
println!("Ecliptic Longitude: {} degrees", lambda_deg);
approx::assert_abs_diff_eq!(lambda_deg, -225.05353, epsilon = 0.5);
}
#[test]
fn test_phase() {
let time = Instant::from_datetime(2025, 11, 12, 0, 46, 0.0).unwrap();
let phasename = phase_name(&time);
let illumination = illumination(&time);
approx::assert_relative_eq!(illumination, 0.52, epsilon = 0.02);
assert!(phasename == MoonPhase::LastQuarter);
let time = Instant::from_datetime(2025, 11, 5, 13, 19, 0.0).unwrap();
let phase_rad = phase(&time);
let phase_deg = phase_rad.to_degrees();
println!("Phase degrees: {}", phase_deg);
approx::assert_relative_eq!(phase_deg, 180.0, epsilon = 0.2);
}
#[test]
fn test_moon_phases() {
let new_moon = Instant::from_datetime(2024, 1, 11, 12, 0, 0.0).unwrap();
assert_eq!(phase_name(&new_moon), MoonPhase::NewMoon);
let first_quarter = Instant::from_datetime(2024, 1, 18, 12, 0, 0.0).unwrap();
let phase = phase_name(&first_quarter);
assert!(
phase == MoonPhase::FirstQuarter
|| phase == MoonPhase::WaxingCrescent
|| phase == MoonPhase::WaxingGibbous,
"First quarter should be near 90 degrees, got {:?}",
phase
);
let full_moon = Instant::from_datetime(2024, 1, 25, 12, 0, 0.0).unwrap();
let phase = phase_name(&full_moon);
assert!(
phase == MoonPhase::FullMoon
|| phase == MoonPhase::WaxingGibbous
|| phase == MoonPhase::WaningGibbous,
"Full moon should be near 180 degrees, got {:?}",
phase
);
let last_quarter = Instant::from_datetime(2024, 2, 2, 12, 0, 0.0).unwrap();
let phase = phase_name(&last_quarter);
assert!(
phase == MoonPhase::LastQuarter
|| phase == MoonPhase::WaningGibbous
|| phase == MoonPhase::WaningCrescent,
"Last quarter should be near 270 degrees, got {:?}",
phase
);
}
#[test]
fn test_phase_name_method() {
assert_eq!(MoonPhase::NewMoon.name(), "New Moon");
assert_eq!(MoonPhase::WaxingCrescent.name(), "Waxing Crescent");
assert_eq!(MoonPhase::FirstQuarter.name(), "First Quarter");
assert_eq!(MoonPhase::WaxingGibbous.name(), "Waxing Gibbous");
assert_eq!(MoonPhase::FullMoon.name(), "Full Moon");
assert_eq!(MoonPhase::WaningGibbous.name(), "Waning Gibbous");
assert_eq!(MoonPhase::LastQuarter.name(), "Last Quarter");
assert_eq!(MoonPhase::WaningCrescent.name(), "Waning Crescent");
}
}