1use crate::consts::{
2 DEG_TO_RAD, EARTH_ANGULAR_VELOCITY, EARTH_RADIUS_KM_WGS84, EARTH_ROTATIONS_PER_SIDERIAL_DAY,
3 FLATTENING_FACTOR, JULIAN_TIME_DIFF, MAXELE_MAX_NUM_ITERATIONS, MAXELE_TIME_EQUALITY_THRESHOLD,
4 MINUTES_PER_DAY, NAUTICAL_TWILIGHT_SUN_ELEVATION, RAD_TO_DEG, SECONDS_PER_DAY,
5};
6use crate::geodetic::Geodetic;
7use crate::julian_date::{predict_to_julian_double, theta_g_jd};
8use crate::math::{asin_clamped, fmod2p, vec3_dot, vec3_length, vec3_sub, Vec3};
9use crate::orbit::{
10 predict_aos_happens, predict_is_geosynchronous, predict_orbit, OrbitPredictionError,
11};
12use crate::predict::{
13 ObserverElements, Pass, Passes, PredictObservation, PredictObserver, PredictPosition,
14};
15use crate::sun::predict_observe_sun;
16use core::f64::consts::PI;
17
18#[non_exhaustive]
20#[derive(PartialEq)]
21pub enum StepPassDirection {
22 PositiveDirection,
23 NegativeDirection,
24}
25
26#[non_exhaustive]
28#[derive(Debug, PartialEq)]
29pub enum RefineMode {
30 AOS,
31 LOS,
32}
33
34#[must_use]
37pub fn predict_observe_orbit(
38 observer: &PredictObserver,
39 orbit: &PredictPosition,
40) -> PredictObservation {
41 let mut observation = observer_calculate(observer, orbit);
42 observation.visible = false;
43
44 let sun_obs = predict_observe_sun(observer, orbit.time);
45 if !orbit.eclipsed
46 && (sun_obs.elevation * 180.0 / PI < NAUTICAL_TWILIGHT_SUN_ELEVATION)
47 && (observation.elevation * 180.0 / PI > 0.0)
48 {
49 observation.visible = true;
50 }
51 observation
52}
53
54#[must_use]
56#[allow(clippy::similar_names)]
57pub fn observer_calculate(
58 observer: &PredictObserver,
59 orbit: &PredictPosition,
60) -> PredictObservation {
61 let jultime = predict_to_julian_double(orbit.time) + JULIAN_TIME_DIFF;
62
63 let mut geodetic = Geodetic {
64 lat: observer.latitude,
65 lon: observer.longitude,
66 alt: observer.altitude / 1000.0,
67 theta: 0.0,
68 };
69 let (obs_pos, obs_vel) = calculate_user_posvel(jultime, &mut geodetic);
70
71 let range = vec3_sub(orbit.position, obs_pos);
72 let rgvel = vec3_sub(orbit.velocity, obs_vel);
73
74 let range_length = vec3_length(range);
75 let range_rate_length = vec3_dot(range, rgvel) / range_length;
76
77 let theta_dot = 2.0 * PI * EARTH_ROTATIONS_PER_SIDERIAL_DAY / SECONDS_PER_DAY;
78 let sin_lat = geodetic.lat.sin();
79 let cos_lat = geodetic.lat.cos();
80 let sin_theta = geodetic.theta.sin();
81 let cos_theta = geodetic.theta.cos();
82
83 let top_s = sin_lat * cos_theta * range.0 + sin_lat * sin_theta * range.1 - cos_lat * range.2;
84 let top_e = -sin_theta * range.0 + cos_theta * range.1;
85 let top_z = cos_lat * cos_theta * range.0 + cos_lat * sin_theta * range.1 + sin_lat * range.2;
86
87 let top_s_dot = sin_lat * (cos_theta * rgvel.0 - sin_theta * range.0 * theta_dot)
88 + sin_lat * (sin_theta * rgvel.1 + cos_theta * range.1 * theta_dot)
89 - cos_lat * rgvel.2;
90 let top_e_dot = -(sin_theta * rgvel.0 + cos_theta * range.0 * theta_dot)
91 + (cos_theta * rgvel.1 - sin_theta * range.1 * theta_dot);
92
93 let top_z_dot = cos_lat
94 * (cos_theta * (rgvel.0 + range.1 * theta_dot)
95 + sin_theta * (rgvel.1 - range.0 * theta_dot))
96 + sin_lat * rgvel.2;
97
98 let y = -top_e / top_s;
100 let mut az = (-top_e / top_s).atan();
101
102 if top_s > 0.0 {
103 az += PI;
104 }
105 if az < 0.0 {
106 az += 2.0 * PI;
107 }
108
109 let y_dot = -(top_e_dot * top_s - top_s_dot * top_e) / (top_s * top_s);
111 let az_dot = y_dot / (1.0 + y * y);
112
113 let x = top_z / range_length;
115 let el = asin_clamped(x);
116
117 let x_dot =
119 (top_z_dot * range_length - range_rate_length * top_z) / (range_length * range_length);
120 let el_dot = x_dot / (1.0 - x * x).sqrt();
121 PredictObservation {
122 azimuth: az,
123 azimuth_rate: az_dot,
124 elevation: el,
125 elevation_rate: el_dot,
126 range: range_length,
127 range_rate: range_rate_length,
128 range_x: range.0,
129 range_y: range.1,
130 range_z: range.2,
131 time: orbit.time,
132 revolutions: orbit.revolutions,
133 visible: false,
134 }
135}
136
137pub fn predict_next_aos(
141 oe: &ObserverElements,
142 start_utc: f64,
143) -> Result<PredictObservation, OrbitPredictionError> {
144 let mut curr_time = start_utc;
145 let mut time_step;
146
147 let mut orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
148 let mut obs = predict_observe_orbit(oe.observer, &orbit);
149
150 if predict_aos_happens(oe.elements, oe.observer.latitude)
152 && !predict_is_geosynchronous(oe.elements)
153 && !orbit.decayed
154 {
155 if obs.elevation > 0.0 {
165 let los = predict_next_los(oe, curr_time)?;
166 curr_time = los.time;
167 curr_time += 1.0 / (MINUTES_PER_DAY * 1.0) * 20.0; orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
169 obs = predict_observe_orbit(oe.observer, &orbit);
170 }
171
172 while (obs.elevation * 180.0 / PI < -1.0) || (obs.elevation_rate < 0.0) {
174 time_step =
175 0.00035 * (obs.elevation * 180.0 / PI * ((orbit.altitude / 8400.0) + 0.46) - 2.0);
176 curr_time -= time_step;
177 orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
178 obs = predict_observe_orbit(oe.observer, &orbit);
179 }
180
181 while (obs.elevation * 180.0 / PI).abs() > oe.observer.min_elevation {
183 time_step = obs.elevation * 180.0 / PI * orbit.altitude.sqrt() / 530_000.0;
184 curr_time -= time_step;
185 orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
186 obs = predict_observe_orbit(oe.observer, &orbit);
187 }
188 }
189 Ok(obs)
190}
191
192pub fn step_pass(
203 oe: &ObserverElements,
204 mut curr_time: f64,
205 direction: &StepPassDirection,
206) -> Result<(PredictObservation, f64), OrbitPredictionError> {
207 loop {
208 let orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
209 let obs = predict_observe_orbit(oe.observer, &orbit);
210
211 let mut time_step = (obs.elevation - 1.0).cos() * orbit.altitude.sqrt() / 25000.0;
213 if ((*direction == StepPassDirection::PositiveDirection) && time_step < 0.0)
214 || ((*direction == StepPassDirection::NegativeDirection) && time_step > 0.0)
215 {
216 time_step = -time_step;
217 }
218
219 curr_time += time_step;
220 if (obs.elevation < oe.observer.min_elevation * DEG_TO_RAD)
221 || ((*direction == StepPassDirection::PositiveDirection) && (obs.elevation_rate <= 0.0))
222 {
223 return Ok((obs, curr_time));
224 }
225 }
226}
227
228pub fn refine_obs_elevation(
232 oe: &ObserverElements,
233 mut curr_time: f64,
234 mode: &RefineMode,
235) -> Result<(PredictPosition, PredictObservation, f64), OrbitPredictionError> {
236 let time_step = 0.001;
237 loop {
238 let orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
239 let obs = predict_observe_orbit(oe.observer, &orbit);
240 curr_time += time_step;
241 if *mode == RefineMode::AOS
242 && obs.elevation_rate > 0.0
243 && (obs.elevation * RAD_TO_DEG > oe.observer.min_elevation)
244 {
245 return Ok((orbit, obs, curr_time));
246 }
247 if *mode == RefineMode::LOS
248 && obs.elevation_rate < 0.0
249 && (obs.elevation * RAD_TO_DEG < oe.observer.min_elevation)
250 {
251 return Ok((orbit, obs, curr_time));
252 }
253 }
254}
255
256pub fn predict_next_los(
260 oe: &ObserverElements,
261 start_utc: f64,
262) -> Result<PredictObservation, OrbitPredictionError> {
263 let mut curr_time = start_utc;
264 let mut time_step;
265
266 let mut orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
267 let mut obs = predict_observe_orbit(oe.observer, &orbit);
268
269 if predict_aos_happens(oe.elements, oe.observer.latitude)
271 && !predict_is_geosynchronous(oe.elements)
272 && !orbit.decayed
273 {
274 if obs.elevation < 0.0 {
278 let aos = predict_next_aos(oe, curr_time)?;
279 curr_time = aos.time;
280 orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
281 obs = predict_observe_orbit(oe.observer, &orbit);
282 }
283
284 (_, curr_time) = step_pass(oe, curr_time, &StepPassDirection::PositiveDirection)?;
286
287 loop {
289 time_step = obs.elevation * 180.0 / PI * orbit.altitude.sqrt() / 502_500.0;
290 curr_time += time_step;
291 orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
292 obs = predict_observe_orbit(oe.observer, &orbit);
293 if (obs.elevation * 180.0 / PI).abs() <= oe.observer.min_elevation {
294 break;
295 }
296 }
297 }
298 Ok(obs)
299}
300
301pub fn elevation_derivative(oe: &ObserverElements, time: f64) -> Result<f64, OrbitPredictionError> {
311 let orbit = predict_orbit(oe.elements, oe.constants, time)?;
312 let obs = predict_observe_orbit(oe.observer, &orbit);
313 Ok(obs.elevation_rate)
314}
315
316pub fn find_max_elevation(
327 oe: &ObserverElements,
328 lower_time: f64,
329 upper_time: f64,
330) -> Result<PredictObservation, OrbitPredictionError> {
331 let mut max_ele_time_candidate = (upper_time + lower_time) / 2.0;
332 let mut iteration = 0.0;
333 let mut lower_time = lower_time;
334 let mut upper_time = upper_time;
335 while ((lower_time - upper_time).abs() > MAXELE_TIME_EQUALITY_THRESHOLD)
336 && (iteration < MAXELE_MAX_NUM_ITERATIONS)
337 {
338 max_ele_time_candidate = (upper_time + lower_time) / 2.0;
339
340 let candidate_deriv = elevation_derivative(oe, max_ele_time_candidate)?;
342 let lower_deriv = elevation_derivative(oe, lower_time)?;
343 let upper_deriv = elevation_derivative(oe, upper_time)?;
344
345 if candidate_deriv * lower_deriv < 0.0 {
347 upper_time = max_ele_time_candidate;
348 } else if candidate_deriv * upper_deriv < 0.0 {
349 lower_time = max_ele_time_candidate;
350 } else {
351 break;
352 }
353 iteration += 1.0;
354 }
355
356 let orbit = predict_orbit(oe.elements, oe.constants, max_ele_time_candidate)?;
358 Ok(predict_observe_orbit(oe.observer, &orbit))
359}
360
361pub fn calculate_user_posvel(time: f64, geodetic: &mut Geodetic) -> (Vec3, Vec3) {
367 geodetic.theta = fmod2p(theta_g_jd(time) + geodetic.lon); let c = 1.0
370 / (1.0 + FLATTENING_FACTOR * (FLATTENING_FACTOR - 2.0) * geodetic.lat.sin().powf(2.0))
371 .sqrt();
372
373 let sq = (1.0 - FLATTENING_FACTOR).powf(2.0) * c;
374 let achcp = (EARTH_RADIUS_KM_WGS84 * c + geodetic.alt) * geodetic.lat.cos();
375 let obs_pos = Vec3(
376 achcp * geodetic.theta.cos(), achcp * geodetic.theta.sin(),
378 (EARTH_RADIUS_KM_WGS84 * sq + geodetic.alt) * geodetic.lat.sin(),
379 );
380 (
381 obs_pos,
382 Vec3(
383 -EARTH_ANGULAR_VELOCITY * obs_pos.1, EARTH_ANGULAR_VELOCITY * obs_pos.0,
385 0.0,
386 ),
387 )
388}
389
390pub fn get_passes(
394 oe: &ObserverElements,
395 start_utc: f64,
396 stop_utc: f64,
397) -> Result<Passes, OrbitPredictionError> {
398 let mut orbit = predict_orbit(oe.elements, oe.constants, start_utc)?;
399 let mut obs = predict_observe_orbit(oe.observer, &orbit);
400 let satellite_el = obs.elevation * RAD_TO_DEG;
401 let mut currtime = start_utc;
402 let mut passes = vec![];
403
404 if satellite_el.abs() >= oe.observer.min_elevation {
405 let (_, real_aos) = step_pass(oe, currtime, &StepPassDirection::NegativeDirection)?;
407 currtime = real_aos - 1.0;
408 }
409 'outer: loop {
410 let mut pass = Pass {
411 aos: None,
412 los: None,
413 satellite_position_at_aos: None,
414 satellite_position_at_los: None,
415 max_elevation: None,
416 };
417 loop {
419 if currtime >= stop_utc {
420 break 'outer;
421 }
422 orbit = predict_orbit(oe.elements, oe.constants, currtime)?;
423 obs = predict_observe_orbit(oe.observer, &orbit);
424 let satellite_el = obs.elevation * RAD_TO_DEG;
425 if satellite_el >= oe.observer.min_elevation && obs.elevation_rate > 0.0 {
426 currtime -= 1.0;
427 let (satpos, observation, _) =
428 refine_obs_elevation(oe, currtime, &RefineMode::AOS)?;
429 pass.aos = Some(observation);
430 pass.satellite_position_at_aos = Some(satpos);
431 currtime += 1.0;
432 break;
433 }
434 currtime += 1.0;
435 }
436 if pass.aos.is_none() {
437 println!("Shouldn't be here");
438 }
439 loop {
441 orbit = predict_orbit(oe.elements, oe.constants, currtime)?;
442 obs = predict_observe_orbit(oe.observer, &orbit);
443 let satellite_el = obs.elevation * RAD_TO_DEG;
444 if satellite_el <= oe.observer.min_elevation && obs.elevation_rate < 0.0 {
445 currtime -= 1.0;
446 let (satpos, observation, _) =
447 refine_obs_elevation(oe, currtime, &RefineMode::LOS)?;
448 pass.los = Some(observation);
449 pass.satellite_position_at_los = Some(satpos);
450 currtime += 1.0;
451 break;
452 }
453 currtime += 1.0;
454 if currtime >= stop_utc {
455 break;
456 }
457 }
458 if pass.aos.is_some() && pass.los.is_some() {
459 let maxel_obs = find_max_elevation(
460 oe,
461 pass.aos.as_ref().expect("already checked").time,
462 pass.los.as_ref().expect("already checked").time,
463 )?;
464 pass.max_elevation = Some(maxel_obs.elevation);
465 }
466 passes.push(pass);
467 }
468 Ok(Passes { passes })
469}
470
471#[cfg(test)]
472#[allow(clippy::unwrap_used)]
473mod tests {
474 use crate::observer::{get_passes, DEG_TO_RAD};
475 use crate::predict::{ObserverElements, PredictObserver};
476
477 #[allow(clippy::float_cmp)]
479 fn assert_eq_f64(first: f64, second: f64) {
480 if second == 0.0 {
481 assert_eq!(first, 0.0);
482 } else {
483 assert!((first - second).abs() / second < f64::EPSILON);
484 }
485 }
486
487 #[test]
488 fn test_observer() {
489 let elements = sgp4::Elements::from_tle(
490 Some("RADARSAT-2".to_string()),
491 "1 32382U 07061A 25046.11055603 .00000166 00000+0 81462-4 0 9997".as_bytes(),
492 "2 32382 98.5808 54.9834 0001175 88.7553 271.3764 14.29984911896451".as_bytes(),
493 )
494 .unwrap();
495 let observer = PredictObserver {
496 name: "TEST".to_string(),
497 latitude: 50.0 * DEG_TO_RAD,
498 longitude: -130.0 * DEG_TO_RAD,
499 altitude: 0.0905,
500 min_elevation: 0.0,
501 };
502 let constants = sgp4::Constants::from_elements(&elements).unwrap();
503 let oe = ObserverElements {
504 elements: &elements,
505 constants: &constants,
506 observer: &observer,
507 };
508 #[allow(clippy::cast_precision_loss)]
509 let start_epoch = elements.datetime.and_utc().timestamp() as f64;
510 let mut passes = get_passes(&oe, start_epoch, start_epoch + 3.0 * 3600.0)
511 .unwrap()
512 .passes;
513
514 let second = passes.pop().unwrap();
515 let first = passes.pop().unwrap();
516 assert_eq_f64(first.aos.as_ref().unwrap().time, 1_739_587_541.817_650_3);
517 assert_eq_f64(first.los.as_ref().unwrap().time, 1_739_588_409.207_694_5);
518 assert_eq_f64(second.aos.as_ref().unwrap().time, 1_739_593_848.217_693_8);
519 assert_eq_f64(second.los.as_ref().unwrap().time, 1_739_594_238.896_644_6);
520 assert_eq_f64(
521 first.aos.as_ref().unwrap().revolutions,
522 89_645.818_476_411_63,
523 );
524 assert_eq_f64(
525 second.aos.as_ref().unwrap().revolutions,
526 89_646.862_134_186_38,
527 );
528 }
529}