geoconvert/coords/
mgrs.rs

1use std::{fmt::Display, str::FromStr};
2
3use lazy_static::lazy_static;
4use num::Integer;
5
6use crate::{Error, utm::{zonespec::{MINUTMZONE, MAXUTMZONE, UPS, self}, UtmUps}, utility::{dms, GeoMath}, ThisOrThat, latlon::LatLon};
7
8const HEMISPHERES: &str = "SN";
9const UTMCOLS: &[&str] = &["ABCDEFGH", "JKLMNPQR", "STUVWXYZ"];
10const UTMROW: &str = "ABCDEFGHJKLMNPQRSTUV";
11const UPSCOLS: &[&str] = &["JKLPQRSTUXYZ", "ABCFGHJKLPQR", "RSTUXYZ", "ABCFGHJ"];
12const UPSROWS: &[&str] = &["ABCDEFGHJKLMNPQRSTUVWXYZ", "ABCDEFGHJKLMNP"];
13const LATBAND: &str = "CDEFGHJKLMNPQRSTUVWX";
14const UPSBAND: &str = "ABYZ";
15const DIGITS: &str = "0123456789";
16
17pub(crate) const TILE: i32= 100_000;
18pub(crate) const MINUTMCOL: i32= 1;
19pub(crate) const MAXUTMCOL: i32= 9;
20pub(crate) const MINUTM_S_ROW: i32= 10;
21pub(crate) const MAXUTM_S_ROW: i32= 100;
22pub(crate) const MINUTM_N_ROW: i32= 0;
23pub(crate) const MAXUTM_N_ROW: i32= 95;
24pub(crate) const MINUPS_S_IND: i32= 8;
25pub(crate) const MAXUPS_S_IND: i32= 32;
26pub(crate) const MINUPS_N_IND: i32= 13;
27pub(crate) const MAXUPS_N_IND: i32= 27;
28pub(crate) const UPSEASTING: i32= 20;
29pub(crate) const UTMEASTING: i32= 5;
30pub(crate) const UTM_N_SHIFT: i32= (MAXUTM_S_ROW - MINUTM_N_ROW) * TILE;
31
32const MIN_EASTING: [i32; 4] = [
33    MINUPS_S_IND,
34    MINUPS_N_IND,
35    MINUTMCOL,
36    MINUTMCOL,
37];
38
39const MAX_EASTING: [i32; 4] = [
40    MAXUPS_S_IND,
41    MAXUPS_N_IND,
42    MAXUTMCOL,
43    MAXUTMCOL,
44];
45
46const MIN_NORTHING: [i32; 4] = [
47    MINUPS_S_IND,
48    MINUPS_N_IND,
49    MINUTM_S_ROW,
50    MINUTM_S_ROW - MAXUTM_S_ROW - MINUTM_N_ROW,
51];
52
53const MAX_NORTHING: [i32; 4] = [
54    MAXUPS_S_IND,
55    MAXUPS_N_IND,
56    MAXUTM_N_ROW + MAXUTM_S_ROW - MINUTM_N_ROW,
57    MAXUTM_N_ROW,
58];
59
60pub(crate) const BASE: i32= 10;
61pub(crate) const UTM_ROW_PERIOD: i32 = 20;
62pub(crate) const UTM_EVEN_ROW_SHIFT: i32= 5;
63pub(crate) const MAX_PRECISION: i32= 5 + 6;
64pub(crate) const MULT: i32= 1_000_000;
65
66/// Representation of a WGS84 
67/// [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System)
68/// point. Stored internally as a [`UtmUps`] point with a precision.
69#[derive(Clone, Copy, Debug)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71pub struct Mgrs {
72    #[cfg_attr(feature = "serde", serde(flatten))]
73    pub(crate) utm: UtmUps,
74    pub(crate) precision: i32,
75}
76
77impl Mgrs {
78    /// Tries to create a MGRS point from its constituent parts. Validates the
79    /// arguments to ensure a valid MGRS point can be created. You most likely
80    /// want to instantiate this via [`parse_str`](#method.parse_str) or [`UtmUps`] instead
81    /// of manually specifying the values.
82    /// 
83    /// # Errors
84    /// 
85    /// Returns [`Error::InvalidMgrs`] if the position is invalid.
86    /// Returns [`Error::InvalidPrecision`] if the precision is not in range `[1, 11]`.
87    /// 
88    /// # Usage
89    /// 
90    /// ```
91    /// use geoconvert::Mgrs;
92    /// 
93    /// let coord = Mgrs::create(18, true, 585664.121, 4511315.422, 6);
94    /// 
95    /// assert!(coord.is_ok());
96    /// 
97    /// let coord = coord.unwrap();
98    /// 
99    /// assert_eq!(coord.zone(), 18);
100    /// assert_eq!(coord.is_north(), true);
101    /// assert!((coord.easting() - 585664.121).abs() < 1e-3);
102    /// assert!((coord.northing() - 4511315.422).abs() < 1e-3);
103    /// assert_eq!(coord.precision(), 6);
104    /// 
105    /// let invalid_coord_zone_neg = Mgrs::create(-10, true, 585664.121, 4511315.422, 6);
106    /// assert!(invalid_coord_zone_neg.is_err());
107    /// 
108    /// let invalid_coord_zone_too_big = Mgrs::create(70, true, 585664.121, 4511315.422, 6);
109    /// assert!(invalid_coord_zone_too_big.is_err());
110    /// ```
111    pub fn create(zone: i32, northp: bool, easting: f64, northing: f64, precision: i32) -> Result<Mgrs, Error> {
112        // Make sure zone is a valid value
113        if !(zonespec::MINZONE..=zonespec::MAXZONE).contains(&zone) {
114            return Err(Error::InvalidZone(zone));
115        }
116
117        let utmp = zone != zonespec::UPS;
118
119        check_coords(utmp, northp, easting, northing)?;
120
121        Ok(Mgrs {
122            utm: UtmUps::new(zone, northp, easting, northing),
123            precision,
124        })
125    }
126
127    /// Returns whether the MGRS is stored as UTM or UPS.
128    /// 
129    /// # Example
130    /// ```
131    /// use geoconvert::Mgrs;
132    /// 
133    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
134    /// assert_eq!(coord.is_utm(), true);
135    /// ```
136    #[inline]
137    pub fn is_utm(&self) -> bool {
138        self.utm.zone != UPS
139    }
140
141    /// Returns the UTM zone.
142    /// 
143    /// # Example
144    /// ```
145    /// use geoconvert::Mgrs;
146    /// 
147    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
148    /// assert_eq!(coord.zone(), 18);
149    /// ```
150    #[inline]
151    pub fn zone(&self) -> i32 {
152        self.utm.zone
153    }
154
155    /// Returns whether the coordinate is in the northern hemisphere.
156    /// 
157    /// # Example
158    /// ```
159    /// use geoconvert::Mgrs;
160    /// 
161    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
162    /// assert_eq!(coord.is_north(), true);
163    /// ```
164    #[inline]
165    pub fn is_north(&self) -> bool {
166        self.utm.northp
167    }
168
169    /// Returns the UTM easting.
170    /// 
171    /// # Example
172    /// ```
173    /// use geoconvert::Mgrs;
174    /// 
175    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
176    /// assert!((coord.easting() - 585664.15).abs() < 1e-2);
177    /// ```
178    #[inline]
179    pub fn easting(&self) -> f64 {
180        self.utm.easting
181    }
182
183    /// Returns the UTM northing.
184    /// 
185    /// # Example
186    /// ```
187    /// use geoconvert::Mgrs;
188    /// 
189    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
190    /// assert!((coord.northing() - 4511315.45).abs() < 1e-2);
191    /// ```
192    #[inline]
193    pub fn northing(&self) -> f64 {
194        self.utm.northing
195    }
196
197    /// Returns the current precision for outputting to a string.
198    /// 
199    /// # Example
200    /// ```
201    /// use geoconvert::Mgrs;
202    /// 
203    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
204    /// assert_eq!(coord.precision(), 6);
205    /// ```
206    #[inline]
207    pub fn precision(&self) -> i32 {
208        self.precision
209    }
210
211    /// Set the precision.
212    /// 
213    /// Must be in range `[1, 11]`.
214    /// 
215    /// # Errors
216    /// 
217    /// * [`Error::InvalidPrecision`]: `precision` is not in the valid range
218    /// 
219    /// # Example
220    /// ```
221    /// use geoconvert::Mgrs;
222    /// 
223    /// let mut coord = Mgrs::parse_str("18TWL856641113154").unwrap();
224    /// coord.set_precision(7);
225    /// 
226    /// assert_eq!(coord.precision(), 7);
227    /// ```
228    #[inline]
229    pub fn set_precision(&mut self, precision: i32) -> Result<(), Error> {
230        if !(1..=11).contains(&precision) {
231            return Err(Error::InvalidPrecision(precision));
232        }
233
234        self.precision = precision;
235        Ok(())
236    }
237
238    /// Parses a string as MGRS. Assumes the string is _only_ composed of
239    /// the MGRS coordinate (e.g. no preceding/trailing whitespace) and there
240    /// are no spaces in the string. Example valid strings:
241    /// 
242    /// * `27UXQ0314512982`
243    /// * `YXL6143481146`
244    /// 
245    /// # Errors
246    /// 
247    /// * [`Error::InvalidMgrs`]: the string couldn't be parsed to a valid MGRS coordinate.
248    pub fn parse_str(mgrs_str: &str) -> Result<Mgrs, Error> {
249        Self::from_str(mgrs_str)
250    }
251
252    /// Converts from [`LatLon`] to [`Mgrs`]
253    /// 
254    /// # Usage
255    /// 
256    /// ```
257    /// use geoconvert::{LatLon, Mgrs};
258    /// 
259    /// let coord = LatLon::create(40.748333, -73.985278).unwrap();
260    /// let coord_mgrs = Mgrs::parse_str("18TWL856641113154").unwrap();
261    /// 
262    /// let converted = LatLon::from_mgrs(&coord_mgrs);
263    /// 
264    /// // Check if the converted coordinate is accurate to 6 decimals (same as reference)
265    /// assert!((converted.latitude() - coord.latitude()).abs() < 1e-6);
266    /// assert!((converted.longitude() - coord.longitude()).abs() < 1e-6);
267    /// ```
268    pub fn from_latlon(value: &LatLon, precision: i32) -> Mgrs {
269        Mgrs {
270            utm: UtmUps::from_latlon(value),
271            precision,
272        }
273    }
274
275    /// Converts from [`Mgrs`] to [`LatLon`]
276    /// 
277    /// # Usage
278    /// 
279    /// ```
280    /// use geoconvert::{LatLon, Mgrs};
281    /// 
282    /// let coord = LatLon::create(40.748333, -73.985278).unwrap();
283    /// 
284    /// let converted = coord.to_mgrs(6);
285    /// 
286    /// assert_eq!(converted.to_string(), "18TWL856641113154");
287    /// ```
288    pub fn to_latlon(&self) -> LatLon {
289        self.utm.to_latlon()
290    }
291
292    
293    /// Converts from [`UtmUps`] to [`Mgrs`]
294    /// 
295    /// # Usage
296    /// 
297    /// ```
298    /// use geoconvert::{Mgrs, UtmUps};
299    /// 
300    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
301    /// let coord_utm = UtmUps::create(18, true, 585664.15, 4511315.45).unwrap();
302    /// 
303    /// let converted = Mgrs::from_utmups(&coord_utm, 6);
304    /// 
305    /// // Check if the converted coordinate is accurate to 6 decimals (same as reference)
306    /// assert_eq!(coord.zone(), converted.zone());
307    /// assert_eq!(coord.is_north(), converted.is_north());
308    /// assert!((coord.easting() - converted.easting()).abs() < 1e-2);
309    /// assert!((coord.northing() - converted.northing()).abs() < 1e-2);
310    /// assert_eq!(coord.precision(), converted.precision());
311    /// ```
312    pub fn from_utmups(value: &UtmUps, precision: i32) -> Mgrs {
313        Mgrs {
314            utm: *value,
315            precision,
316        }
317    }
318
319    /// Converts from [`Mgrs`] to [`UtmUps`]
320    /// 
321    /// # Usage
322    /// 
323    /// ```
324    /// use geoconvert::{Mgrs, UtmUps};
325    /// 
326    /// let coord = Mgrs::parse_str("18TWL856641113154").unwrap();
327    /// let coord_utm = UtmUps::create(18, true, 585664.15, 4511315.45).unwrap();
328    /// 
329    /// let converted = coord.to_utmups();
330    /// 
331    /// // Check if the converted coordinate is accurate to 6 decimals (same as reference)
332    /// assert_eq!(coord_utm.zone(), converted.zone());
333    /// assert_eq!(coord_utm.is_north(), converted.is_north());
334    /// assert!((coord_utm.easting() - converted.easting()).abs() < 1e-2);
335    /// assert!((coord_utm.northing() - converted.northing()).abs() < 1e-2);
336    /// ```
337    pub fn to_utmups(&self) -> UtmUps {
338        self.utm
339    }
340}
341
342fn utm_row(band_idx: i32, col_idx: i32, row_idx: i32) -> i32 {
343    let c = 100.0 * (8.0 * f64::from(band_idx) + 4.0) / f64::from(dms::QD);
344    let northp = band_idx >= 0;
345    // These are safe bounds on the rows
346    //  band_idx  minrow maxrow
347    //   -10      -90    -81
348    //    -9      -80    -72
349    //    -8      -71    -63
350    //    -7      -63    -54
351    //    -6      -54    -45
352    //    -5      -45    -36
353    //    -4      -36    -27
354    //    -3      -27    -18
355    //    -2      -18     -9
356    //    -1       -9     -1
357    //     0        0      8
358    //     1        8     17
359    //     2       17     26
360    //     3       26     35
361    //     4       35     44
362    //     5       44     53
363    //     6       53     62
364    //     7       62     70
365    //     8       71     79
366    //     9       80     94
367
368    let min_row = if band_idx > -10 {
369        (c - 4.3 - 0.1 * f64::from(u8::from(northp))).floor() as i32
370    } else {
371        -90
372    };
373
374    let max_row = if band_idx < 9 {
375        (c + 4.4 - 0.1 * f64::from(u8::from(northp))).floor() as i32
376    } else {
377        94
378    };
379
380    let base_row = (min_row + max_row) / 2 - UTM_ROW_PERIOD / 2;
381    // Offset row_idx by the multiple of UTM_ROW_PERIOD which brings it as close as
382    // possible to the center of the latitude band, (min_row + max_row) / 2.
383    // (Add MAXUTM_S_ROW = 5 * UTM_ROW_PERIOD to ensure operand is positive.0)
384    let mut row_idx = (row_idx - base_row + MAXUTM_S_ROW) % UTM_ROW_PERIOD + base_row;
385    
386    if !(row_idx >= min_row && row_idx <= max_row) {
387        // Outside the safe bounds, so need to check...
388        // Northing = 71e5 and 80e5 intersect band boundaries
389        //   y = 71e5 in scol = 2 (x = [3e5,4e5] and x = [6e5,7e5])
390        //   y = 80e5 in scol = 1 (x = [2e5,3e5] and x = [7e5,8e5])
391        // This holds for all the ellipsoids given in NGA.SIG.0012_2.0.0_UTMUPS.
392        // The following deals with these special cases.
393
394        // Fold [-10,-1] -> [9,0]
395        let safe_band = (band_idx >= 0).ternary(band_idx, -band_idx - 1);
396        // Fold [-90,-1] -> [89,0]
397        let safe_row = (row_idx >= 0).ternary(row_idx, -row_idx - 1);
398        // Fold [4,7] -> [3,0]
399        let safe_col = (col_idx < 4).ternary(col_idx, -col_idx + 7);
400
401        if !(
402            (safe_row == 70 && safe_band == 8 && safe_col >= 2) ||
403            (safe_row == 71 && safe_band == 7 && safe_col <= 2) ||
404            (safe_row == 79 && safe_band == 9 && safe_col >= 1) ||
405            (safe_row == 80 && safe_band == 8 && safe_col <= 1)
406        ) {
407            row_idx = MAXUTM_S_ROW;
408        }
409    }
410
411    row_idx
412}
413
414impl FromStr for Mgrs {
415    type Err = Error;
416
417    #[allow(clippy::too_many_lines)]
418    fn from_str(s: &str) -> Result<Self, Self::Err> {
419        let value = s.to_ascii_uppercase();
420        let mut p = 0;
421        let len = value.len();
422        if !value.is_ascii() {
423            return Err(Error::InvalidMgrs("String contains unicode characters".to_string()))
424        }
425        let chars = value.as_bytes();
426
427        if len >= 3 && value.starts_with("INV") {
428            return Err(Error::InvalidMgrs("Starts with 'INV'".to_string()))
429        }
430
431        let mut zone = 0i32;
432        while p < len {
433            // if let Some(i) = DIGITS_MAP.get(&(chars[p] as char)) {
434            if (chars[p] as char).is_ascii_digit() {
435                zone = 10 * zone + i32::from(chars[p] - b'0');
436                p += 1;
437            } else {
438                break;
439            }
440        }
441        // Check if zone is within valid range
442        if p > 0 && !(MINUTMZONE..=MAXUTMZONE).contains(&zone) {
443            return Err(Error::InvalidMgrs(format!("Zone {zone} not in [1,60]")));
444        }
445
446        if p > 2 {
447            return Err(Error::InvalidMgrs(format!("More than 2 digits at start of MGRS {}", &value[..p])));
448        }
449
450        if len - p < 1 {
451            return Err(Error::InvalidMgrs(format!("Too short: {value}")));
452        }
453
454        let utmp = zone != UPS;
455        let zonem = zone - 1;
456
457        let cur_char = chars[p];
458        #[allow(clippy::collapsible_else_if)]
459        let mut band_idx = if utmp {
460            // First check if it's a valid latband
461            if (b'C'..=b'X').contains(&cur_char) && cur_char != b'I' && cur_char != b'O' {
462                // Then convert to index
463                // First make it relative to C
464                let idx = cur_char - b'C';
465                // Decrement if it's past H to account for missing I
466                let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
467                // Decrement if it's past N to account for missing O
468                let idx = (cur_char > b'N').ternary_lazy(|| idx - 1, || idx);
469                i32::from(idx)
470            } else {
471                -1
472            }
473        } else {
474            if cur_char == b'A' {
475                0
476            } else if cur_char == b'B' {
477                1
478            } else if cur_char == b'Y' {
479                2
480            } else if cur_char == b'Z' {
481                3
482            } else {
483                -1
484            }
485        };
486
487        if band_idx == -1 {
488            let band = utmp.ternary(LATBAND, UPSBAND);
489            let label = utmp.ternary("UTM", "UPS");
490            return Err(Error::InvalidMgrs(format!("Band letter {} not in {label} set {band}", chars[p] as char)));
491        }
492
493        p += 1;
494
495        let northp = band_idx >= utmp.ternary(10, 2);
496
497        if p == len { // Grid zone only (ignore centerp)
498            // Approx length of a degree of meridian arc in units of tile
499            let deg = (f64::from(UTM_N_SHIFT)) / f64::from(dms::QD * TILE);
500            let (x, y) = if utmp {
501                // Pick central meridian except for 31V
502                let x = f64::from(TILE) * (zone == 31 && band_idx == 17).ternary(4.0, 5.0);
503                // TODO: continue from here
504                let y_add = northp.ternary(0.0, f64::from(UTM_N_SHIFT));
505                let y = (8.0 * (f64::from(band_idx) - 9.5) * deg + 0.5).floor() * f64::from(TILE) + y_add;
506
507                (x, y)
508            } else {
509                let x_cond = band_idx.is_odd().ternary(1.0, -1.0);
510                let x = (x_cond * (4.0 * deg + 0.5).floor() + f64::from(UPSEASTING)) * f64::from(TILE);
511                let y = f64::from(UPSEASTING * TILE);
512                (x, y)
513            };
514
515            return Ok(Mgrs {
516                utm: UtmUps::new(zone, northp, x, y),
517                precision: -1
518            })
519        } else if len - p < 2 {
520            return Err(Error::InvalidMgrs(format!("Missing row letter in {value}")));
521        }
522
523        let cur_char = chars[p];
524        // More efficient than find()
525        let mut col_idx = if utmp {
526            match zonem % 3 {
527                0 => {
528                    if (b'A'..=b'H').contains(&cur_char) {
529                        i32::from(cur_char - b'A')
530                    } else {
531                        -1
532                    }
533                }
534                1 => {
535                    if (b'J'..=b'R').contains(&cur_char) && cur_char != b'O' {
536                        if cur_char < b'O' {
537                            i32::from(cur_char - b'J')
538                        } else {
539                            i32::from(cur_char - b'J' - 1)
540                        }
541                    } else {
542                        -1
543                    }
544                } 
545                2 => {
546                    if (b'S'..=b'Z').contains(&cur_char) {
547                        i32::from(cur_char - b'S')
548                    } else {
549                        -1
550                    }
551                }
552                _ => unreachable!()
553            }
554        } else {
555            // &["JKLPQRSTUXYZ", "ABCFGHJKLPQR", "RSTUXYZ", "ABCFGHJ"]
556            match band_idx {
557                // JKLPQRSTUXYZ
558                0 => {
559                    if (b'J'..=b'Z').contains(&cur_char) && !(b'M'..=b'O').contains(&cur_char) && cur_char != b'V' && cur_char != b'W' {
560                        let idx = cur_char - b'J';
561                        let idx = (cur_char > b'L').ternary_lazy(|| idx - 3, || idx);
562                        let idx = (cur_char > b'U').ternary_lazy(|| idx - 2, || idx);
563                        i32::from(idx)
564                    } else {
565                        -1
566                    }
567                }
568                // ABCFGHJKLPQR
569                1 => {
570                    if  (b'A'..=b'R').contains(&cur_char) && 
571                        cur_char != b'D' &&
572                        cur_char != b'E' &&
573                        cur_char != b'I' &&
574                        !(b'M'..=b'O').contains(&cur_char)
575                    {
576                        let idx = cur_char - b'A';
577                        let idx = (cur_char > b'C').ternary_lazy(|| idx - 2, || idx);
578                        let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
579                        let idx = (cur_char > b'L').ternary_lazy(|| idx - 3, || idx);
580                        i32::from(idx)
581                    } else {
582                        -1
583                    }
584                }
585                // RSTUXYZ
586                2 => {
587                    if  (b'R'..=b'Z').contains(&cur_char) && cur_char != b'V' && cur_char != b'W' {
588                        let idx = cur_char - b'R';
589                        let idx = (cur_char > b'U').ternary_lazy(|| idx - 2, || idx);
590                        i32::from(idx)
591                    } else {
592                        -1
593                    }
594                }
595                // ABCFGHJ
596                3 => {
597                    if  (b'A'..=b'J').contains(&cur_char) && 
598                        cur_char != b'D' &&
599                        cur_char != b'E' &&
600                        cur_char != b'I'
601                    {
602                        let idx = cur_char - b'A';
603                        let idx = (cur_char > b'C').ternary_lazy(|| idx - 2, || idx);
604                        let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
605                        i32::from(idx)
606                    } else {
607                        -1
608                    }
609                }
610                _ => unreachable!()
611            }
612        };
613
614        if col_idx == -1 {
615            #[allow(clippy::cast_sign_loss)]
616            let col = utmp.ternary_lazy(|| UTMCOLS[(zonem % 3) as usize], || UPSCOLS[band_idx as usize]);
617            let label = if utmp { format!("zone {}", &value[..p-1]) } else { format!("UPS band {}", &value[p-1..p]) };
618            return Err(Error::InvalidMgrs(format!("Column letter {} not in {label} set {col}", &value[p..=p])));
619        }
620
621        p += 1;
622
623        let cur_char = chars[p];
624        // More efficient than find()
625        let mut row_idx = if utmp {
626            // "ABCDEFGHJKLMNPQRSTUV"
627            // First check if it's a valid latband
628            if (b'A'..=b'V').contains(&cur_char) && cur_char != b'I' && cur_char != b'O' {
629                // Then convert to index
630                // First make it relative to A
631                let idx = cur_char - b'A';
632                // Decrement if it's past H to account for missing I
633                let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
634                // Decrement if it's past N to account for missing O
635                let idx = (cur_char > b'N').ternary_lazy(|| idx - 1, || idx);
636                i32::from(idx)
637            } else {
638                -1
639            }
640        } else {
641            // &["ABCDEFGHJKLMNPQRSTUVWXYZ", "ABCDEFGHJKLMNP"]
642            #[allow(clippy::collapsible_else_if)]
643            if northp {
644                if (b'A'..=b'P').contains(&cur_char) && cur_char != b'I' && cur_char != b'O' {
645                    // Then convert to index
646                    // First make it relative to A
647                    let idx = cur_char - b'A';
648                    // Decrement if it's past H to account for missing I
649                    let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
650                    // Decrement if it's past N to account for missing O
651                    let idx = (cur_char > b'N').ternary_lazy(|| idx - 1, || idx);
652                    i32::from(idx)
653                } else {
654                    -1
655                }
656            } else {
657                if cur_char.is_ascii_uppercase() && cur_char != b'I' && cur_char != b'O' {
658                    // Then convert to index
659                    // First make it relative to A
660                    let idx = cur_char - b'A';
661                    // Decrement if it's past H to account for missing I
662                    let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
663                    // Decrement if it's past N to account for missing O
664                    let idx = (cur_char > b'N').ternary_lazy(|| idx - 1, || idx);
665                    i32::from(idx)
666                } else {
667                    -1
668                }
669            }
670        };
671        
672        if row_idx == -1 {
673            #[allow(clippy::cast_sign_loss)]
674            let row = utmp.ternary_lazy(|| UTMROW, || UPSROWS[usize::from(northp)]);
675            let northp = usize::from(northp);
676            let label = if utmp { "UTM".to_string() } else { format!("UPS {}", &HEMISPHERES[northp..=northp]) };
677            return Err(Error::InvalidMgrs(format!("Row letter {} not in {label} set {row}", chars[p] as char)));
678        }
679
680        p += 1;
681
682        if utmp {
683            if zonem.is_odd() {
684                row_idx = (row_idx + UTM_ROW_PERIOD - UTM_EVEN_ROW_SHIFT) % UTM_ROW_PERIOD;
685            }
686
687            band_idx -= 10;
688
689            row_idx = utm_row(band_idx, col_idx, row_idx);
690            if row_idx == MAXUTM_S_ROW {
691                return Err(Error::InvalidMgrs(format!("Block {} not in zone/band {}", &value[p-2..p], &value[0..p-2])))
692            }
693
694            row_idx = northp.ternary_lazy(|| row_idx, || row_idx + 100);
695            col_idx += MINUTMCOL;
696        }
697        else {
698            let eastp = band_idx.is_odd();
699            col_idx += if eastp { UPSEASTING } else if northp { MINUPS_N_IND } else { MINUPS_S_IND };
700            row_idx += if northp { MINUPS_N_IND } else { MINUPS_S_IND };
701        }
702
703        let precision = (len - p) / 2;
704        let mut unit = 1;
705        let mut x = col_idx;
706        let mut y = row_idx;
707
708        for i in 0..precision {
709            unit *= BASE;
710            let x_char = chars[p + i];
711            let x_idx = if x_char.is_ascii_digit() {
712                i32::from(x_char - b'0')
713            } else {
714                return Err(Error::InvalidMgrs(format!("Encountered a non-digit in {}", &value[p..])));
715            };
716
717            let y_char = chars[p + i + precision];
718            let y_idx = if y_char.is_ascii_digit() {
719                i32::from(y_char - b'0')
720            } else {
721                return Err(Error::InvalidMgrs(format!("Encountered a non-digit in {}", &value[p..])));
722            };
723            
724            x = BASE * x + x_idx;
725            y = BASE * y + y_idx;
726        }
727
728        if (len - p) % 2 == 1 {
729            if !(chars[len - 1] as char).is_ascii_digit() {
730                return Err(Error::InvalidMgrs(format!("Encountered a non-digit in {}", &value[p..])));
731            }
732
733            return Err(Error::InvalidMgrs(format!("Not an even number of digits in {}", &value[p..])));
734        }
735
736        if precision > MAX_PRECISION as usize {
737            return Err(Error::InvalidMgrs(format!("More than {} digits in {}", 2*MAX_PRECISION, &value[p..])));
738        }
739
740        let centerp = true;
741        if centerp {
742            unit *= 2;
743            x = 2 * x + 1;
744            y = 2 * y + 1;
745        }
746
747        let x = (f64::from(TILE) * f64::from(x)) / f64::from(unit);
748        let y = (f64::from(TILE) * f64::from(y)) / f64::from(unit);
749
750        Ok(Self {
751            utm: UtmUps::new(
752                zone,
753                northp,
754                x,
755                y,
756            ),
757            precision: precision as i32,
758        })
759    }
760}
761
762pub(crate) fn to_latitude_band(lat: f64) -> i32 {
763    let lat_int = lat.floor() as i32;
764    (-10).max(9.min((lat_int + 80) / 8 - 10))
765}
766
767pub(crate) fn check_coords(utmp: bool, northp: bool, x: f64, y: f64) -> Result<(bool, f64, f64), Error> {
768    lazy_static! {
769        static ref ANG_EPS: f64 = 1_f64 * 2_f64.powi(-(f64::DIGITS as i32 - 25));
770    }
771
772    let x_int = (x / f64::from(TILE)).floor() as i32;
773    let y_int = (y / f64::from(TILE)).floor() as i32;
774    let ind = utmp.ternary(2, 0) + northp.ternary(1, 0);
775
776    let mut x_new = x;
777    let mut y_new = y;
778
779    if !(MIN_EASTING[ind]..MAX_EASTING[ind]).contains(&x_int) {
780        if x_int == MAX_EASTING[ind] && x.eps_eq(f64::from(MAX_EASTING[ind] * TILE)) {
781            x_new -= *ANG_EPS;
782        } else {
783            return Err(Error::InvalidMgrs(
784                format!(
785                    "Easting {:.2}km not in MGRS/{} range for {} hemisphere [{:.2}km, {:.2}km]",
786                    x / 1000.0,
787                    utmp.ternary("UTM", "UPS"),
788                    northp.ternary("N", "S"),
789                    MIN_EASTING[ind] * (TILE / 1000),
790                    MAX_EASTING[ind] * (TILE / 1000),
791                )
792            ));
793        }
794    }
795
796    if !(MIN_NORTHING[ind]..MAX_NORTHING[ind]).contains(&y_int) {
797        if y_int == MAX_NORTHING[ind] && y.eps_eq(f64::from(MAX_NORTHING[ind] * TILE)) {
798            y_new -= *ANG_EPS;
799        } else {
800            return Err(Error::InvalidMgrs(
801                format!(
802                    "Northing {:.2}km not in MGRS/{} range for {} hemisphere [{:.2}km, {:.2}km]",
803                    y / 1000.0,
804                    utmp.ternary("UTM", "UPS"),
805                    northp.ternary("N", "S"),
806                    MIN_NORTHING[ind] * (TILE / 1000),
807                    MAX_NORTHING[ind] * (TILE / 1000),
808                )
809            ));
810        }
811    }
812
813    let (northp_new, y_new) = if utmp {
814        if northp && y_int < MINUTM_S_ROW {
815            (false, y_new + f64::from(UTM_N_SHIFT))
816        } else if !northp && y_int >= MAXUTM_S_ROW {
817            if y.eps_eq(f64::from(MAXUTM_S_ROW * TILE)) {
818                (northp, y_new - *ANG_EPS)
819            } else {
820                (true, y - f64::from(UTM_N_SHIFT))
821            }
822        } else {
823            (northp, y_new)
824        }
825    } else {
826        (northp, y_new)
827    };
828
829    Ok((northp_new, x_new, y_new))
830}
831
832impl Display for Mgrs {
833    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834        lazy_static! {
835            static ref ANG_EPS: f64 = 1_f64 * 2_f64.powi(-(f64::MANTISSA_DIGITS as i32 - 7));
836        }
837
838        let lat = if self.utm.zone > 0 {
839            // Does a rough estimate for latitude determine the latitude band?
840            let y_est = self.utm.northp.ternary_lazy(|| self.utm.northing, || self.utm.northing - f64::from(UTM_N_SHIFT));
841            // A cheap calculation of the latitude which results in an "allowed"
842            // latitude band would be
843            //   lat = ApproxLatitudeBand(ys) * 8 + 4;
844            //
845            // Here we do a more careful job using the band letter corresponding to
846            // the actual latitude.
847            let y_est = y_est / f64::from(TILE);
848            if y_est.abs() < 1.0 {
849                0.9 * y_est
850            }
851            else {
852                let pole_add = (y_est > 0.0).ternary(1.0, -1.0);
853                let lat_poleward = 0.901 * y_est + pole_add * 0.135;
854                let lat_eastward = 0.902 * y_est * (1.0 - 1.85e-6 * y_est.powi(2));
855
856                if to_latitude_band(lat_poleward) == to_latitude_band(lat_eastward) {
857                    lat_poleward
858                } else {
859                    let coord = UtmUps::new(self.utm.zone, self.utm.northp, self.utm.easting, self.utm.northing).to_latlon();
860                    coord.latitude
861                }
862            }
863        } else {
864            0.
865        };
866        
867        // Other Forward call
868        let utmp = self.utm.zone != 0;
869        let (northp, easting, northing) = check_coords(utmp, self.utm.northp, self.utm.easting, self.utm.northing)
870            .expect("Invalid coords; please report this to the library author");
871        // Create pre-allocated string of the correct length
872        let mut mgrs_str = [0u8; 2 + 3 + 2*MAX_PRECISION as usize];
873        let zone = self.utm.zone - 1;
874        let mut z: usize = utmp.ternary(2, 0);
875
876        let digits = DIGITS.as_bytes();
877
878        #[allow(clippy::cast_sign_loss)]
879        if utmp {
880            mgrs_str[0] = digits[(self.utm.zone / BASE) as usize];
881            mgrs_str[1] = digits[(self.utm.zone % BASE) as usize];
882        }
883
884        let xx = easting * f64::from(MULT);
885        let yy = northing * f64::from(MULT);
886
887        let ix = xx.floor() as i64;
888        let iy = yy.floor() as i64;
889        let m = i64::from(MULT) * i64::from(TILE);
890
891        let xh = (ix / m) as i32;
892        let yh = (iy / m) as i32;
893
894        #[allow(clippy::cast_sign_loss)]
895        if utmp {
896            // Correct fuzziness in latitude near equator
897            let band_idx = (lat.abs() < *ANG_EPS).ternary_lazy(|| northp.ternary(0, -1), || to_latitude_band(lat));
898            let col_idx = xh - MINUTMCOL;
899            let row_idx = utm_row(band_idx, col_idx, yh % UTM_ROW_PERIOD);
900
901            assert!(
902                row_idx == yh - northp.ternary(MINUTM_N_ROW, MAXUTM_S_ROW),
903                "Latitude is inconsistent with UTM; this should not occur."
904            );
905
906            mgrs_str[z] = LATBAND.as_bytes()[(10 + band_idx) as usize];
907            z += 1;
908            mgrs_str[z] = UTMCOLS[(zone % 3) as usize].as_bytes()[col_idx as usize];
909            z += 1;
910            let idx = (yh + zone.is_odd().ternary(UTM_EVEN_ROW_SHIFT, 0)) % UTM_ROW_PERIOD;
911            mgrs_str[z] = UTMROW.as_bytes()[idx as usize];
912            z += 1;
913        } else {
914            let eastp = xh >= UPSEASTING;
915            let band_idx: usize = northp.ternary(2, 0) + eastp.ternary(1, 0);
916            mgrs_str[z] = UPSBAND.as_bytes()[band_idx];
917            z += 1;
918            let idx = xh - eastp.ternary(UPSEASTING, northp.ternary(MINUPS_N_IND, MINUPS_S_IND));
919            mgrs_str[z] = UPSCOLS[band_idx].as_bytes()[idx as usize];
920            z += 1;
921            let idx = yh - northp.ternary(MINUPS_N_IND, MINUPS_S_IND);
922            mgrs_str[z] = UPSROWS[usize::from(northp)].as_bytes()[idx as usize];
923            z += 1;
924        }
925
926        if self.precision > 0 {
927            let mut ix = ix - m * i64::from(xh);
928            let mut iy = iy - m * i64::from(yh);
929            #[allow(clippy::cast_sign_loss)]
930            let d = i64::from(BASE).pow((MAX_PRECISION - self.precision) as u32);
931            ix /= d;
932            iy /= d;
933
934            #[allow(clippy::cast_sign_loss)]
935            for c in (0..self.precision as usize).rev() {
936                mgrs_str[z + c] = digits[(ix % i64::from(BASE)) as usize];
937                ix /= i64::from(BASE);
938                mgrs_str[z + c + self.precision as usize] = digits[(iy % i64::from(BASE)) as usize];
939                iy /= i64::from(BASE);
940            }
941        }
942
943        write!(f, "{}", String::from_utf8_lossy(&mgrs_str).trim_end_matches('\0'))
944    }
945}