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#[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 pub fn create(zone: i32, northp: bool, easting: f64, northing: f64, precision: i32) -> Result<Mgrs, Error> {
112 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 #[inline]
137 pub fn is_utm(&self) -> bool {
138 self.utm.zone != UPS
139 }
140
141 #[inline]
151 pub fn zone(&self) -> i32 {
152 self.utm.zone
153 }
154
155 #[inline]
165 pub fn is_north(&self) -> bool {
166 self.utm.northp
167 }
168
169 #[inline]
179 pub fn easting(&self) -> f64 {
180 self.utm.easting
181 }
182
183 #[inline]
193 pub fn northing(&self) -> f64 {
194 self.utm.northing
195 }
196
197 #[inline]
207 pub fn precision(&self) -> i32 {
208 self.precision
209 }
210
211 #[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 pub fn parse_str(mgrs_str: &str) -> Result<Mgrs, Error> {
249 Self::from_str(mgrs_str)
250 }
251
252 pub fn from_latlon(value: &LatLon, precision: i32) -> Mgrs {
269 Mgrs {
270 utm: UtmUps::from_latlon(value),
271 precision,
272 }
273 }
274
275 pub fn to_latlon(&self) -> LatLon {
289 self.utm.to_latlon()
290 }
291
292
293 pub fn from_utmups(value: &UtmUps, precision: i32) -> Mgrs {
313 Mgrs {
314 utm: *value,
315 precision,
316 }
317 }
318
319 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 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 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 let safe_band = (band_idx >= 0).ternary(band_idx, -band_idx - 1);
396 let safe_row = (row_idx >= 0).ternary(row_idx, -row_idx - 1);
398 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 (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 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 if (b'C'..=b'X').contains(&cur_char) && cur_char != b'I' && cur_char != b'O' {
462 let idx = cur_char - b'C';
465 let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
467 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 { let deg = (f64::from(UTM_N_SHIFT)) / f64::from(dms::QD * TILE);
500 let (x, y) = if utmp {
501 let x = f64::from(TILE) * (zone == 31 && band_idx == 17).ternary(4.0, 5.0);
503 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 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 match band_idx {
557 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 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 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 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 let mut row_idx = if utmp {
626 if (b'A'..=b'V').contains(&cur_char) && cur_char != b'I' && cur_char != b'O' {
629 let idx = cur_char - b'A';
632 let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
634 let idx = (cur_char > b'N').ternary_lazy(|| idx - 1, || idx);
636 i32::from(idx)
637 } else {
638 -1
639 }
640 } else {
641 #[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 let idx = cur_char - b'A';
648 let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
650 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 let idx = cur_char - b'A';
661 let idx = (cur_char > b'H').ternary_lazy(|| idx - 1, || idx);
663 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 let y_est = self.utm.northp.ternary_lazy(|| self.utm.northing, || self.utm.northing - f64::from(UTM_N_SHIFT));
841 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 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 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 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}