use std::f64::consts::PI;
const WGS84_A: f64 = 6378.137; const WGS84_B: f64 = 6356.752314245; const WGS84_F: f64 = 1.0 / 298.257223563; const WGS84_E2: f64 = WGS84_F * (2.0 - WGS84_F); const WGS84_EP2: f64 = (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
const GEODETIC_TOLERANCE: f64 = 1e-12;
const MAX_GEODETIC_ITERATIONS: usize = 10;
#[derive(Debug, Clone, Copy)]
pub struct GeodeticCoordinates {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
}
impl GeodeticCoordinates {
pub fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
Self {
latitude,
longitude,
altitude,
}
}
pub fn to_degrees(&self) -> (f64, f64, f64) {
(
self.latitude.to_degrees(),
self.longitude.to_degrees(),
self.altitude,
)
}
}
pub fn ecef_to_geodetic(ecef: &[f64; 3]) -> GeodeticCoordinates {
let x = ecef[0];
let y = ecef[1];
let z = ecef[2];
let longitude = y.atan2(x);
let p = (x * x + y * y).sqrt();
if p < 1e-10 {
let latitude = if z >= 0.0 { PI / 2.0 } else { -PI / 2.0 };
let altitude = z.abs() - WGS84_B;
return GeodeticCoordinates::new(latitude, longitude, altitude);
}
let mut latitude = (z / ((1.0 - WGS84_E2) * p)).atan();
let mut altitude = 0.0;
for _ in 0..MAX_GEODETIC_ITERATIONS {
let sin_lat = latitude.sin();
let cos_lat = latitude.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let h_new = if cos_lat.abs() > 1e-10 {
p / cos_lat - n
} else {
z / sin_lat - n * (1.0 - WGS84_E2)
};
let lat_new = (z / ((1.0 - WGS84_E2 * n / (n + h_new)) * p)).atan();
let lat_change = (lat_new - latitude).abs();
latitude = lat_new;
altitude = h_new;
if lat_change < GEODETIC_TOLERANCE {
break;
}
}
GeodeticCoordinates::new(latitude, longitude, altitude)
}
#[inline]
pub fn sub_satellite_point(sat_ecef: &[f64; 3]) -> GeodeticCoordinates {
ecef_to_geodetic(sat_ecef)
}
#[derive(Debug, Clone, Copy)]
pub struct GroundTrackPoint {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
pub time: f64,
}
impl GroundTrackPoint {
pub fn new(latitude: f64, longitude: f64, altitude: f64, time: f64) -> Self {
Self {
latitude,
longitude,
altitude,
time,
}
}
pub fn to_degrees(&self) -> (f64, f64, f64, f64) {
(
self.latitude.to_degrees(),
self.longitude.to_degrees(),
self.altitude,
self.time,
)
}
}
pub fn compute_ground_track(
ecef_positions: &[[f64; 3]],
times: &[f64],
) -> Vec<GroundTrackPoint> {
assert_eq!(
ecef_positions.len(),
times.len(),
"ECEF positions and times must have the same length"
);
ecef_positions
.iter()
.zip(times.iter())
.map(|(pos, &time)| {
let geodetic = ecef_to_geodetic(pos);
GroundTrackPoint::new(
geodetic.latitude,
geodetic.longitude,
geodetic.altitude,
time,
)
})
.collect()
}
pub fn calculate_swath_width(altitude: f64, min_elevation: f64) -> f64 {
let max_range = maximum_ground_range(altitude);
let elevation_factor = (PI / 2.0 - min_elevation) / (PI / 2.0);
2.0 * max_range * elevation_factor
}
pub fn maximum_ground_range(altitude: f64) -> f64 {
let r_earth = 6371.0; let r_sat = r_earth + altitude;
let lambda = (r_earth / r_sat).acos();
r_earth * lambda
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_ecef_to_geodetic_equator() {
let ecef = [WGS84_A, 0.0, 0.0];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, 0.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.longitude, 0.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, 0.0, epsilon = 1e-3); }
#[test]
fn test_ecef_to_geodetic_north_pole() {
let ecef = [0.0, 0.0, WGS84_B];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, PI / 2.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, 0.0, epsilon = 1e-3);
}
#[test]
fn test_ecef_to_geodetic_south_pole() {
let ecef = [0.0, 0.0, -WGS84_B];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, -PI / 2.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, 0.0, epsilon = 1e-3);
}
#[test]
fn test_ecef_to_geodetic_prime_meridian() {
let lat = 45.0_f64.to_radians();
let lon = 0.0;
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let x = n * cos_lat;
let y = 0.0;
let z = n * (1.0 - WGS84_E2) * sin_lat;
let ecef = [x, y, z];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, lat, epsilon = 1e-9);
assert_relative_eq!(geodetic.longitude, lon, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, 0.0, epsilon = 1e-3);
}
#[test]
fn test_ecef_to_geodetic_with_altitude() {
let h = 500.0;
let ecef = [WGS84_A + h, 0.0, 0.0];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, 0.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.longitude, 0.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, h, epsilon = 1e-3);
}
#[test]
fn test_ecef_to_geodetic_leo_satellite() {
let ecef = [6700.0, 0.0, 500.0];
let geodetic = ecef_to_geodetic(&ecef);
assert!(geodetic.latitude.abs() < 10.0_f64.to_radians()); assert!(geodetic.altitude > 300.0 && geodetic.altitude < 500.0); }
#[test]
fn test_ecef_to_geodetic_western_longitude() {
let ecef = [0.0, -WGS84_A, 0.0];
let geodetic = ecef_to_geodetic(&ecef);
assert_relative_eq!(geodetic.latitude, 0.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.longitude, -PI / 2.0, epsilon = 1e-9);
assert_relative_eq!(geodetic.altitude, 0.0, epsilon = 1e-3);
}
#[test]
fn test_sub_satellite_point() {
let ecef = [6700.0, 1000.0, 200.0];
let geodetic1 = ecef_to_geodetic(&ecef);
let geodetic2 = sub_satellite_point(&ecef);
assert_relative_eq!(geodetic1.latitude, geodetic2.latitude, epsilon = 1e-12);
assert_relative_eq!(geodetic1.longitude, geodetic2.longitude, epsilon = 1e-12);
assert_relative_eq!(geodetic1.altitude, geodetic2.altitude, epsilon = 1e-12);
}
#[test]
fn test_compute_ground_track() {
let positions = vec![
[WGS84_A + 400.0, 0.0, 0.0],
[WGS84_A + 400.0, 100.0, 50.0],
];
let times = vec![0.0, 1.0];
let track = compute_ground_track(&positions, ×);
assert_eq!(track.len(), 2);
assert_relative_eq!(track[0].time, 0.0, epsilon = 1e-12);
assert_relative_eq!(track[1].time, 1.0, epsilon = 1e-12);
assert_relative_eq!(track[0].latitude, 0.0, epsilon = 1e-6);
assert_relative_eq!(track[0].longitude, 0.0, epsilon = 1e-6);
assert!((track[0].altitude - 400.0).abs() < 10.0);
assert!((track[1].altitude - 400.0).abs() < 50.0);
}
#[test]
fn test_calculate_swath_width_leo() {
let altitude = 500.0;
let min_elevation = 10.0_f64.to_radians();
let swath = calculate_swath_width(altitude, min_elevation);
let max_range = maximum_ground_range(altitude);
assert!(swath > 0.0 && swath < 2.0 * max_range, "Swath was {:.1} km", swath);
}
#[test]
fn test_calculate_swath_width_higher_altitude() {
let altitude = 800.0;
let min_elevation = 10.0_f64.to_radians();
let swath = calculate_swath_width(altitude, min_elevation);
assert!(swath > 0.0, "Swath should be positive: {:.1} km", swath);
}
#[test]
fn test_calculate_swath_width_lower_elevation() {
let altitude = 500.0;
let min_elev_5 = 5.0_f64.to_radians();
let min_elev_10 = 10.0_f64.to_radians();
let swath_5 = calculate_swath_width(altitude, min_elev_5);
let swath_10 = calculate_swath_width(altitude, min_elev_10);
assert!(swath_5 > swath_10, "swath_5={:.1}, swath_10={:.1}", swath_5, swath_10);
}
#[test]
fn test_maximum_ground_range_leo() {
let altitude = 400.0;
let max_range = maximum_ground_range(altitude);
assert!(max_range > 2200.0 && max_range < 2400.0);
}
#[test]
fn test_maximum_ground_range_scaling() {
let range_400 = maximum_ground_range(400.0);
let range_800 = maximum_ground_range(800.0);
assert!(range_800 > range_400);
}
}