1

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

3

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

5

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

7 and L{toMgrs}.

8

9 Pure Python implementation of MGRS, UTM and UPS conversions covering the entire

10 I{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

13 I{Charles Karney}'s C++ class U{MGRS<https://GeographicLib.SourceForge.io/C++/doc/

14 classGeographicLib_1_1MGRS.html>}.

15

16 MGRS references comprise a grid zone designation (GZD), a 100 Km grid (square)

17 tile identification and an easting and northing (in C{meter}). The GZD consists

18 of a longitudinal zone (or column) I{number} and latitudinal band (row) I{letter}

19 in the UTM region between 80°S and 84°N. Each zone (column) is 6° wide and each

20 band (row) is 8° high, except top band 'X' is 12° tall. In UPS polar regions

21 below 80°S and above 84°N the GZD contains only a single I{letter}, C{'A'} or

22 C{'B'} near the south and C{'Y'} or C{'Z'} around the north pole (for west

23 respectively east longitudes).

24

25 See also the U{United States National Grid<https://www.FGDC.gov/standards/projects/

26 FGDC-standards-projects/usng/fgdc_std_011_2001_usng.pdf>} and U{Military Grid

27 Reference System<https://WikiPedia.org/wiki/Military_grid_reference_system>}.

28

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

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

31

32 Set env variable C{PYGEODESY_GEOCONVERT} to the (fully qualified) path of the

33 C{GeoConvert} executable to run this module as I{python[3] -m pygeodesy.mgrs}

34 and 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

38 from pygeodesy.basics import halfs2, _splituple, _xinstanceof

39# from pygeodesy.constants import _0_5 # from .units

40 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

41 from pygeodesy.errors import _AssertionError, MGRSError, _parseX, \

42 _ValueError, _xkwds, _ALL_LAZY, _MODS

43 from pygeodesy.interns import NN, _0_, _A_, _AtoZnoIO_, _band_, _B_, \

44 _COMMASPACE_, _datum_, _easting_, _invalid_, \

45 _northing_, _not_, _SPACE_, _W_, _Y_, _Z_, _zone_

46# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .errors

47 from pygeodesy.named import _NamedBase, _NamedTuple, _Pass, _xnamed

48 from pygeodesy.namedTuples import EasNor2Tuple, UtmUps5Tuple

49 from pygeodesy.props import deprecated_property_RO, property_RO, Property_RO

50 from pygeodesy.streprs import enstr2, _enstr2m3, Fmt, _resolution10, _xzipairs

51 from pygeodesy.units import Easting, Northing, Str, _100km, _0_5

52 from pygeodesy.units import _1um, _2000km # PYCHOK used!

53 from pygeodesy.ups import _hemi, toUps8, Ups, _UPS_ZONE

54 from pygeodesy.utm import toUtm8, _to3zBlat, Utm, _UTM_ZONE_MAX, _UTM_ZONE_MIN

55# from pygeodesy.utmupsBase import _UTM_ZONE_MAX, _UTM_ZONE_MIN # from .utm

56

57__all__ = _ALL_LAZY.mgrs

58__version__ = '23.08.24'

59

60_AN_ = 'AN' # default south pole grid tile and band B

61_AtoPx_ = _AtoZnoIO_.tillP

62# <https://GitHub.com/hrbrmstr/mgrs/blob/master/src/mgrs.c>

63_FeUPS = {_A_: 8, _B_: 20, _Y_: 8, _Z_: 20} # falsed offsets (C{_100kms})

64_FnUPS = {_A_: 8, _B_: 8, _Y_: 13, _Z_: 13} # falsed offsets (C{_100kms})

65_JtoZx_ = 'JKLPQRSTUXYZZ' # _AtoZnoDEIMNOVW.fromJ, duplicate Z

66# 100 Km grid tile UTM column (E) letters, repeating every third zone

67_LeUTM = _AtoZnoIO_.tillH, _AtoZnoIO_.fromJ.tillR, _AtoZnoIO_.fromS # grid E colums

68# 100 Km grid tile UPS column (E) letters for each polar zone

69_LeUPS = {_A_: _JtoZx_, _B_: 'ABCFGHJKLPQR', _Y_: _JtoZx_, _Z_: 'ABCFGHJ'}

70# 100 Km grid tile UTM and UPS row (N) letters, repeating every other zone

71_LnUTM = _AtoZnoIO_.tillV, _AtoZnoIO_.fromF.tillV + _AtoZnoIO_.tillE # grid N rows

72_LnUPS = {_A_: _AtoZnoIO_, _B_: _AtoZnoIO_, _Y_: _AtoPx_, _Z_: _AtoPx_}

73_polar_ = _SPACE_('polar', _zone_)

74

75

76 class Mgrs(_NamedBase):

77 '''Military Grid Reference System (MGRS/NATO) references,

78 with method to convert to UTM coordinates.

79 '''

80 _band = NN # latitudinal (C..X) or polar (ABYZ) band

81 _bandLat = None # band latitude (C{degrees90} or C{None})

82 _datum = _WGS84 # Datum (L{Datum})

83 _easting = 0 # Easting (C{meter}), within 100 Km grid tile

84 _EN = NN # EN digraph (C{str}), 100 Km grid tile

85 _northing = 0 # Northing (C{meter}), within 100 Km grid tile

86 _resolution = 0 # from L{parseMGRS}, centering (C{meter})

87 _zone = 0 # longitudinal or polar zone (C{int}), 0..60

88

89 def __init__(self, zone=0, EN=NN, easting=0, northing=0, band=NN,

90 datum=_WGS84, resolution=0, name=NN):

91 '''New L{Mgrs} Military grid reference.

92

93 @arg zone: The 6° I{longitudinal} zone (C{int}), 1..60 covering

94 180°W..180°E or C{0} for I{polar} regions or (C{str})

95 with the zone number and I{latitudinal} band letter.

96 @arg EN: Two-letter EN digraph (C{str}), grid tile I{using only}

97 the I{AA} aka I{MGRS-New} (row) U{lettering scheme

98 <http://Wikipedia.org/wiki/Military_Grid_Reference_System>}.

99 @kwarg easting: Easting (C{meter}), within 100 Km grid tile.

100 @kwarg northing: Northing (C{meter}), within 100 Km grid tile.

101 @kwarg band: Optional, I{latitudinal} band or I{polar} region letter

102 (C{str}), 'C'|..|'X' covering 80°S..84°N (no 'I'|'O'),

103 'A'|'B' at the south or 'Y'|'Z' at the north pole.

104 @kwarg datum: This reference's datum (L{Datum}, L{Ellipsoid},

105 L{Ellipsoid2} or L{a_f2Tuple}).

106 @kwarg resolution: Optional resolution (C{meter}), C{0} for default.

107 @kwarg name: Optional name (C{str}).

108

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

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

111

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

113

114 @example:

115

116 >>> from pygeodesy import Mgrs

117 >>> m = Mgrs('31U', 'DQ', 48251, 11932) # 31U DQ 48251 11932

118 >>> m = Mgrs() # defaults to south pole

119 >>> m.toLatLon() # ... lat=-90.0, lon=0.0, datum=...

120 '''

121 if name:

122 self.name = name

123

124 if not (zone or EN or band):

125 EN, band = _AN_, _B_ # default, south pole

126 try:

127 self._zone, self._band, self._bandLat = _to3zBlat(zone, band, Error=MGRSError)

128 en = str(EN)

129 if len(en) != 2 or not en.isalpha():

130 raise ValueError # caught below

131 self._EN = en.upper()

132 _ = self._EN2m # check E and N

133 except (IndexError, KeyError, TypeError, ValueError):

134 raise MGRSError(band=band, EN=EN, zone=zone)

135

136 self._easting = Easting(easting, Error=MGRSError)

137 self._northing = Northing(northing, Error=MGRSError)

138 if datum not in (None, Mgrs._datum):

139 self._datum = _ellipsoidal_datum(datum, name=name) # XXX raiser=_datum_

140

141 if resolution:

142 self.resolution = resolution

143

144 def __str__(self):

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

146

147 @property_RO

148 def band(self):

149 '''Get the I{latitudinal} band C{'C'|..|'X'} (no C{'I'|'O'})

150 or I{polar} region C{'A'|'B'|'Y'|'Z'}) letter (C{str}).

151 '''

152 return self._band

153

154 @Property_RO

155 def bandLatitude(self):

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

157 '''

158 return self._bandLat

159

160 @Property_RO

161 def datum(self):

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

163 '''

164 return self._datum

165

166 @deprecated_property_RO

167 def digraph(self):

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

169 return self.EN

170

171 @property_RO

172 def EN(self):

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

174 '''

175 return self._EN

176

177 @deprecated_property_RO

178 def en100k(self):

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

180 return self.EN

181

182 @Property_RO

183 def _EN2m(self):

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

185

186 @note: Raises AssertionError, IndexError or KeyError: Invalid

187 C{zone} number, C{EN} letter or I{polar} region letter.

188 '''

189 EN = self.EN

190 if self.isUTM:

191 i = self.zone - 1

192 # get easting from the E column (note, +1 because

193 # easting starts at 166e3 due to 500 Km falsing)

194 e = _LeUTM[i % 3].index(EN[0]) + 1

195 # similarly, get northing from the N row

196 n = _LnUTM[i % 2].index(EN[1])

197 elif self.isUPS:

198 B = self.band

199 e = _LeUPS[B].index(EN[0]) + _FeUPS[B]

200 n = _LnUPS[B].index(EN[1]) + _FnUPS[B]

201 else:

202 raise _AssertionError(zone=self.zone)

203 return float(e * _100km), float(n * _100km) # meter

204

205 @property_RO

206 def easting(self):

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

208 '''

209 return self._easting

210

211 @Property_RO

212 def eastingnorthing(self):

213 '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)})

214 I{within} the MGRS grid tile, both in C{meter}.

215 '''

216 return EasNor2Tuple(self.easting, self.northing)

217

218 @Property_RO

219 def isUPS(self):

220 '''Is this MGRS in a (polar) UPS zone (C{bool}).

221 '''

222 return self._zone == _UPS_ZONE

223

224 @Property_RO

225 def isUTM(self):

226 '''Is this MGRS in a (non-polar) UTM zone (C{bool}).

227 '''

228 return _UTM_ZONE_MIN <= self._zone <= _UTM_ZONE_MAX

229

230 @property_RO

231 def northing(self):

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

233 '''

234 return self._northing

235

236 @Property_RO

237 def northingBottom(self):

238 '''Get the northing of the band bottom (C{meter}).

239 '''

240 a = self.bandLatitude

241 u = toUtm8(a, 0, datum=self.datum, Utm=None) if self.isUTM else \

242 toUps8(a, 0, datum=self.datum, Ups=None)

243 return int(u.northing / _100km) * _100km

244

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

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

247

248 @arg strMGRS: The MGRS reference (C{str}),

249 see function L{parseMGRS}.

250 @kwarg name: Optional instance name (C{str}),

251 overriding this name.

252

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

254

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

256 '''

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

258 name=name or self.name)

