Coverage for pygeodesy/mgrs.py: 99%

264 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-08-20 14:00 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Military Grid Reference System (MGRS/NATO) references. 

5 

6Classes L{Mgrs}, L{Mgrs4Tuple} and L{Mgrs6Tuple} and functions L{parseMGRS} 

7and L{toMgrs}. 

8 

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>}. 

15 

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). 

24 

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>}. 

28 

29See module L{pygeodesy.ups} for env variable C{PYGEODESY_UPS_POLES} determining 

30the UPS encoding I{at} the south and north pole. 

31 

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''' 

37 

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 

57 

58__all__ = _ALL_LAZY.mgrs 

59__version__ = '23.05.26' 

60 

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_) 

75 

76 

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 

89 

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. 

93 

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}). 

109 

110 @raise MGRSError: Invalid B{C{zone}}, B{C{EN}}, B{C{easting}}, 

111 B{C{northing}}, B{C{band}} or B{C{resolution}}. 

112 

113 @raise TypeError: Invalid B{C{datum}}. 

114 

115 @example: 

116 

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 

124 

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) 

136 

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_ 

141 

142 if resolution: 

143 self.resolution = resolution 

144 

145 def __str__(self): 

146 return self.toStr(sep=_SPACE_) # for backward compatibility 

147 

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 

154 

155 @Property_RO 

156 def bandLatitude(self): 

157 '''Get the band latitude (C{degrees90}). 

158 ''' 

159 return self._bandLat 

160 

161 @Property_RO 

162 def datum(self): 

163 '''Get the datum (L{Datum}). 

164 ''' 

165 return self._datum 

166 

167 @deprecated_property_RO 

168 def digraph(self): 

169 '''DEPRECATED, use property C{EN}.''' 

170 return self.EN 

171 

172 @property_RO 

173 def EN(self): 

174 '''Get the 2-letter grid tile (C{str}). 

175 ''' 

176 return self._EN 

177 

178 @deprecated_property_RO 

179 def en100k(self): 

180 '''DEPRECATED, use property C{EN}.''' 

181 return self.EN 

182 

183 @Property_RO 

184 def _EN2m(self): 

185 '''(INTERNAL) Get the grid 2-tuple (easting, northing) in C{meter}. 

186 

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 

205 

206 @property_RO 

207 def easting(self): 

208 '''Get the easting (C{meter} within grid tile). 

209 ''' 

210 return self._easting 

211 

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) 

218 

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 

224 

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 

230 

231 @property_RO 

232 def northing(self): 

233 '''Get the northing (C{meter} within grid tile). 

234 ''' 

235 return self._northing 

236 

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 

245 

246 def parse(self, strMGRS, name=NN): 

247 '''Parse a string to a similar L{Mgrs} instance. 

248 

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. 

253 

254 @return: The similar instance (L{Mgrs}). 

255 

256 @raise MGRSError: Invalid B{C{strMGRS}}. 

257 ''' 

258 return parseMGRS(strMGRS, datum=self.datum, Mgrs=self.classof, 

259 name=name or self.name) 

260 

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 

267 

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. 

272 

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 

282 

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 

289 

290 def toLatLon(self, LatLon=None, center=True, **toLatLon_kwds): 

291 '''Convert this MGRS grid reference to a UTM coordinate. 

292 

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. 

299 

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)}. 

303 

304 @raise TypeError: If B{C{LatLon}} is not ellipsoidal. 

305 

306 @raise UTMError: Invalid meridional radius or H-value. 

307 

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) 

312 

313 def toRepr(self, fmt=Fmt.SQUARE, sep=_COMMASPACE_, **prec): # PYCHOK expected 

314 '''Return a string representation of this MGRS grid reference. 

315 

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}. 

319 

320 @return: This Mgrs as "[Z:[dd]B, G:EN, E:easting, N:northing]" 

321 (C{str}), with C{B{sep} ", "}. 

322 

323 @note: MGRS grid references are truncated, not rounded (unlike 

324 UTM/UPS coordinates). 

