Places

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

#mgrs#utm#ups#grid#coordinates#military

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 error

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.

mgrs_from_utm(zone: int, hemisphere: str, easting: float, northing: float) → anyor an error

UTM 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 error

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.

mgrs_from_ups(pole: str, easting: float, northing: float) → anyor an error

UPS back to a position: { lat, lon }. pole is north or south.

mgrs_encode(lat: float, lon: float, precision: int) → stror an error

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.

mgrs_decode(text: str) → anyor an error

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.

mgrs_square(text: str) → anyor an error

The 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 error

A reference written with its groups apart, as maps print it: 33U XP 05004 44998.

mgrs_zone(lat: float, lon: float) → anyor an error

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.

Get it

$ quillo install mgrs               # mgrs.qwer into this folder$ quillo install mgrs --example     # and its example, mgrs.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/mgrs.qwer$ curl -O https://quillolang.org/plugins/files/mgrs.qwe

Then, in your program, all of it or only what you call:

main.qwe
use mgrs.qweruse mgrs_to_utm, mgrs_from_utm from mgrs      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

mgrs.qwe
// 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

mgrs.qwer
//! 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
48Useful? Like it, or add to its thread below.

Thread 0 posts

On MGRS and UTM: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.