MGRS and UTM
Military Grid Reference System and UTM both ways on WGS 84, by Krüger's series with no projection library: band letters, 100 km square letters and their repeats, Norway's wide zone 32V and Svalbard's odd zones, squares drawn from references, and UPS at the poles, where `coordinates` and UTM stop.
Version 1.0.0 · 9 functions · 1 crate
Functions
What use mgrs.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use mgrs_to_utm, mgrs_from_utm from mgrs (Quillo 0.6).
mgrs_to_utm(lat: float, lon: float) → anyor an errorA position in UTM: { zone, band, hemisphere, easting, northing, text }. Keeps Norway's zone 32V and Svalbard's 31X to 37X. Past 84°N or 80°S UTM stops, and that is an error value pointing to UPS.
mgrs_from_utm(zone: int, hemisphere: str, easting: float, northing: float) → anyor an errorUTM back to a position: { lat, lon }, from zone, hemisphere (N or S, or a band letter), easting and northing.
mgrs_to_ups(lat: float, lon: float) → anyor an errorA polar position in UPS: { pole, zone, easting, northing, text }, for north of 84° or south of 80°, where UTM stops. Zones are A and B round the south pole, Y and Z round the north.
mgrs_from_ups(pole: str, easting: float, northing: float) → anyor an errorUPS back to a position: { lat, lon }. pole is north or south.
mgrs_encode(lat: float, lon: float, precision: int) → stror an errorA position as an MGRS reference. precision is 0 to 5 digits each way: 5 is one metre, 0 the 100 km square. Digits are cut, not rounded, as MGRS wants. Works at the poles through UPS: ZAH0000000000 is the north pole.
mgrs_decode(text: str) → anyor an errorAn MGRS reference read: { lat, lon, zone, band, square, precision_m, easting, northing }, the position being the south-west corner of the square named. Spaces are fine. A square outside its band, or a zone that does not exist, is an error value.
mgrs_square(text: str) → anyor an errorThe middle of the square a reference names, and its corners: { center, south_west, north_east, size_m }, for drawing it on a map.
mgrs_format(text: str) → stror an errorA reference written with its groups apart, as maps print it: 33U XP 05004 44998.
mgrs_zone(lat: float, lon: float) → anyor an errorThe grid zone a position is in, with the zone's edges: { zone, band, gzd, exception, west, east, south, north }. exception names Norway or Svalbard where the normal 6° does not hold.
Get it
$ quillo install mgrs # mgrs.qwer into this folder$ quillo install mgrs --example # and its example, mgrs.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/mgrs.qwer$ curl -O https://quillolang.org/plugins/files/mgrs.qweThen, in your program, all of it or only what you call:
use mgrs.qweruse mgrs_to_utm, mgrs_from_utm from mgrs // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// Military grid references: positions to MGRS and back, the zones that break the 6° rule in// Norway and Svalbard, UTM, and UPS round the poles.//// quillo run mgrs.qwe// then open http://localhost:8080/encode?lat=48.24949&lon=16.4145, /decode?ref=33UXP0500444998,// /zones, /poles, /utm, /square and /refuseduse "mgrs.qwer"route GET /encode ? { lat: float, lon: float, precision: int = 5 } { let r = mgrs_encode(lat, lon, precision) if is_err(r) { reply 422 r.error } { mgrs: r, printed: mgrs_format(r)? }}route GET /decode ? { ref: str } { let r = mgrs_decode(ref) if is_err(r) { reply 422 r.error } r}// Digits are cut, not rounded, so a metre's difference from programs that round is expected.route GET /known = { vienna: mgrs_encode(48.24949, 16.4145, 5)?, monument: mgrs_encode(38.8895, -77.0352, 5)?, sydney: mgrs_encode(-33.8688, 151.2093, 5)?, arctic: mgrs_decode("24XWT783908")?, low_precision: mgrs_encode(48.24949, 16.4145, 1)?, square: mgrs_encode(48.24949, 16.4145, 0)?,}// Bergen is in the wide zone 32V, Svalbard's towns in odd zones only.route GET /zones = { bergen: mgrs_zone(60.39, 5.32)?, shetland: mgrs_zone(60.15, 1.15)?.gzd, svalbard: mgrs_zone(78.22, 15.65)?, west_svalbard: mgrs_zone(75.0, 8.0)?.gzd, ordinary: mgrs_zone(40.0, -74.0)?, bergen_ref: mgrs_encode(60.39, 5.32, 3)?, pole: mgrs_zone(89.0, 0.0)?.exception,}route GET /utm { let t = mgrs_to_utm(40.7128, -74.006)? let s = mgrs_to_utm(-33.8688, 151.2093)? { new_york: t, back: mgrs_from_utm(t.zone, t.hemisphere, t.easting, t.northing)?, sydney: s.text, sydney_back: mgrs_from_utm(s.zone, "H", s.easting, s.northing)?, }}route GET /poles { let n = mgrs_to_ups(85.0, 30.0)? { north: mgrs_encode(90.0, 0.0, 5)?, south: mgrs_encode(-90.0, 0.0, 5)?, ups: n, back: mgrs_from_ups(n.pole, n.easting, n.northing)?, south_edge: mgrs_encode(-85.0, -40.0, 3)?, south_back: mgrs_decode(mgrs_encode(-85.0, -40.0, 5)?)?, north_back: mgrs_decode(mgrs_encode(86.0, -120.0, 5)?)?, }}route GET /square = { box: mgrs_square("33UXP0544")?, round_trip: mgrs_decode(mgrs_encode(-12.5, 130.8, 5)?)?,}route GET /refused = { letters: mgrs_decode("hello").error, odd: mgrs_decode("33UXP123").error, no_zone: mgrs_decode("40XWT1234").error, svalbard: mgrs_decode("32XMH0000000000").error, row: mgrs_decode("33UAA0000000000").error, band: mgrs_decode("33CXP0000000000").error, lat: mgrs_encode(91.0, 0.0, 5).error, precision: mgrs_encode(10.0, 10.0, 7).error, utm_past: mgrs_to_utm(85.0, 0.0).error, ups_inside: mgrs_to_ups(50.0, 0.0).error,}$ quillo run mgrs.qwe$ curl 'localhost:8080/encode?lat=48.24949&lon=16.4145'$ curl 'localhost:8080/decode?ref=33UXP0500444998'$ curl 'localhost:8080/known'$ curl 'localhost:8080/zones'$ curl 'localhost:8080/utm'$ curl 'localhost:8080/poles'$ curl 'localhost:8080/square'$ curl 'localhost:8080/refused'Source
//! Military Grid Reference System and UTM both ways by Krüger series, band letters, the Norway and Svalbard zone exceptions, and UPS at the poles, on WGS 84.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};const A: f64 = 6378137.0;const F: f64 = 1.0 / 298.257223563;const K0: f64 = 0.9996;const E0: f64 = 500000.0;const UPS_K0: f64 = 0.994;const RAD: f64 = std::f64::consts::PI / 180.0;const BANDS: &[u8; 20] = b"CDEFGHJKLMNPQRSTUVWX";const ROWS: &[u8; 20] = b"ABCDEFGHJKLMNPQRSTUV";const COLS: [&[u8; 8]; 3] = [b"ABCDEFGH", b"JKLMNPQR", b"STUVWXYZ"];fn ecc() -> f64 { (F * (2.0 - F)).sqrt()}// Rectifying radius and series for the transverse Mercator: Krüger, to the fifth order in n.fn series() -> (f64, [f64; 5], [f64; 5]) { let n = F / (2.0 - F); let (n2, n3, n4, n5) = (n * n, n * n * n, n.powi(4), n.powi(5)); let a = A / (1.0 + n) * (1.0 + n2 / 4.0 + n4 / 64.0); let alpha = [ n / 2.0 - 2.0 * n2 / 3.0 + 5.0 * n3 / 16.0 + 41.0 * n4 / 180.0 - 127.0 * n5 / 288.0, 13.0 * n2 / 48.0 - 3.0 * n3 / 5.0 + 557.0 * n4 / 1440.0 + 281.0 * n5 / 630.0, 61.0 * n3 / 240.0 - 103.0 * n4 / 140.0 + 15061.0 * n5 / 26880.0, 49561.0 * n4 / 161280.0 - 179.0 * n5 / 168.0, 34729.0 * n5 / 80640.0, ]; let beta = [ n / 2.0 - 2.0 * n2 / 3.0 + 37.0 * n3 / 96.0 - n4 / 360.0 - 81.0 * n5 / 512.0, n2 / 48.0 + n3 / 15.0 - 437.0 * n4 / 1440.0 + 46.0 * n5 / 105.0, 17.0 * n3 / 480.0 - 37.0 * n4 / 840.0 - 209.0 * n5 / 4480.0, 4397.0 * n4 / 161280.0 - 11.0 * n5 / 504.0, 4583.0 * n5 / 161280.0, ]; (a, alpha, beta)}// The exceptions: south-west Norway has a wide zone 32, and Svalbard has no 32, 34 or 36.fn zone_of(lat: f64, lon: f64) -> i64 { let lon = if lon >= 180.0 { lon - 360.0 } else { lon }; if (56.0..64.0).contains(&lat) && (3.0..12.0).contains(&lon) { return 32; } if (72.0..=84.0).contains(&lat) { if (0.0..9.0).contains(&lon) { return 31; } if (9.0..21.0).contains(&lon) { return 33; } if (21.0..33.0).contains(&lon) { return 35; } if (33.0..42.0).contains(&lon) { return 37; } } (((lon + 180.0) / 6.0).floor() as i64 + 1).clamp(1, 60)}fn band_of(lat: f64) -> Option<u8> { if !(-80.0..=84.0).contains(&lat) { return None; } let i = (((lat + 80.0) / 8.0).floor() as usize).min(19); Some(BANDS[i])}fn central(zone: i64) -> f64 { (zone as f64 - 1.0) * 6.0 - 180.0 + 3.0}fn tm_forward(lat: f64, lon: f64, zone: i64) -> (f64, f64) { let (a, alpha, _) = series(); let e = ecc(); let phi = lat * RAD; let lam = ((lon - central(zone) + 180.0).rem_euclid(360.0) - 180.0) * RAD; let t = (phi.sin().atanh() - e * (e * phi.sin()).atanh()).sinh(); let xi0 = t.atan2(lam.cos()); let eta0 = (lam.sin() / (1.0 + t * t).sqrt()).atanh(); let (mut xi, mut eta) = (xi0, eta0); for (j, c) in alpha.iter().enumerate() { let m = 2.0 * (j + 1) as f64; xi += c * (m * xi0).sin() * (m * eta0).cosh(); eta += c * (m * xi0).cos() * (m * eta0).sinh(); } let east = E0 + K0 * a * eta; let north = K0 * a * xi + if lat < 0.0 { 10_000_000.0 } else { 0.0 }; (east, north)}fn tm_inverse(east: f64, north: f64, zone: i64, southern: bool) -> (f64, f64) { let (a, _, beta) = series(); let e = ecc(); let xi = (north - if southern { 10_000_000.0 } else { 0.0 }) / (K0 * a); let eta = (east - E0) / (K0 * a); let (mut x, mut y) = (xi, eta); for (j, c) in beta.iter().enumerate() { let m = 2.0 * (j + 1) as f64; x -= c * (m * xi).sin() * (m * eta).cosh(); y -= c * (m * xi).cos() * (m * eta).sinh(); } let chi = (x.sin() / y.cosh()).asin(); // The conformal latitude back to the geodetic one, by iteration. let mut phi = chi; for _ in 0..8 { phi = (chi.sin().atanh() + e * (e * phi.sin()).atanh()).tanh().asin(); } let lon = central(zone) + y.sinh().atan2(x.cos()) / RAD; (phi / RAD, (lon + 180.0).rem_euclid(360.0) - 180.0)}fn ups_scale() -> f64 { let e = ecc(); 2.0 * A * UPS_K0 / ((1.0 + e).powf(1.0 + e) * (1.0 - e).powf(1.0 - e)).sqrt()}fn ups_forward(lat: f64, lon: f64) -> (f64, f64) { let e = ecc(); let phi = lat.abs() * RAD; let s = phi.sin(); let t = ((std::f64::consts::FRAC_PI_4 - phi / 2.0).tan()) * ((1.0 + e * s) / (1.0 - e * s)).powf(e / 2.0); let rho = ups_scale() * t; let lam = lon * RAD; let x = 2_000_000.0 + rho * lam.sin(); let y = if lat >= 0.0 { 2_000_000.0 - rho * lam.cos() } else { 2_000_000.0 + rho * lam.cos() }; (x, y)}fn ups_inverse(east: f64, north: f64, northern: bool) -> (f64, f64) { let e = ecc(); let (x, y) = (east - 2_000_000.0, north - 2_000_000.0); let rho = x.hypot(y); let t = rho / ups_scale(); let mut phi = std::f64::consts::FRAC_PI_2 - 2.0 * t.atan(); for _ in 0..12 { let s = phi.sin(); phi = std::f64::consts::FRAC_PI_2 - 2.0 * (t * ((1.0 - e * s) / (1.0 + e * s)).powf(e / 2.0)).atan(); } let lon = if rho == 0.0 { 0.0 } else if northern { x.atan2(-y) } else { x.atan2(y) }; let lat = phi / RAD; (if northern { lat } else { -lat }, lon / RAD)}fn check(lat: f64, lon: f64) -> Result<(), String> { if !(-90.0..=90.0).contains(&lat) || !lat.is_finite() { return Err(format!("latitude is -90 to 90, not {}", lat)); } if !(-180.0..=360.0).contains(&lon) || !lon.is_finite() { return Err(format!("longitude is -180 to 180, not {}", lon)); } Ok(())}fn wrap(lon: f64) -> f64 { let l = (lon + 180.0).rem_euclid(360.0) - 180.0; if l == -180.0 && lon > 0.0 { 180.0 } else { l }}fn r(x: f64, p: i32) -> f64 { let m = 10f64.powi(p); (x * m).round() / m}/// A position in UTM: { zone, band, hemisphere, easting, northing, text }. Keeps Norway's zone 32V and Svalbard's 31X to 37X. Past 84°N or 80°S UTM stops, and that is an error value pointing to UPS.pub fn mgrs_to_utm(lat: f64, lon: f64) -> Result<Value, String> { check(lat, lon)?; let lon = wrap(lon); let band = band_of(lat).ok_or(format!("UTM stops at 84°N and 80°S, and {} is past that: use mgrs_to_ups", lat))?; let zone = zone_of(lat, lon); let (e, n) = tm_forward(lat, lon, zone); Ok(json!({ "zone": zone, "band": (band as char).to_string(), "hemisphere": if lat >= 0.0 { "N" } else { "S" }, "easting": r(e, 3), "northing": r(n, 3), "text": format!("{}{} {:.0} {:.0}", zone, band as char, e.floor(), n.floor()), }))}/// UTM back to a position: { lat, lon }, from zone, hemisphere (`N` or `S`, or a band letter), easting and northing.pub fn mgrs_from_utm(zone: i64, hemisphere: &str, easting: f64, northing: f64) -> Result<Value, String> { if !(1..=60).contains(&zone) { return Err(format!("a UTM zone is 1 to 60, not {}", zone)); } let h = hemisphere.trim().to_uppercase(); let south = match h.as_str() { "N" => false, "S" => true, b if b.len() == 1 && BANDS.contains(&b.as_bytes()[0]) => b.as_bytes()[0] < b'N', o => return Err(format!("`{}` is no hemisphere: N, S, or a band letter", o)), }; if !(100_000.0..=900_000.0).contains(&easting) { return Err(format!("an easting of {} is off any UTM zone: 100000 to 900000", easting)); } if !(0.0..=10_000_000.0).contains(&northing) { return Err(format!("a northing of {} is out of range", northing)); } let (lat, lon) = tm_inverse(easting, northing, zone, south); Ok(json!({ "lat": r(lat, 7), "lon": r(lon, 7) }))}/// A polar position in UPS: { pole, zone, easting, northing, text }, for north of 84° or south of 80°, where UTM stops. Zones are A and B round the south pole, Y and Z round the north.pub fn mgrs_to_ups(lat: f64, lon: f64) -> Result<Value, String> { check(lat, lon)?; if (-80.0..84.0).contains(&lat) { return Err(format!("UPS is for north of 84° and south of 80°, and {} is in UTM's range", lat)); } let lon = wrap(lon); let (e, n) = ups_forward(lat, lon); let zone = match (lat >= 0.0, e >= 2_000_000.0) { (true, false) => "Y", (true, true) => "Z", (false, false) => "A", (false, true) => "B", }; Ok(json!({ "pole": if lat >= 0.0 { "north" } else { "south" }, "zone": zone, "easting": r(e, 3), "northing": r(n, 3), "text": format!("{} {:.0} {:.0}", zone, e.floor(), n.floor()) }))}/// UPS back to a position: { lat, lon }. `pole` is north or south.pub fn mgrs_from_ups(pole: &str, easting: f64, northing: f64) -> Result<Value, String> { let northern = match pole.trim().to_lowercase().as_str() { "north" | "n" | "y" | "z" => true, "south" | "s" | "a" | "b" => false, o => return Err(format!("`{}` is no pole: north or south", o)), }; let (lat, lon) = ups_inverse(easting, northing, northern); Ok(json!({ "lat": r(lat, 7), "lon": r(lon, 7) }))}fn digits_of(v: f64, precision: usize) -> String { let m = (v.floor() as i64).rem_euclid(100_000); format!("{:05}", m)[..precision].to_string()}fn utm_letters(zone: i64, e: f64, n: f64) -> (u8, u8) { let set = ((zone - 1) % 3) as usize; let col = ((e / 100_000.0).floor() as i64 - 1).clamp(0, 7) as usize; let shift = if zone % 2 == 0 { 5 } else { 0 }; let row = (((n / 100_000.0).floor() as i64) + shift).rem_euclid(20) as usize; (COLS[set][col], ROWS[row])}// Geotrans's table: lowest second letter, false easting and northing, by first letter.fn ups_table(letter: u8) -> (u8, f64, f64) { match letter { b'A' => (b'J', 800_000.0, 800_000.0), b'B' => (b'A', 2_000_000.0, 800_000.0), b'Y' => (b'J', 800_000.0, 1_300_000.0), _ => (b'A', 2_000_000.0, 1_300_000.0), }}fn ups_letters(east: f64, north: f64, northern: bool) -> [u8; 3] { let first = if northern { if east >= 2e6 { b'Z' } else { b'Y' } } else if east >= 2e6 { b'B' } else { b'A' }; let (low, fe, fne) = ups_table(first); let mut third = b'A' + ((north - fne) / 100_000.0).floor() as u8; if third > b'H' { third += 1; } if third > b'N' { third += 1; } let mut second = low + ((east - fe) / 100_000.0).floor() as u8; if east < 2e6 { if second > b'L' { second += 3; } if second > b'U' { second += 2; } } else { if second > b'C' { second += 2; } if second > b'H' { second += 1; } if second > b'L' { second += 3; } } [first, second, third]}/// A position as an MGRS reference. `precision` is 0 to 5 digits each way: 5 is one metre, 0 the 100 km square. Digits are cut, not rounded, as MGRS wants. Works at the poles through UPS: `ZAH0000000000` is the north pole.pub fn mgrs_encode(lat: f64, lon: f64, precision: i64) -> Result<String, String> { check(lat, lon)?; if !(0..=5).contains(&precision) { return Err(format!("precision is 0 to 5, not {}", precision)); } let p = precision as usize; let lon = wrap(lon); if let Some(b) = band_of(lat) { let zone = zone_of(lat, lon); let (e, n) = tm_forward(lat, lon, zone); let (c, ro) = utm_letters(zone, e, n); return Ok(format!("{:02}{}{}{}{}{}", zone, b as char, c as char, ro as char, digits_of(e, p), digits_of(n, p))); } let (e, n) = ups_forward(lat, lon); let l = ups_letters(e, n, lat > 0.0); Ok(format!("{}{}{}{}{}", l[0] as char, l[1] as char, l[2] as char, digits_of(e, p), digits_of(n, p)))}struct Parsed { zone: Option<i64>, band: u8, col: u8, row: u8, east: f64, north: f64, precision: usize,}fn split_text(text: &str) -> Result<Parsed, String> { let t: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| c.to_ascii_uppercase()).collect(); let b = t.as_bytes(); let bad = || format!("`{}` is not an MGRS reference: like 33UXP0500444998", text); let digits = b.iter().take_while(|c| c.is_ascii_digit()).count(); if digits > 2 || b.len() < digits + 3 { return Err(bad()); } let zone = if digits == 0 { None } else { Some(t[..digits].parse::<i64>().map_err(|_| bad())?) }; let (band, col, row) = (b[digits], b[digits + 1], b[digits + 2]); if !band.is_ascii_uppercase() || !col.is_ascii_uppercase() || !row.is_ascii_uppercase() { return Err(bad()); } let rest = &t[digits + 3..]; if !rest.bytes().all(|c| c.is_ascii_digit()) { return Err(bad()); } if rest.len() % 2 != 0 || rest.len() > 10 { return Err(format!("`{}` must end with an even number of digits, at most ten", text)); } let p = rest.len() / 2; let scale = 10f64.powi(5 - p as i32); let part = |s: &str| if s.is_empty() { 0.0 } else { s.parse::<f64>().unwrap_or(0.0) * scale }; Ok(Parsed { zone, band, col, row, east: part(&rest[..p]), north: part(&rest[p..]), precision: p })}fn band_south(band: u8) -> f64 { let i = BANDS.iter().position(|b| *b == band).unwrap_or(0); -80.0 + 8.0 * i as f64}fn decode(p: &Parsed) -> Result<(f64, f64, f64, f64), String> { let Some(zone) = p.zone else { if !matches!(p.band, b'A' | b'B' | b'Y' | b'Z') { return Err(format!("no zone number, and `{}` is not a polar letter: A, B, Y or Z", p.band as char)); } let northern = p.band >= b'Y'; let (low, fe, fne) = ups_table(p.band); let cols: Vec<u8> = if p.band == b'A' || p.band == b'Y' { b"JKLPQRSTUXYZ".to_vec() } else { b"ABCFGHJKLPQR".to_vec() }; let _ = low; let ci = cols.iter().position(|c| *c == p.col).ok_or(format!("`{}` is no column of polar zone {}", p.col as char, p.band as char))?; let rows: &[u8] = if northern { b"ABCDEFGHJKLMNP" } else { b"ABCDEFGHJKLMNPQRSTUVWXYZ" }; let ri = rows.iter().position(|c| *c == p.row).ok_or(format!("`{}` is no row of polar zone {}", p.row as char, p.band as char))?; let (e, n) = (fe + ci as f64 * 100_000.0 + p.east, fne + ri as f64 * 100_000.0 + p.north); let (lat, lon) = ups_inverse(e, n, northern); if (northern && lat < 83.5) || (!northern && lat > -79.5) { return Err(format!("that square lies outside UPS's cap, at latitude {:.1}", lat)); } return Ok((lat, lon, e, n)); }; if !(1..=60).contains(&zone) { return Err(format!("a zone is 1 to 60, not {}", zone)); } if !BANDS.contains(&p.band) { return Err(format!("`{}` is not a band: C to X without I and O", p.band as char)); } if p.band == b'X' && matches!(zone, 32 | 34 | 36) { return Err(format!("zone {}X does not exist: Svalbard's zones are 31, 33, 35 and 37", zone)); } let set = ((zone - 1) % 3) as usize; let ci = COLS[set].iter().position(|c| *c == p.col).ok_or(format!("`{}` is no column letter of zone {}", p.col as char, zone))?; let ri = ROWS.iter().position(|c| *c == p.row).ok_or(format!("`{}` is no row letter", p.row as char))? as i64; let shift = if zone % 2 == 0 { 5 } else { 0 }; let e = (ci as f64 + 1.0) * 100_000.0 + p.east; let base = (ri - shift).rem_euclid(20) as f64 * 100_000.0; // Rows repeat every 2000 km: the band tells which round is meant. let south = p.band < b'N'; let lat0 = band_south(p.band); let floor = tm_forward(lat0.max(-80.0) + if south { 1e-9 } else { 0.0 }, central(zone), zone).1; let floor = floor - 110_000.0; let mut n = base + p.north; while n < floor { n += 2_000_000.0; } let (lat, lon) = tm_inverse(e, n, zone, south); let top = if p.band == b'X' { 84.0 } else { lat0 + 8.0 }; if lat < lat0 - 0.5 || lat > top + 0.5 { return Err(format!("square {}{} does not lie in band {}", p.col as char, p.row as char, p.band as char)); } Ok((lat, lon, e, n))}/// An MGRS reference read: { lat, lon, zone, band, square, precision_m, easting, northing }, the position being the south-west corner of the square named. Spaces are fine. A square outside its band, or a zone that does not exist, is an error value.pub fn mgrs_decode(text: &str) -> Result<Value, String> { let p = split_text(text)?; let (lat, lon, e, n) = decode(&p)?; Ok(json!({ "lat": r(lat, 7), "lon": r(lon, 7), "zone": p.zone, "band": (p.band as char).to_string(), "square": format!("{}{}", p.col as char, p.row as char), "precision_m": 10i64.pow(5 - p.precision as u32), "easting": e, "northing": n, }))}/// The middle of the square a reference names, and its corners: { center, south_west, north_east, size_m }, for drawing it on a map.pub fn mgrs_square(text: &str) -> Result<Value, String> { let p = split_text(text)?; let (lat, lon, ..) = decode(&p)?; let cell = 10f64.powi(5 - p.precision as i32); let up = Parsed { east: p.east + cell, north: p.north + cell, ..p }; let (lat2, lon2, ..) = decode(&up)?; let half = Parsed { east: up.east - cell / 2.0, north: up.north - cell / 2.0, ..up }; let (cl, co, ..) = decode(&half)?; Ok(json!({ "center": [r(cl, 7), r(co, 7)], "south_west": [r(lat, 7), r(lon, 7)], "north_east": [r(lat2, 7), r(lon2, 7)], "size_m": cell }))}/// A reference written with its groups apart, as maps print it: `33U XP 05004 44998`.pub fn mgrs_format(text: &str) -> Result<String, String> { let p = split_text(text)?; decode(&p)?; let t: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| c.to_ascii_uppercase()).collect(); let d = t.len() - p.precision * 2; let (head, nums) = t.split_at(d); let gzd = head.len() - 2; let mut o = format!("{} {}", &head[..gzd], &head[gzd..]); if p.precision > 0 { o.push_str(&format!(" {} {}", &nums[..p.precision], &nums[p.precision..])); } Ok(o)}/// The grid zone a position is in, with the zone's edges: { zone, band, gzd, exception, west, east, south, north }. `exception` names Norway or Svalbard where the normal 6° does not hold.pub fn mgrs_zone(lat: f64, lon: f64) -> Result<Value, String> { check(lat, lon)?; let lon = wrap(lon); let Some(b) = band_of(lat) else { return Ok(json!({ "zone": null, "band": if lat > 0.0 { "polar north" } else { "polar south" }, "gzd": null, "exception": "UPS" })); }; let z = zone_of(lat, lon); let normal = (((lon + 180.0) / 6.0).floor() as i64 + 1).clamp(1, 60); let south = band_south(b); let north = if b == b'X' { 84.0 } else { south + 8.0 }; let (mut w, mut e) = (central(z) - 3.0, central(z) + 3.0); let mut exception = Value::Null; if b == b'V' && z == 32 { w = 3.0; exception = json!("Norway"); } else if b == b'V' && z == 31 { e = 3.0; exception = json!("Norway"); } else if b == b'X' && matches!(z, 31 | 33 | 35 | 37) { (w, e) = match z { 31 => (0.0, 9.0), 33 => (9.0, 21.0), 35 => (21.0, 33.0), _ => (33.0, 42.0), }; exception = json!("Svalbard"); } let _ = normal; Ok(json!({ "zone": z, "band": (b as char).to_string(), "gzd": format!("{}{}", z, b as char), "exception": exception, "west": w, "east": e, "south": south, "north": north }))}Thread 0 posts
On MGRS and UTM: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.