use crate::broadcast::satellite_state;
use crate::constants::{
BDS_EPOCH_MINUS_GPS_EPOCH_S, GPST_MINUS_BDT_S, GPS_EPOCH_TO_J2000_S, SECONDS_PER_WEEK,
};
use crate::glonass;
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::spp::EphemerisSource;
use super::{
is_beidou_geo, parse_glonass, parse_iono_corrections, parse_leap_seconds, parse_nav,
BroadcastRecord, GlonassRecord, IonoCorrections, NavMessage, NavParseError, GLONASS_MAX_AGE_S,
MAX_EPHEMERIS_AGE_S,
};
pub struct BroadcastStore {
records: Vec<BroadcastRecord>,
glonass: Vec<GlonassRecord>,
leap_seconds: Option<f64>,
iono: IonoCorrections,
}
impl BroadcastStore {
pub fn new(records: Vec<BroadcastRecord>) -> Self {
Self {
records,
glonass: Vec::new(),
leap_seconds: None,
iono: IonoCorrections::default(),
}
}
pub fn from_nav(text: &str) -> Result<Self, NavParseError> {
let records = parse_nav(text)?
.into_iter()
.filter(Self::is_default_usable)
.collect();
let glonass = parse_glonass(text)?
.into_iter()
.filter(|r| r.sv_health == 0.0)
.collect();
Ok(Self {
records,
glonass,
leap_seconds: parse_leap_seconds(text),
iono: parse_iono_corrections(text),
})
}
pub fn iono_corrections(&self) -> IonoCorrections {
self.iono
}
pub fn glonass_records(&self) -> &[GlonassRecord] {
&self.glonass
}
fn is_default_usable(r: &BroadcastRecord) -> bool {
r.sv_health == 0.0
&& matches!(
r.message,
NavMessage::GpsLnav
| NavMessage::GalileoInav
| NavMessage::BeidouD1
| NavMessage::BeidouD2
)
}
pub fn records(&self) -> &[BroadcastRecord] {
&self.records
}
pub fn retain(&mut self, keep: impl FnMut(&BroadcastRecord) -> bool) {
self.records.retain(keep);
}
fn toe_continuous_s(rec: &BroadcastRecord) -> f64 {
f64::from(rec.week) * SECONDS_PER_WEEK + rec.elements.toe_sow
}
fn half_window_s(rec: &BroadcastRecord) -> f64 {
match rec.fit_interval_s {
Some(fit) => fit / 2.0,
None => MAX_EPHEMERIS_AGE_S,
}
}
fn select(&self, sat: GnssSatelliteId, t_continuous_s: f64) -> Option<&BroadcastRecord> {
self.records
.iter()
.filter(|r| r.satellite_id == sat)
.filter(|r| {
(t_continuous_s - Self::toe_continuous_s(r)).abs() <= Self::half_window_s(r)
})
.min_by(|a, b| {
let da = (t_continuous_s - Self::toe_continuous_s(a)).abs();
let db = (t_continuous_s - Self::toe_continuous_s(b)).abs();
da.partial_cmp(&db).unwrap_or(core::cmp::Ordering::Equal)
})
}
fn select_glonass(
&self,
sat: GnssSatelliteId,
t_j2000_s: f64,
) -> Option<(&GlonassRecord, f64)> {
let leap = self.leap_seconds?;
let toe_gpst = |r: &GlonassRecord| r.toe_utc_j2000_s + leap;
let rec = self
.glonass
.iter()
.filter(|r| r.satellite_id == sat)
.min_by(|a, b| {
let da = (t_j2000_s - toe_gpst(a)).abs();
let db = (t_j2000_s - toe_gpst(b)).abs();
da.partial_cmp(&db).unwrap_or(core::cmp::Ordering::Equal)
})?;
let tk = t_j2000_s - toe_gpst(rec);
if tk.abs() <= GLONASS_MAX_AGE_S {
Some((rec, tk))
} else {
None
}
}
}
impl core::str::FromStr for BroadcastStore {
type Err = NavParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_nav(s)
}
}
impl EphemerisSource for BroadcastStore {
fn position_clock_at_j2000_s(
&self,
sat: GnssSatelliteId,
t_j2000_s: f64,
) -> Option<([f64; 3], f64)> {
if sat.system == GnssSystem::Glonass {
let (rec, tk) = self.select_glonass(sat, t_j2000_s)?;
let state0 = [
rec.pos_m[0],
rec.pos_m[1],
rec.pos_m[2],
rec.vel_m_s[0],
rec.vel_m_s[1],
rec.vel_m_s[2],
];
let state = glonass::propagate(state0, rec.acc_m_s2, tk);
let clock = glonass::clock_offset_s(rec.clk_bias, rec.gamma_n, tk);
return Some(([state[0], state[1], state[2]], clock));
}
if !matches!(
sat.system,
GnssSystem::Gps | GnssSystem::Galileo | GnssSystem::BeiDou
) {
return None;
}
let gpst_continuous = t_j2000_s + GPS_EPOCH_TO_J2000_S;
let (t_continuous, is_geo) = if sat.system == GnssSystem::BeiDou {
(
gpst_continuous - GPST_MINUS_BDT_S - BDS_EPOCH_MINUS_GPS_EPOCH_S,
is_beidou_geo(sat),
)
} else {
(gpst_continuous, false)
};
let rec = self.select(sat, t_continuous)?;
let sow = t_continuous.rem_euclid(SECONDS_PER_WEEK);
let state = satellite_state(
&rec.elements,
&rec.clock,
&rec.constants(),
sow,
rec.group_delay_s,
is_geo,
);
Some((
state.orbit.position().as_array(),
state.clock.dt_clock_total_s,
))
}
}