Skip to main content

native/sidereon_nif/src/cdm.rs

//! Rustler boundary for the CCSDS CDM KVN and XML reader/writer.
//!
//! Pure glue over `sidereon_core::astro::cdm`: decode the raw KVN/XML text or the
//! normalized field map, forward to the crate codec, and encode the unchanged
//! Sidereon result shapes. No grammar, unit stripping, or number parsing lives here.
//! Date/time fields cross as raw strings; the Elixir binding resolves them to/from
//! its native `DateTime`.

use rustler::{Encoder, Env, NifResult, Term};
use sidereon_core::astro::cdm::{self, CdmKvn, CdmObject};

mod atoms {
    rustler::atoms! {
        ok,
        error
    }
}

/// One object block exchanged with the Elixir binding. Mirrors
/// `%Sidereon.CCSDS.CDM.ObjectData{}` with the state as a nested `{r, v}` tuple,
/// the RTN position covariance as a six-element list, and the optional RTN
/// velocity covariance (the 15 lower-triangle rows completing the 6x6 matrix) as
/// a list of 15 floats or `nil`. Every CCSDS metadata field the core carries is
/// crossed verbatim as a string.
#[derive(Debug, Clone, rustler::NifMap)]
struct ObjectFields {
    object_designator: Option<String>,
    catalog_name: Option<String>,
    object_name: Option<String>,
    international_designator: Option<String>,
    object_type: Option<String>,
    operator_contact_position: Option<String>,
    operator_organization: Option<String>,
    operator_phone: Option<String>,
    operator_email: Option<String>,
    ephemeris_name: Option<String>,
    covariance_method: Option<String>,
    maneuverable: Option<String>,
    orbit_center: Option<String>,
    ref_frame: Option<String>,
    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>,
    state: ((f64, f64, f64), (f64, f64, f64)),
    covariance_rtn: Vec<f64>,
    velocity_covariance_rtn: Option<Vec<f64>>,
}

/// Normalized CDM fields exchanged with the Elixir binding. Mirrors the
/// `%Sidereon.CCSDS.CDM{}` numeric/string content with `creation_date` / `tca` left
/// as the raw textual values for the host to resolve.
#[derive(Debug, Clone, rustler::NifMap)]
struct CdmFields {
    creation_date: Option<String>,
    originator: Option<String>,
    message_id: Option<String>,
    tca: Option<String>,
    miss_distance_m: Option<f64>,
    relative_speed_m_s: Option<f64>,
    collision_probability: Option<f64>,
    collision_probability_method: Option<String>,
    hard_body_radius_m: Option<f64>,
    object1: ObjectFields,
    object2: ObjectFields,
}

impl From<CdmObject> for ObjectFields {
    fn from(o: CdmObject) -> Self {
        Self {
            object_designator: o.object_designator,
            catalog_name: o.catalog_name,
            object_name: o.object_name,
            international_designator: o.international_designator,
            object_type: o.object_type,
            operator_contact_position: o.operator_contact_position,
            operator_organization: o.operator_organization,
            operator_phone: o.operator_phone,
            operator_email: o.operator_email,
            ephemeris_name: o.ephemeris_name,
            covariance_method: o.covariance_method,
            maneuverable: o.maneuverable,
            orbit_center: o.orbit_center,
            ref_frame: o.ref_frame,
            gravity_model: o.gravity_model,
            atmospheric_model: o.atmospheric_model,
            n_body_perturbations: o.n_body_perturbations,
            solar_rad_pressure: o.solar_rad_pressure,
            earth_tides: o.earth_tides,
            intrack_thrust: o.intrack_thrust,
            state: o.state,
            covariance_rtn: o.covariance_rtn.to_vec(),
            velocity_covariance_rtn: o.velocity_covariance_rtn.map(|v| v.to_vec()),
        }
    }
}

