use crate::tle::Tle;
use std::f64::consts::PI;
const MIN_PER_DAY: f64 = 1440.0;
#[derive(Clone, Debug)]
pub struct OmmFile {
pub version: String,
pub creation_date: String,
pub originator: String,
pub object_name: String,
pub object_id: String,
pub center_name: String,
pub ref_frame: String,
pub time_system: String,
pub mean_element_theory: String,
pub epoch: String,
pub mean_motion_rev_day: f64,
pub eccentricity: f64,
pub inclination_deg: f64,
pub ra_of_asc_node_deg: f64,
pub arg_of_pericenter_deg: f64,
pub mean_anomaly_deg: f64,
pub bstar: f64,
pub norad_cat_id: u32,
pub ephemeris_type: u32,
pub classification: char,
}
impl OmmFile {
#[allow(clippy::too_many_arguments)]
pub fn from_tle(
tle: &Tle,
object_name: &str,
object_id: &str,
epoch: &str,
creation_date: &str,
originator: &str,
norad_cat_id: u32,
) -> Self {
Self {
version: "2.0".into(),
creation_date: creation_date.into(),
originator: originator.into(),
object_name: object_name.into(),
object_id: object_id.into(),
center_name: "EARTH".into(),
ref_frame: "TEME".into(),
time_system: "UTC".into(),
mean_element_theory: "SGP4".into(),
epoch: epoch.into(),
mean_motion_rev_day: tle.no_kozai_rad_min * MIN_PER_DAY / (2.0 * PI),
eccentricity: tle.ecco,
inclination_deg: tle.inclo_rad.to_degrees(),
ra_of_asc_node_deg: tle.nodeo_rad.to_degrees(),
arg_of_pericenter_deg: tle.argpo_rad.to_degrees(),
mean_anomaly_deg: tle.mo_rad.to_degrees(),
bstar: tle.bstar,
norad_cat_id,
ephemeris_type: 0,
classification: 'U',
}
}
pub fn from_tle_block(
text: &str,
originator: &str,
creation_date: &str,
) -> Result<Vec<OmmFile>, String> {
let mut out = Vec::new();
let mut name: Option<String> = None;
let mut pending_l1: Option<String> = None;
for raw in text.lines() {
let line = raw.trim();
if line.starts_with("1 ") && line.len() >= 63 {
pending_l1 = Some(line.to_string());
} else if line.starts_with("2 ") && line.len() >= 63 {
match pending_l1.take() {
Some(l1) => {
let tle = crate::tle::parse_tle(&l1, line)?;
let id = crate::tle::parse_tle_identity(&l1)?;
let object_name = name
.take()
.unwrap_or_else(|| format!("OBJECT {}", id.norad_cat_id));
out.push(OmmFile::from_tle(
&tle,
&object_name,
&id.intl_designator,
&id.epoch_iso,
creation_date,
originator,
id.norad_cat_id,
));
}
None => name = None,
}
} else if !line.is_empty() {
name = Some(line.to_string());
pending_l1 = None;
}
}
if out.is_empty() {
return Err("OMM export needs full (line 1 + line 2) TLEs; this block has none".into());
}
Ok(out)
}
pub fn to_omm_kvn(&self) -> String {
let mut s = String::new();
s.push_str(&format!("CCSDS_OMM_VERS = {}\n", self.version));
s.push_str(&format!("CREATION_DATE = {}\n", self.creation_date));
s.push_str(&format!("ORIGINATOR = {}\n", self.originator));
s.push_str("META_START\n");
s.push_str(&format!("OBJECT_NAME = {}\n", self.object_name));
s.push_str(&format!("OBJECT_ID = {}\n", self.object_id));
s.push_str(&format!("CENTER_NAME = {}\n", self.center_name));
s.push_str(&format!("REF_FRAME = {}\n", self.ref_frame));
s.push_str(&format!("TIME_SYSTEM = {}\n", self.time_system));
s.push_str(&format!(
"MEAN_ELEMENT_THEORY = {}\n",
self.mean_element_theory
));
s.push_str("META_STOP\n");
s.push_str(&format!("EPOCH = {}\n", self.epoch));
s.push_str(&format!("MEAN_MOTION = {:.8}\n", self.mean_motion_rev_day));
s.push_str(&format!("ECCENTRICITY = {:.7}\n", self.eccentricity));
s.push_str(&format!("INCLINATION = {:.4}\n", self.inclination_deg));
s.push_str(&format!(
"RA_OF_ASC_NODE = {:.4}\n",
self.ra_of_asc_node_deg
));
s.push_str(&format!(
"ARG_OF_PERICENTER = {:.4}\n",
self.arg_of_pericenter_deg
));
s.push_str(&format!("MEAN_ANOMALY = {:.4}\n", self.mean_anomaly_deg));
s.push_str(&format!("EPHEMERIS_TYPE = {}\n", self.ephemeris_type));
s.push_str(&format!("CLASSIFICATION_TYPE = {}\n", self.classification));
s.push_str(&format!("NORAD_CAT_ID = {}\n", self.norad_cat_id));
s.push_str(&format!("BSTAR = {:.8e}\n", self.bstar));
s
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_tle() -> Tle {
Tle {
epoch_days_1950: 26000.0,
bstar: 1.0e-4,
ecco: 0.000_6,
argpo_rad: 30.0_f64.to_radians(),
inclo_rad: 51.6_f64.to_radians(),
mo_rad: 120.0_f64.to_radians(),
no_kozai_rad_min: 15.5 * 2.0 * PI / MIN_PER_DAY,
nodeo_rad: 247.0_f64.to_radians(),
}
}
#[test]
fn from_tle_converts_to_omm_units() {
let omm = OmmFile::from_tle(
&sample_tle(),
"ISS",
"1998-067A",
"2024-01-01T00:00:00",
"2024-01-01T01:00:00",
"KSHANA",
25544,
);
assert!(
(omm.mean_motion_rev_day - 15.5).abs() < 1e-9,
"n = {}",
omm.mean_motion_rev_day
);
assert!((omm.inclination_deg - 51.6).abs() < 1e-9);
assert!((omm.ra_of_asc_node_deg - 247.0).abs() < 1e-9);
assert!((omm.mean_anomaly_deg - 120.0).abs() < 1e-9);
assert!((omm.eccentricity - 0.000_6).abs() < 1e-12);
}
#[test]
fn kvn_has_the_required_omm_fields() {
let omm = OmmFile::from_tle(
&sample_tle(),
"ISS",
"1998-067A",
"2024-01-01T00:00:00",
"2024-01-01T01:00:00",
"KSHANA",
25544,
);
let kvn = omm.to_omm_kvn();
assert!(kvn.starts_with("CCSDS_OMM_VERS = 2.0\n"));
for key in [
"MEAN_ELEMENT_THEORY = SGP4",
"MEAN_MOTION = 15.5",
"ECCENTRICITY = 0.0006000",
"INCLINATION = 51.6000",
"RA_OF_ASC_NODE = 247.0000",
"ARG_OF_PERICENTER = 30.0000",
"MEAN_ANOMALY = 120.0000",
"NORAD_CAT_ID = 25544",
"META_START",
"META_STOP",
] {
assert!(kvn.contains(key), "OMM KVN missing {key}:\n{kvn}");
}
}
const BLOCK: &str = "ISS (ZARYA)\n\
1 25544U 98067A 24001.00000000 .00000000 00000-0 00000-0 0 9990\n\
2 25544 51.6400 247.4627 0006703 130.5360 325.0288 15.72125391563537\n\
VANGUARD 1\n\
1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\n\
2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667";
#[test]
fn from_tle_block_builds_one_omm_per_satellite_with_real_ids() {
let files = OmmFile::from_tle_block(BLOCK, "KSHANA", "2024-01-01T00:00:00")
.expect("valid TLE block");
assert_eq!(files.len(), 2);
assert_eq!(files[0].object_name, "ISS (ZARYA)");
assert_eq!(files[0].object_id, "1998-067A");
assert_eq!(files[0].norad_cat_id, 25544);
assert_eq!(files[0].epoch, "2024-001T00:00:00.000000");
assert!((files[0].mean_motion_rev_day - 15.72125391).abs() < 1e-6);
assert_eq!(files[1].object_name, "VANGUARD 1");
assert_eq!(files[1].object_id, "1958-002B");
assert_eq!(files[1].norad_cat_id, 5);
}
#[test]
fn from_tle_block_names_unnamed_objects_by_catalog_number() {
let bare = "1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\n\
2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667";
let files = OmmFile::from_tle_block(bare, "KSHANA", "2024-01-01T00:00:00").unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].object_name, "OBJECT 5");
}
#[test]
fn from_tle_block_errors_when_there_are_no_full_element_sets() {
let line2_only = "2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667";
assert!(OmmFile::from_tle_block(line2_only, "KSHANA", "2024-01-01T00:00:00").is_err());
}
}