use std::path::Path;
use crate::ccsds::common::{
CCSDSCovariance, CCSDSFormat, CCSDSRefFrame, CCSDSSpacecraftParameters, CCSDSTimeSystem,
CCSDSUserDefined, ODMHeader,
};
use crate::time::Epoch;
use crate::utils::errors::BraheError;
#[derive(Debug, Clone)]
pub struct OMM {
pub header: ODMHeader,
pub metadata: OMMMetadata,
pub mean_elements: OMMeanElements,
pub tle_parameters: Option<OMMTleParameters>,
pub spacecraft_parameters: Option<CCSDSSpacecraftParameters>,
pub covariance: Option<CCSDSCovariance>,
pub user_defined: Option<CCSDSUserDefined>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OMMMetadata {
pub object_name: String,
pub object_id: String,
pub center_name: String,
pub ref_frame: CCSDSRefFrame,
pub ref_frame_epoch: Option<Epoch>,
pub time_system: CCSDSTimeSystem,
pub mean_element_theory: String,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OMMeanElements {
pub epoch: Epoch,
pub mean_motion: Option<f64>,
pub semi_major_axis: Option<f64>,
pub eccentricity: f64,
pub inclination: f64,
pub ra_of_asc_node: f64,
pub arg_of_pericenter: f64,
pub mean_anomaly: f64,
pub gm: Option<f64>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OMMTleParameters {
pub ephemeris_type: Option<u32>,
pub classification_type: Option<char>,
pub norad_cat_id: Option<u32>,
pub element_set_no: Option<u32>,
pub rev_at_epoch: Option<u32>,
pub bstar: Option<f64>,
pub bterm: Option<f64>,
pub mean_motion_dot: Option<f64>,
pub mean_motion_ddot: Option<f64>,
pub agom: Option<f64>,
pub comments: Vec<String>,
}
impl OMMMetadata {
pub fn new(
object_name: String,
object_id: String,
center_name: String,
ref_frame: CCSDSRefFrame,
time_system: CCSDSTimeSystem,
mean_element_theory: String,
) -> Self {
Self {
object_name,
object_id,
center_name,
ref_frame,
ref_frame_epoch: None,
time_system,
mean_element_theory,
comments: Vec::new(),
}
}
}
impl OMMeanElements {
pub fn new(
epoch: Epoch,
eccentricity: f64,
inclination: f64,
ra_of_asc_node: f64,
arg_of_pericenter: f64,
mean_anomaly: f64,
) -> Self {
Self {
epoch,
mean_motion: None,
semi_major_axis: None,
eccentricity,
inclination,
ra_of_asc_node,
arg_of_pericenter,
mean_anomaly,
gm: None,
comments: Vec::new(),
}
}
pub fn with_mean_motion(mut self, mean_motion: f64) -> Self {
self.mean_motion = Some(mean_motion);
self
}
pub fn with_gm(mut self, gm: f64) -> Self {
self.gm = Some(gm);
self
}
}
impl OMM {
pub fn new(originator: String, metadata: OMMMetadata, mean_elements: OMMeanElements) -> Self {
Self {
header: ODMHeader {
format_version: 3.0,
classification: None,
creation_date: Epoch::now(),
originator,
message_id: None,
comments: Vec::new(),
},
metadata,
mean_elements,
tle_parameters: None,
spacecraft_parameters: None,
covariance: None,
user_defined: None,
comments: Vec::new(),
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(content: &str) -> Result<Self, BraheError> {
let format = crate::ccsds::common::detect_format(content);
match format {
CCSDSFormat::KVN => crate::ccsds::kvn::parse_omm(content),
CCSDSFormat::XML => crate::ccsds::xml::parse_omm_xml(content),
CCSDSFormat::JSON => crate::ccsds::json::parse_omm_json(content),
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, BraheError> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| BraheError::IoError(format!("Failed to read OMM file: {}", e)))?;
Self::from_str(&content)
}
pub fn to_string(&self, format: CCSDSFormat) -> Result<String, BraheError> {
match format {
CCSDSFormat::KVN => crate::ccsds::kvn::write_omm(self),
CCSDSFormat::XML => crate::ccsds::xml::write_omm_xml(self),
CCSDSFormat::JSON => crate::ccsds::json::write_omm_json(
self,
crate::ccsds::common::CCSDSJsonKeyCase::Lower,
),
}
}
pub fn to_json_string(
&self,
key_case: crate::ccsds::common::CCSDSJsonKeyCase,
) -> Result<String, BraheError> {
crate::ccsds::json::write_omm_json(self, key_case)
}
pub fn to_file<P: AsRef<Path>>(&self, path: P, format: CCSDSFormat) -> Result<(), BraheError> {
let content = self.to_string(format)?;
std::fs::write(path.as_ref(), content)
.map_err(|e| BraheError::IoError(format!("Failed to write OMM file: {}", e)))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::ccsds::common::CCSDSJsonKeyCase;
use crate::time::TimeSystem;
#[test]
fn test_omm_json_round_trip_via_dispatch() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
let json_str = omm.to_string(CCSDSFormat::JSON).unwrap();
assert!(json_str.contains("object_name") || json_str.contains("OBJECT_NAME"));
let omm2 = OMM::from_str(&json_str).unwrap();
assert_eq!(omm2.metadata.object_name, omm.metadata.object_name);
assert_eq!(omm2.metadata.object_id, omm.metadata.object_id);
assert!((omm2.mean_elements.eccentricity - omm.mean_elements.eccentricity).abs() < 1e-10);
assert!((omm2.mean_elements.inclination - omm.mean_elements.inclination).abs() < 1e-10);
}
#[test]
fn test_omm_from_file_nonexistent() {
let result = OMM::from_file("nonexistent_file.txt");
assert!(result.is_err());
}
#[test]
fn test_omm_metadata_new() {
let meta = OMMMetadata::new(
"ISS".to_string(),
"1998-067A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::TEME,
CCSDSTimeSystem::UTC,
"SGP4".to_string(),
);
assert_eq!(meta.object_name, "ISS");
assert_eq!(meta.object_id, "1998-067A");
assert_eq!(meta.center_name, "EARTH");
assert!(matches!(meta.ref_frame, CCSDSRefFrame::TEME));
assert!(matches!(meta.time_system, CCSDSTimeSystem::UTC));
assert_eq!(meta.mean_element_theory, "SGP4");
assert!(meta.ref_frame_epoch.is_none());
assert!(meta.comments.is_empty());
}
#[test]
fn test_omm_mean_elements_new() {
let epoch = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let elems = OMMeanElements::new(epoch, 0.001, 51.6, 120.0, 90.0, 45.0);
assert!((elems.eccentricity - 0.001).abs() < 1e-15);
assert!((elems.inclination - 51.6).abs() < 1e-15);
assert!((elems.ra_of_asc_node - 120.0).abs() < 1e-15);
assert!((elems.arg_of_pericenter - 90.0).abs() < 1e-15);
assert!((elems.mean_anomaly - 45.0).abs() < 1e-15);
assert!(elems.mean_motion.is_none());
assert!(elems.semi_major_axis.is_none());
assert!(elems.gm.is_none());
assert!(elems.comments.is_empty());
}
#[test]
fn test_omm_mean_elements_with_mean_motion() {
let epoch = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let elems =
OMMeanElements::new(epoch, 0.001, 51.6, 120.0, 90.0, 45.0).with_mean_motion(15.5);
assert_eq!(elems.mean_motion, Some(15.5));
}
#[test]
fn test_omm_mean_elements_with_gm() {
let epoch = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let elems = OMMeanElements::new(epoch, 0.001, 51.6, 120.0, 90.0, 45.0).with_gm(398600.8e9);
assert_eq!(elems.gm, Some(398600.8e9));
}
#[test]
fn test_omm_new() {
let meta = OMMMetadata::new(
"SAT1".to_string(),
"2024-001A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::TEME,
CCSDSTimeSystem::UTC,
"SGP/SGP4".to_string(),
);
let epoch = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let elems = OMMeanElements::new(epoch, 0.001, 51.6, 120.0, 90.0, 45.0);
let omm = OMM::new("TEST_ORG".to_string(), meta, elems);
assert_eq!(omm.header.originator, "TEST_ORG");
assert!((omm.header.format_version - 3.0).abs() < 1e-15);
assert!(omm.header.classification.is_none());
assert!(omm.header.message_id.is_none());
assert_eq!(omm.metadata.object_name, "SAT1");
assert_eq!(omm.metadata.object_id, "2024-001A");
assert!((omm.mean_elements.eccentricity - 0.001).abs() < 1e-15);
assert!(omm.tle_parameters.is_none());
assert!(omm.spacecraft_parameters.is_none());
assert!(omm.covariance.is_none());
assert!(omm.user_defined.is_none());
assert!(omm.comments.is_empty());
}
#[test]
fn test_omm_kvn_parse_example1() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
assert_eq!(omm.metadata.object_name, "GOES 9");
assert_eq!(omm.metadata.object_id, "1995-025A");
assert_eq!(omm.metadata.center_name, "EARTH");
assert_eq!(omm.metadata.mean_element_theory, "SGP/SGP4");
assert!((omm.mean_elements.eccentricity - 0.0005013).abs() < 1e-10);
assert!((omm.mean_elements.inclination - 3.0539).abs() < 1e-10);
assert!((omm.mean_elements.ra_of_asc_node - 81.7939).abs() < 1e-10);
assert!((omm.mean_elements.arg_of_pericenter - 249.2363).abs() < 1e-10);
assert!((omm.mean_elements.mean_anomaly - 150.1602).abs() < 1e-10);
assert!((omm.mean_elements.mean_motion.unwrap() - 1.00273272).abs() < 1e-10);
let tle = omm.tle_parameters.as_ref().unwrap();
assert_eq!(tle.norad_cat_id, Some(23581));
assert_eq!(tle.element_set_no, Some(925));
assert_eq!(tle.rev_at_epoch, Some(4316));
assert!((tle.bstar.unwrap() - 0.0001).abs() < 1e-10);
}
#[test]
fn test_omm_kvn_parse_example2_with_covariance() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
assert_eq!(omm.metadata.object_name, "GOES 9");
assert_eq!(omm.metadata.object_id, "1995-025A");
assert!(omm.covariance.is_some());
let cov = omm.covariance.as_ref().unwrap();
assert!((cov.matrix[(0, 0)] - 3.331349476038534e-04 * 1e6).abs() < 1e-2);
}
#[test]
fn test_omm_kvn_round_trip() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
let kvn_str = omm.to_string(CCSDSFormat::KVN).unwrap();
let omm2 = OMM::from_str(&kvn_str).unwrap();
assert_eq!(omm2.metadata.object_name, omm.metadata.object_name);
assert_eq!(omm2.metadata.object_id, omm.metadata.object_id);
assert!((omm2.mean_elements.eccentricity - omm.mean_elements.eccentricity).abs() < 1e-10);
assert!((omm2.mean_elements.inclination - omm.mean_elements.inclination).abs() < 1e-10);
assert!(
(omm2.mean_elements.mean_motion.unwrap() - omm.mean_elements.mean_motion.unwrap())
.abs()
< 1e-10
);
assert!((omm2.mean_elements.gm.unwrap() - omm.mean_elements.gm.unwrap()).abs() < 1e3);
let tle1 = omm.tle_parameters.as_ref().unwrap();
let tle2 = omm2.tle_parameters.as_ref().unwrap();
assert_eq!(tle2.norad_cat_id, tle1.norad_cat_id);
assert!((tle2.bstar.unwrap() - tle1.bstar.unwrap()).abs() < 1e-10);
}
#[test]
fn test_omm_kvn_round_trip_with_covariance() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
let kvn_str = omm.to_string(CCSDSFormat::KVN).unwrap();
let omm2 = OMM::from_str(&kvn_str).unwrap();
assert!(omm2.covariance.is_some());
let cov1 = omm.covariance.as_ref().unwrap();
let cov2 = omm2.covariance.as_ref().unwrap();
assert!((cov2.matrix[(0, 0)] - cov1.matrix[(0, 0)]).abs() < 1.0);
}
#[test]
fn test_omm_xml_round_trip() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample2.xml").unwrap();
let xml_str = omm.to_string(CCSDSFormat::XML).unwrap();
let omm2 = OMM::from_str(&xml_str).unwrap();
assert_eq!(omm2.metadata.object_name, omm.metadata.object_name);
assert_eq!(omm2.metadata.object_id, omm.metadata.object_id);
assert!((omm2.mean_elements.eccentricity - omm.mean_elements.eccentricity).abs() < 1e-10);
assert!(
(omm2.mean_elements.mean_motion.unwrap() - omm.mean_elements.mean_motion.unwrap())
.abs()
< 1e-10
);
assert!(omm2.covariance.is_some());
let cov1 = omm.covariance.as_ref().unwrap();
let cov2 = omm2.covariance.as_ref().unwrap();
assert!((cov2.matrix[(0, 0)] - cov1.matrix[(0, 0)]).abs() < 1.0);
}
#[test]
fn test_omm_xml_parse_example4() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample4.xml").unwrap();
assert_eq!(omm.metadata.object_name, "STARLETTE");
assert_eq!(omm.metadata.object_id, "1975-010A");
assert_eq!(omm.metadata.mean_element_theory, "SGP4");
assert!((omm.mean_elements.mean_motion.unwrap() - 13.82309053).abs() < 1e-8);
let tle = omm.tle_parameters.as_ref().unwrap();
assert_eq!(tle.norad_cat_id, Some(7646));
}
#[test]
fn test_omm_to_file_kvn() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
let dir = std::env::temp_dir();
let path = dir.join("brahe_test_omm.txt");
omm.to_file(&path, CCSDSFormat::KVN).unwrap();
let omm2 = OMM::from_file(&path).unwrap();
assert_eq!(omm2.metadata.object_name, omm.metadata.object_name);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_omm_kvn_parse_with_gm() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
assert!(omm.mean_elements.gm.is_some());
let gm = omm.mean_elements.gm.unwrap();
assert!((gm - 398600.8e9).abs() < 1e6);
}
#[test]
fn test_omm_to_json_string_upper_key_case() {
let omm = OMM::from_file("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
let json_str = omm.to_json_string(CCSDSJsonKeyCase::Upper).unwrap();
assert!(json_str.contains("OBJECT_NAME"));
assert!(json_str.contains("ECCENTRICITY"));
}
fn assert_omm_fields_match(omm1: &OMM, omm2: &OMM) {
assert_eq!(omm1.header.format_version, omm2.header.format_version);
assert_eq!(omm1.header.originator, omm2.header.originator);
assert_eq!(omm1.header.classification, omm2.header.classification);
assert_eq!(omm1.header.message_id, omm2.header.message_id);
assert_eq!(omm1.metadata.object_name, omm2.metadata.object_name);
assert_eq!(omm1.metadata.object_id, omm2.metadata.object_id);
assert_eq!(omm1.metadata.center_name, omm2.metadata.center_name);
assert_eq!(omm1.metadata.ref_frame, omm2.metadata.ref_frame);
assert_eq!(omm1.metadata.time_system, omm2.metadata.time_system);
assert_eq!(
omm1.metadata.mean_element_theory,
omm2.metadata.mean_element_theory
);
assert!((omm1.mean_elements.eccentricity - omm2.mean_elements.eccentricity).abs() < 1e-10);
assert!((omm1.mean_elements.inclination - omm2.mean_elements.inclination).abs() < 1e-6);
assert!(
(omm1.mean_elements.ra_of_asc_node - omm2.mean_elements.ra_of_asc_node).abs() < 1e-6
);
assert!(
(omm1.mean_elements.arg_of_pericenter - omm2.mean_elements.arg_of_pericenter).abs()
< 1e-6
);
assert!((omm1.mean_elements.mean_anomaly - omm2.mean_elements.mean_anomaly).abs() < 1e-6);
assert_eq!(
omm1.mean_elements.mean_motion.is_some(),
omm2.mean_elements.mean_motion.is_some()
);
if let (Some(mm1), Some(mm2)) = (
omm1.mean_elements.mean_motion,
omm2.mean_elements.mean_motion,
) {
assert!((mm1 - mm2).abs() < 1e-10);
}
assert_eq!(
omm1.mean_elements.semi_major_axis.is_some(),
omm2.mean_elements.semi_major_axis.is_some()
);
assert_eq!(
omm1.mean_elements.gm.is_some(),
omm2.mean_elements.gm.is_some()
);
if let (Some(gm1), Some(gm2)) = (omm1.mean_elements.gm, omm2.mean_elements.gm) {
assert!((gm1 - gm2).abs() < 1e3);
}
assert_eq!(omm1.tle_parameters.is_some(), omm2.tle_parameters.is_some());
if let (Some(t1), Some(t2)) = (&omm1.tle_parameters, &omm2.tle_parameters) {
assert_eq!(t1.ephemeris_type, t2.ephemeris_type);
assert_eq!(t1.classification_type, t2.classification_type);
assert_eq!(t1.norad_cat_id, t2.norad_cat_id);
assert_eq!(t1.element_set_no, t2.element_set_no);
assert_eq!(t1.rev_at_epoch, t2.rev_at_epoch);
assert_eq!(t1.bstar.is_some(), t2.bstar.is_some());
if let (Some(b1), Some(b2)) = (t1.bstar, t2.bstar) {
assert!((b1 - b2).abs() < 1e-10);
}
assert_eq!(t1.mean_motion_dot.is_some(), t2.mean_motion_dot.is_some());
if let (Some(d1), Some(d2)) = (t1.mean_motion_dot, t2.mean_motion_dot) {
assert!((d1 - d2).abs() < 1e-12);
}
assert_eq!(t1.mean_motion_ddot.is_some(), t2.mean_motion_ddot.is_some());
if let (Some(d1), Some(d2)) = (t1.mean_motion_ddot, t2.mean_motion_ddot) {
assert!((d1 - d2).abs() < 1e-12);
}
}
assert_eq!(
omm1.spacecraft_parameters.is_some(),
omm2.spacecraft_parameters.is_some()
);
assert_eq!(omm1.covariance.is_some(), omm2.covariance.is_some());
if let (Some(cov1), Some(cov2)) = (&omm1.covariance, &omm2.covariance) {
assert_eq!(cov1.cov_ref_frame, cov2.cov_ref_frame);
for i in 0..6 {
for j in 0..6 {
let rel = if cov1.matrix[(i, j)].abs() > 1e-20 {
((cov1.matrix[(i, j)] - cov2.matrix[(i, j)]) / cov1.matrix[(i, j)]).abs()
} else {
(cov1.matrix[(i, j)] - cov2.matrix[(i, j)]).abs()
};
assert!(
rel < 1e-4,
"cov({},{}) mismatch: {} vs {}",
i,
j,
cov1.matrix[(i, j)],
cov2.matrix[(i, j)]
);
}
}
}
assert_eq!(omm1.user_defined.is_some(), omm2.user_defined.is_some());
if let (Some(ud1), Some(ud2)) = (&omm1.user_defined, &omm2.user_defined) {
assert_eq!(ud1.parameters.len(), ud2.parameters.len());
for (k, v) in &ud1.parameters {
assert_eq!(ud2.parameters.get(k), Some(v));
}
}
}
#[test]
fn test_omm_kvn_full_round_trip() {
let omm1 = OMM::from_file("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
let kvn = omm1.to_string(CCSDSFormat::KVN).unwrap();
let omm2 = OMM::from_str(&kvn).unwrap();
assert_omm_fields_match(&omm1, &omm2);
}
#[test]
fn test_omm_xml_full_round_trip() {
let omm1 = OMM::from_file("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
let xml = omm1.to_string(CCSDSFormat::XML).unwrap();
let omm2 = OMM::from_str(&xml).unwrap();
assert_omm_fields_match(&omm1, &omm2);
}
#[test]
fn test_omm_json_full_round_trip() {
let omm1 = OMM::from_file("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
let json = omm1.to_string(CCSDSFormat::JSON).unwrap();
let omm2 = OMM::from_str(&json).unwrap();
assert_omm_fields_match(&omm1, &omm2);
}
}