pygeodesy 24.3.24__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. pygeodesy/wgrs.py +439 -0
pygeodesy/mgrs.py ADDED
@@ -0,0 +1,721 @@
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
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, _PYGEODESY_GEOCONVERT_
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.12.03'
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
+ if name:
115
+ self.name = name
116
+
117
+ if not (zone or EN or band):
118
+ EN, band = _AN_, _B_ # default, south pole
119
+ try:
120
+ self._zone, self._band, self._bandLat = _to3zBlat(zone, band, Error=MGRSError)
121
+ en = str(EN)
122
+ if len(en) != 2 or not en.isalpha():
123
+ raise ValueError() # caught below
124
+ self._EN = en.upper()
125
+ _ = self._EN2m # check E and N
126
+ except (IndexError, KeyError, TypeError, ValueError):
127
+ raise MGRSError(band=band, EN=EN, zone=zone)
128
+
129
+ self._easting = Easting(easting, Error=MGRSError)
130
+ self._northing = Northing(northing, Error=MGRSError)
131
+ if datum not in (None, Mgrs._datum):
132
+ self._datum = _ellipsoidal_datum(datum, name=name) # XXX raiser=_datum_
133
+
134
+ if resolution:
135
+ self.resolution = resolution
136
+
137
+ def __str__(self):
138
+ return self.toStr(sep=_SPACE_) # for backward compatibility
139
+
140
+ @property_RO
141
+ def band(self):
142
+ '''Get the I{latitudinal} band C{'C'|..|'X'} (no C{'I'|'O'})
143
+ or I{polar} region C{'A'|'B'|'Y'|'Z'}) letter (C{str}).
144
+ '''
145
+ return self._band
146
+
147
+ @Property_RO
148
+ def bandLatitude(self):
149
+ '''Get the band latitude (C{degrees90}).
150
+ '''
151
+ return self._bandLat
152
+
153
+ @Property_RO
154
+ def datum(self):
155
+ '''Get the datum (L{Datum}).
156
+ '''
157
+ return self._datum
158
+
159
+ @deprecated_property_RO
160
+ def digraph(self):
161
+ '''DEPRECATED, use property C{EN}.'''
162
+ return self.EN
163
+
164
+ @property_RO
165
+ def EN(self):
166
+ '''Get the 2-letter grid tile (C{str}).
167
+ '''
168
+ return self._EN
169
+
170
+ @deprecated_property_RO
171
+ def en100k(self):
172
+ '''DEPRECATED, use property C{EN}.'''
173
+ return self.EN
174
+
175
+ @Property_RO
176
+ def _EN2m(self):
177
+ '''(INTERNAL) Get the grid 2-tuple (easting, northing) in C{meter}.
178
+
179
+ @note: Raises AssertionError, IndexError or KeyError: Invalid
180
+ C{zone} number, C{EN} letter or I{polar} region letter.
181
+ '''
182
+ EN = self.EN
183
+ if self.isUTM:
184
+ i = self.zone - 1
185
+ # get easting from the E column (note, +1 because
186
+ # easting starts at 166e3 due to 500 Km falsing)
187
+ e = _LeUTM[i % 3].index(EN[0]) + 1
188
+ # similarly, get northing from the N row
189
+ n = _LnUTM[i % 2].index(EN[1])
190
+ elif self.isUPS:
191
+ B = self.band
192
+ e = _LeUPS[B].index(EN[0]) + _FeUPS[B]
193
+ n = _LnUPS[B].index(EN[1]) + _FnUPS[B]
194
+ else:
195
+ raise _AssertionError(zone=self.zone)
196
+ return float(e * _100km), float(n * _100km) # meter
197
+
198
+ @property_RO
199
+ def easting(self):
200
+ '''Get the easting (C{meter} within grid tile).
201
+ '''
202
+ return self._easting
203
+
204
+ @Property_RO
205
+ def eastingnorthing(self):
206
+ '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)})
207
+ I{within} the MGRS grid tile, both in C{meter}.
208
+ '''
209
+ return EasNor2Tuple(self.easting, self.northing)
210
+
211
+ @Property_RO
212
+ def isUPS(self):
213
+ '''Is this MGRS in a (polar) UPS zone (C{bool}).
214
+ '''
215
+ return self._zone == _UPS_ZONE
216
+
217
+ @Property_RO
218
+ def isUTM(self):
219
+ '''Is this MGRS in a (non-polar) UTM zone (C{bool}).
220
+ '''
221
+ return _UTM_ZONE_MIN <= self._zone <= _UTM_ZONE_MAX
222
+
223
+ @property_RO
224
+ def northing(self):
225
+ '''Get the northing (C{meter} within grid tile).
226
+ '''
227
+ return self._northing
228
+
229
+ @Property_RO
230
+ def northingBottom(self):
231
+ '''Get the northing of the band bottom (C{meter}).
232
+ '''
233
+ a = self.bandLatitude
234
+ u = toUtm8(a, 0, datum=self.datum, Utm=None) if self.isUTM else \
235
+ toUps8(a, 0, datum=self.datum, Ups=None)
236
+ return int(u.northing / _100km) * _100km
237
+
238
+ def parse(self, strMGRS, name=NN):
239
+ '''Parse a string to a similar L{Mgrs} instance.
240
+
241
+ @arg strMGRS: The MGRS reference (C{str}),
242
+ see function L{parseMGRS}.
243
+ @kwarg name: Optional instance name (C{str}),
244
+ overriding this name.
245
+
246
+ @return: The similar instance (L{Mgrs}).
247
+
248
+ @raise MGRSError: Invalid B{C{strMGRS}}.
249
+ '''
250
+ return parseMGRS(strMGRS, datum=self.datum, Mgrs=self.classof,
251
+ name=name or self.name)
252
+
253
+ @property
254
+ def resolution(self):
255
+ '''Get the MGRS resolution (C{meter}, power of 10)
256
+ or C{0} if undefined.
257
+ '''
258
+ return self._resolution
259
+
260
+ @resolution.setter # PYCHOK setter!
261
+ def resolution(self, resolution):
262
+ '''Set the MGRS resolution (C{meter}, power of 10)
263
+ or C{0} to undefine and disable UPS/UTM centering.
264
+
265
+ @raise MGRSError: Invalid B{C{resolution}}, over
266
+ C{1.e+5} or under C{1.e-6}.
267
+ '''
268
+ if resolution: # and resolution > 0
269
+ r = _resolution10(resolution, Error=MGRSError)
270
+ else:
271
+ r = 0
272
+ if self._resolution != r:
273
+ self._resolution = r
274
+
275
+ @Property_RO
276
+ def tilesize(self):
277
+ '''Get the MGRS grid tile size (C{meter}).
278
+ '''
279
+ assert _MODS.utmups._MGRS_TILE is _100km
280
+ return _100km
281
+
282
+ def toLatLon(self, LatLon=None, center=True, **toLatLon_kwds):
283
+ '''Convert this MGRS grid reference to a UTM coordinate.
284
+
285
+ @kwarg LatLon: Optional, ellipsoidal class to return the
286
+ geodetic point (C{LatLon}) or C{None}.
287
+ @kwarg center: Optionally, return the grid's center or
288
+ lower left corner (C{bool}).
289
+ @kwarg toLatLon_kwds: Optional, additional L{Utm.toLatLon}
290
+ and B{C{LatLon}} keyword arguments.
291
+
292
+ @return: A B{C{LatLon}} instance or if C{B{LatLon} is None}
293
+ a L{LatLonDatum5Tuple}C{(lat, lon, datum, gamma,
294
+ scale)}.
295
+
296
+ @raise TypeError: If B{C{LatLon}} is not ellipsoidal.
297
+
298
+ @raise UTMError: Invalid meridional radius or H-value.
299
+
300
+ @see: Methods L{Mgrs.toUtm} and L{Utm.toLatLon}.
301
+ '''
302
+ u = self.toUtmUps(center=center)
303
+ return u.toLatLon(LatLon=LatLon, **toLatLon_kwds)
304
+
305
+ def toRepr(self, fmt=Fmt.SQUARE, sep=_COMMASPACE_, **prec): # PYCHOK expected
306
+ '''Return a string representation of this MGRS grid reference.
307
+
308
+ @kwarg fmt: Enclosing backets format (C{str}).
309
+ @kwarg sep: Separator between name:values (C{str}).
310
+ @kwarg prec: Precision (C{int}), see method L{Mgrs.toStr}.
311
+
312
+ @return: This Mgrs as "[Z:[dd]B, G:EN, E:easting, N:northing]"
313
+ (C{str}), with C{B{sep} ", "}.
314
+
315
+ @note: MGRS grid references are truncated, not rounded (unlike
316
+ UTM/UPS coordinates).
317
+
318
+ @raise ValueError: Invalid B{C{prec}}.
319
+ '''
320
+ t = self.toStr(sep=None, **prec)
321
+ return _xzipairs('ZGEN', t, sep=sep, fmt=fmt)
322
+
323
+ def toStr(self, prec=0, sep=NN): # PYCHOK expected
324
+ '''Return this MGRS grid reference as a string.
325
+
326
+ @kwarg prec: Precision, the number of I{decimal} digits (C{int}) or if
327
+ negative, the number of I{units to drop}, like MGRS U{PRECISION
328
+ <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}.
329
+ @kwarg sep: Optional separator to join (C{str}) or C{None} to return an unjoined
330
+ 3-C{tuple} of C{str}s.
331
+
332
+ @return: This Mgrs as 4-tuple C{("dd]B", "EN", "easting", "northing")} if C{B{sep}=NN}
333
+ or "[dd]B EN easting northing" (C{str}) with C{B{sep} " "}.
334
+
335
+ @note: Both C{easting} and C{northing} strings are C{NN} or missing if C{B{prec} <= -5}.
336
+
337
+ @note: MGRS grid references are truncated, not rounded (unlike UTM/UPS).
338
+
339
+ @raise ValueError: Invalid B{C{prec}}.
340
+ '''
341
+ zB = self.zoneB
342
+ t = enstr2(self._easting, self._northing, prec, zB, self.EN)
343
+ return t if sep is None else sep.join(t).rstrip()
344
+
345
+ def toUps(self, Ups=Ups, center=False):
346
+ '''Convert this MGRS grid reference to a UPS coordinate.
347
+
348
+ @kwarg Ups: Optional class to return the UPS coordinate
349
+ (L{Ups}) or C{None}.
350
+ @kwarg center: Optionally, center easting and northing
351
+ by the resolution (C{bool}).
352
+
353
+ @return: A B{C{Ups}} instance or if C{B{Ups} is None}
354
+ a L{UtmUps5Tuple}C{(zone, hemipole, easting,
355
+ northing, band)}.
356
+
357
+ @raise MGRSError: This MGRS is a I{non-polar} UTM reference.
358
+ '''
359
+ if self.isUTM:
360
+ raise MGRSError(zoneB=self.zoneB, txt=_not_(_polar_))
361
+ return self._toUtmUps(Ups, center)
362
+
363
+ def toUtm(self, Utm=Utm, center=False):
364
+ '''Convert this MGRS grid reference to a UTM coordinate.
365
+
366
+ @kwarg Utm: Optional class to return the UTM coordinate
367
+ (L{Utm}) or C{None}.
368
+ @kwarg center: Optionally, center easting and northing
369
+ by the resolution (C{bool}).
370
+
371
+ @return: A B{C{Utm}} instance or if C{B{Utm} is None}
372
+ a L{UtmUps5Tuple}C{(zone, hemipole, easting,
373
+ northing, band)}.
374
+
375
+ @raise MGRSError: This MGRS is a I{polar} UPS reference.
376
+ '''
377
+ if self.isUPS:
378
+ raise MGRSError(zoneB=self.zoneB, txt=_polar_)
379
+ return self._toUtmUps(Utm, center)
380
+
381
+ def toUtmUps(self, Utm=Utm, Ups=Ups, center=False):
382
+ '''Convert this MGRS grid reference to a UTM or UPS coordinate.
383
+
384
+ @kwarg Utm: Optional class to return the UTM coordinate
385
+ (L{Utm}) or C{None}.
386
+ @kwarg Ups: Optional class to return the UPS coordinate
387
+ (L{Utm}) or C{None}.
388
+ @kwarg center: Optionally, center easting and northing
389
+ by the resolution (C{bool}).
390
+
391
+ @return: A B{C{Utm}} or B{C{Ups}} instance or if C{B{Utm}
392
+ or B{Ups} is None} a L{UtmUps5Tuple}C{(zone,
393
+ hemipole, easting, northing, band)}.
394
+ '''
395
+ return self._toUtmUps((Utm if self.isUTM else
396
+ (Ups if self.isUPS else None)), center)
397
+
398
+ def _toUtmUps(self, U, center):
399
+ '''(INTERNAL) Helper for C{.toUps} and C{.toUtm}.
400
+ '''
401
+ e, n = self._EN2m
402
+ e += self.easting
403
+ n += self.northing
404
+ if self.isUTM:
405
+ # 100 Km row letters repeat every 2,000 Km north;
406
+ # add 2,000 Km blocks to get into required band
407
+ b = (self.northingBottom - n) / _2000km
408
+ if b > 0:
409
+ b = int(b) + 1
410
+ b = min(b, (3 if self.band == _W_ else 4))
411
+ n += b * _2000km
412
+ if center:
413
+ c = self.resolution
414
+ if c:
415
+ c *= _0_5
416
+ e += c
417
+ n += c
418
+ z = self.zone
419
+ h = _hemi(self.bandLatitude) # _S_ if self.band < _N_ else _N_
420
+ B = self.band
421
+ m = self.name
422
+ return UtmUps5Tuple(z, h, e, n, B, name=m, Error=MGRSError) if U is None \
423
+ else U(z, h, e, n, B, name=m, datum=self.datum)
424
+
425
+ @property_RO
426
+ def zone(self):
427
+ '''Get the I{longitudinal} zone (C{int}), 1..60 or 0 for I{polar}.
428
+ '''
429
+ return self._zone
430
+
431
+ @Property_RO
432
+ def zoneB(self):
433
+ '''Get the I{polar} region letter or the I{longitudinal} zone digits
434
+ plus I{latitudinal} band letter (C{str}).
435
+ '''
436
+ return self.band if self.isUPS else NN(Fmt.zone(self.zone), self.band)
437
+
438
+
439
+ class Mgrs4Tuple(_NamedTuple):
440
+ '''4-Tuple C{(zone, EN, easting, northing)}, C{zone} and grid
441
+ tile C{EN} as C{str}, C{easting} and C{northing} in C{meter}.
442
+
443
+ @note: The C{zone} consists of either the I{longitudinal} zone
444
+ number plus the I{latitudinal} band letter or only the
445
+ I{polar} region letter.
446
+ '''
447
+ _Names_ = (_zone_, 'EN', _easting_, _northing_)
448
+ _Units_ = ( Str, Str, Easting, Northing)
449
+
450
+ @deprecated_property_RO
451
+ def digraph(self):
452
+ '''DEPRECATED, use attribute C{EN}.'''
453
+ return self.EN # PYCHOK or [1]
454
+
455
+ def toMgrs(self, **Mgrs_and_kwds):
456
+ '''Return this L{Mgrs4Tuple} as an L{Mgrs} instance.
457
+ '''
458
+ return self.to6Tuple(NN, _WGS84).toMgrs(**Mgrs_and_kwds)
459
+
460
+ def to6Tuple(self, band=NN, datum=_WGS84):
461
+ '''Extend this L{Mgrs4Tuple} to a L{Mgrs6Tuple}.
462
+
463
+ @kwarg band: The band (C{str}).
464
+ @kwarg datum: The datum (L{Datum}).
465
+
466
+ @return: An L{Mgrs6Tuple}C{(zone, EN, easting,
467
+ northing, band, datum)}.
468
+ '''
469
+ z = self.zone # PYCHOK or [0]
470
+ B = z[-1:]
471
+ if B.isalpha():
472
+ z = z[:-1] or Fmt.zone(0)
473
+ t = Mgrs6Tuple(z, self.EN, self.easting, self.northing, # PYCHOK attrs
474
+ band or B, datum, name=self.name)
475
+ else:
476
+ t = self._xtend(Mgrs6Tuple, band, datum)
477
+ return t
478
+
479
+
480
+ class Mgrs6Tuple(_NamedTuple): # XXX only used above
481
+ '''6-Tuple C{(zone, EN, easting, northing, band, datum)}, with
482
+ C{zone}, grid tile C{EN} and C{band} as C{str}, C{easting}
483
+ and C{northing} in C{meter} and C{datum} a L{Datum}.
484
+
485
+ @note: The C{zone} is the I{longitudinal} zone C{"01".."60"}
486
+ or C{"00"} for I{polar} regions and C{band} is the
487
+ I{latitudinal} band or I{polar} region letter.
488
+ '''
489
+ _Names_ = Mgrs4Tuple._Names_ + (_band_, _datum_)
490
+ _Units_ = Mgrs4Tuple._Units_ + ( Str, _Pass)
491
+
492
+ @deprecated_property_RO
493
+ def digraph(self):
494
+ '''DEPRECATED, use attribute C{EN}.'''
495
+ return self.EN # PYCHOK or [1]
496
+
497
+ def toMgrs(self, Mgrs=Mgrs, **Mgrs_kwds):
498
+ '''Return this L{Mgrs6Tuple} as an L{Mgrs} instance.
499
+ '''
500
+ kwds = dict(self.items())
501
+ if self.name:
502
+ kwds.update(name=self.name)
503
+ if Mgrs_kwds:
504
+ kwds.update(Mgrs_kwds)
505
+ return Mgrs(**kwds)
506
+
507
+
508
+ class _RE(object):
509
+ '''(INTERNAL) Lazily compiled C{re}gex-es to parse MGRS strings.
510
+ '''
511
+ _EN = '([A-Z]{2})' # 2-letter grid tile designation
512
+ _en = '([0-9]+)' # easting_northing digits, 2-10+
513
+ _pB = '([ABYZ]{1})' # polar region letter, pseudo-zone 0
514
+ _zB = '([0-9]{1,2}[C-X]{1})' # zone number and band letter, no I|O
515
+
516
+ @Property_RO
517
+ def pB_EN(self): # split polar "BEN" into 2 parts
518
+ import re # PYCHOK warning locale.Error
519
+ return re.compile(_RE._pB + _RE._EN, re.IGNORECASE)
520
+
521
+ @Property_RO
522
+ def pB_EN_en(self): # split polar "BEN1235..." into 3 parts
523
+ import re # PYCHOK warning locale.Error
524
+ return re.compile(_RE._pB + _RE._EN + _RE._en, re.IGNORECASE)
525
+
526
+ @Property_RO
527
+ def zB_EN(self): # split "1[2]BEN" into 2 parts
528
+ import re # PYCHOK warning locale.Error
529
+ return re.compile(_RE._zB + _RE._EN, re.IGNORECASE)
530
+
531
+ @Property_RO
532
+ def zB_EN_en(self): # split "1[2]BEN1235..." into 3 parts
533
+ import re # PYCHOK warning locale.Error
534
+ return re.compile(_RE._zB + _RE._EN + _RE._en, re.IGNORECASE)
535
+
536
+ _RE = _RE() # PYCHOK singleton
537
+
538
+
539
+ def parseMGRS(strMGRS, datum=_WGS84, Mgrs=Mgrs, name=NN):
540
+ '''Parse a string representing a MGRS grid reference,
541
+ consisting of C{"[zone]Band, EN, easting, northing"}.
542
+
543
+ @arg strMGRS: MGRS grid reference (C{str}).
544
+ @kwarg datum: Optional datum to use (L{Datum}).
545
+ @kwarg Mgrs: Optional class to return the MGRS grid
546
+ reference (L{Mgrs}) or C{None}.
547
+ @kwarg name: Optional B{C{Mgrs}} name (C{str}).
548
+
549
+ @return: The MGRS grid reference as B{C{Mgrs}} or if
550
+ C{B{Mgrs} is None} as an L{Mgrs4Tuple}C{(zone,
551
+ EN, easting, northing)}.
552
+
553
+ @raise MGRSError: Invalid B{C{strMGRS}}.
554
+ '''
555
+ def _mg(s, re_UTM, re_UPS): # return re.match groups
556
+ m = re_UTM.match(s)
557
+ if m:
558
+ return m.groups()
559
+ m = re_UPS.match(s.lstrip(_0_))
560
+ if m:
561
+ return m.groups()
562
+ # m = m.groups()
563
+ # t = '00' + m[0]
564
+ # return (t,) + m[1:]
565
+ raise ValueError(_SPACE_(repr(s), _invalid_))
566
+
567
+ def _MGRS(strMGRS, datum, Mgrs, name):
568
+ m = _splituple(strMGRS.strip())
569
+ if len(m) == 1: # [01]BEN1234512345'
570
+ m = _mg(m[0], _RE.zB_EN_en, _RE.pB_EN_en)
571
+ m = m[:2] + halfs2(m[2])
572
+ elif len(m) == 2: # [01]BEN 1234512345'
573
+ m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + halfs2(m[1])
574
+ elif len(m) == 3: # [01]BEN 12345 12345'
575
+ m = _mg(m[0], _RE.zB_EN, _RE.pB_EN) + m[1:]
576
+ if len(m) != 4: # [01]B EN 12345 12345
577
+ raise ValueError
578
+
579
+ zB, EN = m[0].upper(), m[1].upper()
580
+ if zB[-1:] in 'IO':
581
+ raise ValueError(_SPACE_(repr(m[0]), _invalid_))
582
+ e, n, m = _enstr2m3(*m[2:])
583
+
584
+ if Mgrs is None:
585
+ r = Mgrs4Tuple(zB, EN, e, n, name=name)
586
+ _ = r.toMgrs(resolution=m) # validate
587
+ else:
588
+ r = Mgrs(zB, EN, e, n, datum=datum, resolution=m, name=name)
589
+ return r
590
+
591
+ return _parseX(_MGRS, strMGRS, datum, Mgrs, name,
592
+ strMGRS=strMGRS, Error=MGRSError)
593
+
594
+
595
+ def toMgrs(utmups, Mgrs=Mgrs, name=NN, **Mgrs_kwds):
596
+ '''Convert a UTM or UPS coordinate to an MGRS grid reference.
597
+
598
+ @arg utmups: A UTM or UPS coordinate (L{Utm}, L{Etm} or L{Ups}).
599
+ @kwarg Mgrs: Optional class to return the MGRS grid reference
600
+ (L{Mgrs}) or C{None}.
601
+ @kwarg name: Optional B{C{Mgrs}} name (C{str}).
602
+ @kwarg Mgrs_kwds: Optional, additional B{C{Mgrs}} keyword
603
+ arguments, ignored if C{B{Mgrs} is None}.
604
+
605
+ @return: The MGRS grid reference as B{C{Mgrs}} or if
606
+ C{B{Mgrs} is None} as an L{Mgrs6Tuple}C{(zone,
607
+ EN, easting, northing, band, datum)}.
608
+
609
+ @raise MGRSError: Invalid B{C{utmups}}.
610
+
611
+ @raise TypeError: If B{C{utmups}} is not L{Utm} nor L{Etm}
612
+ nor L{Ups}.
613
+ '''
614
+ # _MODS.utmups.utmupsValidate(utmups, MGRS=True, Error-MGRSError)
615
+ _xinstanceof(Utm, Ups, utmups=utmups) # Utm, Etm, Ups
616
+ try:
617
+ e, n = utmups.eastingnorthing2(falsed=True)
618
+ E, e = _um100km2(e)
619
+ N, n = _um100km2(n)
620
+ B, z = utmups.band, utmups.zone
621
+ if _UTM_ZONE_MIN <= z <= _UTM_ZONE_MAX:
622
+ i = z - 1
623
+ # columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z,
624
+ # then repeating every 3rd zone (note E-1 because
625
+ # eastings start at 166e3 due to 500km false origin)
626
+ EN = _LeUTM[i % 3][E - 1]
627
+ # rows in even zones are A-V, in odd zones are F-E
628
+ EN += _LnUTM[i % 2][N % len(_LnUTM[0])]
629
+ elif z == _UPS_ZONE:
630
+ EN = _LeUPS[B][E - _FeUPS[B]]
631
+ EN += _LnUPS[B][N - _FnUPS[B]]
632
+ else:
633
+ raise _ValueError(zone=z)
634
+ except (IndexError, TypeError, ValueError) as x:
635
+ raise MGRSError(B=B, E=E, N=N, utmups=utmups, cause=x)
636
+
637
+ if Mgrs is None:
638
+ r = Mgrs4Tuple(Fmt.zone(z), EN, e, n).to6Tuple(B, utmups.datum)
639
+ else:
640
+ kwds = _xkwds(Mgrs_kwds, band=B, datum=utmups.datum)
641
+ r = Mgrs(z, EN, e, n, **kwds)
642
+ return _xnamed(r, name or utmups.name)
643
+
644
+
645
+ def _um100km2(m):
646
+ '''(INTERNAL) An MGRS east-/northing truncated to micrometer (um)
647
+ precision and to grid tile C{M} and C{m}eter within the tile.
648
+ '''
649
+ m = int(m / _1um) * _1um # micrometer
650
+ M, m = divmod(m, _100km)
651
+ return int(M), m
652
+
653
+
654
+ if __name__ == '__main__':
655
+
656
+ from pygeodesy.ellipsoidalVincenty import fabs, LatLon
657
+ from pygeodesy.lazily import _getenv, printf
658
+
659
+ # from math import fabs # from .ellipsoidalVincenty
660
+ from os import access as _access, linesep as _NL, X_OK as _X_OK
661
+
662
+ # <https://GeographicLib.sourceforge.io/C++/doc/GeoConvert.1.html>
663
+ _GeoConvert = _getenv(_PYGEODESY_GEOCONVERT_, '/opt/local/bin/GeoConvert')
664
+ if _access(_GeoConvert, _X_OK):
665
+ GC_m = _GeoConvert, '-m' # -m converts latlon to MGRS
666
+ printf(' using: %s ...', _SPACE_.join(GC_m))
667
+ from pygeodesy.solveBase import _popen2
668
+ else:
669
+ GC_m = _popen2 = None
670
+
671
+ e = n = 0
672
+ try:
673
+ for lat in range(-90, 91, 1):
674
+ printf('%6s: lat %s ...', n, lat, end=NN, flush=True)
675
+ nl = _NL
676
+ for lon in range(-180, 181, 1):
677
+ m = LatLon(lat, lon).toMgrs()
678
+ if _popen2:
679
+ t = '%s %s' % (lat, lon)
680
+ g = _popen2(GC_m, stdin=t)[1]
681
+ t = m.toStr() # sep=NN
682
+ if t != g:
683
+ e += 1
684
+ printf('%s%6s: %s: %r vs %r (lon %s)', nl, -e, m, t, g, lon)
685
+ nl = NN
686
+ t = m.toLatLon(LatLon=LatLon)
687
+ d = max(fabs(t.lat - lat), fabs(t.lon - lon))
688
+ if d > 1e-9 and -90 < lat < 90 and -180 < lon < 180:
689
+ e += 1
690
+ printf('%s%6s: %s: %s vs %s %.6e', nl, -e, m, t.latlon, (float(lat), float(lon)), d)
691
+ nl = NN
692
+ n += 1
693
+ if nl:
694
+ printf(' OK')
695
+ except KeyboardInterrupt:
696
+ printf(nl)
697
+
698
+ p = e * 100.0 / n
699
+ printf('%6s: %s errors (%.2f%%)', n, (e if e else 'no'), p)
700
+
701
+ # **) MIT License
702
+ #
703
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
704
+ #
705
+ # Permission is hereby granted, free of charge, to any person obtaining a
706
+ # copy of this software and associated documentation files (the "Software"),
707
+ # to deal in the Software without restriction, including without limitation
708
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
709
+ # and/or sell copies of the Software, and to permit persons to whom the
710
+ # Software is furnished to do so, subject to the following conditions:
711
+ #
712
+ # The above copyright notice and this permission notice shall be included
713
+ # in all copies or substantial portions of the Software.
714
+ #
715
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
716
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
717
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
718
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
719
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
720
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
721
+ # OTHER DEALINGS IN THE SOFTWARE.