1use std::num::ParseFloatError;
6
7use glam::DVec3;
8use lox_bodies::{DynOrigin, Origin};
9use lox_core::coords::{Cartesian, CartesianTrajectory, TimeStampedCartesian};
10use lox_ephem::Ephemeris;
11use lox_frames::{DynFrame, Icrf, ReferenceFrame, rotations::TryRotation, traits::frame_id};
12use lox_time::{
13 Time,
14 deltas::TimeDelta,
15 offsets::{DefaultOffsetProvider, Offset},
16 time_scales::{DynTimeScale, Tai, Tdb, TimeScale},
17 utc::Utc,
18};
19use thiserror::Error;
20
21use lox_time::intervals::TimeInterval;
22
23use crate::propagators::Propagator;
24
25use super::{CartesianOrbit, Orbit, TrajectorError};
26
27#[derive(Debug, Clone)]
29pub struct Trajectory<T: TimeScale, O: Origin, R: ReferenceFrame> {
30 epoch: Time<T>,
31 origin: O,
32 frame: R,
33 data: CartesianTrajectory,
34}
35
36impl<T, O, R> Trajectory<T, O, R>
37where
38 T: TimeScale + Copy,
39 O: Origin + Copy,
40 R: ReferenceFrame + Copy,
41{
42 pub fn new(states: impl IntoIterator<Item = CartesianOrbit<T, O, R>>) -> Self {
44 let mut states = states.into_iter().peekable();
45 let first = states.peek().unwrap();
46 let epoch = first.time();
47 let origin = first.origin();
48 let frame = first.reference_frame();
49 let data = states
50 .map(|orb| {
51 let time = orb.time() - epoch;
52 TimeStampedCartesian {
53 time,
54 state: orb.state(),
55 }
56 })
57 .collect();
58 Self::from_parts(epoch, origin, frame, data)
59 }
60
61 pub fn from_parts(epoch: Time<T>, origin: O, frame: R, data: CartesianTrajectory) -> Self {
63 Self {
64 epoch,
65 origin,
66 frame,
67 data,
68 }
69 }
70
71 pub fn into_parts(self) -> (Time<T>, O, R, CartesianTrajectory) {
73 (self.epoch, self.origin, self.frame, self.data)
74 }
75
76 pub fn try_new(
78 states: impl IntoIterator<Item = CartesianOrbit<T, O, R>>,
79 ) -> Result<Self, TrajectorError> {
80 let mut states = states.into_iter().peekable();
81 for i in 0..2 {
82 states.peek().ok_or(TrajectorError::InsufficientStates(i))?;
83 }
84 Ok(Self::new(states))
85 }
86
87 pub fn at(&self, time: Time<T>) -> CartesianOrbit<T, O, R> {
89 let t = (time - self.epoch).to_seconds().to_f64();
90 let state = self.data.at(t);
91 Orbit::from_state(state, time, self.origin, self.frame)
92 }
93
94 pub fn into_frame<R1, P>(
96 self,
97 frame: R1,
98 provider: &P,
99 ) -> Result<Trajectory<T, O, R1>, Box<dyn std::error::Error>>
100 where
101 R1: ReferenceFrame + Copy,
102 P: TryRotation<R, R1, T>,
103 {
104 if frame_id(&self.frame) == frame_id(&frame) {
105 return Ok(Trajectory::from_parts(
106 self.epoch,
107 self.origin,
108 frame,
109 self.data,
110 ));
111 }
112
113 let data: Result<CartesianTrajectory, P::Error> = self
114 .data
115 .into_iter()
116 .map(|TimeStampedCartesian { time, state }| {
117 let t = self.epoch + time;
118 provider
119 .try_rotation(self.frame, frame, t)
120 .map(|rot| TimeStampedCartesian {
121 time,
122 state: rot * state,
123 })
124 })
125 .collect();
126
127 Ok(Trajectory::from_parts(
128 self.epoch,
129 self.origin,
130 frame,
131 data?,
132 ))
133 }
134
135 pub fn epoch(&self) -> Time<T> {
137 self.epoch
138 }
139
140 pub fn origin(&self) -> O {
142 self.origin
143 }
144
145 pub fn reference_frame(&self) -> R {
147 self.frame
148 }
149
150 pub fn start_time(&self) -> Time<T> {
152 self.epoch
153 }
154
155 pub fn end_time(&self) -> Time<T> {
157 let time_steps = self.data.time_steps();
158 let last = time_steps.last().copied().unwrap_or(0.0);
159 self.epoch + TimeDelta::from_seconds_f64(last)
160 }
161
162 pub fn times(&self) -> Vec<Time<T>> {
164 let time_steps = self.data.time_steps();
165 time_steps
166 .iter()
167 .map(|&t| self.epoch + TimeDelta::from_seconds_f64(t))
168 .collect()
169 }
170
171 pub fn states(&self) -> Vec<CartesianOrbit<T, O, R>> {
173 let time_steps = self.data.time_steps();
174 time_steps
175 .iter()
176 .map(|&t| {
177 let state = self.data.at(t);
178 let time = self.epoch + TimeDelta::from_seconds_f64(t);
179 Orbit::from_state(state, time, self.origin, self.frame)
180 })
181 .collect()
182 }
183
184 pub fn to_vec(&self) -> Vec<Vec<f64>> {
186 let time_steps = self.data.time_steps();
187 time_steps
188 .iter()
189 .map(|&t| {
190 let state = self.data.at(t);
191 vec![
192 t,
193 state.position().x,
194 state.position().y,
195 state.position().z,
196 state.velocity().x,
197 state.velocity().y,
198 state.velocity().z,
199 ]
200 })
201 .collect()
202 }
203
204 pub fn interpolate(&self, dt: TimeDelta) -> CartesianOrbit<T, O, R> {
206 let t = dt.to_seconds().to_f64();
207 let state = self.data.at(t);
208 Orbit::from_state(state, self.epoch + dt, self.origin, self.frame)
209 }
210
211 pub fn interpolate_at(&self, time: Time<T>) -> CartesianOrbit<T, O, R> {
213 self.interpolate(time - self.epoch)
214 }
215
216 pub fn position(&self, t: f64) -> DVec3 {
218 self.data.position(t)
219 }
220
221 pub fn velocity(&self, t: f64) -> DVec3 {
223 self.data.velocity(t)
224 }
225
226 pub fn find_events<F>(
231 &self,
232 func: F,
233 step: TimeDelta,
234 ) -> Result<Vec<crate::events::Event<T>>, crate::events::DetectError>
235 where
236 F: Fn(CartesianOrbit<T, O, R>) -> f64,
237 {
238 let interval = TimeInterval::new(self.start_time(), self.end_time());
239 crate::events::find_events(|t| func(self.interpolate_at(t)), interval, step)
240 }
241
242 pub fn try_find_events<F, E>(
245 &self,
246 func: F,
247 step: TimeDelta,
248 ) -> Result<Vec<crate::events::Event<T>>, crate::events::DetectError>
249 where
250 F: Fn(CartesianOrbit<T, O, R>) -> Result<f64, E>,
251 E: std::error::Error + Send + Sync + 'static,
252 {
253 let interval = TimeInterval::new(self.start_time(), self.end_time());
254 crate::events::try_find_events(|t| func(self.interpolate_at(t)), interval, step)
255 }
256
257 pub fn find_windows<F>(
260 &self,
261 func: F,
262 step: TimeDelta,
263 ) -> Result<Vec<TimeInterval<T>>, crate::events::DetectError>
264 where
265 F: Fn(CartesianOrbit<T, O, R>) -> f64,
266 {
267 let interval = TimeInterval::new(self.start_time(), self.end_time());
268 crate::events::find_windows(|t| func(self.interpolate_at(t)), interval, step)
269 }
270
271 pub fn try_find_windows<F, E>(
274 &self,
275 func: F,
276 step: TimeDelta,
277 ) -> Result<Vec<TimeInterval<T>>, crate::events::DetectError>
278 where
279 F: Fn(CartesianOrbit<T, O, R>) -> Result<f64, E>,
280 E: std::error::Error + Send + Sync + 'static,
281 {
282 let interval = TimeInterval::new(self.start_time(), self.end_time());
283 crate::events::try_find_windows(|t| func(self.interpolate_at(t)), interval, step)
284 }
285}
286
287impl<T, O, R> Trajectory<T, O, R>
288where
289 T: TimeScale + Copy + Into<DynTimeScale>,
290 O: Origin + Copy + Into<DynOrigin>,
291 R: ReferenceFrame + Copy + Into<DynFrame>,
292{
293 pub fn into_dyn(self) -> DynTrajectory {
295 Trajectory::from_parts(
296 self.epoch.into_dyn(),
297 self.origin.into(),
298 self.frame.into(),
299 self.data,
300 )
301 }
302}
303
304impl<T, O, R> Propagator<T, O> for Trajectory<T, O, R>
305where
306 T: TimeScale + Copy + PartialOrd,
307 O: Origin + Copy,
308 R: ReferenceFrame + Copy,
309{
310 type Frame = R;
311 type Error = TrajectorError;
312
313 fn state_at(&self, time: Time<T>) -> Result<CartesianOrbit<T, O, R>, TrajectorError> {
314 Ok(self.at(time))
315 }
316
317 fn propagate(&self, interval: TimeInterval<T>) -> Result<Trajectory<T, O, R>, Self::Error> {
318 let mut states = Vec::new();
319 states.push(self.at(interval.start()));
320 for s in self.states() {
321 if s.time() > interval.start() && s.time() < interval.end() {
322 states.push(s);
323 }
324 }
325 states.push(self.at(interval.end()));
326 Trajectory::try_new(states)
327 }
328}
329
330#[derive(Clone, Debug, Error, PartialEq)]
332pub enum TrajectoryError {
333 #[error("`states` must have at least 2 elements but had {0}")]
335 InsufficientStates(usize),
336 #[error("CSV error: {0}")]
338 CsvError(String),
339}
340
341impl From<csv::Error> for TrajectoryError {
342 fn from(err: csv::Error) -> Self {
343 TrajectoryError::CsvError(err.to_string())
344 }
345}
346
347#[derive(Debug, Error)]
349pub enum TrajectoryTransformationError {
350 #[error(transparent)]
352 TrajectoryError(#[from] TrajectorError),
353 #[error("state transformation failed: {0}")]
355 StateTransformationError(String),
356}
357
358impl<T, O> Trajectory<T, O, Icrf>
359where
360 T: TimeScale + Copy,
361 O: Origin + Copy,
362 DefaultOffsetProvider: Offset<T, Tdb>,
363{
364 pub fn to_origin<O1: Origin + Copy, E: Ephemeris>(
366 &self,
367 target: O1,
368 ephemeris: &E,
369 ) -> Result<Trajectory<T, O1, Icrf>, TrajectoryTransformationError> {
370 if self.origin().id() == target.id() {
371 return Ok(Trajectory::from_parts(
372 self.epoch,
373 target,
374 Icrf,
375 self.data.clone(),
376 ));
377 }
378 let states: Result<Vec<_>, _> = self
379 .states()
380 .into_iter()
381 .map(|state| {
382 state.to_origin(target, ephemeris).map_err(|e| {
383 TrajectoryTransformationError::StateTransformationError(e.to_string())
384 })
385 })
386 .collect();
387 Ok(Trajectory::new(states?))
388 }
389}
390
391impl<O, R> Trajectory<Tai, O, R>
392where
393 O: Origin + Copy,
394 R: ReferenceFrame + Copy,
395{
396 pub fn from_csv(csv: &str, origin: O, frame: R) -> Result<Self, TrajectoryError> {
398 let states = parse_csv_states(csv, origin, frame)?;
399 if states.len() < 2 {
400 return Err(TrajectoryError::InsufficientStates(states.len()));
401 }
402 Ok(Trajectory::new(states))
403 }
404}
405
406pub type DynTrajectory = Trajectory<DynTimeScale, DynOrigin, DynFrame>;
408
409impl DynTrajectory {
410 pub fn from_csv_dyn(
412 csv: &str,
413 origin: DynOrigin,
414 frame: DynFrame,
415 ) -> Result<DynTrajectory, TrajectoryError> {
416 Ok(Trajectory::from_csv(csv, origin, frame)?.into_dyn())
417 }
418}
419
420impl<T, O, R> FromIterator<CartesianOrbit<T, O, R>> for Trajectory<T, O, R>
421where
422 T: TimeScale + Copy,
423 O: Origin + Copy,
424 R: ReferenceFrame + Copy,
425{
426 fn from_iter<U: IntoIterator<Item = CartesianOrbit<T, O, R>>>(iter: U) -> Self {
427 Self::new(iter)
428 }
429}
430
431fn parse_csv_f64(record: &csv::StringRecord, idx: usize) -> Result<f64, TrajectoryError> {
432 record
433 .get(idx)
434 .unwrap()
435 .parse()
436 .map_err(|e: ParseFloatError| TrajectoryError::CsvError(format!("invalid value: {e}")))
437}
438
439fn parse_csv_vec3(
440 record: &csv::StringRecord,
441 i0: usize,
442 i1: usize,
443 i2: usize,
444) -> Result<DVec3, TrajectoryError> {
445 Ok(DVec3::new(
446 parse_csv_f64(record, i0)?,
447 parse_csv_f64(record, i1)?,
448 parse_csv_f64(record, i2)?,
449 ))
450}
451
452fn parse_csv_states<O: Origin + Copy, R: ReferenceFrame + Copy>(
453 csv: &str,
454 origin: O,
455 frame: R,
456) -> Result<Vec<CartesianOrbit<Tai, O, R>>, TrajectoryError> {
457 let mut reader = csv::Reader::from_reader(csv.as_bytes());
458 let mut states = Vec::new();
459 for result in reader.records() {
460 let record = result?;
461 if record.len() != 7 {
462 return Err(TrajectoryError::CsvError(
463 "invalid record length".to_string(),
464 ));
465 }
466 let time: Time<Tai> = Utc::from_iso(record.get(0).unwrap())
467 .map_err(|e| TrajectoryError::CsvError(e.to_string()))?
468 .to_time();
469 let position = parse_csv_vec3(&record, 1, 2, 3)? * 1e3;
471 let velocity = parse_csv_vec3(&record, 4, 5, 6)? * 1e3;
472 let state = Cartesian::from_vecs(position, velocity);
473 states.push(CartesianOrbit::new(state, time, origin, frame));
474 }
475 Ok(states)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use glam::DVec3;
482 use lox_bodies::{DynOrigin, Earth};
483 use lox_frames::{DynFrame, Icrf};
484 use lox_time::time_scales::DynTimeScale;
485 use lox_time::{time, time_scales::Tdb};
486
487 fn sample_trajectory() -> Trajectory<Tdb, Earth, Icrf> {
488 let t0 = time!(Tdb, 2023, 1, 1, 12).unwrap();
489 let t1 = t0 + lox_time::deltas::TimeDelta::from_seconds(60);
490 let t2 = t0 + lox_time::deltas::TimeDelta::from_seconds(120);
491 let s0 = CartesianOrbit::new(
492 Cartesian::from_vecs(DVec3::new(7000e3, 0.0, 0.0), DVec3::new(0.0, 7500.0, 0.0)),
493 t0,
494 Earth,
495 Icrf,
496 );
497 let s1 = CartesianOrbit::new(
498 Cartesian::from_vecs(
499 DVec3::new(6999e3, 100e3, 0.0),
500 DVec3::new(-10.0, 7499.0, 0.0),
501 ),
502 t1,
503 Earth,
504 Icrf,
505 );
506 let s2 = CartesianOrbit::new(
507 Cartesian::from_vecs(
508 DVec3::new(6996e3, 200e3, 0.0),
509 DVec3::new(-20.0, 7498.0, 0.0),
510 ),
511 t2,
512 Earth,
513 Icrf,
514 );
515 Trajectory::new(vec![s0, s1, s2])
516 }
517
518 #[test]
519 fn test_trajectory_into_dyn() {
520 let traj = sample_trajectory();
521 let first_pos = traj.states().first().unwrap().position();
522 let dyn_traj = traj.into_dyn();
523
524 assert_eq!(dyn_traj.origin(), DynOrigin::Earth);
525 assert_eq!(dyn_traj.reference_frame(), DynFrame::Icrf);
526 assert_eq!(
527 dyn_traj.states().first().unwrap().time().scale(),
528 DynTimeScale::Tdb
529 );
530 assert_eq!(dyn_traj.states().first().unwrap().position(), first_pos);
531 }
532
533 #[test]
534 fn test_trajectory_into_parts() {
535 let traj = sample_trajectory();
536 let epoch_before = traj.epoch();
537 let (epoch, origin, frame, _data) = traj.into_parts();
538
539 assert_eq!(origin, Earth);
540 assert_eq!(frame, Icrf);
541 assert_eq!(epoch, epoch_before);
542 }
543}