use super::{PolarStereographic, TransverseMercator};
use crate::angle::{ang_diff, ang_normalize};
use crate::ellipsoid::{Ellipsoid, WGS84};
#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::position::Geodetic;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UtmUps {
pub zone: u8,
pub north: bool,
pub easting_m: f64,
pub northing_m: f64,
}
const TILE: f64 = 100_000.0;
impl UtmUps {
#[inline]
pub const fn new(zone: u8, north: bool, easting_m: f64, northing_m: f64) -> Self {
UtmUps {
zone,
north,
easting_m,
northing_m,
}
}
pub fn central_meridian_deg(self) -> f64 {
if self.zone == 0 {
f64::NAN
} else {
6.0 * self.zone as f64 - 183.0
}
}
pub fn standard_zone(g: Geodetic) -> Option<u8> {
let (lat, lon) = (g.lat_deg, g.lon_deg);
if !(lat.abs() <= 90.0 && lon.is_finite()) {
return None;
}
if !(-80.0..84.0).contains(&lat) {
return Some(0);
}
let mut ilon = ang_normalize(lon).floor() as i32;
if ilon == 180 {
ilon = -180; }
let mut zone = (ilon + 186) / 6;
let band = ((lat.floor() as i32 + 80) / 8 - 10).clamp(-10, 9);
if band == 7 && zone == 31 && ilon >= 3 {
zone = 32; } else if band == 9 && (0..42).contains(&ilon) {
zone = 2 * ((ilon + 183) / 12) + 1; }
Some(zone as u8)
}
fn in_range(utm: bool, north: bool, easting_m: f64, northing_m: f64) -> bool {
let (e0, e1, n0, n1) = match (utm, north) {
(false, false) => (8.0, 32.0, 8.0, 32.0),
(false, true) => (13.0, 27.0, 13.0, 27.0),
(true, false) => (1.0, 9.0, 10.0, 100.0 + 95.0),
(true, true) => (1.0, 9.0, 10.0 - 100.0, 95.0),
};
(e0 - 1.0) * TILE <= easting_m
&& easting_m <= (e1 + 1.0) * TILE
&& (n0 - 1.0) * TILE <= northing_m
&& northing_m <= (n1 + 1.0) * TILE
}
}
impl Ellipsoid {
#[doc(alias = "utm")]
#[doc(alias = "ups")]
pub fn to_utm_ups(self, g: Geodetic) -> Option<UtmUps> {
self.to_utm_ups_zone(g, UtmUps::standard_zone(g)?)
}
pub fn to_utm_ups_zone(self, g: Geodetic, zone: u8) -> Option<UtmUps> {
if !(g.lat_deg.abs() <= 90.0 && g.lon_deg.is_finite()) || zone > 60 {
return None;
}
let north = !g.lat_deg.is_sign_negative();
let (easting_m, northing_m) = if zone == 0 {
if g.lat_deg.abs() < 70.0 {
return None;
}
let p = self.ups().forward(north, g);
(p.x_m + 20.0 * TILE, p.y_m + 20.0 * TILE)
} else {
let lon0 = 6.0 * zone as f64 - 183.0;
let dlon = ang_diff(lon0, g.lon_deg).0;
if dlon.is_nan() || dlon.abs() > 60.0 {
return None;
}
let p = self.utm().project::<false>(lon0, g);
(p.x_m + 5.0 * TILE, p.y_m + if north { 0.0 } else { 100.0 * TILE })
};
UtmUps::in_range(zone != 0, north, easting_m, northing_m).then_some(UtmUps {
zone,
north,
easting_m,
northing_m,
})
}
pub fn from_utm_ups(self, u: UtmUps) -> Option<Geodetic> {
if u.zone > 60 || !UtmUps::in_range(u.zone != 0, u.north, u.easting_m, u.northing_m) {
return None;
}
Some(if u.zone == 0 {
self.ups()
.reverse(u.north, u.easting_m - 20.0 * TILE, u.northing_m - 20.0 * TILE)
} else {
let y = u.northing_m - if u.north { 0.0 } else { 100.0 * TILE };
self.utm()
.reverse(u.central_meridian_deg(), u.easting_m - 5.0 * TILE, y)
})
}
fn utm(self) -> TransverseMercator {
if self == WGS84 {
TransverseMercator::UTM
} else {
TransverseMercator::new(self, 0.9996)
}
}
fn ups(self) -> PolarStereographic {
PolarStereographic::new(self, 0.994)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zone_edges_and_exceptions() {
let z = |lat, lon| UtmUps::standard_zone(Geodetic::new(lat, lon, 0.0)).unwrap();
assert_eq!((z(10.0, -180.0), z(10.0, -174.000_000_1), z(10.0, -174.0)), (1, 1, 2));
assert_eq!((z(10.0, 179.999), z(10.0, 180.0), z(10.0, 0.0)), (60, 1, 31));
assert_eq!(
(
z(56.0, 3.0),
z(55.999, 3.0),
z(63.999, 3.0),
z(64.0, 3.0),
z(60.0, 2.999)
),
(32, 31, 32, 31, 31)
);
assert_eq!(
(
z(72.0, 8.999),
z(72.0, 9.0),
z(72.0, 21.0),
z(72.0, 33.0),
z(72.0, 42.0)
),
(31, 33, 35, 37, 38)
);
assert_eq!(z(71.999, 9.0), 32);
assert_eq!(
(z(83.999, 0.0), z(84.0, 0.0), z(-80.0, 0.0), z(-80.000_1, 0.0)),
(31, 0, 31, 0)
);
}
}