use crate::spacetime::{metres_to_bright_meters, BRIGHT_METER_M, SPEED_OF_LIGHT_M_PER_S};
pub const WGS84_SEMI_MAJOR_AXIS_M: f64 = 6_378_137.0;
pub const WGS84_INVERSE_FLATTENING: f64 = 298.257_223_563;
pub const WGS84_FLATTENING: f64 = 1.0 / WGS84_INVERSE_FLATTENING;
pub const WGS84_FIRST_ECCENTRICITY_SQUARED: f64 =
WGS84_FLATTENING * (2.0 - WGS84_FLATTENING);
pub const WGS84_SEMI_MINOR_AXIS_M: f64 =
WGS84_SEMI_MAJOR_AXIS_M * (1.0 - WGS84_FLATTENING);
pub const EARTH_MEAN_RADIUS_M: f64 = 6_371_008.8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeodeticCoordinate {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
}
impl GeodeticCoordinate {
pub const fn surface(latitude: f64, longitude: f64) -> Self {
Self {
latitude,
longitude,
altitude: 0.0,
}
}
pub const fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
Self {
latitude,
longitude,
altitude,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EcefCoordinate {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl EcefCoordinate {
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DistancePair {
pub chord_metres: f64,
pub chord_bright_meters: f64,
pub surface_metres: f64,
pub surface_minus_chord_metres: f64,
pub light_travel_seconds: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BrightSpaceDistance {
pub chord_metres: f64,
pub chord_bright_meters: f64,
pub central_angle_radians: f64,
pub arc_mean_earth_radius_metres: f64,
pub arc_average_radius_metres: f64,
pub light_travel_seconds: f64,
}
const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0;
const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
pub fn geodetic_to_ecef(coord: GeodeticCoordinate) -> EcefCoordinate {
let GeodeticCoordinate {
latitude,
longitude,
altitude,
} = coord;
let phi = latitude * DEG_TO_RAD;
let lambda = longitude * DEG_TO_RAD;
let sin_phi = phi.sin();
let cos_phi = phi.cos();
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
let n = WGS84_SEMI_MAJOR_AXIS_M
/ (1.0 - WGS84_FIRST_ECCENTRICITY_SQUARED * sin_phi * sin_phi).sqrt();
let x = (n + altitude) * cos_phi * cos_lambda;
let y = (n + altitude) * cos_phi * sin_lambda;
let z = (n * (1.0 - WGS84_FIRST_ECCENTRICITY_SQUARED) + altitude) * sin_phi;
EcefCoordinate { x, y, z }
}
pub fn ecef_to_geodetic(ecef: EcefCoordinate) -> GeodeticCoordinate {
let EcefCoordinate { x, y, z } = ecef;
let a = WGS84_SEMI_MAJOR_AXIS_M;
let b = WGS84_SEMI_MINOR_AXIS_M;
let e2 = WGS84_FIRST_ECCENTRICITY_SQUARED;
let e_prime_2 = (a * a - b * b) / (b * b);
let p = (x * x + y * y).sqrt();
if p == 0.0 {
return GeodeticCoordinate {
latitude: if z >= 0.0 { 90.0 } else { -90.0 },
longitude: 0.0,
altitude: z.abs() - b,
};
}
let theta = (z * a).atan2(p * b);
let sin_theta = theta.sin();
let cos_theta = theta.cos();
let phi = (z + e_prime_2 * b * sin_theta * sin_theta * sin_theta)
.atan2(p - e2 * a * cos_theta * cos_theta * cos_theta);
let lambda = y.atan2(x);
let sin_phi = phi.sin();
let n = a / (1.0 - e2 * sin_phi * sin_phi).sqrt();
let cos_phi = phi.cos();
let altitude = if cos_phi.abs() > 1e-12 {
p / cos_phi - n
} else {
z / sin_phi - n * (1.0 - e2)
};
GeodeticCoordinate {
latitude: phi * RAD_TO_DEG,
longitude: lambda * RAD_TO_DEG,
altitude,
}
}
#[inline]
pub fn ecef_magnitude(v: EcefCoordinate) -> f64 {
(v.x * v.x + v.y * v.y + v.z * v.z).sqrt()
}
#[inline]
pub fn ecef_chord_metres(a: EcefCoordinate, b: EcefCoordinate) -> f64 {
let dx = a.x - b.x;
let dy = a.y - b.y;
let dz = a.z - b.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[inline]
pub fn ecef_chord_bright_meters(a: EcefCoordinate, b: EcefCoordinate) -> f64 {
metres_to_bright_meters(ecef_chord_metres(a, b))
}
pub fn ecef_central_angle(a: EcefCoordinate, b: EcefCoordinate) -> f64 {
let mag_a = ecef_magnitude(a);
let mag_b = ecef_magnitude(b);
if mag_a == 0.0 || mag_b == 0.0 {
return f64::NAN;
}
let chord = ecef_chord_metres(a, b);
let radial_sep = mag_a - mag_b;
let abs_radial = radial_sep.abs();
let factor1 = (chord - abs_radial).max(0.0);
let factor2 = chord + abs_radial;
let sin_half_squared = (factor1 * factor2) / (4.0 * mag_a * mag_b);
2.0 * sin_half_squared.clamp(0.0, 1.0).sqrt().asin()
}
#[inline]
pub fn ecef_arc_metres(a: EcefCoordinate, b: EcefCoordinate) -> f64 {
ecef_central_angle(a, b) * EARTH_MEAN_RADIUS_M
}
#[inline]
pub fn ecef_arc_metres_at_radius(
a: EcefCoordinate,
b: EcefCoordinate,
radius_metres: f64,
) -> f64 {
ecef_central_angle(a, b) * radius_metres
}
pub fn bright_space_distance(
a: EcefCoordinate,
b: EcefCoordinate,
) -> BrightSpaceDistance {
let chord_metres = ecef_chord_metres(a, b);
let angle = ecef_central_angle(a, b);
let mag_a = ecef_magnitude(a);
let mag_b = ecef_magnitude(b);
let avg_radius = (mag_a + mag_b) / 2.0;
BrightSpaceDistance {
chord_metres,
chord_bright_meters: chord_metres / BRIGHT_METER_M,
central_angle_radians: angle,
arc_mean_earth_radius_metres: angle * EARTH_MEAN_RADIUS_M,
arc_average_radius_metres: angle * avg_radius,
light_travel_seconds: chord_metres / SPEED_OF_LIGHT_M_PER_S,
}
}
pub fn surface_distance_metres(a: GeodeticCoordinate, b: GeodeticCoordinate) -> f64 {
let phi1 = a.latitude * DEG_TO_RAD;
let phi2 = b.latitude * DEG_TO_RAD;
let d_phi = (b.latitude - a.latitude) * DEG_TO_RAD;
let d_lambda = (b.longitude - a.longitude) * DEG_TO_RAD;
let sin_d_phi = (d_phi / 2.0).sin();
let sin_d_lambda = (d_lambda / 2.0).sin();
let h = sin_d_phi * sin_d_phi
+ phi1.cos() * phi2.cos() * sin_d_lambda * sin_d_lambda;
let c = 2.0 * h.sqrt().min(1.0).asin();
EARTH_MEAN_RADIUS_M * c
}
#[inline]
pub fn light_travel_time_seconds(a: EcefCoordinate, b: EcefCoordinate) -> f64 {
ecef_chord_metres(a, b) / SPEED_OF_LIGHT_M_PER_S
}
pub fn gps_distance(a: GeodeticCoordinate, b: GeodeticCoordinate) -> DistancePair {
let ecef_a = geodetic_to_ecef(a);
let ecef_b = geodetic_to_ecef(b);
let chord_metres = ecef_chord_metres(ecef_a, ecef_b);
let surface_metres = surface_distance_metres(a, b);
DistancePair {
chord_metres,
chord_bright_meters: chord_metres / BRIGHT_METER_M,
surface_metres,
surface_minus_chord_metres: surface_metres - chord_metres,
light_travel_seconds: chord_metres / SPEED_OF_LIGHT_M_PER_S,
}
}