use chrono::{Datelike, Timelike};
use qtty::angular::Degrees;
use qtty::angular::Turn;
use qtty::angular_rate::AngularRate;
use qtty::time::Day;
use tempoch::{Time, UTC};
use super::parse::compute_tle_checksum;
use super::TleError;
use crate::formats::tle::{Classification, InternationalDesignator, SatelliteNumber, TLE};
#[derive(Default, Clone, Debug)]
pub struct TleBuilder {
name: Option<String>,
norad_id: Option<SatelliteNumber>,
classification: Option<Classification>,
international_designator: Option<InternationalDesignator>,
epoch: Option<Time<UTC>>,
mean_motion_dot: f64,
mean_motion_ddot: f64,
bstar: f64,
element_set_number: u16,
revolution_number_at_epoch: u32,
inclination: Option<Degrees>,
raan: Option<Degrees>,
eccentricity: Option<f64>,
argument_of_perigee: Option<Degrees>,
mean_anomaly: Option<Degrees>,
mean_motion: Option<AngularRate<Turn, Day>>,
}
impl TleBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn norad_id(mut self, n: SatelliteNumber) -> Self {
self.norad_id = Some(n);
self
}
pub fn classification(mut self, c: Classification) -> Self {
self.classification = Some(c);
self
}
pub fn international_designator(mut self, d: InternationalDesignator) -> Self {
self.international_designator = Some(d);
self
}
pub fn epoch(mut self, t: Time<UTC>) -> Self {
self.epoch = Some(t);
self
}
pub fn mean_motion_dot(mut self, v: f64) -> Self {
self.mean_motion_dot = v;
self
}
pub fn mean_motion_ddot(mut self, v: f64) -> Self {
self.mean_motion_ddot = v;
self
}
pub fn bstar(mut self, v: f64) -> Self {
self.bstar = v;
self
}
pub fn element_set_number(mut self, v: u16) -> Self {
self.element_set_number = v;
self
}
pub fn revolution_number_at_epoch(mut self, v: u32) -> Self {
self.revolution_number_at_epoch = v;
self
}
pub fn inclination(mut self, v: Degrees) -> Self {
self.inclination = Some(v);
self
}
pub fn raan(mut self, v: Degrees) -> Self {
self.raan = Some(v);
self
}
pub fn eccentricity(mut self, v: f64) -> Self {
self.eccentricity = Some(v);
self
}
pub fn argument_of_perigee(mut self, v: Degrees) -> Self {
self.argument_of_perigee = Some(v);
self
}
pub fn mean_anomaly(mut self, v: Degrees) -> Self {
self.mean_anomaly = Some(v);
self
}
pub fn mean_motion(mut self, v: AngularRate<Turn, Day>) -> Self {
self.mean_motion = Some(v);
self
}
pub fn build(self) -> Result<TLE, TleError> {
Ok(TLE {
name: self.name,
norad_id: self
.norad_id
.ok_or(TleError::BuilderMissingField("norad_id"))?,
classification: self.classification.unwrap_or(Classification::Unclassified),
international_designator: self
.international_designator
.ok_or(TleError::BuilderMissingField("international_designator"))?,
epoch: self.epoch.ok_or(TleError::BuilderMissingField("epoch"))?,
mean_motion_dot: self.mean_motion_dot,
mean_motion_ddot: self.mean_motion_ddot,
bstar: self.bstar,
element_set_number: self.element_set_number,
revolution_number_at_epoch: self.revolution_number_at_epoch,
inclination: self
.inclination
.ok_or(TleError::BuilderMissingField("inclination"))?,
raan: self.raan.ok_or(TleError::BuilderMissingField("raan"))?,
eccentricity: self
.eccentricity
.ok_or(TleError::BuilderMissingField("eccentricity"))?,
argument_of_perigee: self
.argument_of_perigee
.ok_or(TleError::BuilderMissingField("argument_of_perigee"))?,
mean_anomaly: self
.mean_anomaly
.ok_or(TleError::BuilderMissingField("mean_anomaly"))?,
mean_motion: self
.mean_motion
.ok_or(TleError::BuilderMissingField("mean_motion"))?,
})
}
}
pub fn format_tle(tle: &TLE) -> Result<(String, String), TleError> {
let cat = tle.norad_id.format_alpha5()?;
let cls = tle.classification.as_char();
let intl = format!("{:<8}", tle.international_designator.0);
let dt = tle
.epoch
.try_to_chrono()
.map_err(|e| TleError::EpochConversion(format!("{e:?}")))?;
let year = dt.year();
let yy = ((year % 100) + 100) % 100;
let doy = dt.ordinal();
let secs_of_day =
dt.num_seconds_from_midnight() as f64 + dt.timestamp_subsec_nanos() as f64 * 1e-9;
let frac_day = secs_of_day / 86_400.0;
let day_full = doy as f64 + frac_day;
let n_dot = format_signed_decimal(tle.mean_motion_dot);
let n_ddot = format_assumed_decimal_exponent(tle.mean_motion_ddot);
let bstar_s = format_assumed_decimal_exponent(tle.bstar);
let elset = format!("{:>4}", tle.element_set_number);
let mut l1 =
format!("1 {cat}{cls} {intl} {yy:02}{day_full:012.8} {n_dot} {n_ddot} {bstar_s} 0 {elset}");
debug_assert_eq!(
l1.len(),
68,
"rendered TLE line1 prefix not 68 chars: {l1:?}"
);
let cs1 = compute_tle_checksum(&l1);
l1.push_str(&cs1.to_string());
let inc = format!("{:8.4}", tle.inclination.value());
let raan = format!("{:8.4}", tle.raan.value());
let ecc_scaled = (tle.eccentricity * 1.0e7).round() as u64;
let ecc = format!("{ecc_scaled:07}");
let argp = format!("{:8.4}", tle.argument_of_perigee.value());
let m_ano = format!("{:8.4}", tle.mean_anomaly.value());
let n_str = format!("{:11.8}", tle.mean_motion.value());
let rev = format!("{:>5}", tle.revolution_number_at_epoch);
let mut l2 = format!("2 {cat} {inc} {raan} {ecc} {argp} {m_ano} {n_str}{rev}");
debug_assert_eq!(
l2.len(),
68,
"rendered TLE line2 prefix not 68 chars: {l2:?}"
);
let cs2 = compute_tle_checksum(&l2);
l2.push_str(&cs2.to_string());
Ok((l1, l2))
}
fn format_signed_decimal(v: f64) -> String {
let sign = if v.is_sign_negative() { '-' } else { ' ' };
let mag = v.abs();
let fract = (mag * 1.0e8).round() as u64;
format!("{sign}.{fract:08}")
}
fn format_assumed_decimal_exponent(v: f64) -> String {
if v == 0.0 || !v.is_finite() {
return " 00000-0".to_string();
}
let sign = if v.is_sign_negative() { '-' } else { ' ' };
let mag = v.abs();
let mut exp = mag.log10().floor() as i32 + 1;
let mut mantissa = mag / 10f64.powi(exp);
let scaled = (mantissa * 1.0e5).round() as u64;
if scaled >= 1_000_000 {
mantissa /= 10.0;
exp += 1;
}
let scaled = (mantissa * 1.0e5).round() as u64;
let exp_sign = if exp < 0 { '-' } else { '+' };
let exp_abs = exp.unsigned_abs();
if exp_sign == '+' {
format!("{sign}{scaled:05}+{exp_abs}")
} else {
format!("{sign}{scaled:05}-{exp_abs}")
}
}