use std::borrow::Cow;
use std::fmt;
use crate::astro::constants::time::{SECONDS_PER_DAY, SECONDS_PER_DAY_I64};
use crate::atmosphere::Ionex;
use crate::ephemeris::{EphemerisSource, Sp3, Sp3State};
use crate::frame::Wgs84Geodetic;
use crate::id::GnssSatelliteId;
use crate::ionex::ionex_slant_delay;
pub const DEFAULT_MAX_STALENESS_DAYS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DegradationKind {
Exact,
NearestPrior,
DiurnalShift,
}
impl DegradationKind {
pub fn is_exact(self) -> bool {
matches!(self, DegradationKind::Exact)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StalenessMetadata {
pub kind: DegradationKind,
pub requested_epoch_j2000_s: f64,
pub source_epoch_j2000_s: f64,
pub staleness_s: f64,
pub staleness_days: f64,
}
impl StalenessMetadata {
fn exact(epoch_j2000_s: f64) -> Self {
Self {
kind: DegradationKind::Exact,
requested_epoch_j2000_s: epoch_j2000_s,
source_epoch_j2000_s: epoch_j2000_s,
staleness_s: 0.0,
staleness_days: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StalenessPolicy {
pub max_staleness_s: f64,
}
impl StalenessPolicy {
pub fn days(days: f64) -> Self {
Self {
max_staleness_s: days * SECONDS_PER_DAY,
}
}
pub fn seconds(seconds: f64) -> Self {
Self {
max_staleness_s: seconds,
}
}
}
impl Default for StalenessPolicy {
fn default() -> Self {
Self::days(f64::from(DEFAULT_MAX_STALENESS_DAYS))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SelectionError {
EmptyProductSet,
InvalidRange {
start_epoch_j2000_s: f64,
end_epoch_j2000_s: f64,
},
NoPriorProduct {
requested_epoch_j2000_s: f64,
},
BeyondStalenessCap {
requested_epoch_j2000_s: f64,
source_epoch_j2000_s: f64,
staleness_s: f64,
max_staleness_s: f64,
},
InvalidProduct(String),
InvalidPolicy {
max_staleness_s: f64,
},
Overflow {
context: &'static str,
},
}
impl fmt::Display for SelectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectionError::EmptyProductSet => write!(f, "product set is empty"),
SelectionError::InvalidRange {
start_epoch_j2000_s,
end_epoch_j2000_s,
} => write!(
f,
"invalid epoch range [{start_epoch_j2000_s}, {end_epoch_j2000_s}]"
),
SelectionError::NoPriorProduct {
requested_epoch_j2000_s,
} => write!(
f,
"no product at or before requested epoch {requested_epoch_j2000_s} J2000 s"
),
SelectionError::BeyondStalenessCap {
requested_epoch_j2000_s,
source_epoch_j2000_s,
staleness_s,
max_staleness_s,
} => write!(
f,
"nearest product (epoch {source_epoch_j2000_s} J2000 s) is {staleness_s} s stale \
for requested epoch {requested_epoch_j2000_s} J2000 s, over the {max_staleness_s} s cap"
),
SelectionError::InvalidProduct(msg) => write!(f, "invalid product in set: {msg}"),
SelectionError::InvalidPolicy { max_staleness_s } => write!(
f,
"staleness cap {max_staleness_s} s is not a finite, non-negative number of seconds"
),
SelectionError::Overflow { context } => {
write!(f, "epoch arithmetic overflow: {context}")
}
}
}
}
fn validate_policy(policy: StalenessPolicy) -> Result<(), SelectionError> {
if policy.max_staleness_s.is_finite() && policy.max_staleness_s >= 0.0 {
Ok(())
} else {
Err(SelectionError::InvalidPolicy {
max_staleness_s: policy.max_staleness_s,
})
}
}
impl std::error::Error for SelectionError {}
#[derive(Debug, Clone, PartialEq)]
pub struct IonexSelection<'a> {
ionex: Cow<'a, Ionex>,
metadata: StalenessMetadata,
}
impl IonexSelection<'_> {
pub fn metadata(&self) -> StalenessMetadata {
self.metadata
}
pub fn ionex(&self) -> &Ionex {
self.ionex.as_ref()
}
pub fn slant_delay(
&self,
receiver: Wgs84Geodetic,
elevation_rad: f64,
azimuth_rad: f64,
epoch_j2000_s: i64,
frequency_hz: f64,
) -> crate::Result<f64> {
ionex_slant_delay(
self.ionex.as_ref(),
receiver,
elevation_rad,
azimuth_rad,
epoch_j2000_s,
frequency_hz,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Sp3Selection<'a> {
sp3: &'a Sp3,
metadata: StalenessMetadata,
}
impl Sp3Selection<'_> {
pub fn metadata(&self) -> StalenessMetadata {
self.metadata
}
pub fn sp3(&self) -> &Sp3 {
self.sp3
}
pub fn position_at_j2000_seconds(
&self,
sat: GnssSatelliteId,
query_j2000_s: f64,
) -> crate::Result<Sp3State> {
self.sp3.position_at_j2000_seconds(sat, query_j2000_s)
}
}
impl EphemerisSource for Sp3Selection<'_> {
fn position_clock_at_j2000_s(
&self,
sat: GnssSatelliteId,
t_j2000_s: f64,
) -> Option<([f64; 3], f64)> {
self.sp3.position_clock_at_j2000_s(sat, t_j2000_s)
}
}
pub fn select_ionex(
products: &[Ionex],
requested_epoch_j2000_s: i64,
policy: StalenessPolicy,
) -> Result<IonexSelection<'_>, SelectionError> {
select_ionex_over_range(
products,
requested_epoch_j2000_s,
requested_epoch_j2000_s,
policy,
)
}
pub fn select_ionex_over_range(
products: &[Ionex],
start_epoch_j2000_s: i64,
end_epoch_j2000_s: i64,
policy: StalenessPolicy,
) -> Result<IonexSelection<'_>, SelectionError> {
validate_policy(policy)?;
if products.is_empty() {
return Err(SelectionError::EmptyProductSet);
}
if end_epoch_j2000_s < start_epoch_j2000_s {
return Err(SelectionError::InvalidRange {
start_epoch_j2000_s: start_epoch_j2000_s as f64,
end_epoch_j2000_s: end_epoch_j2000_s as f64,
});
}
let mut exact: Option<(&Ionex, i64, i64)> = None;
for product in products {
let (lo, hi) = ionex_span(product)?;
if lo <= start_epoch_j2000_s && end_epoch_j2000_s <= hi {
let better = match exact {
None => true,
Some((_, best_lo, best_hi)) => lo > best_lo || (lo == best_lo && hi < best_hi),
};
if better {
exact = Some((product, lo, hi));
}
}
}
if let Some((product, _, _)) = exact {
return Ok(IonexSelection {
ionex: Cow::Borrowed(product),
metadata: StalenessMetadata::exact(end_epoch_j2000_s as f64),
});
}
let mut priors: Vec<(&Ionex, i64, i64)> = products
.iter()
.filter_map(|product| match ionex_span(product) {
Ok((lo, hi)) if hi < start_epoch_j2000_s => Some(Ok((product, lo, hi))),
Ok(_) => None,
Err(error) => Some(Err(error)),
})
.collect::<Result<_, _>>()?;
if priors.is_empty() {
return Err(SelectionError::NoPriorProduct {
requested_epoch_j2000_s: end_epoch_j2000_s as f64,
});
}
priors.sort_by(|a, b| b.2.cmp(&a.2).then(a.1.cmp(&b.1)));
let mut beyond_cap: Option<(i64, i64)> = None; let mut overflow_ctx: Option<&'static str> = None;
for (product, lo, hi) in priors {
let Some(gap_s) = end_epoch_j2000_s.checked_sub(hi) else {
overflow_ctx.get_or_insert("end - hi");
continue;
}; let days = gap_s / SECONDS_PER_DAY_I64 + i64::from(gap_s % SECONDS_PER_DAY_I64 != 0); let Some(staleness_s) = days.checked_mul(SECONDS_PER_DAY_I64) else {
overflow_ctx.get_or_insert("days * 86400");
continue;
};
let Some(source_epoch_j2000_s) = end_epoch_j2000_s.checked_sub(staleness_s) else {
overflow_ctx.get_or_insert("end - staleness");
continue;
};
if staleness_s as f64 > policy.max_staleness_s {
beyond_cap = Some((source_epoch_j2000_s, staleness_s));
break;
}
let (Some(shifted_lo), Some(shifted_hi)) =
(lo.checked_add(staleness_s), hi.checked_add(staleness_s))
else {
overflow_ctx.get_or_insert("epoch + staleness");
continue;
};
if shifted_lo <= start_epoch_j2000_s && end_epoch_j2000_s <= shifted_hi {
let shifted = product
.with_map_epochs_shifted_days(days)
.map_err(|error| SelectionError::InvalidProduct(error.to_string()))?;
return Ok(IonexSelection {
ionex: Cow::Owned(shifted),
metadata: StalenessMetadata {
kind: DegradationKind::DiurnalShift,
requested_epoch_j2000_s: end_epoch_j2000_s as f64,
source_epoch_j2000_s: source_epoch_j2000_s as f64,
staleness_s: staleness_s as f64,
staleness_days: days as f64,
},
});
}
}
if let Some((source_epoch_j2000_s, staleness_s)) = beyond_cap {
return Err(SelectionError::BeyondStalenessCap {
requested_epoch_j2000_s: end_epoch_j2000_s as f64,
source_epoch_j2000_s: source_epoch_j2000_s as f64,
staleness_s: staleness_s as f64,
max_staleness_s: policy.max_staleness_s,
});
}
if let Some(context) = overflow_ctx {
return Err(SelectionError::Overflow { context });
}
Err(SelectionError::InvalidProduct(format!(
"no prior IONEX product covers requested range \
[{start_epoch_j2000_s}, {end_epoch_j2000_s}] J2000 s after a whole-day diurnal shift"
)))
}
pub fn select_sp3(
products: &[Sp3],
requested_epoch_j2000_s: f64,
policy: StalenessPolicy,
) -> Result<Sp3Selection<'_>, SelectionError> {
select_sp3_over_range(
products,
requested_epoch_j2000_s,
requested_epoch_j2000_s,
policy,
)
}
pub fn select_sp3_over_range(
products: &[Sp3],
start_epoch_j2000_s: f64,
end_epoch_j2000_s: f64,
policy: StalenessPolicy,
) -> Result<Sp3Selection<'_>, SelectionError> {
validate_policy(policy)?;
if products.is_empty() {
return Err(SelectionError::EmptyProductSet);
}
if !start_epoch_j2000_s.is_finite()
|| !end_epoch_j2000_s.is_finite()
|| end_epoch_j2000_s < start_epoch_j2000_s
{
return Err(SelectionError::InvalidRange {
start_epoch_j2000_s,
end_epoch_j2000_s,
});
}
let mut exact: Option<(&Sp3, f64, f64)> = None;
for product in products {
let (lo, hi) = sp3_span(product)?;
if lo <= start_epoch_j2000_s && end_epoch_j2000_s <= hi {
let better = match exact {
None => true,
Some((_, best_lo, best_hi)) => lo > best_lo || (lo == best_lo && hi < best_hi),
};
if better {
exact = Some((product, lo, hi));
}
}
}
if let Some((product, _, _)) = exact {
return Ok(Sp3Selection {
sp3: product,
metadata: StalenessMetadata::exact(end_epoch_j2000_s),
});
}
let mut best: Option<(&Sp3, f64)> = None;
for product in products {
let (lo, hi) = sp3_span(product)?;
if lo <= start_epoch_j2000_s
&& hi < end_epoch_j2000_s
&& best.is_none_or(|(_, best_hi)| hi > best_hi)
{
best = Some((product, hi));
}
}
let (product, hi) = best.ok_or(SelectionError::NoPriorProduct {
requested_epoch_j2000_s: end_epoch_j2000_s,
})?;
let staleness_s = end_epoch_j2000_s - hi; if staleness_s > policy.max_staleness_s {
return Err(SelectionError::BeyondStalenessCap {
requested_epoch_j2000_s: end_epoch_j2000_s,
source_epoch_j2000_s: hi,
staleness_s,
max_staleness_s: policy.max_staleness_s,
});
}
Ok(Sp3Selection {
sp3: product,
metadata: StalenessMetadata {
kind: DegradationKind::NearestPrior,
requested_epoch_j2000_s: end_epoch_j2000_s,
source_epoch_j2000_s: hi,
staleness_s,
staleness_days: staleness_s / SECONDS_PER_DAY,
},
})
}
fn ionex_span(product: &Ionex) -> Result<(i64, i64), SelectionError> {
let epochs = product.map_epochs_s();
let first = *epochs
.first()
.ok_or_else(|| SelectionError::InvalidProduct("IONEX product has no maps".into()))?;
let last = *epochs.last().expect("non-empty epochs has a last element");
Ok((first, last))
}
fn sp3_span(product: &Sp3) -> Result<(f64, f64), SelectionError> {
let epochs = product.epochs_j2000_seconds();
let first = *epochs
.first()
.ok_or_else(|| SelectionError::InvalidProduct("SP3 product has no epochs".into()))?;
let last = *epochs.last().expect("non-empty epochs has a last element");
Ok((first, last))
}
#[cfg(test)]
mod tests;