325 

326 @raise ValueError: Invalid B{C{prec}}. 

327 ''' 

328 t = self.toStr(sep=None, **prec) 

329 return _xzipairs('ZGEN', t, sep=sep, fmt=fmt) 

330 

331 def toStr(self, prec=0, sep=NN): # PYCHOK expected 

332 '''Return this MGRS grid reference as a string. 

333 

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. 

339 

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} " "}. 

342 

343 @note: Both C{easting} and C{northing} strings are C{NN} or missing if C{B{prec} <= -5}. 

344 

345 @note: MGRS grid references are truncated, not rounded (unlike UTM/UPS). 

346 

347 @raise ValueError: Invalid B{C{prec}}. 

348 

349 @example: 

350 

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() 

362 

363 def toUps(self, Ups=Ups, center=False): 

364 '''Convert this MGRS grid reference to a UPS coordinate. 

365 

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}). 

370 

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)}. 

374 

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) 

380 

381 def toUtm(self, Utm=Utm, center=False): 

382 '''Convert this MGRS grid reference to a UTM coordinate. 

383 

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}). 

388 

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)}. 

392 

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) 

398 

399 def toUtmUps(self, Utm=Utm, Ups=Ups, center=False): 

400 '''Convert this MGRS grid reference to a UTM or UPS coordinate. 

401 

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}). 

408 

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) 

415 

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) 

442 

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 

448 

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) 

455 

456 

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}. 

460 

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) 

467 

468 @deprecated_property_RO 

469 def digraph(self): 

470 '''DEPRECATED, use attribute C{EN}.''' 

471 return self.EN # PYCHOK or [1] 

472 

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) 

477 

478 def to6Tuple(self, band=NN, datum=_WGS84): 

479 '''Extend this L{Mgrs4Tuple} to a L{Mgrs6Tuple}. 

480 

481 @kwarg band: The band (C{str}). 

482 @kwarg datum: The datum (L{Datum}). 

483 

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 

496 

497 

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}. 

502 

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) 

509 

510 @deprecated_property_RO 

511 def digraph(self): 

512 '''DEPRECATED, use attribute C{EN}.''' 

513 return self.EN # PYCHOK or [1] 

514 

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) 

524 

525 

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 

533 

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) 

538 

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) 

543 

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) 

548 

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) 

553 

554_RE = _RE() # PYCHOK singleton 

555 

556 

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"}. 

560 

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}). 

566 

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)}. 

570 

571 @raise MGRSError: Invalid B{C{strMGRS}}. 

572 

573 @example: 

574 

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_)) 

597 

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 

609 

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:]) 

614 

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 

621 

622 return _parseX(_MGRS, strMGRS, datum, Mgrs, name, 

623 strMGRS=strMGRS, Error=MGRSError) 

624 

625 

626def toMgrs(utmups, Mgrs=Mgrs, name=NN, **Mgrs_kwds): 

627 '''Convert a UTM or UPS coordinate to an MGRS grid reference. 

628 

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}. 

635 

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)}. 

639 

640 @raise MGRSError: Invalid B{C{utmups}}. 

641 

642 @raise TypeError: If B{C{utmups}} is not L{Utm} nor L{Etm} 

643 nor L{Ups}. 

644 

645 @example: 

646 

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) 

672 

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) 

679 

680 

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 

688 

689 

690if __name__ == '__main__': 

691 

692 from pygeodesy.ellipsoidalVincenty import fabs, LatLon 

693 from pygeodesy.lazily import _getenv, printf 

694 

695# from math import fabs # from .ellipsoidalVincenty 

696 from os import access as _access, linesep as _NL, X_OK as _X_OK 

697 

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 

706 

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 printf(' OK') 

731 except KeyboardInterrupt: 

732 printf(nl) 

733 

734 p = e * 100.0 / n 

735 printf('%6s: %s errors (%.2f%%)', n, (e if e else 'no'), p) 

736 

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.