impl From<ObjectFields> for CdmObject {
    fn from(f: ObjectFields) -> Self {
        Self {
            object_designator: f.object_designator,
            catalog_name: f.catalog_name,
            object_name: f.object_name,
            international_designator: f.international_designator,
            object_type: f.object_type,
            operator_contact_position: f.operator_contact_position,
            operator_organization: f.operator_organization,
            operator_phone: f.operator_phone,
            operator_email: f.operator_email,
            ephemeris_name: f.ephemeris_name,
            covariance_method: f.covariance_method,
            maneuverable: f.maneuverable,
            orbit_center: f.orbit_center,
            ref_frame: f.ref_frame,
            gravity_model: f.gravity_model,
            atmospheric_model: f.atmospheric_model,
            n_body_perturbations: f.n_body_perturbations,
            solar_rad_pressure: f.solar_rad_pressure,
            earth_tides: f.earth_tides,
            intrack_thrust: f.intrack_thrust,
            state: f.state,
            covariance_rtn: to_covariance(&f.covariance_rtn),
            velocity_covariance_rtn: f
                .velocity_covariance_rtn
                .as_deref()
                .map(to_velocity_covariance),
        }
    }
}

impl From<CdmKvn> for CdmFields {
    fn from(c: CdmKvn) -> Self {
        Self {
            creation_date: c.creation_date,
            originator: c.originator,
            message_id: c.message_id,
            tca: c.tca,
            miss_distance_m: c.miss_distance_m,
            relative_speed_m_s: c.relative_speed_m_s,
            collision_probability: c.collision_probability,
            collision_probability_method: c.collision_probability_method,
            hard_body_radius_m: c.hard_body_radius_m,
            object1: c.object1.into(),
            object2: c.object2.into(),
        }
    }
}

impl From<CdmFields> for CdmKvn {
    fn from(f: CdmFields) -> Self {
        Self {
            creation_date: f.creation_date,
            originator: f.originator,
            message_id: f.message_id,
            tca: f.tca,
            miss_distance_m: f.miss_distance_m,
            relative_speed_m_s: f.relative_speed_m_s,
            collision_probability: f.collision_probability,
            collision_probability_method: f.collision_probability_method,
            hard_body_radius_m: f.hard_body_radius_m,
            object1: f.object1.into(),
            object2: f.object2.into(),
        }
    }
}

/// Copy the six RTN covariance components, leaving any missing slot at zero.
fn to_covariance(values: &[f64]) -> [f64; 6] {
    let mut out = [0.0_f64; 6];
    for (slot, value) in out.iter_mut().zip(values) {
        *slot = *value;
    }
    out
}

/// Copy the 15 RTN velocity-covariance components, leaving any missing slot at
/// zero. The core emits the full block when present, so a faithful caller passes
/// 15 values.
fn to_velocity_covariance(values: &[f64]) -> [f64; 15] {
    let mut out = [0.0_f64; 15];
    for (slot, value) in out.iter_mut().zip(values) {
        *slot = *value;
    }
    out
}

/// Returns `{:ok, fields}` with date/time fields as raw strings, or
/// `{:error, reason}` for a structurally invalid message.
#[rustler::nif]
fn cdm_parse_kvn<'a>(env: Env<'a>, text: String) -> Term<'a> {
    match cdm::parse_kvn(&text) {
        Ok(parsed) => (atoms::ok(), CdmFields::from(parsed)).encode(env),
        Err(e) => (atoms::error(), e.to_string()).encode(env),
    }
}

#[rustler::nif]
fn cdm_encode_kvn(fields: CdmFields) -> NifResult<String> {
    cdm::encode_kvn(&fields.into()).map_err(crate::errors::invalid_input)
}

/// Returns `{:ok, fields}` with date/time fields as raw strings, or
/// `{:error, reason}` for a structurally invalid message.
#[rustler::nif]
fn cdm_parse_xml<'a>(env: Env<'a>, text: String) -> Term<'a> {
    match cdm::parse_xml(&text) {
        Ok(parsed) => (atoms::ok(), CdmFields::from(parsed)).encode(env),
        Err(e) => (atoms::error(), e.to_string()).encode(env),
    }
}

#[rustler::nif]
fn cdm_encode_xml(fields: CdmFields) -> NifResult<String> {
    cdm::encode_xml(&fields.into()).map_err(crate::errors::invalid_input)
}