use crate::body::Body;
use crate::ephem::{moon_position, sun_position};
type Vec3 = [f64; 3];
const JD_J2000: f64 = 2_451_545.0;
const DAYS_PER_JULIAN_CENTURY: f64 = 36_525.0;
pub trait EphemerisProvider: std::fmt::Debug {
fn relative_position(&self, target: &Body, center: &Body, jd_tdb: f64) -> Option<Vec3>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BuiltinEphemeris;
impl BuiltinEphemeris {
fn t_tt_jc(jd_tdb: f64) -> f64 {
(jd_tdb - JD_J2000) / DAYS_PER_JULIAN_CENTURY
}
}
impl EphemerisProvider for BuiltinEphemeris {
fn relative_position(&self, target: &Body, center: &Body, jd_tdb: f64) -> Option<Vec3> {
if target.name == center.name {
return Some([0.0, 0.0, 0.0]);
}
let t = Self::t_tt_jc(jd_tdb);
match (target.name, center.name) {
("Sun", "Earth") => Some(sun_position(t)),
("Moon", "Earth") => Some(moon_position(t)),
("Earth", "Sun") => Some(neg(sun_position(t))),
("Earth", "Moon") => Some(neg(moon_position(t))),
_ => None,
}
}
}
fn neg(v: Vec3) -> Vec3 {
[-v[0], -v[1], -v[2]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ephem::{moon_position, sun_position};
const PROBE_JD: f64 = 2_459_580.5;
fn t_at(jd: f64) -> f64 {
(jd - 2_451_545.0) / 36_525.0
}
#[test]
fn sun_relative_to_earth_is_the_builtin_sun_series() {
let p = BuiltinEphemeris;
let got = p
.relative_position(&Body::sun(), &Body::earth(), PROBE_JD)
.expect("builtin must supply Sun relative to Earth");
let want = sun_position(t_at(PROBE_JD));
for k in 0..3 {
assert!(
(got[k] - want[k]).abs() < 1e-9,
"Sun/Earth component {k}: got {} want {}",
got[k],
want[k]
);
}
}
#[test]
fn moon_relative_to_earth_is_the_builtin_moon_series() {
let p = BuiltinEphemeris;
let got = p
.relative_position(&Body::moon(), &Body::earth(), PROBE_JD)
.expect("builtin must supply Moon relative to Earth");
let want = moon_position(t_at(PROBE_JD));
for k in 0..3 {
assert!(
(got[k] - want[k]).abs() < 1e-9,
"Moon/Earth component {k}: got {} want {}",
got[k],
want[k]
);
}
}
#[test]
fn body_relative_to_itself_is_the_origin() {
let p = BuiltinEphemeris;
for b in [Body::earth(), Body::sun(), Body::moon(), Body::mars()] {
let got = p
.relative_position(&b, &b, PROBE_JD)
.expect("a body relative to itself is always defined");
assert_eq!(got, [0.0, 0.0, 0.0], "{} relative to itself", b.name);
}
}
#[test]
fn earth_relative_to_sun_is_the_negated_sun_vector() {
let p = BuiltinEphemeris;
let earth_sun = p
.relative_position(&Body::earth(), &Body::sun(), PROBE_JD)
.expect("builtin must supply Earth relative to Sun");
let sun_earth = sun_position(t_at(PROBE_JD));
for k in 0..3 {
assert!(
(earth_sun[k] + sun_earth[k]).abs() < 1e-9,
"Earth/Sun component {k} must be the negated Sun/Earth: {} vs {}",
earth_sun[k],
-sun_earth[k]
);
}
}
#[test]
fn earth_relative_to_moon_is_the_negated_moon_vector() {
let p = BuiltinEphemeris;
let earth_moon = p
.relative_position(&Body::earth(), &Body::moon(), PROBE_JD)
.expect("builtin must supply Earth relative to Moon");
let moon_earth = moon_position(t_at(PROBE_JD));
for k in 0..3 {
assert!(
(earth_moon[k] + moon_earth[k]).abs() < 1e-9,
"Earth/Moon component {k} must be the negated Moon/Earth",
);
}
}
#[test]
fn mars_relative_to_earth_is_unsupported() {
let p = BuiltinEphemeris;
assert!(
p.relative_position(&Body::mars(), &Body::earth(), PROBE_JD)
.is_none(),
"the kernel-free builtin must not invent a Mars position"
);
assert!(
p.relative_position(&Body::earth(), &Body::mars(), PROBE_JD)
.is_none(),
"the reverse Mars pair is equally unsupported"
);
assert!(
p.relative_position(&Body::sun(), &Body::mars(), PROBE_JD)
.is_none(),
"Sun relative to Mars is unsupported by the geocentric builtin"
);
}
#[test]
fn uncomposable_sun_relative_to_moon_is_none() {
let p = BuiltinEphemeris;
assert!(
p.relative_position(&Body::sun(), &Body::moon(), PROBE_JD)
.is_none(),
"the builtin does not compose Sun relative to Moon"
);
}
#[test]
fn usable_as_a_trait_object() {
let p: &dyn EphemerisProvider = &BuiltinEphemeris;
assert!(p
.relative_position(&Body::sun(), &Body::earth(), PROBE_JD)
.is_some());
}
}