use crate::astro::conic::ConicError;
use crate::astro::units::GaussianYears;
use crate::qtty::angular_rate::AngularRate;
use crate::qtty::*;
use crate::time::JulianDate;
use affn::conic::{
ClassifiedSemiMajorAxisParam, ConicOrientation, Elliptic, SemiMajorAxisParam,
TypedSemiMajorAxisParam,
};
use affn::frames::EclipticMeanJ2000;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))]
pub struct KeplerianOrbit<U: LengthUnit = AstronomicalUnit> {
shape: TypedSemiMajorAxisParam<U, Elliptic>,
orientation: ConicOrientation<EclipticMeanJ2000>,
pub mean_anomaly_at_epoch: Degrees,
pub epoch: JulianDate,
}
impl<U: LengthUnit> std::fmt::Display for KeplerianOrbit<U>
where
Quantity<U>: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"a={}, e={:.6}, i={}, \u{03a9}={}, \u{03c9}={}, M\u{2080}={}, epoch={}",
self.shape.semi_major_axis(),
self.shape.eccentricity(),
self.orientation.inclination(),
self.orientation.longitude_of_ascending_node(),
self.orientation.argument_of_periapsis(),
self.mean_anomaly_at_epoch,
self.epoch,
)
}
}
impl<U: LengthUnit> KeplerianOrbit<U> {
pub const fn new(
semi_major_axis: Quantity<U>,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_anomaly_at_epoch: Degrees,
epoch: JulianDate,
) -> Self {
Self {
shape: TypedSemiMajorAxisParam::new_unchecked(SemiMajorAxisParam::new_unchecked(
semi_major_axis,
eccentricity,
)),
orientation: ConicOrientation::new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
),
mean_anomaly_at_epoch,
epoch,
}
}
pub fn try_new(
semi_major_axis: Quantity<U>,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_anomaly_at_epoch: Degrees,
epoch: JulianDate,
) -> Result<Self, crate::astro::conic::ConicError> {
use crate::astro::conic::map_validation_error;
let sma = SemiMajorAxisParam::try_new(semi_major_axis, eccentricity)
.map_err(map_validation_error)?;
let typed = match sma.classify() {
ClassifiedSemiMajorAxisParam::Elliptic(t) => t,
ClassifiedSemiMajorAxisParam::Hyperbolic(_) => {
return Err(ConicError::HyperbolicNotSupported);
}
};
let orientation = ConicOrientation::try_new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
)
.map_err(map_validation_error)?;
if !mean_anomaly_at_epoch.is_finite() {
return Err(ConicError::InvalidMeanAnomaly);
}
if !epoch.jd_value().is_finite() {
return Err(ConicError::InvalidEpoch);
}
Ok(Self {
shape: typed,
orientation,
mean_anomaly_at_epoch,
epoch,
})
}
#[inline]
pub fn shape(&self) -> &TypedSemiMajorAxisParam<U, Elliptic> {
&self.shape
}
#[inline]
pub fn orientation(&self) -> &ConicOrientation<EclipticMeanJ2000> {
&self.orientation
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct OrientationTrig {
sin_i: f64,
cos_i: f64,
sin_omega: f64,
cos_omega: f64,
sin_node: f64,
cos_node: f64,
}
impl OrientationTrig {
#[inline]
pub(crate) fn sin_i(&self) -> f64 {
self.sin_i
}
#[inline]
pub(crate) fn cos_i(&self) -> f64 {
self.cos_i
}
#[inline]
pub(crate) fn sin_omega(&self) -> f64 {
self.sin_omega
}
#[inline]
pub(crate) fn cos_omega(&self) -> f64 {
self.cos_omega
}
#[inline]
pub(crate) fn sin_node(&self) -> f64 {
self.sin_node
}
#[inline]
pub(crate) fn cos_node(&self) -> f64 {
self.cos_node
}
pub(crate) fn from_orientation(o: &ConicOrientation<EclipticMeanJ2000>) -> Self {
let (sin_i, cos_i) = o.inclination().sin_cos();
let (sin_omega, cos_omega) = o.argument_of_periapsis().sin_cos();
let (sin_node, cos_node) = o.longitude_of_ascending_node().sin_cos();
Self {
sin_i,
cos_i,
sin_omega,
cos_omega,
sin_node,
cos_node,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PreparedOrbit {
elements: KeplerianOrbit,
mean_motion: AngularRate<Radian, Day>,
m0_rad: f64,
trig: OrientationTrig,
}
impl PreparedOrbit {
pub fn try_from_elements(
semi_major_axis: AstronomicalUnits,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_anomaly_at_epoch: Degrees,
epoch: JulianDate,
) -> Result<Self, ConicError> {
let orbit = KeplerianOrbit::try_new(
semi_major_axis,
eccentricity,
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
mean_anomaly_at_epoch,
epoch,
)?;
Ok(Self::from_validated(orbit))
}
#[inline]
pub fn elements(&self) -> &KeplerianOrbit {
&self.elements
}
#[inline]
pub fn mean_motion(&self) -> AngularRate<Radian, Day> {
self.mean_motion
}
#[inline]
pub(crate) fn m0_rad(&self) -> f64 {
self.m0_rad
}
#[inline]
pub(crate) fn orientation_trig(&self) -> &OrientationTrig {
&self.trig
}
pub(crate) fn from_validated(orbit: KeplerianOrbit) -> Self {
let a = orbit.shape().semi_major_axis().value();
let t_gaussian_years = a * a.sqrt();
let period_days = GaussianYears::new(t_gaussian_years).to::<Day>().value();
let mean_motion = AngularRate::<Radian, Day>::new(std::f64::consts::TAU / period_days);
let m0_rad = orbit.mean_anomaly_at_epoch.to::<Radian>().value();
let trig = OrientationTrig::from_orientation(orbit.orientation());
Self {
elements: orbit,
mean_motion,
m0_rad,
trig,
}
}
}
impl TryFrom<KeplerianOrbit> for PreparedOrbit {
type Error = ConicError;
fn try_from(orbit: KeplerianOrbit) -> Result<Self, Self::Error> {
Ok(Self::from_validated(orbit))
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for PreparedOrbit {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let orbit = KeplerianOrbit::deserialize(deserializer)?;
Self::try_from(orbit).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl Serialize for PreparedOrbit {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.elements.serialize(serializer)
}
}