use crate::broadcast::{satellite_state, satellite_state_unchecked};
use crate::constants::{
BDS_EPOCH_MINUS_GPS_EPOCH_S, GPST_MINUS_BDT_S, GPS_EPOCH_TO_J2000_S, SECONDS_PER_WEEK,
};
use crate::error::{Error, Result as CoreResult};
use crate::glonass;
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::spp::EphemerisSource;
use super::{
is_beidou_geo, parse_glonass, parse_iono_corrections_checked, parse_leap_seconds_checked,
parse_nav, BroadcastGroupDelays, BroadcastIssue, 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>) -> CoreResult<Self> {
for record in &records {
validate_manual_record(record)?;
}
Ok(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_checked(text)?,
iono: parse_iono_corrections_checked(text)?,
})
}
pub fn iono_corrections(&self) -> IonoCorrections {
self.iono
}
pub fn glonass_records(&self) -> &[GlonassRecord] {
&self.glonass
}
pub fn glonass_frequency_channels(&self) -> std::collections::BTreeMap<u8, i8> {
self.glonass
.iter()
.map(|r| (r.satellite_id.prn, r.freq_channel as i8))
.collect()
}
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 select_by_iode_at(
&self,
sat: GnssSatelliteId,
iode: u8,
t_j2000_s: f64,
) -> Option<&BroadcastRecord> {
let (t_continuous, _) = query_continuous_time(sat, t_j2000_s)?;
self.records
.iter()
.filter(|r| r.satellite_id == sat)
.filter(|r| r.issue_of_data.issue == u32::from(iode))
.filter(|r| (t_continuous - Self::toe_continuous_s(r)).abs() <= Self::half_window_s(r))
.min_by(|a, b| {
let da = (t_continuous - Self::toe_continuous_s(a)).abs();
let db = (t_continuous - Self::toe_continuous_s(b)).abs();
da.partial_cmp(&db).unwrap_or(core::cmp::Ordering::Equal)
})
}
pub fn state_by_iode_at(
&self,
sat: GnssSatelliteId,
iode: u8,
t_j2000_s: f64,
) -> Option<([f64; 3], f64)> {
let (t_continuous, is_geo) = query_continuous_time(sat, t_j2000_s)?;
let rec = self.select_by_iode_at(sat, iode, t_j2000_s)?;
let sow = t_continuous.rem_euclid(SECONDS_PER_WEEK);
let state = satellite_state_unchecked(
&rec.elements,
&rec.clock,
&rec.constants(),
sow,
rec.broadcast_clock_group_delay_s(),
is_geo,
);
let position = state.orbit.position().ok()?;
Some((position.as_array(), state.clock.dt_clock_total_s))
}
pub fn retain(&mut self, keep: impl FnMut(&BroadcastRecord) -> bool) {
self.records.retain(keep);
}
fn toe_continuous_s(rec: &BroadcastRecord) -> f64 {
f64::from(rec.toe.week) * SECONDS_PER_WEEK + rec.toe.tow_s
}
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)
})
}
pub fn select_by_issue_at(
&self,
sat: GnssSatelliteId,
issue: BroadcastIssue,
nav_message: NavMessage,
t_j2000_s: f64,
) -> Option<&BroadcastRecord> {
if issue.message != nav_message {
return None;
}
let (t_continuous_s, _) = query_continuous_time(sat, t_j2000_s)?;
self.records
.iter()
.filter(|r| {
r.satellite_id == sat
&& r.message == nav_message
&& r.issue_of_data == issue
&& (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
}
}
}
fn validate_manual_record(record: &BroadcastRecord) -> CoreResult<()> {
validate_finite(record.toe.tow_s, "record.toe.tow_s")?;
validate_finite(record.toc.tow_s, "record.toc.tow_s")?;
validate_finite(record.sv_health, "record.sv_health")?;
validate_finite(record.sv_accuracy_m, "record.sv_accuracy_m")?;
if let Some(fit) = record.fit_interval_s {
validate_finite(fit, "record.fit_interval_s")?;
if fit <= 0.0 {
return Err(invalid_input("record.fit_interval_s", "not positive"));
}
}
validate_group_delays(record.group_delays)?;
satellite_state(
&record.elements,
&record.clock,
&record.constants(),
record.elements.toe_sow,
record.broadcast_clock_group_delay_s(),
is_beidou_geo(record.satellite_id),
)
.map(|_| ())
}
fn validate_group_delays(delays: BroadcastGroupDelays) -> CoreResult<()> {
for (field, value) in [
("group_delays.gps_tgd_s", delays.gps_tgd_s),
(
"group_delays.galileo_bgd_e5a_e1_s",
delays.galileo_bgd_e5a_e1_s,
),
(
"group_delays.galileo_bgd_e5b_e1_s",
delays.galileo_bgd_e5b_e1_s,
),
("group_delays.beidou_tgd1_s", delays.beidou_tgd1_s),
("group_delays.beidou_tgd2_s", delays.beidou_tgd2_s),
] {
if let Some(value) = value {
validate_finite(value, field)?;
}
}
Ok(())
}
fn validate_finite(value: f64, field: &'static str) -> CoreResult<()> {
if value.is_finite() {
Ok(())
} else {
Err(invalid_input(field, "not finite"))
}
}
fn invalid_input(field: &'static str, reason: &'static str) -> Error {
Error::InvalidInput(format!("{field} {reason}"))
}
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).ok()?;
let clock = glonass::clock_offset_s(rec.clk_bias, rec.gamma_n, tk);
return Some(([state[0], state[1], state[2]], clock));
}
let (t_continuous, is_geo) = query_continuous_time(sat, t_j2000_s)?;
let rec = self.select(sat, t_continuous)?;
let sow = t_continuous.rem_euclid(SECONDS_PER_WEEK);
let state = satellite_state_unchecked(
&rec.elements,
&rec.clock,
&rec.constants(),
sow,
rec.broadcast_clock_group_delay_s(),
is_geo,
);
let position = state.orbit.position().ok()?;
Some((position.as_array(), state.clock.dt_clock_total_s))
}
}
fn query_continuous_time(sat: GnssSatelliteId, t_j2000_s: f64) -> Option<(f64, bool)> {
if !matches!(
sat.system,
GnssSystem::Gps | GnssSystem::Galileo | GnssSystem::BeiDou
) {
return None;
}
let gpst_continuous = t_j2000_s + GPS_EPOCH_TO_J2000_S;
if sat.system == GnssSystem::BeiDou {
Some((
gpst_continuous - GPST_MINUS_BDT_S - BDS_EPOCH_MINUS_GPS_EPOCH_S,
is_beidou_geo(sat),
))
} else {
Some((gpst_continuous, false))
}
}