mgrs_convert/lib.rs
1//! Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS.
2//!
3//! The whole globe is covered: every valid latitude and longitude has a UTM
4//! (latitudes -80 to 84) or UPS (the polar caps) representation and an MGRS
5//! reference at every precision from 0 to 11 digit pairs, and every valid MGRS
6//! reference names a cell with a well defined center. Forward and reverse both
7//! ship. The projection core is the Krueger series to order n^6 from Karney,
8//! "Transverse Mercator with an accuracy of a few nanometers" (2011). The MGRS
9//! grid follows NGA TM 8358.1, checked cell by cell against GeographicLib's
10//! `GeoConvert`.
11//!
12//! This crate is `std` (the projection core needs the `f64` math functions) and
13//! has zero runtime dependencies.
14//!
15//! ```
16//! use mgrs_convert::{LatLon, Mgrs, Precision};
17//!
18//! let position = LatLon::new(40.748333, -73.985278)?;
19//! let cell = position.to_mgrs(Precision::ONE_METER);
20//! assert_eq!(cell.to_string(), "18TWL8566411315");
21//!
22//! let parsed: Mgrs = "18TWL8566411315".parse()?;
23//! assert_eq!(parsed, cell);
24//! let center = parsed.to_latlon();
25//! assert!((center.lat() - 40.748333).abs() < 1e-5);
26//! assert!((center.lon() + 73.985278).abs() < 1e-5);
27//! # Ok::<(), mgrs_convert::Error>(())
28//! ```
29//!
30//! # Types
31//!
32//! Four types, one error enum, nothing else:
33//!
34//! - [`LatLon`], a validated geodetic coordinate. Its [`LatLon::to_mgrs`] is
35//! infallible because every valid input has an MGRS reference.
36//! - [`UtmUps`], a grid coordinate, where zone 0 is UPS. Its [`UtmUps::to_mgrs`] is
37//! fallible by design: the accepted transfer range extends 100 km past the
38//! MGRS grid on every side, and the ring in between returns
39//! [`Error::OutsideMgrsExtent`] instead of panicking.
40//! - [`Mgrs`], a grid cell. Only constructible through validated paths, so its
41//! `Display` is total. Reverse conversion returns the cell center.
42//! - [`Precision`], the validated digit-pair count, 0 through 11.
43//!
44//! # Boundary behavior
45//!
46//! - Latitude 0 and `-0.0` are both northern, band N, on every path.
47//! - The UTM latitude interval is [-80, 84), matching GeoConvert: -80 exactly
48//! is UTM band C, 84 exactly is UPS north, and the last UTM latitude is one
49//! ulp below 84 (band X).
50//! - A coordinate exactly on an excluded upper grid edge is nudged inside by
51//! 2^-28 m before truncation, so it formats as the last cell of its own
52//! square.
53//! - Grid-zone-only references such as `"18T"` are rejected with
54//! [`ParseErrorKind::GridZoneOnly`], because a cell six degrees wide has no useful
55//! center.
56//!
57//! # Migrating from `geoconvert`
58//!
59//! | `geoconvert` 1.0 call | replacement |
60//! | --- | --- |
61//! | `LatLon::create(lat, lon)` | [`LatLon::new`] |
62//! | `latlon.to_mgrs(6)` | `latlon.to_mgrs(Precision::new(6)?)` |
63//! | `Mgrs::parse_str(s)` | `s.parse::<Mgrs>()` |
64//! | `mgrs.to_string()` | unchanged |
65//! | `mgrs.to_latlon()` | unchanged |
66//! | `UtmUps::create(z, north, e, n)` | [`UtmUps::utm`] or [`UtmUps::ups`] |
67//! | `utmups.to_mgrs(p)` | `utmups.to_mgrs(Precision::new(p)?)?` |
68//! | `Mgrs::create(...)` | removed, construct via `to_mgrs` or `parse` |
69//!
70//! The two new `?`s are deliberate: precision validation and the MGRS-extent
71//! check return errors a caller can print instead of panicking.
72//!
73//! ```
74//! use mgrs_convert::{Error, Precision, UtmUps};
75//! // Precision is validated at the edge.
76//! assert!(matches!(Precision::new(12), Err(Error::PrecisionOutOfRange { .. })));
77//! // The transfer ring converts to lat/lon but has no MGRS reference.
78//! let ring = UtmUps::utm(18, true, 50_000.0, 4_511_315.0)?;
79//! assert!(matches!(ring.to_mgrs(Precision::ONE_METER), Err(Error::OutsideMgrsExtent { .. })));
80//! # Ok::<(), mgrs_convert::Error>(())
81//! ```
82//!
83//! # Performance
84//!
85//! Linear per conversion, no allocation outside `Display`, no lookup table
86//! computed at runtime. The doctest below is the quadratic-regression tripwire:
87//! one hundred thousand round trips inside a generous fixed budget.
88//!
89//! ```
90//! use mgrs_convert::{LatLon, Precision};
91//! let start = std::time::Instant::now();
92//! let mut checksum = 0.0_f64;
93//! for i in 0..100_000_u32 {
94//! let lat = -80.0 + (f64::from(i) * 1.6377) % 160.0;
95//! let lon = -180.0 + (f64::from(i) * 3.9119) % 360.0;
96//! let p = LatLon::new(lat, lon)?;
97//! let back = p.to_utmups().to_latlon();
98//! checksum += back.lat();
99//! }
100//! assert!(checksum.is_finite());
101//! assert!(start.elapsed().as_secs() < 60, "quadratic regression tripwire");
102//! # Ok::<(), mgrs_convert::Error>(())
103//! ```
104
105#![forbid(unsafe_code)]
106#![deny(missing_docs)]
107#![deny(
108 clippy::unwrap_used,
109 clippy::expect_used,
110 clippy::panic,
111 clippy::indexing_slicing
112)]
113
114mod error;
115mod grid;
116mod latlon;
117mod math;
118mod mgrs;
119mod polar;
120mod precision;
121mod tm;
122mod utmups;
123
124pub use error::{Error, ParseErrorKind};
125pub use latlon::LatLon;
126pub use mgrs::Mgrs;
127pub use precision::Precision;
128pub use utmups::UtmUps;
129
130#[cfg(test)]
131mod tests {
132 use crate::math;
133
134 #[test]
135 fn geodetic_constants_pinned() {
136 // Invariant 9: the only two geodetic parameters in the crate.
137 assert_eq!(math::A, 6_378_137.0);
138 assert_eq!(math::F, 1.0 / 298.257_223_563);
139 }
140}