use crate::ephemeris::observe::{apparent, ecliptic_latlon, observe, EARTH};
use crate::ephemeris::{Kernel, KernelSet};
use crate::error::EngineError;
use crate::frames::{mxv, Orientation};
use crate::minor_bodies::{chiron, minor_body, minor_body_symbol, MINOR_BODIES};
use crate::pyfloat;
use crate::time::Time;
#[derive(Debug, Clone, PartialEq)]
pub struct BodyPosition {
pub name: &'static str,
pub symbol: &'static str,
pub longitude: f64,
pub latitude: f64,
pub distance: f64,
pub speed: f64,
pub is_retrograde: bool,
}
const TARGETS: [(&str, i32, &str, bool); 10] = [
("Sun", 10, "☉", false),
("Moon", 301, "☽", false),
("Mercury", 199, "☿", true),
("Venus", 299, "♀", true),
("Mars", 4, "♂", true),
("Jupiter", 5, "♃", true),
("Saturn", 6, "♄", true),
("Uranus", 7, "♅", true),
("Neptune", 8, "♆", true),
("Pluto", 9, "♇", true),
];
pub fn require_kernel(kernels: &KernelSet, jd: f64) -> Result<&Kernel, EngineError> {
kernels.for_jd(jd).ok_or(EngineError::OutOfRange {
jd,
coverage: kernels.coverage(),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum NodeType {
#[default]
Mean,
True,
}
impl NodeType {
pub fn from_code(code: &str) -> Self {
let code = code.trim();
if code.eq_ignore_ascii_case("true") || code.eq_ignore_ascii_case("osculating") {
NodeType::True
} else {
NodeType::Mean
}
}
pub fn code(self) -> &'static str {
match self {
NodeType::Mean => "mean",
NodeType::True => "true",
}
}
}
pub struct Planets {
pub bodies: Vec<BodyPosition>,
}
pub fn planets_and_sidereal_time(
kernels: &KernelSet,
jd: f64,
shift: f64,
node: NodeType,
) -> Result<(Planets, f64), EngineError> {
let sample = SpeedSample::at(kernels, jd)?;
let mut bodies = Vec::with_capacity(14 + MINOR_BODIES.len());
for (name, code, symbol, can_retrograde) in TARGETS {
let Ok((lat, lon, dist, speed)) = sample.observe(code) else {
continue;
};
bodies.push(BodyPosition {
name,
symbol,
longitude: pyfloat::rem(lon - shift, 360.0),
latitude: lat,
distance: dist,
speed,
is_retrograde: can_retrograde && speed < 0.0,
});
}
let t_cent = (jd - 2_451_545.0) / 36525.0;
let omega = pyfloat::rem(
125.0445479 - 1934.1362891 * t_cent
+ 0.0020754 * t_cent.powf(2.0)
+ t_cent.powf(3.0) / 467440.0
- t_cent.powf(4.0) / 60616000.0,
360.0,
);
let (node_longitude, node_speed) = match node {
NodeType::Mean => (omega, -0.05295),
NodeType::True => sample.true_node()?,
};
let north = pyfloat::rem(node_longitude - shift, 360.0);
for (name, symbol, longitude) in [
("North Node", "☊", north),
("South Node", "☋", pyfloat::rem(north + 180.0, 360.0)),
] {
bodies.push(BodyPosition {
name,
symbol,
longitude,
latitude: 0.0,
distance: 0.00257,
speed: node_speed,
is_retrograde: node_speed < 0.0,
});
}
if let Some(c) = chiron(jd, shift) {
bodies.push(BodyPosition {
name: "Chiron",
symbol: "⚷",
longitude: c.longitude,
latitude: 0.0,
distance: 0.0,
speed: c.speed,
is_retrograde: c.is_retrograde,
});
}
let perigee = pyfloat::rem(
83.3532465 + 4069.0137287 * t_cent
- 0.01032 * t_cent.powf(2.0)
- t_cent.powf(3.0) / 80053.0,
360.0,
);
let apogee = pyfloat::rem(perigee + 180.0, 360.0);
let u = pyfloat::rem(apogee - omega, 360.0).to_radians();
let inc = 5.1453964_f64.to_radians();
let proj_u = (u.sin() * inc.cos()).atan2(u.cos());
bodies.push(BodyPosition {
name: "Lilith",
symbol: "⚸",
longitude: pyfloat::rem(proj_u.to_degrees() + omega - shift, 360.0),
latitude: 0.0,
distance: 0.0027,
speed: 0.1114,
is_retrograde: false,
});
for name in MINOR_BODIES {
if let Some(m) = minor_body(name, sample.t.tt(), shift) {
bodies.push(BodyPosition {
name,
symbol: minor_body_symbol(name),
longitude: m.longitude,
latitude: 0.0,
distance: 0.0,
speed: m.speed,
is_retrograde: m.is_retrograde,
});
}
}
Ok((Planets { bodies }, sample.orientation.gast_hours))
}
pub fn planet_speed(kernels: &KernelSet, jd: f64, name: &str) -> Result<f64, EngineError> {
let (.., code, _, _) = TARGETS
.iter()
.find(|(n, ..)| *n == name)
.ok_or_else(|| EngineError::InvalidInput(format!("not a planet: {name}")))?;
Ok(SpeedSample::at(kernels, jd)?.observe(*code)?.3)
}
struct SpeedSample<'k> {
kernel: &'k Kernel,
t: Time,
t_plus: Time,
orientation: Orientation,
orientation_plus: Orientation,
}
impl<'k> SpeedSample<'k> {
fn at(kernels: &'k KernelSet, jd: f64) -> Result<Self, EngineError> {
let t = Time::from_ut1(jd);
let t_plus = Time::from_ut1(t.ut1() + 1.0 / 24.0);
let mut kernel = require_kernel(kernels, jd)?;
let plus_kernel = kernels.for_jd(t_plus.ut1());
if !plus_kernel.is_some_and(|k| std::ptr::eq(k, kernel)) {
kernel = require_kernel(kernels, t_plus.ut1())?;
}
Ok(SpeedSample {
kernel,
orientation: Orientation::at(&t),
orientation_plus: Orientation::at(&t_plus),
t,
t_plus,
})
}
fn observe(&self, code: i32) -> Result<(f64, f64, f64, f64), EngineError> {
let (kernel, t, t_plus) = (self.kernel, &self.t, &self.t_plus);
let a = observe(kernel, code, t)?;
let app = apparent(kernel, &a, t)?;
let a_plus = observe(kernel, code, t_plus)?;
let app_plus = apparent(kernel, &a_plus, t_plus)?;
let (lat, lon, dist) = ecliptic_latlon(&self.orientation, &app);
let lon_plus = ecliptic_latlon(&self.orientation_plus, &app_plus).1;
let d_lon = pyfloat::wrap180(lon_plus - lon);
Ok((lat, lon, dist, d_lon * 24.0))
}
fn true_node(&self) -> Result<(f64, f64), EngineError> {
let node_at = |t: &Time, orientation: &Orientation| -> Result<f64, EngineError> {
let (moon, moon_velocity) = self.kernel.barycentric(301, t)?;
let (earth, earth_velocity) = self.kernel.barycentric(EARTH, t)?;
let r = mxv(
&orientation.ecliptic,
&[moon[0] - earth[0], moon[1] - earth[1], moon[2] - earth[2]],
);
let v = mxv(
&orientation.ecliptic,
&[
moon_velocity[0] - earth_velocity[0],
moon_velocity[1] - earth_velocity[1],
moon_velocity[2] - earth_velocity[2],
],
);
let h_x = r[1] * v[2] - r[2] * v[1];
let h_y = r[2] * v[0] - r[0] * v[2];
Ok(h_x.atan2(-h_y).to_degrees().rem_euclid(360.0))
};
let lon = node_at(&self.t, &self.orientation)?;
let lon_plus = node_at(&self.t_plus, &self.orientation_plus)?;
Ok((lon, pyfloat::wrap180(lon_plus - lon) * 24.0))
}
}