259

260 @property

261 def resolution(self):

262 '''Get the MGRS resolution (C{meter}, power of 10)

263 or C{0} if undefined.

264 '''

265 return self._resolution

266

267 @resolution.setter # PYCHOK setter!

268 def resolution(self, resolution):

269 '''Set the MGRS resolution (C{meter}, power of 10)

270 or C{0} to undefine and disable UPS/UTM centering.

271

272 @raise MGRSError: Invalid B{C{resolution}}, over

273 C{1.e+5} or under C{1.e-6}.

274 '''

275 if resolution: # and resolution > 0

276 r = _resolution10(resolution, Error=MGRSError)

277 else:

278 r = 0

279 if self._resolution != r:

280 self._resolution = r

281

282 @Property_RO

283 def tilesize(self):

284 '''Get the MGRS grid tile size (C{meter}).

285 '''

286 assert _MODS.utmups._MGRS_TILE is _100km

287 return _100km

288

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

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

291

292 @kwarg LatLon: Optional, ellipsoidal class to return the

293 geodetic point (C{LatLon}) or C{None}.

294 @kwarg center: Optionally, return the grid's center or

295 lower left corner (C{bool}).

296 @kwarg toLatLon_kwds: Optional, additional L{Utm.toLatLon}

297 and B{C{LatLon}} keyword arguments.

