Coverage for pygeodesy/mgrs.py: 99%
264 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-21 13:14 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-21 13:14 -0400
2# -*- coding: utf-8 -*-
4u'''Military Grid Reference System (MGRS/NATO) references.
6Classes L{Mgrs}, L{Mgrs4Tuple} and L{Mgrs6Tuple} and functions L{parseMGRS}
7and L{toMgrs}.
9Pure Python implementation of MGRS, UTM and UPS conversions covering the entire
10I{ellipsoidal} earth, transcoded from I{Chris Veness}' JavaScript originals U{MGRS
11<https://www.Movable-Type.co.UK/scripts/latlong-utm-mgrs.html>} and U{Module mgrs
12<https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-mgrs.html>} and from
13I{Charles Karney}'s C++ class U{MGRS<https://GeographicLib.SourceForge.io/C++/doc/
14classGeographicLib_1_1MGRS.html>}.
16MGRS references comprise a grid zone designation (GZD), a 100 km grid (square)
17tile identification and an easting and northing (in C{meter}). The GZD consists
18of a longitudinal zone (or column) I{number} and latitudinal band (row) I{letter}
19in the UTM region between 80°S and 84°N. Each zone (column) is 6° wide and each
20band (row) is 8° high, except top band 'X' is 12° tall. In UPS polar regions
21below 80°S and above 84°N the GZD contains only a single I{letter}, C{'A'} or
22C{'B'} near the south and C{'Y'} or C{'Z'} around the north pole (for west
23respectively east longitudes).
25See also the U{United States National Grid<https://www.FGDC.gov/standards/projects/
26FGDC-standards-projects/usng/fgdc_std_011_2001_usng.pdf>} and U{Military Grid
27Reference System<https://WikiPedia.org/wiki/Military_grid_reference_system>}.
29See module L{pygeodesy.ups} for env variable C{PYGEODESY_UPS_POLES} determining
30the UPS encoding I{at} the south and north pole.
32Set env variable C{PYGEODESY_GEOCONVERT} to the (fully qualified) path of the
33C{GeoConvert} executable to run this module as I{python[3] -m pygeodesy.mgrs}
34and compare the MGRS results with those from I{Karney}'s utility U{GeoConvert
35<https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html>}.
36'''
38from pygeodesy.basics import halfs2, _xinstanceof
39# from pygeodesy.constants import _0_5 # from .units
40from pygeodesy.datums import _ellipsoidal_datum, _WGS84
41from pygeodesy.errors import _AssertionError, MGRSError, _parseX, \
42 _ValueError, _xkwds, _ALL_LAZY, _MODS
43from pygeodesy.interns import NN, _0_, _A_, _AtoZnoIO_, _band_, _B_, \
44 _COMMASPACE_, _datum_, _easting_, _invalid_, \
45 _northing_, _not_, _SPACE_, _splituple, _W_, \
46 _Y_, _Z_, _zone_
47# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .errors
48from pygeodesy.named import _NamedBase, _NamedTuple, _Pass, _xnamed
49from pygeodesy.namedTuples import EasNor2Tuple, UtmUps5Tuple
50from pygeodesy.props import deprecated_property_RO, property_RO, Property_RO
51from pygeodesy.streprs import enstr2, _enstr2m3, Fmt, _resolution10, _xzipairs
52from pygeodesy.units import Easting, Northing, Str, _100km, _0_5
53from pygeodesy.units import _1um, _2000km # PYCHOK used!
54from pygeodesy.ups import _hemi, toUps8, Ups, _UPS_ZONE
55from pygeodesy.utm import toUtm8, _to3zBlat, Utm, _UTM_ZONE_MAX, _UTM_ZONE_MIN
56# from pygeodesy.utmupsBase import _UTM_ZONE_MAX, _UTM_ZONE_MIN # from .utm
58__all__ = _ALL_LAZY.mgrs
59__version__ = '23.03.19'
61_AN_ = 'AN' # default south pole grid tile and band B
62_AtoPx_ = _AtoZnoIO_.tillP
63# <https://GitHub.com/hrbrmstr/mgrs/blob/master/src/mgrs.c>
64_FeUPS = {_A_: 8, _B_: 20, _Y_: 8, _Z_: 20} # falsed offsets (C{_100kms})
65_FnUPS = {_A_: 8, _B_: 8, _Y_: 13, _Z_: 13} # falsed offsets (C{_100kms})
66_JtoZx_ = 'JKLPQRSTUXYZZ' # _AtoZnoDEIMNOVW.fromJ, duplicate Z
67# 100 km grid tile UTM column (E) letters, repeating every third zone
68_LeUTM = _AtoZnoIO_.tillH, _AtoZnoIO_.fromJ.tillR, _AtoZnoIO_.fromS # grid E colums
69# 100 km grid tile UPS column (E) letters for each polar zone
70_LeUPS = {_A_: _JtoZx_, _B_: 'ABCFGHJKLPQR', _Y_: _JtoZx_, _Z_: 'ABCFGHJ'}
71# 100 km grid tile UTM and UPS row (N) letters, repeating every other zone
72_LnUTM = _AtoZnoIO_.tillV, _AtoZnoIO_.fromF.tillV + _AtoZnoIO_.tillE # grid N rows
73_LnUPS = {_A_: _AtoZnoIO_, _B_: _AtoZnoIO_, _Y_: _AtoPx_, _Z_: _AtoPx_}
74_polar_ = _SPACE_('polar', _zone_)
77class Mgrs(_NamedBase):
78 '''Military Grid Reference System (MGRS/NATO) references,
79 with method to convert to UTM coordinates.
80 '''
81 _band = NN # latitudinal (C..X) or polar (ABYZ) band
82 _bandLat = None # band latitude (C{degrees90} or C{None})
83 _datum = _WGS84 # Datum (L{Datum})
84 _easting = 0 # Easting (C{meter}), within 100 km grid tile
85 _EN = NN # EN digraph (C{str}), 100 km grid tile
86 _northing = 0 # Northing (C{meter}), within 100 km grid tile
87 _resolution = 0 # from L{parseMGRS}, centering (C{meter})
88 _zone = 0 # longitudinal or polar zone (C{int}), 0..60
90 def __init__(self, zone=0, EN=NN, easting=0, northing=0, band=NN,
91 datum=_WGS84, resolution=0, name=NN):
92 '''New L{Mgrs} Military grid reference.
94 @arg zone: The 6° I{longitudinal} zone (C{int}), 1..60 covering
95 180°W..180°E or C{0} for I{polar} regions or (C{str})
96 with the zone number and I{latitudinal} band letter.
97 @arg EN: Two-letter EN digraph (C{str}), grid tile I{using only}
98 the I{AA} aka I{MGRS-New} (row) U{lettering scheme
99 <http://Wikipedia.org/wiki/Military_Grid_Reference_System>}.
100 @kwarg easting: Easting (C{meter}), within 100 km grid tile.
101 @kwarg northing: Northing (C{meter}), within 100 km grid tile.
102 @kwarg band: Optional, I{latitudinal} band or I{polar} region letter
103 (C{str}), 'C'|..|'X' covering 80°S..84°N (no 'I'|'O'),
104 'A'|'B' at the south or 'Y'|'Z' at the north pole.
105 @kwarg datum: This reference's datum (L{Datum}, L{Ellipsoid},
106 L{Ellipsoid2} or L{a_f2Tuple}).
107 @kwarg resolution: Optional resolution (C{meter}), C{0} for default.
108 @kwarg name: Optional name (C{str}).
110 @raise MGRSError: Invalid B{C{zone}}, B{C{EN}}, B{C{easting}},
111 B{C{northing}}, B{C{band}} or B{C{resolution}}.
113 @raise TypeError: Invalid B{C{datum}}.
115 @example:
117 >>> from pygeodesy import Mgrs
118 >>> m = Mgrs('31U', 'DQ', 48251, 11932) # 31U DQ 48251 11932
119 >>> m = Mgrs() # defaults to south pole
120 >>> m.toLatLon() # ... lat=-90.0, lon=0.0, datum=...
121 '''
122 if name:
123 self.name = name
125 if not (zone or EN or band):
126 EN, band = _AN_, _B_ # default, south pole
127 try:
128 self._zone, self._band, self._bandLat = _to3zBlat(zone, band, Error=MGRSError)
129 en = str(EN)
130 if len(en) != 2 or not en.isalpha():
131 raise ValueError # caught below
132 self._EN = en.upper()
133 _ = self._EN2m # check E and N
134 except (IndexError, KeyError, TypeError, ValueError):
135 raise MGRSError(band=band, EN=EN, zone=zone)
137 self._easting = Easting(easting, Error=MGRSError)
138 self._northing = Northing(northing, Error=MGRSError)
139 if datum not in (None, Mgrs._datum):
140 self._datum = _ellipsoidal_datum(datum, name=name) # XXX raiser=_datum_
142 if resolution:
143 self.resolution = resolution
145 def __str__(self):
146 return self.toStr(sep=_SPACE_) # for backward compatibility
148 @property_RO
149 def band(self):
150 '''Get the I{latitudinal} band C{'C'|..|'X'} (no C{'I'|'O'})
151 or I{polar} region C{'A'|'B'|'Y'|'Z'}) letter (C{str}).
152 '''
153 return self._band
155 @Property_RO
156 def bandLatitude(self):
157 '''Get the band latitude (C{degrees90}).
158 '''
159 return self._bandLat
161 @Property_RO
162 def datum(self):
163 '''Get the datum (L{Datum}).
164 '''
165 return self._datum
167 @deprecated_property_RO
168 def digraph(self):
169 '''DEPRECATED, use property C{EN}.'''
170 return self.EN
172 @property_RO
173 def EN(self):
174 '''Get the 2-letter grid tile (C{str}).
175 '''
176 return self._EN
178 @deprecated_property_RO
179 def en100k(self):
180 '''DEPRECATED, use property C{EN}.'''
181 return self.EN
183 @Property_RO
184 def _EN2m(self):
185 '''(INTERNAL) Get the grid 2-tuple (easting, northing) in C{meter}.
187 @note: Raises AssertionError, IndexError or KeyError: Invalid
188 C{zone} number, C{EN} letter or I{polar} region letter.
189 '''
190 EN = self.EN
191 if self.isUTM:
192 i = self.zone - 1
193 # get easting from the E column (note, +1 because
194 # easting starts at 166e3 due to 500 km falsing)
195 e = _LeUTM[i % 3].index(EN[0]) + 1
196 # similarly, get northing from the N row
197 n = _LnUTM[i % 2].index(EN[1])
198 elif self.isUPS:
199 B = self.band
200 e = _LeUPS[B].index(EN[0]) + _FeUPS[B]
201 n = _LnUPS[B].index(EN[1]) + _FnUPS[B]
202 else:
203 raise _AssertionError(zone=self.zone)
204 return float(e * _100km), float(n * _100km) # meter
206 @property_RO
207 def easting(self):
208 '''Get the easting (C{meter} within grid tile).
209 '''
210 return self._easting
212 @Property_RO
213 def eastingnorthing(self):
214 '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)})
215 I{within} the MGRS grid tile, both in C{meter}.
216 '''
217 return EasNor2Tuple(self.easting, self.northing)
219 @Property_RO
220 def isUPS(self):
221 '''Is this MGRS in a (polar) UPS zone (C{bool}).
222 '''
223 return self._zone == _UPS_ZONE
225 @Property_RO
226 def isUTM(self):
227 '''Is this MGRS in a (non-polar) UTM zone (C{bool}).
228 '''
229 return _UTM_ZONE_MIN <= self._zone <= _UTM_ZONE_MAX
231 @property_RO
232 def northing(self):
233 '''Get the northing (C{meter} within grid tile).
234 '''
235 return self._northing
237 @Property_RO
238 def northingBottom(self):
239 '''Get the northing of the band bottom (C{meter}).
240 '''
241 a = self.bandLatitude
242 u = toUtm8(a, 0, datum=self.datum, Utm=None) if self.isUTM else \
243 toUps8(a, 0, datum=self.datum, Ups=None)
244 return int(u.northing / _100km) * _100km
246 def parse(self, strMGRS, name=NN):
247 '''Parse a string to a similar L{Mgrs} instance.
249 @arg strMGRS: The MGRS reference (C{str}),
250 see function L{parseMGRS}.
251 @kwarg name: Optional instance name (C{str}),
252 overriding this name.
254 @return: The similar instance (L{Mgrs}).
256 @raise MGRSError: Invalid B{C{strMGRS}}.
257 '''
258 return parseMGRS(strMGRS, datum=self.datum, Mgrs=self.classof,
259 name=name or self.name)
261 @property
262 def resolution(self):
263 '''Get the MGRS resolution (C{meter}, power of 10)
264 or C{0} if undefined.
265 '''
266 return self._resolution
268 @resolution.setter # PYCHOK setter!
269 def resolution(self, resolution):
270 '''Set the MGRS resolution (C{meter}, power of 10)
271 or C{0} to undefine and disable UPS/UTM centering.
273 @raise MGRSError: Invalid B{C{resolution}}, over
274 C{1.e+5} or under C{1.e-6}.
275 '''
276 if resolution: # and resolution > 0
277 r = _resolution10(resolution, Error=MGRSError)
278 else:
279 r = 0
280 if self._resolution != r:
281 self._resolution = r
283 @Property_RO
284 def tilesize(self):
285 '''Get the MGRS grid tile size (C{meter}).
286 '''
287 assert _MODS.utmups._MGRS_TILE is _100km
288 return _100km
290 def toLatLon(self, LatLon=None, center=True, **toLatLon_kwds):
291 '''Convert this MGRS grid reference to a UTM coordinate.
293 @kwarg LatLon: Optional, ellipsoidal class to return the
294 geodetic point (C{LatLon}) or C{None}.
295 @kwarg center: Optionally, return the grid's center or
296 lower left corner (C{bool}).
297 @kwarg toLatLon_kwds: Optional, additional L{Utm.toLatLon}
298 and B{C{LatLon}} keyword arguments.
300 @return: A B{C{LatLon}} instance or if C{B{LatLon} is None}
301 a L{LatLonDatum5Tuple}C{(lat, lon, datum, gamma,
302 scale)}.
304 @raise TypeError: If B{C{LatLon}} is not ellipsoidal.
306 @raise UTMError: Invalid meridional radius or H-value.
308 @see: Methods L{Mgrs.toUtm} and L{Utm.toLatLon}.
309 '''
310 u = self.toUtmUps(center=center)
311 return u.toLatLon(LatLon=LatLon, **toLatLon_kwds)
313 def toRepr(self, fmt=Fmt.SQUARE, sep=_COMMASPACE_, **prec): # PYCHOK expected
314 '''Return a string representation of this MGRS grid reference.
316 @kwarg fmt: Enclosing backets format (C{str}).
317 @kwarg sep: Separator between name:values (C{str}).
318 @kwarg prec: Precision (C{int}), see method L{Mgrs.toStr}.
320 @return: This Mgrs as "[Z:[dd]B, G:EN, E:easting, N:northing]"
321 (C{str}), with C{B{sep} ", "}.
323 @note: MGRS grid references are truncated, not rounded (unlike
324 UTM/UPS coordinates).
326 @raise ValueError: Invalid B{C{prec}}.
327 '''
328 t = self.toStr(sep=None, **prec)
329 return _xzipairs('ZGEN', t, sep=sep, fmt=fmt)
331 def toStr(self, prec=0, sep=NN): # PYCHOK expected
332 '''Return this MGRS grid reference as a string.
334 @kwarg prec: Precision, the number of I{decimal} digits (C{int}) or if
335 negative, the number of I{units to drop}, like MGRS U{PRECISION
336 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}.
337 @kwarg sep: Optional separator to join (C{str}) or C{None} to return an unjoined
338 3-C{tuple} of C{str}s.
340 @return: This Mgrs as 4-tuple C{("dd]B", "EN", "easting", "northing")} if C{B{sep}=NN}
341 or "[dd]B EN easting northing" (C{str}) with C{B{sep} " "}.
343 @note: Both C{easting} and C{northing} strings are C{NN} or missing if C{B{prec} <= -5}.
345 @note: MGRS grid references are truncated, not rounded (unlike UTM/UPS).
347 @raise ValueError: Invalid B{C{prec}}.
349 @example:
351 >>> from pygeodesy import Mgrs, NN, parseMGRS
352 >>> m = Mgrs(31, 'DQ', 48251, 11932, band='U')
353 >>> m.toStr() # '31U DQ 48251 11932'
354 >>> m = parseMGRS('BAN1234567890')
355 >>> str(m) # 'B AN 12345 67890'
356 >>> m.toStr() # 'BAN1234567890'
357 >>> m.toStr(prec=-2) # 'BAN123678'
358 '''
359 zB = self.zoneB
360 t = enstr2(self._easting, self._northing, prec, zB, self.EN)
361 return t if sep is None else sep.join(t).rstrip()
363 def toUps(self, Ups=Ups, center=False):
364 '''Convert this MGRS grid reference to a UPS coordinate.
366 @kwarg Ups: Optional class to return the UPS coordinate
367 (L{Ups}) or C{None}.
368 @kwarg center: Optionally, center easting and northing
369 by the resolution (C{bool}).
371 @return: A B{C{Ups}} instance or if C{B{Ups} is None}
372 a L{UtmUps5Tuple}C{(zone, hemipole, easting,
373 northing, band)}.
375 @raise MGRSError: This MGRS is a I{non-polar} UTM reference.
376 '''
377 if self.isUTM:
378 raise MGRSError(zoneB=self.zoneB, txt=_not_(_polar_))
379 return self._toUtmUps(Ups, center)
381 def toUtm(self, Utm=Utm, center=False):
382 '''Convert this MGRS grid reference to a UTM coordinate.
384 @kwarg Utm: Optional class to return the UTM coordinate
385 (L{Utm}) or C{None}.
386 @kwarg center: Optionally, center easting and northing
387 by the resolution (C{bool}).
389 @return: A B{C{Utm}} instance or if C{B{Utm} is None}
390 a L{UtmUps5Tuple}C{(zone, hemipole, easting,
391 northing, band)}.
393 @raise MGRSError: This MGRS is a I{polar} UPS reference.
394 '''
395 if self.isUPS:
396 raise MGRSError(zoneB=self.zoneB, txt=_polar_)
397 return self._toUtmUps(Utm, center)
399 def toUtmUps(self, Utm=Utm, Ups=Ups, center=False):
400 '''Convert this MGRS grid reference to a UTM or UPS coordinate.
402 @kwarg Utm: Optional class to return the UTM coordinate
403 (L{Utm}) or C{None}.
404 @kwarg Ups: Optional class to return the UPS coordinate
405 (L{Utm}) or C{None}.
406 @kwarg center: Optionally, center easting and northing
407 by the resolution (C{bool}).
409 @return: A B{C{Utm}} or B{C{Ups}} instance or if C{B{Utm}
410 or B{Ups} is None} a L{UtmUps5Tuple}C{(zone,
411 hemipole, easting, northing, band)}.
412 '''
413 return self._toUtmUps((Utm if self.isUTM else
414 (Ups if self.isUPS else None)), center)
416 def _toUtmUps(self, U, center):
417 '''(INTERNAL) Helper for C{.toUps} and C{.toUtm}.
418 '''
419 e, n = self._EN2m
420 e += self.easting
421 n += self.northing
422 if self.isUTM:
423 # 100 km row letters repeat every 2,000 km north;
424 # add 2,000 km blocks to get into required band
425 b = (self.northingBottom - n) / _2000km
426 if b > 0:
427 b = int(b) + 1
428 b = min(b, (3 if self.band == _W_ else 4))
429 n += b * _2000km
430 if center:
431 c = self.resolution
432 if c:
433 c *= _0_5
434 e += c
435 n += c
436 z = self.zone
437 h = _hemi(self.bandLatitude) # _S_ if self.band < _N_ else _N_
438 B = self.band
439 m = self.name
440 return UtmUps5Tuple(z, h, e, n, B, name=m, Error=MGRSError) if U is None \
441 else U(z, h, e, n, B, name=m, datum=self.datum)
443 @property_RO
444 def zone(self):
445 '''Get the I{longitudinal} zone (C{int}), 1..60 or 0 for I{polar}.
446 '''
447 return self._zone
449 @Property_RO
450 def zoneB(self):
451 '''Get the I{polar} region letter or the I{longitudinal} zone digits
452 plus I{latitudinal} band letter (C{str}).
453 '''
454 return self.band if self.isUPS else NN(Fmt.zone(self.zone), self.band)
457class Mgrs4Tuple(_NamedTuple):
458 '''4-Tuple C{(zone, EN, easting, northing)}, C{zone} and grid
459 tile C{EN} as C{str}, C{easting} and C{northing} in C{meter}.
461 @note: The C{zone} consists of either the I{longitudinal} zone
462 number plus the I{latitudinal} band letter or only the
463 I{polar} region letter.
464 '''
465 _Names_ = (_zone_, 'EN', _easting_, _northing_)
466 _Units_ = ( Str, Str, Easting, Northing)
468 @deprecated_property_RO
469 def digraph(self):
470 '''DEPRECATED, use attribute C{EN}.'''
471 return self.EN # PYCHOK or [1]
473 def toMgrs(self, **Mgrs_and_kwds):
474 '''Return this L{Mgrs4Tuple} as an L{Mgrs} instance.
475 '''
476 return self.to6Tuple(NN, _WGS84).toMgrs(**Mgrs_and_kwds)
478 def to6Tuple(self, band=NN, datum=_WGS84):
479 '''Extend this L{Mgrs4Tuple} to a L{Mgrs6Tuple}.
481 @kwarg band: The band (C{str}).
482 @kwarg datum: The datum (L{Datum}).
484 @return: An L{Mgrs6Tuple}C{(zone, EN, easting,
485 northing, band, datum)}.
486 '''
487 z = self.zone # PYCHOK or [0]
488 B = z[-1:]
489 if B.isalpha():
490 z = z[:-1] or Fmt.zone(0)
491 t = Mgrs6Tuple(z, self.EN, self.easting, self.northing, # PYCHOK attrs
492 band or B, datum, name=self.name)
493 else:
494 t = self._xtend(Mgrs6Tuple, band, datum)
495 return t
498class Mgrs6Tuple(_NamedTuple): # XXX only used above
499 '''6-Tuple C{(zone, EN, easting, northing, band, datum)}, with
500 C{zone}, grid tile C{EN} and C{band} as C{str}, C{easting}
501 and C{northing} in C{meter} and C{datum} a L{Datum}.
503 @note: The C{zone} is the I{longitudinal} zone C{"01".."60"}
504 or C{"00"} for I{polar} regions and C{band} is the
505 I{latitudinal} band or I{polar} region letter.
506 '''
507 _Names_ = Mgrs4Tuple._Names_ + (_band_, _datum_)
508 _Units_ = Mgrs4Tuple._Units_ + ( Str, _Pass)
510 @deprecated_property_RO
511 def digraph(self):
512 '''DEPRECATED, use attribute C{EN}.'''
513 return self.EN # PYCHOK or [1]
515 def toMgrs(self, Mgrs=Mgrs, **Mgrs_kwds):
516 '''Return this L{Mgrs6Tuple} as an L{Mgrs} instance.
517 '''
518 kwds = dict(self.items())
519 if self.name:
520 kwds.update(name=self.name)
521 if Mgrs_kwds:
522 kwds.update(Mgrs_kwds)
523 return Mgrs(**kwds)
526class _RE(object):
527 '''(INTERNAL) Lazily compiled C{re}gex-es to parse MGRS strings.
528 '''
529 _EN = '([A-Z]{2})' # 2-letter grid tile designation
530 _en = '([0-9]+)' # easting_northing digits, 2-10+
531 _pB = '([ABYZ]{1})' # polar region letter, pseudo-zone 0
532 _zB = '([0-9]{1,2}[C-X]{1})' # zone number and band letter, no I|O
534 @Property_RO
535 def pB_EN(self): # split polar "BEN" into 2 parts
536 import re # PYCHOK warning locale.Error
537 return re.compile(_RE._pB + _RE._EN, re.IGNORECASE)
539 @Property_RO
540 def pB_EN_en(self): # split polar "BEN1235..." into 3 parts
541 import re # PYCHOK warning locale.Error
542 return re.compile(_RE._pB + _RE._EN + _RE._en, re.IGNORECASE)
544 @Property_RO
545 def zB_EN(self): # split "1[2]BEN" into 2 parts
546 import re # PYCHOK warning locale.Error
547 return re.compile(_RE._zB + _RE._EN, re.IGNORECASE)
549 @Property_RO
550 def zB_EN_en(self): # split "1[2]BEN1235..." into 3 parts
551 import re # PYCHOK warning locale.Error
552 return re.compile(_RE._zB + _RE._EN + _RE._en, re.IGNORECASE)
554_RE = _RE() # PYCHOK singleton
557def parseMGRS(strMGRS, datum=_WGS84, Mgrs=Mgrs, name=NN):
558 '''Parse a string representing a MGRS grid reference,
559 consisting of C{"[zone]Band, EN, easting, northing"}.
561 @arg strMGRS: MGRS grid reference (C{str}).
562 @kwarg datum: Optional datum to use (L{Datum}).
563 @kwarg Mgrs: Optional class to return the MGRS grid
564 reference (L{Mgrs}) or C{None}.
565 @kwarg name: Optional B{C{Mgrs}} name (C{str}).
567 @return: The MGRS grid reference as B{C{Mgrs}} or if
568 C{B{Mgrs} is None} as an L{Mgrs4Tuple}C{(zone,
569 EN, easting, northing)}.
571 @raise MGRSError: Invalid B{C{strMGRS}}.
573 @example:
575 >>> m = parseMGRS('31U DQ 48251 11932')
576 >>> str(m) # '31U DQ 48251 11932'
577 >>> m = parseMGRS('31UDQ4825111932')
578 >>> repr(m) # [Z:31U, G:DQ, E:48251, N:11932]
579 >>> m = parseMGRS('42SXD0970538646')
580 >>> str(m) # '42S XD 09705 38646'
581 >>> m = parseMGRS('42SXD9738') # Km
582 >>> str(m) # '42S XD 97000 38000'
583 >>> m = parseMGRS('YUB17770380') # polar
584 >>> str(m) # 'Y UB 17770 03800'
585 '''
586 def _mg(s, re_UTM, re_UPS): # return re.match groups
587 m = re_UTM.match(s)
588 if m:
589 return m.groups()
590 m = re_UPS.match(s.lstrip(_0_))
591 if m:
592 return m.groups()
593# m = m.groups()
594# t = '00' + m[0]
595# return (t,) + m[1:]
596 raise ValueError(_SPACE_(repr(s), _invalid_))
598 def _MGRS(strMGRS, datum, Mgrs, name):
599 m = _splituple(strMGRS.strip())
600 if len(m) == 1: # [01]BEN1234512345'
601 m = _mg(m[0], _RE.zB_EN_en, _RE.pB_EN_en)
602 m = m[:2] + halfs2(m[2])
603 elif len(m) == 2: # [01]BEN 1234512345'
604 m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + halfs2(m[1])
605 elif len(m) == 3: # [01]BEN 12345 12345'
606 m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + m[1:]
607 if len(m) != 4: # [01]B EN 12345 12345
608 raise ValueError
610 zB, EN = m[0].upper(), m[1].upper()
611 if zB[-1:] in 'IO':
612 raise ValueError(_SPACE_(repr(m[0]), _invalid_))
613 e, n, m = _enstr2m3(*m[2:])
615 if Mgrs is None:
616 r = Mgrs4Tuple(zB, EN, e, n, name=name)
617 _ = r.toMgrs(resolution=m) # validate
618 else:
619 r = Mgrs(zB, EN, e, n, datum=datum, resolution=m, name=name)
620 return r
622 return _parseX(_MGRS, strMGRS, datum, Mgrs, name,
623 strMGRS=strMGRS, Error=MGRSError)
626def toMgrs(utmups, Mgrs=Mgrs, name=NN, **Mgrs_kwds):
627 '''Convert a UTM or UPS coordinate to an MGRS grid reference.
629 @arg utmups: A UTM or UPS coordinate (L{Utm}, L{Etm} or L{Ups}).
630 @kwarg Mgrs: Optional class to return the MGRS grid reference
631 (L{Mgrs}) or C{None}.
632 @kwarg name: Optional B{C{Mgrs}} name (C{str}).
633 @kwarg Mgrs_kwds: Optional, additional B{C{Mgrs}} keyword
634 arguments, ignored if C{B{Mgrs} is None}.
636 @return: The MGRS grid reference as B{C{Mgrs}} or if
637 C{B{Mgrs} is None} as an L{Mgrs6Tuple}C{(zone,
638 EN, easting, northing, band, datum)}.
640 @raise MGRSError: Invalid B{C{utmups}}.
642 @raise TypeError: If B{C{utmups}} is not L{Utm} nor L{Etm}
643 nor L{Ups}.
645 @example:
647 >>> u = Utm(31, 'N', 448251, 5411932)
648 >>> m = u.toMgrs() # 31U DQ 48251 11932
649 '''
650# _MODS.utmups.utmupsValidate(utmups, MGRS=True, Error-MGRSError)
651 _xinstanceof(Utm, Ups, utmups=utmups) # Utm, Etm, Ups
652 try:
653 e, n = utmups.eastingnorthing2(falsed=True)
654 E, e = _um100km2(e)
655 N, n = _um100km2(n)
656 B, z = utmups.band, utmups.zone
657 if _UTM_ZONE_MIN <= z <= _UTM_ZONE_MAX:
658 i = z - 1
659 # columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z,
660 # then repeating every 3rd zone (note E-1 because
661 # eastings start at 166e3 due to 500km false origin)
662 EN = _LeUTM[i % 3][E - 1]
663 # rows in even zones are A-V, in odd zones are F-E
664 EN += _LnUTM[i % 2][N % len(_LnUTM[0])]
665 elif z == _UPS_ZONE:
666 EN = _LeUPS[B][E - _FeUPS[B]]
667 EN += _LnUPS[B][N - _FnUPS[B]]
668 else:
669 raise _ValueError(zone=z)
670 except (IndexError, TypeError, ValueError) as x:
671 raise MGRSError(B=B, E=E, N=N, utmups=utmups, cause=x)
673 if Mgrs is None:
674 r = Mgrs4Tuple(Fmt.zone(z), EN, e, n).to6Tuple(B, utmups.datum)
675 else:
676 kwds = _xkwds(Mgrs_kwds, band=B, datum=utmups.datum)
677 r = Mgrs(z, EN, e, n, **kwds)
678 return _xnamed(r, name or utmups.name)
681def _um100km2(m):
682 '''(INTERNAL) An MGRS east-/northing truncated to micrometer (um)
683 precision and to grid tile C{M} and C{m}eter within the tile.
684 '''
685 m = int(m / _1um) * _1um # micrometer
686 M, m = divmod(m, _100km)
687 return int(M), m
690if __name__ == '__main__':
692 from pygeodesy.ellipsoidalVincenty import fabs, LatLon
693 from pygeodesy.lazily import _getenv, printf
695# from math import fabs # from .ellipsoidalVincenty
696 from os import access as _access, linesep as _NL, X_OK as _X_OK
698 # <https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html>
699 _GeoConvert = _getenv('PYGEODESY_GEOCONVERT', '/opt/local/bin/GeoConvert')
700 if _access(_GeoConvert, _X_OK):
701 GC_m = _GeoConvert, '-m' # -m converts latlon to MGRS
702 printf(' using: %s ...', _SPACE_.join(GC_m))
703 from pygeodesy.solveBase import _popen2
704 else:
705 GC_m = _popen2 = None
707 e = n = 0
708 try:
709 for lat in range(-90, 91, 1):
710 printf('%6s: lat %s ...', n, lat, end=NN, flush=True)
711 nl = _NL
712 for lon in range(-180, 181, 1):
713 m = LatLon(lat, lon).toMgrs()
714 if _popen2:
715 t = '%s %s' % (lat, lon)
716 g = _popen2(GC_m, stdin=t)[1]
717 t = m.toStr() # sep=NN
718 if t != g:
719 e += 1
720 printf('%s%6s: %s: %r vs %r (lon %s)', nl, -e, m, t, g, lon)
721 nl = NN
722 t = m.toLatLon(LatLon=LatLon)
723 d = max(fabs(t.lat - lat), fabs(t.lon - lon))
724 if d > 1e-9 and -90 < lat < 90 and -180 < lon < 180:
725 e += 1
726 printf('%s%6s: %s: %s vs %s %.6e', nl, -e, m, t.latlon, (float(lat), float(lon)), d)
727 nl = NN
728 n += 1
729 if nl:
730 print(' OK')
731 except KeyboardInterrupt:
732 printf(nl)
734 p = e * 100.0 / n
735 printf('%6s: %s errors (%.2f%%)', n, (e if e else 'no'), p)
737# **) MIT License
738#
739# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
740#
741# Permission is hereby granted, free of charge, to any person obtaining a
742# copy of this software and associated documentation files (the "Software"),
743# to deal in the Software without restriction, including without limitation
744# the rights to use, copy, modify, merge, publish, distribute, sublicense,
745# and/or sell copies of the Software, and to permit persons to whom the
746# Software is furnished to do so, subject to the following conditions:
747#
748# The above copyright notice and this permission notice shall be included
749# in all copies or substantial portions of the Software.
750#
751# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
752# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
753# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
754# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
755# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
756# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
757# OTHER DEALINGS IN THE SOFTWARE.