Skip to main content

native/sidereon_nif/src/rinex_clock.rs

//! Rustler boundary for RINEX clock products.
//!
//! This module is glue only: it decodes Erlang terms, calls the
//! `sidereon-core` RINEX clock parser/interpolator, and encodes the public
//! Sidereon series shape back to Elixir.

use rustler::{Encoder, Env, NifResult, Term};
use sidereon_core::rinex::clock::{ClockEpoch, RinexClock};

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

/// Parse RINEX clock text into `[{"G05", [{gps_seconds, bias_s}, ...]}, ...]`.
#[rustler::nif(schedule = "DirtyCpu")]
fn rinex_clock_parse<'a>(env: Env<'a>, text: String) -> Term<'a> {
    match RinexClock::parse(&text) {
        Ok(clock) => (atoms::ok(), clock.series_rows()).encode(env),
        Err(err) => (atoms::error(), err.to_string()).encode(env),
    }
}

/// Parse RINEX clock text while skipping malformed and non-`AS` rows.
#[rustler::nif(schedule = "DirtyCpu")]
fn rinex_clock_parse_lossy<'a>(env: Env<'a>, text: String) -> Term<'a> {
    let clock = RinexClock::parse_lossy(&text);
    (atoms::ok(), clock.series_rows()).encode(env)
}

/// Serialize RINEX clock rows into RINEX clock text.
#[rustler::nif(schedule = "DirtyCpu")]
fn rinex_clock_to_string<'a>(
    env: Env<'a>,
    series: Vec<(String, Vec<(f64, f64)>)>,
) -> Term<'a> {
    match RinexClock::from_series_rows(series) {
        Ok(clock) => (atoms::ok(), clock.to_rinex_string()).encode(env),
        Err(err) => (atoms::error(), err.to_string()).encode(env),
    }
}

/// Interpolate one satellite clock from the public series row shape.
#[rustler::nif]
fn rinex_clock_clock_s<'a>(
    env: Env<'a>,
    series: Vec<(String, Vec<(f64, f64)>)>,
    satellite_id: String,
    datetime_tuple: Term<'a>,
) -> NifResult<Term<'a>> {
    let Some((year, month, day, hour, minute, second)) = decode_datetime(datetime_tuple)? else {
        return Ok((atoms::error(), atoms::no_clock()).encode(env));
    };

    let clock = RinexClock::from_series_rows(series).map_err(crate::errors::invalid_input)?;
    let epoch = ClockEpoch {
        year,
        month,
        day,
        hour,
        minute,
        second,
    };
    match clock
        .clock_s(&satellite_id, epoch)
        .map_err(crate::errors::invalid_input)?
    {
        Some(bias_s) => Ok((atoms::ok(), bias_s).encode(env)),
        None => Ok((atoms::error(), atoms::no_clock()).encode(env)),
    }
}

#[allow(clippy::type_complexity)]
fn decode_datetime(term: Term) -> NifResult<Option<(i32, u8, u8, u8, u8, f64)>> {
    #[allow(clippy::type_complexity)]
    let ((year, month, day), (hour, minute, second, microsecond)): (
        (i32, i32, i32),
        (i32, i32, i32, i32),
    ) = term.decode()?;

    let Ok(month) = u8::try_from(month) else {
        return Ok(None);
    };
    let Ok(day) = u8::try_from(day) else {
        return Ok(None);
    };
    let Ok(hour) = u8::try_from(hour) else {
        return Ok(None);
    };
    let Ok(minute) = u8::try_from(minute) else {
        return Ok(None);
    };
    if second < 0 || microsecond < 0 {
        return Ok(None);
    }

    let second = second as f64 + microsecond as f64 / 1_000_000.0;
    Ok(Some((year, month, day, hour, minute, second)))
}