Coverage for pygeodesy/mgrs.py : 99%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: utf-8 -*-
u'''Military Grid Reference System (MGRS/NATO) references.
Classes L{Mgrs}, L{Mgrs4Tuple} and L{Mgrs6Tuple} and functions L{parseMGRS} and L{toMgrs}.
Pure Python implementation of MGRS, UTM and UPS conversions covering the entire I{ellipsoidal} earth, transcoded from I{Chris Veness}' JavaScript originals U{MGRS <https://www.Movable-Type.co.UK/scripts/latlong-utm-mgrs.html>} and U{Module mgrs <https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-mgrs.html>} and from I{Charles Karney}'s C++ class U{MGRS<https://GeographicLib.SourceForge.io/C++/doc/ classGeographicLib_1_1MGRS.html>}.
MGRS references comprise a grid zone designation (GZD), a 100 km grid (square) tile identification and an easting and northing (in C{meter}). The GZD consists of a longitudinal zone (or column) I{number} and latitudinal band (row) I{letter} in the UTM region between 80°S and 84°N. Each zone (column) is 6° wide and each band (row) is 8° high, except top band 'X' is 12° tall. In UPS polar regions below 80°S and above 84°N the GZD contains only a single I{letter}, C{'A'} or C{'B'} near the south and C{'Y'} or C{'Z'} around the north pole (for west respectively east longitudes).
See also the U{United States National Grid<https://www.FGDC.gov/standards/projects/ FGDC-standards-projects/usng/fgdc_std_011_2001_usng.pdf>} and U{Military Grid Reference System<https://WikiPedia.org/wiki/Military_grid_reference_system>}.
See module L{pygeodesy.ups} for env variable C{PYGEODESY_UPS_POLES} determining the UPS encoding I{at} the south and north pole.
Set env variable C{PYGEODESY_GEOCONVERT} to the (fully qualified) path of the C{GeoConvert} executable to run this module as I{python[3] -m pygeodesy.mgrs} and compare the MGRS results with those from I{Karney}'s utility U{GeoConvert <https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html>}. '''
# from pygeodesy.constants import _0_5 # from .units _ValueError, _xkwds, _ALL_LAZY, _MODS _COMMASPACE_, _datum_, _easting_, _invalid_, \ _northing_, _not_, _SPACE_, _splituple, _W_, \ _Y_, _Z_, _zone_ # from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .errors # from pygeodesy.utmupsBase import _UTM_ZONE_MAX, _UTM_ZONE_MIN # from .utm
# <https://GitHub.com/hrbrmstr/mgrs/blob/master/src/mgrs.c> # 100 km grid tile UTM column (E) letters, repeating every third zone # 100 km grid tile UPS column (E) letters for each polar zone # 100 km grid tile UTM and UPS row (N) letters, repeating every other zone
'''Military Grid Reference System (MGRS/NATO) references, with method to convert to UTM coordinates. '''
datum=_WGS84, resolution=0, name=NN): '''New L{Mgrs} Military grid reference.
@arg zone: The 6° I{longitudinal} zone (C{int}), 1..60 covering 180°W..180°E or C{0} for I{polar} regions or (C{str}) with the zone number and I{latitudinal} band letter. @arg EN: Two-letter EN digraph (C{str}), grid tile I{using only} the I{AA} aka I{MGRS-New} (row) U{lettering scheme <http://Wikipedia.org/wiki/Military_Grid_Reference_System>}. @kwarg easting: Easting (C{meter}), within 100 km grid tile. @kwarg northing: Northing (C{meter}), within 100 km grid tile. @kwarg band: Optional, I{latitudinal} band or I{polar} region letter (C{str}), 'C'|..|'X' covering 80°S..84°N (no 'I'|'O'), 'A'|'B' at the south or 'Y'|'Z' at the north pole. @kwarg datum: This reference's datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). @kwarg resolution: Optional resolution (C{meter}), C{0} for default. @kwarg name: Optional name (C{str}).
@raise MGRSError: Invalid B{C{zone}}, B{C{EN}}, B{C{easting}}, B{C{northing}}, B{C{band}} or B{C{resolution}}.
@raise TypeError: Invalid B{C{datum}}.
@example:
>>> from pygeodesy import Mgrs >>> m = Mgrs('31U', 'DQ', 48251, 11932) # 31U DQ 48251 11932 >>> m = Mgrs() # defaults to south pole >>> m.toLatLon() # ... lat=-90.0, lon=0.0, datum=... '''
raise ValueError # caught below except (IndexError, KeyError, TypeError, ValueError): raise MGRSError(band=band, EN=EN, zone=zone)
self._datum = _ellipsoidal_datum(datum, name=name) # XXX raiser=_datum_
'''Get the I{latitudinal} band C{'C'|..|'X'} (no C{'I'|'O'}) or I{polar} region C{'A'|'B'|'Y'|'Z'}) letter (C{str}). '''
'''Get the band latitude (C{degrees90}). '''
'''Get the datum (L{Datum}). '''
'''DEPRECATED, use property C{EN}.'''
'''Get the 2-letter grid tile (C{str}). '''
'''DEPRECATED, use property C{EN}.'''
'''(INTERNAL) Get the grid 2-tuple (easting, northing) in C{meter}.
@note: Raises AssertionError, IndexError or KeyError: Invalid C{zone} number, C{EN} letter or I{polar} region letter. ''' # get easting from the E column (note, +1 because # easting starts at 166e3 due to 500 km falsing) # similarly, get northing from the N row else: raise _AssertionError(zone=self.zone)
'''Get the easting (C{meter} within grid tile). '''
'''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)}) I{within} the MGRS grid tile, both in C{meter}. '''
'''Is this MGRS in a (polar) UPS zone (C{bool}). '''
'''Is this MGRS in a (non-polar) UTM zone (C{bool}). '''
'''Get the northing (C{meter} within grid tile). '''
'''Get the northing of the band bottom (C{meter}). ''' toUps8(a, 0, datum=self.datum, Ups=None)
'''Parse a string to a similar L{Mgrs} instance.
@arg strMGRS: The MGRS reference (C{str}), see function L{parseMGRS}. @kwarg name: Optional instance name (C{str}), overriding this name.
@return: The similar instance (L{Mgrs}).
@raise MGRSError: Invalid B{C{strMGRS}}. ''' name=name or self.name)
'''Get the MGRS resolution (C{meter}, power of 10) or C{0} if undefined. '''
'''Set the MGRS resolution (C{meter}, power of 10) or C{0} to undefine and disable UPS/UTM centering.
@raise MGRSError: Invalid B{C{resolution}}, over C{1.e+5} or under C{1.e-6}. ''' else: r = 0
'''Get the MGRS grid tile size (C{meter}). '''
'''Convert this MGRS grid reference to a UTM coordinate.
@kwarg LatLon: Optional, ellipsoidal class to return the geodetic point (C{LatLon}) or C{None}. @kwarg center: Optionally, return the grid's center or lower left corner (C{bool}). @kwarg toLatLon_kwds: Optional, additional L{Utm.toLatLon} and B{C{LatLon}} keyword arguments.
@return: A B{C{LatLon}} instance or if C{B{LatLon} is None} a L{LatLonDatum5Tuple}C{(lat, lon, datum, convergence, scale)}.
@raise TypeError: If B{C{LatLon}} is not ellipsoidal.
@raise UTMError: Invalid meridional radius or H-value.
@see: Methods L{Mgrs.toUtm} and L{Utm.toLatLon}. '''
'''Return a string representation of this MGRS grid reference.
@kwarg fmt: Enclosing backets format (C{str}). @kwarg sep: Separator between name:values (C{str}). @kwarg prec: Precision (C{int}), see method L{Mgrs.toStr}.
@return: This Mgrs as "[Z:[dd]B, G:EN, E:easting, N:northing]" (C{str}), with C{B{sep} ", "}.
@note: MGRS grid references are truncated, not rounded (unlike UTM/UPS coordinates).
@raise ValueError: Invalid B{C{prec}}. '''
'''Return this MGRS grid reference as a string.
@kwarg prec: Precision, the number of I{decimal} digits (C{int}) or if negative, the number of I{units to drop}, like MGRS U{PRECISION <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}. @kwarg sep: Optional separator to join (C{str}) or C{None} to return an unjoined 3-C{tuple} of C{str}s.
@return: This Mgrs as 4-tuple C{("dd]B", "EN", "easting", "northing")} if C{B{sep}=NN} or "[dd]B EN easting northing" (C{str}) with C{B{sep} " "}.
@note: Both C{easting} and C{northing} strings are C{NN} or missing if C{B{prec} <= -5}.
@note: MGRS grid references are truncated, not rounded (unlike UTM/UPS).
@raise ValueError: Invalid B{C{prec}}.
@example:
>>> from pygeodesy import Mgrs, NN, parseMGRS >>> m = Mgrs(31, 'DQ', 48251, 11932, band='U') >>> m.toStr() # '31U DQ 48251 11932' >>> m = parseMGRS('BAN1234567890') >>> str(m) # 'B AN 12345 67890' >>> m.toStr() # 'BAN1234567890' >>> m.toStr(prec=-2) # 'BAN123678' '''
'''Convert this MGRS grid reference to a UPS coordinate.
@kwarg Ups: Optional class to return the UPS coordinate (L{Ups}) or C{None}. @kwarg center: Optionally, center easting and northing by the resolution (C{bool}).
@return: A B{C{Ups}} instance or if C{B{Ups} is None} a L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}.
@raise MGRSError: This MGRS is a I{non-polar} UTM reference. ''' raise MGRSError(zoneB=self.zoneB, txt=_not_(_polar_))
'''Convert this MGRS grid reference to a UTM coordinate.
@kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) or C{None}. @kwarg center: Optionally, center easting and northing by the resolution (C{bool}).
@return: A B{C{Utm}} instance or if C{B{Utm} is None} a L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}.
@raise MGRSError: This MGRS is a I{polar} UPS reference. ''' raise MGRSError(zoneB=self.zoneB, txt=_polar_)
'''Convert this MGRS grid reference to a UTM or UPS coordinate.
@kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) or C{None}. @kwarg Ups: Optional class to return the UPS coordinate (L{Utm}) or C{None}. @kwarg center: Optionally, center easting and northing by the resolution (C{bool}).
@return: A B{C{Utm}} or B{C{Ups}} instance or if C{B{Utm} or B{Ups} is None} a L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}. ''' (Ups if self.isUPS else None)), center)
'''(INTERNAL) Helper for C{.toUps} and C{.toUtm}. ''' # 100 km row letters repeat every 2,000 km north; # add 2,000 km blocks to get into required band else U(z, h, e, n, B, name=m, datum=self.datum)
'''Get the I{longitudinal} zone (C{int}), 1..60 or 0 for I{polar}. '''
'''Get the I{polar} region letter or the I{longitudinal} zone digits plus I{latitudinal} band letter (C{str}). '''
'''4-Tuple C{(zone, EN, easting, northing)}, C{zone} and grid tile C{EN} as C{str}, C{easting} and C{northing} in C{meter}.
@note: The C{zone} consists of either the I{longitudinal} zone number plus the I{latitudinal} band letter or only the I{polar} region letter. '''
'''DEPRECATED, use attribute C{EN}.'''
'''Return this L{Mgrs4Tuple} as an L{Mgrs} instance. '''
'''Extend this L{Mgrs4Tuple} to a L{Mgrs6Tuple}.
@kwarg band: The band (C{str}). @kwarg datum: The datum (L{Datum}).
@return: An L{Mgrs6Tuple}C{(zone, EN, easting, northing, band, datum)}. ''' band or B, datum, name=self.name) else:
'''6-Tuple C{(zone, EN, easting, northing, band, datum)}, with C{zone}, grid tile C{EN} and C{band} as C{str}, C{easting} and C{northing} in C{meter} and C{datum} a L{Datum}.
@note: The C{zone} is the I{longitudinal} zone C{"01".."60"} or C{"00"} for I{polar} regions and C{band} is the I{latitudinal} band or I{polar} region letter. '''
'''DEPRECATED, use attribute C{EN}.'''
'''Return this L{Mgrs6Tuple} as an L{Mgrs} instance. '''
'''(INTERNAL) Lazily compiled C{re}gex-es to parse MGRS strings. '''
'''Parse a string representing a MGRS grid reference, consisting of C{"[zone]Band, EN, easting, northing"}.
@arg strMGRS: MGRS grid reference (C{str}). @kwarg datum: Optional datum to use (L{Datum}). @kwarg Mgrs: Optional class to return the MGRS grid reference (L{Mgrs}) or C{None}. @kwarg name: Optional B{C{Mgrs}} name (C{str}).
@return: The MGRS grid reference as B{C{Mgrs}} or if C{B{Mgrs} is None} as an L{Mgrs4Tuple}C{(zone, EN, easting, northing)}.
@raise MGRSError: Invalid B{C{strMGRS}}.
@example:
>>> m = parseMGRS('31U DQ 48251 11932') >>> str(m) # '31U DQ 48251 11932' >>> m = parseMGRS('31UDQ4825111932') >>> repr(m) # [Z:31U, G:DQ, E:48251, N:11932] >>> m = parseMGRS('42SXD0970538646') >>> str(m) # '42S XD 09705 38646' >>> m = parseMGRS('42SXD9738') # Km >>> str(m) # '42S XD 97000 38000' >>> m = parseMGRS('YUB17770380') # polar >>> str(m) # 'Y UB 17770 03800' ''' # m = m.groups() # t = '00' + m[0] # return (t,) + m[1:] raise ValueError(_SPACE_(repr(s), _invalid_))
raise ValueError
raise ValueError(_SPACE_(repr(m[0]), _invalid_))
else:
strMGRS=strMGRS, Error=MGRSError)
'''Convert a UTM or UPS coordinate to an MGRS grid reference.
@arg utmups: A UTM or UPS coordinate (L{Utm}, L{Etm} or L{Ups}). @kwarg Mgrs: Optional class to return the MGRS grid reference (L{Mgrs}) or C{None}. @kwarg name: Optional B{C{Mgrs}} name (C{str}). @kwarg Mgrs_kwds: Optional, additional B{C{Mgrs}} keyword arguments, ignored if C{B{Mgrs} is None}.
@return: The MGRS grid reference as B{C{Mgrs}} or if C{B{Mgrs} is None} as an L{Mgrs6Tuple}C{(zone, EN, easting, northing, band, datum)}.
@raise MGRSError: Invalid B{C{utmups}}.
@raise TypeError: If B{C{utmups}} is not L{Utm} nor L{Etm} nor L{Ups}.
@example:
>>> u = Utm(31, 'N', 448251, 5411932) >>> m = u.toMgrs() # 31U DQ 48251 11932 ''' # _MODS.utmups.utmupsValidate(utmups, MGRS=True, Error-MGRSError) # columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z, # then repeating every 3rd zone (note E-1 because # eastings start at 166e3 due to 500km false origin) # rows in even zones are A-V, in odd zones are F-E else: raise _ValueError(zone=z) except (IndexError, TypeError, ValueError) as x: raise MGRSError(B=B, E=E, N=N, utmups=utmups, txt=str(x))
else:
'''(INTERNAL) An MGRS east-/northing truncated to micrometer (um) precision and to grid tile C{M} and C{m}eter within the tile. '''
if __name__ == '__main__':
from pygeodesy.ellipsoidalVincenty import LatLon from pygeodesy.lazily import _getenv, printf
from os import access as _access, linesep as _NL, X_OK as _X_OK
# <https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html> _GeoConvert = _getenv('PYGEODESY_GEOCONVERT', '/opt/local/bin/GeoConvert') if _access(_GeoConvert, _X_OK): GC_m = _GeoConvert, '-m' # -m converts latlon to MGRS printf(' using: %s ...', _SPACE_.join(GC_m)) from pygeodesy.solveBase import _popen2 else: GC_m = _popen2 = None
e = n = 0 try: for lat in range(-90, 91, 1): printf('%6s: lat %s ...', n, lat, end=NN, flush=True) nl = _NL for lon in range(-180, 181, 1): m = LatLon(lat, lon).toMgrs() if _popen2: t = '%s %s' % (lat, lon) g = _popen2(GC_m, stdin=t)[1] t = m.toStr() # sep=NN if t != g: e += 1 printf('%s%6s: %s: %r vs %r (lon %s)', nl, -e, m, t, g, lon) nl = NN t = m.toLatLon(LatLon=LatLon) d = max(abs(t.lat - lat), abs(t.lon - lon)) if d > 1e-9 and -90 < lat < 90 and -180 < lon < 180: e += 1 printf('%s%6s: %s: %s vs %s %.6e', nl, -e, m, t.latlon, (float(lat), float(lon)), d) nl = NN n += 1 if nl: print(' OK') except KeyboardInterrupt: printf(nl)
p = e * 100.0 / n printf('%6s: %s errors (%.2f%%)', n, (e if e else 'no'), p)
# **) MIT License # # Copyright (C) 2016-2022 -- mrJean1 at Gmail -- All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. |