use crate::{
space::{
propagation::{SGP4ErrorOutput, SGP4Output, sgp4, sgp4init},
util::{
constants::MINUTES_PER_DAY,
time::{TimeStamp, days2mdhms, jday},
},
},
util::Date,
};
use alloc::{format, string::String, vec, vec::Vec};
use core::f64::consts::PI;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum Classification {
#[default]
U,
C,
S,
}
impl From<&str> for Classification {
fn from(s: &str) -> Self {
match s {
"U" | "u" => Classification::U,
"C" | "c" => Classification::C,
"S" | "s" => Classification::S,
_ => Classification::U,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum OperationMode {
A,
#[default]
I,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum Method {
D,
#[default]
N,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct TLEData {
pub name: String,
pub number: f64,
pub class: Classification,
pub id: String,
pub date: Date,
pub epochdays: f64,
pub fdmm: f64,
pub sdmm: f64,
pub drag: f64,
pub ephemeris: f64,
pub esn: f64,
pub inclination: f64,
pub ascension: f64,
pub eccentricity: f64,
pub perigee: f64,
pub anomaly: f64,
pub motion: f64,
pub revolution: f64,
pub rms: Option<f64>,
}
impl From<&str> for TLEData {
fn from(value: &str) -> Self {
let mut lines: Vec<&str> = trim(value).lines().collect();
let mut tle = TLEData::default();
if lines.len() >= 3 {
let mut name = trim(lines.remove(0));
if name.starts_with("0 ") {
name = &name[2..];
}
tle.name = name.into();
}
let line = lines.remove(0);
let checksum = check(line);
if checksum != line[68..69].parse::<u32>().unwrap() {
panic!("Line 1 checksum mismatch: {} != {}: {}", checksum, &line[68..69], line);
}
tle.number = parse_float(&alpha5_converter(&line[2..7]));
tle.class = trim(&line[7..9]).into();
tle.id = trim(&line[9..18]).into();
(tle.date, tle.epochdays) = parse_epoch(&line[18..33]);
tle.fdmm = parse_float(&line[33..44]);
tle.sdmm = parse_float(&line[44..53]);
tle.drag = parse_drag(&line[53..62]);
tle.ephemeris = parse_float(&line[62..64]);
tle.esn = parse_float(&line[64..68]);
let line = lines.remove(0);
let checksum = check(line);
if checksum != line[68..69].parse::<u32>().unwrap() {
panic!("Line 2 checksum mismatch: {} != {}: {}", checksum, &line[68..69], line);
}
tle.inclination = parse_float(&line[8..17]);
tle.ascension = parse_float(&line[17..26]);
tle.eccentricity = parse_float(&format!("0.{}", &line[26..34]));
tle.perigee = parse_float(&line[34..43]);
tle.anomaly = parse_float(&line[43..52]);
tle.motion = parse_float(&line[52..63]);
tle.revolution = parse_float(&line[63..68]);
tle
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct TLEDataCelestrak {
#[serde(rename = "OBJECT_NAME")]
pub object_name: String,
#[serde(rename = "OBJECT_ID")]
pub object_id: String,
#[serde(rename = "EPOCH")]
pub epoch: String,
#[serde(rename = "MEAN_MOTION")]
pub mean_motion: f64,
#[serde(rename = "ECCENTRICITY")]
pub eccentricity: f64,
#[serde(rename = "INCLINATION")]
pub inclination: f64,
#[serde(rename = "RA_OF_ASC_NODE")]
pub ra_of_asc_node: f64,
#[serde(rename = "ARG_OF_PERICENTER")]
pub arg_of_pericenter: f64,
#[serde(rename = "MEAN_ANOMALY")]
pub mean_anomaly: f64,
#[serde(rename = "EPHEMERIS_TYPE")]
pub ephemeris_type: f64,
#[serde(rename = "CLASSIFICATION_TYPE")]
pub classification_type: String,
#[serde(rename = "NORAD_CAT_ID")]
pub norad_cat_id: f64,
#[serde(rename = "ELEMENT_SET_NO")]
pub element_set_no: f64,
#[serde(rename = "REV_AT_EPOCH")]
pub rev_at_epoch: f64,
#[serde(rename = "BSTAR")]
pub bstar: f64,
#[serde(rename = "MEAN_MOTION_DOT")]
pub mean_motion_dot: f64,
#[serde(rename = "MEAN_MOTION_DDOT")]
pub mean_motion_ddot: f64,
#[serde(rename = "RMS")]
pub rms: String,
#[serde(rename = "DATA_SOURCE")]
pub data_source: String,
}
impl From<&TLEDataCelestrak> for TLEData {
fn from(data: &TLEDataCelestrak) -> Self {
let date: Date = (&*data.epoch).into();
let start = Date::new(date.year, 0, 0);
TLEData {
name: data.object_name.clone(),
number: data.norad_cat_id,
class: (&*data.classification_type).into(),
id: data.object_id.clone(),
date,
epochdays: jday(&date) - jday(&start),
fdmm: data.mean_motion_dot,
sdmm: data.mean_motion_ddot,
drag: data.bstar,
ephemeris: data.ephemeris_type,
esn: data.element_set_no,
inclination: data.inclination,
ascension: data.ra_of_asc_node,
eccentricity: data.eccentricity,
perigee: data.arg_of_pericenter,
anomaly: data.mean_anomaly,
motion: data.mean_motion,
revolution: data.rev_at_epoch,
rms: data.rms.parse().ok(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct Satellite {
pub init: bool, pub name: String, pub number: f64,
pub class: Classification,
pub id: String, pub date: Date, pub epochyr: f64,
pub epochdays: f64,
pub jdsatepoch: f64,
pub fdmm: f64,
pub sdmm: f64,
pub drag: f64,
pub ephemeris: f64,
pub esn: f64,
pub inclination: f64,
pub ascension: f64,
pub eccentricity: f64,
pub perigee: f64,
pub anomaly: f64,
pub motion: f64,
pub revolution: f64,
pub opsmode: OperationMode,
pub rms: Option<f64>,
pub isimp: f64,
pub method: Method,
pub aycof: f64,
pub con41: f64,
pub cc1: f64,
pub cc4: f64,
pub cc5: f64,
pub d2: f64,
pub d3: f64,
pub d4: f64,
pub delmo: f64,
pub eta: f64,
pub argpdot: f64,
pub omgcof: f64,
pub sinmao: f64,
pub t2cof: f64,
pub t3cof: f64,
pub t4cof: f64,
pub t5cof: f64,
pub x1mth2: f64,
pub x7thm1: f64,
pub mdot: f64,
pub nodedot: f64,
pub xlcof: f64,
pub xmcof: f64,
pub nodecf: f64,
pub irez: f64,
pub d2201: f64,
pub d2211: f64,
pub d3210: f64,
pub d3222: f64,
pub d4410: f64,
pub d4422: f64,
pub d5220: f64,
pub d5232: f64,
pub d5421: f64,
pub d5433: f64,
pub dedt: f64,
pub del1: f64,
pub del2: f64,
pub del3: f64,
pub didt: f64,
pub dmdt: f64,
pub dnodt: f64,
pub domdt: f64,
pub e3: f64,
pub ee2: f64,
pub peo: f64,
pub pgho: f64,
pub pho: f64,
pub pinco: f64,
pub plo: f64,
pub se2: f64,
pub se3: f64,
pub sgh2: f64,
pub sgh3: f64,
pub sgh4: f64,
pub sh2: f64,
pub sh3: f64,
pub si2: f64,
pub si3: f64,
pub sl2: f64,
pub sl3: f64,
pub sl4: f64,
pub gsto: f64,
pub xfact: f64,
pub xgh2: f64,
pub xgh3: f64,
pub xgh4: f64,
pub xh2: f64,
pub xh3: f64,
pub xi2: f64,
pub xi3: f64,
pub xl2: f64,
pub xl3: f64,
pub xl4: f64,
pub xlamo: f64,
pub zmol: f64,
pub zmos: f64,
pub atime: f64,
pub xli: f64,
pub xni: f64,
}
impl Satellite {
pub fn new(data: &TLEData, initialize: Option<bool>) -> Self {
let mut this = Self::default();
let initialize = initialize.unwrap_or(true);
this.rms = data.rms;
this.name = data.name.clone();
this.number = data.number;
this.class = data.class;
this.id = data.id.clone();
this.date = data.date;
this.fdmm = data.fdmm;
this.sdmm = data.sdmm;
this.drag = data.drag;
this.ephemeris = data.ephemeris;
this.esn = data.esn;
this.inclination = data.inclination.to_radians();
this.ascension = data.ascension.to_radians();
this.eccentricity = data.eccentricity;
this.perigee = data.perigee.to_radians();
this.anomaly = data.anomaly.to_radians();
this.motion = data.motion;
this.revolution = data.revolution;
this.epochdays = data.epochdays;
this.epochyr = (this.date.year % 100) as f64;
this.motion /= 1440. / (2. * PI); let year = if this.epochyr < 57. { this.epochyr + 2000. } else { this.epochyr + 1900. };
let mdhms_result = days2mdhms(year as u16, this.epochdays);
let TimeStamp { mon, day, hr, min, sec } = mdhms_result;
this.jdsatepoch = jday(&Date::new_full(
year as u16,
mon as u8,
day as u8,
hr as u8,
min as u8,
sec as u8,
));
if initialize {
sgp4init(&mut this);
}
this
}
pub fn gpu(&self) -> Vec<f64> {
vec![
self.anomaly,
self.motion,
self.eccentricity,
self.inclination,
if self.method == Method::D { 0. } else { 1. }, if self.opsmode == OperationMode::A { 0. } else { 1. }, self.drag,
self.mdot,
self.perigee,
self.argpdot,
self.ascension,
self.nodedot,
self.nodecf,
self.cc1,
self.cc4,
self.cc5,
self.t2cof,
self.isimp,
self.omgcof,
self.eta,
self.xmcof,
self.delmo,
self.d2,
self.d3,
self.d4,
self.sinmao,
self.t3cof,
self.t4cof,
self.t5cof,
self.irez,
self.d2201,
self.d2211,
self.d3210,
self.d3222,
self.d4410,
self.d4422,
self.d5220,
self.d5232,
self.d5421,
self.d5433,
self.dedt,
self.del1,
self.del2,
self.del3,
self.didt,
self.dmdt,
self.dnodt,
self.domdt,
self.gsto,
self.xfact,
self.xlamo,
self.atime,
self.xli,
self.xni,
self.aycof,
self.xlcof,
self.con41,
self.x1mth2,
self.x7thm1,
self.zmos,
self.zmol,
self.se2,
self.se3,
self.si2,
self.si3,
self.sl2,
self.sl3,
self.sl4,
self.sgh2,
self.sgh3,
self.sgh4,
self.sh2,
self.sh3,
self.ee2,
self.e3,
self.xi2,
self.xi3,
self.xl2,
self.xl3,
self.xl4,
self.xgh2,
self.xgh3,
self.xgh4,
self.xh2,
self.xh3,
self.peo,
self.pinco,
self.plo,
self.pgho,
self.pho,
]
}
pub fn propagate(&self, time: &Date) -> Result<SGP4Output, SGP4ErrorOutput> {
let j = jday(time);
sgp4(self, (j - self.jdsatepoch) * MINUTES_PER_DAY)
}
pub fn sgp4(&self, time: f64) -> Result<SGP4Output, SGP4ErrorOutput> {
sgp4(self, time)
}
}
fn parse_float(value: &str) -> f64 {
let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
if let Some(caps) = re.captures(value) {
let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
let base = caps.get(2).map_or("0", |m| m.as_str());
let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
let combined = format!("{}{}", base, power);
return sign * combined.parse::<f64>().unwrap();
}
0.0
}
fn parse_drag(value: &str) -> f64 {
let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
if let Some(caps) = re.captures(value) {
let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
let base = caps.get(2).map_or("0", |m| m.as_str());
let base = if base.contains('.') { base.into() } else { format!("0.{}", base) };
let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
return sign * format!("{}{}", base, power).parse::<f64>().unwrap();
}
0.0
}
fn parse_epoch(value: &str) -> (Date, f64) {
let re = Regex::new(r"^\s+|\s+$").unwrap();
let value: String = re.replace_all(value, "").into();
let epoch = value[0..2].parse::<u16>().unwrap();
let days = value[2..].parse::<f64>().unwrap();
let now_year = 2025;
let current_epoch = (now_year % 100) as u16;
let century = now_year - current_epoch as i32;
let year = if epoch > current_epoch + 1 {
(century - 100 + epoch as i32) as u16
} else {
(century + epoch as i32) as u16
};
let ts = days2mdhms(year, days);
(
Date::new_full(year, ts.mon as u8, ts.day as u8, ts.hr as u8, ts.min as u8, ts.sec as u8),
days,
)
}
fn check(line: &str) -> u32 {
let mut sum = 0;
for c in line.chars().take(68) {
if c.is_ascii_digit() {
sum += c.to_digit(10).unwrap();
} else if c == '-' {
sum += 1;
}
}
sum % 10
}
fn alpha5_converter(s: &str) -> String {
if let Some(first_char) = s.chars().next() {
if first_char.is_ascii_digit() {
return s.into();
}
let alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
if let Some(idx) = alphabet.find(first_char) {
let rest: String = s.chars().skip(1).collect();
return format!("{}{}", idx + 10, rest);
}
}
s.into()
}
fn trim(s: &str) -> &str {
fn is_trim_char(c: char) -> bool {
c.is_whitespace() || c == '\u{FEFF}' || c == '\u{00A0}'
}
s.trim_matches(is_trim_char)
}