298

299 @return: A B{C{LatLon}} instance or if C{B{LatLon} is None}

300 a L{LatLonDatum5Tuple}C{(lat, lon, datum, gamma,

301 scale)}.

302

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

304

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

306

307 @see: Methods L{Mgrs.toUtm} and L{Utm.toLatLon}.

308 '''

309 u = self.toUtmUps(center=center)

310 return u.toLatLon(LatLon=LatLon, **toLatLon_kwds)

311

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

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

314

315 @kwarg fmt: Enclosing backets format (C{str}).

316 @kwarg sep: Separator between name:values (C{str}).

317 @kwarg prec: Precision (C{int}), see method L{Mgrs.toStr}.

318

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

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

321

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

323 UTM/UPS coordinates).

324

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

326 '''

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

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

329

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

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

332

333 @kwarg prec: Precision, the number of I{decimal} digits (C{int}) or if

334 negative, the number of I{units to drop}, like MGRS U{PRECISION

335 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}.

336 @kwarg sep: Optional separator to join (C{str}) or C{None} to return an unjoined

337 3-C{tuple} of C{str}s.

338

339 @return: This Mgrs as 4-tuple C{("dd]B", "EN", "easting", "northing")} if C{B{sep}=NN}

340 or "[dd]B EN easting northing" (C{str}) with C{B{sep} " "}.

