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)]
281pub struct Satellite {
282 pub init: bool, pub name: String, pub number: f64,
290 pub class: Classification,
292 pub id: String, pub date: Date, pub epochyr: f64,
298 pub epochdays: f64,
300 pub jdsatepoch: f64,
302 pub fdmm: f64,
304 pub sdmm: f64,
306 pub drag: f64,
308 pub ephemeris: f64,
310 pub esn: f64,
312 pub inclination: f64,
315 pub ascension: f64,
317 pub eccentricity: f64,
319 pub perigee: f64,
321 pub anomaly: f64,
323 pub motion: f64,
325 pub revolution: f64,
327 pub opsmode: OperationMode,
330 pub rms: Option<f64>,
332 pub isimp: f64,
335 pub method: Method,
337 pub aycof: f64,
339 pub con41: f64,
341 pub cc1: f64,
343 pub cc4: f64,
345 pub cc5: f64,
347 pub d2: f64,
349 pub d3: f64,
351 pub d4: f64,
353 pub delmo: f64,
355 pub eta: f64,
357 pub argpdot: f64,
359 pub omgcof: f64,
361 pub sinmao: f64,
363 pub t2cof: f64,
365 pub t3cof: f64,
367 pub t4cof: f64,
369 pub t5cof: f64,
371 pub x1mth2: f64,
373 pub x7thm1: f64,
375 pub mdot: f64,
377 pub nodedot: f64,
379 pub xlcof: f64,
381 pub xmcof: f64,
383 pub nodecf: f64,
385 pub irez: f64,
388 pub d2201: f64,
390 pub d2211: f64,
392 pub d3210: f64,
394 pub d3222: f64,
396 pub d4410: f64,
398 pub d4422: f64,
400 pub d5220: f64,
402 pub d5232: f64,
404 pub d5421: f64,
406 pub d5433: f64,
408 pub dedt: f64,
410 pub del1: f64,
412 pub del2: f64,
414 pub del3: f64,
416 pub didt: f64,
418 pub dmdt: f64,
420 pub dnodt: f64,
422 pub domdt: f64,
424 pub e3: f64,
426 pub ee2: f64,
428 pub peo: f64,
430 pub pgho: f64,
432 pub pho: f64,
434 pub pinco: f64,
436 pub plo: f64,
438 pub se2: f64,
440 pub se3: f64,
442 pub sgh2: f64,
444 pub sgh3: f64,
446 pub sgh4: f64,
448 pub sh2: f64,
450 pub sh3: f64,
452 pub si2: f64,
454 pub si3: f64,
456 pub sl2: f64,
458 pub sl3: f64,
460 pub sl4: f64,
462 pub gsto: f64,
464 pub xfact: f64,
466 pub xgh2: f64,
468 pub xgh3: f64,
470 pub xgh4: f64,
472 pub xh2: f64,
474 pub xh3: f64,
476 pub xi2: f64,
478 pub xi3: f64,
480 pub xl2: f64,
482 pub xl3: f64,
484 pub xl4: f64,
486 pub xlamo: f64,
488 pub zmol: f64,
490 pub zmos: f64,
492 pub atime: f64,
494 pub xli: f64,
496 pub xni: f64,
498}
499impl Satellite {
500 pub fn new(data: &TLEData, initialize: Option<bool>) -> Self {
506 let mut this = Self::default();
507 let initialize = initialize.unwrap_or(true);
508 this.rms = data.rms;
509 this.name = data.name.clone();
510 this.number = data.number;
511 this.class = data.class;
512 this.id = data.id.clone();
513 this.date = data.date;
514 this.fdmm = data.fdmm;
515 this.sdmm = data.sdmm;
516 this.drag = data.drag;
517 this.ephemeris = data.ephemeris;
518 this.esn = data.esn;
519 this.inclination = data.inclination.to_radians();
520 this.ascension = data.ascension.to_radians();
521 this.eccentricity = data.eccentricity;
522 this.perigee = data.perigee.to_radians();
523 this.anomaly = data.anomaly.to_radians();
524 this.motion = data.motion;
526 this.revolution = data.revolution;
527 this.epochdays = data.epochdays;
528
529 this.epochyr = (this.date.year % 100) as f64;
530 this.motion /= 1440. / (2. * PI); let year = if this.epochyr < 57. { this.epochyr + 2000. } else { this.epochyr + 1900. };
536 let mdhms_result = days2mdhms(year as u16, this.epochdays);
537
538 let TimeStamp { mon, day, hr, min, sec } = mdhms_result;
539 this.jdsatepoch = jday(&Date::new_full(
540 year as u16,
541 mon as u8,
542 day as u8,
543 hr as u8,
544 min as u8,
545 sec as u8,
546 ));
547
548 if initialize {
549 sgp4init(&mut this);
550 }
551
552 this
553 }
554
555 pub fn gpu(&self) -> Vec<f64> {
560 vec![
561 self.anomaly,
562 self.motion,
563 self.eccentricity,
564 self.inclination,
565 if self.method == Method::D { 0. } else { 1. }, if self.opsmode == OperationMode::A { 0. } else { 1. }, self.drag,
568 self.mdot,
569 self.perigee,
570 self.argpdot,
571 self.ascension,
572 self.nodedot,
573 self.nodecf,
574 self.cc1,
575 self.cc4,
576 self.cc5,
577 self.t2cof,
578 self.isimp,
579 self.omgcof,
580 self.eta,
581 self.xmcof,
582 self.delmo,
583 self.d2,
584 self.d3,
585 self.d4,
586 self.sinmao,
587 self.t3cof,
588 self.t4cof,
589 self.t5cof,
590 self.irez,
591 self.d2201,
592 self.d2211,
593 self.d3210,
594 self.d3222,
595 self.d4410,
596 self.d4422,
597 self.d5220,
598 self.d5232,
599 self.d5421,
600 self.d5433,
601 self.dedt,
602 self.del1,
603 self.del2,
604 self.del3,
605 self.didt,
606 self.dmdt,
607 self.dnodt,
608 self.domdt,
609 self.gsto,
610 self.xfact,
611 self.xlamo,
612 self.atime,
613 self.xli,
614 self.xni,
615 self.aycof,
616 self.xlcof,
617 self.con41,
618 self.x1mth2,
619 self.x7thm1,
620 self.zmos,
621 self.zmol,
622 self.se2,
623 self.se3,
624 self.si2,
625 self.si3,
626 self.sl2,
627 self.sl3,
628 self.sl4,
629 self.sgh2,
630 self.sgh3,
631 self.sgh4,
632 self.sh2,
633 self.sh3,
634 self.ee2,
635 self.e3,
636 self.xi2,
637 self.xi3,
638 self.xl2,
639 self.xl3,
640 self.xl4,
641 self.xgh2,
642 self.xgh3,
643 self.xgh4,
644 self.xh2,
645 self.xh3,
646 self.peo,
647 self.pinco,
648 self.plo,
649 self.pgho,
650 self.pho,
651 ]
652 }
653
654 pub fn propagate(&self, time: &Date) -> Result<SGP4Output, SGP4ErrorOutput> {
662 let j = jday(time);
663 sgp4(self, (j - self.jdsatepoch) * MINUTES_PER_DAY)
664 }
665
666 pub fn sgp4(&self, time: f64) -> Result<SGP4Output, SGP4ErrorOutput> {
674 sgp4(self, time)
675 }
676}
677
678fn parse_float(value: &str) -> f64 {
680 let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
681
682 if let Some(caps) = re.captures(value) {
683 let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
684 let base = caps.get(2).map_or("0", |m| m.as_str());
685 let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
686 let combined = format!("{}{}", base, power);
687 return sign * combined.parse::<f64>().unwrap();
688 }
689
690 0.0
691}
692
693fn parse_drag(value: &str) -> f64 {
695 let re = Regex::new(r"([-])?([.\d]+)([+-]\d+)?").unwrap();
696 if let Some(caps) = re.captures(value) {
697 let sign = if caps.get(1).map_or("", |m| m.as_str()) == "-" { -1.0 } else { 1.0 };
698 let base = caps.get(2).map_or("0", |m| m.as_str());
699 let base = if base.contains('.') { base.into() } else { format!("0.{}", base) };
700 let power = caps.get(3).map_or_else(|| "e0".into(), |m| format!("e{}", m.as_str()));
701 return sign * format!("{}{}", base, power).parse::<f64>().unwrap();
702 }
703
704 0.0
705}
706
707fn parse_epoch(value: &str) -> (Date, f64) {
709 let re = Regex::new(r"^\s+|\s+$").unwrap();
711 let value: String = re.replace_all(value, "").into();
712
713 let epoch = value[0..2].parse::<u16>().unwrap();
715 let days = value[2..].parse::<f64>().unwrap();
716
717 let now_year = 2025;
719 let current_epoch = (now_year % 100) as u16;
720 let century = now_year - current_epoch as i32;
721 let year = if epoch > current_epoch + 1 {
722 (century - 100 + epoch as i32) as u16
723 } else {
724 (century + epoch as i32) as u16
725 };
726
727 let ts = days2mdhms(year, days);
729
730 (
732 Date::new_full(year, ts.mon as u8, ts.day as u8, ts.hr as u8, ts.min as u8, ts.sec as u8),
733 days,
734 )
735}
736
737fn check(line: &str) -> u32 {
739 let mut sum = 0;
740
741 for c in line.chars().take(68) {
742 if c.is_ascii_digit() {
743 sum += c.to_digit(10).unwrap();
744 } else if c == '-' {
745 sum += 1;
746 }
747 }
748
749 sum % 10
750}
751
752fn alpha5_converter(s: &str) -> String {
755 if let Some(first_char) = s.chars().next() {
756 if first_char.is_ascii_digit() {
758 return s.into();
759 }
760
761 let alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
762 if let Some(idx) = alphabet.find(first_char) {
763 let rest: String = s.chars().skip(1).collect();
765 return format!("{}{}", idx + 10, rest);
766 }
767 }
768
769 s.into()
771}
772
773fn trim(s: &str) -> &str {
775 fn is_trim_char(c: char) -> bool {
776 c.is_whitespace() || c == '\u{FEFF}' || c == '\u{00A0}'
777 }
778 s.trim_matches(is_trim_char)
779}