1use crate::{
2 space::{
3 propagation::{SGP4ErrorOutput, SGP4Output, sgp4, sgp4init},
4 util::{
5 constants::MINUTES_PER_DAY,
6 time::{TimeStamp, days2mdhms, jday},
7 },
8 },
9 util::Date,
10};
11use alloc::{format, string::String, vec, vec::Vec};
12use core::f64::consts::PI;
13use regex::Regex;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[repr(u8)]
22pub enum Classification {
23 #[default]
25 U,
26 C,
28 S,
30}
31impl From<&str> for Classification {
32 fn from(s: &str) -> Self {
33 match s {
34 "U" | "u" => Classification::U,
35 "C" | "c" => Classification::C,
36 "S" | "s" => Classification::S,
37 _ => Classification::U,
38 }
39 }
40}
41
42#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[repr(u8)]
47pub enum OperationMode {
48 A,
50 #[default]
52 I,
53}
54#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[repr(u8)]
59pub enum Method {
60 D,
62 #[default]
64 N,
65}
66
67#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
69pub struct TLEData {
70 pub name: String,
72 pub number: f64,
74 pub class: Classification,
76 pub id: String,
78 pub date: Date,
80 pub epochdays: f64,
82 pub fdmm: f64,
84 pub sdmm: f64,
86 pub drag: f64,
88 pub ephemeris: f64,
90 pub esn: f64,
92 pub inclination: f64,
94 pub ascension: f64,
96 pub eccentricity: f64,
98 pub perigee: f64,
100 pub anomaly: f64,
102 pub motion: f64,
104 pub revolution: f64,
106 pub rms: Option<f64>,
108}
109impl From<&str> for TLEData {
110 fn from(value: &str) -> Self {
113 let mut lines: Vec<&str> = trim(value).lines().collect();
114 let mut tle = TLEData::default();
115
116 if lines.len() >= 3 {
118 let mut name = trim(lines.remove(0));
119 if name.starts_with("0 ") {
120 name = &name[2..];
121 }
122 tle.name = name.into();
123 }
124
125 let line = lines.remove(0);
127 let checksum = check(line);
128 if checksum != line[68..69].parse::<u32>().unwrap() {
129 panic!("Line 1 checksum mismatch: {} != {}: {}", checksum, &line[68..69], line);
130 }
131
132 tle.number = parse_float(&alpha5_converter(&line[2..7]));
133 tle.class = trim(&line[7..9]).into();
134 tle.id = trim(&line[9..18]).into();
135 (tle.date, tle.epochdays) = parse_epoch(&line[18..33]);
136 tle.fdmm = parse_float(&line[33..44]);
137 tle.sdmm = parse_float(&line[44..53]);
138 tle.drag = parse_drag(&line[53..62]);
139 tle.ephemeris = parse_float(&line[62..64]);
140 tle.esn = parse_float(&line[64..68]);
141
142 let line = lines.remove(0);
144 let checksum = check(line);
145 if checksum != line[68..69].parse::<u32>().unwrap() {
146 panic!("Line 2 checksum mismatch: {} != {}: {}", checksum, &line[68..69], line);
147 }
148
149 tle.inclination = parse_float(&line[8..17]);
150 tle.ascension = parse_float(&line[17..26]);
151 tle.eccentricity = parse_float(&format!("0.{}", &line[26..34]));
152 tle.perigee = parse_float(&line[34..43]);
153 tle.anomaly = parse_float(&line[43..52]);
154 tle.motion = parse_float(&line[52..63]);
155 tle.revolution = parse_float(&line[63..68]);
156
157 tle
158 }
159}
160
161#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
163pub struct TLEDataCelestrak {
164 #[serde(rename = "OBJECT_NAME")]
166 pub object_name: String,
167 #[serde(rename = "OBJECT_ID")]
169 pub object_id: String,
170 #[serde(rename = "EPOCH")]
172 pub epoch: String,
173 #[serde(rename = "MEAN_MOTION")]
175 pub mean_motion: f64,
176 #[serde(rename = "ECCENTRICITY")]
178 pub eccentricity: f64,
179 #[serde(rename = "INCLINATION")]
181 pub inclination: f64,
182 #[serde(rename = "RA_OF_ASC_NODE")]
184 pub ra_of_asc_node: f64,
185 #[serde(rename = "ARG_OF_PERICENTER")]
187 pub arg_of_pericenter: f64,
188 #[serde(rename = "MEAN_ANOMALY")]
190 pub mean_anomaly: f64,
191 #[serde(rename = "EPHEMERIS_TYPE")]
193 pub ephemeris_type: f64,
194 #[serde(rename = "CLASSIFICATION_TYPE")]
196 pub classification_type: String,
197 #[serde(rename = "NORAD_CAT_ID")]
199 pub norad_cat_id: f64,
200 #[serde(rename = "ELEMENT_SET_NO")]
202 pub element_set_no: f64,
203 #[serde(rename = "REV_AT_EPOCH")]
205 pub rev_at_epoch: f64,
206 #[serde(rename = "BSTAR")]
208 pub bstar: f64,
209 #[serde(rename = "MEAN_MOTION_DOT")]
211 pub mean_motion_dot: f64,
212 #[serde(rename = "MEAN_MOTION_DDOT")]
214 pub mean_motion_ddot: f64,
215 #[serde(rename = "RMS")]
217 pub rms: String,
218 #[serde(rename = "DATA_SOURCE")]
220 pub data_source: String,
221}
222impl From<&TLEDataCelestrak> for TLEData {
225 fn from(data: &TLEDataCelestrak) -> Self {
226 let date: Date = (&*data.epoch).into();
228 let start = Date::new(date.year, 0, 0);
229 TLEData {
230 name: data.object_name.clone(),
231 number: data.norad_cat_id,
232 class: (&*data.classification_type).into(),
233 id: data.object_id.clone(),
234 date,
235 epochdays: jday(&date) - jday(&start),
236 fdmm: data.mean_motion_dot,
237 sdmm: data.mean_motion_ddot,
238 drag: data.bstar,
239 ephemeris: data.ephemeris_type,
240 esn: data.element_set_no,
241 inclination: data.inclination,
242 ascension: data.ra_of_asc_node,
243 eccentricity: data.eccentricity,
244 perigee: data.arg_of_pericenter,
245 anomaly: data.mean_anomaly,
246 motion: data.mean_motion,
247 revolution: data.rev_at_epoch,
248 rms: data.rms.parse().ok(),
249 }
250 }
251}
252
253#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
287pub struct Satellite {
288 pub init: bool, pub name: String, pub number: f64,
296 pub class: Classification,
298 pub id: String, pub date: Date, pub epochyr: f64,
304 pub epochdays: f64,
306 pub jdsatepoch: f64,
308 pub fdmm: f64,
310 pub sdmm: f64,
312 pub drag: f64,
314 pub ephemeris: f64,
316 pub esn: f64,
318 pub inclination: f64,
321 pub ascension: f64,
323 pub eccentricity: f64,
325 pub perigee: f64,
327 pub anomaly: f64,
329 pub motion: f64,
331 pub revolution: f64,
333 pub opsmode: OperationMode,
336 pub rms: Option<f64>,
338 pub isimp: f64,
341 pub method: Method,
343 pub aycof: f64,
345 pub con41: f64,
347 pub cc1: f64,
349 pub cc4: f64,
351 pub cc5: f64,
353 pub d2: f64,
355 pub d3: f64,
357 pub d4: f64,
359 pub delmo: f64,
361 pub eta: f64,
363 pub argpdot: f64,
365 pub omgcof: f64,
367 pub sinmao: f64,
369 pub t2cof: f64,
371 pub t3cof: f64,
373 pub t4cof: f64,
375 pub t5cof: f64,
377 pub x1mth2: f64,
379 pub x7thm1: f64,
381 pub mdot: f64,
383 pub nodedot: f64,
385 pub xlcof: f64,
387 pub xmcof: f64,
389 pub nodecf: f64,
391 pub irez: f64,
394 pub d2201: f64,
396 pub d2211: f64,
398 pub d3210: f64,
400 pub d3222: f64,
402 pub d4410: f64,
404 pub d4422: f64,
406 pub d5220: f64,
408 pub d5232: f64,
410 pub d5421: f64,
412 pub d5433: f64,
414 pub dedt: f64,
416 pub del1: f64,
418 pub del2: f64,
420 pub del3: f64,
422 pub didt: f64,
424 pub dmdt: f64,
426 pub dnodt: f64,
428 pub domdt: f64,
430 pub e3: f64,
432 pub ee2: f64,
434 pub peo: f64,
436 pub pgho: f64,
438 pub pho: f64,
440 pub pinco: f64,
442 pub plo: f64,
444 pub se2: f64,
446 pub se3: f64,
448 pub sgh2: f64,
450 pub sgh3: f64,
452 pub sgh4: f64,
454 pub sh2: f64,
456 pub sh3: f64,
458 pub si2: f64,
460 pub si3: f64,
462 pub sl2: f64,
464 pub sl3: f64,
466 pub sl4: f64,
468 pub gsto: f64,
470 pub xfact: f64,
472 pub xgh2: f64,
474 pub xgh3: f64,
476 pub xgh4: f64,
478 pub xh2: f64,
480 pub xh3: f64,
482 pub xi2: f64,
484 pub xi3: f64,
486 pub xl2: f64,
488 pub xl3: f64,
490 pub xl4: f64,
492 pub xlamo: f64,
494 pub zmol: f64,
496 pub zmos: f64,
498 pub atime: f64,
500 pub xli: f64,
502 pub xni: f64,
504}
505impl Satellite {
506 pub fn new(data: &TLEData, initialize: Option<bool>) -> Self {
512 let mut this = Self::default();
513 let initialize = initialize.unwrap_or(true);
514 this.rms = data.rms;
515 this.name = data.name.clone();
516 this.number = data.number;
517 this.class = data.class;
518 this.id = data.id.clone();
519 this.date = data.date;
520 this.fdmm = data.fdmm;
521 this.sdmm = data.sdmm;
522 this.drag = data.drag;
523 this.ephemeris = data.ephemeris;
524 this.esn = data.esn;
525 this.inclination = data.inclination.to_radians();
526 this.ascension = data.ascension.to_radians();
527 this.eccentricity = data.eccentricity;
528 this.perigee = data.perigee.to_radians();
529 this.anomaly = data.anomaly.to_radians();
530 this.motion = data.motion;
532 this.revolution = data.revolution;
533 this.epochdays = data.epochdays;
534
535 this.epochyr = (this.date.year % 100) as f64;
536 this.motion /= 1440. / (2. * PI); let year = if this.epochyr < 57. { this.epochyr + 2000. } else { this.epochyr + 1900. };
542 let mdhms_result = days2mdhms(year as u16, this.epochdays);
543
544 let TimeStamp { mon, day, hr, min, sec } = mdhms_result;
545 this.jdsatepoch = jday(&Date::new_full(
546 year as u16,
547 mon as u8,
548 day as u8,
549 hr as u8,
550 min as u8,
551 sec as u8,
552 ));
553
554 if initialize {
555 sgp4init(&mut this);
556 }
557
558 this
559 }
560
561 pub fn gpu(&self) -> Vec<f64> {
566 vec![
567 self.anomaly,
568 self.motion,
569 self.eccentricity,
570 self.inclination,
571 if self.method == Method::D { 0. } else { 1. }, if self.opsmode == OperationMode::A { 0. } else { 1. }, self.drag,
574 self.mdot,
575 self.perigee,
576 self.argpdot,
577 self.ascension,
578 self.nodedot,
579 self.nodecf,
580 self.cc1,
581 self.cc4,
582 self.cc5,
583 self.t2cof,
584 self.isimp,
585 self.omgcof,
586 self.eta,
587 self.xmcof,
588 self.delmo,
589 self.d2,
590 self.d3,
591 self.d4,
592 self.sinmao,
593 self.t3cof,
594 self.t4cof,
595 self.t5cof,
596 self.irez,
597 self.d2201,
598 self.d2211,
599 self.d3210,
600 self.d3222,
601 self.d4410,
602 self.d4422,
603 self.d5220,
604 self.d5232,
605 self.d5421,
606 self.d5433,
607 self.dedt,
608 self.del1,
609 self.del2,
610 self.del3,
611 self.didt,
612 self.dmdt,
613 self.dnodt,
614 self.domdt,
615 self.gsto,
616 self.xfact,
617 self.xlamo,
618 self.atime,
619 self.xli,
620 self.xni,
621 self.aycof,
622 self.xlcof,
623 self.con41,
624 self.x1mth2,
625 self.x7thm1,
626 self.zmos,
627 self.zmol,
628 self.se2,
629 self.se3,
630 self.si2,
631 self.si3,
632 self.sl2,
633 self.sl3,
634 self.sl4,
635 self.sgh2,
636 self.sgh3,
637 self.sgh4,
638 self.sh2,
639 self.sh3,
640 self.ee2,
641 self.e3,
642 self.xi2,
643 self.xi3,
644 self.xl2,
645 self.xl3,
646 self.xl4,
647 self.xgh2,
648 self.xgh3,
649 self.xgh4,
650 self.xh2,
651 self.xh3,
652 self.peo,
653 self.pinco,
654 self.plo,
655 self.pgho,
656 self.pho,
657 ]
658 }
659
660 pub fn propagate(&self, time: &Date) -> Result<SGP4Output, SGP4ErrorOutput> {
668 let j = jday(time);
669 sgp4(self, (j - self.jdsatepoch) * MINUTES_PER_DAY)
670 }
671
672 pub fn sgp4(&self, time: f64) -> Result<SGP4Output, SGP4ErrorOutput> {
680 sgp4(self, time)
681 }
682}
683
684fn parse_float(value: &str) -> f64 {
686 let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
687
688 if let Some(caps) = re.captures(value) {
689 let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
690 let base = caps.get(2).map_or("0", |m| m.as_str());
691 let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
692 let combined = format!("{}{}", base, power);
693 return sign * combined.parse::<f64>().unwrap();
694 }
695
696 0.0
697}
698
699fn parse_drag(value: &str) -> f64 {
701 let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
702 if let Some(caps) = re.captures(value) {
703 let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
704 let base = caps.get(2).map_or("0", |m| m.as_str());
705 let base = if base.contains('.') { base.into() } else { format!("0.{}", base) };
706 let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
707 return sign * format!("{}{}", base, power).parse::<f64>().unwrap();
708 }
709
710 0.0
711}
712
713fn parse_epoch(value: &str) -> (Date, f64) {
715 let re = Regex::new(r"^\s+|\s+$").unwrap();
717 let value: String = re.replace_all(value, "").into();
718
719 let epoch = value[0..2].parse::<u16>().unwrap();
721 let days = value[2..].parse::<f64>().unwrap();
722
723 let now_year = 2025;
725 let current_epoch = (now_year % 100) as u16;
726 let century = now_year - current_epoch as i32;
727 let year = if epoch > current_epoch + 1 {
728 (century - 100 + epoch as i32) as u16
729 } else {
730 (century + epoch as i32) as u16
731 };
732
733 let ts = days2mdhms(year, days);
735
736 (
738 Date::new_full(year, ts.mon as u8, ts.day as u8, ts.hr as u8, ts.min as u8, ts.sec as u8),
739 days,
740 )
741}
742
743fn check(line: &str) -> u32 {
745 let mut sum = 0;
746
747 for c in line.chars().take(68) {
748 if c.is_ascii_digit() {
749 sum += c.to_digit(10).unwrap();
750 } else if c == '-' {
751 sum += 1;
752 }
753 }
754
755 sum % 10
756}
757
758fn alpha5_converter(s: &str) -> String {
761 if let Some(first_char) = s.chars().next() {
762 if first_char.is_ascii_digit() {
764 return s.into();
765 }
766
767 let alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
768 if let Some(idx) = alphabet.find(first_char) {
769 let rest: String = s.chars().skip(1).collect();
771 return format!("{}{}", idx + 10, rest);
772 }
773 }
774
775 s.into()
777}
778
779fn trim(s: &str) -> &str {
781 fn is_trim_char(c: char) -> bool {
782 c.is_whitespace() || c == '\u{FEFF}' || c == '\u{00A0}'
783 }
784 s.trim_matches(is_trim_char)
785}