#![allow(clippy::all)]
#![allow(clippy::needless_return)]
use std::f64::consts::PI;
use std::sync::OnceLock;
use crate::egm96_data::EGM96_DATA;
const NMAX: usize = 360;
const NMAX1: usize = 361;
const N361: usize = 361;
const COEFFS: usize = 65341;
#[cfg(feature = "raster_15_min")]
const EGM96_15_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/egm96-15.png"));
#[cfg(feature = "raster_5_min")]
const EGM96_5_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/egm96-5.png"));
fn dscml(rlon: f64, sinml: &mut [f64; N361 + 1], cosml: &mut [f64; N361 + 1]) {
let a = rlon.sin();
let b = rlon.cos();
sinml[1] = a;
cosml[1] = b;
sinml[2] = 2.0 * b * a;
cosml[2] = 2.0 * b * b - 1.0;
for m in 3..=NMAX {
sinml[m] = 2.0 * b * sinml[m - 1] - sinml[m - 2];
cosml[m] = 2.0 * b * cosml[m - 1] - cosml[m - 2];
}
}
fn hundu(
p: &[f64; COEFFS + 1],
sinml: &[f64; N361 + 1],
cosml: &[f64; N361 + 1],
gr: f64,
re: f64,
) -> f64 {
const GM: f64 = 0.3986004418e15;
const AE: f64 = 6378137.0;
let ar = AE / re;
let mut arn = ar;
let mut ac = 0.0;
let mut a = 0.0;
let mut k = 3;
for n in 2..=NMAX {
arn *= ar;
k += 1;
let mut sum = p[k] * EGM96_DATA[k][2] as f64;
let mut sumc = p[k] * EGM96_DATA[k][0] as f64;
for m in 1..=n {
k += 1;
let tempc = EGM96_DATA[k][0] as f64 * cosml[m] + EGM96_DATA[k][1] as f64 * sinml[m];
let temp = EGM96_DATA[k][2] as f64 * cosml[m] + EGM96_DATA[k][3] as f64 * sinml[m];
sumc += p[k] * tempc;
sum += p[k] * temp;
}
ac += sumc;
a += sum * arn;
}
ac += EGM96_DATA[1][0] as f64
+ (p[2] * EGM96_DATA[2][0] as f64)
+ (p[3] * (EGM96_DATA[3][0] as f64 * cosml[1] + EGM96_DATA[3][1] as f64 * sinml[1]));
((a * GM) / (gr * re)) + (ac / 100.0) - 0.53
}
fn radgra(lat: f64, lon: f64, rlat: &mut f64, gr: &mut f64, re: &mut f64) {
const A: f64 = 6378137.0;
const E2: f64 = 0.00669437999013;
const GEQT: f64 = 9.7803253359;
const K: f64 = 0.00193185265246;
let t1 = lat.sin().powi(2);
let n = A / (1.0 - (E2 * t1)).sqrt();
let t2 = n * lat.cos();
let x = t2 * lon.cos();
let y = t2 * lon.sin();
let z = (n * (1.0 - E2)) * lat.sin();
*re = (x * x + y * y + z * z).sqrt(); *rlat = (z / (x * x + y * y).sqrt()).atan(); *gr = GEQT * (1.0 + (K * t1)) / (1.0 - (E2 * t1)).sqrt(); }
fn undulation(lat: f64, lon: f64) -> f64 {
static DRTS_DIRT: OnceLock<([f64; 1301], [f64; 1301])> = OnceLock::new();
let (drts, dirt) = DRTS_DIRT.get_or_init(|| {
let nmax2p = (2 * NMAX) + 1;
let mut drts = [0.0; 1301];
let mut dirt = [0.0; 1301];
for n in 1..=nmax2p {
drts[n] = (n as f64).sqrt();
dirt[n] = 1.0 / drts[n];
}
return (drts, dirt);
});
let mut p = [0.0; COEFFS + 1];
let mut sinml = [0.0; N361 + 1];
let mut cosml = [0.0; N361 + 1];
let mut rleg = [0.0; N361 + 1];
let mut rlnn = [0.0; N361 + 1];
let mut rlat = 0.0;
let mut gr = 0.0;
let mut re = 0.0;
radgra(lat, lon, &mut rlat, &mut gr, &mut re);
rlat = (PI / 2.0) - rlat;
let cothet = rlat.cos();
let sithet = rlat.sin();
rlnn[1] = 1.0;
rlnn[2] = sithet * drts[3];
for j in 1..=NMAX1 {
let m = j - 1;
let m1 = m + 1;
for n1 in 3..=m1 {
let n = n1 - 1;
let n2 = 2 * n;
rlnn[n1] = drts[n2 + 1] * dirt[n2] * sithet * rlnn[n];
}
}
for j in 1..=NMAX1 {
let m = j - 1;
let m1 = m + 1;
let m2 = m + 2;
let m3 = m + 3;
if m == 0 {
rleg[1] = 1.0;
rleg[2] = cothet * drts[3];
} else if m == 1 {
rleg[2] = rlnn[2];
rleg[3] = drts[5] * cothet * rleg[2];
}
rleg[m1] = rlnn[m1];
if m2 <= NMAX1 {
rleg[m2] = drts[m1 * 2 + 1] * cothet * rleg[m1];
for n1 in m3..=NMAX1 {
let n = n1 - 1;
if (!m == 0 && n < 2) || (m == 1 && n < 3) {
continue;
}
let n2 = 2 * n;
rleg[n1] = drts[n2 + 1]
* dirt[n + m]
* dirt[n - m]
* (drts[n2 - 1] * cothet * rleg[n1 - 1]
- drts[n + m - 1] * drts[n - m - 1] * dirt[n2 - 3] * rleg[n1 - 2]);
}
}
for i in j..=NMAX1 {
p[((i - 1) * i) / 2 + m + 1] = rleg[i];
}
}
dscml(lon, &mut sinml, &mut cosml);
hundu(&p, &sinml, &cosml, gr, re)
}
fn wrap_degrees(mut degrees: f64) -> f64 {
degrees += 180.0;
degrees = degrees.rem_euclid(360.0);
degrees - 180.0
}
pub fn egm96_compute_altitude_offset(lat: f64, lon: f64) -> f64 {
let lon = wrap_degrees(lon);
let lat = lat.clamp(-90.0, 90.0);
undulation(lat.to_radians(), lon.to_radians())
}
#[allow(unused)]
fn interpolate<const WIDTH: usize, const HEIGHT: usize>(
lat: f64,
lon: f64,
x_start: f64,
y_start: f64,
x_step: f64,
y_step: f64,
pixels: &[u16],
) -> f64 {
const SCALE: f64 = 0.003;
const OFFSET: f64 = -108.0;
let x = (lon - x_start) / x_step;
let y = (lat - y_start) / y_step;
let x0 = x.floor() as isize;
let y0 = y.floor() as isize;
let x1 = x0 + 1;
let y1 = y0 + 1;
let x0_clamped = x0.clamp(0, (WIDTH - 1) as isize) as usize;
let y0_clamped = y0.clamp(0, (HEIGHT - 1) as isize) as usize;
let x1_clamped = x1.clamp(0, (WIDTH - 1) as isize) as usize;
let y1_clamped = y1.clamp(0, (HEIGHT - 1) as isize) as usize;
let dx = (x - x0 as f64).clamp(0.0, 1.0);
let dy = (y - y0 as f64).clamp(0.0, 1.0);
let top_left = pixels[y0_clamped * WIDTH + x0_clamped] as f64 * SCALE + OFFSET;
let top_right = pixels[y0_clamped * WIDTH + x1_clamped] as f64 * SCALE + OFFSET;
let bottom_left = pixels[y1_clamped * WIDTH + x0_clamped] as f64 * SCALE + OFFSET;
let bottom_right = pixels[y1_clamped * WIDTH + x1_clamped] as f64 * SCALE + OFFSET;
let top = top_left + dx * (top_right - top_left);
let bottom = bottom_left + dx * (bottom_right - bottom_left);
return top + dy * (bottom - top);
}
fn load_image<const WIDTH: usize, const HEIGHT: usize>(bytes: &[u8]) -> Vec<u16> {
let decoder = png::Decoder::new(bytes);
let mut reader = decoder.read_info().expect("Failed to check info");
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).expect("Failed to get frame");
buf.truncate(info.buffer_size());
assert!(buf.len() == WIDTH * HEIGHT * 2);
let mut out = vec![0; WIDTH * HEIGHT];
for row in 0..HEIGHT {
for col in 0..WIDTH {
let index = 2 * (col + row * WIDTH);
out[row * WIDTH + col] =
u16::from_be_bytes(buf[index..(index + 2)].try_into().expect("pair"));
}
}
return out;
}
#[cfg(feature = "raster_5_min")]
pub fn egm96_raster_5_min_altitude_offset(lat: f64, lon: f64) -> f64 {
use std::sync::OnceLock;
const WIDTH: usize = 4320;
const HEIGHT: usize = 2161;
static IMAGE: OnceLock<Vec<u16>> = OnceLock::new();
let image = IMAGE.get_or_init(|| load_image::<WIDTH, HEIGHT>(EGM96_5_BYTES));
let mut lon = wrap_degrees(lon);
if lon < 0.0 {
lon += 360.0;
}
let lat = lat.clamp(-90.0, 90.0);
return interpolate::<WIDTH, HEIGHT>(
lat,
lon,
-0.04166666666666666,
90.04166666666666666,
0.08333333333333333,
-0.08333333333333333,
&image,
);
}
#[cfg(feature = "raster_15_min")]
pub fn egm96_raster_15_min_altitude_offset(lat: f64, lon: f64) -> f64 {
use std::sync::OnceLock;
const WIDTH: usize = 1440;
const HEIGHT: usize = 721;
static IMAGE: OnceLock<Vec<u16>> = OnceLock::new();
let image = IMAGE.get_or_init(|| load_image::<WIDTH, HEIGHT>(EGM96_15_BYTES));
let mut lon = wrap_degrees(lon);
if lon < 0.0 {
lon += 360.0;
}
let lat = lat.clamp(-90.0, 90.0);
return interpolate::<WIDTH, HEIGHT>(lat, lon, -0.125, 90.125, 0.25, -0.25, &image);
}
pub fn egm96_altitude_offset(lat: f64, lon: f64) -> f64 {
#[cfg(feature = "raster_5_min")]
{
egm96_raster_5_min_altitude_offset(lat, lon)
}
#[cfg(all(feature = "raster_15_min", not(feature = "raster_5_min")))]
{
egm96_raster_15_min_altitude_offset(lat, lon)
}
#[cfg(all(not(feature = "raster_15_min"), not(feature = "raster_5_min")))]
{
egm96_compute_altitude_offset(lat, lon)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::FRAC_PI_2;
#[test]
fn test_radgra_at_equator() {
let lat = 0.0;
let lon = 0.0;
let mut rlat = 0.0;
let mut gr = 0.0;
let mut re = 0.0;
radgra(lat, lon, &mut rlat, &mut gr, &mut re);
assert!((rlat).abs() < 1e-9); assert!((re - 6378137.0).abs() < 1e-9); assert!((gr - 9.7803253359).abs() < 1e-9); }
#[test]
fn test_radgra_at_pole() {
let lat = FRAC_PI_2;
let lon = 0.0;
let mut rlat = 0.0;
let mut gr = 0.0;
let mut re = 0.0;
radgra(lat, lon, &mut rlat, &mut gr, &mut re);
assert!((rlat - FRAC_PI_2).abs() < 1e-9); assert!((re - 6356752.314245).abs() < 1e-6); assert!((gr - 9.8321863685).abs() < 1e-5); }
#[test]
fn test_undulation_at_locations() {
struct Check {
lat: f64,
lon: f64,
geoid: f64,
}
let checks = [
Check {
lat: 29.7604,
lon: -95.3698,
geoid: -28.41,
},
Check {
lat: 29.4241,
lon: -98.4936,
geoid: -26.52,
},
Check {
lat: 32.7157,
lon: -117.1611,
geoid: -35.22,
},
Check {
lat: 32.7767,
lon: -96.797,
geoid: -27.34,
},
Check {
lat: 37.3382,
lon: -121.8863,
geoid: -32.37,
},
Check {
lat: 34.0522,
lon: -118.2437,
geoid: -35.17,
},
Check {
lat: 40.7128,
lon: -74.006,
geoid: -32.73,
},
Check {
lat: 37.7749,
lon: -122.4194,
geoid: -32.17,
},
Check {
lat: 41.8781,
lon: -87.6298,
geoid: -33.93,
},
Check {
lat: 51.5074,
lon: 0.1278,
geoid: 45.78,
},
Check {
lat: 48.8566,
lon: 2.3522,
geoid: 44.61,
},
Check {
lat: 35.6895,
lon: 139.6917,
geoid: 36.71,
},
Check {
lat: 40.05,
lon: -75.45,
geoid: -34.32,
},
Check {
lat: 33.4484,
lon: -112.074,
geoid: -30.25,
},
Check {
lat: 0.0,
lon: 0.0,
geoid: 17.22,
},
];
for check in checks {
let computed = egm96_compute_altitude_offset(check.lat, check.lon);
let expected = check.geoid;
let err = (computed - expected).abs();
if err.is_nan() || err.is_infinite() || err > 0.5 {
panic!(
"Lat: {}, Lon: {}, Expected: {expected}, Computed: {computed}",
check.lat, check.lon
);
}
}
}
#[test]
fn test_wrap_degrees() {
assert_eq!(wrap_degrees(0.0), 0.0);
assert_eq!(wrap_degrees(179.8), 179.8);
assert_eq!(wrap_degrees(-179.0), -179.0);
assert_eq!(wrap_degrees(-181.0), 179.0);
assert_eq!(wrap_degrees(190.0), -170.0);
assert_eq!(wrap_degrees(-190.0), 170.0);
assert_eq!(wrap_degrees(-190.0 - 360.0), 170.0);
assert_eq!(wrap_degrees(360.0), 0.0);
assert_eq!(wrap_degrees(540.0), -180.0);
assert_eq!(wrap_degrees(-540.0), -180.0);
assert_eq!(wrap_degrees(1000.0), -80.0);
assert_eq!(wrap_degrees(-1000.0), 80.0);
}
#[cfg(feature = "raster_5_min")]
#[test]
fn test_5min_at_locations() {
let _ = env_logger::builder().is_test(true).try_init();
struct Check {
lat: f64,
lon: f64,
geoid: f64,
}
let checks = [
Check {
lat: 29.7604,
lon: -95.3698,
geoid: -28.41,
},
Check {
lat: 29.4241,
lon: -98.4936,
geoid: -26.52,
},
Check {
lat: 32.7157,
lon: -117.1611,
geoid: -35.22,
},
Check {
lat: 32.7767,
lon: -96.797,
geoid: -27.34,
},
Check {
lat: 37.3382,
lon: -121.8863,
geoid: -32.37,
},
Check {
lat: 34.0522,
lon: -118.2437,
geoid: -35.17,
},
Check {
lat: 40.7128,
lon: -74.006,
geoid: -32.73,
},
Check {
lat: 37.7749,
lon: -122.4194,
geoid: -32.17,
},
Check {
lat: 41.8781,
lon: -87.6298,
geoid: -33.93,
},
Check {
lat: 51.5074,
lon: 0.1278,
geoid: 45.78,
},
Check {
lat: 48.8566,
lon: 2.3522,
geoid: 44.61,
},
Check {
lat: 35.6895,
lon: 139.6917,
geoid: 36.71,
},
Check {
lat: 40.05,
lon: -75.45,
geoid: -34.32,
},
Check {
lat: 33.4484,
lon: -112.074,
geoid: -30.25,
},
Check {
lat: 0.0,
lon: 0.0,
geoid: 17.22,
},
];
for check in checks {
let computed = egm96_raster_5_min_altitude_offset(check.lat, check.lon);
let expected = check.geoid;
let err = (computed - expected).abs();
if err.is_nan() || err.is_infinite() || err > 0.5 {
panic!(
"Lat: {}, Lon: {}, Expected: {expected}, Computed: {computed}",
check.lat, check.lon
);
}
}
}
#[test]
#[cfg(feature = "raster_15_min")]
fn test_15min_at_locations() {
let _ = env_logger::builder().is_test(true).try_init();
struct Check {
lat: f64,
lon: f64,
geoid: f64,
}
let checks = [
Check {
lat: 29.7604,
lon: -95.3698,
geoid: -28.41,
},
Check {
lat: 29.4241,
lon: -98.4936,
geoid: -26.52,
},
Check {
lat: 32.7157,
lon: -117.1611,
geoid: -35.22,
},
Check {
lat: 32.7767,
lon: -96.797,
geoid: -27.34,
},
Check {
lat: 37.3382,
lon: -121.8863,
geoid: -32.37,
},
Check {
lat: 34.0522,
lon: -118.2437,
geoid: -35.17,
},
Check {
lat: 40.7128,
lon: -74.006,
geoid: -32.73,
},
Check {
lat: 37.7749,
lon: -122.4194,
geoid: -32.17,
},
Check {
lat: 41.8781,
lon: -87.6298,
geoid: -33.93,
},
Check {
lat: 51.5074,
lon: 0.1278,
geoid: 45.78,
},
Check {
lat: 48.8566,
lon: 2.3522,
geoid: 44.61,
},
Check {
lat: 35.355,
lon: 139.895,
geoid: 47303.0 * 0.003 - 108.0,
},
Check {
lat: 40.05,
lon: -75.45,
geoid: -34.32,
},
Check {
lat: 33.4484,
lon: -112.074,
geoid: -30.25,
},
Check {
lat: 0.0,
lon: 0.0,
geoid: 17.22,
},
];
for (i, check) in checks.iter().enumerate() {
let computed = egm96_raster_15_min_altitude_offset(check.lat, check.lon);
let expected = check.geoid;
let err = (computed - expected).abs();
if err.is_nan() || err.is_infinite() || err > 0.5 {
panic!(
"{i}, Lat: {}, Lon: {}, Expected: {expected}, Computed: {computed}",
check.lat, check.lon
);
}
}
}
const WIDTH: usize = 3;
const HEIGHT: usize = 3;
const PIXELS: [u16; WIDTH * HEIGHT] = [100, 110, 120, 130, 140, 150, 160, 170, 180];
const X_START: f64 = 0.0;
const Y_START: f64 = 0.0;
const X_STEP: f64 = 1.0;
const Y_STEP: f64 = 1.0;
fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
(a - b).abs() < epsilon
}
#[test]
fn test_exact_top_left() {
let lat = 0.0;
let lon = 0.0;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = 100.0 * 0.003 - 108.0;
assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
#[test]
fn test_exact_bottom_right() {
let lat = 2.0;
let lon = 2.0;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = 180.0 * 0.003 - 108.0;
assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
#[test]
fn test_center_interpolation() {
let lat = 0.5;
let lon = 0.5;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = -107.64; assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
#[test]
fn test_right_edge_clamping() {
let lat = 0.0;
let lon = 2.0;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = 120.0 * 0.003 - 108.0;
assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
#[test]
fn test_negative_coordinates_clamping() {
let lat = -0.5;
let lon = -0.5;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = 100.0 * 0.003 - 108.0;
assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
#[test]
fn test_bottom_edge_interpolation() {
let lat = 2.0;
let lon = 1.5;
let result =
interpolate::<WIDTH, HEIGHT>(lat, lon, X_START, Y_START, X_STEP, Y_STEP, &PIXELS);
let expected = -107.475;
assert!(
approx_eq(result, expected, 1e-6),
"Expected {}, got {}",
expected,
result
);
}
}