use nalgebra::{Vector3, Matrix3};
use std::f64::consts::PI;
const WGS84_A: f64 = 6378.137; const WGS84_F: f64 = 1.0 / 298.257223563; const WGS84_E2: f64 = WGS84_F * (2.0 - WGS84_F);
const SMALL: f64 = 1e-8;
#[derive(Debug, Clone, Copy)]
pub struct Observer {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
}
impl Observer {
pub fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
Observer { latitude, longitude, altitude }
}
pub fn to_ecef(&self) -> Vector3<f64> {
let sin_lat = self.latitude.sin();
let cos_lat = self.latitude.cos();
let sin_lon = self.longitude.sin();
let cos_lon = self.longitude.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let x = (n + self.altitude) * cos_lat * cos_lon;
let y = (n + self.altitude) * cos_lat * sin_lon;
let z = (n * (1.0 - WGS84_E2) + self.altitude) * sin_lat;
Vector3::new(x, y, z)
}
pub fn ecef_to_enu_matrix(&self) -> Matrix3<f64> {
let sin_lat = self.latitude.sin();
let cos_lat = self.latitude.cos();
let sin_lon = self.longitude.sin();
let cos_lon = self.longitude.cos();
Matrix3::new(
-sin_lon, cos_lon, 0.0,
-cos_lon * sin_lat, -sin_lon * sin_lat, cos_lat,
cos_lon * cos_lat, sin_lon * cos_lat, sin_lat,
)
}
pub fn ecef_to_sez_matrix(&self) -> Matrix3<f64> {
let sin_lat = self.latitude.sin();
let cos_lat = self.latitude.cos();
let sin_lon = self.longitude.sin();
let cos_lon = self.longitude.cos();
Matrix3::new(
sin_lat * cos_lon, sin_lat * sin_lon, -cos_lat,
-sin_lon, cos_lon, 0.0,
cos_lat * cos_lon, cos_lat * sin_lon, sin_lat,
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct TopocentricCoordinates {
pub range: f64,
pub azimuth: f64,
pub elevation: f64,
pub range_rate: Option<f64>,
pub enu: [f64; 3],
}
pub fn compute_azimuth_elevation(
sat_ecef: &[f64; 3],
observer: &Observer,
) -> TopocentricCoordinates {
let obs_ecef = observer.to_ecef();
let sat_vec = Vector3::new(sat_ecef[0], sat_ecef[1], sat_ecef[2]);
let rho_ecef = sat_vec - obs_ecef;
let range = rho_ecef.norm();
let rot_enu = observer.ecef_to_enu_matrix();
let rho_enu = rot_enu * rho_ecef;
let e = rho_enu[0]; let n = rho_enu[1]; let u = rho_enu[2];
let mut azimuth = e.atan2(n);
if azimuth < 0.0 {
azimuth += 2.0 * PI; }
let horizontal_range = (e * e + n * n).sqrt();
let elevation = if horizontal_range < SMALL {
if u > 0.0 { PI / 2.0 } else { -PI / 2.0 }
} else {
u.atan2(horizontal_range)
};
TopocentricCoordinates {
range,
azimuth,
elevation,
range_rate: None,
enu: [e, n, u],
}
}
pub fn compute_azimuth_elevation_rate(
sat_ecef: &[f64; 3],
vel_ecef: &[f64; 3],
observer: &Observer,
) -> TopocentricCoordinates {
let mut result = compute_azimuth_elevation(sat_ecef, observer);
let vel_vec = Vector3::new(vel_ecef[0], vel_ecef[1], vel_ecef[2]);
let rot_enu = observer.ecef_to_enu_matrix();
let vel_enu = rot_enu * vel_vec;
let rho_enu = Vector3::new(result.enu[0], result.enu[1], result.enu[2]);
let range_rate = rho_enu.dot(&vel_enu) / result.range;
result.range_rate = Some(range_rate);
result
}
pub fn is_visible(
sat_ecef: &[f64; 3],
observer: &Observer,
min_elevation: f64,
) -> bool {
let topo = compute_azimuth_elevation(sat_ecef, observer);
topo.elevation >= min_elevation
}
pub fn has_line_of_sight(
sat_ecef: &[f64; 3],
observer: &Observer,
) -> bool {
is_visible(sat_ecef, observer, 0.0)
}
#[derive(Debug, Clone, Copy)]
pub struct SatellitePass {
pub rise_time: f64,
pub set_time: f64,
pub max_elevation_time: f64,
pub max_elevation: f64,
pub rise_azimuth: f64,
pub set_azimuth: f64,
pub duration: f64,
}
pub fn find_next_pass<F>(
propagate_fn: &F,
observer: &Observer,
start_time: f64,
end_time: f64,
min_elevation: f64,
time_step: f64,
) -> Option<SatellitePass>
where
F: Fn(f64) -> [f64; 3],
{
let mut t = start_time;
let mut was_visible = {
let pos = propagate_fn(t);
is_visible(&pos, observer, min_elevation)
};
let mut rise_time = None;
while t < end_time {
t += time_step;
if t > end_time {
t = end_time;
}
let pos = propagate_fn(t);
let is_vis = is_visible(&pos, observer, min_elevation);
if !was_visible && is_vis {
let refined_rise = refine_event_time(
propagate_fn,
observer,
min_elevation,
t - time_step,
t,
false, );
rise_time = Some(refined_rise);
}
if was_visible && !is_vis && rise_time.is_some() {
let refined_set = refine_event_time(
propagate_fn,
observer,
min_elevation,
t - time_step,
t,
true, );
let rise = rise_time.unwrap();
let set = refined_set;
let (max_time, max_elev) = find_maximum_elevation(
propagate_fn,
observer,
rise,
set,
time_step / 10.0, );
let rise_pos = propagate_fn(rise);
let set_pos = propagate_fn(set);
let rise_az = compute_azimuth_elevation(&rise_pos, observer).azimuth;
let set_az = compute_azimuth_elevation(&set_pos, observer).azimuth;
return Some(SatellitePass {
rise_time: rise,
set_time: set,
max_elevation_time: max_time,
max_elevation: max_elev,
rise_azimuth: rise_az,
set_azimuth: set_az,
duration: set - rise,
});
}
was_visible = is_vis;
}
None
}
fn refine_event_time<F>(
propagate_fn: &F,
observer: &Observer,
min_elevation: f64,
t_start: f64,
t_end: f64,
looking_for_set: bool,
) -> f64
where
F: Fn(f64) -> [f64; 3],
{
let mut a = t_start;
let mut b = t_end;
const TOLERANCE: f64 = 1.0 / 3600.0;
while (b - a).abs() > TOLERANCE {
let mid = (a + b) / 2.0;
let pos = propagate_fn(mid);
let topo = compute_azimuth_elevation(&pos, observer);
let is_above = topo.elevation >= min_elevation;
if looking_for_set {
if is_above {
a = mid;
} else {
b = mid;
}
} else {
if is_above {
b = mid;
} else {
a = mid;
}
}
}
(a + b) / 2.0
}
fn find_maximum_elevation<F>(
propagate_fn: &F,
observer: &Observer,
t_start: f64,
t_end: f64,
time_step: f64,
) -> (f64, f64)
where
F: Fn(f64) -> [f64; 3],
{
let mut max_elev = f64::NEG_INFINITY;
let mut max_time = t_start;
let mut t = t_start;
while t <= t_end {
let pos = propagate_fn(t);
let topo = compute_azimuth_elevation(&pos, observer);
if topo.elevation > max_elev {
max_elev = topo.elevation;
max_time = t;
}
t += time_step;
}
let search_window = time_step * 2.0;
let refined_time = golden_section_search(
propagate_fn,
observer,
(max_time - search_window).max(t_start),
(max_time + search_window).min(t_end),
);
let pos = propagate_fn(refined_time);
let final_elev = compute_azimuth_elevation(&pos, observer).elevation;
(refined_time, final_elev)
}
fn golden_section_search<F>(
propagate_fn: &F,
observer: &Observer,
mut a: f64,
mut b: f64,
) -> f64
where
F: Fn(f64) -> [f64; 3],
{
const GOLDEN_RATIO: f64 = 1.618033988749895;
const TOLERANCE: f64 = 1.0 / 3600.0;
let mut c = b - (b - a) / GOLDEN_RATIO;
let mut d = a + (b - a) / GOLDEN_RATIO;
while (b - a).abs() > TOLERANCE {
let fc = compute_azimuth_elevation(&propagate_fn(c), observer).elevation;
let fd = compute_azimuth_elevation(&propagate_fn(d), observer).elevation;
if fc > fd {
b = d;
d = c;
c = b - (b - a) / GOLDEN_RATIO;
} else {
a = c;
c = d;
d = a + (b - a) / GOLDEN_RATIO;
}
}
(a + b) / 2.0
}
pub fn find_all_passes<F>(
propagate_fn: &F,
observer: &Observer,
start_time: f64,
end_time: f64,
min_elevation: f64,
time_step: f64,
) -> Vec<SatellitePass>
where
F: Fn(f64) -> [f64; 3],
{
let mut passes = Vec::new();
let mut search_start = start_time;
while search_start < end_time {
if let Some(pass) = find_next_pass(
propagate_fn,
observer,
search_start,
end_time,
min_elevation,
time_step,
) {
passes.push(pass);
search_start = pass.set_time + time_step;
} else {
break;
}
}
passes
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_observer_ecef_conversion() {
let obs = Observer::new(0.0, 0.0, 0.0);
let ecef = obs.to_ecef();
assert_relative_eq!(ecef[0], WGS84_A, epsilon = 1e-6);
assert_relative_eq!(ecef[1], 0.0, epsilon = 1e-6);
assert_relative_eq!(ecef[2], 0.0, epsilon = 1e-6);
}
#[test]
fn test_observer_north_pole() {
let obs = Observer::new(PI / 2.0, 0.0, 0.0);
let ecef = obs.to_ecef();
assert_relative_eq!(ecef[0], 0.0, epsilon = 1e-6);
assert_relative_eq!(ecef[1], 0.0, epsilon = 1e-6);
assert!(ecef[2] > 6350.0 && ecef[2] < 6360.0);
}
#[test]
fn test_azimuth_elevation_zenith() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let radial_direction = obs_ecef.normalize();
let sat_ecef_vec = obs_ecef + radial_direction * 500.0;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.elevation, PI / 2.0, epsilon = 0.01);
assert_relative_eq!(topo.range, 500.0, epsilon = 1.0);
}
#[test]
fn test_azimuth_north() {
let obs = Observer::new(0.0, 0.0, 0.0);
let enu_north = Vector3::new(0.0, 500.0, 500.0);
let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu_north;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert!(topo.azimuth < 0.1 || topo.azimuth > 2.0 * PI - 0.1,
"Azimuth was {:.4} rad ({:.1}°), expected ~0°",
topo.azimuth, topo.azimuth.to_degrees());
assert_relative_eq!(topo.elevation, PI / 4.0, epsilon = 0.1);
}
#[test]
fn test_visibility_above_horizon() {
let obs = Observer::new(
42.36_f64.to_radians(),
-71.09_f64.to_radians(),
0.010, );
let obs_ecef = obs.to_ecef();
let sat_ecef = [
obs_ecef[0] + 200.0,
obs_ecef[1] + 200.0,
obs_ecef[2] + 400.0,
];
assert!(is_visible(&sat_ecef, &obs, 0.0));
assert!(has_line_of_sight(&sat_ecef, &obs));
}
#[test]
fn test_visibility_below_horizon() {
let obs = Observer::new(
42.36_f64.to_radians(),
-71.09_f64.to_radians(),
0.010,
);
let obs_ecef = obs.to_ecef();
let sat_ecef = [
-obs_ecef[0],
-obs_ecef[1],
-obs_ecef[2],
];
assert!(!is_visible(&sat_ecef, &obs, 0.0));
assert!(!has_line_of_sight(&sat_ecef, &obs));
}
#[test]
fn test_minimum_elevation_threshold() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let sat_ecef = [
obs_ecef[0] + 400.0,
obs_ecef[1],
obs_ecef[2] + 35.0, ];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert!(is_visible(&sat_ecef, &obs, 0.0));
let min_el_10deg = 10.0_f64.to_radians();
assert_eq!(is_visible(&sat_ecef, &obs, min_el_10deg), topo.elevation >= min_el_10deg);
}
#[test]
fn test_range_rate_calculation() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let sat_ecef = [obs_ecef[0], obs_ecef[1], obs_ecef[2] + 500.0];
let vel_ecef = [0.0, 0.0, 5.0];
let topo = compute_azimuth_elevation_rate(&sat_ecef, &vel_ecef, &obs);
assert!(topo.range_rate.is_some());
let range_rate = topo.range_rate.unwrap();
assert!(range_rate > 0.0);
assert_relative_eq!(range_rate, 5.0, epsilon = 0.5);
}
#[test]
fn test_range_rate_approaching() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let sat_ecef = [obs_ecef[0] + 500.0, obs_ecef[1], obs_ecef[2]];
let vel_ecef = [-3.0, 0.0, 0.0];
let topo = compute_azimuth_elevation_rate(&sat_ecef, &vel_ecef, &obs);
assert!(topo.range_rate.is_some());
let range_rate = topo.range_rate.unwrap();
assert!(range_rate < 0.0);
}
#[test]
fn test_ecef_to_enu_matrix_equator() {
let obs = Observer::new(0.0, 0.0, 0.0);
let rot = obs.ecef_to_enu_matrix();
let det = rot.determinant();
assert_relative_eq!(det.abs(), 1.0, epsilon = 1e-10);
let ecef_east = Vector3::new(0.0, 1.0, 0.0);
let enu_east = rot * ecef_east;
assert_relative_eq!(enu_east[0], 1.0, epsilon = 1e-10); assert_relative_eq!(enu_east[1], 0.0, epsilon = 1e-10); assert_relative_eq!(enu_east[2], 0.0, epsilon = 1e-10); }
#[test]
fn test_ecef_to_enu_matrix_north_pole() {
let obs = Observer::new(PI / 2.0, 0.0, 0.0);
let rot = obs.ecef_to_enu_matrix();
let det = rot.determinant();
assert_relative_eq!(det.abs(), 1.0, epsilon = 1e-10);
let ecef_up = Vector3::new(0.0, 0.0, 1.0);
let enu = rot * ecef_up;
assert_relative_eq!(enu[2], 1.0, epsilon = 1e-10); }
#[test]
fn test_ecef_to_sez_matrix() {
let obs = Observer::new(0.0, 0.0, 0.0);
let rot_sez = obs.ecef_to_sez_matrix();
let det = rot_sez.determinant();
assert_relative_eq!(det.abs(), 1.0, epsilon = 1e-10);
}
#[test]
fn test_azimuth_east() {
let obs = Observer::new(0.0, 0.0, 0.0);
let enu_east = Vector3::new(500.0, 0.0, 500.0); let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu_east;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.azimuth, PI / 2.0, epsilon = 0.1);
}
#[test]
fn test_azimuth_south() {
let obs = Observer::new(0.0, 0.0, 0.0);
let enu_south = Vector3::new(0.0, -500.0, 500.0); let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu_south;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.azimuth, PI, epsilon = 0.1);
}
#[test]
fn test_azimuth_west() {
let obs = Observer::new(0.0, 0.0, 0.0);
let enu_west = Vector3::new(-500.0, 0.0, 500.0); let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu_west;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.azimuth, 3.0 * PI / 2.0, epsilon = 0.1);
}
#[test]
fn test_observer_southern_hemisphere() {
let obs = Observer::new(
-33.87_f64.to_radians(),
151.21_f64.to_radians(),
0.050, );
let ecef = obs.to_ecef();
assert!(ecef[2] < 0.0);
let radial_direction = ecef.normalize();
let sat_ecef_vec = ecef + radial_direction * 500.0; let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert!(topo.azimuth >= 0.0 && topo.azimuth <= 2.0 * PI);
assert!(topo.elevation > 0.0);
}
#[test]
fn test_observer_with_altitude() {
let obs_sea = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_mountain = Observer::new(45.0_f64.to_radians(), 0.0, 5.0);
let ecef_sea = obs_sea.to_ecef();
let ecef_mountain = obs_mountain.to_ecef();
assert!(ecef_mountain.norm() > ecef_sea.norm());
assert_relative_eq!(ecef_mountain.norm() - ecef_sea.norm(), 5.0, epsilon = 0.1);
}
#[test]
fn test_elevation_nadir() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let radial_direction = obs_ecef.normalize();
let sat_ecef_vec = obs_ecef - radial_direction * 100.0; let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert!(topo.elevation < 0.0);
assert_relative_eq!(topo.elevation, -PI / 2.0, epsilon = 0.01);
}
#[test]
fn test_azimuth_wrapping() {
let obs = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
for angle in [0.0_f64, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0] {
let az_rad = angle.to_radians();
let e = 500.0 * az_rad.sin();
let n = 500.0 * az_rad.cos();
let u = 200.0;
let enu = Vector3::new(e, n, u);
let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert!(topo.azimuth >= 0.0);
assert!(topo.azimuth < 2.0 * PI);
}
}
#[test]
fn test_find_next_pass_simple() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |t_minutes: f64| -> [f64; 3] {
let phase = (t_minutes - 5.0) / 5.0; let elevation_factor = 1.0 - phase * phase;
let enu = Vector3::new(
0.0, t_minutes * 50.0, elevation_factor * 500.0 - 100.0, );
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let pass = find_next_pass(&propagate, &observer, 0.0, 20.0, 0.0, 0.5);
assert!(pass.is_some());
let pass = pass.unwrap();
assert!(pass.rise_time > 0.0 && pass.rise_time < 5.0);
assert!(pass.set_time > 5.0 && pass.set_time < 10.0);
assert!(pass.max_elevation_time > pass.rise_time);
assert!(pass.max_elevation_time < pass.set_time);
assert!(pass.max_elevation > 0.0);
assert!(pass.duration > 0.0);
}
#[test]
fn test_find_next_pass_no_pass() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |_t_minutes: f64| -> [f64; 3] {
let enu = Vector3::new(0.0, 1000.0, -200.0); let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let pass = find_next_pass(&propagate, &observer, 0.0, 100.0, 0.0, 1.0);
assert!(pass.is_none());
}
#[test]
fn test_find_next_pass_with_min_elevation() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |t_minutes: f64| -> [f64; 3] {
let phase = (t_minutes - 10.0) / 8.0; let elevation_factor = 1.0 - phase * phase;
let enu = Vector3::new(
0.0,
t_minutes * 50.0,
elevation_factor * 600.0 - 150.0, );
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let pass_0deg = find_next_pass(&propagate, &observer, 0.0, 25.0, 0.0, 0.5);
assert!(pass_0deg.is_some());
let pass_10deg = find_next_pass(&propagate, &observer, 0.0, 25.0, 10.0_f64.to_radians(), 0.5);
assert!(pass_10deg.is_some());
let pass_60deg = find_next_pass(&propagate, &observer, 0.0, 25.0, 60.0_f64.to_radians(), 0.5);
assert!(pass_60deg.is_none());
}
#[test]
fn test_find_all_passes() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |t_minutes: f64| -> [f64; 3] {
let period = 20.0;
let t_in_period = t_minutes % period;
let phase = (t_in_period - 5.0) / 5.0;
let elevation_factor = 1.0 - phase * phase;
let enu = Vector3::new(
0.0,
t_minutes * 50.0,
elevation_factor * 500.0 - 100.0,
);
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let passes = find_all_passes(&propagate, &observer, 0.0, 40.0, 0.0, 0.5);
assert!(passes.len() >= 1); for pass in &passes {
assert!(pass.rise_time < pass.set_time);
assert!(pass.max_elevation_time >= pass.rise_time);
assert!(pass.max_elevation_time <= pass.set_time);
assert!(pass.duration > 0.0);
}
}
#[test]
fn test_find_all_passes_empty() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |_t_minutes: f64| -> [f64; 3] {
let enu = Vector3::new(0.0, 1000.0, -200.0);
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let passes = find_all_passes(&propagate, &observer, 0.0, 100.0, 0.0, 1.0);
assert_eq!(passes.len(), 0);
}
#[test]
fn test_enu_components_accuracy() {
let obs = Observer::new(0.0, 0.0, 0.0);
let e_expected = 100.0;
let n_expected = 200.0;
let u_expected = 300.0;
let enu_vec = Vector3::new(e_expected, n_expected, u_expected);
let rot_enu = obs.ecef_to_enu_matrix();
let obs_ecef = obs.to_ecef();
let ecef_offset = rot_enu.transpose() * enu_vec;
let sat_ecef_vec = obs_ecef + ecef_offset;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.enu[0], e_expected, epsilon = 1e-6);
assert_relative_eq!(topo.enu[1], n_expected, epsilon = 1e-6);
assert_relative_eq!(topo.enu[2], u_expected, epsilon = 1e-6);
}
#[test]
fn test_range_accuracy() {
let obs = Observer::new(0.0, 0.0, 0.0);
let obs_ecef = obs.to_ecef();
let range_expected = 500.0; let direction = Vector3::new(1.0, 1.0, 1.0).normalize();
let sat_ecef_vec = obs_ecef + direction * range_expected;
let sat_ecef = [sat_ecef_vec[0], sat_ecef_vec[1], sat_ecef_vec[2]];
let topo = compute_azimuth_elevation(&sat_ecef, &obs);
assert_relative_eq!(topo.range, range_expected, epsilon = 0.1);
}
#[test]
fn test_satellite_pass_duration() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |t_minutes: f64| -> [f64; 3] {
let phase = (t_minutes - 10.0) / 10.0;
let elevation_factor = 1.0 - phase * phase;
let enu = Vector3::new(0.0, t_minutes * 50.0, elevation_factor * 500.0 - 50.0);
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let pass = find_next_pass(&propagate, &observer, 0.0, 30.0, 0.0, 0.5);
assert!(pass.is_some());
let pass = pass.unwrap();
assert_relative_eq!(pass.duration, pass.set_time - pass.rise_time, epsilon = 1e-10);
}
#[test]
fn test_max_elevation_time_bounds() {
let observer = Observer::new(45.0_f64.to_radians(), 0.0, 0.0);
let obs_ecef = observer.to_ecef();
let propagate = |t_minutes: f64| -> [f64; 3] {
let phase = (t_minutes - 15.0) / 10.0;
let elevation_factor = 1.0 - phase * phase;
let enu = Vector3::new(0.0, t_minutes * 30.0, elevation_factor * 600.0);
let rot_enu = observer.ecef_to_enu_matrix();
let ecef_offset = rot_enu.transpose() * enu;
let sat_ecef = obs_ecef + ecef_offset;
[sat_ecef[0], sat_ecef[1], sat_ecef[2]]
};
let pass = find_next_pass(&propagate, &observer, 0.0, 40.0, 0.0, 0.5);
assert!(pass.is_some());
let pass = pass.unwrap();
assert!(pass.max_elevation_time >= pass.rise_time);
assert!(pass.max_elevation_time <= pass.set_time);
}
}