Coverage for pygeodesy/utmupsBase.py: 97%
237 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private class C{UtmUpsBase}, functions and constants
5for L{epsg}, L{etm}, L{mgrs}, L{ups} and L{utm}.
6'''
8from pygeodesy.basics import isint, isscalar, isstr, neg_, \
9 _xinstanceof, _xsubclassof
10from pygeodesy.constants import _float, _0_0, _0_5, _N_90_0, _180_0
11from pygeodesy.datums import _ellipsoidal_datum, _WGS84
12from pygeodesy.dms import degDMS, parseDMS2
13from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB
14from pygeodesy.errors import _or, ParseError, _parseX, _UnexpectedError, \
15 _ValueError, _xkwds, _xkwds_not, _xkwds_pop2
16# from pygeodesy.internals import _name__, _under # from .named
17from pygeodesy.interns import NN, _A_, _B_, _COMMA_, _Error_, \
18 _gamma_, _n_a_, _not_, _N_, _NS_, _PLUS_, \
19 _scale_, _SPACE_, _Y_, _Z_
20from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
21from pygeodesy.named import _NamedBase, _xnamed, _name__, _under
22from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple
23from pygeodesy.props import deprecated_method, property_doc_, _update_all, \
24 deprecated_property_RO, Property_RO, property_RO
25from pygeodesy.streprs import Fmt, fstr, _fstrENH2, _xattrs, _xzipairs
26from pygeodesy.units import Band, Easting, Northing, Scalar, Zone
27from pygeodesy.utily import _Wrap, wrap360
29__all__ = _ALL_LAZY.utmupsBase
30__version__ = '24.05.19'
32_UPS_BANDS = _A_, _B_, _Y_, _Z_ # UPS polar bands SE, SW, NE, NW
33# _UTM_BANDS = _MODS.utm._Bands
35_UTM_LAT_MAX = _float( 84) # PYCHOK for export (C{degrees})
36_UTM_LAT_MIN = _float(-80) # PYCHOK for export (C{degrees})
38_UPS_LAT_MAX = _UTM_LAT_MAX - _0_5 # PYCHOK includes 30' UTM overlap
39_UPS_LAT_MIN = _UTM_LAT_MIN + _0_5 # PYCHOK includes 30' UTM overlap
41_UPS_LATS = {_A_: _N_90_0, _Y_: _UTM_LAT_MAX, # UPS band bottom latitudes,
42 _B_: _N_90_0, _Z_: _UTM_LAT_MAX} # PYCHOK see .Mgrs.bandLatitude
44_UTM_ZONE_MAX = 60 # PYCHOK for export
45_UTM_ZONE_MIN = 1 # PYCHOK for export
46_UTM_ZONE_OFF_MAX = 60 # PYCHOK max Central meridian offset (C{degrees})
48_UPS_ZONE = _UTM_ZONE_MIN - 1 # PYCHOK for export
49_UPS_ZONE_STR = Fmt.zone(_UPS_ZONE) # PYCHOK for export
51_UTMUPS_ZONE_INVALID = -4 # PYCHOK for export too
52_UTMUPS_ZONE_MAX = _UTM_ZONE_MAX # PYCHOK for export too, by .units.py
53_UTMUPS_ZONE_MIN = _UPS_ZONE # PYCHOK for export too, by .units.py
55# _MAX_PSEUDO_ZONE = -1
56# _MIN_PSEUDO_ZONE = -4
57# _UTMUPS_ZONE_MATCH = -3
58# _UTMUPS_ZONE_STANDARD = -1
59# _UTM = -2
62def _hemi(lat, N=0): # imported by .ups, .utm
63 '''Return the hemisphere letter.
65 @arg lat: Latitude (C{degrees} or C{radians}).
66 @kwarg N: Minimal North latitude, C{0} or C{_N_}.
68 @return: C{'N'|'S'} for north-/southern hemisphere.
69 '''
70 return _NS_[int(lat < N)]
73def _to4lldn(latlon, lon, datum, name, wrap=False):
74 '''(INTERNAL) Return 4-tuple (C{lat, lon, datum, name}).
75 '''
76 try:
77 # if lon is not None:
78 # raise AttributeError
79 lat, lon = float(latlon.lat), float(latlon.lon)
80 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon)
81 if wrap:
82 _Wrap.latlon(lat, lon)
83 d = datum or latlon.datum
84 except AttributeError:
85 lat, lon = _Wrap.latlonDMS2(latlon, lon) if wrap else \
86 parseDMS2(latlon, lon) # clipped
87 d = datum or _WGS84
88 return lat, lon, d, _name__(name, _or_nameof=latlon)
91def _to3zBhp(zone, band, hemipole=NN, Error=_ValueError): # imported by .epsg, .ups, .utm, .utmups
92 '''Parse UTM/UPS zone, Band letter and hemisphere/pole letter.
94 @arg zone: Zone with/-out Band (C{scalar} or C{str}).
95 @kwarg band: Optional I{longitudinal/polar} Band letter (C{str}).
96 @kwarg hemipole: Optional hemisphere/pole letter (C{str}).
97 @kwarg Error: Optional error to raise, overriding the default
98 C{ValueError}.
100 @return: 3-Tuple (C{zone, Band, hemisphere/pole}) as (C{int, str,
101 'N'|'S'}) where C{zone} is C{0} for UPS or C{1..60} for
102 UTM and C{Band} is C{'A'..'Z'} I{NOT} checked for valid
103 UTM/UPS bands.
105 @raise ValueError: Invalid B{C{zone}}, B{C{band}} or B{C{hemipole}}.
106 '''
107 try:
108 B, z = band, _UTMUPS_ZONE_INVALID
109 if isscalar(zone):
110 z = int(zone)
111 elif zone and isstr(zone):
112 if zone.isdigit():
113 z = int(zone)
114 elif len(zone) > 1:
115 B = zone[-1:]
116 z = int(zone[:-1])
117 elif zone.upper() in _UPS_BANDS: # single letter
118 B = zone
119 z = _UPS_ZONE
121 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX:
122 hp = hemipole[:1].upper()
123 if hp in _NS_ or not hp:
124 z = Zone(z)
125 B = Band(B.upper())
126 if B.isalpha():
127 return z, B, (hp or _hemi(B, _N_))
128 elif not B:
129 return z, B, hp
131 raise ValueError # _invalid_
132 except (AttributeError, IndexError, TypeError, ValueError) as x:
133 raise Error(zone=zone, band=B, hemipole=hemipole, cause=x)
136def _to3zll(lat, lon): # imported by .ups, .utm
137 '''Wrap lat- and longitude and determine UTM zone.
139 @arg lat: Latitude (C{degrees}).
140 @arg lon: Longitude (C{degrees}).
142 @return: 3-Tuple (C{zone, lat, lon}) as (C{int}, C{degrees90},
143 C{degrees180}) where C{zone} is C{1..60} for UTM.
144 '''
145 x = wrap360(lon + _180_0) # use wrap360 to get ...
146 z = int(x) // 6 + 1 # ... longitudinal UTM zone [1, 60] and ...
147 return Zone(z), lat, (x - _180_0) # ... -180 <= lon < 180
150class UtmUpsBase(_NamedBase):
151 '''(INTERNAL) Base class for L{Utm} and L{Ups} coordinates.
152 '''
153 _band = NN # latitude band letter ('A..Z')
154 _Bands = NN # valid Band letters, see L{Utm} and L{Ups}
155 _datum = _WGS84 # L{Datum}
156 _easting = _0_0 # Easting, see B{C{falsed}} (C{meter})
157 _Error = None # I{Must be overloaded}, see function C{notOverloaded}
158 _falsed = True # falsed easting and northing (C{bool})
159 _gamma = None # meridian conversion (C{degrees})
160 _hemisphere = NN # hemisphere ('N' or 'S'), different from UPS pole
161 _latlon = None # cached toLatLon (C{LatLon} or C{._toLLEB})
162 _northing = _0_0 # Northing, see B{C{falsed}} (C{meter})
163 _scale = None # grid or point scale factor (C{scalar}) or C{None}
164# _scale0 = _K0 # central scale factor (C{scalar})
165 _ups = None # cached toUps (L{Ups})
166 _utm = None # cached toUtm (L{Utm})
168 def __init__(self, easting, northing, band=NN, datum=None, falsed=True,
169 gamma=None, scale=None, **convergence):
170 '''(INTERNAL) New L{UtmUpsBase}.
171 '''
172 E = self._Error
173 if not E: # PYCHOK no cover
174 self._notOverloaded(callername=_under(_Error_))
176 self._easting = Easting(easting, Error=E)
177 self._northing = Northing(northing, Error=E)
179 if band:
180 self._band1(band)
182 if datum not in (None, self._datum):
183 self._datum = _ellipsoidal_datum(datum) # raiser=_datum_, name=band
185 if not falsed:
186 self._falsed = False
188 if convergence: # for backward compatibility
189 gamma, kwds = _xkwds_pop2(convergence, convergence=gamma)
190 if kwds:
191 raise _UnexpectedError(**kwds)
192 if gamma is not self._gamma:
193 self._gamma = Scalar(gamma=gamma, Error=E)
194 if scale is not self._scale:
195 self._scale = Scalar(scale=scale, Error=E)
197 def __repr__(self):
198 return self.toRepr(B=True)
200 def __str__(self):
201 return self.toStr()
203 def _band1(self, band):
204 '''(INTERNAL) Re/set the latitudinal or polar band.
205 '''
206 if band:
207 _xinstanceof(str, band=band)
208# if not self._Bands: # PYCHOK no cover
209# self._notOverloaded(callername=_under('Bands'))
210 if band not in self._Bands:
211 t = _or(*sorted(set(map(repr, self._Bands))))
212 raise self._Error(band=band, txt_not_=t)
213 self._band = band
214 elif self._band: # reset
215 self._band = NN
217 @deprecated_property_RO
218 def convergence(self):
219 '''DEPRECATED, use property C{gamma}.'''
220 return self.gamma
222 @property_doc_(''' the (ellipsoidal) datum of this coordinate.''')
223 def datum(self):
224 '''Get the datum (L{Datum}).
225 '''
226 return self._datum
228 @datum.setter # PYCHOK setter!
229 def datum(self, datum):
230 '''Set the (ellipsoidal) datum L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
231 '''
232 d = _ellipsoidal_datum(datum)
233 if self._datum != d:
234 _update_all(self)
235 self._datum = d
237 @Property_RO
238 def easting(self):
239 '''Get the easting (C{meter}).
240 '''
241 return self._easting
243 @Property_RO
244 def eastingnorthing(self):
245 '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)}).
246 '''
247 return EasNor2Tuple(self.easting, self.northing)
249 def eastingnorthing2(self, falsed=True):
250 '''Return easting and northing, falsed or unfalsed.
252 @kwarg falsed: If C{True} return easting and northing falsed
253 (C{bool}), otherwise unfalsed.
255 @return: An L{EasNor2Tuple}C{(easting, northing)} in C{meter}.
256 '''
257 e, n = self.falsed2
258 if self.falsed and not falsed:
259 e, n = neg_(e, n)
260 elif falsed and not self.falsed:
261 pass
262 else:
263 e = n = _0_0
264 return EasNor2Tuple(Easting( e + self.easting, Error=self._Error),
265 Northing(n + self.northing, Error=self._Error))
267 @Property_RO
268 def _epsg(self):
269 '''(INTERNAL) Cache for method L{toEpsg}.
270 '''
271 return _MODS.epsg.Epsg(self)
273 @Property_RO
274 def falsed(self):
275 '''Get easting and northing falsed (C{bool}).
276 '''
277 return self._falsed
279 @Property_RO
280 def falsed2(self): # PYCHOK no cover
281 '''I{Must be overloaded}.'''
282 self._notOverloaded(self)
284 @Property_RO
285 def gamma(self):
286 '''Get the meridian convergence (C{degrees}) or C{None}
287 if not available.
288 '''
289 return self._gamma
291 @property_RO
292 def hemisphere(self):
293 '''Get the hemisphere (C{str}, 'N'|'S').
294 '''
295 if not self._hemisphere:
296 self._toLLEB()
297 return self._hemisphere
299 def _latlon5(self, LatLon, **LatLon_kwds):
300 '''(INTERNAL) Get cached C{._toLLEB} as B{C{LatLon}} instance.
301 '''
302 ll = self._latlon
303 if LatLon is None:
304 r = LatLonDatum5Tuple(ll.lat, ll.lon, ll.datum,
305 ll.gamma, ll.scale)
306 else:
307 _xsubclassof(_LLEB, LatLon=LatLon)
308 kwds = _xkwds(LatLon_kwds, datum=ll.datum)
309 r = _xattrs(LatLon(ll.lat, ll.lon, **kwds),
310 ll, _under(_gamma_), _under(_scale_))
311 return _xnamed(r, ll.name)
313 def _latlon5args(self, ll, _toBand, unfalse, *other):
314 '''(INTERNAL) See C{._toLLEB} methods, functions C{ups.toUps8} and C{utm._toXtm8}
315 '''
316 ll._toLLEB_args = (unfalse,) + other
317 if unfalse:
318 if not self._band:
319 self._band = _toBand(ll.lat, ll.lon)
320 if not self._hemisphere:
321 self._hemisphere = _hemi(ll.lat)
322 self._latlon = ll
324 @Property_RO
325 def _lowerleft(self): # by .ellipsoidalBase._lowerleft
326 '''Get this UTM or UPS C{un}-centered (L{Utm} or L{Ups}) to its C{lowerleft}.
327 '''
328 return _lowerleft(self, 0)
330 @Property_RO
331 def _mgrs(self):
332 '''(INTERNAL) Cache for method L{toMgrs}.
333 '''
334 return _toMgrs(self)
336 @Property_RO
337 def _mgrs_lowerleft(self):
338 '''(INTERNAL) Cache for method L{toMgrs}, I{un}-centered.
339 '''
340 utmups = self._lowerleft
341 return self._mgrs if utmups is self else _toMgrs(utmups)
343 @Property_RO
344 def northing(self):
345 '''Get the northing (C{meter}).
346 '''
347 return self._northing
349 @Property_RO
350 def scale(self):
351 '''Get the grid scale (C{float}) or C{None}.
352 '''
353 return self._scale
355 @Property_RO
356 def scale0(self):
357 '''Get the central scale factor (C{float}).
358 '''
359 return self._scale0
361 @deprecated_method
362 def to2en(self, falsed=True): # PYCHOK no cover
363 '''DEPRECATED, use method C{eastingnorthing2}.
365 @return: An L{EasNor2Tuple}C{(easting, northing)}.
366 '''
367 return self.eastingnorthing2(falsed=falsed)
369 def toEpsg(self):
370 '''Determine the B{EPSG (European Petroleum Survey Group)} code.
372 @return: C{EPSG} code (C{int}).
374 @raise EPSGError: See L{Epsg}.
375 '''
376 return self._epsg
378 def _toLLEB(self, **kwds): # PYCHOK no cover
379 '''(INTERNAL) I{Must be overloaded}.'''
380 self._notOverloaded(**kwds)
382 def toMgrs(self, center=False):
383 '''Convert this UTM/UPS coordinate to an MGRS grid reference.
385 @kwarg center: If C{True}, I{un}-center this UTM or UPS to
386 its C{lowerleft} (C{bool}) or by C{B{center}
387 meter} (C{scalar}).
389 @return: The MGRS grid reference (L{Mgrs}).
391 @see: Function L{pygeodesy.toMgrs} in module L{mgrs} for more details.
393 @note: If not specified, the I{latitudinal} C{band} is computed from
394 the (geodetic) latitude and the C{datum}.
395 '''
396 return self._mgrs if center in (False, 0, _0_0) else (
397 self._mgrs_lowerleft if center in (True,) else
398 _toMgrs(_lowerleft(self, center))) # PYCHOK indent
400 def _toRepr(self, fmt, B, cs, prec, sep): # PYCHOK expected
401 '''(INTERNAL) Return a representation for this ETM/UTM/UPS coordinate.
402 '''
403 t = self.toStr(prec=prec, sep=None, B=B, cs=cs) # hemipole
404 T = 'ZHENCS'[:len(t)]
405 return _xzipairs(T, t, sep=sep, fmt=fmt)
407 def _toStr(self, hemipole, B, cs, prec, sep):
408 '''(INTERNAL) Return a string for this ETM/UTM/UPS coordinate.
409 '''
410 z = NN(Fmt.zone(self.zone), (self.band if B else NN)) # PYCHOK band
411 t = (z, hemipole) + _fstrENH2(self, prec, None)[0]
412 if cs:
413 prec = cs if isint(cs) else 8 # for backward compatibility
414 t += (_n_a_ if self.gamma is None else
415 degDMS(self.gamma, prec=prec, pos=_PLUS_),
416 _n_a_ if self.scale is None else
417 fstr(self.scale, prec=prec))
418 return t if sep is None else sep.join(t)
421def _lowerleft(utmups, center): # by .ellipsoidalBase._lowerleft
422 '''(INTERNAL) I{Un}-center a B{C{utmups}} to its C{lowerleft} by
423 C{B{center} meter} or by a I{guess} if B{C{center}} is C{0}.
424 '''
425 if center:
426 e = n = -center
427 else:
428 c = 5 # center
429 for _ in range(3):
430 c *= 10 # 50, 500, 5000
431 t = c * 2
432 e = int(utmups.easting % t)
433 n = int(utmups.northing % t)
434 if (e == c and n in (c, c - 1)) or \
435 (n == c and e in (c, c - 1)):
436 break
437 else:
438 return utmups # unchanged
440 r = _xkwds_not(None, datum=utmups.datum,
441 gamma=utmups.gamma,
442 scale=utmups.scale)
443 return utmups.classof(utmups.zone, utmups.hemisphere,
444 utmups.easting - e, utmups.northing - n,
445 band=utmups.band, falsed=utmups.falsed, **r)
448def _parseUTMUPS5(strUTMUPS, UPS, Error=ParseError, band=NN, sep=_COMMA_):
449 '''(INTERNAL) Parse a string representing a UTM or UPS coordinate
450 consisting of C{"zone[band] hemisphere/pole easting northing"}.
452 @arg strUTMUPS: A UTM or UPS coordinate (C{str}).
453 @kwarg band: Optional, default Band letter (C{str}).
454 @kwarg sep: Optional, separator to split (",").
456 @return: 5-Tuple (C{zone, hemisphere/pole, easting, northing,
457 band}).
459 @raise ParseError: Invalid B{C{strUTMUPS}}.
460 '''
461 def _UTMUPS5(strUTMUPS, UPS, band, sep):
462 u = strUTMUPS.lstrip()
463 if UPS and not u.startswith(_UPS_ZONE_STR):
464 raise ValueError(_not_(_UPS_ZONE_STR))
466 u = u.replace(sep, _SPACE_).strip().split()
467 if len(u) < 4:
468 raise ValueError(_not_(sep))
470 z, h = u[:2]
471 if h[:1].upper() not in _NS_:
472 raise ValueError(_SPACE_(h, _not_(_NS_)))
474 if z.isdigit():
475 z, B = int(z), band
476 else:
477 for i in range(len(z)):
478 if not z[i].isdigit():
479 # int('') raises ValueError
480 z, B = int(z[:i]), z[i:]
481 break
482 else:
483 raise ValueError(z)
485 e, n = map(float, u[2:4])
486 return z, h.upper(), e, n, B.upper()
488 return _parseX(_UTMUPS5, strUTMUPS, UPS, band, sep,
489 strUTMUPS=strUTMUPS, Error=Error)
492def _toMgrs(utmups):
493 '''(INTERNAL) Convert a L{Utm} or L{Ups} to an L{Mgrs} instance.
494 '''
495 return _MODS.mgrs.toMgrs(utmups, datum=utmups.datum, name=utmups.name)
498__all__ += _ALL_DOCS(UtmUpsBase)
500# **) MIT License
501#
502# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
503#
504# Permission is hereby granted, free of charge, to any person obtaining a
505# copy of this software and associated documentation files (the "Software"),
506# to deal in the Software without restriction, including without limitation
507# the rights to use, copy, modify, merge, publish, distribute, sublicense,
508# and/or sell copies of the Software, and to permit persons to whom the
509# Software is furnished to do so, subject to the following conditions:
510#
511# The above copyright notice and this permission notice shall be included
512# in all copies or substantial portions of the Software.
513#
514# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
515# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
516# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
517# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
518# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
519# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
520# OTHER DEALINGS IN THE SOFTWARE.