use crate::math::{tauf, taupf, A, F};
pub(crate) const K0: f64 = 0.9996;
const N: f64 = F / (2.0 - F);
const N2: f64 = N * N;
const N3: f64 = N2 * N;
const N4: f64 = N2 * N2;
const N5: f64 = N4 * N;
const N6: f64 = N4 * N2;
pub(crate) const A1: f64 = A / (1.0 + N) * (1.0 + N2 / 4.0 + N4 / 64.0 + N6 / 256.0);
const ALPHA: [f64; 6] = [
N / 2.0 - 2.0 / 3.0 * N2 + 5.0 / 16.0 * N3 + 41.0 / 180.0 * N4 - 127.0 / 288.0 * N5
+ 7891.0 / 37800.0 * N6,
13.0 / 48.0 * N2 - 3.0 / 5.0 * N3 + 557.0 / 1440.0 * N4 + 281.0 / 630.0 * N5
- 1_983_433.0 / 1_935_360.0 * N6,
61.0 / 240.0 * N3 - 103.0 / 140.0 * N4 + 15061.0 / 26880.0 * N5 + 167_603.0 / 181_440.0 * N6,
49561.0 / 161_280.0 * N4 - 179.0 / 168.0 * N5 + 6_601_661.0 / 7_257_600.0 * N6,
34729.0 / 80640.0 * N5 - 3_418_889.0 / 1_995_840.0 * N6,
212_378_941.0 / 319_334_400.0 * N6,
];
const BETA: [f64; 6] = [
N / 2.0 - 2.0 / 3.0 * N2 + 37.0 / 96.0 * N3 - 1.0 / 360.0 * N4 - 81.0 / 512.0 * N5
+ 96199.0 / 604_800.0 * N6,
N2 / 48.0 + N3 / 15.0 - 437.0 / 1440.0 * N4 + 46.0 / 105.0 * N5
- 1_118_711.0 / 3_870_720.0 * N6,
17.0 / 480.0 * N3 - 37.0 / 840.0 * N4 - 209.0 / 4480.0 * N5 + 5569.0 / 90720.0 * N6,
4397.0 / 161_280.0 * N4 - 11.0 / 504.0 * N5 - 830_251.0 / 7_257_600.0 * N6,
4583.0 / 161_280.0 * N5 - 108_847.0 / 3_991_680.0 * N6,
20_648_693.0 / 638_668_800.0 * N6,
];
fn clenshaw(c: &[f64; 6], xi: f64, eta: f64) -> (f64, f64) {
let (s2x, c2x) = (2.0 * xi).sin_cos();
let ch2e = (2.0 * eta).cosh();
let sh2e = (2.0 * eta).sinh();
let xr = 2.0 * c2x * ch2e;
let xi_im = -2.0 * s2x * sh2e;
let (mut b1r, mut b1i) = (0.0_f64, 0.0_f64);
let (mut b2r, mut b2i) = (0.0_f64, 0.0_f64);
for &cj in c.iter().rev() {
let tr = cj + (xr * b1r - xi_im * b1i) - b2r;
let ti = (xr * b1i + xi_im * b1r) - b2i;
b2r = b1r;
b2i = b1i;
b1r = tr;
b1i = ti;
}
let sr = s2x * ch2e;
let si = c2x * sh2e;
(b1r * sr - b1i * si, b1r * si + b1i * sr)
}
pub(crate) fn forward(lat: f64, dlon: f64) -> (f64, f64) {
let phi = lat.to_radians();
let lam = dlon.to_radians();
let tau = phi.tan();
let taup = taupf(tau);
let (slam, clam) = lam.sin_cos();
let xip = taup.atan2(clam);
let etap = (slam / taup.hypot(clam)).asinh();
let (dre, dim) = clenshaw(&ALPHA, xip, etap);
let xi = xip + dre;
let eta = etap + dim;
(K0 * A1 * eta, K0 * A1 * xi)
}
pub(crate) fn reverse(x: f64, y: f64) -> (f64, f64) {
let xi = y / (K0 * A1);
let eta = x / (K0 * A1);
let (dre, dim) = clenshaw(&BETA, xi, eta);
let xip = xi - dre;
let etap = eta - dim;
let (sxip, cxip) = xip.sin_cos();
let sheta = etap.sinh();
let taup = sxip / sheta.hypot(cxip);
let tau = tauf(taup);
let lat = tau.atan().to_degrees();
let dlon = sheta.atan2(cxip).to_degrees();
(lat, dlon)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constants_pinned() {
assert!((A1 - 6_367_449.145_823_415).abs() < 1e-6);
assert_eq!(K0, 0.9996);
}
#[test]
fn equator_on_central_meridian_is_exact_zero() {
let (x, y) = forward(0.0, 0.0);
assert_eq!(x, 0.0);
assert_eq!(y, 0.0);
}
#[test]
fn forward_reverse_round_trip_is_tight() {
for &(lat, dlon) in &[
(40.748333, -1.985278),
(-33.856784, 2.215297),
(83.5, 2.9),
(-79.9, -2.9),
(0.5, -0.5),
] {
let (x, y) = forward(lat, dlon);
let (lat2, dlon2) = reverse(x, y);
let (x2, y2) = forward(lat2, dlon2);
assert!((x - x2).abs() < 1e-7, "x drift {lat} {dlon}");
assert!((y - y2).abs() < 1e-7, "y drift {lat} {dlon}");
}
}
}