use crate::evaluator::Target;
use crate::units::angular::{Degrees, Radians};
use siderust::bodies::Sun as SunBody;
use siderust::coordinates::centers::Geodetic;
use siderust::coordinates::frames::{EclipticMeanJ2000, ECEF};
use siderust::coordinates::spherical::Direction as SphericalDirection;
use siderust::coordinates::transform::TransformFrame;
use siderust::event::horizontal::star_horizontal;
use siderust::qtty::Radian;
use siderust::JulianDate;
use tempoch::{Time, UTC};
use crate::error::{NsbError, Result};
#[derive(Debug, Clone, Copy)]
pub(super) struct ZodiacalGeometry {
pub beta: Radians,
pub delta_lambda: Radians,
pub zenith: Option<Degrees>,
}
pub(super) fn compute_observed(
time: Time<UTC>,
location: Geodetic<ECEF>,
target: Target,
) -> Result<ZodiacalGeometry> {
compute_observed_with_longitude(
time,
location,
target,
SunBody::ecliptic_longitude_geocentric(to_jd(time)),
)
}
pub(super) fn compute_observed_with_longitude(
time: Time<UTC>,
location: Geodetic<ECEF>,
target: Target,
longitude: Radians,
) -> Result<ZodiacalGeometry> {
let jd = to_jd(time);
let (beta, delta_lambda) = ecliptic_geometry_with_longitude(target, longitude)?;
let hz = star_horizontal(target.ra(), target.dec(), &location, jd);
let alt = hz.alt();
let zenith = Degrees::new(90.0) - alt;
Ok(ZodiacalGeometry {
beta,
delta_lambda,
zenith: Some(zenith),
})
}
fn ecliptic_geometry_with_longitude(
target: Target,
lambda_sun: Radians,
) -> Result<(Radians, Radians)> {
let ecl: SphericalDirection<EclipticMeanJ2000> = target.to_frame();
let beta = ecl.lat().to::<Radian>();
let ecliptic_lon = ecl.lon().to::<Radian>();
let delta_lambda = ecliptic_lon.abs_separation(lambda_sun);
if !beta.is_finite() {
return Err(NsbError::OutOfRange(format!(
"computed ecliptic latitude β={} rad is not finite",
beta.value()
)));
}
if !delta_lambda.is_finite() {
return Err(NsbError::OutOfRange(format!(
"computed Δλ={} rad is not finite",
delta_lambda.value()
)));
}
Ok((beta, delta_lambda))
}
fn to_jd(time: Time<UTC>) -> JulianDate {
use tempoch::{JD, TT};
time.to::<TT>().to::<JD>()
}
#[cfg(test)]
pub(super) mod test_support {
use super::*;
pub(in crate::components::zodiacal) fn compute_exoatmospheric(
time: Time<UTC>,
target: Target,
) -> Result<ZodiacalGeometry> {
let jd = to_jd(time);
let (beta, delta_lambda) =
ecliptic_geometry_with_longitude(target, SunBody::ecliptic_longitude_geocentric(jd))?;
Ok(ZodiacalGeometry {
beta,
delta_lambda,
zenith: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approximate_solar_longitude_j2000(jd: JulianDate) -> Radians {
let days = jd.raw().value() - 2_451_545.0;
let centuries = days / 36_525.0;
let mean_longitude = (280.466_46 + 0.985_647_36 * days).to_radians();
let mean_anomaly = (357.529_11 + 0.985_600_28 * days).to_radians();
let longitude_of_date = mean_longitude
+ 1.914_602_f64.to_radians() * mean_anomaly.sin()
+ 0.019_993_f64.to_radians() * (2.0 * mean_anomaly).sin()
+ 0.000_289_f64.to_radians() * (3.0 * mean_anomaly).sin();
let ecliptic_precession =
(1.397 * centuries + 0.000_31 * centuries * centuries).to_radians();
Radians::new((longitude_of_date - ecliptic_precession).rem_euclid(std::f64::consts::TAU))
}
use chrono::{TimeZone, Utc};
#[test]
fn reduced_solar_longitude_tracks_vsop87_over_a_year() {
let start =
Time::<UTC>::from_chrono(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).single().unwrap());
let mut max_error_deg = 0.0_f64;
for day in 0..=365 {
let time =
Time::<UTC>::from_chrono(start.to_chrono().unwrap() + chrono::Duration::days(day));
let jd = to_jd(time);
let exact = SunBody::ecliptic_longitude_geocentric(jd);
let approximate = approximate_solar_longitude_j2000(jd);
max_error_deg =
max_error_deg.max(exact.abs_separation(approximate).value().to_degrees());
}
assert!(
max_error_deg < 0.02,
"maximum longitude error {max_error_deg} deg"
);
}
}