#![deny(clippy::arithmetic_side_effects)]
use core::fmt;
use core::str::FromStr;
use crate::error::{Error, ParseErrorKind};
use crate::grid;
use crate::latlon::LatLon;
use crate::precision::Precision;
use crate::utmups::UtmUps;
const BAND_LETTERS: &str = "CDEFGHJKLMNPQRSTUVWX";
const ROW_LETTERS: &str = "ABCDEFGHJKLMNPQRSTUV";
const COL_SETS: [&str; 3] = ["STUVWXYZ", "ABCDEFGH", "JKLMNPQR"];
const UPS_COLS: [(u8, i64, &str); 4] = [
(b'Y', 13, "RSTUXYZ"),
(b'Z', 20, "ABCFGHJ"),
(b'A', 8, "JKLPQRSTUXYZ"),
(b'B', 20, "ABCFGHJKLPQR"),
];
const UPS_N_ROWS: (i64, &str) = (13, "ABCDEFGHJKLMNP");
const UPS_S_ROWS: (i64, &str) = (8, "ABCDEFGHJKLMNPQRSTUVWXYZ");
const BAND_ROW_WINDOW: [(i64, i64); 20] = [
(-90, -81), (-80, -72), (-71, -63), (-63, -54), (-54, -45), (-45, -36), (-36, -27), (-27, -18), (-18, -9), (-9, -1), (0, 8), (8, 17), (17, 26), (26, 35), (35, 44), (44, 53), (53, 62), (62, 70), (71, 79), (80, 94), ];
fn letter_at(table: &str, pos: i64) -> u8 {
usize::try_from(pos)
.ok()
.and_then(|i| table.as_bytes().get(i))
.copied()
.unwrap_or(b'?')
}
fn pos_of(table: &str, c: u8) -> Option<i64> {
table
.bytes()
.position(|b| b == c)
.and_then(|p| i64::try_from(p).ok())
}
fn is_mgrs_letter(c: u8) -> bool {
c.is_ascii_uppercase() && c != b'I' && c != b'O'
}
fn row_shift(zone: u8) -> i64 {
if zone.rem_euclid(2) == 0 {
5
} else {
0
}
}
fn band_window(ib: i64) -> (i64, i64) {
usize::try_from(ib.saturating_add(10))
.ok()
.and_then(|i| BAND_ROW_WINDOW.get(i))
.copied()
.unwrap_or((0, 0))
}
fn row_exception(cand: i64, ib: i64, col: i64) -> bool {
let sband = if ib >= 0 {
ib
} else {
ib.saturating_add(1).saturating_neg()
};
let srow = if cand >= 0 {
cand
} else {
cand.saturating_add(1).saturating_neg()
};
let c0 = col.saturating_sub(1); let scol = if c0 <= 3 {
c0
} else {
7_i64.saturating_sub(c0)
};
match (srow, sband) {
(70, 8) => scol >= 2,
(71, 7) => scol <= 2,
(79, 9) => scol >= 1,
(80, 8) => scol <= 1,
_ => false,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Mgrs {
zone: u8,
north: bool,
band: u8,
col: i64,
row: i64,
e_digits: i64,
n_digits: i64,
precision: Precision,
}
impl Mgrs {
pub fn precision(&self) -> Precision {
self.precision
}
pub fn to_utmups(&self) -> UtmUps {
let p = self.precision.get();
let e = grid::cell_center_axis(self.col, self.e_digits, p);
let n = grid::cell_center_axis(self.row, self.n_digits, p);
if self.zone == 0 {
UtmUps::raw(0, self.north, e, n)
} else {
UtmUps::raw(
self.zone,
self.north,
e,
grid::frame_northing(n, self.north),
)
}
}
pub fn to_latlon(&self) -> LatLon {
self.to_utmups().to_latlon()
}
fn square_letters(&self) -> (u8, u8) {
if self.zone == 0 {
let col = UPS_COLS
.iter()
.find(|(b, _, _)| *b == self.band)
.map(|&(_, base, table)| letter_at(table, self.col.saturating_sub(base)))
.unwrap_or(b'?');
let (rbase, rtable) = if self.north { UPS_N_ROWS } else { UPS_S_ROWS };
(col, letter_at(rtable, self.row.saturating_sub(rbase)))
} else {
let set = COL_SETS
.get(usize::from(self.zone.rem_euclid(3)))
.copied()
.unwrap_or("");
let col = letter_at(set, self.col.saturating_sub(1));
let row = letter_at(
ROW_LETTERS,
self.row.saturating_add(row_shift(self.zone)).rem_euclid(20),
);
(col, row)
}
}
}
impl fmt::Display for Mgrs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.zone > 0 {
write!(f, "{:02}", self.zone)?;
}
let (col, row) = self.square_letters();
write!(f, "{}{}{}", self.band as char, col as char, row as char)?;
let width = usize::from(self.precision.get());
if width > 0 {
write!(f, "{:0width$}", self.e_digits)?;
write!(f, "{:0width$}", self.n_digits)?;
}
Ok(())
}
}
pub(crate) fn from_latlon(ll: LatLon, precision: Precision) -> Mgrs {
let (zone, north, e, n) = ll.utmups_parts();
if zone == 0 {
let (e, n) = grid::ups_nudge(north, e, n);
make_ups(north, e, n, precision)
} else {
let (e, n) = grid::utm_nudge(north, e, n);
make_utm(zone, north, grid::band_index(ll.lat()), e, n, precision)
}
}
pub(crate) fn from_utmups(u: UtmUps, precision: Precision) -> Result<Mgrs, Error> {
let outside = Error::OutsideMgrsExtent {
easting: u.easting(),
northing: u.northing(),
};
if u.zone() == 0 {
let (e, n) = grid::ups_extent(u.is_north(), u.easting(), u.northing()).ok_or(outside)?;
Ok(make_ups(u.is_north(), e, n, precision))
} else {
let (e, n) = grid::utm_extent(u.is_north(), u.easting(), u.northing()).ok_or(outside)?;
let lat = UtmUps::raw(u.zone(), u.is_north(), e, n).latlon_parts().0;
Ok(make_utm(
u.zone(),
u.is_north(),
grid::band_index(lat),
e,
n,
precision,
))
}
}
fn make_utm(zone: u8, north: bool, ib: i64, e: f64, n_frame: f64, precision: Precision) -> Mgrs {
let (col, off_e) = grid::axis_fields(e);
let (row_frame, off_n) = grid::axis_fields(n_frame);
let row = row_frame.saturating_sub(if north { 0 } else { 100 });
let ib = if row < 0 { ib.min(-1) } else { ib.max(0) };
let band = letter_at(BAND_LETTERS, ib.saturating_add(10));
Mgrs {
zone,
north: row >= 0,
band,
col: col.clamp(1, 8),
row: row.clamp(-90, 94),
e_digits: grid::digits_at(off_e, precision.get()),
n_digits: grid::digits_at(off_n, precision.get()),
precision,
}
}
fn make_ups(north: bool, e: f64, n: f64, precision: Precision) -> Mgrs {
let (col, off_e) = grid::axis_fields(e);
let (row, off_n) = grid::axis_fields(n);
let (lo, hi) = if north { (13, 26) } else { (8, 31) };
let east_half = e >= 2_000_000.0;
let band = match (north, east_half) {
(true, true) => b'Z',
(true, false) => b'Y',
(false, true) => b'B',
(false, false) => b'A',
};
Mgrs {
zone: 0,
north,
band,
col: col.clamp(lo, hi),
row: row.clamp(lo, hi),
e_digits: grid::digits_at(off_e, precision.get()),
n_digits: grid::digits_at(off_n, precision.get()),
precision,
}
}
impl FromStr for Mgrs {
type Err = Error;
fn from_str(s: &str) -> Result<Mgrs, Error> {
parse_mgrs(s)
}
}
fn parse_mgrs(s: &str) -> Result<Mgrs, Error> {
use ParseErrorKind as K;
let t = s.trim_matches(|c: char| c.is_ascii_whitespace());
if t.is_empty() {
return Err(Error::Parse(K::Empty));
}
if !t.is_ascii() {
return Err(Error::Parse(K::NonAsciiOrWhitespace));
}
let bytes = t.as_bytes();
if bytes.iter().any(|b| b.is_ascii_whitespace()) {
return Err(Error::Parse(K::NonAsciiOrWhitespace));
}
let zone_len = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
if zone_len > 2 {
return Err(Error::Parse(K::BadZoneNumber));
}
let zone: u8 = {
let mut z: u8 = 0;
for b in bytes.iter().take(zone_len) {
z = z.saturating_mul(10).saturating_add(b.saturating_sub(b'0'));
}
if zone_len > 0 && !(1..=60).contains(&z) {
return Err(Error::Parse(K::BadZoneNumber));
}
z
};
let mut letters = bytes.iter().skip(zone_len);
let band = match letters.next() {
Some(b) => b.to_ascii_uppercase(),
None => return Err(Error::Parse(K::BadBandLetter)),
};
if zone > 0 {
if pos_of(BAND_LETTERS, band).is_none() {
return Err(Error::Parse(K::BadBandLetter));
}
} else if !matches!(band, b'A' | b'B' | b'Y' | b'Z') {
return Err(Error::Parse(K::BadBandLetter));
}
let (col_c, row_c) = match (letters.next(), letters.next()) {
(None, _) => return Err(Error::Parse(K::GridZoneOnly)),
(Some(_), None) => return Err(Error::Parse(K::BadSquareLetters)),
(Some(c), Some(r)) => (c.to_ascii_uppercase(), r.to_ascii_uppercase()),
};
if !col_c.is_ascii_alphabetic() || !row_c.is_ascii_alphabetic() {
return Err(Error::Parse(K::BadSquareLetters));
}
let digits = bytes.get(zone_len.saturating_add(3)..).unwrap_or(&[]);
if let Some(_bad) = digits.iter().find(|b| !b.is_ascii_digit()) {
return Err(Error::Parse(K::TrailingInput));
}
if digits.len() > 22 {
return Err(Error::Parse(K::TooManyDigits));
}
if digits.len().rem_euclid(2) != 0 {
return Err(Error::Parse(K::OddDigitCount));
}
let half = digits.len().div_euclid(2);
let precision = Precision::new(half as u8).map_err(|_| Error::Parse(K::TooManyDigits))?;
let accumulate = |ds: &[u8]| -> Result<i64, Error> {
ds.iter()
.try_fold(0_i64, |acc, b| {
acc.checked_mul(10)?
.checked_add(i64::from(b.saturating_sub(b'0')))
})
.ok_or(Error::Parse(K::TooManyDigits))
};
let e_digits = accumulate(digits.get(..half).unwrap_or(&[]))?;
let n_digits = accumulate(digits.get(half..).unwrap_or(&[]))?;
if zone > 0 {
let set = COL_SETS
.get(usize::from(zone.rem_euclid(3)))
.copied()
.unwrap_or("");
let col = match pos_of(set, col_c) {
Some(p) => p.saturating_add(1),
None if is_mgrs_letter(col_c) => {
return Err(Error::Parse(K::SquareZoneMismatch));
}
None => return Err(Error::Parse(K::BadSquareLetters)),
};
let row_pos = match pos_of(ROW_LETTERS, row_c) {
Some(p) => p,
None => return Err(Error::Parse(K::BadSquareLetters)),
};
let ib = pos_of(BAND_LETTERS, band)
.map(|p| p.saturating_sub(10))
.unwrap_or(0);
let residue = row_pos.saturating_sub(row_shift(zone)).rem_euclid(20);
let (minrow, maxrow) = band_window(ib);
let start = minrow.saturating_sub(1);
let row = start.saturating_add(residue.saturating_sub(start).rem_euclid(20));
let in_window = row >= minrow && row <= maxrow;
if !in_window && !row_exception(row, ib, col) {
return Err(Error::Parse(K::SquareBandMismatch));
}
Ok(Mgrs {
zone,
north: ib >= 0,
band,
col,
row,
e_digits,
n_digits,
precision,
})
} else {
let north = matches!(band, b'Y' | b'Z');
let col = match UPS_COLS
.iter()
.find(|(b, _, _)| *b == band)
.and_then(|&(_, base, table)| pos_of(table, col_c).map(|p| base.saturating_add(p)))
{
Some(c) => c,
None if is_mgrs_letter(col_c) => {
return Err(Error::Parse(K::SquareZoneMismatch));
}
None => return Err(Error::Parse(K::BadSquareLetters)),
};
let (rbase, rtable) = if north { UPS_N_ROWS } else { UPS_S_ROWS };
let row = match pos_of(rtable, row_c) {
Some(p) => rbase.saturating_add(p),
None if is_mgrs_letter(row_c) => {
return Err(Error::Parse(K::SquareZoneMismatch));
}
None => return Err(Error::Parse(K::BadSquareLetters)),
};
Ok(Mgrs {
zone: 0,
north,
band,
col,
row,
e_digits,
n_digits,
precision,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letter_tables_pinned() {
assert_eq!(BAND_LETTERS.len(), 20);
assert_eq!(ROW_LETTERS.len(), 20);
for set in COL_SETS {
assert_eq!(set.len(), 8);
}
assert_eq!(UPS_N_ROWS.1.len(), 14);
assert_eq!(UPS_S_ROWS.1.len(), 24);
let widths: Vec<usize> = UPS_COLS.iter().map(|(_, _, t)| t.len()).collect();
assert_eq!(widths, vec![7, 7, 12, 12]);
}
#[test]
fn band_windows_cover_the_grid_contiguously() {
for pair in BAND_ROW_WINDOW.windows(2) {
if let [(_, hi), (lo2, _)] = pair {
assert!(*lo2 == *hi || *lo2 == hi.saturating_add(1));
}
}
assert_eq!(BAND_ROW_WINDOW.first(), Some(&(-90, -81)));
assert_eq!(BAND_ROW_WINDOW.last(), Some(&(80, 94)));
}
}