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
//! Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS.
//!
//! The whole globe is covered: every valid latitude and longitude has a UTM
//! (latitudes -80 to 84) or UPS (the polar caps) representation and an MGRS
//! reference at every precision from 0 to 11 digit pairs, and every valid MGRS
//! reference names a cell with a well defined center. Forward and reverse both
//! ship. The projection core is the Krueger series to order n^6 from Karney,
//! "Transverse Mercator with an accuracy of a few nanometers" (2011). The MGRS
//! grid follows NGA TM 8358.1, checked cell by cell against GeographicLib's
//! `GeoConvert`.
//!
//! This crate is `std` (the projection core needs the `f64` math functions) and
//! has zero runtime dependencies.
//!
//! ```
//! use mgrs_convert::{LatLon, Mgrs, Precision};
//!
//! let position = LatLon::new(40.748333, -73.985278)?;
//! let cell = position.to_mgrs(Precision::ONE_METER);
//! assert_eq!(cell.to_string(), "18TWL8566411315");
//!
//! let parsed: Mgrs = "18TWL8566411315".parse()?;
//! assert_eq!(parsed, cell);
//! let center = parsed.to_latlon();
//! assert!((center.lat() - 40.748333).abs() < 1e-5);
//! assert!((center.lon() + 73.985278).abs() < 1e-5);
//! # Ok::<(), mgrs_convert::Error>(())
//! ```
//!
//! # Types
//!
//! Four types, one error enum, nothing else:
//!
//! - [`LatLon`], a validated geodetic coordinate. Its [`LatLon::to_mgrs`] is
//!   infallible because every valid input has an MGRS reference.
//! - [`UtmUps`], a grid coordinate, where zone 0 is UPS. Its [`UtmUps::to_mgrs`] is
//!   fallible by design: the accepted transfer range extends 100 km past the
//!   MGRS grid on every side, and the ring in between returns
//!   [`Error::OutsideMgrsExtent`] instead of panicking.
//! - [`Mgrs`], a grid cell. Only constructible through validated paths, so its
//!   `Display` is total. Reverse conversion returns the cell center.
//! - [`Precision`], the validated digit-pair count, 0 through 11.
//!
//! # Boundary behavior
//!
//! - Latitude 0 and `-0.0` are both northern, band N, on every path.
//! - The UTM latitude interval is [-80, 84), matching GeoConvert: -80 exactly
//!   is UTM band C, 84 exactly is UPS north, and the last UTM latitude is one
//!   ulp below 84 (band X).
//! - A coordinate exactly on an excluded upper grid edge is nudged inside by
//!   2^-28 m before truncation, so it formats as the last cell of its own
//!   square.
//! - Grid-zone-only references such as `"18T"` are rejected with
//!   [`ParseErrorKind::GridZoneOnly`], because a cell six degrees wide has no useful
//!   center.
//!
//! # Migrating from `geoconvert`
//!
//! | `geoconvert` 1.0 call | replacement |
//! | --- | --- |
//! | `LatLon::create(lat, lon)` | [`LatLon::new`] |
//! | `latlon.to_mgrs(6)` | `latlon.to_mgrs(Precision::new(6)?)` |
//! | `Mgrs::parse_str(s)` | `s.parse::<Mgrs>()` |
//! | `mgrs.to_string()` | unchanged |
//! | `mgrs.to_latlon()` | unchanged |
//! | `UtmUps::create(z, north, e, n)` | [`UtmUps::utm`] or [`UtmUps::ups`] |
//! | `utmups.to_mgrs(p)` | `utmups.to_mgrs(Precision::new(p)?)?` |
//! | `Mgrs::create(...)` | removed, construct via `to_mgrs` or `parse` |
//!
//! The two new `?`s are deliberate: precision validation and the MGRS-extent
//! check return errors a caller can print instead of panicking.
//!
//! ```
//! use mgrs_convert::{Error, Precision, UtmUps};
//! // Precision is validated at the edge.
//! assert!(matches!(Precision::new(12), Err(Error::PrecisionOutOfRange { .. })));
//! // The transfer ring converts to lat/lon but has no MGRS reference.
//! let ring = UtmUps::utm(18, true, 50_000.0, 4_511_315.0)?;
//! assert!(matches!(ring.to_mgrs(Precision::ONE_METER), Err(Error::OutsideMgrsExtent { .. })));
//! # Ok::<(), mgrs_convert::Error>(())
//! ```
//!
//! # Performance
//!
//! Linear per conversion, no allocation outside `Display`, no lookup table
//! computed at runtime. The doctest below is the quadratic-regression tripwire:
//! one hundred thousand round trips inside a generous fixed budget.
//!
//! ```
//! use mgrs_convert::{LatLon, Precision};
//! let start = std::time::Instant::now();
//! let mut checksum = 0.0_f64;
//! for i in 0..100_000_u32 {
//!     let lat = -80.0 + (f64::from(i) * 1.6377) % 160.0;
//!     let lon = -180.0 + (f64::from(i) * 3.9119) % 360.0;
//!     let p = LatLon::new(lat, lon)?;
//!     let back = p.to_utmups().to_latlon();
//!     checksum += back.lat();
//! }
//! assert!(checksum.is_finite());
//! assert!(start.elapsed().as_secs() < 60, "quadratic regression tripwire");
//! # Ok::<(), mgrs_convert::Error>(())
//! ```

#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing
)]

mod error;
mod grid;
mod latlon;
mod math;
mod mgrs;
mod polar;
mod precision;
mod tm;
mod utmups;

pub use error::{Error, ParseErrorKind};
pub use latlon::LatLon;
pub use mgrs::Mgrs;
pub use precision::Precision;
pub use utmups::UtmUps;

#[cfg(test)]
mod tests {
    use crate::math;

    #[test]
    fn geodetic_constants_pinned() {
        // Invariant 9: the only two geodetic parameters in the crate.
        assert_eq!(math::A, 6_378_137.0);
        assert_eq!(math::F, 1.0 / 298.257_223_563);
    }
}