341

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

343

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

345

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

347

348 @example:

349

350 >>> from pygeodesy import Mgrs, NN, parseMGRS

351 >>> m = Mgrs(31, 'DQ', 48251, 11932, band='U')

352 >>> m.toStr() # '31U DQ 48251 11932'

353 >>> m = parseMGRS('BAN1234567890')

354 >>> str(m) # 'B AN 12345 67890'

355 >>> m.toStr() # 'BAN1234567890'

356 >>> m.toStr(prec=-2) # 'BAN123678'

357 '''

358 zB = self.zoneB

359 t = enstr2(self._easting, self._northing, prec, zB, self.EN)

360 return t if sep is None else sep.join(t).rstrip()

361

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

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

364

365 @kwarg Ups: Optional class to return the UPS coordinate

366 (L{Ups}) or C{None}.

367 @kwarg center: Optionally, center easting and northing

368 by the resolution (C{bool}).

369

370 @return: A B{C{Ups}} instance or if C{B{Ups} is None}

371 a L{UtmUps5Tuple}C{(zone, hemipole, easting,

372 northing, band)}.

373

374 @raise MGRSError: This MGRS is a I{non-polar} UTM reference.

375 '''

376 if self.isUTM:

377 raise MGRSError(zoneB=self.zoneB, txt=_not_(_polar_))

378 return self._toUtmUps(Ups, center)

379

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

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

382

383 @kwarg Utm: Optional class to return the UTM coordinate

384 (L{Utm}) or C{None}.

385 @kwarg center: Optionally, center easting and northing

386 by the resolution (C{bool}).

387

388 @return: A B{C{Utm}} instance or if C{B{Utm} is None}

389 a L{UtmUps5Tuple}C{(zone, hemipole, easting,

390 northing, band)}.

391

392 @raise MGRSError: This MGRS is a I{polar} UPS reference.

393 '''

394 if self.isUPS:

395 raise MGRSError(zoneB=self.zoneB, txt=_polar_)

396 return self._toUtmUps(Utm, center)

397

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

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

400

401 @kwarg Utm: Optional class to return the UTM coordinate

402 (L{Utm}) or C{None}.

403 @kwarg Ups: Optional class to return the UPS coordinate

404 (L{Utm}) or C{None}.

405 @kwarg center: Optionally, center easting and northing

406 by the resolution (C{bool}).

407

408 @return: A B{C{Utm}} or B{C{Ups}} instance or if C{B{Utm}

409 or B{Ups} is None} a L{UtmUps5Tuple}C{(zone,

410 hemipole, easting, northing, band)}.

411 '''

412 return self._toUtmUps((Utm if self.isUTM else

413 (Ups if self.isUPS else None)), center)

414

415 def _toUtmUps(self, U, center):

416 '''(INTERNAL) Helper for C{.toUps} and C{.toUtm}.

417 '''

418 e, n = self._EN2m

419 e += self.easting

420 n += self.northing

421 if self.isUTM:

422 # 100 Km row letters repeat every 2,000 Km north;

423 # add 2,000 Km blocks to get into required band

424 b = (self.northingBottom - n) / _2000km

425 if b > 0:

426 b = int(b) + 1

427 b = min(b, (3 if self.band == _W_ else 4))

428 n += b * _2000km

429 if center:

430 c = self.resolution

431 if c:

432 c *= _0_5

433 e += c

434 n += c

435 z = self.zone

436 h = _hemi(self.bandLatitude) # _S_ if self.band < _N_ else _N_

437 B = self.band

438 m = self.name

439 return UtmUps5Tuple(z, h, e, n, B, name=m, Error=MGRSError) if U is None \

440 else U(z, h, e, n, B, name=m, datum=self.datum)

441

442 @property_RO

