use crate::{wrap_latitude, wrap_to_180};
use nalgebra::{Matrix3, Vector3};
use nav_types::{ECEF, WGS84};
use world_magnetic_model::GeomagneticField;
pub const RATE: f64 = 7.2921159e-5;
pub const RATE_VECTOR: Vector3<f64> = Vector3::new(0.0, 0.0, RATE);
pub const EQUATORIAL_RADIUS: f64 = 6378137.0; pub const POLAR_RADIUS: f64 = 6356752.31425; pub const MEAN_RADIUS: f64 = 6371000.0; pub const ECCENTRICITY: f64 = 0.0818191908425; pub const ECCENTRICITY_SQUARED: f64 = ECCENTRICITY * ECCENTRICITY;
pub const GE: f64 = 9.7803253359; pub const GP: f64 = 9.8321849378; pub const G0: f64 = 9.80665; pub const F: f64 = 1.0 / 298.257223563; pub const K: f64 = (POLAR_RADIUS * GP - EQUATORIAL_RADIUS * GE) / (EQUATORIAL_RADIUS * GE); pub const MAGNETIC_NORTH_LATITUDE: f64 = 80.8; pub const MAGNETIC_NORTH_LONGITUDE: f64 = -72.8; pub const MAGNETIC_REFERENCE_RADIUS: f64 = 6371200.0; pub const MAGNETIC_FIELD_STRENGTH: f64 = 3.12e-5; pub const METERS_TO_DEGREES: f64 = 1.0 / (60.0 * 1852.0);
pub const DEGREES_TO_METERS: f64 = 60.0 * 1852.0;
pub const SEA_LEVEL_PRESSURE: f64 = 101325.0;
pub const SEA_LEVEL_TEMPERATURE: f64 = 288.15;
pub const MOLAR_MASS_DRY_AIR: f64 = 0.0289644;
pub const UNIVERSAL_GAS_CONSTANT: f64 = 8.314462618;
pub const STANDARD_LAPSE_RATE: f64 = 0.0065;
pub fn barometric_altitude(pressure: &f64) -> f64 {
let exponent: f64 = -(UNIVERSAL_GAS_CONSTANT * STANDARD_LAPSE_RATE) / (G0 * MOLAR_MASS_DRY_AIR);
(SEA_LEVEL_PRESSURE / STANDARD_LAPSE_RATE)
* (pressure / SEA_LEVEL_PRESSURE - 1.0)
* exponent.exp()
}
pub fn relative_barometric_altitude(
pressure: f64,
initial_pressure: f64,
average_temperature: Option<f64>,
) -> f64 {
let average_temperature = average_temperature.unwrap_or(SEA_LEVEL_TEMPERATURE);
((UNIVERSAL_GAS_CONSTANT * average_temperature) / (G0 * MOLAR_MASS_DRY_AIR))
* (initial_pressure / pressure).ln()
}
pub fn expected_barometric_pressure(altitude: f64, sea_level_pressure: f64) -> f64 {
let exponent =
-(G0 * MOLAR_MASS_DRY_AIR * altitude) / (UNIVERSAL_GAS_CONSTANT * SEA_LEVEL_TEMPERATURE);
sea_level_pressure * exponent.exp()
}
pub fn vector_to_skew_symmetric(v: &Vector3<f64>) -> Matrix3<f64> {
let mut skew: Matrix3<f64> = Matrix3::zeros();
skew[(0, 1)] = -v[2];
skew[(0, 2)] = v[1];
skew[(1, 0)] = v[2];
skew[(1, 2)] = -v[0];
skew[(2, 0)] = -v[1];
skew[(2, 1)] = v[0];
skew
}
pub fn skew_symmetric_to_vector(skew: &Matrix3<f64>) -> Vector3<f64> {
let mut v: Vector3<f64> = Vector3::zeros();
v[0] = skew[(2, 1)];
v[1] = skew[(0, 2)];
v[2] = skew[(1, 0)];
v
}
pub fn eci_to_ecef(time: f64) -> Matrix3<f64> {
let mut rot: Matrix3<f64> = Matrix3::zeros();
rot[(0, 0)] = (RATE * time).cos();
rot[(0, 1)] = (RATE * time).sin();
rot[(1, 0)] = -(RATE * time).sin();
rot[(1, 1)] = (RATE * time).cos();
rot[(2, 2)] = 1.0;
rot
}
pub fn ecef_to_eci(time: f64) -> Matrix3<f64> {
eci_to_ecef(time).transpose()
}
pub fn ecef_to_lla(latitude: &f64, longitude: &f64) -> Matrix3<f64> {
let lat: f64 = (*latitude).to_radians();
let lon: f64 = (*longitude).to_radians();
let mut rot: Matrix3<f64> = Matrix3::zeros();
rot[(0, 0)] = -lon.sin() * lat.cos();
rot[(0, 1)] = -lon.sin() * lat.sin();
rot[(0, 2)] = lat.cos();
rot[(1, 0)] = -lon.sin();
rot[(1, 1)] = lon.cos();
rot[(2, 0)] = -lat.cos() * lon.cos();
rot[(2, 1)] = -lat.cos() * lon.sin();
rot[(2, 2)] = -lat.sin();
rot
}
pub fn lla_to_ecef(latitude: &f64, longitude: &f64) -> Matrix3<f64> {
ecef_to_lla(latitude, longitude).transpose()
}
pub fn meters_ned_to_dlat_dlon(lat_rad: f64, alt_m: f64, d_n: f64, d_e: f64) -> (f64, f64) {
let a = 6378137.0;
let e2 = 6.69437999014e-3;
let sin_lat = lat_rad.sin();
let rn = a / (1.0 - e2 * sin_lat * sin_lat).sqrt();
let re = rn * (1.0 - e2) / (1.0 - e2 * sin_lat * sin_lat);
let dlat = d_n / (rn + alt_m);
let dlon = d_e / ((re + alt_m) * lat_rad.cos().max(1e-6));
(dlat, dlon)
}
pub fn haversine_distance(
lat1_rad: f64,
lon1_rad: f64,
lat2_rad: f64,
lon2_rad: f64,
) -> f64 {
let dlat = lat2_rad - lat1_rad;
let dlon = lon2_rad - lon1_rad;
let a = (dlat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
const R: f64 = 6_371_000.0; R * c
}
pub fn principal_radii(latitude: &f64, altitude: &f64) -> (f64, f64, f64) {
let latitude_rad: f64 = (latitude).to_radians();
let sin_lat: f64 = latitude_rad.sin();
let sin_lat_sq: f64 = sin_lat * sin_lat;
let r_n: f64 = (EQUATORIAL_RADIUS * (1.0 - ECCENTRICITY_SQUARED))
/ (1.0 - ECCENTRICITY_SQUARED * sin_lat_sq).powf(3.0 / 2.0);
let r_e: f64 = EQUATORIAL_RADIUS / (1.0 - ECCENTRICITY_SQUARED * sin_lat_sq).sqrt();
let r_p: f64 = r_e * latitude_rad.cos() + altitude;
(r_n, r_e, r_p)
}
pub fn gravity(latitude: &f64, altitude: &f64) -> f64 {
let sin_lat: f64 = (latitude).to_radians().sin();
let g0: f64 = (GE * (1.0 + K * sin_lat * sin_lat))
/ (1.0 - ECCENTRICITY_SQUARED * sin_lat * sin_lat).sqrt();
g0 - 3.08e-6 * altitude
}
pub fn gravitation(latitude: &f64, longitude: &f64, altitude: &f64) -> Vector3<f64> {
let latitude = wrap_latitude(*latitude);
let longitude = wrap_to_180(*longitude);
let wgs84: WGS84<f64> = WGS84::from_degrees_and_meters(latitude, longitude, *altitude);
let ecef: ECEF<f64> = ECEF::from(wgs84);
let ecef_vec: Vector3<f64> = Vector3::new(ecef.x(), ecef.y(), ecef.z());
let omega_ie: Matrix3<f64> = vector_to_skew_symmetric(&RATE_VECTOR);
let rot: Matrix3<f64> = ecef_to_lla(&latitude, &longitude);
let gravity: Vector3<f64> = Vector3::new(0.0, 0.0, gravity(&latitude, altitude));
gravity + rot * omega_ie * omega_ie * ecef_vec
}
pub fn gravity_anomaly(
latitude: &f64,
altitude: &f64,
north_velocity: &f64,
east_velocity: &f64,
gravity_observed: &f64,
) -> f64 {
let normal_gravity: f64 = gravity(latitude, &0.0);
let eotvos_correction: f64 = eotvos(latitude, altitude, north_velocity, east_velocity);
*gravity_observed - normal_gravity - eotvos_correction
}
pub fn eotvos(latitude: &f64, altitude: &f64, north_velocity: &f64, east_velocity: &f64) -> f64 {
let (_, _, r_p) = principal_radii(latitude, altitude);
2.0 * RATE * *east_velocity * latitude.cos()
+ (north_velocity.powi(2) + east_velocity.powi(2)) / r_p
}
pub fn earth_rate_lla(latitude: &f64) -> Vector3<f64> {
let sin_lat: f64 = (latitude).to_radians().sin();
let cos_lat: f64 = (latitude).to_radians().cos();
let omega_ie: Vector3<f64> = Vector3::new(RATE * cos_lat, 0.0, -RATE * sin_lat);
omega_ie
}
pub fn transport_rate(latitude: &f64, altitude: &f64, velocities: &Vector3<f64>) -> Vector3<f64> {
let (r_n, r_e, _) = principal_radii(latitude, altitude);
let lat_rad = latitude.to_radians();
let omega_en_n: Vector3<f64> = Vector3::new(
-velocities[1] / (r_n + *altitude),
velocities[0] / (r_e + *altitude),
velocities[0] * lat_rad.tan() / (r_n + *altitude),
);
omega_en_n
}
pub fn calculate_magnetic_field(latitude: &f64, longitude: &f64, altitude: &f64) -> Vector3<f64> {
let (mag_colatitude, _) = wgs84_to_magnetic(latitude, longitude);
let radial_field = calculate_radial_magnetic_field(mag_colatitude.to_radians(), *altitude);
let lat_field = calculate_latitudinal_magnetic_field(mag_colatitude.to_radians(), *altitude);
let b_vector: Vector3<f64> = Vector3::new(radial_field, lat_field, 0.0);
b_vector
}
pub fn calculate_radial_magnetic_field(colatitude: f64, radius: f64) -> f64 {
-2.0 * MAGNETIC_FIELD_STRENGTH * (MEAN_RADIUS / radius).powi(3) * colatitude.cos()
}
pub fn calculate_latitudinal_magnetic_field(colatitude: f64, radius: f64) -> f64 {
-MAGNETIC_FIELD_STRENGTH * (MEAN_RADIUS / radius).powi(3) * colatitude.sin()
}
pub fn wgs84_to_magnetic(latitude: &f64, longitude: &f64) -> (f64, f64) {
let lat_rad = latitude.to_radians();
let lon_rad = longitude.to_radians();
let mag_lat_rad = MAGNETIC_NORTH_LATITUDE.to_radians();
let mag_lon_rad = MAGNETIC_NORTH_LONGITUDE.to_radians();
let cos_theta = lat_rad.sin() * mag_lat_rad.sin()
+ lat_rad.cos() * mag_lat_rad.cos() * (lon_rad - mag_lon_rad).cos();
let y = (lon_rad - mag_lon_rad).sin() * lat_rad.cos();
let x = mag_lat_rad.cos() * lat_rad.sin()
- mag_lat_rad.sin() * lat_rad.cos() * (lon_rad - mag_lon_rad).cos();
let mag_latitude = cos_theta.acos().to_degrees();
let mag_longitude = y.atan2(x).to_degrees();
(mag_latitude, mag_longitude)
}
pub fn magnetic_inclination(latitude: &f64, longitude: &f64, altitude: &f64) -> f64 {
let b_vector = calculate_magnetic_field(latitude, longitude, altitude);
let b_h = (b_vector[0].powi(2) + b_vector[1].powi(2)).sqrt();
(b_vector[2] / b_h).atan().to_degrees()
}
pub fn magnetic_declination(latitude: &f64, longitude: &f64, altitude: &f64) -> f64 {
let b_vector = calculate_magnetic_field(latitude, longitude, altitude);
(b_vector[1] / b_vector[0]).atan().to_degrees()
}
pub fn magnetic_anomaly(
magnetic_field: GeomagneticField,
mag_x: f64,
mag_y: f64,
mag_z: f64,
) -> f64 {
let obs = (mag_x.powi(2) + mag_y.powi(2) + mag_z.powi(2)).sqrt();
obs - (magnetic_field.f().value as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
const KM: f64 = 1000.0;
#[test]
fn vector_to_skew_symmetric() {
let v: Vector3<f64> = Vector3::new(1.0, 2.0, 3.0);
let skew: Matrix3<f64> = super::vector_to_skew_symmetric(&v);
assert_eq!(skew[(0, 1)], -v[2]);
assert_eq!(skew[(0, 2)], v[1]);
assert_eq!(skew[(1, 0)], v[2]);
assert_eq!(skew[(1, 2)], -v[0]);
assert_eq!(skew[(2, 0)], -v[1]);
assert_eq!(skew[(2, 1)], v[0]);
}
#[test]
fn skew_symmetric_to_vector() {
let v: Vector3<f64> = Vector3::new(1.0, 2.0, 3.0);
let skew: Matrix3<f64> = super::vector_to_skew_symmetric(&v);
let v2: Vector3<f64> = super::skew_symmetric_to_vector(&skew);
assert_eq!(v, v2);
}
#[test]
fn gravity() {
let latitude: f64 = 90.0;
let grav = super::gravity(&latitude, &0.0);
assert_approx_eq!(grav, GP);
let latitude: f64 = 0.0;
let grav = super::gravity(&latitude, &0.0);
assert_approx_eq!(grav, GE);
}
#[test]
fn gravitation() {
let latitude: f64 = 0.0;
let altitude: f64 = 0.0;
let grav: Vector3<f64> = super::gravitation(&latitude, &0.0, &altitude);
assert_approx_eq!(grav[0], 0.0);
assert_approx_eq!(grav[1], 0.0);
assert_approx_eq!(grav[2], (GE + 0.0339), 1e-4);
let latitude: f64 = 90.0;
let grav: Vector3<f64> = super::gravitation(&latitude, &0.0, &altitude);
assert_approx_eq!(grav[0], 0.0);
assert_approx_eq!(grav[1], 0.0);
assert_approx_eq!(grav[2], GP, 1e-2);
}
#[test]
fn magnetic_radial_field() {
let lat: f64 = 0.0;
let b_r: f64 = calculate_radial_magnetic_field(lat.to_radians(), MEAN_RADIUS);
assert_approx_eq!(b_r, -2.0 * MAGNETIC_FIELD_STRENGTH, 1e-7);
let lat: f64 = 180.0;
let b_r: f64 = calculate_radial_magnetic_field(lat.to_radians(), MEAN_RADIUS);
assert_approx_eq!(b_r, 2.0 * MAGNETIC_FIELD_STRENGTH, 1e-7);
let lat: f64 = 90.0;
let b_r: f64 = calculate_radial_magnetic_field(lat.to_radians(), MEAN_RADIUS);
assert_approx_eq!(b_r, 0.0, 1e-7);
}
#[test]
fn wgs84_to_magnetic() {
let lat: f64 = 80.8;
let lon: f64 = -72.8;
let (mag_lat, mag_lon) = super::wgs84_to_magnetic(&lat, &lon);
assert_approx_eq!(mag_lat, 0.0, 1e-7);
assert_approx_eq!(mag_lon, 0.0, 1e-7);
}
#[test]
fn eci_to_ecef() {
let time: f64 = 30.0;
let rot: Matrix3<f64> = super::eci_to_ecef(time);
assert_approx_eq!(rot[(0, 0)], (RATE * time).cos(), 1e-7);
assert_approx_eq!(rot[(0, 1)], (RATE * time).sin(), 1e-7);
assert_approx_eq!(rot[(1, 0)], -(RATE * time).sin(), 1e-7);
assert_approx_eq!(rot[(1, 1)], (RATE * time).cos(), 1e-7);
let rot_t = ecef_to_eci(time);
assert_approx_eq!(rot_t[(0, 0)], (RATE * time).cos(), 1e-7);
assert_approx_eq!(rot_t[(0, 1)], -(RATE * time).sin(), 1e-7);
assert_approx_eq!(rot_t[(1, 0)], (RATE * time).sin(), 1e-7);
assert_approx_eq!(rot_t[(1, 1)], (RATE * time).cos(), 1e-7);
assert_approx_eq!(rot_t[(2, 2)], 1.0, 1e-7);
}
#[test]
fn ecef_to_lla() {
let latitude: f64 = 45.0;
let longitude: f64 = 90.0;
let rot: Matrix3<f64> = super::ecef_to_lla(&latitude, &longitude);
assert_approx_eq!(
rot[(0, 0)],
-longitude.to_radians().sin() * latitude.to_radians().cos(),
1e-7
);
assert_approx_eq!(
rot[(0, 1)],
-longitude.to_radians().sin() * latitude.to_radians().sin(),
1e-7
);
assert_approx_eq!(rot[(0, 2)], latitude.to_radians().cos(), 1e-7);
assert_approx_eq!(rot[(1, 0)], -longitude.to_radians().sin(), 1e-7);
assert_approx_eq!(rot[(1, 1)], longitude.to_radians().cos(), 1e-7);
assert_approx_eq!(
rot[(2, 0)],
-latitude.to_radians().cos() * longitude.to_radians().cos(),
1e-7
);
assert_approx_eq!(
rot[(2, 1)],
-latitude.to_radians().cos() * longitude.to_radians().sin(),
1e-7
);
assert_approx_eq!(rot[(2, 2)], -latitude.to_radians().sin(), 1e-7);
}
#[test]
fn lla_to_ecef() {
let latitude: f64 = 45.0;
let longitude: f64 = 90.0;
let rot1: Matrix3<f64> = super::lla_to_ecef(&latitude, &longitude);
let rot2: Matrix3<f64> = super::ecef_to_lla(&latitude, &longitude).transpose();
assert_approx_eq!(rot1[(0, 0)], rot2[(0, 0)], 1e-7);
assert_approx_eq!(rot1[(0, 1)], rot2[(0, 1)], 1e-7);
assert_approx_eq!(rot1[(0, 2)], rot2[(0, 2)], 1e-7);
assert_approx_eq!(rot1[(1, 0)], rot2[(1, 0)], 1e-7);
assert_approx_eq!(rot1[(1, 1)], rot2[(1, 1)], 1e-7);
assert_approx_eq!(rot1[(1, 2)], rot2[(1, 2)], 1e-7);
assert_approx_eq!(rot1[(2, 0)], rot2[(2, 0)], 1e-7);
assert_approx_eq!(rot1[(2, 1)], rot2[(2, 1)], 1e-7);
assert_approx_eq!(rot1[(2, 2)], rot2[(2, 2)], 1e-7);
}
#[test]
fn test_meters_ned_to_dlat_dlon() {
let lat_rad = 0.0; let alt_m = 0.0;
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 1852.0, 0.0);
assert_approx_eq!(dlat.abs(), (1.0 / 60.0 as f64).to_radians(), 0.0001); assert_approx_eq!(dlon.abs(), 0.0, 0.0001);
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 0.0, 1852.0);
assert!(dlat.abs() < 0.0001); assert_approx_eq!(dlon.abs(), (1.0 / 60.0 as f64).to_radians(), 0.0001);
let lat_rad = 45.0_f64.to_radians();
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 0.0, 111.320);
assert_approx_eq!(dlat.abs(), 0.0, 0.0001); assert_approx_eq!(dlon.abs(), 0.0, 0.0001); }
#[test]
fn zero_distance() {
let lat = 40.0_f64.to_radians();
let lon = -75.0_f64.to_radians();
let d = haversine_distance(lat, lon, lat, lon);
assert!(d.abs() < 1e-9, "Zero distance should be ~0, got {}", d);
}
#[test]
fn quarter_circumference_equator() {
let d = haversine_distance(0.0, 0.0, 0.0, 90_f64.to_radians());
let expected = std::f64::consts::FRAC_PI_2 * 6_371_000.0; let err = (d - expected).abs();
assert!(err < 1e-6 * expected, "Error too large: got {}, expected {}", d, expected);
}
#[test]
fn antipodal_points() {
let d = haversine_distance(0.0, 0.0, 0.0, 180_f64.to_radians());
let expected = std::f64::consts::PI * 6_371_000.0; let err = (d - expected).abs();
assert!(err < 1e-6 * expected, "Error too large: got {}, expected {}", d, expected);
}
#[test]
fn known_baseline_nyc_to_london() {
let nyc_lat = 40.7128_f64.to_radians();
let nyc_lon = -74.0060_f64.to_radians();
let lon_lat = 51.5074_f64.to_radians();
let lon_lon = -0.1278_f64.to_radians();
let d = haversine_distance(nyc_lat, nyc_lon, lon_lat, lon_lon);
assert!((d / KM - 5567.0).abs() < 50.0, "NYC-LON distance off: {} km", d / KM);
}
}