mgrs-convert 0.1.0

Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS grid references, round-tripping exactly at every precision from 100 km to 1 um
Documentation
//! The validated geodetic coordinate.
//
// Bounded floating-point arithmetic only; see the note in `math.rs`.

use crate::error::Error;
use crate::mgrs::Mgrs;
use crate::precision::Precision;
use crate::utmups::{
    central_meridian, standard_zone, UtmUps, UTM_FALSE_EASTING, UTM_FALSE_NORTHING,
};
use crate::{polar, tm};

/// Geodetic coordinate on WGS84. Constructor-validated: latitude is a finite
/// value in [-90, 90], longitude is finite and normalized to [-180, 180).
///
/// Latitude `-0.0` behaves identically to `0.0` on every path: both are
/// northern hemisphere, band N.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LatLon {
    lat: f64,
    lon: f64,
}

impl LatLon {
    /// Validates a geodetic coordinate. Longitude is accepted at any finite
    /// value and normalized into [-180, 180). Longitude 180 normalizes to -180.
    ///
    /// ```
    /// use mgrs_convert::LatLon;
    /// let p = LatLon::new(40.748333, -73.985278)?;
    /// assert_eq!(p.lat(), 40.748333);
    /// assert_eq!(LatLon::new(10.0, 180.0)?.lon(), -180.0);
    /// assert!(LatLon::new(f64::NAN, 0.0).is_err());
    /// # Ok::<(), mgrs_convert::Error>(())
    /// ```
    pub fn new(lat: f64, lon: f64) -> Result<LatLon, Error> {
        if !lat.is_finite() || !(-90.0..=90.0).contains(&lat) {
            return Err(Error::LatOutOfRange { found: lat });
        }
        if !lon.is_finite() {
            return Err(Error::LonOutOfRange { found: lon });
        }
        Ok(LatLon {
            lat,
            lon: normalize_lon(lon),
        })
    }

    /// Internal total constructor for reverse projections, whose results are
    /// inside the domain by construction. The clamp cannot move any reachable
    /// value. It keeps the function total without a panic path.
    pub(crate) fn new_normalized(lat: f64, lon: f64) -> LatLon {
        LatLon {
            lat: lat.clamp(-90.0, 90.0),
            lon: normalize_lon(lon),
        }
    }

    /// Latitude in degrees, in [-90, 90].
    pub fn lat(self) -> f64 {
        self.lat
    }

    /// Longitude in degrees, always in [-180, 180).
    pub fn lon(self) -> f64 {
        self.lon
    }

    /// Projects to UTM or UPS under the standard zone rules: latitude in
    /// [-80, 84) is UTM (-80 included, 84 excluded), latitude 84 and above is
    /// UPS north and below -80 is UPS south, with the Norway and Svalbard
    /// exceptions.
    ///
    /// ```
    /// use mgrs_convert::LatLon;
    /// let u = LatLon::new(60.0, 3.0)?.to_utmups();
    /// assert_eq!(u.zone(), 32); // Norway exception
    /// # Ok::<(), mgrs_convert::Error>(())
    /// ```
    pub fn to_utmups(self) -> UtmUps {
        let (zone, north, easting, northing) = self.utmups_parts();
        UtmUps::raw(zone, north, easting, northing)
    }

    /// Forward projection parts. North is `lat >= 0`, which is true for
    /// `-0.0`: no code consults the sign bit of a zero.
    pub(crate) fn utmups_parts(self) -> (u8, bool, f64, f64) {
        let zone = standard_zone(self.lat, self.lon);
        if zone == 0 {
            let north = self.lat >= 0.0;
            let (e, n) = polar::forward(north, self.lat, self.lon);
            (0, north, e, n)
        } else {
            let north = self.lat >= 0.0;
            let (x, y) = tm::forward(self.lat, self.lon - central_meridian(zone));
            let fold = if north { 0.0 } else { UTM_FALSE_NORTHING };
            (zone, north, UTM_FALSE_EASTING + x, fold + y)
        }
    }

    /// The MGRS cell containing this coordinate, truncated to `precision`.
    ///
    /// Infallible: every valid latitude and longitude has an MGRS reference.
    /// The band letter comes from the input latitude.
    ///
    /// ```
    /// use mgrs_convert::{LatLon, Precision};
    /// let p = LatLon::new(0.0, 0.0)?;
    /// assert_eq!(p.to_mgrs(Precision::new(6)?).to_string(), "31NAA660214000000");
    /// # Ok::<(), mgrs_convert::Error>(())
    /// ```
    pub fn to_mgrs(self, precision: Precision) -> Mgrs {
        crate::mgrs::from_latlon(self, precision)
    }
}

/// Folds any finite longitude into [-180, 180).
fn normalize_lon(lon: f64) -> f64 {
    if (-180.0..180.0).contains(&lon) {
        return lon;
    }
    let r = lon.rem_euclid(360.0); // in [0, 360)
    if r >= 180.0 {
        r - 360.0
    } else {
        r
    }
}