443 def zone(self):

444 '''Get the I{longitudinal} zone (C{int}), 1..60 or 0 for I{polar}.

445 '''

446 return self._zone

447

448 @Property_RO

449 def zoneB(self):

450 '''Get the I{polar} region letter or the I{longitudinal} zone digits

451 plus I{latitudinal} band letter (C{str}).

452 '''

453 return self.band if self.isUPS else NN(Fmt.zone(self.zone), self.band)

454

455

456 class Mgrs4Tuple(_NamedTuple):

457 '''4-Tuple C{(zone, EN, easting, northing)}, C{zone} and grid

458 tile C{EN} as C{str}, C{easting} and C{northing} in C{meter}.

459

460 @note: The C{zone} consists of either the I{longitudinal} zone

461 number plus the I{latitudinal} band letter or only the

462 I{polar} region letter.

463 '''

464 _Names_ = (_zone_, 'EN', _easting_, _northing_)

465 _Units_ = ( Str, Str, Easting, Northing)

466

467 @deprecated_property_RO

468 def digraph(self):

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

470 return self.EN # PYCHOK or [1]

471

472 def toMgrs(self, **Mgrs_and_kwds):

473 '''Return this L{Mgrs4Tuple} as an L{Mgrs} instance.

474 '''

475 return self.to6Tuple(NN, _WGS84).toMgrs(**Mgrs_and_kwds)

476

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

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

479

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

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

482

483 @return: An L{Mgrs6Tuple}C{(zone, EN, easting,

484 northing, band, datum)}.

485 '''

486 z = self.zone # PYCHOK or [0]

487 B = z[-1:]

488 if B.isalpha():

489 z = z[:-1] or Fmt.zone(0)

490 t = Mgrs6Tuple(z, self.EN, self.easting, self.northing, # PYCHOK attrs

491 band or B, datum, name=self.name)

492 else:

493 t = self._xtend(Mgrs6Tuple, band, datum)

494 return t

495

496

497 class Mgrs6Tuple(_NamedTuple): # XXX only used above

498 '''6-Tuple C{(zone, EN, easting, northing, band, datum)}, with

499 C{zone}, grid tile C{EN} and C{band} as C{str}, C{easting}

500 and C{northing} in C{meter} and C{datum} a L{Datum}.

501

502 @note: The C{zone} is the I{longitudinal} zone C{"01".."60"}

503 or C{"00"} for I{polar} regions and C{band} is the

504 I{latitudinal} band or I{polar} region letter.

505 '''

506 _Names_ = Mgrs4Tuple._Names_ + (_band_, _datum_)

507 _Units_ = Mgrs4Tuple._Units_ + ( Str, _Pass)

508

509 @deprecated_property_RO

510 def digraph(self):

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

512 return self.EN # PYCHOK or [1]

513

514 def toMgrs(self, Mgrs=Mgrs, **Mgrs_kwds):

515 '''Return this L{Mgrs6Tuple} as an L{Mgrs} instance.

516 '''

517 kwds = dict(self.items())

518 if self.name:

519 kwds.update(name=self.name)

520 if Mgrs_kwds:

521 kwds.update(Mgrs_kwds)

522 return Mgrs(**kwds)

523

524

525 class _RE(object):

526 '''(INTERNAL) Lazily compiled C{re}gex-es to parse MGRS strings.

527 '''

528 _EN = '([A-Z]{2})' # 2-letter grid tile designation

529 _en = '([0-9]+)' # easting_northing digits, 2-10+

530 _pB = '([ABYZ]{1})' # polar region letter, pseudo-zone 0

531 _zB = '([0-9]{1,2}[C-X]{1})' # zone number and band letter, no I|O

532

533 @Property_RO

534 def pB_EN(self): # split polar "BEN" into 2 parts

535 import re # PYCHOK warning locale.Error

536 return re.compile(_RE._pB + _RE._EN, re.IGNORECASE)

537

538 @Property_RO

539 def pB_EN_en(self): # split polar "BEN1235..." into 3 parts

540 import re # PYCHOK warning locale.Error

541 return re.compile(_RE._pB + _RE._EN + _RE._en, re.IGNORECASE)

542

543 @Property_RO

544 def zB_EN(self): # split "1[2]BEN" into 2 parts

545 import re # PYCHOK warning locale.Error

546 return re.compile(_RE._zB + _RE._EN, re.IGNORECASE)

547

548 @Property_RO

549 def zB_EN_en(self): # split "1[2]BEN1235..." into 3 parts

550 import re # PYCHOK warning locale.Error

551 return re.compile(_RE._zB + _RE._EN + _RE._en, re.IGNORECASE)

552

553_RE = _RE() # PYCHOK singleton

554

555

556 def parseMGRS(strMGRS, datum=_WGS84, Mgrs=Mgrs, name=NN):

557 '''Parse a string representing a MGRS grid reference,

