use std::f64::consts::PI;
use crate::satellite::visibility::SatellitePass;
const R_EARTH: f64 = 6371.0; const WGS84_A: f64 = 6378.137;
#[derive(Debug, Clone, Copy)]
pub struct GeodeticPoint {
pub latitude: f64,
pub longitude: f64,
}
#[derive(Debug, Clone)]
pub struct AccessStatistics {
pub num_passes: usize,
pub total_access_time: f64,
pub average_pass_duration: f64,
pub max_elevation: f64,
pub best_pass_index: Option<usize>,
pub min_pass_duration: f64,
pub max_pass_duration: f64,
}
pub fn visibility_circle(
lat_sub: f64,
lon_sub: f64,
altitude: f64,
min_elevation: f64,
num_points: usize,
) -> Vec<GeodeticPoint> {
assert!(altitude >= 0.0, "Altitude must be non-negative");
assert!(num_points > 0, "Number of points must be positive");
let lat_rad = lat_sub.to_radians();
let lon_rad = lon_sub.to_radians();
let elev_rad = min_elevation.to_radians();
let r_sat = R_EARTH + altitude;
let sin_eta = elev_rad.cos() * R_EARTH / r_sat;
let sin_eta = sin_eta.min(1.0).max(-1.0);
let eta = sin_eta.asin();
let lambda = (PI / 2.0) - elev_rad - eta;
let mut points = Vec::with_capacity(num_points);
for i in 0..num_points {
let bearing = 2.0 * PI * (i as f64) / (num_points as f64);
let lat2 = (lat_rad.sin() * lambda.cos()
+ lat_rad.cos() * lambda.sin() * bearing.cos()).asin();
let lon2 = lon_rad + (bearing.sin() * lambda.sin() * lat_rad.cos())
.atan2(lambda.cos() - lat_rad.sin() * lat2.sin());
points.push(GeodeticPoint {
latitude: lat2.to_degrees(),
longitude: normalize_longitude(lon2.to_degrees()),
});
}
points
}
pub fn coverage_area(altitude: f64, min_elevation: f64) -> f64 {
assert!(altitude >= 0.0, "Altitude must be non-negative");
let elev_rad = min_elevation.to_radians();
let r_sat = R_EARTH + altitude;
let sin_eta = elev_rad.cos() * R_EARTH / r_sat;
let sin_eta = sin_eta.min(1.0).max(-1.0);
let eta = sin_eta.asin();
let lambda = (PI / 2.0) - elev_rad - eta;
2.0 * PI * WGS84_A * WGS84_A * (1.0 - lambda.cos())
}
pub fn compute_access_statistics(passes: &[SatellitePass]) -> Option<AccessStatistics> {
if passes.is_empty() {
return None;
}
let num_passes = passes.len();
let mut total_time = 0.0;
let mut max_elevation = 0.0;
let mut best_pass_index = 0;
let mut min_duration = f64::INFINITY;
let mut max_duration = 0.0;
for (i, pass) in passes.iter().enumerate() {
let duration = pass.set_time - pass.rise_time;
total_time += duration;
if duration < min_duration {
min_duration = duration;
}
if duration > max_duration {
max_duration = duration;
}
if pass.max_elevation > max_elevation {
max_elevation = pass.max_elevation;
best_pass_index = i;
}
}
let average_duration = total_time / (num_passes as f64);
Some(AccessStatistics {
num_passes,
total_access_time: total_time,
average_pass_duration: average_duration,
max_elevation,
best_pass_index: Some(best_pass_index),
min_pass_duration: if min_duration == f64::INFINITY { 0.0 } else { min_duration },
max_pass_duration: max_duration,
})
}
fn normalize_longitude(lon: f64) -> f64 {
let mut normalized = lon;
while normalized > 180.0 {
normalized -= 360.0;
}
while normalized < -180.0 {
normalized += 360.0;
}
normalized
}
pub fn coverage_percentage(total_access_time: f64, time_span: f64) -> f64 {
if time_span <= 0.0 {
return 0.0;
}
(total_access_time / time_span).min(1.0).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_visibility_circle_basic() {
let circle = visibility_circle(0.0, 0.0, 400.0, 0.0, 32);
assert_eq!(circle.len(), 32);
for point in &circle {
let distance = angular_distance(0.0, 0.0, point.latitude, point.longitude);
assert!(distance > 19.0 && distance < 21.0,
"Distance {:.2}° outside expected range", distance);
}
}
#[test]
fn test_visibility_circle_symmetry() {
let circle = visibility_circle(0.0, 0.0, 400.0, 10.0, 36);
let point_0 = &circle[0]; let point_18 = &circle[18];
assert_relative_eq!(point_0.latitude, -point_18.latitude, epsilon = 1.0);
}
#[test]
fn test_coverage_area_increases_with_altitude() {
let area_leo = coverage_area(400.0, 10.0); let area_meo = coverage_area(20000.0, 10.0); let area_geo = coverage_area(35786.0, 10.0);
assert!(area_leo < area_meo);
assert!(area_meo < area_geo);
assert!(area_leo > 5_000_000.0 && area_leo < 7_000_000.0,
"ISS coverage area {:.0} km² outside expected range", area_leo);
}
#[test]
fn test_coverage_area_decreases_with_elevation() {
let area_0deg = coverage_area(400.0, 0.0); let area_10deg = coverage_area(400.0, 10.0); let area_45deg = coverage_area(400.0, 45.0);
assert!(area_45deg < area_10deg);
assert!(area_10deg < area_0deg);
}
#[test]
fn test_access_statistics_basic() {
let passes = vec![
SatellitePass {
rise_time: 0.0,
set_time: 10.0,
max_elevation_time: 5.0,
max_elevation: 45.0,
rise_azimuth: 0.0,
set_azimuth: 180.0,
duration: 10.0,
},
SatellitePass {
rise_time: 100.0,
set_time: 115.0,
max_elevation_time: 107.5,
max_elevation: 80.0,
rise_azimuth: 180.0,
set_azimuth: 0.0,
duration: 15.0,
},
];
let stats = compute_access_statistics(&passes).unwrap();
assert_eq!(stats.num_passes, 2);
assert_relative_eq!(stats.total_access_time, 25.0, epsilon = 1e-6); assert_relative_eq!(stats.average_pass_duration, 12.5, epsilon = 1e-6);
assert_relative_eq!(stats.max_elevation, 80.0, epsilon = 1e-6);
assert_eq!(stats.best_pass_index, Some(1));
assert_relative_eq!(stats.min_pass_duration, 10.0, epsilon = 1e-6);
assert_relative_eq!(stats.max_pass_duration, 15.0, epsilon = 1e-6);
}
#[test]
fn test_access_statistics_empty() {
let passes: Vec<SatellitePass> = vec![];
let stats = compute_access_statistics(&passes);
assert!(stats.is_none());
}
#[test]
fn test_coverage_percentage() {
let fraction = coverage_percentage(90.0, 1440.0);
assert_relative_eq!(fraction, 0.0625, epsilon = 1e-6);
let full = coverage_percentage(100.0, 100.0);
assert_relative_eq!(full, 1.0, epsilon = 1e-6);
let zero = coverage_percentage(0.0, 100.0);
assert_relative_eq!(zero, 0.0, epsilon = 1e-6);
}
#[test]
fn test_normalize_longitude() {
assert_relative_eq!(normalize_longitude(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(normalize_longitude(180.0), 180.0, epsilon = 1e-10);
assert_relative_eq!(normalize_longitude(-180.0), -180.0, epsilon = 1e-10);
assert_relative_eq!(normalize_longitude(190.0), -170.0, epsilon = 1e-10);
assert_relative_eq!(normalize_longitude(-190.0), 170.0, epsilon = 1e-10);
assert_relative_eq!(normalize_longitude(370.0), 10.0, epsilon = 1e-10);
}
fn angular_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let dlon = (lon2 - lon1).to_radians();
let a = ((lat2_rad - lat1_rad) / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
c.to_degrees()
}
}