1use crate::orbits::{CartesianOrbit, TrajectorError, Trajectory};
6use crate::propagators::Propagator;
7use glam::{DMat3, DVec3};
8use lox_bodies::{DynOrigin, RotationalElements, Spheroid, TrySpheroid};
9use lox_core::coords::{Cartesian, LonLatAlt};
10use lox_core::types::units::Radians;
11use lox_core::units::Distance;
12use lox_frames::{DynFrame, Iau, ReferenceFrame};
13use lox_time::Time;
14use lox_time::deltas::TimeDelta;
15use lox_time::intervals::TimeInterval;
16use lox_time::time_scales::TimeScale;
17use thiserror::Error;
18
19#[derive(Clone, Debug)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct Observables {
23 azimuth: Radians,
24 elevation: Radians,
25 range: f64,
26 range_rate: f64,
27}
28
29impl Observables {
30 pub fn new(azimuth: Radians, elevation: Radians, range: f64, range_rate: f64) -> Self {
32 Observables {
33 azimuth,
34 elevation,
35 range,
36 range_rate,
37 }
38 }
39 pub fn azimuth(&self) -> Radians {
41 self.azimuth
42 }
43
44 pub fn elevation(&self) -> Radians {
46 self.elevation
47 }
48
49 pub fn range(&self) -> f64 {
51 self.range
52 }
53
54 pub fn range_rate(&self) -> f64 {
56 self.range_rate
57 }
58}
59
60#[derive(Clone, Debug)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct GroundLocation<B: TrySpheroid> {
64 coordinates: LonLatAlt,
65 body: B,
66}
67
68pub type DynGroundLocation = GroundLocation<DynOrigin>;
70
71impl<B: Spheroid> GroundLocation<B> {
73 pub fn new(coordinates: LonLatAlt, body: B) -> Self {
75 GroundLocation { coordinates, body }
76 }
77}
78
79impl<B: TrySpheroid> GroundLocation<B> {
81 pub fn try_new(coordinates: LonLatAlt, body: B) -> Result<Self, &'static str> {
83 if body.try_equatorial_radius().is_err() {
84 return Err("no spheroid");
85 }
86 Ok(GroundLocation { coordinates, body })
87 }
88}
89
90impl<B: TrySpheroid + Into<DynOrigin>> GroundLocation<B> {
91 pub fn into_dyn(self) -> DynGroundLocation {
93 GroundLocation {
94 coordinates: self.coordinates,
95 body: self.body.into(),
96 }
97 }
98}
99
100impl<B: TrySpheroid> GroundLocation<B> {
101 pub fn origin(&self) -> B
103 where
104 B: Clone,
105 {
106 self.body.clone()
107 }
108
109 pub fn coordinates(&self) -> LonLatAlt {
111 self.coordinates
112 }
113
114 pub fn longitude(&self) -> f64 {
116 self.coordinates.lon().to_radians()
117 }
118
119 pub fn latitude(&self) -> f64 {
121 self.coordinates.lat().to_radians()
122 }
123
124 pub fn altitude(&self) -> f64 {
126 self.coordinates.alt().to_kilometers()
127 }
128
129 fn equatorial_radius(&self) -> Distance {
130 self.body
131 .try_equatorial_radius()
132 .expect("equatorial radius should be available")
133 }
134
135 fn flattening(&self) -> f64 {
136 self.body
137 .try_flattening()
138 .expect("flattening should be available")
139 }
140
141 pub fn body_fixed_position(&self) -> DVec3 {
143 self.coordinates
144 .to_body_fixed(self.equatorial_radius(), self.flattening())
145 }
146
147 pub fn rotation_to_topocentric(&self) -> DMat3 {
149 self.coordinates.rotation_to_topocentric()
150 }
151
152 pub fn compute_observables(&self, state_position: DVec3, state_velocity: DVec3) -> Observables {
154 let rot = self.rotation_to_topocentric();
155 let position = rot * (state_position - self.body_fixed_position());
156 let velocity = rot * state_velocity;
157 let range = position.length();
158 let range_rate = position.dot(velocity) / range;
159 let elevation = (position.z / range).asin();
160 let azimuth = position.y.atan2(-position.x);
161 Observables {
162 azimuth,
163 elevation,
164 range,
165 range_rate,
166 }
167 }
168
169 pub fn observables<T: TimeScale + Copy>(
171 &self,
172 state: CartesianOrbit<T, B, Iau<B>>,
173 ) -> Observables
174 where
175 B: RotationalElements + Copy,
176 {
177 self.compute_observables(state.position(), state.velocity())
178 }
179
180 pub fn observables_dyn(&self, state: crate::orbits::DynCartesianOrbit) -> Observables {
182 self.compute_observables(state.position(), state.velocity())
183 }
184}
185
186#[derive(Debug, Error)]
188pub enum GroundPropagatorError {
189 #[error("frame transformation error: {0}")]
191 FrameTransformation(String),
192 #[error(transparent)]
194 Trajectory(#[from] TrajectorError),
195}
196
197pub struct GroundPropagator<B: TrySpheroid, R: ReferenceFrame> {
199 location: GroundLocation<B>,
200 frame: R,
201 step: Option<TimeDelta>,
202}
203
204pub type DynGroundPropagator = GroundPropagator<DynOrigin, DynFrame>;
206
207impl<B: Spheroid + RotationalElements> GroundPropagator<B, Iau<B>> {
209 pub fn new(location: GroundLocation<B>) -> Self
211 where
212 B: Copy,
213 {
214 let frame = Iau::new(location.body);
215 GroundPropagator {
216 location,
217 frame,
218 step: None,
219 }
220 }
221}
222
223impl GroundPropagator<DynOrigin, DynFrame> {
225 pub fn try_new(location: GroundLocation<DynOrigin>) -> Result<Self, &'static str> {
227 if location.body.try_equatorial_radius().is_err() {
228 return Err("no spheroid");
229 }
230 let frame = DynFrame::Iau(location.body);
231 Ok(GroundPropagator {
232 location,
233 frame,
234 step: None,
235 })
236 }
237}
238
239impl<B: TrySpheroid, R: ReferenceFrame> GroundPropagator<B, R> {
240 pub fn with_step(mut self, step: TimeDelta) -> Self {
242 self.step = Some(step);
243 self
244 }
245
246 pub fn location(&self) -> &GroundLocation<B> {
248 &self.location
249 }
250
251 pub fn state_at<T: TimeScale + Copy>(&self, time: Time<T>) -> CartesianOrbit<T, B, R>
253 where
254 B: Copy,
255 R: Copy,
256 {
257 let pos = self.location.body_fixed_position();
258 CartesianOrbit::new(
259 Cartesian::from_vecs(pos, DVec3::ZERO),
260 time,
261 self.location.body,
262 self.frame,
263 )
264 }
265}
266
267impl<T, B, R> Propagator<T, B> for GroundPropagator<B, R>
269where
270 T: TimeScale + Copy + PartialOrd,
271 B: TrySpheroid + lox_bodies::Origin + Copy,
272 R: ReferenceFrame + Copy,
273{
274 type Frame = R;
275 type Error = GroundPropagatorError;
276
277 fn state_at(&self, time: Time<T>) -> Result<CartesianOrbit<T, B, R>, GroundPropagatorError> {
278 Ok(self.state_at(time))
279 }
280
281 fn propagate(&self, interval: TimeInterval<T>) -> Result<Trajectory<T, B, R>, Self::Error> {
282 let pos = self.location.body_fixed_position();
283 let step = self.step.unwrap_or(TimeDelta::from_seconds(60));
284 let states: Vec<_> = interval
285 .step_by(step)
286 .map(|t| {
287 CartesianOrbit::new(
288 Cartesian::from_vecs(pos, DVec3::ZERO),
289 t,
290 self.location.body,
291 self.frame,
292 )
293 })
294 .collect();
295 Trajectory::try_new(states).map_err(Into::into)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use lox_bodies::Earth;
302 use lox_core::coords::Cartesian;
303 use lox_frames::Icrf;
304 use lox_frames::providers::DefaultRotationProvider;
305 use lox_test_utils::assert_approx_eq;
306 use lox_time::intervals::Interval;
307 use lox_time::time_scales::Tdb;
308 use lox_time::utc::Utc;
309 use lox_time::{Time, time, utc};
310
311 use super::*;
312
313 #[test]
314 fn test_ground_location_to_body_fixed() {
315 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
316 let location = GroundLocation::new(coords, Earth);
317 let expected = DVec3::new(4846130.017870638, -370132.8551351891, 4116364.272747229);
318 assert_approx_eq!(location.body_fixed_position(), expected);
319 }
320
321 #[test]
322 fn test_ground_location_rotation_to_topocentric() {
323 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
324 let location = GroundLocation::new(coords, Earth);
325 let act = location.rotation_to_topocentric();
326 let exp = DMat3::from_cols(
327 DVec3::new(0.6469358921661584, 0.07615519584215287, 0.7587320591443464),
328 DVec3::new(
329 -0.049411020334552434,
330 0.9970959763965771,
331 -0.05794967578213965,
332 ),
333 DVec3::new(-0.7609418522440956, 0.0, 0.6488200809957448),
334 );
335 assert_approx_eq!(exp, act);
336 }
337
338 #[test]
339 fn test_ground_location_observables() {
340 let coords = LonLatAlt::from_degrees(-4.0, 41.0, 0.0).unwrap();
341 let location = GroundLocation::new(coords, Earth);
342 let position = DVec3::new(3359927.0, -2398072.0, 5153000.0);
343 let velocity = DVec3::new(5065.7, 5485.0, -744.0);
344 let time = time!(Tdb, 2012, 7, 1).unwrap();
345 let state = CartesianOrbit::new(
346 Cartesian::from_vecs(position, velocity),
347 time,
348 Earth,
349 Iau::new(Earth),
350 );
351 let observables = location.observables(state);
352 let expected_range = 2707700.0;
353 let expected_range_rate = -7160.0;
354 let expected_azimuth = -53.418f64.to_radians();
355 let expected_elevation = -7.077f64.to_radians();
356 assert_approx_eq!(observables.range, expected_range, rtol <= 1e-2);
357 assert_approx_eq!(observables.range_rate, expected_range_rate, rtol <= 1e-2);
358 assert_approx_eq!(observables.azimuth, expected_azimuth, rtol <= 1e-2);
359 assert_approx_eq!(observables.elevation, expected_elevation, rtol <= 1e-2);
360 }
361
362 #[test]
363 fn test_ground_propagator_body_fixed() {
364 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
365 let location = GroundLocation::new(coords, Earth);
366 let propagator = GroundPropagator::new(location.clone());
367 let time = utc!(2022, 1, 31, 23).unwrap().to_time();
368 let t1 = time + TimeDelta::from_minutes(5);
369 let interval = Interval::new(time, t1);
370 let traj = propagator.propagate(interval).unwrap();
371 let expected = location.body_fixed_position();
373 for state in traj.states() {
374 assert_approx_eq!(state.position(), expected);
375 assert_approx_eq!(state.velocity(), DVec3::ZERO);
376 }
377 }
378
379 #[test]
380 fn test_ground_propagator_in_icrf() {
381 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
382 let location = GroundLocation::new(coords, Earth);
383 let propagator = GroundPropagator::new(location);
384 let time = utc!(2022, 1, 31, 23).unwrap().to_time();
385 let t1 = time + TimeDelta::from_minutes(5);
386 let interval = Interval::new(time, t1);
387 let traj = propagator
388 .propagate(interval)
389 .unwrap()
390 .into_frame(Icrf, &DefaultRotationProvider)
391 .unwrap();
392 let state = traj.states()[0];
393 let expected = DVec3::new(-1765953.5510583583, 4524585.984442561, 4120189.198495323);
394 assert_approx_eq!(state.position(), expected);
395 }
396
397 #[test]
398 fn test_try_new_with_static_body() {
399 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
400 let location = GroundLocation::try_new(coords, Earth).unwrap();
401 assert_approx_eq!(location.longitude(), -4.3676f64.to_radians());
402 assert_approx_eq!(location.latitude(), 40.4527f64.to_radians());
403 assert_approx_eq!(location.altitude(), 0.0);
404 }
405
406 #[test]
407 fn test_try_new_with_dyn_origin() {
408 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
409 let location = GroundLocation::try_new(coords, DynOrigin::Earth).unwrap();
410 assert_eq!(location.origin(), DynOrigin::Earth);
411 }
412
413 #[test]
414 fn test_try_new_rejects_non_spheroid() {
415 let coords = LonLatAlt::from_degrees(0.0, 0.0, 0.0).unwrap();
416 let result = GroundLocation::try_new(coords, DynOrigin::SolarSystemBarycenter);
417 assert!(result.is_err());
418 }
419
420 #[test]
421 fn test_into_dyn_ground_location() {
422 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
423 let location = GroundLocation::new(coords, Earth);
424 let dyn_location = location.into_dyn();
425 assert_eq!(dyn_location.origin(), DynOrigin::Earth);
426 assert_approx_eq!(dyn_location.longitude(), -4.3676f64.to_radians());
427 assert_approx_eq!(dyn_location.latitude(), 40.4527f64.to_radians());
428 }
429
430 #[test]
431 fn test_ground_propagator_try_new_with_dyn_origin() {
432 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
433 let location = GroundLocation::try_new(coords, DynOrigin::Earth).unwrap();
434 let propagator = GroundPropagator::try_new(location).unwrap();
435 let time = utc!(2022, 1, 31, 23).unwrap().to_time();
436 let t1 = time + TimeDelta::from_minutes(5);
437 let interval = Interval::new(time, t1);
438 let traj = propagator
439 .propagate(interval)
440 .unwrap()
441 .into_frame(DynFrame::Icrf, &DefaultRotationProvider)
442 .unwrap();
443 let state = traj.states()[0];
444 let expected = DVec3::new(-1765953.5510583583, 4524585.984442561, 4120189.198495323);
446 assert_approx_eq!(state.position(), expected);
447 }
448}