1use crate::proj::TransformCoordinates;
6use alloc::{format, string::String, vec::Vec};
7use libm::{cos, floor, pow, sin, sqrt, tan, trunc};
8use s2json::BBox;
9
10const NUM_100K_SETS: u8 = 6;
12const SET_ORIGIN_COLUMN_LETTERS: &str = "AJSAJS";
14const SET_ORIGIN_ROW_LETTERS: &str = "AFAFAF";
16const 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
22const ECC_SQUARED: f64 = 0.00669438;
24const SCALE_FACTOR: f64 = 0.9996;
26const SEMI_MAJOR_AXIS: f64 = 6378137.;
28const EASTING_OFFSET: f64 = 500000.;
30const NORTHING_OFFFSET: f64 = 10000000.;
32const UTM_ZONE_WIDTH: f64 = 6.;
34const 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
43pub fn mgrs_forward<P: TransformCoordinates>(ll: &P, accuracy: Option<u8>) -> String {
51 let accuracy = accuracy.unwrap_or(5); 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
71pub fn mgrs_inverse(mgrs: &str) -> Option<BBox> {
76 utm_to_ll(decode(mgrs.to_uppercase()))
77}
78
79pub 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
95fn 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 if lon == 180. {
116 zone_number = 60.;
117 }
118
119 if (56. ..64.).contains(&lat) && (3. ..12.).contains(&lon) {
121 zone_number = 32.;
122 }
123
124 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 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#[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
212fn 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 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 let x = utm_easting - EASTING_OFFSET;
241 let mut y = utm_northing;
242
243 if zone_letter < 'N' {
248 y -= NORTHING_OFFFSET; }
250
251 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 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
322fn mgrs_get_letter_designator(latitude: f64) -> char {
327 if (72. ..=84.).contains(&latitude) {
328 'X'
330 } else if (-80. ..72.).contains(&latitude) {
331 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 'Z'
341 }
342}
343
344fn encode(utm: Utm, accuracy: usize) -> String {
350 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
366fn 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
379fn 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
392fn 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
474fn decode(mut mgrs_string: String) -> Utm {
483 if mgrs_string.is_empty() {
484 panic!("MGRSPoint coverting from nothing");
485 }
486
487 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 while i < mgrs_string.len() {
495 let test_char = mgrs_string.chars().nth(i).unwrap();
496 if test_char.is_ascii_uppercase() {
497 break; }
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 panic!("MGRSPoint bad conversion from {}", mgrs_string);
512 }
513
514 let zone_letter = mgrs_string.chars().nth(i).unwrap();
515 i += 1;
516
517 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 while north100k < get_min_northing(zone_letter) {
542 north100k += 2000000.;
543 }
544
545 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
577fn 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
596fn 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
624fn 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}