use crate::geodetic::Geodetic;
use crate::math::wrap_pi;
use crate::units::Radians;
const SCALE: f32 = 0.01;
const RESOLUTION: f32 = 10.0;
#[rustfmt::skip]
const TABLE: [[i16; 37]; 19] = [
[
14809, 13809, 12809, 11809, 10809, 9809, 8809, 7809, 6809, 5809, 4809, 3809, 2809, 1809,
809, -191, -1191, -2191, -3191, -4191, -5191, -6191, -7191, -8191, -9191, -10191, -11191,
-12191, -13191, -14191, -15191, -16191, -17191, 17809, 16809, 15809, 14809
],
[
12827, 11615, 10511, 9500, 8564, 7685, 6849, 6042, 5256, 4484, 3723, 2970, 2222, 1476, 727,
-28, -798, -1586, -2397, -3232, -4093, -4978, -5886, -6818, -7777, -8766, -9795, -10874,
-12017, -13238, -14548, -15948, -17421, 17070, 15577, 14151, 12827
],
[
8590, 7787, 7135, 6576, 6070, 5586, 5097, 4583, 4033, 3447, 2834, 2211, 1595, 997, 417,
-159, -755, -1395, -2092, -2848, -3650, -4478, -5315, -6148, -6970, -7786, -8611, -9471,
-10416, -11539, -13029, -15274, 17321, 13694, 11208, 9654, 8590
],
[
4898, 4743, 4569, 4398, 4240, 4083, 3895, 3635, 3272, 2793, 2215, 1580, 949, 377, -110,
-533, -951, -1435, -2033, -2744, -3528, -4328, -5093, -5789, -6396, -6906, -7310, -7589,
-7681, -7413, -6150, -1692, 3093, 4546, 4941, 4991, 4898
],
[
3197, 3214, 3174, 3114, 3064, 3034, 3005, 2920, 2708, 2313, 1721, 989, 234, -413, -875,
-1167, -1383, -1653, -2091, -2725, -3478, -4229, -4883, -5387, -5708, -5819, -5678, -5212,
-4313, -2930, -1257, 307, 1502, 2310, 2809, 3081, 3197
],
[
2300, 2352, 2353, 2322, 2283, 2259, 2257, 2241, 2125, 1797, 1189, 352, -531, -1246, -1691,
-1904, -1978, -2017, -2175, -2596, -3223, -3855, -4342, -4614, -4632, -4370, -3821, -3005,
-2023, -1064, -246, 441, 1034, 1531, 1911, 2164, 2300
],
[
1731, 1780, 1794, 1782, 1745, 1699, 1663, 1638, 1548, 1246, 621, -268, -1174, -1849, -2220,
-2373, -2384, -2249, -2044, -2030, -2331, -2774, -3134, -3279, -3151, -2769, -2193, -1502,
-829, -312, 57, 391, 748, 1098, 1397, 1612, 1731
],
[
1356, 1380, 1388, 1386, 1358, 1304, 1244, 1197, 1096, 778, 134, -737, -1565, -2125, -2374,
-2397, -2251, -1915, -1446, -1068, -994, -1229, -1577, -1796, -1767, -1524, -1142, -682,
-259, -3, 128, 289, 542, 826, 1080, 1264, 1356
],
[
1126, 1119, 1103, 1099, 1081, 1033, 972, 917, 793, 442, -204, -1006, -1710, -2133, -2225,
-2049, -1701, -1256, -793, -402, -184, -238, -511, -776, -863, -782, -586, -303, -36, 80,
86, 162, 376, 642, 883, 1056, 1126
],
[
1000, 976, 939, 932, 924, 886, 828, 760, 597, 209, -418, -1126, -1700, -1982, -1922, -1597,
-1152, -721, -371, -86, 128, 166, -9, -239, -364, -375, -304, -152, 3, 35, -24, 8, 205, 477,
735, 926, 1000
],
[
915, 914, 884, 891, 905, 878, 811, 703, 473, 37, -568, -1182, -1623, -1766, -1600, -1227,
-785, -397, -124, 79, 253, 319, 211, 29, -92, -141, -144, -94, -40, -79, -182, -185, -10,
267, 556, 796, 915
],
[
798, 876, 907, 958, 1007, 996, 912, 738, 417, -89, -693, -1225, -1534, -1556, -1335, -975,
-570, -215, 28, 190, 329, 398, 332, 194, 91, 30, -18, -52, -100, -222, -377, -424, -288,
-21, 297, 596, 798
],
[
609, 811, 956, 1085, 1178, 1187, 1085, 844, 422, -170, -800, -1280, -1488, -1424, -1176,
-837, -463, -122, 124, 283, 405, 476, 453, 368, 290, 222, 133, 12, -152, -377, -602, -701,
-607, -359, -28, 318, 609
],
[
386, 712, 990, 1216, 1363, 1394, 1278, 978, 460, -231, -916, -1384, -1543, -1438, -1172,
-830, -458, -108, 168, 360, 501, 602, 646, 639, 598, 513, 355, 115, -199, -553, -856, -996,
-927, -691, -356, 19, 386
],
[
194, 612, 997, 1317, 1532, 1602, 1482, 1122, 489, -336, -1113, -1607, -1757, -1637, -1353,
-984, -583, -193, 146, 421, 646, 836, 987, 1078, 1084, 968, 701, 283, -235, -750, -1131,
-1288, -1218, -976, -629, -228, 194
],
[
42, 518, 974, 1373, 1667, 1799, 1695, 1262, 450, -601, -1528, -2064, -2202, -2055, -1733,
-1313, -849, -377, 74, 491, 875, 1225, 1524, 1739, 1815, 1688, 1296, 633, -185, -929, -1410,
-1579, -1492, -1230, -859, -425, 42
],
[
-175, 346, 850, 1303, 1656, 1834, 1719, 1147, 25, -1357, -2406, -2885, -2918, -2667, -2248,
-1730, -1158, -560, 44, 641, 1219, 1762, 2250, 2643, 2883, 2876, 2488, 1593, 295, -919,
-1656, -1911, -1835, -1556, -1154, -683, -175
],
[
-864, -349, 123, 505, 721, 655, 138, -946, -2328, -3394, -3877, -3895, -3615, -3150, -2570,
-1916, -1216, -486, 259, 1010, 1757, 2490, 3194, 3852, 4434, 4886, 5104, 4853, 3625, 1039,
-1322, -2314, -2481, -2266, -1869, -1384, -864
],
[
-16076, -15076, -14076, -13076, -12076, -11076, -10076, -9076, -8076, -7076, -6076, -5076,
-4076, -3076, -2076, -1076, -76, 924, 1924, 2924, 3924, 4924, 5924, 6924, 7924, 8924, 9924,
10924, 11924, 12924, 13924, 14924, 15924, 16924, 17924, -17076, -16076
],
];
pub(crate) fn declination_at(site: Geodetic) -> Option<Radians> {
let (latitude, longitude) = (site.latitude_deg() as f32, site.longitude_deg() as f32);
if !latitude.is_finite() || !longitude.is_finite() {
return None;
}
let latitude = latitude.clamp(-90.0, 90.0);
let mut longitude = longitude;
if longitude > 180.0 {
longitude -= 360.0;
}
if longitude < -180.0 {
longitude += 360.0;
}
interpolate(&TABLE, latitude, longitude.clamp(-180.0, 180.0))
}
fn interpolate(table: &[[i16; 37]; 19], latitude: f32, longitude: f32) -> Option<Radians> {
let (row, north) = cell(latitude + 90.0, table.len());
let (column, east) = cell(longitude + 180.0, table[0].len());
let (south_row, north_row) = (table.get(row)?, table.get(row + 1)?);
let corner = |line: &[i16; 37], at: usize| line.get(at).map(|&v| f32::from(v));
let sw = corner(south_row, column)?;
let near_sw = |value: f32| {
let (turn, step) = (360.0 / SCALE, value - sw);
if step > turn / 2.0 {
value - turn
} else if step < -turn / 2.0 {
value + turn
} else {
value
}
};
let se = near_sw(corner(south_row, column + 1)?);
let (nw, ne) = (
near_sw(corner(north_row, column)?),
near_sw(corner(north_row, column + 1)?),
);
let southern = sw + east * (se - sw);
let northern = nw + east * (ne - nw);
let degrees = (southern + north * (northern - southern)) * SCALE;
Some(Radians::from_radians(wrap_pi(degrees.to_radians())))
}
fn cell(offset: f32, lines: usize) -> (usize, f32) {
let index = ((offset / RESOLUTION) as usize).min(lines - 2);
(index, (offset / RESOLUTION - index as f32).clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
fn degrees_at(latitude: f64, longitude: f64) -> f32 {
let radians = declination_at(Geodetic::from_degrees(latitude, longitude, 0.0));
radians
.map(|d| d.as_radians().to_degrees())
.unwrap_or(f32::NAN)
}
#[test]
fn grid_points_agree_with_px4s_own_test() {
assert!((degrees_at(-50.0, -180.0) - 31.7).abs() <= 1.4);
assert!((degrees_at(-50.0, -100.0) - 27.2).abs() <= 1.4);
}
#[test]
fn between_grid_points_the_table_is_the_model_to_its_interpolation_error() {
for (latitude, longitude, model) in [
(56.41, 43.76, 13.8117),
(47.397742, 8.545594, 3.7347),
(-33.9, 151.2, 12.8519),
] {
let got = degrees_at(latitude, longitude);
assert!(
(got - model).abs() < 0.25,
"{latitude}, {longitude}: {got} against {model}"
);
}
}
#[test]
fn at_a_grid_point_the_lookup_is_the_cell() {
let cell = |row: usize, column: usize| f32::from(TABLE[row][column]) * SCALE;
assert!((degrees_at(-50.0, -180.0) - cell(4, 0)).abs() < 1e-4);
assert!((degrees_at(-80.0, -170.0) - cell(1, 1)).abs() < 1e-4);
assert!((degrees_at(90.0, 180.0) - cell(18, 36)).abs() < 1e-4);
}
#[test]
fn corners_either_side_of_a_half_turn_are_unwrapped_before_interpolating() {
let got = degrees_at(-65.0, 135.0);
let off = (got - -173.3 + 180.0).rem_euclid(360.0) - 180.0;
assert!(off.abs() < 2.5, "{got} against -173.3");
assert!(got.abs() <= 180.0);
}
#[test]
fn interpolation_crosses_the_turn_in_either_direction_and_wraps() {
let across = |sw: i16, rest: i16, latitude: f32, longitude: f32| {
let mut table = [[0; 37]; 19];
table[0][0] = sw;
(table[0][1], table[1][0], table[1][1]) = (rest, rest, rest);
interpolate(&table, latitude, longitude)
.map(|d| d.as_radians().to_degrees())
.unwrap_or(f32::NAN)
};
for (sw, rest) in [(17_900, -17_900), (-17_900, 17_900)] {
let got = across(sw, rest, -90.0, -175.0);
assert!((got.abs() - 180.0).abs() < 1e-3, "{sw} to {rest}: {got}");
}
let got = across(17_900, -17_800, -90.0, -172.5);
assert!((got - -178.75).abs() < 1e-3, "{got}");
}
#[test]
fn halfway_between_two_columns_is_their_mean() {
let mean = (degrees_at(-50.0, -180.0) + degrees_at(-50.0, -170.0)) / 2.0;
assert!((degrees_at(-50.0, -175.0) - mean).abs() < 1e-5);
}
#[test]
fn longitude_wraps_and_latitude_clamps() {
assert_eq!(degrees_at(0.0, 190.0), degrees_at(0.0, -170.0));
assert_eq!(degrees_at(0.0, -190.0), degrees_at(0.0, 170.0));
assert_eq!(degrees_at(95.0, 20.0), degrees_at(90.0, 20.0));
}
#[test]
fn a_site_that_is_not_a_number_has_no_declination() {
assert!(declination_at(Geodetic::from_degrees(f64::NAN, 0.0, 0.0)).is_none());
assert!(declination_at(Geodetic::from_degrees(0.0, f64::INFINITY, 0.0)).is_none());
}
}