mod formatting;
pub mod orbits;
mod parsing;
pub mod flags;
use orbits::OrbitItem;
use flags::{
bds::{BdsHealth, BdsSatH1},
glonass::{GlonassHealth, GlonassHealth2},
gps::GpsQzssl1cHealth,
};
#[cfg(feature = "log")]
use log::error;
#[cfg(feature = "nav")]
#[cfg_attr(docsrs, doc(cfg(feature = "nav")))]
pub mod kepler;
#[cfg(feature = "nav")]
use crate::prelude::nav::Almanac;
#[cfg(feature = "ublox")]
mod ublox;
#[cfg(feature = "nav")]
use anise::{
astro::AzElRange,
errors::AlmanacResult,
prelude::{Frame, Orbit},
};
use std::collections::HashMap;
use crate::prelude::{Constellation, Duration, Epoch, TimeScale, SV};
#[derive(Default, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Ephemeris {
pub clock_bias: f64,
pub clock_drift: f64,
pub clock_drift_rate: f64,
pub orbits: HashMap<String, OrbitItem>,
}
impl Ephemeris {
pub fn sv_clock(&self) -> (f64, f64, f64) {
(self.clock_bias, self.clock_drift, self.clock_drift_rate)
}
pub fn get_orbit_f64(&self, field: &str) -> Option<f64> {
let value = self.orbits.get(field)?;
Some(value.as_f64())
}
pub(crate) fn set_orbit_f64(&mut self, field: &str, value: f64) {
self.orbits
.insert(field.to_string(), OrbitItem::from(value));
}
pub(crate) fn get_week(&self) -> Option<u32> {
self.orbits
.get("week")
.and_then(|value| Some(value.as_u32()))
}
pub fn tgd(&self) -> Option<Duration> {
let tgd_s = self.get_orbit_f64("tgd")?;
Some(Duration::from_seconds(tgd_s))
}
pub fn sv_healthy(&self) -> bool {
let health = self.orbits.get("health");
if health.is_none() {
return false;
}
let health = health.unwrap();
if let Some(flag) = health.as_gps_qzss_l1l2l5_health_flag() {
flag.healthy()
} else if let Some(flag) = health.as_gps_qzss_l1c_health_flag() {
!flag.intersects(GpsQzssl1cHealth::UNHEALTHY)
} else if let Some(flag) = health.as_glonass_health_flag() {
if let Some(flag2) = self
.orbits
.get("health2")
.and_then(|item| Some(item.as_glonass_health2_flag().unwrap()))
{
!flag.intersects(GlonassHealth::UNHEALTHY)
&& flag2.intersects(GlonassHealth2::HEALTHY_ALMANAC)
} else {
!flag.intersects(GlonassHealth::UNHEALTHY)
}
} else if let Some(flag) = health.as_geo_health_flag() {
false
} else if let Some(flag) = health.as_bds_sat_h1_flag() {
!flag.intersects(BdsSatH1::UNHEALTHY)
} else if let Some(flag) = health.as_bds_health_flag() {
flag == BdsHealth::Healthy
} else {
false
}
}
pub fn sv_in_testing(&self) -> bool {
let health = self.orbits.get("health");
if health.is_none() {
return false;
}
let health = health.unwrap();
if let Some(flag) = health.as_bds_health_flag() {
flag == BdsHealth::UnhealthyTesting
} else {
false
}
}
pub fn glonass_freq_channel(&self) -> Option<i8> {
if let Some(value) = self.orbits.get("channel") {
Some(value.as_i8())
} else {
None
}
}
pub fn toe(&self, sv: SV) -> Option<Epoch> {
let (week, seconds) = (self.get_week()?, self.get_orbit_f64("toe")?);
let nanos = (seconds * 1.0E9).round() as u64;
match sv.constellation {
Constellation::GPS | Constellation::QZSS | Constellation::Galileo => {
Some(Epoch::from_time_of_week(week, nanos, TimeScale::GPST))
},
Constellation::BeiDou => Some(Epoch::from_time_of_week(week, nanos, TimeScale::BDT)),
_ => {
#[cfg(feature = "log")]
error!("{} is not supported", sv.constellation);
None
},
}
}
pub(crate) fn a_dot(&self) -> Option<f64> {
self.get_orbit_f64("a_dot")
}
}
impl Ephemeris {
pub fn with_orbit(&self, key: &str, orbit: OrbitItem) -> Self {
let mut s = self.clone();
s.orbits.insert(key.to_string(), orbit);
s
}
pub fn with_week(&self, week: u32) -> Self {
self.with_orbit("week", OrbitItem::from(week))
}
pub fn clock_correction(
&self,
toc: Epoch,
t: Epoch,
sv: SV,
max_iter: usize,
) -> Option<Duration> {
let sv_ts = sv.constellation.timescale()?;
let t_sv = t.to_time_scale(sv_ts);
let toc_sv = toc.to_time_scale(sv_ts);
if t_sv < toc_sv {
#[cfg(feature = "log")]
error!("t < t_oc: bad op!");
None
} else {
let (a0, a1, a2) = (self.clock_bias, self.clock_drift, self.clock_drift_rate);
let mut dt = (t_sv - toc_sv).to_seconds();
for _ in 0..max_iter {
dt -= a0 + a1 * dt + a2 * dt.powi(2);
}
Some(Duration::from_seconds(a0 + a1 * dt + a2 * dt.powi(2)))
}
}
#[cfg(feature = "nav")]
#[cfg_attr(docsrs, doc(cfg(feature = "nav")))]
pub fn elevation_azimuth_range(
t: Epoch,
almanac: &Almanac,
fixed_body_frame: Frame,
sv_position_km: (f64, f64, f64),
rx_position_km: (f64, f64, f64),
) -> AlmanacResult<AzElRange> {
let (rx_x_km, rx_y_km, rx_z_km) = rx_position_km;
let (tx_x_km, tx_y_km, tx_z_km) = sv_position_km;
let rx_orbit = Orbit::from_position(rx_x_km, rx_y_km, rx_z_km, t, fixed_body_frame);
let tx_orbit = Orbit::from_position(tx_x_km, tx_y_km, tx_z_km, t, fixed_body_frame);
almanac.azimuth_elevation_range_sez(rx_orbit, tx_orbit, None, None)
}
pub fn is_valid(&self, sv: SV, t: Epoch) -> bool {
if let Some(toe) = self.toe(sv) {
if let Some(max_dtoe) = Self::validity_duration(sv.constellation) {
(t - toe).abs() < max_dtoe
} else {
#[cfg(feature = "log")]
error!("{} - validity period", sv.constellation);
false
}
} else {
#[cfg(feature = "log")]
error!("{} - ToE calculation", sv.constellation);
false
}
}
pub fn validity_duration(c: Constellation) -> Option<Duration> {
match c {
Constellation::GPS | Constellation::QZSS => Some(Duration::from_seconds(7200.0)),
Constellation::Galileo => Some(Duration::from_seconds(10800.0)),
Constellation::BeiDou => Some(Duration::from_seconds(21600.0)),
Constellation::IRNSS => Some(Duration::from_seconds(7200.0)),
Constellation::Glonass => Some(Duration::from_seconds(1800.0)),
c => {
if c.is_sbas() {
Some(Duration::from_days(1.0))
} else {
None
}
},
}
}
}