558 consisting of C{"[zone]Band, EN, easting, northing"}.

559

560 @arg strMGRS: MGRS grid reference (C{str}).

561 @kwarg datum: Optional datum to use (L{Datum}).

562 @kwarg Mgrs: Optional class to return the MGRS grid

563 reference (L{Mgrs}) or C{None}.

564 @kwarg name: Optional B{C{Mgrs}} name (C{str}).

565

566 @return: The MGRS grid reference as B{C{Mgrs}} or if

567 C{B{Mgrs} is None} as an L{Mgrs4Tuple}C{(zone,

568 EN, easting, northing)}.

569

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

571

572 @example:

573

574 >>> m = parseMGRS('31U DQ 48251 11932')

575 >>> str(m) # '31U DQ 48251 11932'

576 >>> m = parseMGRS('31UDQ4825111932')

577 >>> repr(m) # [Z:31U, G:DQ, E:48251, N:11932]

578 >>> m = parseMGRS('42SXD0970538646')

579 >>> str(m) # '42S XD 09705 38646'

580 >>> m = parseMGRS('42SXD9738') # Km

581 >>> str(m) # '42S XD 97000 38000'

582 >>> m = parseMGRS('YUB17770380') # polar

583 >>> str(m) # 'Y UB 17770 03800'

584 '''

585 def _mg(s, re_UTM, re_UPS): # return re.match groups

586 m = re_UTM.match(s)

587 if m:

588 return m.groups()

589 m = re_UPS.match(s.lstrip(_0_))

590 if m:

591 return m.groups()

592# m = m.groups()

593# t = '00' + m[0]

594# return (t,) + m[1:]

595 raise ValueError(_SPACE_(repr(s), _invalid_))

596

597 def _MGRS(strMGRS, datum, Mgrs, name):

598 m = _splituple(strMGRS.strip())

599 if len(m) == 1: # [01]BEN1234512345'

600 m = _mg(m[0], _RE.zB_EN_en, _RE.pB_EN_en)

601 m = m[:2] + halfs2(m[2])

602 elif len(m) == 2: # [01]BEN 1234512345'

603 m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + halfs2(m[1])

604 elif len(m) == 3: # [01]BEN 12345 12345'

605 m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + m[1:]

606 if len(m) != 4: # [01]B EN 12345 12345

607 raise ValueError

608

609 zB, EN = m[0].upper(), m[1].upper()

610 if zB[-1:] in 'IO':

611 raise ValueError(_SPACE_(repr(m[0]), _invalid_))

612 e, n, m = _enstr2m3(*m[2:])

613

614 if Mgrs is None:

615 r = Mgrs4Tuple(zB, EN, e, n, name=name)

616 _ = r.toMgrs(resolution=m) # validate

617 else:

618 r = Mgrs(zB, EN, e, n, datum=datum, resolution=m, name=name)

619 return r

620

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

622 strMGRS=strMGRS, Error=MGRSError)

623

624

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

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

627

628 @arg utmups: A UTM or UPS coordinate (L{Utm}, L{Etm} or L{Ups}).

629 @kwarg Mgrs: Optional class to return the MGRS grid reference

630 (L{Mgrs}) or C{None}.

631 @kwarg name: Optional B{C{Mgrs}} name (C{str}).

632 @kwarg Mgrs_kwds: Optional, additional B{C{Mgrs}} keyword

633 arguments, ignored if C{B{Mgrs} is None}.

634

635 @return: The MGRS grid reference as B{C{Mgrs}} or if

636 C{B{Mgrs} is None} as an L{Mgrs6Tuple}C{(zone,

637 EN, easting, northing, band, datum)}.

638

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

640

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

642 nor L{Ups}.

643

644 @example:

645

646 >>> u = Utm(31, 'N', 448251, 5411932)

647 >>> m = u.toMgrs() # 31U DQ 48251 11932

648 '''

649# _MODS.utmups.utmupsValidate(utmups, MGRS=True, Error-MGRSError)

