use std::f64::consts::PI;
use geonative_core::Coord;
use crate::ellipsoid::Ellipsoid;
#[derive(Debug, Clone, Copy)]
pub struct TmParams {
pub ellipsoid: Ellipsoid,
pub lon0_deg: f64,
pub k0: f64,
pub false_easting: f64,
pub false_northing: f64,
}
impl TmParams {
pub const fn utm_north(zone: u8, ellipsoid: Ellipsoid) -> Self {
Self {
ellipsoid,
lon0_deg: (zone as f64) * 6.0 - 183.0,
k0: 0.9996,
false_easting: 500_000.0,
false_northing: 0.0,
}
}
pub const fn utm_south(zone: u8, ellipsoid: Ellipsoid) -> Self {
Self {
ellipsoid,
lon0_deg: (zone as f64) * 6.0 - 183.0,
k0: 0.9996,
false_easting: 500_000.0,
false_northing: 10_000_000.0,
}
}
}
pub fn forward(p: &TmParams, c: &mut Coord) {
let coeffs = Coeffs::new(p.ellipsoid.n());
let lat_rad = c.y.to_radians();
let dlon_rad = (c.x - p.lon0_deg).to_radians();
let sin_phi = lat_rad.sin();
let two_n_sqrt_over_one_plus_n = 2.0 * coeffs.n.sqrt() / (1.0 + coeffs.n);
let t = ((lat_rad.tan()).asinh() - two_n_sqrt_over_one_plus_n
* (two_n_sqrt_over_one_plus_n * sin_phi).atanh())
.sinh();
let cos_dlon = dlon_rad.cos();
let xi_prime = (t / cos_dlon).atan().rem_pi();
let eta_prime = (dlon_rad.sin() / (t * t + cos_dlon * cos_dlon).sqrt()).atanh();
let (xi, eta) = sum_series(&coeffs.alpha, xi_prime, eta_prime);
c.x = p.false_easting + p.k0 * coeffs.a_hat * eta;
c.y = p.false_northing + p.k0 * coeffs.a_hat * xi;
}
pub fn inverse(p: &TmParams, c: &mut Coord) {
let coeffs = Coeffs::new(p.ellipsoid.n());
let xi = (c.y - p.false_northing) / (p.k0 * coeffs.a_hat);
let eta = (c.x - p.false_easting) / (p.k0 * coeffs.a_hat);
let (xi_prime, eta_prime) = sum_series_neg(&coeffs.beta, xi, eta);
let tanh_eta = eta_prime.tanh();
let chi = (xi_prime.sin() / (1.0 + tanh_eta * tanh_eta).sqrt()).asin();
let dlon = tanh_eta.atan2(xi_prime.cos());
let e = (p.ellipsoid.e2()).sqrt();
let target_atanh_sin_chi = chi.sin().atanh();
let mut phi = chi;
for _ in 0..15 {
let sigma = target_atanh_sin_chi + e * (e * phi.sin()).atanh();
let new_phi = sigma.sinh().atan();
if (new_phi - phi).abs() < 1e-15 {
phi = new_phi;
break;
}
phi = new_phi;
}
c.x = p.lon0_deg + dlon.to_degrees();
c.y = phi.to_degrees();
}
fn sum_series(a: &[f64; 6], xi_p: f64, eta_p: f64) -> (f64, f64) {
let mut xi = xi_p;
let mut eta = eta_p;
for (i, &ai) in a.iter().enumerate() {
let k = 2.0 * (i + 1) as f64;
xi += ai * (k * xi_p).sin() * (k * eta_p).cosh();
eta += ai * (k * xi_p).cos() * (k * eta_p).sinh();
}
(xi, eta)
}
fn sum_series_neg(b: &[f64; 6], xi: f64, eta: f64) -> (f64, f64) {
let mut xi_p = xi;
let mut eta_p = eta;
for (i, &bi) in b.iter().enumerate() {
let k = 2.0 * (i + 1) as f64;
xi_p -= bi * (k * xi).sin() * (k * eta).cosh();
eta_p -= bi * (k * xi).cos() * (k * eta).sinh();
}
(xi_p, eta_p)
}
struct Coeffs {
n: f64,
a_hat: f64,
alpha: [f64; 6],
beta: [f64; 6],
}
impl Coeffs {
fn new(n: f64) -> Self {
let n2 = n * n;
let n3 = n2 * n;
let n4 = n3 * n;
let n5 = n4 * n;
let n6 = n5 * n;
let a = 6_378_137.0_f64;
let a_hat = (a / (1.0 + n)) * (1.0 + n2 / 4.0 + n4 / 64.0 + n6 / 256.0);
let alpha = [
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 / 37_800.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 + 15_061.0 / 26_880.0 * n5
+ 167_603.0 / 181_440.0 * n6,
49_561.0 / 161_280.0 * n4 - 179.0 / 168.0 * n5 + 6_601_661.0 / 7_257_600.0 * n6,
34_729.0 / 80_640.0 * n5 - 3_418_889.0 / 1_995_840.0 * n6,
212_378_941.0 / 319_334_400.0 * n6,
];
let beta = [
n / 2.0 - 2.0 / 3.0 * n2 + 37.0 / 96.0 * n3 - n4 / 360.0 - 81.0 / 512.0 * n5
+ 96_199.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 / 90_720.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,
];
Self {
n,
a_hat,
alpha,
beta,
}
}
}
trait RemPi {
fn rem_pi(self) -> f64;
}
impl RemPi for f64 {
fn rem_pi(self) -> f64 {
let half = PI * 0.5;
if self > half {
self - PI
} else if self < -half {
self + PI
} else {
self
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ellipsoid::{GRS80, WGS84};
fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() < eps
}
#[test]
fn equator_one_degree_east_of_cm() {
let params = TmParams::utm_south(56, GRS80);
let mut c = Coord::xy(154.0, 0.0); forward(¶ms, &mut c);
assert!(approx_eq(c.x, 611_298.0, 5.0), "easting got {}", c.x);
assert!(approx_eq(c.y, 10_000_000.0, 1e-6), "northing got {}", c.y);
}
#[test]
fn wgs84_utm_melbourne_natural_zone_55_forward() {
let params = TmParams::utm_south(55, WGS84);
let mut c = Coord::xy(144.9631, -37.8136);
forward(¶ms, &mut c);
assert!(c.x < 500_000.0, "easting {} should be < false_easting", c.x);
assert!(c.x > 300_000.0, "easting {} too far from CM", c.x);
assert!(approx_eq(c.y, 5_813_000.0, 5_000.0), "northing got {}", c.y);
}
#[test]
fn wgs84_utm_round_trip_melbourne() {
let params = TmParams::utm_south(55, WGS84);
let original = Coord::xy(144.9631, -37.8136);
let mut c = original;
forward(¶ms, &mut c);
inverse(¶ms, &mut c);
assert!(approx_eq(c.x, original.x, 1e-8), "got {}", c.x);
assert!(approx_eq(c.y, original.y, 1e-8), "got {}", c.y);
}
#[test]
fn grs80_mga_zone_56_sydney_forward() {
let params = TmParams::utm_south(56, GRS80);
let mut c = Coord::xy(151.2093, -33.8688);
forward(¶ms, &mut c);
assert!(c.x < 500_000.0 && c.x > 300_000.0, "easting got {}", c.x);
assert!(approx_eq(c.y, 6_251_000.0, 2_000.0), "northing got {}", c.y);
}
#[test]
fn grs80_mga_round_trip_sydney() {
let params = TmParams::utm_south(56, GRS80);
let original = Coord::xy(151.2093, -33.8688);
let mut c = original;
forward(¶ms, &mut c);
inverse(¶ms, &mut c);
assert!(approx_eq(c.x, original.x, 1e-8));
assert!(approx_eq(c.y, original.y, 1e-8));
}
#[test]
fn northern_hemisphere_utm() {
let params = TmParams::utm_north(30, WGS84);
let original = Coord::xy(-0.1276, 51.5074);
let mut c = original;
forward(¶ms, &mut c);
inverse(¶ms, &mut c);
assert!(approx_eq(c.x, original.x, 1e-8));
assert!(approx_eq(c.y, original.y, 1e-8));
}
#[test]
fn central_meridian_easting_is_false_easting() {
let params = TmParams::utm_south(55, WGS84); let mut c = Coord::xy(147.0, -30.0);
forward(¶ms, &mut c);
assert!(approx_eq(c.x, 500_000.0, 1e-3));
}
}