use std::collections::HashMap;
use crate::ccsds::common::{
CCSDSCovariance, CCSDSRefFrame, CCSDSSpacecraftParameters, CCSDSTimeSystem, CCSDSUserDefined,
ODMHeader, covariance_from_lower_triangular, parse_ccsds_datetime, strip_units,
};
use crate::ccsds::error::{ccsds_missing_field, ccsds_parse_error};
use crate::ccsds::oem::{OEM, OEMMetadata, OEMSegment, OEMStateVector};
use crate::ccsds::omm::{OMM, OMMMetadata, OMMTleParameters, OMMeanElements};
use crate::ccsds::opm::{OPM, OPMKeplerianElements, OPMManeuver, OPMMetadata, OPMStateVector};
use crate::time::Epoch;
use crate::utils::errors::BraheError;
#[derive(Debug)]
enum KVNToken {
KeyValue { key: String, value: String },
Comment(String),
DataLine(Vec<String>),
Empty,
}
const SECTION_MARKERS: &[&str] = &[
"META_START",
"META_STOP",
"COVARIANCE_START",
"COVARIANCE_STOP",
];
fn tokenize_line(line: &str) -> KVNToken {
let trimmed = line.trim();
if trimmed.is_empty() {
return KVNToken::Empty;
}
if let Some(rest) = trimmed.strip_prefix("COMMENT") {
let text = rest.trim().to_string();
return KVNToken::Comment(text);
}
if SECTION_MARKERS.contains(&trimmed) {
return KVNToken::KeyValue {
key: trimmed.to_string(),
value: String::new(),
};
}
if let Some(eq_pos) = trimmed.find('=') {
let key = trimmed[..eq_pos].trim().to_string();
let value = trimmed[eq_pos + 1..].trim().to_string();
return KVNToken::KeyValue { key, value };
}
let parts: Vec<String> = trimmed.split_whitespace().map(|s| s.to_string()).collect();
if parts.is_empty() {
KVNToken::Empty
} else {
KVNToken::DataLine(parts)
}
}
#[derive(Debug, PartialEq)]
enum OEMState {
Header,
Metadata,
EphemerisBlock,
CovarianceBlock,
}
pub fn parse_oem(content: &str) -> Result<OEM, BraheError> {
let mut state = OEMState::Header;
let mut format_version: Option<f64> = None;
let mut classification: Option<String> = None;
let mut creation_date: Option<Epoch> = None;
let mut originator: Option<String> = None;
let mut message_id: Option<String> = None;
let mut header_comments: Vec<String> = Vec::new();
let mut segments: Vec<OEMSegment> = Vec::new();
let mut current_metadata: Option<OEMMetadata> = None;
let mut current_states: Vec<OEMStateVector> = Vec::new();
let mut current_covariances: Vec<CCSDSCovariance> = Vec::new();
let mut current_data_comments: Vec<String> = Vec::new();
let mut current_metadata_comments: Vec<String> = Vec::new();
let mut meta_object_name: Option<String> = None;
let mut meta_object_id: Option<String> = None;
let mut meta_center_name: Option<String> = None;
let mut meta_ref_frame: Option<CCSDSRefFrame> = None;
let mut meta_ref_frame_epoch: Option<Epoch> = None;
let mut meta_time_system: Option<CCSDSTimeSystem> = None;
let mut meta_start_time: Option<Epoch> = None;
let mut meta_useable_start_time: Option<Epoch> = None;
let mut meta_useable_stop_time: Option<Epoch> = None;
let mut meta_stop_time: Option<Epoch> = None;
let mut meta_interpolation: Option<String> = None;
let mut meta_interpolation_degree: Option<u32> = None;
let mut cov_epoch: Option<Epoch> = None;
let mut cov_ref_frame: Option<CCSDSRefFrame> = None;
let mut cov_values: Vec<f64> = Vec::new();
let mut cov_comments: Vec<String> = Vec::new();
let mut active_time_system = CCSDSTimeSystem::UTC;
for line in content.lines() {
let token = tokenize_line(line);
match (&state, token) {
(OEMState::Header, KVNToken::KeyValue { key, value }) => {
match key.as_str() {
"CCSDS_OEM_VERS" => {
format_version = Some(value.parse::<f64>().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid version '{}'", value))
})?);
}
"CLASSIFICATION" => classification = Some(value),
"CREATION_DATE" => {
creation_date = Some(parse_ccsds_datetime(&value, &CCSDSTimeSystem::UTC)?);
}
"ORIGINATOR" => originator = Some(value),
"MESSAGE_ID" => message_id = Some(value),
"META_START" => {
state = OEMState::Metadata;
}
_ => {
return Err(ccsds_parse_error(
"OEM",
&format!("unexpected header keyword '{}'", key),
));
}
}
}
(OEMState::Header, KVNToken::Comment(text)) => {
header_comments.push(text);
}
(OEMState::Header, KVNToken::Empty) => {}
(OEMState::Metadata, KVNToken::KeyValue { key, value }) => {
match key.as_str() {
"META_START" => {
}
"OBJECT_NAME" => meta_object_name = Some(value),
"OBJECT_ID" => meta_object_id = Some(value),
"CENTER_NAME" => meta_center_name = Some(value),
"REF_FRAME" => meta_ref_frame = Some(CCSDSRefFrame::parse(&value)),
"REF_FRAME_EPOCH" => {
let ts = meta_time_system.as_ref().unwrap_or(&CCSDSTimeSystem::UTC);
meta_ref_frame_epoch = Some(parse_ccsds_datetime(&value, ts)?);
}
"TIME_SYSTEM" => {
let ts = CCSDSTimeSystem::parse(&value)?;
active_time_system = ts.clone();
meta_time_system = Some(ts);
}
"START_TIME" => {
let ts = meta_time_system.as_ref().unwrap_or(&CCSDSTimeSystem::UTC);
meta_start_time = Some(parse_ccsds_datetime(&value, ts)?);
}
"USEABLE_START_TIME" => {
let ts = meta_time_system.as_ref().unwrap_or(&CCSDSTimeSystem::UTC);
meta_useable_start_time = Some(parse_ccsds_datetime(&value, ts)?);
}
"USEABLE_STOP_TIME" => {
let ts = meta_time_system.as_ref().unwrap_or(&CCSDSTimeSystem::UTC);
meta_useable_stop_time = Some(parse_ccsds_datetime(&value, ts)?);
}
"STOP_TIME" => {
let ts = meta_time_system.as_ref().unwrap_or(&CCSDSTimeSystem::UTC);
meta_stop_time = Some(parse_ccsds_datetime(&value, ts)?);
}
"INTERPOLATION" => meta_interpolation = Some(value),
"INTERPOLATION_DEGREE" => {
meta_interpolation_degree = Some(value.parse::<u32>().map_err(|_| {
ccsds_parse_error(
"OEM",
&format!("invalid interpolation degree '{}'", value),
)
})?);
}
"META_STOP" => {
let metadata = OEMMetadata {
object_name: meta_object_name
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "OBJECT_NAME"))?,
object_id: meta_object_id
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "OBJECT_ID"))?,
center_name: meta_center_name
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "CENTER_NAME"))?,
ref_frame: meta_ref_frame
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "REF_FRAME"))?,
ref_frame_epoch: meta_ref_frame_epoch.take(),
time_system: meta_time_system
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "TIME_SYSTEM"))?,
start_time: meta_start_time
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "START_TIME"))?,
useable_start_time: meta_useable_start_time.take(),
useable_stop_time: meta_useable_stop_time.take(),
stop_time: meta_stop_time
.take()
.ok_or_else(|| ccsds_missing_field("OEM", "STOP_TIME"))?,
interpolation: meta_interpolation.take(),
interpolation_degree: meta_interpolation_degree.take(),
comments: std::mem::take(&mut current_metadata_comments),
};
if let Some(prev_meta) = current_metadata.take() {
segments.push(OEMSegment {
metadata: prev_meta,
comments: std::mem::take(&mut current_data_comments),
states: std::mem::take(&mut current_states),
covariances: std::mem::take(&mut current_covariances),
});
}
current_metadata = Some(metadata);
state = OEMState::EphemerisBlock;
}
_ => {
return Err(ccsds_parse_error(
"OEM",
&format!("unexpected metadata keyword '{}'", key),
));
}
}
}
(OEMState::Metadata, KVNToken::Comment(text)) => {
current_metadata_comments.push(text);
}
(OEMState::Metadata, KVNToken::Empty) => {}
(OEMState::EphemerisBlock, KVNToken::DataLine(parts)) => {
if parts.len() < 7 {
return Err(ccsds_parse_error(
"OEM",
&format!(
"ephemeris data line has {} columns, expected at least 7",
parts.len()
),
));
}
let epoch = parse_ccsds_datetime(&parts[0], &active_time_system)?;
let x: f64 = parts[1].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid X value '{}'", parts[1]))
})?;
let y: f64 = parts[2].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid Y value '{}'", parts[2]))
})?;
let z: f64 = parts[3].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid Z value '{}'", parts[3]))
})?;
let vx: f64 = parts[4].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid VX value '{}'", parts[4]))
})?;
let vy: f64 = parts[5].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid VY value '{}'", parts[5]))
})?;
let vz: f64 = parts[6].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid VZ value '{}'", parts[6]))
})?;
let position = [x * 1000.0, y * 1000.0, z * 1000.0];
let velocity = [vx * 1000.0, vy * 1000.0, vz * 1000.0];
let acceleration = if parts.len() >= 10 {
let ax: f64 = parts[7].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid AX value '{}'", parts[7]))
})?;
let ay: f64 = parts[8].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid AY value '{}'", parts[8]))
})?;
let az: f64 = parts[9].parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid AZ value '{}'", parts[9]))
})?;
Some([ax * 1000.0, ay * 1000.0, az * 1000.0])
} else {
None
};
current_states.push(OEMStateVector {
epoch,
position,
velocity,
acceleration,
});
}
(OEMState::EphemerisBlock, KVNToken::KeyValue { key, value: _ }) => {
match key.as_str() {
"META_START" => {
state = OEMState::Metadata;
}
"COVARIANCE_START" => {
cov_epoch = None;
cov_ref_frame = None;
cov_values.clear();
cov_comments.clear();
state = OEMState::CovarianceBlock;
}
_ => {
return Err(ccsds_parse_error(
"OEM",
&format!("unexpected keyword '{}' in ephemeris block", key),
));
}
}
}
(OEMState::EphemerisBlock, KVNToken::Comment(text)) => {
current_data_comments.push(text);
}
(OEMState::EphemerisBlock, KVNToken::Empty) => {}
(OEMState::CovarianceBlock, KVNToken::KeyValue { key, value }) => {
match key.as_str() {
"EPOCH" => {
if cov_values.len() == 21 {
let mut vals = [0.0_f64; 21];
vals.copy_from_slice(&cov_values);
let matrix = covariance_from_lower_triangular(&vals, 1e6);
current_covariances.push(CCSDSCovariance {
epoch: cov_epoch.take(),
cov_ref_frame: cov_ref_frame.take(),
matrix,
comments: std::mem::take(&mut cov_comments),
});
cov_values.clear();
}
cov_epoch = Some(parse_ccsds_datetime(&value, &active_time_system)?);
}
"COV_REF_FRAME" => {
cov_ref_frame = Some(CCSDSRefFrame::parse(&value));
}
"COVARIANCE_STOP" => {
if cov_values.len() == 21 {
let mut vals = [0.0_f64; 21];
vals.copy_from_slice(&cov_values);
let matrix = covariance_from_lower_triangular(&vals, 1e6);
current_covariances.push(CCSDSCovariance {
epoch: cov_epoch.take(),
cov_ref_frame: cov_ref_frame.take(),
matrix,
comments: std::mem::take(&mut cov_comments),
});
cov_values.clear();
}
state = OEMState::EphemerisBlock;
}
_ => {
return Err(ccsds_parse_error(
"OEM",
&format!("unexpected keyword '{}' in covariance block", key),
));
}
}
}
(OEMState::CovarianceBlock, KVNToken::DataLine(parts)) => {
for part in &parts {
let val: f64 = part.parse().map_err(|_| {
ccsds_parse_error("OEM", &format!("invalid covariance value '{}'", part))
})?;
cov_values.push(val);
}
}
(OEMState::CovarianceBlock, KVNToken::Comment(text)) => {
cov_comments.push(text);
}
(OEMState::CovarianceBlock, KVNToken::Empty) => {}
(st, token) => {
return Err(ccsds_parse_error(
"OEM",
&format!("unexpected token {:?} in state {:?}", token, st),
));
}
}
}
if let Some(meta) = current_metadata.take() {
segments.push(OEMSegment {
metadata: meta,
comments: current_data_comments,
states: current_states,
covariances: current_covariances,
});
}
let header = ODMHeader {
format_version: format_version
.ok_or_else(|| ccsds_missing_field("OEM", "CCSDS_OEM_VERS"))?,
classification,
creation_date: creation_date.ok_or_else(|| ccsds_missing_field("OEM", "CREATION_DATE"))?,
originator: originator.ok_or_else(|| ccsds_missing_field("OEM", "ORIGINATOR"))?,
message_id,
comments: header_comments,
};
Ok(OEM { header, segments })
}
pub fn parse_omm(content: &str) -> Result<OMM, BraheError> {
let mut header_comments: Vec<String> = Vec::new();
let mut metadata_comments: Vec<String> = Vec::new();
let mut mean_element_comments: Vec<String> = Vec::new();
let mut tle_comments: Vec<String> = Vec::new();
let mut spacecraft_comments: Vec<String> = Vec::new();
let mut format_version: Option<f64> = None;
let mut classification: Option<String> = None;
let mut creation_date: Option<Epoch> = None;
let mut originator: Option<String> = None;
let mut message_id: Option<String> = None;
let mut object_name: Option<String> = None;
let mut object_id: Option<String> = None;
let mut center_name: Option<String> = None;
let mut ref_frame: Option<CCSDSRefFrame> = None;
let mut ref_frame_epoch: Option<Epoch> = None;
let mut time_system: Option<CCSDSTimeSystem> = None;
let mut mean_element_theory: Option<String> = None;
let mut epoch: Option<Epoch> = None;
let mut mean_motion: Option<f64> = None;
let mut semi_major_axis: Option<f64> = None;
let mut eccentricity: Option<f64> = None;
let mut inclination: Option<f64> = None;
let mut ra_of_asc_node: Option<f64> = None;
let mut arg_of_pericenter: Option<f64> = None;
let mut mean_anomaly: Option<f64> = None;
let mut gm: Option<f64> = None;
let mut ephemeris_type: Option<u32> = None;
let mut classification_type: Option<char> = None;
let mut norad_cat_id: Option<u32> = None;
let mut element_set_no: Option<u32> = None;
let mut rev_at_epoch: Option<u32> = None;
let mut bstar: Option<f64> = None;
let mut bterm: Option<f64> = None;
let mut mean_motion_dot: Option<f64> = None;
let mut mean_motion_ddot: Option<f64> = None;
let mut agom: Option<f64> = None;
let mut mass: Option<f64> = None;
let mut solar_rad_area: Option<f64> = None;
let mut solar_rad_coeff: Option<f64> = None;
let mut drag_area: Option<f64> = None;
let mut drag_coeff: Option<f64> = None;
let mut cov_ref_frame: Option<CCSDSRefFrame> = None;
let mut cov_values: Vec<f64> = Vec::new();
let mut cov_comments: Vec<String> = Vec::new();
let mut user_defined: HashMap<String, String> = HashMap::new();
let mut in_header = true;
let mut in_metadata = false;
let mut in_mean_elements = false;
let mut in_tle = false;
let mut in_spacecraft = false;
let active_ts = |ts: &Option<CCSDSTimeSystem>| ts.clone().unwrap_or(CCSDSTimeSystem::UTC);
for line in content.lines() {
let token = tokenize_line(line);
match token {
KVNToken::KeyValue { key, value } => {
let val = strip_units(&value);
match key.as_str() {
"CCSDS_OMM_VERS" => {
format_version = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid version"))?,
);
}
"CLASSIFICATION" => {
classification = Some(val.to_string());
}
"CREATION_DATE" => {
creation_date = Some(parse_ccsds_datetime(val, &CCSDSTimeSystem::UTC)?);
in_header = false;
}
"ORIGINATOR" => {
originator = Some(val.to_string());
}
"MESSAGE_ID" => {
message_id = Some(val.to_string());
}
"OBJECT_NAME" => {
object_name = Some(val.to_string());
in_metadata = true;
}
"OBJECT_ID" => {
object_id = Some(val.to_string());
}
"CENTER_NAME" => {
center_name = Some(val.to_string());
}
"REF_FRAME" => {
ref_frame = Some(CCSDSRefFrame::parse(val));
}
"REF_FRAME_EPOCH" => {
ref_frame_epoch =
Some(parse_ccsds_datetime(val, &active_ts(&time_system))?);
}
"TIME_SYSTEM" => {
time_system = Some(CCSDSTimeSystem::parse(val)?);
}
"MEAN_ELEMENT_THEORY" => {
mean_element_theory = Some(val.to_string());
in_metadata = false;
}
"EPOCH" => {
epoch = Some(parse_ccsds_datetime(val, &active_ts(&time_system))?);
in_mean_elements = true;
}
"MEAN_MOTION" => {
mean_motion = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid MEAN_MOTION"))?,
);
}
"SEMI_MAJOR_AXIS" => {
semi_major_axis =
Some(val.parse().map_err(|_| {
ccsds_parse_error("OMM", "invalid SEMI_MAJOR_AXIS")
})?);
}
"ECCENTRICITY" => {
eccentricity = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid ECCENTRICITY"))?,
);
}
"INCLINATION" => {
inclination = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid INCLINATION"))?,
);
}
"RA_OF_ASC_NODE" => {
ra_of_asc_node = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid RA_OF_ASC_NODE"))?,
);
}
"ARG_OF_PERICENTER" => {
arg_of_pericenter =
Some(val.parse().map_err(|_| {
ccsds_parse_error("OMM", "invalid ARG_OF_PERICENTER")
})?);
}
"MEAN_ANOMALY" => {
mean_anomaly = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid MEAN_ANOMALY"))?,
);
in_mean_elements = false;
}
"GM" => {
let gm_val: f64 = val
.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid GM"))?;
gm = Some(gm_val * 1e9); }
"EPHEMERIS_TYPE" => {
ephemeris_type = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid EPHEMERIS_TYPE"))?,
);
in_tle = true;
}
"CLASSIFICATION_TYPE" => {
classification_type = val.chars().next();
}
"NORAD_CAT_ID" => {
norad_cat_id = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid NORAD_CAT_ID"))?,
);
}
"ELEMENT_SET_NO" => {
element_set_no = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid ELEMENT_SET_NO"))?,
);
}
"REV_AT_EPOCH" => {
rev_at_epoch = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid REV_AT_EPOCH"))?,
);
}
"BSTAR" => {
bstar = Some(parse_scientific_notation(val)?);
}
"BTERM" => {
bterm = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid BTERM"))?,
);
}
"MEAN_MOTION_DOT" => {
mean_motion_dot = Some(parse_scientific_notation(val)?);
in_tle = false;
}
"MEAN_MOTION_DDOT" => {
mean_motion_ddot = Some(parse_scientific_notation(val)?);
}
"AGOM" => {
agom = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid AGOM"))?,
);
}
"MASS" => {
mass = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid MASS"))?,
);
in_spacecraft = true;
}
"SOLAR_RAD_AREA" => {
solar_rad_area = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid SOLAR_RAD_AREA"))?,
);
}
"SOLAR_RAD_COEFF" => {
solar_rad_coeff =
Some(val.parse().map_err(|_| {
ccsds_parse_error("OMM", "invalid SOLAR_RAD_COEFF")
})?);
}
"DRAG_AREA" => {
drag_area = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid DRAG_AREA"))?,
);
}
"DRAG_COEFF" => {
drag_coeff = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OMM", "invalid DRAG_COEFF"))?,
);
in_spacecraft = false;
}
"COV_REF_FRAME" => {
cov_ref_frame = Some(CCSDSRefFrame::parse(val));
}
k if k.starts_with("CX_") || k.starts_with("CY_") || k.starts_with("CZ_") => {
let v: f64 = val.parse().map_err(|_| {
ccsds_parse_error("OMM", &format!("invalid covariance value '{}'", val))
})?;
cov_values.push(v);
}
k if k.starts_with("USER_DEFINED_") => {
let param_name = k.strip_prefix("USER_DEFINED_").unwrap_or(k);
user_defined.insert(param_name.to_string(), val.to_string());
}
_ => {
}
}
}
KVNToken::Comment(text) => {
if in_header {
header_comments.push(text);
} else if in_metadata {
metadata_comments.push(text);
} else if in_mean_elements {
mean_element_comments.push(text);
} else if in_tle {
tle_comments.push(text);
} else if in_spacecraft {
spacecraft_comments.push(text);
} else if !cov_values.is_empty() {
cov_comments.push(text);
} else {
metadata_comments.push(text);
}
}
KVNToken::Empty => {}
KVNToken::DataLine(_) => {}
}
}
let header = ODMHeader {
format_version: format_version
.ok_or_else(|| ccsds_missing_field("OMM", "CCSDS_OMM_VERS"))?,
classification,
creation_date: creation_date.ok_or_else(|| ccsds_missing_field("OMM", "CREATION_DATE"))?,
originator: originator.ok_or_else(|| ccsds_missing_field("OMM", "ORIGINATOR"))?,
message_id,
comments: header_comments,
};
let metadata = OMMMetadata {
object_name: object_name.ok_or_else(|| ccsds_missing_field("OMM", "OBJECT_NAME"))?,
object_id: object_id.ok_or_else(|| ccsds_missing_field("OMM", "OBJECT_ID"))?,
center_name: center_name.ok_or_else(|| ccsds_missing_field("OMM", "CENTER_NAME"))?,
ref_frame: ref_frame.ok_or_else(|| ccsds_missing_field("OMM", "REF_FRAME"))?,
ref_frame_epoch,
time_system: time_system.ok_or_else(|| ccsds_missing_field("OMM", "TIME_SYSTEM"))?,
mean_element_theory: mean_element_theory
.ok_or_else(|| ccsds_missing_field("OMM", "MEAN_ELEMENT_THEORY"))?,
comments: metadata_comments,
};
let mean_elements = OMMeanElements {
epoch: epoch.ok_or_else(|| ccsds_missing_field("OMM", "EPOCH"))?,
mean_motion,
semi_major_axis,
eccentricity: eccentricity.ok_or_else(|| ccsds_missing_field("OMM", "ECCENTRICITY"))?,
inclination: inclination.ok_or_else(|| ccsds_missing_field("OMM", "INCLINATION"))?,
ra_of_asc_node: ra_of_asc_node
.ok_or_else(|| ccsds_missing_field("OMM", "RA_OF_ASC_NODE"))?,
arg_of_pericenter: arg_of_pericenter
.ok_or_else(|| ccsds_missing_field("OMM", "ARG_OF_PERICENTER"))?,
mean_anomaly: mean_anomaly.ok_or_else(|| ccsds_missing_field("OMM", "MEAN_ANOMALY"))?,
gm,
comments: mean_element_comments,
};
let tle_parameters = if ephemeris_type.is_some()
|| norad_cat_id.is_some()
|| bstar.is_some()
|| mean_motion_dot.is_some()
{
Some(OMMTleParameters {
ephemeris_type,
classification_type,
norad_cat_id,
element_set_no,
rev_at_epoch,
bstar,
bterm,
mean_motion_dot,
mean_motion_ddot,
agom,
comments: tle_comments,
})
} else {
None
};
let spacecraft_parameters = if mass.is_some() || solar_rad_area.is_some() || drag_area.is_some()
{
Some(CCSDSSpacecraftParameters {
mass,
solar_rad_area,
solar_rad_coeff,
drag_area,
drag_coeff,
comments: spacecraft_comments,
})
} else {
None
};
let covariance = if cov_values.len() == 21 {
let mut vals = [0.0_f64; 21];
vals.copy_from_slice(&cov_values);
let matrix = covariance_from_lower_triangular(&vals, 1e6);
Some(CCSDSCovariance {
epoch: None,
cov_ref_frame,
matrix,
comments: cov_comments,
})
} else {
None
};
let user_def = if user_defined.is_empty() {
None
} else {
Some(CCSDSUserDefined {
parameters: user_defined,
})
};
Ok(OMM {
header,
metadata,
mean_elements,
tle_parameters,
spacecraft_parameters,
covariance,
user_defined: user_def,
comments: Vec::new(),
})
}
fn parse_scientific_notation(s: &str) -> Result<f64, BraheError> {
let s = s.trim();
s.parse::<f64>()
.map_err(|_| ccsds_parse_error("OMM", &format!("invalid numeric value '{}'", s)))
}
pub fn parse_opm(content: &str) -> Result<OPM, BraheError> {
let mut header_comments: Vec<String> = Vec::new();
let mut metadata_comments: Vec<String> = Vec::new();
let mut state_comments: Vec<String> = Vec::new();
let mut kep_comments: Vec<String> = Vec::new();
let spacecraft_comments: Vec<String> = Vec::new();
let mut maneuver_comments: Vec<String> = Vec::new();
let mut format_version: Option<f64> = None;
let mut classification: Option<String> = None;
let mut creation_date: Option<Epoch> = None;
let mut originator: Option<String> = None;
let mut message_id: Option<String> = None;
let mut object_name: Option<String> = None;
let mut object_id: Option<String> = None;
let mut center_name: Option<String> = None;
let mut ref_frame: Option<CCSDSRefFrame> = None;
let mut ref_frame_epoch: Option<Epoch> = None;
let mut time_system: Option<CCSDSTimeSystem> = None;
let mut sv_epoch: Option<Epoch> = None;
let mut sv_x: Option<f64> = None;
let mut sv_y: Option<f64> = None;
let mut sv_z: Option<f64> = None;
let mut sv_vx: Option<f64> = None;
let mut sv_vy: Option<f64> = None;
let mut sv_vz: Option<f64> = None;
let mut kep_sma: Option<f64> = None;
let mut kep_ecc: Option<f64> = None;
let mut kep_inc: Option<f64> = None;
let mut kep_raan: Option<f64> = None;
let mut kep_argp: Option<f64> = None;
let mut kep_ta: Option<f64> = None;
let mut kep_ma: Option<f64> = None;
let mut kep_gm: Option<f64> = None;
let mut mass: Option<f64> = None;
let mut solar_rad_area: Option<f64> = None;
let mut solar_rad_coeff: Option<f64> = None;
let mut drag_area: Option<f64> = None;
let mut drag_coeff: Option<f64> = None;
let mut cov_ref_frame: Option<CCSDSRefFrame> = None;
let mut cov_values: Vec<f64> = Vec::new();
let mut maneuvers: Vec<OPMManeuver> = Vec::new();
let mut man_epoch: Option<Epoch> = None;
let mut man_duration: Option<f64> = None;
let mut man_delta_mass: Option<f64> = None;
let mut man_ref_frame: Option<CCSDSRefFrame> = None;
let mut man_dv1: Option<f64> = None;
let mut man_dv2: Option<f64> = None;
let mut man_dv3: Option<f64> = None;
let mut user_defined: HashMap<String, String> = HashMap::new();
let mut in_header = true;
let active_ts = |ts: &Option<CCSDSTimeSystem>| ts.clone().unwrap_or(CCSDSTimeSystem::UTC);
let flush_maneuver = |man_epoch: &mut Option<Epoch>,
man_duration: &mut Option<f64>,
man_delta_mass: &mut Option<f64>,
man_ref_frame: &mut Option<CCSDSRefFrame>,
man_dv1: &mut Option<f64>,
man_dv2: &mut Option<f64>,
man_dv3: &mut Option<f64>,
maneuvers: &mut Vec<OPMManeuver>,
comments: &mut Vec<String>| {
if let (Some(epoch), Some(dur), Some(frame), Some(dv1), Some(dv2), Some(dv3)) = (
man_epoch.take(),
man_duration.take(),
man_ref_frame.take(),
man_dv1.take(),
man_dv2.take(),
man_dv3.take(),
) {
maneuvers.push(OPMManeuver {
epoch_ignition: epoch,
duration: dur,
delta_mass: man_delta_mass.take(),
ref_frame: frame,
dv: [dv1 * 1000.0, dv2 * 1000.0, dv3 * 1000.0], comments: std::mem::take(comments),
});
}
};
for line in content.lines() {
let token = tokenize_line(line);
match token {
KVNToken::KeyValue { key, value } => {
let val = strip_units(&value);
match key.as_str() {
"CCSDS_OPM_VERS" => {
format_version = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid version"))?,
);
}
"CREATION_DATE" => {
creation_date = Some(parse_ccsds_datetime(val, &CCSDSTimeSystem::UTC)?);
in_header = false;
}
"ORIGINATOR" => {
originator = Some(val.to_string());
}
"MESSAGE_ID" => {
message_id = Some(val.to_string());
}
"CLASSIFICATION" => {
classification = Some(val.to_string());
}
"OBJECT_NAME" => {
object_name = Some(val.to_string());
}
"OBJECT_ID" => {
object_id = Some(val.to_string());
}
"CENTER_NAME" => {
center_name = Some(val.to_string());
}
"REF_FRAME" if ref_frame.is_none() => {
ref_frame = Some(CCSDSRefFrame::parse(val));
}
"REF_FRAME_EPOCH" => {
ref_frame_epoch =
Some(parse_ccsds_datetime(val, &active_ts(&time_system))?);
}
"TIME_SYSTEM" => {
time_system = Some(CCSDSTimeSystem::parse(val)?);
}
"EPOCH" => {
sv_epoch = Some(parse_ccsds_datetime(val, &active_ts(&time_system))?);
}
"X" => {
sv_x = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid X"))?
* 1000.0,
);
}
"Y" => {
sv_y = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid Y"))?
* 1000.0,
);
}
"Z" => {
sv_z = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid Z"))?
* 1000.0,
);
}
"X_DOT" => {
sv_vx = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid X_DOT"))?
* 1000.0,
);
}
"Y_DOT" => {
sv_vy = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid Y_DOT"))?
* 1000.0,
);
}
"Z_DOT" => {
sv_vz = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid Z_DOT"))?
* 1000.0,
);
}
"SEMI_MAJOR_AXIS" => {
kep_sma = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid SMA"))?
* 1000.0,
);
} "ECCENTRICITY" if kep_sma.is_some() || kep_ecc.is_none() => {
kep_ecc = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid ECC"))?,
);
}
"INCLINATION" if kep_sma.is_some() => {
kep_inc = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid INC"))?,
);
}
"RA_OF_ASC_NODE" if kep_sma.is_some() => {
kep_raan = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid RAAN"))?,
);
}
"ARG_OF_PERICENTER" if kep_sma.is_some() => {
kep_argp = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid ARGP"))?,
);
}
"TRUE_ANOMALY" => {
kep_ta = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid TA"))?,
);
}
"MEAN_ANOMALY" => {
kep_ma = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MA"))?,
);
}
"GM" => {
kep_gm = Some(
val.parse::<f64>()
.map_err(|_| ccsds_parse_error("OPM", "invalid GM"))?
* 1e9,
);
}
"MASS" => {
mass = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MASS"))?,
);
}
"SOLAR_RAD_AREA" => {
solar_rad_area = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid SOLAR_RAD_AREA"))?,
);
}
"SOLAR_RAD_COEFF" => {
solar_rad_coeff =
Some(val.parse().map_err(|_| {
ccsds_parse_error("OPM", "invalid SOLAR_RAD_COEFF")
})?);
}
"DRAG_AREA" => {
drag_area = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid DRAG_AREA"))?,
);
}
"DRAG_COEFF" => {
drag_coeff = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid DRAG_COEFF"))?,
);
}
"COV_REF_FRAME" => {
cov_ref_frame = Some(CCSDSRefFrame::parse(val));
}
k if k.starts_with("CX_") || k.starts_with("CY_") || k.starts_with("CZ_") => {
let v: f64 = val.parse().map_err(|_| {
ccsds_parse_error("OPM", &format!("invalid cov value '{}'", val))
})?;
cov_values.push(v);
}
"MAN_EPOCH_IGNITION" => {
flush_maneuver(
&mut man_epoch,
&mut man_duration,
&mut man_delta_mass,
&mut man_ref_frame,
&mut man_dv1,
&mut man_dv2,
&mut man_dv3,
&mut maneuvers,
&mut maneuver_comments,
);
man_epoch = Some(parse_ccsds_datetime(val, &active_ts(&time_system))?);
}
"MAN_DURATION" => {
man_duration = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MAN_DURATION"))?,
);
}
"MAN_DELTA_MASS" => {
man_delta_mass = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MAN_DELTA_MASS"))?,
);
}
"MAN_REF_FRAME" => {
man_ref_frame = Some(CCSDSRefFrame::parse(val));
}
"MAN_DV_1" => {
man_dv1 = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MAN_DV_1"))?,
);
}
"MAN_DV_2" => {
man_dv2 = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MAN_DV_2"))?,
);
}
"MAN_DV_3" => {
man_dv3 = Some(
val.parse()
.map_err(|_| ccsds_parse_error("OPM", "invalid MAN_DV_3"))?,
);
}
k if k.starts_with("USER_DEFINED_") => {
let param_name = k.strip_prefix("USER_DEFINED_").unwrap_or(k);
user_defined.insert(param_name.to_string(), val.to_string());
}
_ => {}
}
}
KVNToken::Comment(text) => {
if in_header {
header_comments.push(text);
} else if man_epoch.is_some() {
maneuver_comments.push(text);
} else if kep_sma.is_some() {
kep_comments.push(text);
} else if sv_epoch.is_some() && sv_vz.is_none() {
state_comments.push(text);
} else {
metadata_comments.push(text);
}
}
KVNToken::Empty | KVNToken::DataLine(_) => {}
}
}
flush_maneuver(
&mut man_epoch,
&mut man_duration,
&mut man_delta_mass,
&mut man_ref_frame,
&mut man_dv1,
&mut man_dv2,
&mut man_dv3,
&mut maneuvers,
&mut maneuver_comments,
);
let header = ODMHeader {
format_version: format_version
.ok_or_else(|| ccsds_missing_field("OPM", "CCSDS_OPM_VERS"))?,
classification,
creation_date: creation_date.ok_or_else(|| ccsds_missing_field("OPM", "CREATION_DATE"))?,
originator: originator.ok_or_else(|| ccsds_missing_field("OPM", "ORIGINATOR"))?,
message_id,
comments: header_comments,
};
let state_vector = OPMStateVector {
epoch: sv_epoch.ok_or_else(|| ccsds_missing_field("OPM", "EPOCH"))?,
position: [
sv_x.ok_or_else(|| ccsds_missing_field("OPM", "X"))?,
sv_y.ok_or_else(|| ccsds_missing_field("OPM", "Y"))?,
sv_z.ok_or_else(|| ccsds_missing_field("OPM", "Z"))?,
],
velocity: [
sv_vx.ok_or_else(|| ccsds_missing_field("OPM", "X_DOT"))?,
sv_vy.ok_or_else(|| ccsds_missing_field("OPM", "Y_DOT"))?,
sv_vz.ok_or_else(|| ccsds_missing_field("OPM", "Z_DOT"))?,
],
comments: state_comments,
};
let keplerian_elements = if let Some(sma) = kep_sma {
Some(OPMKeplerianElements {
semi_major_axis: sma,
eccentricity: kep_ecc.ok_or_else(|| ccsds_missing_field("OPM", "ECCENTRICITY"))?,
inclination: kep_inc.ok_or_else(|| ccsds_missing_field("OPM", "INCLINATION"))?,
ra_of_asc_node: kep_raan.ok_or_else(|| ccsds_missing_field("OPM", "RA_OF_ASC_NODE"))?,
arg_of_pericenter: kep_argp
.ok_or_else(|| ccsds_missing_field("OPM", "ARG_OF_PERICENTER"))?,
true_anomaly: kep_ta,
mean_anomaly: kep_ma,
gm: kep_gm,
comments: kep_comments,
})
} else {
None
};
let spacecraft_parameters = if mass.is_some() || solar_rad_area.is_some() {
Some(CCSDSSpacecraftParameters {
mass,
solar_rad_area,
solar_rad_coeff,
drag_area,
drag_coeff,
comments: spacecraft_comments,
})
} else {
None
};
let covariance = if cov_values.len() == 21 {
let mut vals = [0.0_f64; 21];
vals.copy_from_slice(&cov_values);
let matrix = covariance_from_lower_triangular(&vals, 1e6);
Some(CCSDSCovariance {
epoch: None,
cov_ref_frame,
matrix,
comments: Vec::new(),
})
} else {
None
};
let user_def = if user_defined.is_empty() {
None
} else {
Some(CCSDSUserDefined {
parameters: user_defined,
})
};
Ok(OPM {
header,
metadata: OPMMetadata {
object_name: object_name.ok_or_else(|| ccsds_missing_field("OPM", "OBJECT_NAME"))?,
object_id: object_id.ok_or_else(|| ccsds_missing_field("OPM", "OBJECT_ID"))?,
center_name: center_name.ok_or_else(|| ccsds_missing_field("OPM", "CENTER_NAME"))?,
ref_frame: ref_frame.ok_or_else(|| ccsds_missing_field("OPM", "REF_FRAME"))?,
ref_frame_epoch,
time_system: time_system.ok_or_else(|| ccsds_missing_field("OPM", "TIME_SYSTEM"))?,
comments: metadata_comments,
},
state_vector,
keplerian_elements,
spacecraft_parameters,
covariance,
maneuvers,
user_defined: user_def,
})
}
pub fn parse_cdm(content: &str) -> Result<crate::ccsds::cdm::CDM, BraheError> {
use crate::ccsds::cdm::*;
use crate::ccsds::common::covariance9x9_from_lower_triangular;
#[derive(PartialEq)]
enum CurrentObject {
None,
Object1,
Object2,
}
let mut current_object = CurrentObject::None;
let mut format_version: Option<f64> = None;
let mut classification: Option<String> = None;
let mut creation_date: Option<Epoch> = None;
let mut originator: Option<String> = None;
let mut message_for: Option<String> = None;
let mut message_id: Option<String> = None;
let mut header_comments: Vec<String> = Vec::new();
let mut conjunction_id: Option<String> = None;
let mut tca: Option<Epoch> = None;
let mut miss_distance: Option<f64> = None;
let mut mahalanobis_distance: Option<f64> = None;
let mut relative_speed: Option<f64> = None;
let mut rel_pos_r: Option<f64> = None;
let mut rel_pos_t: Option<f64> = None;
let mut rel_pos_n: Option<f64> = None;
let mut rel_vel_r: Option<f64> = None;
let mut rel_vel_t: Option<f64> = None;
let mut rel_vel_n: Option<f64> = None;
let mut approach_angle: Option<f64> = None;
let mut start_screen_period: Option<Epoch> = None;
let mut stop_screen_period: Option<Epoch> = None;
let mut screen_type: Option<String> = None;
let mut screen_volume_frame: Option<CCSDSRefFrame> = None;
let mut screen_volume_shape: Option<String> = None;
let mut screen_volume_radius: Option<f64> = None;
let mut screen_volume_x: Option<f64> = None;
let mut screen_volume_y: Option<f64> = None;
let mut screen_volume_z: Option<f64> = None;
let mut screen_entry_time: Option<Epoch> = None;
let mut screen_exit_time: Option<Epoch> = None;
let mut screen_pc_threshold: Option<f64> = None;
let mut collision_percentile: Option<Vec<u32>> = None;
let mut collision_probability: Option<f64> = None;
let mut collision_probability_method: Option<String> = None;
let mut collision_max_probability: Option<f64> = None;
let mut collision_max_pc_method: Option<String> = None;
let mut sefi_collision_probability: Option<f64> = None;
let mut sefi_collision_probability_method: Option<String> = None;
let mut sefi_fragmentation_model: Option<String> = None;
let mut previous_message_id: Option<String> = None;
let mut previous_message_epoch: Option<Epoch> = None;
let mut next_message_epoch: Option<Epoch> = None;
let mut rel_comments: Vec<String> = Vec::new();
struct ObjectBuilder {
object: Option<String>,
object_designator: Option<String>,
catalog_name: Option<String>,
object_name: Option<String>,
international_designator: Option<String>,
object_type: Option<String>,
ops_status: Option<String>,
operator_contact_position: Option<String>,
operator_organization: Option<String>,
operator_phone: Option<String>,
operator_email: Option<String>,
ephemeris_name: Option<String>,
odm_msg_link: Option<String>,
adm_msg_link: Option<String>,
obs_before_next_message: Option<String>,
covariance_method: Option<String>,
covariance_source: Option<String>,
maneuverable: Option<String>,
orbit_center: Option<String>,
ref_frame: Option<CCSDSRefFrame>,
alt_cov_type: Option<String>,
alt_cov_ref_frame: Option<CCSDSRefFrame>,
gravity_model: Option<String>,
atmospheric_model: Option<String>,
n_body_perturbations: Option<String>,
solar_rad_pressure: Option<String>,
earth_tides: Option<String>,
intrack_thrust: Option<String>,
metadata_comments: Vec<String>,
time_lastob_start: Option<Epoch>,
time_lastob_end: Option<Epoch>,
recommended_od_span: Option<f64>,
actual_od_span: Option<f64>,
obs_available: Option<u32>,
obs_used: Option<u32>,
tracks_available: Option<u32>,
tracks_used: Option<u32>,
residuals_accepted: Option<f64>,
weighted_rms: Option<f64>,
od_epoch: Option<Epoch>,
od_comments: Vec<String>,
has_od_params: bool,
area_pc: Option<f64>,
area_pc_min: Option<f64>,
area_pc_max: Option<f64>,
area_drg: Option<f64>,
area_srp: Option<f64>,
oeb_parent_frame: Option<String>,
oeb_parent_frame_epoch: Option<Epoch>,
oeb_q1: Option<f64>,
oeb_q2: Option<f64>,
oeb_q3: Option<f64>,
oeb_qc: Option<f64>,
oeb_max: Option<f64>,
oeb_int: Option<f64>,
oeb_min: Option<f64>,
area_along_oeb_max: Option<f64>,
area_along_oeb_int: Option<f64>,
area_along_oeb_min: Option<f64>,
rcs: Option<f64>,
rcs_min: Option<f64>,
rcs_max: Option<f64>,
vm_absolute: Option<f64>,
vm_apparent_min: Option<f64>,
vm_apparent: Option<f64>,
vm_apparent_max: Option<f64>,
reflectance: Option<f64>,
mass: Option<f64>,
hbr: Option<f64>,
cd_area_over_mass: Option<f64>,
cr_area_over_mass: Option<f64>,
thrust_acceleration: Option<f64>,
sedr: Option<f64>,
min_dv: Option<[f64; 3]>,
max_dv: Option<[f64; 3]>,
lead_time_reqd_before_tca: Option<f64>,
apoapsis_altitude: Option<f64>,
periapsis_altitude: Option<f64>,
inclination: Option<f64>,
cov_confidence: Option<f64>,
cov_confidence_method: Option<String>,
add_comments: Vec<String>,
has_add_params: bool,
x: Option<f64>,
y: Option<f64>,
z: Option<f64>,
x_dot: Option<f64>,
y_dot: Option<f64>,
z_dot: Option<f64>,
sv_comments: Vec<String>,
rtn_cov_values: Vec<f64>,
rtn_cov_comments: Vec<String>,
xyz_cov_values: Vec<f64>,
xyz_cov_comments: Vec<String>,
csig3eigvec3: Option<String>,
density_forecast_uncertainty: Option<f64>,
cscale_factor_min: Option<f64>,
cscale_factor: Option<f64>,
cscale_factor_max: Option<f64>,
screening_data_source: Option<String>,
dcp_sensitivity_vector_position: Option<[f64; 3]>,
dcp_sensitivity_vector_velocity: Option<[f64; 3]>,
acm_comments: Vec<String>,
has_acm: bool,
data_comments: Vec<String>,
in_xyz_cov: bool,
}
impl ObjectBuilder {
fn new() -> Self {
Self {
object: None,
object_designator: None,
catalog_name: None,
object_name: None,
international_designator: None,
object_type: None,
ops_status: None,
operator_contact_position: None,
operator_organization: None,
operator_phone: None,
operator_email: None,
ephemeris_name: None,
odm_msg_link: None,
adm_msg_link: None,
obs_before_next_message: None,
covariance_method: None,
covariance_source: None,
maneuverable: None,
orbit_center: None,
ref_frame: None,
alt_cov_type: None,
alt_cov_ref_frame: None,
gravity_model: None,
atmospheric_model: None,
n_body_perturbations: None,
solar_rad_pressure: None,
earth_tides: None,
intrack_thrust: None,
metadata_comments: Vec::new(),
time_lastob_start: None,
time_lastob_end: None,
recommended_od_span: None,
actual_od_span: None,
obs_available: None,
obs_used: None,
tracks_available: None,
tracks_used: None,
residuals_accepted: None,
weighted_rms: None,
od_epoch: None,
od_comments: Vec::new(),
has_od_params: false,
area_pc: None,
area_pc_min: None,
area_pc_max: None,
area_drg: None,
area_srp: None,
oeb_parent_frame: None,
oeb_parent_frame_epoch: None,
oeb_q1: None,
oeb_q2: None,
oeb_q3: None,
oeb_qc: None,
oeb_max: None,
oeb_int: None,
oeb_min: None,
area_along_oeb_max: None,
area_along_oeb_int: None,
area_along_oeb_min: None,
rcs: None,
rcs_min: None,
rcs_max: None,
vm_absolute: None,
vm_apparent_min: None,
vm_apparent: None,
vm_apparent_max: None,
reflectance: None,
mass: None,
hbr: None,
cd_area_over_mass: None,
cr_area_over_mass: None,
thrust_acceleration: None,
sedr: None,
min_dv: None,
max_dv: None,
lead_time_reqd_before_tca: None,
apoapsis_altitude: None,
periapsis_altitude: None,
inclination: None,
cov_confidence: None,
cov_confidence_method: None,
add_comments: Vec::new(),
has_add_params: false,
x: None,
y: None,
z: None,
x_dot: None,
y_dot: None,
z_dot: None,
sv_comments: Vec::new(),
rtn_cov_values: Vec::new(),
rtn_cov_comments: Vec::new(),
xyz_cov_values: Vec::new(),
xyz_cov_comments: Vec::new(),
csig3eigvec3: None,
density_forecast_uncertainty: None,
cscale_factor_min: None,
cscale_factor: None,
cscale_factor_max: None,
screening_data_source: None,
dcp_sensitivity_vector_position: None,
dcp_sensitivity_vector_velocity: None,
acm_comments: Vec::new(),
has_acm: false,
data_comments: Vec::new(),
in_xyz_cov: false,
}
}
}
let mut obj1 = ObjectBuilder::new();
let mut obj2 = ObjectBuilder::new();
let mut user_defined: HashMap<String, String> = HashMap::new();
let utc = CCSDSTimeSystem::UTC;
let parse_f64 = |val: &str| -> Result<f64, BraheError> {
strip_units(val)
.parse()
.map_err(|_| ccsds_parse_error("CDM", &format!("invalid numeric value '{}'", val)))
};
let parse_u32 = |val: &str| -> Result<u32, BraheError> {
strip_units(val)
.parse()
.map_err(|_| ccsds_parse_error("CDM", &format!("invalid integer value '{}'", val)))
};
fn rtn_cov_index(key: &str) -> Option<(usize, usize)> {
match key {
"CR_R" => Some((0, 0)),
"CT_R" => Some((1, 0)),
"CT_T" => Some((1, 1)),
"CN_R" => Some((2, 0)),
"CN_T" => Some((2, 1)),
"CN_N" => Some((2, 2)),
"CRDOT_R" => Some((3, 0)),
"CRDOT_T" => Some((3, 1)),
"CRDOT_N" => Some((3, 2)),
"CRDOT_RDOT" => Some((3, 3)),
"CTDOT_R" => Some((4, 0)),
"CTDOT_T" => Some((4, 1)),
"CTDOT_N" => Some((4, 2)),
"CTDOT_RDOT" => Some((4, 3)),
"CTDOT_TDOT" => Some((4, 4)),
"CNDOT_R" => Some((5, 0)),
"CNDOT_T" => Some((5, 1)),
"CNDOT_N" => Some((5, 2)),
"CNDOT_RDOT" => Some((5, 3)),
"CNDOT_TDOT" => Some((5, 4)),
"CNDOT_NDOT" => Some((5, 5)),
"CDRG_R" => Some((6, 0)),
"CDRG_T" => Some((6, 1)),
"CDRG_N" => Some((6, 2)),
"CDRG_RDOT" => Some((6, 3)),
"CDRG_TDOT" => Some((6, 4)),
"CDRG_NDOT" => Some((6, 5)),
"CDRG_DRG" => Some((6, 6)),
"CSRP_R" => Some((7, 0)),
"CSRP_T" => Some((7, 1)),
"CSRP_N" => Some((7, 2)),
"CSRP_RDOT" => Some((7, 3)),
"CSRP_TDOT" => Some((7, 4)),
"CSRP_NDOT" => Some((7, 5)),
"CSRP_DRG" => Some((7, 6)),
"CSRP_SRP" => Some((7, 7)),
"CTHR_R" => Some((8, 0)),
"CTHR_T" => Some((8, 1)),
"CTHR_N" => Some((8, 2)),
"CTHR_RDOT" => Some((8, 3)),
"CTHR_TDOT" => Some((8, 4)),
"CTHR_NDOT" => Some((8, 5)),
"CTHR_DRG" => Some((8, 6)),
"CTHR_SRP" => Some((8, 7)),
"CTHR_THR" => Some((8, 8)),
_ => None,
}
}
fn xyz_cov_index(key: &str) -> Option<(usize, usize)> {
match key {
"CX_X" => Some((0, 0)),
"CY_X" => Some((1, 0)),
"CY_Y" => Some((1, 1)),
"CZ_X" => Some((2, 0)),
"CZ_Y" => Some((2, 1)),
"CZ_Z" => Some((2, 2)),
"CXDOT_X" => Some((3, 0)),
"CXDOT_Y" => Some((3, 1)),
"CXDOT_Z" => Some((3, 2)),
"CXDOT_XDOT" => Some((3, 3)),
"CYDOT_X" => Some((4, 0)),
"CYDOT_Y" => Some((4, 1)),
"CYDOT_Z" => Some((4, 2)),
"CYDOT_XDOT" => Some((4, 3)),
"CYDOT_YDOT" => Some((4, 4)),
"CZDOT_X" => Some((5, 0)),
"CZDOT_Y" => Some((5, 1)),
"CZDOT_Z" => Some((5, 2)),
"CZDOT_XDOT" => Some((5, 3)),
"CZDOT_YDOT" => Some((5, 4)),
"CZDOT_ZDOT" => Some((5, 5)),
"CDRG_X" => Some((6, 0)),
"CDRG_Y" => Some((6, 1)),
"CDRG_Z" => Some((6, 2)),
"CDRG_XDOT" => Some((6, 3)),
"CDRG_YDOT" => Some((6, 4)),
"CDRG_ZDOT" => Some((6, 5)),
"CSRP_X" => Some((7, 0)),
"CSRP_Y" => Some((7, 1)),
"CSRP_Z" => Some((7, 2)),
"CSRP_XDOT" => Some((7, 3)),
"CSRP_YDOT" => Some((7, 4)),
"CSRP_ZDOT" => Some((7, 5)),
"CTHR_X" => Some((8, 0)),
"CTHR_Y" => Some((8, 1)),
"CTHR_Z" => Some((8, 2)),
"CTHR_XDOT" => Some((8, 3)),
"CTHR_YDOT" => Some((8, 4)),
"CTHR_ZDOT" => Some((8, 5)),
_ => None,
}
}
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(comment_text) = line.strip_prefix("COMMENT") {
let comment = comment_text.trim().to_string();
match current_object {
CurrentObject::None => {
if tca.is_some() {
rel_comments.push(comment);
} else {
header_comments.push(comment);
}
}
CurrentObject::Object1 => obj1.data_comments.push(comment),
CurrentObject::Object2 => obj2.data_comments.push(comment),
}
continue;
}
let eq_pos = match line.find('=') {
Some(pos) => pos,
None => continue,
};
let key = line[..eq_pos].trim();
let raw_val = line[eq_pos + 1..].trim();
let val = strip_units(raw_val);
let obj = match current_object {
CurrentObject::Object1 => &mut obj1,
CurrentObject::Object2 => &mut obj2,
CurrentObject::None => {
match key {
"CCSDS_CDM_VERS" => {
format_version = Some(parse_f64(val)?);
}
"CLASSIFICATION" => {
classification = Some(val.trim_matches('"').to_string());
}
"CREATION_DATE" => {
creation_date = Some(parse_ccsds_datetime(val, &utc)?);
}
"ORIGINATOR" => {
originator = Some(val.to_string());
}
"MESSAGE_FOR" => {
message_for = Some(val.to_string());
}
"MESSAGE_ID" => {
message_id = Some(val.to_string());
}
"CONJUNCTION_ID" => {
conjunction_id = Some(val.to_string());
}
"TCA" => {
tca = Some(parse_ccsds_datetime(val, &utc)?);
}
"MISS_DISTANCE" => {
miss_distance = Some(parse_f64(val)?);
}
"MAHALANOBIS_DISTANCE" => {
mahalanobis_distance = Some(parse_f64(val)?);
}
"RELATIVE_SPEED" => {
relative_speed = Some(parse_f64(val)?);
}
"RELATIVE_POSITION_R" => {
rel_pos_r = Some(parse_f64(val)?);
}
"RELATIVE_POSITION_T" => {
rel_pos_t = Some(parse_f64(val)?);
}
"RELATIVE_POSITION_N" => {
rel_pos_n = Some(parse_f64(val)?);
}
"RELATIVE_VELOCITY_R" => {
rel_vel_r = Some(parse_f64(val)?);
}
"RELATIVE_VELOCITY_T" => {
rel_vel_t = Some(parse_f64(val)?);
}
"RELATIVE_VELOCITY_N" => {
rel_vel_n = Some(parse_f64(val)?);
}
"APPROACH_ANGLE" => {
approach_angle = Some(parse_f64(val)?);
}
"START_SCREEN_PERIOD" => {
start_screen_period = Some(parse_ccsds_datetime(val, &utc)?);
}
"STOP_SCREEN_PERIOD" => {
stop_screen_period = Some(parse_ccsds_datetime(val, &utc)?);
}
"SCREEN_TYPE" => {
screen_type = Some(val.to_string());
}
"SCREEN_VOLUME_FRAME" => {
screen_volume_frame = Some(CCSDSRefFrame::parse(val));
}
"SCREEN_VOLUME_SHAPE" => {
screen_volume_shape = Some(val.to_string());
}
"SCREEN_VOLUME_RADIUS" => {
screen_volume_radius = Some(parse_f64(val)?);
}
"SCREEN_VOLUME_X" => {
screen_volume_x = Some(parse_f64(val)?);
}
"SCREEN_VOLUME_Y" => {
screen_volume_y = Some(parse_f64(val)?);
}
"SCREEN_VOLUME_Z" => {
screen_volume_z = Some(parse_f64(val)?);
}
"SCREEN_ENTRY_TIME" => {
screen_entry_time = Some(parse_ccsds_datetime(val, &utc)?);
}
"SCREEN_EXIT_TIME" => {
screen_exit_time = Some(parse_ccsds_datetime(val, &utc)?);
}
"SCREEN_PC_THRESHOLD" => {
screen_pc_threshold = Some(parse_f64(val)?);
}
"COLLISION_PERCENTILE" => {
let parts: Result<Vec<u32>, _> =
val.split_whitespace().map(|s| s.parse::<u32>()).collect();
collision_percentile = Some(parts.map_err(|_| {
ccsds_parse_error("CDM", "invalid COLLISION_PERCENTILE")
})?);
}
"COLLISION_PROBABILITY" => {
collision_probability = Some(parse_f64(val)?);
}
"COLLISION_PROBABILITY_METHOD" => {
collision_probability_method = Some(val.to_string());
}
"COLLISION_MAX_PROBABILITY" => {
collision_max_probability = Some(parse_f64(val)?);
}
"COLLISION_MAX_PC_METHOD" => {
collision_max_pc_method = Some(val.to_string());
}
"SEFI_COLLISION_PROBABILITY" => {
sefi_collision_probability = Some(parse_f64(val)?);
}
"SEFI_COLLISION_PROBABILITY_METHOD" => {
sefi_collision_probability_method = Some(val.to_string());
}
"SEFI_FRAGMENTATION_MODEL" => {
sefi_fragmentation_model = Some(val.to_string());
}
"PREVIOUS_MESSAGE_ID" => {
previous_message_id = Some(val.to_string());
}
"PREVIOUS_MESSAGE_EPOCH" => {
previous_message_epoch = Some(parse_ccsds_datetime(val, &utc)?);
}
"NEXT_MESSAGE_EPOCH" => {
next_message_epoch = Some(parse_ccsds_datetime(val, &utc)?);
}
"OBJECT" => match val {
"OBJECT1" => {
current_object = CurrentObject::Object1;
obj1.object = Some("OBJECT1".to_string());
}
"OBJECT2" => {
current_object = CurrentObject::Object2;
obj2.object = Some("OBJECT2".to_string());
}
_ => {
return Err(ccsds_parse_error(
"CDM",
&format!("unexpected OBJECT value '{}'", val),
));
}
},
k if k.starts_with("USER_DEFINED_") => {
let ud_key = k.strip_prefix("USER_DEFINED_").unwrap_or(k);
user_defined.insert(ud_key.to_string(), val.to_string());
}
_ => {} }
continue;
}
};
match key {
"OBJECT" => match val {
"OBJECT1" => {
current_object = CurrentObject::Object1;
obj1.object = Some("OBJECT1".to_string());
}
"OBJECT2" => {
current_object = CurrentObject::Object2;
obj2.object = Some("OBJECT2".to_string());
}
_ => {
return Err(ccsds_parse_error(
"CDM",
&format!("unexpected OBJECT value '{}'", val),
));
}
},
"OBJECT_DESIGNATOR" => {
obj.object_designator = Some(val.to_string());
}
"CATALOG_NAME" => {
obj.catalog_name = Some(val.to_string());
}
"OBJECT_NAME" => {
obj.object_name = Some(val.to_string());
}
"INTERNATIONAL_DESIGNATOR" => {
obj.international_designator = Some(val.to_string());
}
"OBJECT_TYPE" => {
obj.object_type = Some(val.to_string());
}
"OPS_STATUS" => {
obj.ops_status = Some(val.to_string());
}
"OPERATOR_CONTACT_POSITION" => {
obj.operator_contact_position = Some(val.to_string());
}
"OPERATOR_ORGANIZATION" => {
obj.operator_organization = Some(val.to_string());
}
"OPERATOR_PHONE" => {
obj.operator_phone = Some(val.to_string());
}
"OPERATOR_EMAIL" => {
obj.operator_email = Some(val.to_string());
}
"EPHEMERIS_NAME" => {
obj.ephemeris_name = Some(val.to_string());
}
"ODM_MSG_LINK" => {
obj.odm_msg_link = Some(val.to_string());
}
"ADM_MSG_LINK" => {
obj.adm_msg_link = Some(val.to_string());
}
"OBS_BEFORE_NEXT_MESSAGE" => {
obj.obs_before_next_message = Some(val.to_string());
}
"COVARIANCE_METHOD" => {
obj.covariance_method = Some(val.to_string());
}
"COVARIANCE_SOURCE" => {
obj.covariance_source = Some(val.to_string());
}
"MANEUVERABLE" => {
obj.maneuverable = Some(val.to_string());
}
"ORBIT_CENTER" => {
obj.orbit_center = Some(val.to_string());
}
"REF_FRAME" => {
obj.ref_frame = Some(CCSDSRefFrame::parse(val));
}
"ALT_COV_TYPE" => {
obj.alt_cov_type = Some(val.to_string());
}
"ALT_COV_REF_FRAME" => {
obj.alt_cov_ref_frame = Some(CCSDSRefFrame::parse(val));
}
"GRAVITY_MODEL" => {
obj.gravity_model = Some(val.to_string());
}
"ATMOSPHERIC_MODEL" => {
obj.atmospheric_model = Some(val.to_string());
}
"N_BODY_PERTURBATIONS" => {
obj.n_body_perturbations = Some(val.to_string());
}
"SOLAR_RAD_PRESSURE" => {
obj.solar_rad_pressure = Some(val.to_string());
}
"EARTH_TIDES" => {
obj.earth_tides = Some(val.to_string());
}
"INTRACK_THRUST" => {
obj.intrack_thrust = Some(val.to_string());
}
"TIME_LASTOB_START" => {
obj.time_lastob_start = Some(parse_ccsds_datetime(val, &utc)?);
obj.has_od_params = true;
}
"TIME_LASTOB_END" => {
obj.time_lastob_end = Some(parse_ccsds_datetime(val, &utc)?);
obj.has_od_params = true;
}
"RECOMMENDED_OD_SPAN" => {
obj.recommended_od_span = Some(parse_f64(val)?);
obj.has_od_params = true;
}
"ACTUAL_OD_SPAN" => {
obj.actual_od_span = Some(parse_f64(val)?);
obj.has_od_params = true;
}
"OBS_AVAILABLE" => {
obj.obs_available = Some(parse_u32(val)?);
obj.has_od_params = true;
}
"OBS_USED" => {
obj.obs_used = Some(parse_u32(val)?);
obj.has_od_params = true;
}
"TRACKS_AVAILABLE" => {
obj.tracks_available = Some(parse_u32(val)?);
obj.has_od_params = true;
}
"TRACKS_USED" => {
obj.tracks_used = Some(parse_u32(val)?);
obj.has_od_params = true;
}
"RESIDUALS_ACCEPTED" => {
obj.residuals_accepted = Some(parse_f64(val)?);
obj.has_od_params = true;
}
"WEIGHTED_RMS" => {
obj.weighted_rms = Some(parse_f64(val)?);
obj.has_od_params = true;
}
"OD_EPOCH" => {
obj.od_epoch = Some(parse_ccsds_datetime(val, &utc)?);
obj.has_od_params = true;
}
"AREA_PC" => {
obj.area_pc = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_PC_MIN" => {
obj.area_pc_min = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_PC_MAX" => {
obj.area_pc_max = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_DRG" => {
obj.area_drg = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_SRP" => {
obj.area_srp = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_PARENT_FRAME" => {
obj.oeb_parent_frame = Some(val.to_string());
obj.has_add_params = true;
}
"OEB_PARENT_FRAME_EPOCH" => {
obj.oeb_parent_frame_epoch = Some(parse_ccsds_datetime(val, &utc)?);
obj.has_add_params = true;
}
"OEB_Q1" => {
obj.oeb_q1 = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_Q2" => {
obj.oeb_q2 = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_Q3" => {
obj.oeb_q3 = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_QC" => {
obj.oeb_qc = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_MAX" => {
obj.oeb_max = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_INT" => {
obj.oeb_int = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"OEB_MIN" => {
obj.oeb_min = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_ALONG_OEB_MAX" => {
obj.area_along_oeb_max = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_ALONG_OEB_INT" => {
obj.area_along_oeb_int = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"AREA_ALONG_OEB_MIN" => {
obj.area_along_oeb_min = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"RCS" => {
obj.rcs = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"RCS_MIN" => {
obj.rcs_min = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"RCS_MAX" => {
obj.rcs_max = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"VM_ABSOLUTE" => {
obj.vm_absolute = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"VM_APPARENT_MIN" => {
obj.vm_apparent_min = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"VM_APPARENT" => {
obj.vm_apparent = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"VM_APPARENT_MAX" => {
obj.vm_apparent_max = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"REFLECTANCE" => {
obj.reflectance = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"MASS" => {
obj.mass = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"HBR" => {
obj.hbr = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"CD_AREA_OVER_MASS" => {
obj.cd_area_over_mass = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"CR_AREA_OVER_MASS" => {
obj.cr_area_over_mass = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"THRUST_ACCELERATION" => {
obj.thrust_acceleration = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"SEDR" => {
obj.sedr = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"MIN_DV" => {
let parts: Vec<f64> = val
.split_whitespace()
.map(|s| s.parse().unwrap_or(0.0))
.collect();
if parts.len() == 3 {
obj.min_dv = Some([parts[0], parts[1], parts[2]]);
}
obj.has_add_params = true;
}
"MAX_DV" => {
let parts: Vec<f64> = val
.split_whitespace()
.map(|s| s.parse().unwrap_or(0.0))
.collect();
if parts.len() == 3 {
obj.max_dv = Some([parts[0], parts[1], parts[2]]);
}
obj.has_add_params = true;
}
"LEAD_TIME_REQD_BEFORE_TCA" => {
obj.lead_time_reqd_before_tca = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"APOAPSIS_ALTITUDE" => {
obj.apoapsis_altitude = Some(parse_f64(val)? * 1e3);
obj.has_add_params = true;
} "PERIAPSIS_ALTITUDE" => {
obj.periapsis_altitude = Some(parse_f64(val)? * 1e3);
obj.has_add_params = true;
} "INCLINATION" => {
obj.inclination = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"COV_CONFIDENCE" => {
obj.cov_confidence = Some(parse_f64(val)?);
obj.has_add_params = true;
}
"COV_CONFIDENCE_METHOD" => {
obj.cov_confidence_method = Some(val.to_string());
obj.has_add_params = true;
}
"X" => {
obj.x = Some(parse_f64(val)? * 1e3);
}
"Y" => {
obj.y = Some(parse_f64(val)? * 1e3);
}
"Z" => {
obj.z = Some(parse_f64(val)? * 1e3);
}
"X_DOT" => {
obj.x_dot = Some(parse_f64(val)? * 1e3);
}
"Y_DOT" => {
obj.y_dot = Some(parse_f64(val)? * 1e3);
}
"Z_DOT" => {
obj.z_dot = Some(parse_f64(val)? * 1e3);
}
"DENSITY_FORECAST_UNCERTAINTY" => {
obj.density_forecast_uncertainty = Some(parse_f64(val)?);
obj.has_acm = true;
}
"CSCALE_FACTOR_MIN" => {
obj.cscale_factor_min = Some(parse_f64(val)?);
obj.has_acm = true;
}
"CSCALE_FACTOR" => {
obj.cscale_factor = Some(parse_f64(val)?);
obj.has_acm = true;
}
"CSCALE_FACTOR_MAX" => {
obj.cscale_factor_max = Some(parse_f64(val)?);
obj.has_acm = true;
}
"SCREENING_DATA_SOURCE" => {
obj.screening_data_source = Some(val.to_string());
obj.has_acm = true;
}
"DCP_SENSITIVITY_VECTOR_POSITION" => {
let parts: Vec<f64> = val
.split_whitespace()
.map(|s| s.parse().unwrap_or(0.0))
.collect();
if parts.len() == 3 {
obj.dcp_sensitivity_vector_position = Some([parts[0], parts[1], parts[2]]);
}
obj.has_acm = true;
}
"DCP_SENSITIVITY_VECTOR_VELOCITY" => {
let parts: Vec<f64> = val
.split_whitespace()
.map(|s| s.parse().unwrap_or(0.0))
.collect();
if parts.len() == 3 {
obj.dcp_sensitivity_vector_velocity = Some([parts[0], parts[1], parts[2]]);
}
obj.has_acm = true;
}
"CSIG3EIGVEC3" => {
obj.csig3eigvec3 = Some(val.to_string());
}
k if k.starts_with("USER_DEFINED_") => {
let ud_key = k.strip_prefix("USER_DEFINED_").unwrap_or(k);
user_defined.insert(ud_key.to_string(), val.to_string());
}
k => {
if let Some((_row, _col)) = xyz_cov_index(k) {
let v = parse_f64(val)?;
obj.xyz_cov_values.push(v);
obj.in_xyz_cov = true; } else if let Some((_row, _col)) = rtn_cov_index(k) {
let v = parse_f64(val)?;
if k == "CR_R" {
obj.in_xyz_cov = false;
}
if obj.in_xyz_cov {
obj.xyz_cov_values.push(v);
} else {
obj.rtn_cov_values.push(v);
}
}
}
}
}
let build_object = |obj: ObjectBuilder, label: &str| -> Result<CDMObject, BraheError> {
let obj_label = obj.object.clone().unwrap_or_else(|| label.to_string());
let state_vector = CDMStateVector {
position: [
obj.x
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} X", obj_label)))?,
obj.y
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} Y", obj_label)))?,
obj.z
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} Z", obj_label)))?,
],
velocity: [
obj.x_dot
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} X_DOT", obj_label)))?,
obj.y_dot
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} Y_DOT", obj_label)))?,
obj.z_dot
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} Z_DOT", obj_label)))?,
],
comments: obj.sv_comments,
};
let rtn_covariance = if obj.rtn_cov_values.is_empty() {
return Err(ccsds_missing_field(
"CDM",
&format!("{} RTN covariance", obj_label),
));
} else {
let (matrix, dim) = covariance9x9_from_lower_triangular(&obj.rtn_cov_values)?;
CDMRTNCovariance {
matrix,
dimension: dim,
comments: obj.rtn_cov_comments,
}
};
let xyz_covariance = if obj.xyz_cov_values.is_empty() {
None
} else {
let (matrix, dim) = covariance9x9_from_lower_triangular(&obj.xyz_cov_values)?;
Some(CDMXYZCovariance {
matrix,
dimension: dim,
comments: obj.xyz_cov_comments,
})
};
let od_parameters = if obj.has_od_params {
Some(CDMODParameters {
time_lastob_start: obj.time_lastob_start,
time_lastob_end: obj.time_lastob_end,
recommended_od_span: obj.recommended_od_span,
actual_od_span: obj.actual_od_span,
obs_available: obj.obs_available,
obs_used: obj.obs_used,
tracks_available: obj.tracks_available,
tracks_used: obj.tracks_used,
residuals_accepted: obj.residuals_accepted,
weighted_rms: obj.weighted_rms,
od_epoch: obj.od_epoch,
comments: obj.od_comments,
})
} else {
None
};
let additional_parameters = if obj.has_add_params {
Some(CDMAdditionalParameters {
area_pc: obj.area_pc,
area_pc_min: obj.area_pc_min,
area_pc_max: obj.area_pc_max,
area_drg: obj.area_drg,
area_srp: obj.area_srp,
oeb_parent_frame: obj.oeb_parent_frame,
oeb_parent_frame_epoch: obj.oeb_parent_frame_epoch,
oeb_q1: obj.oeb_q1,
oeb_q2: obj.oeb_q2,
oeb_q3: obj.oeb_q3,
oeb_qc: obj.oeb_qc,
oeb_max: obj.oeb_max,
oeb_int: obj.oeb_int,
oeb_min: obj.oeb_min,
area_along_oeb_max: obj.area_along_oeb_max,
area_along_oeb_int: obj.area_along_oeb_int,
area_along_oeb_min: obj.area_along_oeb_min,
rcs: obj.rcs,
rcs_min: obj.rcs_min,
rcs_max: obj.rcs_max,
vm_absolute: obj.vm_absolute,
vm_apparent_min: obj.vm_apparent_min,
vm_apparent: obj.vm_apparent,
vm_apparent_max: obj.vm_apparent_max,
reflectance: obj.reflectance,
mass: obj.mass,
hbr: obj.hbr,
cd_area_over_mass: obj.cd_area_over_mass,
cr_area_over_mass: obj.cr_area_over_mass,
thrust_acceleration: obj.thrust_acceleration,
sedr: obj.sedr,
min_dv: obj.min_dv,
max_dv: obj.max_dv,
lead_time_reqd_before_tca: obj.lead_time_reqd_before_tca,
apoapsis_altitude: obj.apoapsis_altitude,
periapsis_altitude: obj.periapsis_altitude,
inclination: obj.inclination,
cov_confidence: obj.cov_confidence,
cov_confidence_method: obj.cov_confidence_method,
comments: obj.add_comments,
})
} else {
None
};
let additional_covariance_metadata = if obj.has_acm {
Some(CDMAdditionalCovarianceMetadata {
density_forecast_uncertainty: obj.density_forecast_uncertainty,
cscale_factor_min: obj.cscale_factor_min,
cscale_factor: obj.cscale_factor,
cscale_factor_max: obj.cscale_factor_max,
screening_data_source: obj.screening_data_source,
dcp_sensitivity_vector_position: obj.dcp_sensitivity_vector_position,
dcp_sensitivity_vector_velocity: obj.dcp_sensitivity_vector_velocity,
comments: obj.acm_comments,
})
} else {
None
};
let metadata = CDMObjectMetadata {
object: obj.object.unwrap_or_else(|| label.to_string()),
object_designator: obj.object_designator.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} OBJECT_DESIGNATOR", obj_label))
})?,
catalog_name: obj.catalog_name.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} CATALOG_NAME", obj_label))
})?,
object_name: obj
.object_name
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} OBJECT_NAME", obj_label)))?,
international_designator: obj.international_designator.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} INTERNATIONAL_DESIGNATOR", obj_label))
})?,
object_type: obj.object_type,
ops_status: obj.ops_status,
operator_contact_position: obj.operator_contact_position,
operator_organization: obj.operator_organization,
operator_phone: obj.operator_phone,
operator_email: obj.operator_email,
ephemeris_name: obj.ephemeris_name.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} EPHEMERIS_NAME", obj_label))
})?,
odm_msg_link: obj.odm_msg_link,
adm_msg_link: obj.adm_msg_link,
obs_before_next_message: obj.obs_before_next_message,
covariance_method: obj.covariance_method.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} COVARIANCE_METHOD", obj_label))
})?,
covariance_source: obj.covariance_source,
maneuverable: obj.maneuverable.ok_or_else(|| {
ccsds_missing_field("CDM", &format!("{} MANEUVERABLE", obj_label))
})?,
orbit_center: obj.orbit_center,
ref_frame: obj
.ref_frame
.ok_or_else(|| ccsds_missing_field("CDM", &format!("{} REF_FRAME", obj_label)))?,
alt_cov_type: obj.alt_cov_type,
alt_cov_ref_frame: obj.alt_cov_ref_frame,
gravity_model: obj.gravity_model,
atmospheric_model: obj.atmospheric_model,
n_body_perturbations: obj.n_body_perturbations,
solar_rad_pressure: obj.solar_rad_pressure,
earth_tides: obj.earth_tides,
intrack_thrust: obj.intrack_thrust,
comments: obj.metadata_comments,
};
Ok(CDMObject {
metadata,
data: CDMObjectData {
od_parameters,
additional_parameters,
state_vector,
rtn_covariance,
xyz_covariance,
additional_covariance_metadata,
csig3eigvec3: obj.csig3eigvec3,
comments: obj.data_comments,
},
})
};
let format_version =
format_version.ok_or_else(|| ccsds_missing_field("CDM", "CCSDS_CDM_VERS"))?;
let creation_date = creation_date.ok_or_else(|| ccsds_missing_field("CDM", "CREATION_DATE"))?;
let originator = originator.ok_or_else(|| ccsds_missing_field("CDM", "ORIGINATOR"))?;
let message_id = message_id.unwrap_or_default();
let tca = tca.ok_or_else(|| ccsds_missing_field("CDM", "TCA"))?;
let miss_distance = miss_distance.ok_or_else(|| ccsds_missing_field("CDM", "MISS_DISTANCE"))?;
let object1 = build_object(obj1, "OBJECT1")?;
let object2 = build_object(obj2, "OBJECT2")?;
let user_defined = if user_defined.is_empty() {
None
} else {
Some(CCSDSUserDefined {
parameters: user_defined,
})
};
Ok(CDM {
header: CDMHeader {
format_version,
classification,
creation_date,
originator,
message_for,
message_id,
comments: header_comments,
},
relative_metadata: CDMRelativeMetadata {
conjunction_id,
tca,
miss_distance,
mahalanobis_distance,
relative_speed,
relative_position_r: rel_pos_r,
relative_position_t: rel_pos_t,
relative_position_n: rel_pos_n,
relative_velocity_r: rel_vel_r,
relative_velocity_t: rel_vel_t,
relative_velocity_n: rel_vel_n,
approach_angle,
start_screen_period,
stop_screen_period,
screen_type,
screen_volume_frame,
screen_volume_shape,
screen_volume_radius,
screen_volume_x,
screen_volume_y,
screen_volume_z,
screen_entry_time,
screen_exit_time,
screen_pc_threshold,
collision_percentile,
collision_probability,
collision_probability_method,
collision_max_probability,
collision_max_pc_method,
sefi_collision_probability,
sefi_collision_probability_method,
sefi_fragmentation_model,
previous_message_id,
previous_message_epoch,
next_message_epoch,
comments: rel_comments,
},
object1,
object2,
user_defined,
})
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_tokenize_empty() {
match tokenize_line("") {
KVNToken::Empty => {}
_ => panic!("Expected Empty"),
}
}
#[test]
fn test_tokenize_comment() {
match tokenize_line("COMMENT This is a test") {
KVNToken::Comment(text) => assert_eq!(text, "This is a test"),
_ => panic!("Expected Comment"),
}
}
#[test]
fn test_tokenize_key_value() {
match tokenize_line("OBJECT_NAME = ISS") {
KVNToken::KeyValue { key, value } => {
assert_eq!(key, "OBJECT_NAME");
assert_eq!(value, "ISS");
}
_ => panic!("Expected KeyValue"),
}
}
#[test]
fn test_tokenize_data_line() {
match tokenize_line(
"2017-04-11T22:31:43.121856 2906.275 4076.358 4561.364 -6.879 1.450 3.081",
) {
KVNToken::DataLine(parts) => {
assert_eq!(parts.len(), 7);
assert_eq!(parts[0], "2017-04-11T22:31:43.121856");
}
_ => panic!("Expected DataLine"),
}
}
#[test]
fn test_parse_oem_example1() {
let content = std::fs::read_to_string("test_assets/ccsds/oem/OEMExample1.txt").unwrap();
let oem = parse_oem(&content).unwrap();
assert!((oem.header.format_version - 3.0).abs() < 1e-10);
assert_eq!(
oem.header.classification.as_deref(),
Some("public, test-data")
);
assert_eq!(oem.header.originator, "NASA/JPL");
assert_eq!(oem.segments.len(), 3);
let seg0 = &oem.segments[0];
assert_eq!(seg0.metadata.object_name, "MARS GLOBAL SURVEYOR");
assert_eq!(seg0.metadata.object_id, "1996-062A");
assert_eq!(seg0.metadata.center_name, "MARS BARYCENTER");
assert_eq!(seg0.metadata.ref_frame, CCSDSRefFrame::J2000);
assert_eq!(seg0.metadata.time_system, CCSDSTimeSystem::UTC);
assert_eq!(seg0.metadata.interpolation.as_deref(), Some("HERMITE"));
assert_eq!(seg0.metadata.interpolation_degree, Some(7));
assert_eq!(seg0.states.len(), 4);
assert!((seg0.states[0].position[0] - 2789.619 * 1000.0).abs() < 1e-3);
assert!((seg0.states[0].position[1] - (-280.045) * 1000.0).abs() < 1e-3);
assert!((seg0.states[0].position[2] - (-1746.755) * 1000.0).abs() < 1e-3);
assert!((seg0.states[0].velocity[0] - 4.73372 * 1000.0).abs() < 1e-3);
assert!((seg0.states[0].velocity[1] - (-2.49586) * 1000.0).abs() < 1e-3);
assert!((seg0.states[0].velocity[2] - (-1.04195) * 1000.0).abs() < 1e-3);
assert!(seg0.covariances.is_empty());
let seg1 = &oem.segments[1];
assert_eq!(seg1.states.len(), 4);
assert_eq!(seg1.covariances.len(), 1);
let cov = &seg1.covariances[0];
assert_eq!(cov.cov_ref_frame.as_ref().unwrap(), &CCSDSRefFrame::EME2000);
assert!((cov.matrix[(0, 0)] - 3.3313494e-04 * 1e6).abs() < 1e-4);
assert!((cov.matrix[(1, 0)] - 4.6189273e-04 * 1e6).abs() < 1e-4);
assert_eq!(cov.matrix[(0, 1)], cov.matrix[(1, 0)]);
let seg2 = &oem.segments[2];
assert_eq!(seg2.covariances.len(), 2);
assert_eq!(
seg2.covariances[0].cov_ref_frame.as_ref().unwrap(),
&CCSDSRefFrame::RTN
);
assert_eq!(
seg2.covariances[1].cov_ref_frame.as_ref().unwrap(),
&CCSDSRefFrame::EME2000
);
assert_eq!(seg0.comments.len(), 2);
assert!(seg0.comments[0].contains("M.R. Somebody"));
}
#[test]
fn test_parse_oem_example2_unsupported_time_system() {
let content = std::fs::read_to_string("test_assets/ccsds/oem/OEMExample2.txt").unwrap();
let result = parse_oem(&content);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("MRT"),
"Error should mention unsupported time system MRT: {}",
err_msg
);
}
#[test]
fn test_parse_ccsds_datetime_doy_utc() {
use crate::ccsds::common::{CCSDSTimeSystem, parse_ccsds_datetime};
let epoch = parse_ccsds_datetime("1996-200T16:00:00", &CCSDSTimeSystem::UTC).unwrap();
let (y, m, d, h, min, s, _ns) = epoch.to_datetime();
assert_eq!(y, 1996);
assert_eq!(m, 7);
assert_eq!(d, 18);
assert_eq!(h, 16);
assert_eq!(min, 0);
assert!((s - 0.0).abs() < 1e-6);
}
#[test]
fn test_parse_oem_example4() {
let content = std::fs::read_to_string("test_assets/ccsds/oem/OEMExample4.txt").unwrap();
let oem = parse_oem(&content).unwrap();
assert!((oem.header.format_version - 2.0).abs() < 1e-10);
assert_eq!(oem.segments.len(), 1);
assert_eq!(oem.segments[0].metadata.object_name, "MARS GLOBAL SURVEYOR");
assert_eq!(oem.segments[0].metadata.center_name, "MARS");
assert_eq!(oem.segments[0].metadata.ref_frame, CCSDSRefFrame::EME2000);
assert_eq!(oem.segments[0].states.len(), 3);
}
#[test]
fn test_parse_oem_example5_gcrf() {
let content = std::fs::read_to_string("test_assets/ccsds/oem/OEMExample5.txt").unwrap();
let oem = parse_oem(&content).unwrap();
assert_eq!(oem.segments.len(), 1);
assert_eq!(oem.segments[0].metadata.ref_frame, CCSDSRefFrame::GCRF);
assert_eq!(oem.segments[0].metadata.object_name, "ISS");
assert_eq!(oem.segments[0].metadata.object_id, "1998-067A");
assert_eq!(oem.segments[0].states.len(), 49);
}
#[test]
fn test_parse_oem_with_header_comment() {
let content =
std::fs::read_to_string("test_assets/ccsds/oem/OEMExampleWithHeaderComment.txt")
.unwrap();
let oem = parse_oem(&content).unwrap();
assert!(!oem.header.comments.is_empty());
}
#[test]
fn test_parse_oem_iss_truncated() {
let content =
std::fs::read_to_string("test_assets/ccsds/oem/ISS.resampled.truncated.txt").unwrap();
let oem = parse_oem(&content).unwrap();
assert_eq!(oem.segments.len(), 1);
assert!(!oem.segments[0].states.is_empty());
}
#[test]
fn test_parse_oem_lowercase_value() {
let content =
std::fs::read_to_string("test_assets/ccsds/oem/oemLowerCaseValue.oem").unwrap();
let result = parse_oem(&content);
let _ = result;
}
#[test]
fn test_parse_omm_example1() {
let content = std::fs::read_to_string("test_assets/ccsds/omm/OMMExample1.txt").unwrap();
let omm = parse_omm(&content).unwrap();
assert!((omm.header.format_version - 3.0).abs() < 1e-10);
assert_eq!(omm.header.originator, "NOAA/USA");
assert!(omm.header.message_id.is_none());
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.ref_frame, CCSDSRefFrame::TEME);
assert_eq!(omm.metadata.time_system, CCSDSTimeSystem::UTC);
assert_eq!(omm.metadata.mean_element_theory, "SGP/SGP4");
assert!(omm.mean_elements.mean_motion.is_some());
assert!((omm.mean_elements.mean_motion.unwrap() - 1.00273272).abs() < 1e-10);
assert!((omm.mean_elements.eccentricity - 0.0005013).abs() < 1e-10);
assert!((omm.mean_elements.inclination - 3.0539).abs() < 1e-4);
assert!((omm.mean_elements.ra_of_asc_node - 81.7939).abs() < 1e-4);
assert!((omm.mean_elements.arg_of_pericenter - 249.2363).abs() < 1e-4);
assert!((omm.mean_elements.mean_anomaly - 150.1602).abs() < 1e-4);
assert!((omm.mean_elements.gm.unwrap() - 398600.8e9).abs() < 1e3);
let tle = omm.tle_parameters.as_ref().unwrap();
assert_eq!(tle.ephemeris_type, Some(0));
assert_eq!(tle.classification_type, Some('U'));
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);
assert!((tle.mean_motion_dot.unwrap() - (-0.00000113)).abs() < 1e-12);
assert!((tle.mean_motion_ddot.unwrap() - 0.0).abs() < 1e-15);
}
#[test]
fn test_parse_omm_example2_with_covariance() {
let content = std::fs::read_to_string("test_assets/ccsds/omm/OMMExample2.txt").unwrap();
let omm = parse_omm(&content).unwrap();
assert_eq!(omm.metadata.object_name, "GOES 9");
assert!(omm.covariance.is_some());
let cov = omm.covariance.as_ref().unwrap();
assert_eq!(cov.cov_ref_frame.as_ref().unwrap(), &CCSDSRefFrame::TEME);
assert!((cov.matrix[(0, 0)] - 3.331349476038534e-04 * 1e6).abs() < 1e-2);
}
#[test]
fn test_parse_omm_example3_unsupported_time_system() {
let content = std::fs::read_to_string("test_assets/ccsds/omm/OMMExample3.txt").unwrap();
let result = parse_omm(&content);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("MRT"),
"Error should mention unsupported time system MRT: {}",
err_msg
);
}
#[test]
fn test_parse_omm_example4() {
let content = std::fs::read_to_string("test_assets/ccsds/omm/OMMExample4.txt").unwrap();
let omm = parse_omm(&content).unwrap();
assert_eq!(omm.metadata.object_name, "STARLETTE");
assert_eq!(omm.metadata.object_id, "1975-010A");
assert!((omm.mean_elements.mean_motion.unwrap() - 13.82309053).abs() < 1e-8);
assert!((omm.mean_elements.eccentricity - 0.0205751).abs() < 1e-7);
let tle = omm.tle_parameters.as_ref().unwrap();
assert_eq!(tle.norad_cat_id, Some(7646));
assert!((tle.bstar.unwrap() - (-4.7102e-6)).abs() < 1e-12);
}
#[test]
fn test_parse_omm_example5_sgp4xp() {
let content = std::fs::read_to_string("test_assets/ccsds/omm/OMMExample5.txt").unwrap();
let omm = parse_omm(&content).unwrap();
assert_eq!(omm.metadata.mean_element_theory, "SGP4-XP");
let tle = omm.tle_parameters.as_ref().unwrap();
assert_eq!(tle.ephemeris_type, Some(4));
assert!((tle.bterm.unwrap() - 0.0015).abs() < 1e-10);
assert!((tle.agom.unwrap() - 0.001).abs() < 1e-10);
}
#[test]
fn test_parse_opm_example1() {
let content = std::fs::read_to_string("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
let opm = parse_opm(&content).unwrap();
assert!((opm.header.format_version - 3.0).abs() < 1e-10);
assert_eq!(opm.header.originator, "JAXA");
assert_eq!(opm.metadata.object_name, "GODZILLA 5");
assert_eq!(opm.metadata.object_id, "1998-999A");
assert_eq!(opm.metadata.ref_frame, CCSDSRefFrame::ITRF2000);
assert!((opm.state_vector.position[0] - 6503514.0).abs() < 1.0);
assert!((opm.state_vector.position[1] - 1239647.0).abs() < 1.0);
assert!((opm.state_vector.position[2] - (-717490.0)).abs() < 1.0);
assert!((opm.state_vector.velocity[0] - (-873.160)).abs() < 0.001);
assert!((opm.state_vector.velocity[1] - 8740.420).abs() < 0.001);
assert!((opm.state_vector.velocity[2] - (-4191.076)).abs() < 0.001);
let sc = opm.spacecraft_parameters.as_ref().unwrap();
assert!((sc.mass.unwrap() - 3000.0).abs() < 1e-3);
assert!((sc.drag_coeff.unwrap() - 2.5).abs() < 1e-3);
assert!(opm.keplerian_elements.is_none());
assert!(opm.maneuvers.is_empty());
assert!(opm.covariance.is_none());
}
#[test]
fn test_parse_opm_example2_with_keplerian_and_maneuvers() {
let content = std::fs::read_to_string("test_assets/ccsds/opm/OPMExample2.txt").unwrap();
let opm = parse_opm(&content).unwrap();
assert_eq!(opm.metadata.object_name, "EUTELSAT W4");
assert_eq!(opm.metadata.ref_frame, CCSDSRefFrame::TOD);
assert!((opm.state_vector.position[0] - 6655994.2).abs() < 1.0);
let kep = opm.keplerian_elements.as_ref().unwrap();
assert!((kep.semi_major_axis - 41399512.3).abs() < 1.0); assert!((kep.eccentricity - 0.020842611).abs() < 1e-9);
assert!((kep.inclination - 0.117746).abs() < 1e-6);
assert!(kep.true_anomaly.is_some());
assert!((kep.true_anomaly.unwrap() - 41.922339).abs() < 1e-6);
assert!((kep.gm.unwrap() - 398600.4415e9).abs() < 1e3);
assert_eq!(opm.maneuvers.len(), 2);
let m1 = &opm.maneuvers[0];
assert!((m1.duration - 132.60).abs() < 0.01);
assert!((m1.delta_mass.unwrap() - (-18.418)).abs() < 0.001);
assert_eq!(m1.ref_frame, CCSDSRefFrame::J2000);
assert!((m1.dv[0] - (-23.257)).abs() < 0.001);
let m2 = &opm.maneuvers[1];
assert!((m2.duration - 0.0).abs() < 1e-10);
assert_eq!(m2.ref_frame, CCSDSRefFrame::RTN);
}
#[test]
fn test_parse_opm_example4_with_covariance_and_user_defined() {
let content = std::fs::read_to_string("test_assets/ccsds/opm/OPMExample4.txt").unwrap();
let opm = parse_opm(&content).unwrap();
let cov = opm.covariance.as_ref().unwrap();
assert_eq!(cov.cov_ref_frame.as_ref().unwrap(), &CCSDSRefFrame::RTN);
assert!((cov.matrix[(0, 0)] - 3.331349476038534e-04 * 1e6).abs() < 1e-2);
let ud = opm.user_defined.as_ref().unwrap();
assert_eq!(
ud.parameters.get("OBJ1_TIME_LASTOB_START").unwrap(),
"2020-01-29T13:30:00"
);
}
#[test]
fn test_parse_opm_example5_with_three_maneuvers() {
let content = std::fs::read_to_string("test_assets/ccsds/opm/OPMExample5.txt").unwrap();
let opm = parse_opm(&content).unwrap();
assert_eq!(opm.metadata.ref_frame, CCSDSRefFrame::GCRF);
assert_eq!(opm.metadata.time_system, CCSDSTimeSystem::GPS);
assert_eq!(opm.maneuvers.len(), 3);
}
}