650 _xinstanceof(Utm, Ups, utmups=utmups) # Utm, Etm, Ups

651 try:

652 e, n = utmups.eastingnorthing2(falsed=True)

653 E, e = _um100km2(e)

654 N, n = _um100km2(n)

655 B, z = utmups.band, utmups.zone

656 if _UTM_ZONE_MIN <= z <= _UTM_ZONE_MAX:

657 i = z - 1

658 # columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z,

659 # then repeating every 3rd zone (note E-1 because

660 # eastings start at 166e3 due to 500km false origin)

661 EN = _LeUTM[i % 3][E - 1]

662 # rows in even zones are A-V, in odd zones are F-E

663 EN += _LnUTM[i % 2][N % len(_LnUTM[0])]

664 elif z == _UPS_ZONE:

665 EN = _LeUPS[B][E - _FeUPS[B]]

666 EN += _LnUPS[B][N - _FnUPS[B]]

667 else:

668 raise _ValueError(zone=z)

669 except (IndexError, TypeError, ValueError) as x:

670 raise MGRSError(B=B, E=E, N=N, utmups=utmups, cause=x)

671

672 if Mgrs is None:

673 r = Mgrs4Tuple(Fmt.zone(z), EN, e, n).to6Tuple(B, utmups.datum)

674 else:

675 kwds = _xkwds(Mgrs_kwds, band=B, datum=utmups.datum)

676 r = Mgrs(z, EN, e, n, **kwds)

677 return _xnamed(r, name or utmups.name)

678

679

680 def _um100km2(m):

681 '''(INTERNAL) An MGRS east-/northing truncated to micrometer (um)

682 precision and to grid tile C{M} and C{m}eter within the tile.

683 '''

684 m = int(m / _1um) * _1um # micrometer

685 M, m = divmod(m, _100km)

686 return int(M), m

687

688

689 if __name__ == '__main__':

690

691 from pygeodesy.ellipsoidalVincenty import fabs, LatLon

692 from pygeodesy.lazily import _getenv, printf

693

694# from math import fabs # from .ellipsoidalVincenty

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

696

697 # <https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html>

698 _GeoConvert = _getenv('PYGEODESY_GEOCONVERT', '/opt/local/bin/GeoConvert')

699 if _access(_GeoConvert, _X_OK):

700 GC_m = _GeoConvert, '-m' # -m converts latlon to MGRS

701 printf(' using: %s ...', _SPACE_.join(GC_m))

702 from pygeodesy.solveBase import _popen2

703 else:

704 GC_m = _popen2 = None

705

706 e = n = 0

707 try:

708 for lat in range(-90, 91, 1):

709 printf('%6s: lat %s ...', n, lat, end=NN, flush=True)

710 nl = _NL

711 for lon in range(-180, 181, 1):

712 m = LatLon(lat, lon).toMgrs()

713 if _popen2:

714 t = '%s %s' % (lat, lon)

715 g = _popen2(GC_m, stdin=t)[1]

716 t = m.toStr() # sep=NN

717 if t != g:

718 e += 1

719 printf('%s%6s: %s: %r vs %r (lon %s)', nl, -e, m, t, g, lon)

720 nl = NN

721 t = m.toLatLon(LatLon=LatLon)

722 d = max(fabs(t.lat - lat), fabs(t.lon - lon))

723 if d > 1e-9 and -90 < lat < 90 and -180 < lon < 180:

724 e += 1

725 printf('%s%6s: %s: %s vs %s %.6e', nl, -e, m, t.latlon, (float(lat), float(lon)), d)

726 nl = NN

727 n += 1

728 if nl:

729 printf(' OK')

730 except KeyboardInterrupt:

731 printf(nl)

732

733 p = e * 100.0 / n

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

735

736# **) MIT License

737#

738# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

739#

740# Permission is hereby granted, free of charge, to any person obtaining a

741# copy of this software and associated documentation files (the "Software"),

742# to deal in the Software without restriction, including without limitation

743# the rights to use, copy, modify, merge, publish, distribute, sublicense,

744# and/or sell copies of the Software, and to permit persons to whom the

745# Software is furnished to do so, subject to the following conditions:

746#

747# The above copyright notice and this permission notice shall be included

748# in all copies or substantial portions of the Software.

749#

750# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

751# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

752# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

753# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

754# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

755# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

756# OTHER DEALINGS IN THE SOFTWARE.