use std::sync::Arc;
use crate::astro::eop::EopValues;
use crate::ephemeris::DynEphemeris;
use crate::time::JulianDate;
use super::density::DensityProvider;
use super::errors::DynamicsError;
use super::gravity::GravityFieldProvider;
pub trait EarthOrientationProvider: Send + Sync {
fn eop_at(&self, _jd_utc: JulianDate) -> Option<EopValues> {
None
}
}
pub trait SolarActivityProvider: Send + Sync {
fn f107_sfu(&self, _jd_utc: JulianDate) -> Option<f64> {
None
}
fn ap_daily(&self, _jd_utc: JulianDate) -> Option<f64> {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PrecessionModel {
#[default]
IAU2006,
IAU2000,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeScaleHint {
#[default]
TT,
TCG,
TDB,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Conventions {
pub iau_precession_model: PrecessionModel,
pub time_scale_default: TimeScaleHint,
}
impl Default for Conventions {
fn default() -> Self {
Self {
iau_precession_model: PrecessionModel::IAU2006,
time_scale_default: TimeScaleHint::TT,
}
}
}
pub struct DynamicsContext {
pub ephemeris: Option<Arc<dyn DynEphemeris + Send + Sync>>,
pub atmosphere: Option<Arc<dyn DensityProvider + Send + Sync>>,
pub gravity: Option<Arc<dyn GravityFieldProvider + Send + Sync>>,
pub eop: Option<Arc<dyn EarthOrientationProvider + Send + Sync>>,
pub solar_activity: Option<Arc<dyn SolarActivityProvider + Send + Sync>>,
pub conventions: Conventions,
}
impl DynamicsContext {
pub fn empty() -> Self {
Self {
ephemeris: None,
atmosphere: None,
gravity: None,
eop: None,
solar_activity: None,
conventions: Conventions::default(),
}
}
pub fn builder() -> DynamicsContextBuilder {
DynamicsContextBuilder::new()
}
pub fn require_ephemeris(&self) -> Result<&Arc<dyn DynEphemeris + Send + Sync>, DynamicsError> {
self.ephemeris
.as_ref()
.ok_or(DynamicsError::EphemerisUnavailable {
body: "(any)",
source: None,
})
}
pub fn require_atmosphere(
&self,
) -> Result<&Arc<dyn DensityProvider + Send + Sync>, DynamicsError> {
self.atmosphere.as_ref().ok_or_else(|| {
DynamicsError::AtmosphereProviderError(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no atmosphere provider in DynamicsContext",
)))
})
}
pub fn require_gravity_field(
&self,
) -> Result<&Arc<dyn GravityFieldProvider + Send + Sync>, DynamicsError> {
self.gravity
.as_ref()
.ok_or(DynamicsError::GravityFieldUnavailable)
}
}
#[derive(Default)]
pub struct DynamicsContextBuilder {
ephemeris: Option<Arc<dyn DynEphemeris + Send + Sync>>,
atmosphere: Option<Arc<dyn DensityProvider + Send + Sync>>,
gravity: Option<Arc<dyn GravityFieldProvider + Send + Sync>>,
eop: Option<Arc<dyn EarthOrientationProvider + Send + Sync>>,
solar_activity: Option<Arc<dyn SolarActivityProvider + Send + Sync>>,
conventions: Conventions,
}
impl DynamicsContextBuilder {
pub fn new() -> Self {
Self {
ephemeris: None,
atmosphere: None,
gravity: None,
eop: None,
solar_activity: None,
conventions: Conventions::default(),
}
}
pub fn with_ephemeris(mut self, e: Arc<dyn DynEphemeris + Send + Sync>) -> Self {
self.ephemeris = Some(e);
self
}
pub fn with_atmosphere(mut self, a: Arc<dyn DensityProvider + Send + Sync>) -> Self {
self.atmosphere = Some(a);
self
}
pub fn with_gravity(mut self, g: Arc<dyn GravityFieldProvider + Send + Sync>) -> Self {
self.gravity = Some(g);
self
}
pub fn with_eop(mut self, e: Arc<dyn EarthOrientationProvider + Send + Sync>) -> Self {
self.eop = Some(e);
self
}
pub fn with_solar_activity(mut self, s: Arc<dyn SolarActivityProvider + Send + Sync>) -> Self {
self.solar_activity = Some(s);
self
}
pub fn with_conventions(mut self, c: Conventions) -> Self {
self.conventions = c;
self
}
pub fn build(self) -> DynamicsContext {
DynamicsContext {
ephemeris: self.ephemeris,
atmosphere: self.atmosphere,
gravity: self.gravity,
eop: self.eop,
solar_activity: self.solar_activity,
conventions: self.conventions,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::dynamics::density::ExponentialAtmosphere;
use crate::astro::dynamics::gravity::TwoBodyEarth;
#[test]
fn empty_has_no_providers() {
let ctx = DynamicsContext::empty();
assert!(ctx.ephemeris.is_none());
assert!(ctx.atmosphere.is_none());
assert!(ctx.gravity.is_none());
assert!(ctx.eop.is_none());
assert!(ctx.solar_activity.is_none());
}
#[test]
fn empty_uses_default_conventions() {
let ctx = DynamicsContext::empty();
assert_eq!(
ctx.conventions.iau_precession_model,
PrecessionModel::IAU2006
);
assert_eq!(ctx.conventions.time_scale_default, TimeScaleHint::TT);
}
#[test]
fn builder_with_gravity_roundtrip() {
let g: Arc<dyn GravityFieldProvider + Send + Sync> = Arc::new(TwoBodyEarth);
let ctx = DynamicsContextBuilder::new().with_gravity(g).build();
assert!(ctx.gravity.is_some());
assert!(ctx.ephemeris.is_none());
assert!(ctx.atmosphere.is_none());
}
#[test]
fn builder_with_atmosphere_roundtrip() {
let a: Arc<dyn DensityProvider + Send + Sync> = Arc::new(ExponentialAtmosphere::LEO_500KM);
let ctx = DynamicsContextBuilder::new().with_atmosphere(a).build();
assert!(ctx.atmosphere.is_some());
assert!(ctx.ephemeris.is_none());
assert!(ctx.gravity.is_none());
}
#[test]
fn builder_with_conventions() {
let c = Conventions {
iau_precession_model: PrecessionModel::IAU2000,
time_scale_default: TimeScaleHint::TDB,
};
let ctx = DynamicsContextBuilder::new().with_conventions(c).build();
assert_eq!(
ctx.conventions.iau_precession_model,
PrecessionModel::IAU2000
);
assert_eq!(ctx.conventions.time_scale_default, TimeScaleHint::TDB);
}
#[test]
fn context_builder_shorthand() {
let ctx = DynamicsContext::builder().build();
assert!(ctx.ephemeris.is_none());
}
#[test]
fn require_ephemeris_returns_error_when_absent() {
let ctx = DynamicsContext::empty();
let result = ctx.require_ephemeris();
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
DynamicsError::EphemerisUnavailable { .. }
));
}
#[test]
fn require_atmosphere_returns_error_when_absent() {
let ctx = DynamicsContext::empty();
let result = ctx.require_atmosphere();
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
DynamicsError::AtmosphereProviderError(_)
));
}
#[test]
fn require_gravity_field_returns_error_when_absent() {
let ctx = DynamicsContext::empty();
let result = ctx.require_gravity_field();
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
DynamicsError::GravityFieldUnavailable
));
}
#[test]
fn require_gravity_field_returns_ok_when_present() {
let g: Arc<dyn GravityFieldProvider + Send + Sync> = Arc::new(TwoBodyEarth);
let ctx = DynamicsContextBuilder::new().with_gravity(g).build();
assert!(ctx.require_gravity_field().is_ok());
}
#[test]
fn require_atmosphere_returns_ok_when_present() {
let a: Arc<dyn DensityProvider + Send + Sync> = Arc::new(ExponentialAtmosphere::LEO_500KM);
let ctx = DynamicsContextBuilder::new().with_atmosphere(a).build();
assert!(ctx.require_atmosphere().is_ok());
}
}