#[allow(
dead_code,
unused_variables,
unused_assignments,
unused_mut,
non_snake_case,
non_camel_case_types,
clippy::approx_constant,
clippy::excessive_precision,
clippy::too_many_arguments,
clippy::needless_return,
clippy::assign_op_pattern,
clippy::manual_range_contains,
clippy::collapsible_if,
clippy::collapsible_else_if,
clippy::float_cmp,
clippy::needless_late_init,
clippy::field_reassign_with_default
)]
mod vallado;
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum Error {
#[error("invalid TLE: {0}")]
InvalidTle(String),
#[error("SGP4 error code {code}")]
Sgp4 { code: i32 },
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MinutesSinceEpoch(pub f64);
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Prediction {
pub position: [f64; 3],
pub velocity: [f64; 3],
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct JulianDate(pub f64, pub f64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OpsMode {
#[default]
Improved,
Afspc,
}
impl OpsMode {
fn as_char(self) -> char {
match self {
OpsMode::Improved => 'i',
OpsMode::Afspc => 'a',
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ElementSet {
pub epoch_year_two_digit: i32,
pub epoch_days: f64,
pub bstar: f64,
pub mean_motion_dot: f64,
pub mean_motion_double_dot: f64,
pub eccentricity: f64,
pub argument_of_perigee_deg: f64,
pub inclination_deg: f64,
pub mean_anomaly_deg: f64,
pub mean_motion_rev_per_day: f64,
pub right_ascension_deg: f64,
pub catalog_number: u32,
}
#[derive(Clone)]
pub struct Satellite {
line1: String,
line2: String,
satrec: Box<vallado::ElsetRec>,
}
impl std::fmt::Debug for Satellite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Satellite")
.field("line1", &self.line1)
.field("line2", &self.line2)
.finish_non_exhaustive()
}
}
impl Satellite {
pub fn from_tle(line1: &str, line2: &str) -> Result<Self, Error> {
Self::from_tle_with_opsmode(line1, line2, OpsMode::Improved)
}
pub fn from_tle_with_opsmode(
line1: &str,
line2: &str,
opsmode: OpsMode,
) -> Result<Self, Error> {
let l1 = line1.trim();
let l2 = line2.trim();
if l1.len() < 69 || !l1.starts_with('1') {
return Err(Error::InvalidTle(
"line 1 too short or doesn't start with '1'".into(),
));
}
if l2.len() < 69 || !l2.starts_with('2') {
return Err(Error::InvalidTle(
"line 2 too short or doesn't start with '2'".into(),
));
}
let satrec = init_satrec_from_tle(l1, l2, opsmode)?;
Ok(Satellite {
line1: l1.to_string(),
line2: l2.to_string(),
satrec: Box::new(satrec),
})
}
pub fn from_elements(elements: &ElementSet) -> Result<Self, Error> {
Self::from_elements_with_opsmode(elements, OpsMode::Improved)
}
pub fn from_elements_with_opsmode(
elements: &ElementSet,
opsmode: OpsMode,
) -> Result<Self, Error> {
let satrec = init_satrec_from_elements(elements, opsmode)?;
Ok(Satellite {
line1: String::new(),
line2: String::new(),
satrec: Box::new(satrec),
})
}
pub fn propagate(&self, t: MinutesSinceEpoch) -> Result<Prediction, Error> {
let mut working = (*self.satrec).clone();
let mut r = [0.0_f64; 3];
let mut v = [0.0_f64; 3];
let ok = vallado::sgp4(&mut working, t.0, &mut r, &mut v);
if !ok || working.error != 0 {
return Err(Error::Sgp4 {
code: working.error,
});
}
Ok(Prediction {
position: r,
velocity: v,
})
}
pub fn propagate_jd(&self, jd: JulianDate) -> Result<Prediction, Error> {
let tsince = (jd.0 - self.satrec.jdsatepoch) * 1440.0
+ (jd.1 - self.satrec.jdsatepochF) * 1440.0;
self.propagate(MinutesSinceEpoch(tsince))
}
pub fn line1(&self) -> &str {
&self.line1
}
pub fn line2(&self) -> &str {
&self.line2
}
pub fn epoch_jd(&self) -> JulianDate {
JulianDate(self.satrec.jdsatepoch, self.satrec.jdsatepochF)
}
}
pub fn propagate_elements(
elements: &ElementSet,
t: MinutesSinceEpoch,
) -> Result<Prediction, Error> {
propagate_elements_with_opsmode(elements, t, OpsMode::Improved)
}
pub fn propagate_elements_with_opsmode(
elements: &ElementSet,
t: MinutesSinceEpoch,
opsmode: OpsMode,
) -> Result<Prediction, Error> {
let mut satrec = init_satrec_from_elements(elements, opsmode)?;
let mut r = [0.0_f64; 3];
let mut v = [0.0_f64; 3];
let ok = vallado::sgp4(&mut satrec, t.0, &mut r, &mut v);
if !ok || satrec.error != 0 {
return Err(Error::Sgp4 {
code: satrec.error,
});
}
Ok(Prediction {
position: r,
velocity: v,
})
}
#[cfg(feature = "serde")]
impl serde::Serialize for Satellite {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = s.serialize_struct("Satellite", 2)?;
st.serialize_field("line1", &self.line1)?;
st.serialize_field("line2", &self.line2)?;
st.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Satellite {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct Wire {
line1: String,
line2: String,
}
let w = Wire::deserialize(d)?;
Satellite::from_tle(&w.line1, &w.line2).map_err(serde::de::Error::custom)
}
}
fn init_satrec_from_elements(
elements: &ElementSet,
opsmode: OpsMode,
) -> Result<vallado::ElsetRec, Error> {
let deg2rad = std::f64::consts::PI / 180.0;
let xpdotp = 1440.0 / (2.0 * std::f64::consts::PI);
let inclo = elements.inclination_deg * deg2rad;
let nodeo = elements.right_ascension_deg * deg2rad;
let argpo = elements.argument_of_perigee_deg * deg2rad;
let mo = elements.mean_anomaly_deg * deg2rad;
let no_kozai = elements.mean_motion_rev_per_day / xpdotp;
let ndot = elements.mean_motion_dot / (xpdotp * 1440.0);
let nddot = elements.mean_motion_double_dot / (xpdotp * 1440.0 * 1440.0);
let year_full = if elements.epoch_year_two_digit < 57 {
elements.epoch_year_two_digit + 2000
} else {
elements.epoch_year_two_digit + 1900
};
let (mon, day, hr, minute, sec) =
vallado::days2mdhms_SGP4(year_full, elements.epoch_days);
let (jd, jdfrac_raw) = vallado::jday_SGP4(year_full, mon, day, hr, minute, sec);
let jdfrac = (jdfrac_raw * 100_000_000.0).round() / 100_000_000.0;
let epoch_sgp4 = jd + jdfrac - 2433281.5;
let satnum_str = format!("{:>5}", elements.catalog_number);
let mut satrec = vallado::ElsetRec {
epochyr: elements.epoch_year_two_digit,
epochdays: elements.epoch_days,
jdsatepoch: jd,
jdsatepochF: jdfrac,
..vallado::ElsetRec::default()
};
vallado::sgp4init(
vallado::GravConstType::Wgs72,
opsmode.as_char(),
&satnum_str,
epoch_sgp4,
elements.bstar,
ndot,
nddot,
elements.eccentricity,
argpo,
inclo,
mo,
no_kozai,
nodeo,
&mut satrec,
);
satrec.jdsatepoch = jd;
satrec.jdsatepochF = jdfrac;
Ok(satrec)
}
fn init_satrec_from_tle(
line1: &str,
line2: &str,
opsmode: OpsMode,
) -> Result<vallado::ElsetRec, Error> {
let l1 = line1.trim_end();
let l2 = line2.trim_end();
if l1.len() < 64 || l2.len() < 68 {
return Err(Error::InvalidTle("TLE lines too short".into()));
}
let deg2rad = std::f64::consts::PI / 180.0;
let xpdotp = 1440.0 / (2.0 * std::f64::consts::PI);
let two_digit_year: i32 = l1[18..20]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad epochyr".into()))?;
let epochdays: f64 = l1[20..32]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad epochdays".into()))?;
let ndot_raw: f64 = l1[33..43]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad ndot".into()))?;
let nddot_str = format!("{}.{}", &l1[44..45], &l1[45..50]);
let nddot_mantissa: f64 = nddot_str.trim().parse().unwrap_or(0.0);
let nexp: i32 = l1[50..52].trim().parse().unwrap_or(0);
let bstar_str = format!("{}.{}", &l1[53..54], &l1[54..59]);
let bstar_mantissa: f64 = bstar_str.trim().parse().unwrap_or(0.0);
let ibexp: i32 = l1[59..61].trim().parse().unwrap_or(0);
let inclo_deg: f64 = l2[8..16]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad inclo".into()))?;
let nodeo_deg: f64 = l2[17..25]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad nodeo".into()))?;
let ecco_str = format!("0.{}", l2[26..33].replace(' ', "0"));
let ecco: f64 = ecco_str
.parse()
.map_err(|_| Error::InvalidTle("bad ecco".into()))?;
let argpo_deg: f64 = l2[34..42]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad argpo".into()))?;
let mo_deg: f64 = l2[43..51]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad mo".into()))?;
let no_kozai_revday: f64 = l2[52..63]
.trim()
.parse()
.map_err(|_| Error::InvalidTle("bad no_kozai".into()))?;
let no_kozai = no_kozai_revday / xpdotp;
let nddot = nddot_mantissa * 10.0_f64.powi(nexp) / (xpdotp * 1440.0 * 1440.0);
let bstar = bstar_mantissa * 10.0_f64.powi(ibexp);
let ndot = ndot_raw / (xpdotp * 1440.0);
let inclo = inclo_deg * deg2rad;
let nodeo = nodeo_deg * deg2rad;
let argpo = argpo_deg * deg2rad;
let mo = mo_deg * deg2rad;
let year_full = if two_digit_year < 57 {
two_digit_year + 2000
} else {
two_digit_year + 1900
};
let (mon, day, hr, minute, sec) = vallado::days2mdhms_SGP4(year_full, epochdays);
let (jd, jdfrac_raw) = vallado::jday_SGP4(year_full, mon, day, hr, minute, sec);
let jdfrac = (jdfrac_raw * 100_000_000.0).round() / 100_000_000.0;
let epoch_sgp4 = jd + jdfrac - 2433281.5;
let satnum = l1[2..7].trim();
let mut satrec = vallado::ElsetRec {
epochyr: two_digit_year,
epochdays,
jdsatepoch: jd,
jdsatepochF: jdfrac,
..vallado::ElsetRec::default()
};
vallado::sgp4init(
vallado::GravConstType::Wgs72,
opsmode.as_char(),
satnum,
epoch_sgp4,
bstar,
ndot,
nddot,
ecco,
argpo,
inclo,
mo,
no_kozai,
nodeo,
&mut satrec,
);
satrec.jdsatepoch = jd;
satrec.jdsatepochF = jdfrac;
Ok(satrec)
}