use anyhow::Result;
use serde::Deserialize;
use super::OMM;
fn parse_required_f64(field: &str, value: Option<String>) -> anyhow::Result<f64> {
let value = value.ok_or_else(|| anyhow::anyhow!("Missing required field {field}"))?;
value
.trim()
.parse::<f64>()
.map_err(|e| anyhow::anyhow!("Invalid float for {field}: {e}"))
}
fn parse_optional_f64(value: Option<String>) -> anyhow::Result<Option<f64>> {
match value {
None => Ok(None),
Some(s) => {
let s = s.trim();
if s.is_empty() {
Ok(None)
} else {
s.parse::<f64>()
.map(Some)
.map_err(|e| anyhow::anyhow!("Invalid float value: {e}"))
}
}
}
}
fn parse_optional_u8(value: Option<String>) -> anyhow::Result<Option<u8>> {
match value {
None => Ok(None),
Some(s) => {
let s = s.trim();
if s.is_empty() {
Ok(None)
} else {
s.parse::<u8>()
.map(Some)
.map_err(|e| anyhow::anyhow!("Invalid u8 value: {e}"))
}
}
}
}
fn parse_optional_u32(value: Option<String>) -> anyhow::Result<Option<u32>> {
match value {
None => Ok(None),
Some(s) => {
let s = s.trim();
if s.is_empty() {
Ok(None)
} else {
s.parse::<u32>()
.map(Some)
.map_err(|e| anyhow::anyhow!("Invalid u32 value: {e}"))
}
}
}
}
#[derive(Debug, Deserialize)]
struct OmmXmlRoot {
#[serde(rename = "omm", default)]
omms: Vec<OmmXmlMessage>,
}
#[derive(Debug, Deserialize)]
struct OmmXmlMessage {
#[serde(rename = "@version")]
version: Option<String>,
#[serde(rename = "header")]
header: Option<OmmXmlHeader>,
#[serde(rename = "body")]
body: OmmXmlBody,
}
#[derive(Debug, Deserialize)]
struct OmmXmlHeader {
#[serde(rename = "ORIGINATOR")]
originator: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OmmXmlBody {
#[serde(rename = "segment")]
segment: OmmXmlSegment,
}
#[derive(Debug, Deserialize)]
struct OmmXmlSegment {
#[serde(rename = "metadata")]
metadata: OmmXmlMetadata,
#[serde(rename = "data")]
data: OmmXmlData,
}
#[derive(Debug, Deserialize)]
struct OmmXmlMetadata {
#[serde(rename = "OBJECT_NAME")]
object_name: String,
#[serde(rename = "OBJECT_ID")]
object_id: String,
#[serde(rename = "CENTER_NAME")]
center_name: Option<String>,
#[serde(rename = "REF_FRAME")]
reference_frame: Option<String>,
#[serde(rename = "REF_FRAME_EPOCH")]
reference_frame_epoch: Option<String>,
#[serde(rename = "TIME_SYSTEM")]
time_system: Option<String>,
#[serde(rename = "MEAN_ELEMENT_THEORY")]
mean_element_theory: Option<String>,
#[serde(rename = "CLASSIFICATION")]
classification: Option<String>,
#[serde(rename = "MESSAGE_ID")]
message_id: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OmmXmlData {
#[serde(rename = "meanElements")]
mean_elements: OmmXmlMeanElements,
#[serde(rename = "tleParameters")]
tle_parameters: Option<OmmXmlTleParameters>,
}
#[derive(Debug, Deserialize)]
struct OmmXmlMeanElements {
#[serde(rename = "EPOCH")]
epoch: String,
#[serde(rename = "MEAN_MOTION")]
mean_motion: Option<String>,
#[serde(rename = "ECCENTRICITY")]
eccentricity: Option<String>,
#[serde(rename = "INCLINATION")]
inclination: Option<String>,
#[serde(rename = "RA_OF_ASC_NODE")]
raan: Option<String>,
#[serde(rename = "ARG_OF_PERICENTER")]
arg_of_pericenter: Option<String>,
#[serde(rename = "MEAN_ANOMALY")]
mean_anomaly: Option<String>,
#[serde(rename = "GM")]
gm: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OmmXmlTleParameters {
#[serde(rename = "EPHEMERIS_TYPE")]
ephemeris_type: Option<String>,
#[serde(rename = "CLASSIFICATION_TYPE")]
classification_type: Option<String>,
#[serde(rename = "NORAD_CAT_ID")]
norad_cat_id: Option<String>,
#[serde(rename = "ELEMENT_SET_NO")]
element_set_no: Option<String>,
#[serde(rename = "REV_AT_EPOCH")]
rev_at_epoch: Option<String>,
#[serde(rename = "BSTAR")]
bstar: Option<String>,
#[serde(rename = "BTERM")]
bterm: Option<String>,
#[serde(rename = "MEAN_MOTION_DOT")]
mean_motion_dot: Option<String>,
#[serde(rename = "MEAN_MOTION_DDOT")]
mean_motion_ddot: Option<String>,
#[serde(rename = "AGOM")]
agom: Option<String>,
#[serde(rename = "MASS")]
mass: Option<String>,
#[serde(rename = "SOLAR_RAD_AREA")]
solar_rad_area: Option<String>,
#[serde(rename = "DRAG_AREA")]
drag_area: Option<String>,
#[serde(rename = "SOLAR_RAD_COEFF")]
solar_rad_coeff: Option<String>,
#[serde(rename = "DRAG_COEFF")]
drag_coeff: Option<String>,
}
impl TryFrom<OmmXmlMessage> for OMM {
type Error = anyhow::Error;
fn try_from(xml: OmmXmlMessage) -> std::result::Result<Self, Self::Error> {
let metadata = xml.body.segment.metadata;
let mean = xml.body.segment.data.mean_elements;
let tle = xml.body.segment.data.tle_parameters;
let (ephemeris_type, classification_type, norad_cat_id, element_set_no, rev_at_epoch) =
if let Some(ref tle) = tle {
(
parse_optional_u8(tle.ephemeris_type.clone())?,
tle.classification_type.clone(),
parse_optional_u32(tle.norad_cat_id.clone())?,
parse_optional_u32(tle.element_set_no.clone())?,
parse_optional_u32(tle.rev_at_epoch.clone())?,
)
} else {
(None, None, None, None, None)
};
Ok(Self {
omm_version: xml.version,
comments: None,
originator: xml.header.and_then(|h| h.originator),
classification: metadata.classification,
message_id: metadata.message_id,
object_name: metadata.object_name,
object_id: metadata.object_id,
center_name: metadata.center_name,
reference_frame: metadata.reference_frame,
reference_frame_epoch: metadata.reference_frame_epoch,
time_system: metadata.time_system,
mean_element_theory: metadata.mean_element_theory,
epoch: mean.epoch,
mean_motion: parse_required_f64("MEAN_MOTION", mean.mean_motion)?,
eccentricity: parse_required_f64("ECCENTRICITY", mean.eccentricity)?,
inclination: parse_required_f64("INCLINATION", mean.inclination)?,
raan: parse_required_f64("RA_OF_ASC_NODE", mean.raan)?,
arg_of_pericenter: parse_required_f64("ARG_OF_PERICENTER", mean.arg_of_pericenter)?,
mean_anomaly: parse_required_f64("MEAN_ANOMALY", mean.mean_anomaly)?,
gm: parse_optional_f64(mean.gm)?,
mass: parse_optional_f64(tle.as_ref().and_then(|x| x.mass.clone()))?,
solar_rad_area: parse_optional_f64(
tle.as_ref().and_then(|x| x.solar_rad_area.clone()),
)?,
drag_area: parse_optional_f64(tle.as_ref().and_then(|x| x.drag_area.clone()))?,
solar_rad_coeff: parse_optional_f64(
tle.as_ref().and_then(|x| x.solar_rad_coeff.clone()),
)?,
drag_coeff: parse_optional_f64(tle.as_ref().and_then(|x| x.drag_coeff.clone()))?,
ephemeris_type,
classification_type,
norad_cat_id,
element_set_no,
rev_at_epoch,
bstar: parse_optional_f64(tle.as_ref().and_then(|x| x.bstar.clone()))?,
bterm: parse_optional_f64(tle.as_ref().and_then(|x| x.bterm.clone()))?,
mean_motion_dot: parse_optional_f64(
tle.as_ref().and_then(|x| x.mean_motion_dot.clone()),
)?,
mean_motion_ddot: parse_optional_f64(
tle.as_ref().and_then(|x| x.mean_motion_ddot.clone()),
)?,
agom: parse_optional_f64(tle.as_ref().and_then(|x| x.agom.clone()))?,
satrec: None,
extra_fields: std::collections::HashMap::new(),
})
}
}
impl OMM {
pub fn from_xml_string(s: &str) -> Result<Vec<Self>> {
if s.contains("<ndm") {
let root: OmmXmlRoot = quick_xml::de::from_str(s).map_err(|e| anyhow::anyhow!(e))?;
root.omms
.into_iter()
.map(Self::try_from)
.collect::<Result<Vec<_>>>()
} else {
let msg: OmmXmlMessage = quick_xml::de::from_str(s).map_err(|e| anyhow::anyhow!(e))?;
Ok(vec![Self::try_from(msg)?])
}
}
pub fn from_xml_file<P: AsRef<std::path::Path>>(path: P) -> Result<Vec<Self>> {
let s = std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!(e))?;
Self::from_xml_string(&s)
}
}
#[cfg(test)]
mod tests {
use super::OMM;
#[test]
fn test_parse_omm_celestrak_xml() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ndm xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://sanaregistry.org/r/ndmxml_unqualified/ndmxml-2.0.0-master-2.0.xsd">
<omm id="CCSDS_OMM_VERS" version="2.0">
<header><CREATION_DATE/><ORIGINATOR/></header>
<body>
<segment>
<metadata>
<OBJECT_NAME>ISS (ZARYA)</OBJECT_NAME>
<OBJECT_ID>1998-067A</OBJECT_ID>
<CENTER_NAME>EARTH</CENTER_NAME>
<REF_FRAME>TEME</REF_FRAME>
<TIME_SYSTEM>UTC</TIME_SYSTEM>
<MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>
</metadata>
<data>
<meanElements>
<EPOCH>2026-02-14T05:08:48.534432</EPOCH>
<MEAN_MOTION>15.48593530</MEAN_MOTION>
<ECCENTRICITY>.00110623</ECCENTRICITY>
<INCLINATION>51.6315</INCLINATION>
<RA_OF_ASC_NODE>188.3997</RA_OF_ASC_NODE>
<ARG_OF_PERICENTER>96.9141</ARG_OF_PERICENTER>
<MEAN_ANOMALY>263.3106</MEAN_ANOMALY>
</meanElements>
<tleParameters>
<EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>
<CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>
<NORAD_CAT_ID>25544</NORAD_CAT_ID>
<ELEMENT_SET_NO>999</ELEMENT_SET_NO>
<REV_AT_EPOCH>55269</REV_AT_EPOCH>
<BSTAR>.16303535E-3</BSTAR>
<MEAN_MOTION_DOT>.8429E-4</MEAN_MOTION_DOT>
<MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>
</tleParameters>
</data>
</segment>
</body>
</omm>
</ndm>
"#;
let msg = OMM::from_xml_string(xml).unwrap();
assert_eq!(msg.len(), 1);
assert_eq!(msg[0].object_id, "1998-067A");
assert_eq!(msg[0].omm_version.as_deref(), Some("2.0"));
assert_eq!(msg[0].norad_cat_id, Some(25544));
}
}