gistools/proj/project/
mgrs.rs

1// References:
2// - Wikipedia: https://en.wikipedia.org/wiki/Military_Grid_Reference_System
3// - GEOTRANS: https://earth-info.nga.mil/#geotrans
4
5use crate::proj::TransformCoordinates;
6use alloc::{format, string::String, vec::Vec};
7use libm::{cos, floor, pow, sin, sqrt, tan, trunc};
8use s2json::BBox;
9
10/// UTM zones are grouped, and assigned to one of a group of 6 sets.
11const NUM_100K_SETS: u8 = 6;
12/// The column letters (for easting) of the lower left value, per set.
13const SET_ORIGIN_COLUMN_LETTERS: &str = "AJSAJS";
14/// The row letters (for northing) of the lower left value, per set.
15const SET_ORIGIN_ROW_LETTERS: &str = "AFAFAF";
16/// Column letter list
17const ALPHABET: &[char] = &[
18    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U',
19    'V', 'W', 'X', 'Y', 'Z',
20];
21
22/// First eccentricity squared
23const ECC_SQUARED: f64 = 0.00669438;
24/// Scale factor along the central meridian
25const SCALE_FACTOR: f64 = 0.9996;
26/// Semimajor axis (half the width of the earth) in meters
27const SEMI_MAJOR_AXIS: f64 = 6378137.;
28/// The easting of the central meridian of each UTM zone
29const EASTING_OFFSET: f64 = 500000.;
30/// The northing of the equator for southern hemisphere locations (in UTM)
31const NORTHING_OFFFSET: f64 = 10000000.;
32/// UTM zone width in degrees
33const UTM_ZONE_WIDTH: f64 = 6.;
34/// Half the width of a UTM zone in degrees
35const HALF_UTM_ZONE_WIDTH: f64 = UTM_ZONE_WIDTH / 2.;
36
37const A_U32: u32 = 65;
38const I_U32: u32 = 73;
39const O_U32: u32 = 79;
40const V_U32: u32 = 86;
41const Z_U32: u32 = 90;
42
43/// Convert lat/lon to MGRS.
44///
45/// - `ll`: Array with longitude and latitude on a WGS84 ellipsoid.
46/// - `accuracy`: Accuracy in digits (5 for 1 m, 4 for 10 m, 3 for
47///   100 m, 2 for 1 km, 1 for 10 km or 0 for 100 km). Optional, default is 5.
48///
49/// returns the MGRS string for the given location and accuracy.
50pub fn mgrs_forward<P: TransformCoordinates>(ll: &P, accuracy: Option<u8>) -> String {
51    let accuracy = accuracy.unwrap_or(5); // default accuracy 1m
52
53    let x = ll.x();
54    let y = ll.y();
55    if !(-180. ..=180.).contains(&x) {
56        panic!("forward received an invalid longitude of {x:}");
57    }
58    if !(-90. ..=90.).contains(&y) {
59        panic!("forward received an invalid latitude of {y:}");
60    }
61    if !(-80. ..=84.).contains(&y) {
62        panic!(
63            "forward received a latitude of {y:}, but this library does not support conversions \
64             of points in polar regions below 80°S and above 84°N",
65        );
66    }
67
68    encode(ll_to_utm(ll), accuracy as usize)
69}
70
71/// Convert MGRS to lat/lon bounding box given an MGRS string
72///
73/// returns an array with left (longitude), bottom (latitude), right (longitude) and top (latitude)
74/// values in WGS84, representing the bounding box for the provided MGRS reference.
75pub fn mgrs_inverse(mgrs: &str) -> Option<BBox> {
76    utm_to_ll(decode(mgrs.to_uppercase()))
77}
78
79/// Convert MGRS to lat/lon given an MGRS string
80///
81/// ## Returns
82/// The center of the MGRS bounding box
83pub fn mgrs_to_point<P: TransformCoordinates>(mgrs: &str) -> Option<P> {
84    let bbox = utm_to_ll(decode(mgrs.to_uppercase()));
85    if let Some(bbox) = bbox {
86        let mut point = P::default();
87        point.set_x((bbox.left + bbox.right) / 2.);
88        point.set_y((bbox.top + bbox.bottom) / 2.);
89        Some(point)
90    } else {
91        None
92    }
93}
94
95/// Converts a set of Longitude and Latitude co-ordinates to UTM
96/// using the WGS84 ellipsoid.
97///
98/// ## Parameters
99/// - `ll`: Object literal with lat and lon properties representing the WGS84 coordinate to be
100///   converted.
101///
102/// ## Returns
103/// An Object literal containing the UTM value with easting,
104/// northing, zone_number and zone_letter properties, and an optional
105/// accuracy property in digits. Returns null if the conversion failed.
106fn ll_to_utm<P: TransformCoordinates>(ll: &P) -> Utm {
107    let lon = ll.x();
108    let lat = ll.y();
109    let a = SEMI_MAJOR_AXIS;
110    let lat_rad = lat.to_radians();
111    let long_rad = lon.to_radians();
112    let mut zone_number = floor((lon + 180.) / 6.) + 1.;
113
114    // Make sure the longitude 180 is in Zone 60
115    if lon == 180. {
116        zone_number = 60.;
117    }
118
119    // Special zone for Norway
120    if (56. ..64.).contains(&lat) && (3. ..12.).contains(&lon) {
121        zone_number = 32.;
122    }
123
124    // Special zones for Svalbard
125    if (72. ..84.).contains(&lat) {
126        if (0. ..9.).contains(&lon) {
127            zone_number = 31.;
128        } else if (9. ..21.).contains(&lon) {
129            zone_number = 33.;
130        } else if (21. ..33.).contains(&lon) {
131            zone_number = 35.;
132        } else if (33. ..42.).contains(&lon) {
133            zone_number = 37.;
134        }
135    }
136
137    // +HALF_UTM_ZONE_WIDTH puts origin in middle of zone
138    let long_origin = (zone_number - 1.) * UTM_ZONE_WIDTH - 180. + HALF_UTM_ZONE_WIDTH;
139    let long_origin_rad = long_origin.to_radians();
140
141    let ecc_prime_squared = ECC_SQUARED / (1. - ECC_SQUARED);
142
143    let n = a / sqrt(1. - ECC_SQUARED * sin(lat_rad) * sin(lat_rad));
144    let t = tan(lat_rad) * tan(lat_rad);
145    let c = ecc_prime_squared * cos(lat_rad) * cos(lat_rad);
146    let a_2 = cos(lat_rad) * (long_rad - long_origin_rad);
147
148    let m = a
149        * ((1.
150            - ECC_SQUARED / 4.
151            - (3. * ECC_SQUARED * ECC_SQUARED) / 64.
152            - (5. * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED) / 256.)
153            * lat_rad
154            - ((3. * ECC_SQUARED) / 8.
155                + (3. * ECC_SQUARED * ECC_SQUARED) / 32.
156                + (45. * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED) / 1024.)
157                * sin(2. * lat_rad)
158            + ((15. * ECC_SQUARED * ECC_SQUARED) / 256.
159                + (45. * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED) / 1024.)
160                * sin(4. * lat_rad)
161            - ((35. * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED) / 3072.) * sin(6. * lat_rad));
162
163    let utm_easting = SCALE_FACTOR
164        * n
165        * (a_2
166            + ((1. - t + c) * a_2 * a_2 * a_2) / 6.
167            + ((5. - 18. * t + t * t + 72. * c - 58. * ecc_prime_squared)
168                * a_2
169                * a_2
170                * a_2
171                * a_2
172                * a_2)
173                / 120.)
174        + EASTING_OFFSET;
175
176    let mut utm_northing = SCALE_FACTOR
177        * (m + n
178            * tan(lat_rad)
179            * ((a_2 * a_2) / 2.
180                + ((5. - t + 9. * c + 4. * c * c) * a_2 * a_2 * a_2 * a_2) / 24.
181                + ((61. - 58. * t + t * t + 600. * c - 330. * ecc_prime_squared)
182                    * a_2
183                    * a_2
184                    * a_2
185                    * a_2
186                    * a_2
187                    * a_2)
188                    / 720.));
189    if lat < 0. {
190        utm_northing += NORTHING_OFFFSET;
191    }
192
193    Utm {
194        northing: trunc(utm_northing),
195        easting: trunc(utm_easting),
196        zone_number: zone_number as u8,
197        zone_letter: mgrs_get_letter_designator(lat),
198        accuracy: None,
199    }
200}
201
202/// UTM parameters
203#[derive(Debug, Copy, Clone)]
204struct Utm {
205    northing: f64,
206    easting: f64,
207    zone_number: u8,
208    zone_letter: char,
209    accuracy: Option<f64>,
210}
211
212/// Converts UTM coords to lat/long, using the WGS84 ellipsoid. This is a convenience
213/// class where the Zone can be specified as a single string eg."60N" which
214/// is then broken down into the Zone_number and Zone_letter.
215///
216/// ## Parameters
217/// - `utm`: An object literal with northing, easting, zone_number
218///   and zone_letter properties. If an optional accuracy property is
219///   provided (in meters), a bounding box will be returned instead of
220///   latitude and longitude.
221///
222/// ## Returns
223/// An object literal containing either lat and lon values
224/// (if no accuracy was provided), or top, right, bottom and left values
225/// for the bounding box calculated according to the provided accuracy.
226/// Returns null if the conversion failed.
227fn utm_to_ll(utm: Utm) -> Option<BBox> {
228    let utm_northing = utm.northing;
229    let utm_easting = utm.easting;
230    let Utm { zone_letter, zone_number, .. } = utm;
231    // check the ZoneNummber is valid
232    if !(0..=60).contains(&zone_number) {
233        return None;
234    }
235
236    let a = SEMI_MAJOR_AXIS;
237    let e1 = (1. - sqrt(1. - ECC_SQUARED)) / (1. + sqrt(1. - ECC_SQUARED));
238
239    // remove 500,000 meter offset for longitude
240    let x = utm_easting - EASTING_OFFSET;
241    let mut y = utm_northing;
242
243    // We must know somehow if we are in the Northern or Southern
244    // hemisphere, this is the only time we use the letter So even
245    // if the Zone letter isn't exactly correct it should indicate
246    // the hemisphere correctly
247    if zone_letter < 'N' {
248        y -= NORTHING_OFFFSET; // remove offset used for southern hemisphere
249    }
250
251    // +HALF_UTM_ZONE_WIDTH puts origin in middle of zone
252    let long_origin = (zone_number as f64 - 1.) * UTM_ZONE_WIDTH - 180. + HALF_UTM_ZONE_WIDTH;
253
254    let ecc_prime_squared = ECC_SQUARED / (1. - ECC_SQUARED);
255
256    let m = y / SCALE_FACTOR;
257    let mu = m
258        / (a * (1.
259            - ECC_SQUARED / 4.
260            - (3. * ECC_SQUARED * ECC_SQUARED) / 64.
261            - (5. * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED) / 256.));
262
263    let phi1_rad = mu
264        + ((3. * e1) / 2. - (27. * e1 * e1 * e1) / 32.) * sin(2. * mu)
265        + ((21. * e1 * e1) / 16. - (55. * e1 * e1 * e1 * e1) / 32.) * sin(4. * mu)
266        + ((151. * e1 * e1 * e1) / 96.) * sin(6. * mu);
267    // double phi1 = ProjradToDeg(phi1_rad);
268
269    let n1 = a / sqrt(1. - ECC_SQUARED * sin(phi1_rad) * sin(phi1_rad));
270    let t1 = tan(phi1_rad) * tan(phi1_rad);
271    let c1 = ecc_prime_squared * cos(phi1_rad) * cos(phi1_rad);
272    let r1 = (a * (1. - ECC_SQUARED)) / pow(1. - ECC_SQUARED * sin(phi1_rad) * sin(phi1_rad), 1.5);
273    let d = x / (n1 * SCALE_FACTOR);
274
275    let mut lat = phi1_rad
276        - ((n1 * tan(phi1_rad)) / r1)
277            * ((d * d) / 2.
278                - ((5. + 3. * t1 + 10. * c1 - 4. * c1 * c1 - 9. * ecc_prime_squared)
279                    * d
280                    * d
281                    * d
282                    * d)
283                    / 24.
284                + ((61. + 90. * t1 + 298. * c1 + 45. * t1 * t1
285                    - 252. * ecc_prime_squared
286                    - 3. * c1 * c1)
287                    * d
288                    * d
289                    * d
290                    * d
291                    * d
292                    * d)
293                    / 720.);
294    lat = lat.to_degrees();
295
296    let mut lon = (d - ((1. + 2. * t1 + c1) * d * d * d) / 6.
297        + ((5. - 2. * c1 + 28. * t1 - 3. * c1 * c1 + 8. * ecc_prime_squared + 24. * t1 * t1)
298            * d
299            * d
300            * d
301            * d
302            * d)
303            / 120.)
304        / cos(phi1_rad);
305    lon = long_origin + lon.to_degrees();
306
307    if let Some(accuracy) = utm.accuracy {
308        let top_right = utm_to_ll(Utm {
309            northing: utm.northing + accuracy,
310            easting: utm.easting + accuracy,
311            zone_letter: utm.zone_letter,
312            zone_number: utm.zone_number,
313            accuracy: None,
314        })
315        .unwrap_or_default();
316        Some(BBox { top: top_right.left, right: top_right.bottom, bottom: lat, left: lon })
317    } else {
318        Some(BBox { top: lat, right: lon, bottom: lat, left: lon })
319    }
320}
321
322/// Calculates the MGRS letter designator for the given latitude.
323///
324/// `latitude`: The latitude in WGS84 to get the letter designator for.
325/// returns The letter designator.
326fn mgrs_get_letter_designator(latitude: f64) -> char {
327    if (72. ..=84.).contains(&latitude) {
328        // the X band is 12 degrees high
329        'X'
330    } else if (-80. ..72.).contains(&latitude) {
331        // Latitude bands are lettered C through X, excluding I and O
332        let band_letters = "CDEFGHJKLMNPQRSTUVWX";
333        let band_height = 8.;
334        let min_latitude = -80.;
335        let index = floor((latitude - min_latitude) / band_height) as usize;
336        band_letters.chars().nth(index).unwrap()
337    } else {
338        // This is here as an error flag to show that the Latitude is
339        // outside MGRS limits
340        'Z'
341    }
342}
343
344/// Encodes a UTM location as MGRS string.
345///
346/// `utm`: An object literal with easting, northing, zone_letter, zone_number
347/// `accuracy`: Accuracy in digits (0-5).
348/// returns MGRS string for the given UTM location.
349fn encode(utm: Utm, accuracy: usize) -> String {
350    // prepend with leading zeroes
351    let seasting: String = format!("00000{}", utm.easting);
352    let snorthing: String = format!("00000{}", utm.northing);
353    let se_len = seasting.len() - 5;
354    let sn_len = snorthing.len() - 5;
355
356    format!(
357        "{}{}{}{}{}",
358        utm.zone_number,
359        utm.zone_letter,
360        get_100k_id(utm.easting, utm.northing, utm.zone_number),
361        &seasting[se_len..se_len + accuracy],
362        &snorthing[sn_len..sn_len + accuracy]
363    )
364}
365
366/// Get the two letter 100k designator for a given UTM easting, northing and zone number value.
367///
368/// `easting`: UTM easting
369/// `northing`: UTM northing
370/// `zone_number`: UTM zone number
371/// returns the two letter 100k designator for the given UTM location.
372fn get_100k_id(easting: f64, northing: f64, zone_number: u8) -> String {
373    let set_parm = get_100k_set_for_zone(zone_number);
374    let set_column = floor(easting / 100000.);
375    let set_row = floor(northing / 100000.) % 20.;
376    get_letter_100k_id(set_column as u32, set_row as u32, set_parm as usize)
377}
378
379/// Given a UTM zone number, figure out the MGRS 100K set it is in.
380///
381/// `i`: An UTM zone number.
382/// returns the 100k set the UTM zone is in.
383fn get_100k_set_for_zone(i: u8) -> u8 {
384    let mut set_parm = i % NUM_100K_SETS;
385    if set_parm == 0 {
386        set_parm = NUM_100K_SETS;
387    }
388
389    set_parm
390}
391
392/// Get the two-letter MGRS 100k designator given information translated from the UTM northing,
393/// easting and zone number.
394///
395/// - `column`: the column index as it relates to the MGRS
396///   100k set spreadsheet, created from the UTM easting.
397///   Values are 1-8.
398/// - `row`: the row index as it relates to the MGRS 100k set
399///   spreadsheet, created from the UTM northing value. Values
400///   are from 0-19.
401/// - `parm`: the set block, as it relates to the MGRS 100k set
402///   spreadsheet, created from the UTM zone. Values are from
403///   1-60.
404///
405/// returns two letter MGRS 100k code.
406fn get_letter_100k_id(mut col: u32, mut row: u32, parm: usize) -> String {
407    let index = parm - 1;
408    let col_origin = SET_ORIGIN_COLUMN_LETTERS.chars().nth(index).unwrap() as u32;
409    let row_origin = SET_ORIGIN_ROW_LETTERS.chars().nth(index).unwrap() as u32;
410
411    col += col_origin - 1;
412    row += row_origin;
413
414    let mut rollover_col = false;
415    while col > Z_U32 {
416        col -= Z_U32 - A_U32 + 1;
417        rollover_col = true;
418    }
419
420    if col == I_U32
421        || (col_origin < I_U32 && col > I_U32)
422        || ((col > I_U32 || col_origin < I_U32) && rollover_col)
423    {
424        col += 1;
425    }
426
427    if col == O_U32
428        || (col_origin < O_U32 && col > O_U32)
429        || ((col > O_U32 || col_origin < O_U32) && rollover_col)
430    {
431        col += 1;
432        if col == I_U32 {
433            col += 1;
434        }
435    }
436
437    while col > Z_U32 {
438        col -= Z_U32 - A_U32 + 1;
439    }
440
441    let mut rollover_row = false;
442    while row > V_U32 {
443        row -= V_U32 - A_U32 + 1;
444        rollover_row = true;
445    }
446
447    if row == I_U32
448        || (row_origin < I_U32 && row > I_U32)
449        || ((row > I_U32 || row_origin < I_U32) && rollover_row)
450    {
451        row += 1;
452    }
453
454    if row == O_U32
455        || (row_origin < O_U32 && row > O_U32)
456        || ((row > O_U32 || row_origin < O_U32) && rollover_row)
457    {
458        row += 1;
459        if row == I_U32 {
460            row += 1;
461        }
462    }
463
464    while row > V_U32 {
465        row -= V_U32 - A_U32 + 1;
466    }
467
468    let col_char = char::from_u32(col).unwrap();
469    let row_char = char::from_u32(row).unwrap();
470
471    format!("{col_char}{row_char}")
472}
473
474/// Decode the UTM parameters from a MGRS string.
475///
476/// ## Parameters
477/// - `mgrs_string`: an UPPERCASE coordinate string is expected.
478///
479/// ## Returns
480/// An object literal with easting, northing, zone_letter,
481///     zone_number and accuracy (in meters) properties.
482fn decode(mut mgrs_string: String) -> Utm {
483    if mgrs_string.is_empty() {
484        panic!("MGRSPoint coverting from nothing");
485    }
486
487    // remove any spaces in MGRS String
488    mgrs_string = mgrs_string.trim().into();
489    let length = mgrs_string.len();
490    let mut sb = String::new();
491    let mut i = 0;
492
493    // get Zone number
494    while i < mgrs_string.len() {
495        let test_char = mgrs_string.chars().nth(i).unwrap();
496        if test_char.is_ascii_uppercase() {
497            break; // Found the latitude band letter
498        }
499        if i >= 2 {
500            panic!("MGRSPoint bad conversion from: {}", mgrs_string);
501        }
502        sb.push(test_char);
503        i += 1;
504    }
505
506    let zone_number = sb.parse::<u8>().unwrap();
507
508    if i == 0 || i + 3 > length {
509        // A good MGRS string has to be 4-5 digits long,
510        // ##AAA/#AAA at least.
511        panic!("MGRSPoint bad conversion from {}", mgrs_string);
512    }
513
514    let zone_letter = mgrs_string.chars().nth(i).unwrap();
515    i += 1;
516
517    // Should we check the zone letter here? Why not.
518    if zone_letter <= 'A'
519        || zone_letter == 'B'
520        || zone_letter == 'Y'
521        || zone_letter >= 'Z'
522        || zone_letter == 'I'
523        || zone_letter == 'O'
524    {
525        panic!("MGRSPoint zone letter ${zone_letter} not handled: {}", mgrs_string);
526    }
527
528    let hunk = &mgrs_string[i..i + 2];
529    let hunk_chars = hunk.chars().collect::<Vec<char>>();
530    i += 2;
531
532    let set = get_100k_set_for_zone(zone_number) as usize;
533
534    let east100k = get_easting_from_char(hunk_chars[0], set);
535    let mut north100k = get_northing_from_char(hunk_chars[1], set);
536
537    // We have a bug where the northing may be 2000000 too low.
538    // How
539    // do we know when to roll over?
540
541    while north100k < get_min_northing(zone_letter) {
542        north100k += 2000000.;
543    }
544
545    // calculate the char index for easting/northing separator
546    let remainder = length - i;
547
548    if !remainder.is_multiple_of(2) {
549        panic!(
550            "MGRSPoint has to have an even number of digits after the zone letter and two 100km \
551             letters - front half for easting meters, second half for northing meters {}",
552            mgrs_string
553        );
554    }
555
556    let sep = remainder / 2;
557    let sep_f64 = sep as f64;
558    let mut sep_easting = 0.;
559    let mut sep_northing = 0.;
560    let mut accuracy = 0.;
561    let sep_easting_string: &str;
562    let sep_northing_string: &str;
563    if sep > 0 {
564        accuracy = 100000. / pow(10., sep_f64);
565        sep_easting_string = &mgrs_string[i..i + sep];
566        sep_easting = sep_easting_string.parse::<f64>().unwrap() * accuracy;
567        sep_northing_string = &mgrs_string[i + sep..];
568        sep_northing = sep_northing_string.parse::<f64>().unwrap() * accuracy;
569    }
570
571    let easting = sep_easting + east100k;
572    let northing = sep_northing + north100k;
573
574    Utm { easting, northing, zone_letter, zone_number, accuracy: Some(accuracy) }
575}
576
577/// Given the first letter from a two-letter MGRS 100k zone, and given the
578/// MGRS table set for the zone number, figure out the easting value that
579/// should be added to the other, secondary easting value.
580///
581/// `e`: The first letter from a two-letter MGRS 100´k zone.
582/// `set`: The MGRS table set for the zone number.
583/// returns The easting value for the given letter and set.
584fn get_easting_from_char(e: char, set: usize) -> f64 {
585    let origin_col = SET_ORIGIN_COLUMN_LETTERS.chars().nth(set - 1).unwrap();
586    let origin_index = ALPHABET.iter().position(|&c| c == origin_col).unwrap();
587    let target_index = ALPHABET.iter().position(|&c| c == e).unwrap_or_else(|| {
588        panic!("Bad character: {}", e);
589    });
590    let diff =
591        (target_index as i32 - origin_index as i32 + ALPHABET.len() as i32) % ALPHABET.len() as i32;
592
593    (diff as f64) * 100000.0
594}
595
596/// Given the second letter from a two-letter MGRS 100k zone, and given the
597/// MGRS table set for the zone number, figure out the northing value that
598/// should be added to the other, secondary northing value. You have to
599/// remember that Northings are determined from the equator, and the vertical
600/// cycle of letters mean a 2000000 additional northing meters. This happens
601/// approx. every 18 degrees of latitude. This method does *NOT* count any
602/// additional northings. You have to figure out how many 2000000 meters need
603/// to be added for the zone letter of the MGRS coordinate.
604///
605/// `n`: Second letter of the MGRS 100k zone
606/// `set`: The MGRS table set number, which is dependent on the UTM zone number.
607/// returns the northing value for the given letter and set.
608fn get_northing_from_char(n: char, set: usize) -> f64 {
609    if n > 'V' {
610        panic!("MGRSPoint given invalid Northing {}", n);
611    }
612
613    let origin_row = SET_ORIGIN_ROW_LETTERS.chars().nth(set - 1).unwrap();
614    let origin_index = ALPHABET.iter().position(|&c| c == origin_row).unwrap();
615    let target_index = ALPHABET.iter().position(|&c| c == n).unwrap_or_else(|| {
616        panic!("Bad character: {}", n);
617    });
618    let diff =
619        (target_index as i32 - origin_index as i32 + ALPHABET.len() as i32) % ALPHABET.len() as i32;
620
621    (diff as f64) * 100000.0
622}
623
624/// The function get_min_northing returns the minimum northing value of a MGRS
625/// zone.
626///
627/// Ported from Geotrans' c Lattitude_Band_Value structure table.
628///
629/// `zone_letter`: The MGRS zone to get the min northing for.
630/// returns the minimum northing value of the MGRS zone.
631fn get_min_northing(zone_letter: char) -> f64 {
632    let northing = match zone_letter {
633        'C' => 1100000.,
634        'D' => 2000000.,
635        'E' => 2800000.,
636        'F' => 3700000.,
637        'G' => 4600000.,
638        'H' => 5500000.,
639        'J' => 6400000.,
640        'K' => 7300000.,
641        'L' => 8200000.,
642        'M' => 9100000.,
643        'N' => 0.,
644        'P' => 800000.,
645        'Q' => 1700000.,
646        'R' => 2600000.,
647        'S' => 3500000.,
648        'T' => 4400000.,
649        'U' => 5300000.,
650        'V' => 6200000.,
651        'W' => 7000000.,
652        'X' => 7900000.,
653        _ => -1.,
654    };
655    if northing == -1. {
656        panic!("Invalid zone letter: {}", zone_letter);
657    }
658
659    northing
660}