1use std::ops::Sub;
6
7use glam::{DMat3, DVec3};
8use lox_bodies::{
9 DynOrigin, Origin, PointMass, RotationalElements, Spheroid, TryPointMass, TrySpheroid,
10 UndefinedOriginPropertyError,
11};
12use lox_core::coords::Cartesian;
13use lox_core::coords::FromBodyFixedError;
14use lox_core::coords::LonLatAlt;
15use lox_ephem::Ephemeris;
16use lox_frames::{
17 DynFrame, Iau, Icrf, ReferenceFrame, TryBodyFixed, rotations::TryRotation, traits::frame_id,
18};
19use lox_time::offsets::{DefaultOffsetProvider, Offset};
20use lox_time::time_scales::{Tdb, TimeScale};
21use thiserror::Error;
22
23use crate::ground::{DynGroundLocation, GroundLocation};
24
25use super::{CartesianOrbit, DynCartesianOrbit, KeplerianOrbit, Orbit};
26
27impl<T, O, R> CartesianOrbit<T, O, R>
28where
29 T: TimeScale,
30 O: Origin,
31 R: ReferenceFrame,
32{
33 pub const fn new(cartesian: Cartesian, time: lox_time::Time<T>, origin: O, frame: R) -> Self {
35 Self::from_state(cartesian, time, origin, frame)
36 }
37
38 pub fn position(&self) -> DVec3 {
40 self.state().position()
41 }
42
43 pub fn velocity(&self) -> DVec3 {
45 self.state().velocity()
46 }
47
48 pub fn to_keplerian(&self) -> KeplerianOrbit<T, O, R>
50 where
51 T: Copy,
52 O: Copy + PointMass,
53 R: Copy,
54 {
55 Orbit::from_state(
56 self.state().to_keplerian(self.gravitational_parameter()),
57 self.time(),
58 self.origin(),
59 self.reference_frame(),
60 )
61 }
62
63 pub fn try_to_keplerian(&self) -> Result<KeplerianOrbit<T, O, R>, UndefinedOriginPropertyError>
65 where
66 T: Copy,
67 O: Copy + TryPointMass,
68 R: Copy,
69 {
70 Ok(Orbit::from_state(
71 self.state()
72 .to_keplerian(self.try_gravitational_parameter()?),
73 self.time(),
74 self.origin(),
75 self.reference_frame(),
76 ))
77 }
78
79 pub fn try_to_frame<R1, P>(
81 &self,
82 frame: R1,
83 provider: &P,
84 ) -> Result<CartesianOrbit<T, O, R1>, P::Error>
85 where
86 R: Copy,
87 P: TryRotation<R, R1, T>,
88 R1: ReferenceFrame + Copy,
89 O: Copy,
90 T: Copy,
91 {
92 if let (Some(id0), Some(id1)) = (frame_id(&self.reference_frame()), frame_id(&frame))
93 && id0 == id1
94 {
95 return Ok(CartesianOrbit::new(
96 self.state(),
97 self.time(),
98 self.origin(),
99 frame,
100 ));
101 }
102 let rot = provider.try_rotation(self.reference_frame(), frame, self.time())?;
103 let (r1, v1) = rot.rotate_state(self.state().position(), self.state().velocity());
104 Ok(CartesianOrbit::new(
105 Cartesian::from_vecs(r1, v1),
106 self.time(),
107 self.origin(),
108 frame,
109 ))
110 }
111}
112
113impl<T, O> CartesianOrbit<T, O, Iau<O>>
114where
115 T: TimeScale,
116 O: Origin + RotationalElements + Spheroid + Copy,
117{
118 pub fn to_ground_location(&self) -> Result<GroundLocation<O>, FromBodyFixedError> {
120 let origin = self.origin();
121 let coords = LonLatAlt::from_body_fixed(
122 self.position(),
123 origin.equatorial_radius(),
124 origin.flattening(),
125 )?;
126 Ok(GroundLocation::new(coords, origin))
127 }
128}
129
130#[derive(Debug, Error)]
132pub enum StateToDynGroundError {
133 #[error("equatorial radius and flattening factor are not available for origin `{}`", .0.name())]
135 UndefinedSpheroid(DynOrigin),
136 #[error(transparent)]
138 FromBodyFixed(#[from] FromBodyFixedError),
139 #[error("not a body-fixed frame {0}")]
141 NonBodyFixedFrame(String),
142}
143
144fn rotation_lvlh(position: DVec3, velocity: DVec3) -> DMat3 {
145 let r = position.normalize();
146 let v = velocity.normalize();
147 let z = -r;
148 let y = -r.cross(v);
149 let x = y.cross(z);
150 DMat3::from_cols(x, y, z)
151}
152
153impl<T, O> CartesianOrbit<T, O, Icrf>
154where
155 T: TimeScale,
156 O: Origin,
157{
158 pub fn rotation_lvlh(&self) -> DMat3 {
160 rotation_lvlh(self.position(), self.velocity())
161 }
162}
163
164impl<T, O> CartesianOrbit<T, O, Icrf>
165where
166 T: TimeScale + Copy,
167 O: Origin + Copy,
168{
169 pub fn to_origin<O1: Origin + Copy, E: Ephemeris>(
171 &self,
172 target: O1,
173 ephemeris: &E,
174 ) -> Result<CartesianOrbit<T, O1, Icrf>, E::Error>
175 where
176 DefaultOffsetProvider: Offset<T, Tdb>,
177 {
178 if self.origin().id() == target.id() {
179 return Ok(CartesianOrbit::new(self.state(), self.time(), target, Icrf));
180 }
181 let tdb = self.time().to_scale(Tdb);
182 let delta = ephemeris.state(tdb, self.origin(), target)?;
183 Ok(CartesianOrbit::new(
184 self.state() - delta,
185 self.time(),
186 target,
187 Icrf,
188 ))
189 }
190}
191
192impl DynCartesianOrbit {
193 pub fn try_to_origin<E: Ephemeris>(
195 &self,
196 target: DynOrigin,
197 ephemeris: &E,
198 ) -> Result<DynCartesianOrbit, E::Error> {
199 if self.origin() == target {
200 return Ok(CartesianOrbit::new(
201 self.state(),
202 self.time(),
203 target,
204 self.reference_frame(),
205 ));
206 }
207 let tdb = self
208 .time()
209 .try_to_scale(Tdb, &DefaultOffsetProvider)
210 .unwrap();
211 let delta = ephemeris.state(tdb, self.origin(), target)?;
212 Ok(CartesianOrbit::new(
213 self.state() - delta,
214 self.time(),
215 target,
216 DynFrame::Icrf,
217 ))
218 }
219
220 pub fn try_to_ground_location(&self) -> Result<DynGroundLocation, StateToDynGroundError> {
222 let frame = self.reference_frame();
223 let origin = self.origin();
224 if frame.try_body_fixed().is_err() {
225 return Err(StateToDynGroundError::NonBodyFixedFrame(
226 frame.name().to_string(),
227 ));
228 }
229 let (Ok(r_eq), Ok(f)) = (origin.try_equatorial_radius(), origin.try_flattening()) else {
230 return Err(StateToDynGroundError::UndefinedSpheroid(origin));
231 };
232
233 let coords = LonLatAlt::from_body_fixed(self.position(), r_eq, f)?;
234 Ok(DynGroundLocation::try_new(coords, origin).unwrap())
235 }
236
237 pub fn try_rotation_lvlh(&self) -> Result<DMat3, &'static str> {
239 if self.reference_frame() != DynFrame::Icrf {
240 return Err("only valid for ICRF");
241 }
242 Ok(rotation_lvlh(self.position(), self.velocity()))
243 }
244}
245
246impl<T, O, R> Sub for CartesianOrbit<T, O, R>
247where
248 T: TimeScale + Copy,
249 O: Origin + Copy,
250 R: ReferenceFrame + Copy,
251{
252 type Output = Self;
253
254 fn sub(self, rhs: Self) -> Self::Output {
255 let state = Cartesian::from_vecs(
256 self.position() - rhs.position(),
257 self.velocity() - rhs.velocity(),
258 );
259 CartesianOrbit::new(state, self.time(), self.origin(), self.reference_frame())
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::sync::OnceLock;
266
267 use glam::DVec3;
268 use lox_bodies::{Earth, Jupiter, Venus};
269 use lox_core::coords::Cartesian;
270 use lox_ephem::Ephemeris;
271 use lox_ephem::spk::parser::Spk;
272 use lox_frames::providers::DefaultRotationProvider;
273 use lox_test_utils::{assert_approx_eq, data_file};
274 use lox_time::{Time, time, time_scales::Tdb, utc::Utc};
275
276 use super::*;
277
278 #[test]
279 fn test_bodyfixed() {
280 let iau_jupiter = Iau::new(Jupiter);
281
282 let r0 = DVec3::new(6068.27927, -1692.84394, -2516.61918);
283 let v0 = DVec3::new(-0.660415582, 5.495938726, -5.303093233);
284 let r1 = DVec3::new(3922.220687351738, 5289.381014412637, -1631.4837924820245);
285 let v1 = DVec3::new(-1.852284168309543, -0.8227941105651749, -7.14175174489828);
286
287 let tdb = time!(Tdb, 2000, 1, 1, 12).unwrap();
288 let s = CartesianOrbit::new(Cartesian::from_vecs(r0, v0), tdb, Jupiter, Icrf);
289 let s1 = s
290 .try_to_frame(iau_jupiter, &DefaultRotationProvider)
291 .unwrap();
292 let s0 = s1.try_to_frame(Icrf, &DefaultRotationProvider).unwrap();
293
294 assert_approx_eq!(s0.position(), r0, rtol <= 1e-8);
295 assert_approx_eq!(s0.velocity(), v0, rtol <= 1e-8);
296
297 assert_approx_eq!(s1.position(), r1, rtol <= 1e-8);
298 assert_approx_eq!(s1.velocity(), v1, rtol <= 1e-8);
299 }
300
301 #[test]
302 fn test_cartesian_to_keplerian_roundtrip() {
303 let time = time!(Tdb, 2023, 3, 25, 21, 8, 0.0).expect("time should be valid");
304 let pos = DVec3::new(
305 -0.107622532467967e7,
306 -0.676589636432773e7,
307 -0.332308783350379e6,
308 );
309 let vel = DVec3::new(
310 0.935685775154103e4,
311 -0.331234775037644e4,
312 -0.118801577532701e4,
313 );
314
315 let cartesian = CartesianOrbit::new(Cartesian::from_vecs(pos, vel), time, Earth, Icrf);
316 let cartesian1 = cartesian.to_keplerian().to_cartesian();
317
318 assert_eq!(cartesian1.time(), time);
319 assert_eq!(cartesian1.origin(), Earth);
320 assert_eq!(cartesian1.reference_frame(), Icrf);
321
322 assert_approx_eq!(cartesian.position(), cartesian1.position(), rtol <= 1e-8);
323 assert_approx_eq!(cartesian.velocity(), cartesian1.velocity(), rtol <= 1e-6);
324 }
325
326 #[test]
327 fn test_to_ground_location() {
328 let lat_exp = 51.484f64.to_radians();
329 let lon_exp = -35.516f64.to_radians();
330 let alt_exp = 237.434; let position = DVec3::new(3359927.0, -2398072.0, 5153000.0);
333 let velocity = DVec3::new(5065.7, 5485.0, -744.0);
334 let time = time!(Tdb, 2012, 7, 1).unwrap();
335 let state = CartesianOrbit::new(
336 Cartesian::from_vecs(position, velocity),
337 time,
338 Earth,
339 Iau::new(Earth),
340 );
341 let ground = state.to_ground_location().unwrap();
342 assert_approx_eq!(ground.latitude(), lat_exp, rtol <= 1e-4);
343 assert_approx_eq!(ground.longitude(), lon_exp, rtol <= 1e-4);
344 assert_approx_eq!(ground.altitude(), alt_exp, rtol <= 1e-4);
345 }
346
347 #[test]
348 fn test_to_origin() {
349 let r = DVec3::new(6068279.27, -1692843.94, -2516619.18);
350 let v = DVec3::new(-660.415582, 5495.938726, -5303.093233);
351
352 let utc = Utc::from_iso("2016-05-30T12:00:00.000").unwrap();
353 let tai = utc.to_time();
354 let tdb = tai.to_scale(Tdb);
355
356 let delta = ephemeris().state(tdb, Earth, Venus).unwrap();
358 let r_exp = r - delta.position();
359 let v_exp = v - delta.velocity();
360
361 let s_earth = CartesianOrbit::new(Cartesian::from_vecs(r, v), tai, Earth, Icrf);
362 let s_venus = s_earth.to_origin(Venus, ephemeris()).unwrap();
363
364 assert_approx_eq!(s_venus.position(), r_exp);
365 assert_approx_eq!(s_venus.velocity(), v_exp);
366 }
367
368 #[test]
369 fn test_rotation_lvlh() {
370 let time = time!(Tdb, 2023, 3, 25, 21, 8, 0.0).unwrap();
371 let pos = DVec3::new(6678.0, 0.0, 0.0);
372 let vel = DVec3::new(0.0, 7.73, 0.0);
373
374 let state = CartesianOrbit::new(Cartesian::from_vecs(pos, vel), time, Earth, Icrf);
375 let rot = state.rotation_lvlh();
376
377 let z = rot.col(2);
380 let x = rot.col(0);
381 assert_approx_eq!(z, -DVec3::X, atol <= 1e-10);
382 assert_approx_eq!(x, DVec3::Y, atol <= 1e-10);
383 }
384
385 #[test]
386 fn test_sub_operator() {
387 let time = time!(Tdb, 2023, 3, 25, 21, 8, 0.0).unwrap();
388 let s1 = CartesianOrbit::new(
389 Cartesian::from_vecs(DVec3::new(10.0, 20.0, 30.0), DVec3::new(1.0, 2.0, 3.0)),
390 time,
391 Earth,
392 Icrf,
393 );
394 let s2 = CartesianOrbit::new(
395 Cartesian::from_vecs(DVec3::new(3.0, 5.0, 7.0), DVec3::new(0.5, 1.0, 1.5)),
396 time,
397 Earth,
398 Icrf,
399 );
400 let diff = s1 - s2;
401 assert_approx_eq!(diff.position(), DVec3::new(7.0, 15.0, 23.0));
402 assert_approx_eq!(diff.velocity(), DVec3::new(0.5, 1.0, 1.5));
403 }
404
405 #[test]
406 fn test_into_dyn() {
407 use lox_bodies::DynOrigin;
408 use lox_frames::DynFrame;
409 use lox_time::time_scales::DynTimeScale;
410
411 let time = time!(Tdb, 2023, 3, 25, 21, 8, 0.0).unwrap();
412 let pos = DVec3::new(1000.0, 2000.0, 3000.0);
413 let vel = DVec3::new(1.0, 2.0, 3.0);
414 let state = CartesianOrbit::new(Cartesian::from_vecs(pos, vel), time, Earth, Icrf);
415 let dyn_state = state.into_dyn();
416
417 assert_eq!(dyn_state.origin(), DynOrigin::Earth);
418 assert_eq!(dyn_state.reference_frame(), DynFrame::Icrf);
419 assert_eq!(dyn_state.time().scale(), DynTimeScale::Tdb);
420 assert_approx_eq!(dyn_state.position(), pos);
421 assert_approx_eq!(dyn_state.velocity(), vel);
422 }
423
424 #[test]
425 fn test_try_to_frame_identity() {
426 use lox_bodies::DynOrigin;
427 use lox_frames::DynFrame;
428
429 let time = Utc::from_iso("2024-07-05T09:09:18.173")
430 .unwrap()
431 .to_dyn_time();
432 let pos = DVec3::new(6068.27927, -1692.84394, -2516.61918);
433 let vel = DVec3::new(-0.660415582, 5.495938726, -5.303093233);
434 let state = CartesianOrbit::new(
435 Cartesian::from_vecs(pos, vel),
436 time,
437 DynOrigin::Earth,
438 DynFrame::Icrf,
439 );
440
441 let same = state
443 .try_to_frame(DynFrame::Icrf, &DefaultRotationProvider)
444 .unwrap();
445 assert_eq!(same.position(), pos);
446 assert_eq!(same.velocity(), vel);
447 }
448
449 #[test]
450 fn test_to_origin_same_origin() {
451 let r = DVec3::new(6068279.27, -1692843.94, -2516619.18);
452 let v = DVec3::new(-660.415582, 5495.938726, -5303.093233);
453 let utc = Utc::from_iso("2016-05-30T12:00:00.000").unwrap();
454 let tai = utc.to_time();
455
456 let state = CartesianOrbit::new(Cartesian::from_vecs(r, v), tai, Earth, Icrf);
457 let same = state.to_origin(Earth, ephemeris()).unwrap();
459 assert_eq!(same.position(), r);
460 assert_eq!(same.velocity(), v);
461 }
462
463 #[test]
464 fn test_try_to_origin_same_origin() {
465 use lox_bodies::DynOrigin;
466 use lox_frames::DynFrame;
467
468 let r = DVec3::new(6068279.27, -1692843.94, -2516619.18);
469 let v = DVec3::new(-660.415582, 5495.938726, -5303.093233);
470 let utc = Utc::from_iso("2016-05-30T12:00:00.000").unwrap();
471 let time = utc.to_dyn_time();
472
473 let state = CartesianOrbit::new(
474 Cartesian::from_vecs(r, v),
475 time,
476 DynOrigin::Earth,
477 DynFrame::Icrf,
478 );
479 let same = state.try_to_origin(DynOrigin::Earth, ephemeris()).unwrap();
480 assert_eq!(same.position(), r);
481 assert_eq!(same.velocity(), v);
482 }
483
484 fn ephemeris() -> &'static Spk {
485 static EPHEMERIS: OnceLock<Spk> = OnceLock::new();
486 EPHEMERIS.get_or_init(|| Spk::from_file(data_file("spice/de440s.bsp")).unwrap())
487 }
488}