parse_tle/
tle.rs

1/*
2Parser for TLE
3*/
4use serde::{Deserialize, Serialize};
5use std::convert::From;
6use std::fmt::{Display, Formatter, Result};
7use std::fs;
8use std::io::{BufWriter, Read, Write};
9
10use hifitime::prelude::*;
11
12// TODO-TD: add .txt write
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct TLE {
15    pub name: String,
16    pub catalog_number: String,
17    pub classification: String,
18    pub international_designator: String,
19    pub epoch: Epoch,
20    pub mean_motion_1: f64,
21    pub mean_motion_2: f64,
22    pub radiation_pressure: f64,
23    pub ephemeris_type: u8,
24    pub element_set_number: u64,
25    pub inc: f64,
26    pub raan: f64,
27    pub eccentricity: f64,
28    pub arg_perigee: f64,
29    pub mean_anomaly: f64,
30    pub mean_motion: f64,
31    pub rev_num: u32,
32}
33
34/// From method for `TLE` struct
35impl From<&str> for TLE {
36    /// From
37    fn from(tle_str: &str) -> TLE {
38        return parse(tle_str);
39    }
40}
41
42/// Write TLE struct to JSON formatted file
43///
44/// Inputs
45/// ------
46/// tle: `TLE`
47///     tle struct
48///
49/// path_str: `&String`
50///     Path to write to
51///
52pub fn write_json(tle: &TLE, path_str: &String) {
53    let file: fs::File = fs::File::create(path_str).expect("Unable to create file");
54    let mut writer: BufWriter<fs::File> = BufWriter::new(file);
55    serde_json::to_writer(&mut writer, tle).unwrap();
56    writer.flush().unwrap();
57}
58
59/// Read TLEs from a file path 
60pub fn tles_from_file(path: &str) -> Vec<TLE> {
61    if path.contains(".json") {
62        return vec![read_json(path)];
63    } else {
64        return read_txt(path);
65    }
66}
67
68
69/// Read TLE struct from JSON formatted file
70///
71/// Inputs
72/// ------
73/// json_str: `&str`
74///     File containing json data
75///
76/// Outputs
77/// -------
78/// tle_values: `TLE`
79pub fn read_json(json_path: &str) -> TLE {
80    let mut file: fs::File =
81        fs::File::open(json_path).expect(format!("{json_path} could not be openned").as_str());
82
83    let mut data: String = String::new();
84    file.read_to_string(&mut data)
85        .expect(format!("{json_path} could not be read").as_str());
86
87    // TODO-TD: check for multiple TLEs in json
88    let tle_values: TLE = serde_json::from_str(&data).expect("JSON was not well-formatted");
89    return tle_values;
90}
91
92/// Display method for `TLE` struct
93impl Display for TLE {
94    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
95        write!(
96            formatter,
97            "{}\nCatalog #: {}\nClassification: {}\nIntl Desig: {}\nSet #: {}\nEpoch: {}\nMean Motion: {}\nMean Motion prime: {}\nMean Motion prime 2: {}\nRadiation Pressure: {}\nInclination: {}\nRaan: {}\nEccentricity: {}\nArgument of Perigee: {}\nMean Anomaly: {}\nRevolution #: {}",
98            self.name,
99            self.catalog_number,
100            self.classification,
101            self.international_designator,
102            self.element_set_number,
103            self.epoch,
104            self.mean_motion,
105            self.mean_motion_1,
106            self.mean_motion_2,
107            self.radiation_pressure,
108            self.inc,
109            self.raan,
110            self.eccentricity,
111            self.arg_perigee,
112            self.mean_anomaly,
113            self.rev_num
114        )
115    }
116}
117
118/// Parse standard Two Line Element
119///
120/// Inputs
121/// ------
122/// tle_str : `&str`
123///     NORAD Two Line Element Identification String
124///
125/// Outputs
126/// -------
127/// tle: `TLE`
128///     TLE struct
129pub fn parse(tle_str: &str) -> TLE {
130    let lines: Vec<&str> = tle_str.lines().collect();
131    let n_lines: usize = lines.len();
132
133    let (idx_1, idx_2) = match n_lines {
134        3 => (1, 2),
135        2 => (0, 1),
136        _ => panic!("Invalid number of lines"),
137    };
138
139    let line_1: String = lines[idx_1].trim().to_string();
140    validate_checksum(&line_1);
141
142    let catalog_number: &str = &line_1[2..=6];
143
144    let classification: &str = &line_1[7..=7];
145
146    let intnl_desig: &str = &line_1[9..=16];
147
148    // name
149    let name: &str;
150    if lines.len() == 3 {
151        name = lines[0];
152    } else {
153        name = intnl_desig
154    }
155
156    let epoch_str: &str = &line_1[18..=31];
157
158    let year_endian: i32 = epoch_str[0..=1]
159        .to_string()
160        .parse::<i32>()
161        .expect("Unable to parse year_endian value at epoch_str[0..=1]");
162
163    // epoch_year
164    let epoch_year: i32;
165    if year_endian < 57 {
166        epoch_year = 2000 + year_endian;
167    } else {
168        epoch_year = 1900 + year_endian;
169    }
170
171    let binding: String = epoch_str[2..].to_string();
172    let epoch_day_full: Vec<&str> = binding.split_terminator('.').collect();
173    let day_of_year: u32 = epoch_day_full[0]
174        .to_string()
175        .parse::<u32>()
176        .expect("Unable to parse day_of_year value at epoch_day_full[0]");
177
178    let month_day: (u8, u8) = calc_month_day(day_of_year, epoch_year as u32);
179
180    let percent_of_day: f64 = (".".to_owned() + epoch_day_full[1])
181        .parse::<f64>()
182        .expect("Unable to parse percent_of_day value at epoch_day_full[1]");
183
184    let hours_dec: f64 = percent_of_day * 24.0;
185
186    // epoch_hours
187    let hours_whole: u8 = hours_dec.div_euclid(24.0).floor() as u8;
188    let hours_part: f64 = hours_dec.rem_euclid(24.0);
189    let minutes_dec: f64 = hours_part * 60.;
190
191    // epoch_min
192    let minutes_whole: u8 = minutes_dec.div_euclid(60.).floor() as u8;
193    let minutes_part: f64 = minutes_dec.rem_euclid(60.);
194    let seconds_dec: f64 = minutes_part * 60.;
195
196    // epoch_sec
197    let seconds_whole: u8 = seconds_dec.div_euclid(60.).floor() as u8;
198
199    // hifitime epoch
200    let full_epoch: Epoch = Epoch::from_gregorian_hms(
201        epoch_year as i32,
202        month_day.0,
203        month_day.1,
204        hours_whole,
205        minutes_whole,
206        seconds_whole,
207        TimeScale::UTC,
208    );
209
210    // mean_motion_1
211    let mean_motion_1_sign: f64 = (line_1[33..=33].to_string() + "1")
212        .trim()
213        .parse::<f64>()
214        .expect("Unable to parse mean_motion_1_sign value at line_1[33..=33]");
215
216    let mean_motion_1_base: f64 = line_1[34..=42]
217        .to_string()
218        .parse::<f64>()
219        .expect("Unable to parse mean_motion_1_base value at line_1[34..=42]");
220    let mean_motion_1: f64 = mean_motion_1_base * mean_motion_1_sign;
221
222    // mean_motion_2
223    let mean_mot_2_sign: f64 = (line_1[44..=44].to_string() + "1")
224        .trim()
225        .parse::<f64>()
226        .expect("Invalid mean motion 2 sign");
227    let mean_mot_2_base: f64 = line_1[45..=49].to_string().parse::<f64>().unwrap();
228    let mean_mot_2_exp = line_1[50..=51].to_string().parse::<f64>().unwrap();
229    let mean_mot_2_pow: f64 = 10_f64.powf(mean_mot_2_exp);
230    let mean_motion_2: f64 = (mean_mot_2_sign * mean_mot_2_base) * mean_mot_2_pow;
231
232    // radiation_pressure
233    let rad_press_sign: f64 = (line_1[53..=53].to_string() + "1")
234        .trim()
235        .parse::<f64>()
236        .expect("Invalid radiation pressure sign");
237    let rad_press_base: f64 = line_1[54..=58].to_string().parse::<f64>().unwrap();
238    let rad_press_exp = line_1[59..=60].to_string().parse::<f64>().unwrap();
239    let rad_press_pow: f64 = 10_f64.powf(rad_press_exp);
240    let radiation_pressure: f64 = rad_press_sign * rad_press_base * rad_press_pow;
241
242    let ephemeris_type: u8 = line_1[62..=62]
243        .to_string()
244        .parse::<u8>()
245        .expect("Unable to parse ephemeris_type value at line_1[62..=62]");
246
247    let element_set_number: u64 = line_1[64..=67]
248        .to_string()
249        .trim()
250        .parse::<u64>()
251        .expect("Unable to parse element_set_number value at line_1[64..=67]");
252
253    let line2: String = lines[idx_2].trim().to_string();
254    validate_checksum(&line2);
255
256    // --- Keplerian Parameters
257    // inc
258    let inc: f64 = line2[8..=15].to_string().trim().parse::<f64>().expect("Invalid inclination angle");
259
260    // raan
261    let raan: f64 = line2[17..=24].to_string().trim().parse::<f64>().expect("Invalid right angle of ascending node");
262
263    // eccentricity
264    let eccentricity: f64 = (".".to_owned() + &line2[26..=32]).parse::<f64>().expect("Invalid eccenticity");
265
266    // arg_perigee
267    let arg_perigee: f64 = line2[34..=41].to_string().trim().parse::<f64>().expect("Invalid argument of perigee");
268
269    // mean_anomaly
270    let mean_anomaly: f64 = line2[44..=50].to_string().parse::<f64>().expect("Invalid mean anomaly");
271
272    // mean_motion
273    let mean_motion: f64 = line2[52..=62].to_string().trim().parse::<f64>().expect("Invalid mean motion");
274
275    // rev_num
276    let rev_num: u32 = line2[64..=68].to_string().trim().parse::<u32>().expect("Invalid revolution number");
277
278    let tle: TLE = TLE {
279        name: name.trim().to_string(),
280        catalog_number: catalog_number.trim().to_string(),
281        classification: classification.trim().to_string(),
282        international_designator: intnl_desig.trim().to_string(),
283        epoch: full_epoch,
284        mean_motion_1: mean_motion_1,
285        mean_motion_2: mean_motion_2,
286        radiation_pressure: radiation_pressure,
287        ephemeris_type: ephemeris_type,
288        element_set_number: element_set_number,
289        inc: inc,
290        raan: raan,
291        eccentricity: eccentricity,
292        arg_perigee: arg_perigee,
293        mean_anomaly: mean_anomaly,
294        mean_motion: mean_motion,
295        rev_num: rev_num,
296    };
297
298    return tle;
299}
300
301/// Run checksum on TLE line
302///   
303/// Inputs
304/// ------
305/// line: `&String`
306///     Line to checksum
307pub fn validate_checksum(line: &String) {
308    let mut checksum: u32 = 0;
309    for i_char in line.chars() {
310        if i_char == '-' {
311            checksum += 1;
312        } else if i_char != ' ' && i_char.is_numeric() {
313            checksum += i_char
314                .to_string()
315                .parse::<u32>()
316                .expect(format!("Unable to parse {} as u32", i_char).as_str());
317        }
318    }
319    let tle_checksum: u32 = line[68..=68]
320        .to_string()
321        .parse::<u32>()
322        .expect("Unable to parse checksum value");
323
324    // NOTE: Need to subtract the final due to iteration over entire line
325    let mod_10: u32 = (checksum - tle_checksum) % 10;
326
327    assert!(
328        mod_10 == tle_checksum,
329        "calculated = {}, tle value = {}",
330        mod_10,
331        tle_checksum
332    );
333}
334
335/// Convert day of year, year to month, day
336///
337/// Inputs
338/// ------
339/// day_of_year: `u32`
340///     Day of year (1-365)
341///
342/// year: `u32`
343///     Year (e.g. 2020)
344///
345/// Outputs
346/// -------
347/// month: `u8`
348///     Month (1-12)
349///
350/// day: `u8`
351///     Day of month (1-31)
352pub fn calc_month_day(day_of_year: u32, year: u32) -> (u8, u8) {
353    assert!(day_of_year < 366, "Day of year must be less than 366");
354
355    let feb_days: u32;
356    if check_if_leap_year(year) {
357        feb_days = 29;
358    } else {
359        feb_days = 28;
360    }
361
362    let month_lengths: Vec<u32> = vec![31, feb_days, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
363
364    let mut month: u8 = 1;
365    let mut sum_days: u32 = month_lengths[0];
366
367    while sum_days < day_of_year - month_lengths[month as usize - 1] {
368        month += 1;
369        sum_days += month_lengths[month as usize - 1];
370    }
371
372    let month: u8 = month;
373    let day: u32 = day_of_year - sum_days;
374
375    return (month, day as u8);
376}
377
378/// Check if the year is a leap year
379///
380/// Inputs
381/// ------
382/// year: `u32`
383///     Gregorian Year of common era.
384///
385/// Outputs
386/// -------
387/// is_leap_year: `bool`
388///     Boolean determining if year is a leap year
389fn check_if_leap_year(year: u32) -> bool {
390    let rule1: bool = year % 4 == 0;
391    let rule2: bool = year % 100 != 0;
392    let rule3: bool = year % 400 == 0;
393    let is_leap_year: bool = rule1 && (rule2 || rule3);
394    return is_leap_year;
395}
396
397/// Query celestrak.org api for TLE
398pub fn query_celestrak(query: &str, value: &str, verbose: bool) -> Vec<TLE> {
399    let url: String = "https://celestrak.org/NORAD/elements/gp.php?".to_owned()
400        + query
401        + "="
402        + value
403        + "&FORMAT=tle";
404
405    let mut response = reqwest::blocking::get(url).expect("Expected response");
406    let mut body = String::new();
407    response
408        .read_to_string(&mut body)
409        .expect("Unable to read request");
410
411    if verbose {
412        println!("\n Site Status: {}", response.status());
413        println!("\n Site Headers:\n{:#?}", response.headers());
414        println!("\n Site Body:\n{}", body);
415    }
416
417    return read_multi_tle(&body.as_str());
418}
419
420/// Parse raw .txt file 
421pub fn read_txt(path: &str) -> Vec<TLE> {
422    let contents: String = fs::read_to_string(path)
423        .expect(format!("Unable to read file:\n{}", path).as_str());
424
425    return read_multi_tle(contents.as_str());
426}
427
428/// Sometimes string bodies will hold multiple TLEs, it's good to be adaptive
429pub fn read_multi_tle(contents: &str) -> Vec<TLE> {
430    let mut tles: Vec<TLE> = vec![];
431    let lines: Vec<&str> = contents.lines().collect();
432    // TODO-TD: check the format of the file more closely
433    for i_tle in 0..(lines.len() / 3) {
434        let idx: usize = 3 * i_tle;
435        let line1: &str = lines[idx];
436        let line2: &str = lines[idx + 1];
437        let line3: &str = lines[idx + 2];
438
439        let together: String = format!("{line1}\n{line2}\n{line3}\n");
440        tles.append(&mut vec![parse(&together.as_str())]);
441    }
442    return tles;
443}
444
445
446#[cfg(test)]
447mod tle_tests {
448    use super::*;
449
450    #[test]
451    fn test_calc_month_day() {
452        let year: u32 = 2023;
453        let day_of_year: u32 = 78;
454        let md = calc_month_day(day_of_year, year);
455
456        assert_eq!(md.0, 2);
457        assert_eq!(md.1, 19);
458    }
459
460    #[test]
461    fn test_check_if_leap_year() {
462        let test_year: u32 = 2022;
463        let is_leap_year: bool = check_if_leap_year(test_year);
464        assert_eq!(is_leap_year, false);
465    }
466
467    #[test]
468    fn test_parser() {
469        let sample_tle: &str = "CHANDRAYAAN-3
470        1 57320U 23098A   23208.62000000  .00000392  00000+0  00000+0 0  9994
471        2 57320  21.3360   6.1160 9054012 182.9630  18.4770  0.46841359   195";
472
473        let chandrayaan_3: TLE = parse(sample_tle);
474
475        assert_eq!(chandrayaan_3.name, "CHANDRAYAAN-3".to_string());
476
477        assert_eq!(chandrayaan_3.inc, 21.3360);
478    }
479}