1use chrono::prelude::*;
2use chrono::DateTime;
3use thiserror::Error;
4use uom::si::{angle, angular_velocity::radian_per_second, f64::*, length::kilometer};
5
6mod sgp4_sys;
7#[cfg(feature = "tlegen")]
8mod tlegen;
9
10#[derive(Debug, Error, PartialEq)]
11pub enum Error {
12 #[error("TLE was malformed: {0}")]
13 MalformedTwoLineElement(String),
14 #[error("{0}")]
15 UnknownError(String),
16 #[error(transparent)]
17 PropagationError(#[from] sgp4_sys::Error),
18 #[error("Optimization error: {0}")]
19 OptimizationError(String),
20}
21
22type Result<T> = std::result::Result<T, Error>;
23
24#[derive(Debug, Clone, Copy)]
29pub struct StateVector {
30 pub epoch: DateTime<Utc>,
31
32 pub position: [f64; 3],
34
35 pub velocity: [f64; 3],
37
38 pub coe: ClassicalOrbitalElements,
39}
40
41impl StateVector {
42 pub fn new(epoch: DateTime<Utc>, position: [f64; 3], velocity: [f64; 3]) -> Self {
43 Self {
44 epoch,
45 position,
46 velocity,
47 coe: sgp4_sys::to_classical_elements(&position, &velocity).into(),
48 }
49 }
50
51 pub fn semilatus_rectum(&self) -> Length {
52 self.coe.semilatus_rectum
53 }
54
55 pub fn semimajor_axis(&self) -> Length {
56 self.coe.semimajor_axis
57 }
58
59 pub fn inclination(&self) -> Angle {
60 self.coe.inclination
61 }
62
63 pub fn raan(&self) -> Angle {
64 self.coe.raan
65 }
66
67 pub fn mean_anomaly(&self) -> Angle {
68 self.coe.mean_anomaly
69 }
70
71 pub fn true_anomaly(&self) -> Angle {
72 self.coe.true_anomaly
73 }
74
75 pub fn eccentricity(&self) -> f64 {
76 self.coe.eccentricity
77 }
78
79 pub fn longitude_of_periapsis(&self) -> Angle {
80 self.coe.longitude_of_periapsis
81 }
82
83 pub fn true_longitude(&self) -> Angle {
84 self.coe.true_longitude
85 }
86
87 pub fn argument_of_perigee(&self) -> Angle {
88 self.coe.argument_of_perigee
89 }
90
91 pub fn argument_of_latitude(&self) -> Angle {
92 self.coe.argument_of_latitude
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct ClassicalOrbitalElements {
103 pub semilatus_rectum: Length,
104 pub semimajor_axis: Length,
105 pub eccentricity: f64,
106 pub inclination: Angle,
107 pub raan: Angle,
108 pub argument_of_perigee: Angle,
109 pub true_anomaly: Angle,
110 pub mean_anomaly: Angle,
111 pub argument_of_latitude: Angle,
112 pub true_longitude: Angle,
113 pub longitude_of_periapsis: Angle,
114}
115
116impl From<sgp4_sys::ClassicalOrbitalElements> for ClassicalOrbitalElements {
117 fn from(coe: sgp4_sys::ClassicalOrbitalElements) -> Self {
118 let semilatus_rectum = Length::new::<kilometer>(coe.p);
119 let semimajor_axis = Length::new::<kilometer>(coe.a);
120 let inclination = Angle::new::<angle::radian>(coe.incl);
121 let raan = Angle::new::<angle::radian>(coe.omega);
122 let mean_anomaly = Angle::new::<angle::radian>(coe.m);
123 let true_anomaly = Angle::new::<angle::radian>(coe.nu);
124 let eccentricity = coe.ecc;
125 let longitude_of_periapsis = Angle::new::<angle::radian>(coe.lonper);
126 let true_longitude = Angle::new::<angle::radian>(coe.truelon);
127 let argument_of_perigee = Angle::new::<angle::radian>(coe.argp);
128 let argument_of_latitude = Angle::new::<angle::radian>(coe.arglat);
129
130 Self {
131 semilatus_rectum,
132 semimajor_axis,
133 eccentricity,
134 inclination,
135 raan,
136 argument_of_perigee,
137 true_anomaly,
138 mean_anomaly,
139 argument_of_latitude,
140 true_longitude,
141 longitude_of_periapsis,
142 }
143 }
144}
145
146impl From<StateVector> for ClassicalOrbitalElements {
147 fn from(sv: StateVector) -> Self {
148 sv.coe
149 }
150}
151
152const TLE_LINE_LENGTH: usize = 69;
153
154#[derive(Clone)]
162pub struct TwoLineElement {
163 elements: sgp4_sys::OrbitalElementSet,
164}
165
166impl TwoLineElement {
167 pub fn new(line1: &str, line2: &str) -> Result<TwoLineElement> {
169 let line1 = line1.trim();
170 let line2 = line2.trim();
171
172 if line1.len() != TLE_LINE_LENGTH {
173 return Err(Error::MalformedTwoLineElement(format!(
174 "Line 1 is the wrong length. Expected {}, but got {}\n{}",
175 TLE_LINE_LENGTH,
176 line1.len(),
177 line1
178 )));
179 }
180
181 if line2.len() != TLE_LINE_LENGTH {
182 return Err(Error::MalformedTwoLineElement(format!(
183 "Line 2 is the wrong length. Expected {}, but got {}\n{}",
184 TLE_LINE_LENGTH,
185 line2.len(),
186 line2
187 )));
188 }
189
190 let elements = sgp4_sys::to_orbital_elements(
191 line1,
192 line2,
193 sgp4_sys::RunType::Verification,
194 sgp4_sys::OperationMode::Improved,
195 sgp4_sys::GravitationalConstant::Wgs84,
196 )
197 .map_err(|e| Error::MalformedTwoLineElement(e.to_string()))?;
198
199 Ok(TwoLineElement { elements })
200 }
201
202 pub fn from_lines(combined_lines: &str) -> Result<TwoLineElement> {
204 let lines: Vec<_> = {
205 let mut ls: Vec<_> = combined_lines
206 .split('\n')
207 .filter(|s| !s.is_empty())
208 .collect();
209 if ls.len() == 3 {
210 ls.split_off(1)
211 } else if ls.len() == 2 {
212 ls
213 } else {
214 return Err(Error::MalformedTwoLineElement(format!(
215 "Expected two lines, got {}",
216 ls.len()
217 )));
218 }
219 };
220 TwoLineElement::new(lines[0], lines[1])
221 }
222
223 pub fn epoch(&self) -> Result<DateTime<Utc>> {
225 Ok(self.elements.epoch())
226 }
227
228 pub fn mean_motion(&self) -> AngularVelocity {
229 AngularVelocity::new::<radian_per_second>(self.elements.mean_motion() / 60.)
230 }
231
232 pub fn propagate_to(&self, t: DateTime<Utc>) -> Result<StateVector> {
234 let tle_epoch = self.elements.epoch();
235 let min_since_epoch = (t - tle_epoch).num_milliseconds() as f64 / 60_000.;
239
240 let (r, v) = sgp4_sys::run_sgp4(
241 self.elements,
242 sgp4_sys::GravitationalConstant::Wgs84,
243 min_since_epoch,
244 )?;
245
246 Ok(StateVector::new(t, r.to_owned(), v.to_owned()))
247 }
248}
249
250pub struct JulianDay(f64);
255
256impl From<DateTime<Utc>> for JulianDay {
257 fn from(d: DateTime<Utc>) -> Self {
258 JulianDay(sgp4_sys::datetime_to_julian_day(d))
259 }
260}
261
262impl From<JulianDay> for DateTime<Utc> {
263 fn from(jd: JulianDay) -> Self {
264 sgp4_sys::julian_day_to_datetime(jd.0)
265 }
266}
267
268pub struct GreenwichMeanSiderealTime(f64);
274
275impl GreenwichMeanSiderealTime {
276 pub fn as_radians(&self) -> f64 {
277 self.0
278 }
279}
280
281impl From<DateTime<Utc>> for GreenwichMeanSiderealTime {
282 fn from(d: DateTime<Utc>) -> Self {
283 GreenwichMeanSiderealTime(sgp4_sys::datetime_to_gstime(d))
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 use chrono::Duration;
292 use float_cmp::approx_eq;
293
294 fn vecs_eq(l: &[f64; 3], r: &[f64; 3]) -> bool {
295 approx_eq!(f64, l[0], r[0]) && approx_eq!(f64, l[1], r[1]) && approx_eq!(f64, l[2], r[2])
296 }
297
298 #[test]
299 fn test_simple_propagation() -> Result<()> {
300 let line1 = "1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992";
301 let line2 = "2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767";
302
303 let tle = TwoLineElement::new(line1, line2)?;
304 let epoch = tle.epoch()?;
305
306 let s1 = tle.propagate_to(epoch)?;
307 let s2 = tle.propagate_to(epoch + Duration::hours(1))?;
308
309 assert!(!vecs_eq(&s1.position, &s2.position));
310 assert!(!vecs_eq(&s1.velocity, &s2.velocity));
311
312 Ok(())
313 }
314
315 #[test]
316 fn test_decay_error() -> Result<()> {
317 let line1 = "1 43051U 17071Q 22046.92182028 .07161566 12340-4 74927-3 0 9993";
318 let line2 = "2 43051 51.6207 236.5853 0009084 284.2762 75.7254 16.36736354237455";
319 let tle = TwoLineElement::new(line1, line2)?;
320 let epoch = tle.epoch()?;
321
322 let s2 = tle.propagate_to(epoch + Duration::days(30));
323 assert!(s2.is_err());
324 assert_eq!(
325 s2.unwrap_err(),
326 Error::PropagationError(sgp4_sys::Error::SatelliteDecay)
327 );
328 Ok(())
329 }
330
331 #[test]
332 fn mean_motion_round_trip() -> Result<()> {
333 let line1 = "1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992";
334 let line2 = "2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767";
335 let tle = TwoLineElement::new(line1, line2)?;
336 let mean_motion = tle.mean_motion().get::<radian_per_second>() * 24. * 60.0 * 60.0
337 / (2. * std::f64::consts::PI);
338 assert!(approx_eq!(f64, mean_motion, 15.493968, epsilon = 0.01));
339 Ok(())
340 }
341
342 #[test]
343 fn test_negative_time_propagation() -> Result<()> {
344 let line1 = "1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992";
345 let line2 = "2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767";
346
347 let tle = TwoLineElement::new(line1, line2)?;
348 let epoch = tle.epoch()?;
349
350 let s1 = tle.propagate_to(epoch)?;
351 let s2 = tle.propagate_to(epoch - Duration::days(30))?;
352
353 assert!(!vecs_eq(&s1.position, &s2.position));
354 assert!(!vecs_eq(&s1.velocity, &s2.velocity));
355
356 Ok(())
357 }
358
359 #[test]
360 fn test_tle_from_lines() -> Result<()> {
361 let lines = "1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992
362 2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767";
363
364 let _tle = TwoLineElement::from_lines(lines)?;
365 Ok(())
366 }
367
368 #[test]
369 fn test_tle_from_lines_with_header() -> Result<()> {
370 let lines = "ISS (ZARYA)
371 1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992
372 2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767";
373
374 let _tle = TwoLineElement::from_lines(lines)?;
375 Ok(())
376 }
377
378 #[test]
379 fn test_tle_from_lines_with_surrounding_whitespace() -> Result<()> {
380 let lines = "\nISS (ZARYA)
381 1 25544U 98067A 20148.21301450 .00001715 00000-0 38778-4 0 9992
382 2 25544 51.6435 92.2789 0002570 358.0648 144.9972 15.49396855228767\n";
383
384 let _tle = TwoLineElement::from_lines(lines)?;
385
386 Ok(())
387 }
388
389 #[test]
390 fn test_julian_day_identity() {
391 let t = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
392 assert_eq!(DateTime::<Utc>::from(JulianDay::from(t)), t);
393 }
394
395 #[test]
396 fn test_gmst_conversion() {
397 let t = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
398 let a: f64 = 100.1218209532; let a_rad = a.to_radians();
400 assert!(sgp4_sys::close(
401 GreenwichMeanSiderealTime::from(t).as_radians(),
402 a_rad
403 ));
404 }
405}