#[cfg(feature = "serde")]
pub mod json;
pub mod kvn;
pub mod xml;
use chrono::{Datelike, TimeZone, Timelike, Utc};
use qtty::angular::Degrees;
use qtty::angular::Turn;
use qtty::angular_rate::AngularRate;
use qtty::time::Day;
use tempoch::{Time, UTC};
use crate::formats::tle::TleError;
use crate::formats::tle::{Classification, InternationalDesignator, SatelliteNumber, TLE};
#[derive(Clone, Debug)]
pub struct Omm {
pub object_name: String,
pub object_id: String,
pub epoch: Time<UTC>,
pub mean_motion: AngularRate<Turn, Day>,
pub eccentricity: f64,
pub inclination: Degrees,
pub ra_of_asc_node: Degrees,
pub arg_of_pericenter: Degrees,
pub mean_anomaly: Degrees,
pub ephemeris_type: u8,
pub classification: Classification,
pub norad_id: SatelliteNumber,
pub element_set_no: u16,
pub rev_at_epoch: u32,
pub bstar: f64,
pub mean_motion_dot: f64,
pub mean_motion_ddot: f64,
}
impl Omm {
pub fn from_tle(tle: &TLE) -> Self {
let object_name = tle
.name
.clone()
.unwrap_or_else(|| format!("NORAD {}", tle.norad_id.0));
let object_id = expand_intl_designator(&tle.international_designator.0);
Self {
object_name,
object_id,
epoch: tle.epoch,
mean_motion: tle.mean_motion,
eccentricity: tle.eccentricity,
inclination: tle.inclination,
ra_of_asc_node: tle.raan,
arg_of_pericenter: tle.argument_of_perigee,
mean_anomaly: tle.mean_anomaly,
ephemeris_type: 0,
classification: tle.classification,
norad_id: tle.norad_id,
element_set_no: tle.element_set_number,
rev_at_epoch: tle.revolution_number_at_epoch,
bstar: tle.bstar,
mean_motion_dot: tle.mean_motion_dot,
mean_motion_ddot: tle.mean_motion_ddot,
}
}
pub fn to_tle(&self) -> TLE {
TLE {
name: Some(self.object_name.clone()),
norad_id: self.norad_id,
classification: self.classification,
international_designator: InternationalDesignator(contract_intl_designator(
&self.object_id,
)),
epoch: self.epoch,
mean_motion_dot: self.mean_motion_dot,
mean_motion_ddot: self.mean_motion_ddot,
bstar: self.bstar,
element_set_number: self.element_set_no,
revolution_number_at_epoch: self.rev_at_epoch,
inclination: self.inclination,
raan: self.ra_of_asc_node,
eccentricity: self.eccentricity,
argument_of_perigee: self.arg_of_pericenter,
mean_anomaly: self.mean_anomaly,
mean_motion: self.mean_motion,
}
}
}
pub(crate) fn expand_intl_designator(field: &str) -> String {
let s = field.trim();
if s.len() < 5 || !s.is_ascii() {
return s.to_string();
}
let bytes = s.as_bytes();
if !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
return s.to_string();
}
let yy: i32 = s[..2].parse().unwrap_or(0);
let yyyy = if yy >= 57 { 1900 + yy } else { 2000 + yy };
let launch = &s[2..5];
let piece = &s[5..];
if piece.is_empty() {
format!("{yyyy:04}-{launch}")
} else {
format!("{yyyy:04}-{launch}{piece}")
}
}
pub(crate) fn contract_intl_designator(s: &str) -> String {
let s = s.trim();
if let Some(dash) = s.find('-') {
let (yyyy, rest) = s.split_at(dash);
let rest = &rest[1..];
if yyyy.len() == 4 && yyyy.bytes().all(|b| b.is_ascii_digit()) {
let yy = &yyyy[2..];
return format!("{yy}{rest}");
}
}
s.to_string()
}
pub(crate) fn format_epoch(t: Time<UTC>) -> Result<String, TleError> {
let dt = t
.try_to_chrono()
.map_err(|e| TleError::EpochConversion(format!("{e:?}")))?;
let micros = dt.timestamp_subsec_micros();
Ok(format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
micros
))
}
pub(crate) fn parse_epoch(raw: &str) -> Result<Time<UTC>, TleError> {
let s = raw.trim();
let trimmed = s.trim_end_matches('Z');
let trimmed = trimmed.split('+').next().unwrap_or(trimmed);
let dt = chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S%.f")
.or_else(|_| chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S"))
.map_err(|e| TleError::OmmInvalidEpoch {
raw: raw.to_string(),
reason: match e.kind() {
chrono::format::ParseErrorKind::Invalid => "invalid component",
chrono::format::ParseErrorKind::OutOfRange => "out of range",
_ => "malformed ISO-8601",
},
})?;
let utc_dt = Utc.from_utc_datetime(&dt);
Time::<UTC>::try_from_chrono(utc_dt).map_err(|e| TleError::EpochConversion(format!("{e:?}")))
}