const CPR_DENOM: f64 = 131_072.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CprFormat {
Even,
Odd,
}
impl CprFormat {
pub(crate) const fn from_raw(v: u8) -> Self {
if v == 0 { Self::Even } else { Self::Odd }
}
pub(crate) const fn to_raw(self) -> u8 {
match self {
Self::Even => 0,
Self::Odd => 1,
}
}
}
fn floor(x: f64) -> f64 {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
reason = "CPR intermediate values are always tiny (well under i64::MAX \
or f64's 52-bit mantissa range)"
)]
let truncated = x as i64 as f64;
if x < truncated {
truncated - 1.0
} else {
truncated
}
}
fn rem_euclid(x: f64, y: f64) -> f64 {
let r = x % y;
if r < 0.0 { r + y } else { r }
}
#[rustfmt::skip]
#[allow(clippy::unreadable_literal, reason = "copied verbatim from a verified source, not hand-written")]
const NL_BOUNDARIES: [f64; 58] = [
10.47047129996848, 14.828174368686794, 18.186263570713354, 21.029394926028463, 23.545044865570706, 25.829247070587755, 27.938987101219045, 29.911356857318083, 31.77209707681077, 33.53993436298484, 35.22899597796385, 36.85025107593526, 38.41241892412256, 39.922566843338615, 41.38651832260239, 42.80914012243555, 44.194549514192744, 45.546267226602346, 46.867332524987454, 48.160391280966216, 49.42776439255687, 50.67150165553835, 51.893424691687684, 53.09516152796003, 54.278174722729, 55.44378444495043, 56.59318756205918, 57.72747353866114, 58.84763776148457, 59.954592766940294, 61.04917774246351, 62.13216659210329, 63.20427479381928, 64.2661652256744, 65.31845309682089, 66.36171008382617, 67.39646774084667, 68.4232202208333, 69.44242631144024, 70.454510749876, 71.45986473028982, 72.45884544728945, 73.45177441667865, 74.43893415725137, 75.42056256653356, 76.39684390794469, 77.36789461328188, 78.33374082922747, 79.29428225456925, 80.24923213280512, 81.19801349271948, 82.13956980510606, 83.07199444719814, 83.99173562980565, 84.89166190702085, 85.75541620944418, 86.535369975121, 87.0, ];
#[must_use]
pub fn nl(lat: f64) -> u8 {
let abs_lat = lat.abs();
if abs_lat > 87.0 {
return 1;
}
#[allow(clippy::float_cmp, reason = "intentional exact match, see above")]
if abs_lat == 87.0 {
return 2;
}
#[allow(
clippy::cast_possible_truncation,
reason = "idx is at most 58 (NL_BOUNDARIES.len()), fits u8"
)]
let idx = NL_BOUNDARIES.partition_point(|&boundary| boundary <= abs_lat) as u8;
59 - idx
}
#[must_use]
pub fn global_decode(
lat_cpr_even_raw: u32,
lon_cpr_even_raw: u32,
lat_cpr_odd_raw: u32,
lon_cpr_odd_raw: u32,
even_is_newer: bool,
) -> Option<(f64, f64)> {
let cpr_lat_even = f64::from(lat_cpr_even_raw) / CPR_DENOM;
let cpr_lon_even = f64::from(lon_cpr_even_raw) / CPR_DENOM;
let cpr_lat_odd = f64::from(lat_cpr_odd_raw) / CPR_DENOM;
let cpr_lon_odd = f64::from(lon_cpr_odd_raw) / CPR_DENOM;
let j = floor(59.0 * cpr_lat_even - 60.0 * cpr_lat_odd + 0.5);
let mut lat_even = (360.0 / 60.0) * (rem_euclid(j, 60.0) + cpr_lat_even);
let mut lat_odd = (360.0 / 59.0) * (rem_euclid(j, 59.0) + cpr_lat_odd);
if lat_even >= 270.0 {
lat_even -= 360.0;
}
if lat_odd >= 270.0 {
lat_odd -= 360.0;
}
if nl(lat_even) != nl(lat_odd) {
return None;
}
let lat = if even_is_newer { lat_even } else { lat_odd };
let nl_here = f64::from(nl(lat));
let m = floor(cpr_lon_even * (nl_here - 1.0) - cpr_lon_odd * nl_here + 0.5);
let (ni, cpr_lon_selected) = if even_is_newer {
(nl_here.max(1.0), cpr_lon_even)
} else {
((nl_here - 1.0).max(1.0), cpr_lon_odd)
};
let mut lon = (360.0 / ni) * (rem_euclid(m, ni) + cpr_lon_selected);
if lon > 180.0 {
lon -= 360.0;
}
if lat.abs() > 90.0 || lon.abs() > 180.0 {
return None;
}
Some((lat, lon))
}
#[must_use]
#[allow(
clippy::similar_names,
reason = "north/south suffixes are the clearest naming here"
)]
pub fn surface_global_decode(
lat_cpr_even_raw: u32,
lon_cpr_even_raw: u32,
lat_cpr_odd_raw: u32,
lon_cpr_odd_raw: u32,
lat_ref: f64,
lon_ref: f64,
even_is_newer: bool,
) -> Option<(f64, f64)> {
let cpr_lat_even = f64::from(lat_cpr_even_raw) / CPR_DENOM;
let cpr_lon_even = f64::from(lon_cpr_even_raw) / CPR_DENOM;
let cpr_lat_odd = f64::from(lat_cpr_odd_raw) / CPR_DENOM;
let cpr_lon_odd = f64::from(lon_cpr_odd_raw) / CPR_DENOM;
let j = floor(59.0 * cpr_lat_even - 60.0 * cpr_lat_odd + 0.5);
let lat_even_n = (90.0 / 60.0) * (rem_euclid(j, 60.0) + cpr_lat_even);
let lat_odd_n = (90.0 / 59.0) * (rem_euclid(j, 59.0) + cpr_lat_odd);
let lat_even_s = lat_even_n - 90.0;
let lat_odd_s = lat_odd_n - 90.0;
let newer_n = if even_is_newer { lat_even_n } else { lat_odd_n };
let newer_s = if even_is_newer { lat_even_s } else { lat_odd_s };
let (lat_even, lat_odd) = if (lat_ref - newer_n).abs() <= (lat_ref - newer_s).abs() {
(lat_even_n, lat_odd_n)
} else {
(lat_even_s, lat_odd_s)
};
if nl(lat_even) != nl(lat_odd) {
return None;
}
let lat = if even_is_newer { lat_even } else { lat_odd };
let nl_here = f64::from(nl(lat));
let m = floor(cpr_lon_even * (nl_here - 1.0) - cpr_lon_odd * nl_here + 0.5);
let (ni, cpr_lon_selected) = if even_is_newer {
(nl_here.max(1.0), cpr_lon_even)
} else {
((nl_here - 1.0).max(1.0), cpr_lon_odd)
};
let lon_base = (90.0 / ni) * (rem_euclid(m, ni) + cpr_lon_selected);
let mut best_lon = lon_base;
let mut best_dist = f64::INFINITY;
for q in [0.0, 90.0, 180.0, 270.0] {
let candidate = rem_euclid(lon_base + q + 180.0, 360.0) - 180.0;
let dist = (rem_euclid(candidate - lon_ref + 180.0, 360.0) - 180.0).abs();
if dist < best_dist {
best_dist = dist;
best_lon = candidate;
}
}
Some((lat, best_lon))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nl_matches_known_reference_values() {
assert_eq!(nl(0.0), 59);
assert_eq!(nl(87.0), 2);
assert_eq!(nl(87.1), 1);
assert_eq!(nl(-87.0), 2);
assert_eq!(nl(10.0), 59);
assert_eq!(nl(10.5), 58);
}
#[test]
fn floor_matches_std_for_representative_values() {
assert!((floor(2.3) - 2.0).abs() < 1e-9);
assert!((floor(-2.3) - -3.0).abs() < 1e-9);
assert!((floor(-2.0) - -2.0).abs() < 1e-9);
assert!((floor(0.0) - 0.0).abs() < 1e-9);
}
#[test]
fn rem_euclid_is_always_nonnegative() {
assert!((rem_euclid(-1.0, 60.0) - 59.0).abs() < 1e-9);
assert!((rem_euclid(59.0, 60.0) - 59.0).abs() < 1e-9);
assert!((rem_euclid(61.0, 60.0) - 1.0).abs() < 1e-9);
}
const EVEN_LAT_CPR: u32 = 39848;
const EVEN_LON_CPR: u32 = 83951;
const ODD_LAT_CPR: u32 = 21567;
const ODD_LON_CPR: u32 = 81965;
#[test]
fn decodes_a_real_captured_position_pair_odd_newer() {
let (lat, lon) =
global_decode(EVEN_LAT_CPR, EVEN_LON_CPR, ODD_LAT_CPR, ODD_LON_CPR, false).unwrap();
assert!((lat - 49.817_55).abs() < 1e-3);
assert!((lon - 6.084_42).abs() < 1e-3);
}
#[test]
fn decodes_a_real_captured_position_pair_even_newer() {
let (lat, lon) =
global_decode(EVEN_LAT_CPR, EVEN_LON_CPR, ODD_LAT_CPR, ODD_LON_CPR, true).unwrap();
assert!((lat - 49.824_10).abs() < 1e-3);
assert!((lon - 6.067_85).abs() < 1e-3);
}
#[test]
fn rejects_mismatched_zones() {
assert_eq!(global_decode(8192, 0, 114_688, 0, true), None);
}
#[test]
fn rejects_impossible_latitude() {
assert_eq!(global_decode(113_939, 119_217, 72_761, 59_498, false), None);
}
#[test]
fn surface_decode_resolves_a_real_lfbo_taxiway_pair() {
let (lat, lon) =
surface_global_decode(11052, 86083, 78587, 84090, 43.63, 1.37, false).unwrap();
assert!((lat - 43.626_46).abs() < 1e-3);
assert!((lon - 1.374_76).abs() < 1e-3);
}
}