Coverage for pygeodesy/utm.py: 97%
259 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
2# -*- coding: utf-8 -*-
4u'''I{Veness}' Universal Transverse Mercator (UTM) projection.
6Classes L{Utm} and L{UTMError} and functions L{parseUTM5}, L{toUtm8} and
7L{utmZoneBand5}.
9Pure Python implementation of UTM / WGS-84 conversion functions using
10an ellipsoidal earth model, transcoded from JavaScript originals by
11I{(C) Chris Veness 2011-2024} published under the same MIT Licence**, see
12U{UTM<https://www.Movable-Type.co.UK/scripts/latlong-utm-mgrs.html>} and
13U{Module utm<https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-utm.html>}.
15The U{UTM<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>}
16system is a 2-dimensional Cartesian coordinate system providing another way
17to identify locations on the surface of the earth. UTM is a set of 60
18transverse Mercator projections, normally based on the WGS-84 ellipsoid.
19Within each zone, coordinates are represented as B{C{easting}}s and B{C{northing}}s,
20measured in metres.
22This module includes some of I{Charles Karney}'s U{'Transverse Mercator with an
23accuracy of a few nanometers'<https://ArXiv.org/pdf/1002.1417v3.pdf>}, 2011
24(building on Krüger's U{'Konforme Abbildung des Erdellipsoids in der Ebene'
25<https://bib.GFZ-Potsdam.DE/pub/digi/krueger2.pdf>}, 1912) and C++ class
26U{TransverseMercator<https://GeographicLib.SourceForge.io/C++/doc/
27classGeographicLib_1_1TransverseMercator.html>}.
29Some other references are U{Universal Transverse Mercator coordinate system
30<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>},
31U{Transverse Mercator Projection<https://GeographicLib.SourceForge.io/tm.html>}
32and Henrik Seidel U{'Die Mathematik der Gauß-Krueger-Abbildung'
33<https://DE.WikiPedia.org/wiki/Gauß-Krüger-Koordinatensystem>}, 2006.
34'''
36from pygeodesy.basics import len2, map2, neg # splice
37from pygeodesy.constants import EPS, EPS0, _K0_UTM, _0_0, _0_0001
38from pygeodesy.datums import _ellipsoidal_datum, _WGS84, _under
39from pygeodesy.dms import degDMS, parseDMS2
40from pygeodesy.errors import MGRSError, RangeError, _ValueError, \
41 _xkwds_get, _xkwds_pop2
42from pygeodesy.fmath import fdot_, fdot3, hypot, hypot1, _operator
43# from pygeodesy.internals import _under # from .datums
44from pygeodesy.interns import MISSING, NN, _by_, _COMMASPACE_, _N_, \
45 _NS_, _outside_, _range_, _S_, _scale0_, \
46 _SPACE_, _UTM_, _V_, _X_, _zone_
47from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
48from pygeodesy.namedTuples import EasNor2Tuple, UtmUps5Tuple, \
49 UtmUps8Tuple, UtmUpsLatLon5Tuple
50from pygeodesy.props import deprecated_method, property_doc_, \
51 Property_RO
52from pygeodesy.streprs import Fmt, unstr
53from pygeodesy.units import Band, Int, Lat, Lon, Meter, Zone
54from pygeodesy.utily import atan1, atan2, degrees90, degrees180, sincos2
55from pygeodesy.utmupsBase import _hemi, _LLEB, _parseUTMUPS5, _to4lldn, \
56 _to3zBhp, _to3zll, _UPS_LATS, _UPS_ZONE, \
57 _UTM_LAT_MAX, _UTM_ZONE_MAX, \
58 _UTM_LAT_MIN, _UTM_ZONE_MIN, \
59 _UTM_ZONE_OFF_MAX, UtmUpsBase
61from math import asinh, atanh, cos, cosh, degrees, fabs, radians, \
62 sin, sinh, tan, tanh # tan as _tan
63# import operator as _operator # from .fmath
65__all__ = _ALL_LAZY.utm
66__version__ = '24.11.26'
68_Bands = 'CDEFGHJKLMNPQRSTUVWXX' # UTM latitude bands C..X (no
69# I|O) 8° each, covering 80°S to 84°N and X repeated for 80-84°N
70_bandLat_ = 'bandLat'
71_FalseEasting = Meter( 500e3) # falsed offset (C{meter})
72_FalseNorthing = Meter(10000e3) # falsed offset (C{meter})
73_SvalbardXzone = {32: 9, 34: 21, 36: 33} # [zone] longitude
76class UTMError(_ValueError):
77 '''Universal Transverse Mercator (UTM parse or other L{Utm} issue.
78 '''
79 pass
82class _Kseries(object):
83 '''(INTERNAL) Alpha or Beta Krüger series.
85 Krüger series summations for B{C{eta}}, B{C{ksi}}, B{C{p}} and B{C{q}},
86 caching the C{cos}, C{cosh}, C{sin} and C{sinh} values for
87 the given B{C{eta}} and B{C{ksi}} angles (in C{radians}).
88 '''
89 def __init__(self, AB, x, y):
90 '''(INTERNAL) New Alpha or Beta Krüger series
92 @arg AB: Krüger Alpha or Beta series coefficients
93 (C{4-, 6- or 8-tuple}).
94 @arg x: Eta angle (C{radians}).
95 @arg y: Ksi angle (C{radians}).
96 '''
97 n, j2 = len2(range(2, len(AB) * 2 + 1, 2))
98 _m2, _x = map2, _operator.mul
100 self._ab = AB
101 self._pq = _m2(_x, j2, AB)
102# assert len(self._ab) == len(self._pq) == n
104 x2 = _m2(_x, j2, (x,) * n)
105 self._chx = _m2(cosh, x2)
106 self._shx = _m2(sinh, x2)
107# assert len(x2) == len(self._chx) == len(self._shx) == n
109 y2 = _m2(_x, j2, (y,) * n)
110 self._cy = _m2(cos, y2)
111 self._sy = _m2(sin, y2)
112 # self._sy, self._cy = splice(sincos2(*y2)) # PYCHOK false
113# assert len(y2) == len(self._cy) == len(self._sy) == n
115 def xs(self, x0):
116 '''(INTERNAL) Eta summation (C{float}).
117 '''
118 return fdot3(self._ab, self._cy, self._shx, start=x0)
120 def ys(self, y0):
121 '''(INTERNAL) Ksi summation (C{float}).
122 '''
123 return fdot3(self._ab, self._sy, self._chx, start=y0)
125 def ps(self, p0):
126 '''(INTERNAL) P summation (C{float}).
127 '''
128 return fdot3(self._pq, self._cy, self._chx, start=p0)
130 def qs(self, q0):
131 '''(INTERNAL) Q summation (C{float}).
132 '''
133 return fdot3(self._pq, self._sy, self._shx, start=q0)
136class Utm(UtmUpsBase):
137 '''Universal Transverse Mercator (UTM) coordinate.
138 '''
139# _band = NN # latitudinal band letter ('C'|..|'X', no 'I'|'O')
140 _Bands = _Bands # latitudinal Band letters (C{tuple})
141 _Error = UTMError # or etm.ETMError
142# _scale = None # grid scale factor (C{scalar}) or C{None}
143 _scale0 = _K0_UTM # central scale factor (C{scalar})
144 _zone = 0 # longitudinal zone (C{int} 1..60)
146 def __init__(self, zone=31, hemisphere=_N_, easting=166022, # PYCHOK expected
147 northing=0, band=NN, datum=_WGS84, falsed=True,
148 gamma=None, scale=None, **name_convergence):
149 '''New L{Utm} UTM coordinate.
151 @kwarg zone: Longitudinal UTM zone (C{int}, 1..60) or zone with/-out
152 I{latitudinal} Band letter (C{str}, '1C'|..|'60X').
153 @kwarg hemisphere: Northern or southern hemisphere (C{str}, C{'N[orth]'}
154 or C{'S[outh]'}).
155 @kwarg easting: Easting, see B{C{falsed}} (C{meter}).
156 @kwarg northing: Northing, see B{C{falsed}} (C{meter}).
157 @kwarg band: Optional, I{latitudinal} band (C{str}, 'C'|..|'X', no 'I'|'O').
158 @kwarg datum: Optional, this coordinate's datum (L{Datum}, L{Ellipsoid},
159 L{Ellipsoid2} or L{a_f2Tuple}).
160 @kwarg falsed: If C{True}, both B{C{easting}} and B{C{northing}} are
161 falsed (C{bool}).
162 @kwarg gamma: Optional meridian convergence, bearing off grid North,
163 clockwise from true North to save (C{degrees}) or C{None}.
164 @kwarg scale: Optional grid scale factor to save (C{scalar}) or C{None}.
165 @kwarg name_convergence: Optional C{B{name}=NN} (C{str}) and DEPRECATED
166 keyword argument C{B{convergence}=None}, use B{C{gamma}}.
168 @raise TypeError: Invalid or near-spherical B{C{datum}}.
170 @raise UTMError: Invalid B{C{zone}}, B{C{hemishere}}, B{C{easting}},
171 B{C{northing}}, B{C{band}}, B{C{convergence}} or
172 B{C{scale}}.
173 '''
174 if name_convergence:
175 gamma, name = _xkwds_pop2(name_convergence, convergence=gamma)
176 if name:
177 self.name = name
179 self._zone, B, _ = _to3zBlat(zone, band)
181 h = str(hemisphere)[:1].upper()
182 if h not in _NS_:
183 raise self._Error(hemisphere=hemisphere)
184 self._hemisphere = h
186 e, n = easting, northing # Easting(easting), ...
187# if not falsed:
188# e, n = _false2(e, n, h)
189# # check easting/northing (with 40km overlap
190# # between zones) - is this worthwhile?
191# @raise RangeError: If B{C{easting}} or B{C{northing}} outside
192# the valid UTM range.
193# if 120e3 > e or e > 880e3:
194# raise RangeError(easting=easting)
195# if 0 > n or n > _FalseNorthing:
196# raise RangeError(northing=northing)
198 UtmUpsBase.__init__(self, e, n, band=B, datum=datum, falsed=falsed,
199 gamma=gamma, scale=scale)
201 def __eq__(self, other):
202 return isinstance(other, Utm) and other.zone == self.zone \
203 and other.hemisphere == self.hemisphere \
204 and other.easting == self.easting \
205 and other.northing == self.northing \
206 and other.band == self.band \
207 and other.datum == self.datum
209 def __repr__(self):
210 return self.toRepr(B=True)
212 def __str__(self):
213 return self.toStr()
215 def _xcopy2(self, Xtm, **name):
216 '''(INTERNAL) Make copy as an B{C{Xtm}} instance.
218 @arg Xtm: Class to return the copy (C{Xtm=Etm}, C{Xtm=Utm} or
219 C{self.classof}).
220 '''
221 return Xtm(self.zone, self.hemisphere, self.easting, self.northing,
222 band=self.band, datum=self.datum, falsed=self.falsed,
223 gamma=self.gamma, scale=self.scale, name=self._name__(name))
225 @property_doc_(''' the I{latitudinal} band.''')
226 def band(self):
227 '''Get the I{latitudinal} band (C{'C'|..|'X'}).
228 '''
229 if not self._band:
230 self._toLLEB()
231 return self._band
233 @band.setter # PYCHOK setter!
234 def band(self, band):
235 '''Set or reset the I{latitudinal} band letter (C{'C'|..|'X'})
236 or C{None} or C{""} to reset.
238 @raise TypeError: Invalid B{C{band}}.
240 @raise ValueError: Invalid B{C{band}}.
241 '''
242 self._band1(band)
244 @Property_RO
245 def _etm(self):
246 '''(INTERNAL) Cache for method L{toEtm}.
247 '''
248 return self._xcopy2(_MODS.etm.Etm)
250 @Property_RO
251 def falsed2(self):
252 '''Get the easting and northing falsing (L{EasNor2Tuple}C{(easting, northing)}).
253 '''
254 e = n = 0
255 if self.falsed:
256 e = _FalseEasting # relative to central meridian
257 if self.hemisphere == _S_: # relative to equator
258 n = _FalseNorthing
259 return EasNor2Tuple(e, n)
261 def parse(self, strUTM, **name):
262 '''Parse a string to a similar L{Utm} instance.
264 @arg strUTM: The UTM coordinate (C{str}), see function L{parseUTM5}.
265 @kwarg name: Optional instance name (C{str}), overriding this name.
267 @return: The similar instance (L{Utm}).
269 @raise UTMError: Invalid B{C{strUTM}}.
271 @see: Functions L{pygeodesy.parseUPS5} and L{pygeodesy.parseUTMUPS5}.
272 '''
273 return parseUTM5(strUTM, datum=self.datum, Utm=self.classof,
274 name=self._name__(name))
276 @deprecated_method
277 def parseUTM(self, strUTM): # PYCHOK no cover
278 '''DEPRECATED, use method L{Utm.parse}.'''
279 return self.parse(strUTM)
281 @Property_RO
282 def pole(self):
283 '''Get the top center of (stereographic) projection, C{""} always.
284 '''
285 return NN # N/A for UTM
287 def toEtm(self):
288 '''Copy this UTM to an ETM coordinate.
290 @return: The ETM coordinate (L{Etm}).
291 '''
292 return self._etm
294 def toLatLon(self, LatLon=None, eps=EPS, unfalse=True, **LatLon_kwds):
295 '''Convert this UTM coordinate to an (ellipsoidal) geodetic point.
297 @kwarg LatLon: Optional, ellipsoidal class to return the geodetic
298 point (C{LatLon}) or C{None}.
299 @kwarg eps: Optional convergence limit, L{EPS} or above (C{float}).
300 @kwarg unfalse: Unfalse B{C{easting}} and B{C{northing}}
301 if falsed (C{bool}).
302 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
303 arguments, ignored if C{B{LatLon} is None}.
305 @return: This UTM as (B{C{LatLon}}) or if B{C{LatLon}} is
306 C{None}, as L{LatLonDatum5Tuple}C{(lat, lon, datum,
307 gamma, scale)}.
309 @raise TypeError: Invalid B{C{datum}} or B{C{LatLon}} is not ellipsoidal.
311 @raise UTMError: Invalid meridional radius or H-value.
313 '''
314 if eps < EPS:
315 eps = EPS # less doesn't converge
317 if self._latlon and self._latlon._toLLEB_args == (unfalse, eps):
318 return self._latlon5(LatLon)
319 else:
320 self._toLLEB(unfalse=unfalse, eps=eps)
321 return self._latlon5(LatLon, **LatLon_kwds)
323 def _toLLEB(self, unfalse=True, eps=EPS): # PYCHOK signature
324 '''(INTERNAL) Compute (ellipsoidal) lat- and longitude.
325 '''
326 x, y = self.eastingnorthing2(falsed=not unfalse)
328 E = self.datum.ellipsoid
329 # from Karney 2011 Eq 15-22, 36
330 A0 = self.scale0 * E.A
331 if A0 < EPS0:
332 raise self._Error(meridional=A0)
333 x = x / A0 # /= chokes PyChecker
334 y = y / A0
335 K = _Kseries(E.BetaKs, x, y) # Krüger series
336 x = neg(K.xs(-x)) # η' eta
337 y = neg(K.ys(-y)) # ξ' ksi
339 sy, cy = sincos2(y)
340 shx = sinh(x)
341 H = hypot(shx, cy)
342 if H < EPS0:
343 raise self._Error(H=H)
345 T = sy / H # τʹ == τ0
346 p = _0_0 # previous d
347 e = _0_0001 * eps
348 for T, i, d in E._es_tauf3(T, T): # 4-5 trips
349 # d may toggle on +/-1.12e-16 or +/-4.47e-16,
350 # see the references at C{Ellipsoid.es_tauf}
351 if fabs(d) < eps or fabs(d + p) < e:
352 break
353 p = d
354 else:
355 t = unstr(self.toLatLon, eps=eps, unfalse=unfalse)
356 raise self._Error(Fmt.no_convergence(d, eps), txt=t)
358 a = atan1(T) # phi, lat
359 b = atan2(shx, cy)
360 if unfalse and self.falsed:
361 b += radians(_cmlon(self.zone))
362 ll = _LLEB(degrees90(a), degrees180(b), datum=self.datum, name=self.name)
364 # gamma and scale: Karney 2011 Eq 26, 27 and 28
365 p = neg(K.ps(-1))
366 q = K.qs(0)
367 g = degrees(atan1(tan(y) * tanh(x)) + atan2(q, p))
368 k = hypot(p, q) * E.a / A0
369 if k:
370 k = E.e2s(sin(a)) * hypot1(T) * H / k # INF?
371 ll._iteration = i
372 self._latlon5args(ll, g, k, _toBand, unfalse, eps)
374 def toRepr(self, prec=0, fmt=Fmt.SQUARE, sep=_COMMASPACE_, B=False, cs=False, **unused): # PYCHOK expected
375 '''Return a string representation of this UTM coordinate.
377 Note that UTM coordinates are rounded, not truncated (unlike
378 MGRS grid references).
380 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
381 @kwarg fmt: Enclosing backets format (C{str}).
382 @kwarg sep: Optional separator between name:value pairs (C{str}).
383 @kwarg B: Optionally, include latitudinal band (C{bool}).
384 @kwarg cs: Optionally, include meridian convergence and grid
385 scale factor (C{bool} or non-zero C{int} to specify
386 the precison like B{C{prec}}).
388 @return: This UTM as a string C{"[Z:09[band], H:N|S, E:meter,
389 N:meter]"} plus C{", C:degrees, S:float"} if C{B{cs}
390 is True} (C{str}).
391 '''
392 return self._toRepr(fmt, B, cs, prec, sep)
394 def toStr(self, prec=0, sep=_SPACE_, B=False, cs=False): # PYCHOK expected
395 '''Return a string representation of this UTM coordinate.
397 To distinguish from MGRS grid zone designators, a space is
398 left between the zone and the hemisphere.
400 Note that UTM coordinates are rounded, not truncated (unlike
401 MGRS grid references).
403 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
404 @kwarg sep: Optional separator to join (C{str}) or C{None}
405 to return an unjoined C{tuple} of C{str}s.
406 @kwarg B: Optionally, include latitudinal band (C{bool}).
407 @kwarg cs: Optionally, include meridian convergence and grid
408 scale factor (C{bool} or non-zero C{int} to specify
409 the precison like B{C{prec}}).
411 @return: This UTM as a string with C{zone[band], hemisphere,
412 easting, northing, [convergence, scale]} in
413 C{"00 N|S meter meter"} plus C{" degrees float"} if
414 C{B{cs} is True} (C{str}).
415 '''
416 return self._toStr(self.hemisphere, B, cs, prec, sep)
418 def toUps(self, pole=NN, eps=EPS, falsed=True, **unused):
419 '''Convert this UTM coordinate to a UPS coordinate.
421 @kwarg pole: Optional top/center of the UPS projection,
422 (C{str}, 'N[orth]'|'S[outh]').
423 @kwarg eps: Optional convergence limit, L{EPS} or above
424 (C{float}), see method L{Utm.toLatLon}.
425 @kwarg falsed: If C{True}, false both easting and northing
426 (C{bool}).
428 @return: The UPS coordinate (L{Ups}).
429 '''
430 u = self._ups
431 if u is None or u.pole != (pole or u.pole) or bool(falsed) != u.falsed:
432 ll = self.toLatLon(LatLon=_LLEB, eps=eps, unfalse=True)
433 ups = _MODS.ups
434 self._ups = u = ups.toUps8(ll, Ups=ups.Ups, falsed=falsed,
435 name=self.name, pole=pole)
436 return u
438 def toUtm(self, zone, eps=EPS, falsed=True, **unused):
439 '''Convert this UTM coordinate to a different zone.
441 @arg zone: New UTM zone (C{int}).
442 @kwarg eps: Optional convergence limit, L{EPS} or above
443 (C{float}), see method L{Utm.toLatLon}.
444 @kwarg falsed: If C{True}, fFalse both easting and northing
445 (C{bool}).
447 @return: The UTM coordinate (L{Utm}).
448 '''
449 if zone == self.zone and falsed == self.falsed:
450 return self.copy()
451 elif zone:
452 u = self._utm
453 if u is None or u.zone != zone or bool(falsed) != u.falsed:
454 ll = self.toLatLon(LatLon=_LLEB, eps=eps, unfalse=True)
455 self._utm = u = toUtm8(ll, Utm=self.classof, falsed=falsed,
456 name=self.name, zone=zone)
457 return u
458 raise self._Error(zone=zone)
460 @Property_RO
461 def zone(self):
462 '''Get the (longitudinal) zone (C{int}, 1..60).
463 '''
464 return self._zone
467def _cmlon(zone):
468 '''(INTERNAL) Central meridian longitude (C{degrees180}).
469 '''
470 return (zone * 6) - 183
473def _false2(e, n, h):
474 '''(INTERNAL) False easting and northing.
475 '''
476 # Karney, "Test data for the transverse Mercator projection (2009)"
477 # <https://GeographicLib.SourceForge.io/C++/doc/transversemercator.html>
478 # and <https://Zenodo.org/record/32470#.W4LEJS2ZON8>
479 e += _FalseEasting # make e relative to central meridian
480 if h == _S_:
481 n += _FalseNorthing # make n relative to equator
482 return e, n
485def _parseUTM5(strUTM, datum, Xtm, falsed, Error=UTMError, **name): # imported by .etm
486 '''(INTERNAL) Parse a string representing a UTM coordinate,
487 consisting of C{"zone[band] hemisphere easting northing"},
488 see L{pygeodesy.parseETM5} and L{parseUTM5}.
489 '''
490 z, h, e, n, B = _parseUTMUPS5(strUTM, None, Error=Error)
491 if _UTM_ZONE_MIN > z or z > _UTM_ZONE_MAX or (B and B not in _Bands):
492 raise Error(strUTM=strUTM, zone=z, band=B)
494 return UtmUps5Tuple(z, h, e, n, B, Error=Error, **name) if Xtm is None else \
495 Xtm(z, h, e, n, band=B, datum=datum, falsed=bool(falsed), **name)
498def parseUTM5(strUTM, datum=_WGS84, Utm=Utm, falsed=True, **name):
499 '''Parse a string representing a UTM coordinate, consisting
500 of C{"zone[band] hemisphere easting northing"}.
502 @arg strUTM: A UTM coordinate (C{str}).
503 @kwarg datum: Optional datum to use (L{Datum}, L{Ellipsoid},
504 L{Ellipsoid2} or L{a_f2Tuple}).
505 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm})
506 or C{None}.
507 @kwarg falsed: Use C{B{falsed}=False} if both easting and northing
508 are I{not falsed} (C{bool}).
509 @kwarg name: Optional C{B{name}=NN} (C{str}).
511 @return: The UTM coordinate (B{C{Utm}}) or if C{B{Utm} is None}, a
512 L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}.
513 The C{hemipole} is the C{'N'|'S'} hemisphere.
515 @raise UTMError: Invalid B{C{strUTM}}.
517 @raise TypeError: Invalid B{C{datum}}.
518 '''
519 return _parseUTM5(strUTM, datum, Utm, falsed, **name)
522def toUtm8(latlon, lon=None, datum=None, Utm=Utm, falsed=True,
523 strict=True, zone=None, **name_cmoff):
524 '''Convert a lat-/longitude point to a UTM coordinate.
526 @arg latlon: Latitude (C{degrees}) or an (ellipsoidal)
527 geodetic C{LatLon} point.
528 @kwarg lon: Longitude (C{degrees}), required if B{C{latlon}} is
529 C{degrees}, ignored otherwise.
530 @kwarg datum: Optional datum for this UTM coordinate, overriding
531 B{C{latlon}}'s datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
532 or L{a_f2Tuple}).
533 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) or C{None}.
534 @kwarg falsed: If C{True}, false both easting and northing (C{bool}).
535 @kwarg strict: If C{True}, restrict B{C{lat}} to UTM ranges (C{bool}).
536 @kwarg zone: Optional UTM zone to enforce (C{int} or C{str}).
537 @kwarg name_cmoff: Optional C{B{name}=NN} (C{str}) and DEPRECATED keyword
538 argument C{B{cmoff}=True} to offset the longitude from the zone's
539 central meridian (C{bool}), use C{B{falsed}} instead.
541 @return: The UTM coordinate (B{C{Utm}}) or if C{B{Utm} is None} or C{B{falsed}
542 is False}, a L{UtmUps8Tuple}C{(zone, hemipole, easting, northing, band,
543 datum, gamma, scale)} where C{hemipole} is the C{'N'|'S'} hemisphere.
545 @raise RangeError: If B{C{lat}} outside the valid UTM bands or if B{C{lat}}
546 or B{C{lon}} outside the valid range and L{rangerrors
547 <pygeodesy.rangerrors>} is C{True}.
549 @raise TypeError: Invalid B{C{datum}} or B{C{latlon}} not ellipsoidal.
551 @raise UTMError: Invalid B{C{zone}}.
553 @raise ValueError: If B{C{lon}} is missing or B{C{latlon}} is invalid.
555 @note: Implements Karney’s method, using 8-th order Krüger series, giving
556 results accurate to 5 nm (or better) for distances up to 3,900 Km
557 from the central meridian.
558 '''
559 z, B, lat, lon, d, f, n = _to7zBlldfn(latlon, lon, datum,
560 falsed, zone, strict,
561 UTMError, **name_cmoff)
562 d = _ellipsoidal_datum(d, name=n)
563 E = d.ellipsoid
565 a, b = radians(lat), radians(lon)
566 # easting, northing: Karney 2011 Eq 7-14, 29, 35
567 sb, cb = sincos2(b)
569 T = tan(a)
570 T12 = hypot1(T)
571 S = sinh(E.e * atanh(E.e * T / T12))
573 T_ = fdot_(T, hypot1(S), -S, T12)
574 H = hypot(T_, cb)
576 y = atan2(T_, cb) # ξ' ksi
577 x = asinh(sb / H) # η' eta
579 A0 = E.A * getattr(Utm, _under(_scale0_), _K0_UTM) # Utm is class or None
581 K = _Kseries(E.AlphaKs, x, y) # Krüger series
582 y = K.ys(y) * A0 # ξ
583 x = K.xs(x) * A0 # η
585 # convergence: Karney 2011 Eq 23, 24
586 p_ = K.ps(1)
587 q_ = K.qs(0)
588 g = degrees(atan2(T_ * tan(b), hypot1(T_)) + atan2(q_, p_))
589 # scale: Karney 2011 Eq 25
590 k = E.e2s(sin(a)) * T12 / H * (A0 / E.a * hypot(p_, q_))
592 return _toXtm8(Utm, z, lat, x, y,
593 B, d, g, k, f, n, latlon, EPS)
596def _toBand(lat, *unused, **strict_Error): # see ups._toBand
597 '''(INTERNAL) Get the I{latitudinal} Band (row) letter.
598 '''
599 if _UTM_LAT_MIN <= lat < _UTM_LAT_MAX: # [-80, 84) like Veness
600 return _Bands[int(lat - _UTM_LAT_MIN) >> 3]
601 elif _xkwds_get(strict_Error, strict=True):
602 r = _range_(_UTM_LAT_MIN, _UTM_LAT_MAX, ropen=True)
603 t = _SPACE_(_outside_, _UTM_, _range_, r)
604 E = _xkwds_get(strict_Error, Error=RangeError)
605 raise E(lat=degDMS(lat), txt=t)
606 else:
607 return NN # None
610def _toXtm8(Xtm, z, lat, x, y, B, d, g, k, f, # PYCHOK 13+ args
611 n, latlon, eps_other, Error=UTMError):
612 '''(INTERNAL) Helper for methods L{toEtm8} and L{toUtm8}.
613 '''
614 h = _hemi(lat)
615 if f:
616 x, y = _false2(x, y, h)
617 if Xtm is None: # DEPRECATED
618 r = UtmUps8Tuple(z, h, x, y, B, d, g, k, Error=Error, name=n)
619 else:
620 r = Xtm(z, h, x, y, band=B, datum=d, falsed=f,
621 gamma=g, scale=k, name=n)
622 if isinstance(latlon, _LLEB) and d is latlon.datum: # see ups.toUtm8
623 r._latlon5args(latlon, g, k, _toBand, f, eps_other) # XXX weakref(latlon)?
624 elif not r._band:
625 r._band = _toBand(lat)
626 return r
629def _to3zBlat(zone, band, Error=UTMError): # in .mgrs
630 '''(INTERNAL) Check and return zone, Band and band latitude.
632 @arg zone: Zone number or string.
633 @arg band: Band letter.
634 @arg Error: Exception to raise (L{UTMError}).
636 @return: 3-Tuple (zone, Band, latitude).
637 '''
638 z, B, _ = _to3zBhp(zone, band, Error=Error)
639 if not (_UTM_ZONE_MIN <= z <= _UTM_ZONE_MAX or
640 (_UPS_ZONE == z and Error is MGRSError)):
641 raise Error(zone=zone)
643 b = None
644 if B:
645 if z == _UPS_ZONE: # polar
646 try:
647 b = Lat(_UPS_LATS[B], name=_bandLat_)
648 except KeyError:
649 raise Error(band=band or B, zone=z)
650 else: # UTM
651 b = _Bands.find(B)
652 if b < 0:
653 raise Error(band=band or B, zone=z)
654 b = Int((b << 3) - 80, name=_bandLat_)
655 B = Band(B)
656 elif Error is not UTMError:
657 raise Error(band=band, txt=MISSING)
659 return Zone(z), B, b
662def _to4zBll(lat, lon, cmoff=True, strict=True, Error=RangeError):
663 '''(INTERNAL) Return zone, Band and lat- and (central) longitude in degrees.
665 @arg lat: Latitude (C{degrees}).
666 @arg lon: Longitude (C{degrees}).
667 @kwarg cmoff: If C{True}, offset B{C{lon}} from the zone's central meridian.
668 @kwarg strict: Restrict B{C{lat}} to the UTM ranges (C{bool}).
669 @kwarg Error: Error for out of UTM range B{C{lat}}s.
671 @return: 4-Tuple (zone, Band, lat, lon).
672 '''
673 z, lat, lon = _to3zll(lat, lon) # in .utmupsBase
675 x = lon - _cmlon(z) # z before Norway/Svalbard
676 if fabs(x) > _UTM_ZONE_OFF_MAX:
677 t = _SPACE_(_outside_, _UTM_, _zone_, str(z), _by_, degDMS(x, prec=6))
678 raise Error(lon=degDMS(lon), txt=t)
680 B = _toBand(lat, strict=strict, Error=Error)
681 if B == _X_: # and 0 <= lon < 42: z = int(lon + 183) // 6 + 1
682 x = _SvalbardXzone.get(z, None)
683 if x: # Svalbard/Spitsbergen archipelago
684 z += 1 if lon >= x else -1
685 elif B == _V_ and z == 31 and lon >= 3:
686 z += 1 # SouthWestern Norway
688 if cmoff: # lon off central meridian
689 lon -= _cmlon(z) # z I{after} Norway/Svalbard
690 return Zone(z), (Band(B) if B else None), Lat(lat), Lon(lon)
693def _to7zBlldfn(latlon, lon, datum, falsed, zone, strict, Error, **name_cmoff):
694 '''(INTERNAL) Determine 7-tuple (zone, band, lat, lon, datum,
695 falsed, name) for methods L{toEtm8} and L{toUtm8}.
696 '''
697 f, name = _xkwds_pop2(name_cmoff, cmoff=falsed) # DEPRECATED
698 lat, lon, d, n = _to4lldn(latlon, lon, datum, name)
699 z, B, lat, lon = _to4zBll(lat, lon, cmoff=f, strict=strict)
700 if zone: # re-zone for ETM/UTM
701 r, _, _ = _to3zBhp(zone, B)
702 if r != z:
703 if not _UTM_ZONE_MIN <= r <= _UTM_ZONE_MAX:
704 raise Error(zone=zone)
705 if f: # re-offset from central meridian
706 lon += _cmlon(z) - _cmlon(r)
707 z = r
708 return z, B, lat, lon, d, f, n
711def utmZoneBand5(lat, lon, cmoff=False, **name):
712 '''Return the UTM zone number, Band letter, hemisphere and
713 (clipped) lat- and longitude for a given location.
715 @arg lat: Latitude in degrees (C{scalar} or C{str}).
716 @arg lon: Longitude in degrees (C{scalar} or C{str}).
717 @kwarg cmoff: If C{True}, offset longitude from the zone's central
718 meridian (C{bool}).
719 @kwarg name: Optional C{B{name}=NN} (C{str}).
721 @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole, lat, lon)}
722 where C{hemipole} is the C{'N'|'S'} UTM hemisphere.
724 @raise RangeError: If B{C{lat}} outside the valid UTM bands or if
725 B{C{lat}} or B{C{lon}} outside the valid range and
726 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
728 @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.
729 '''
730 lat, lon = parseDMS2(lat, lon)
731 z, B, lat, lon = _to4zBll(lat, lon, cmoff=cmoff)
732 return UtmUpsLatLon5Tuple(z, B, _hemi(lat), lat, lon, **name)
734# **) MIT License
735#
736# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
737#
738# Permission is hereby granted, free of charge, to any person obtaining a
739# copy of this software and associated documentation files (the "Software"),
740# to deal in the Software without restriction, including without limitation
741# the rights to use, copy, modify, merge, publish, distribute, sublicense,
742# and/or sell copies of the Software, and to permit persons to whom the
743# Software is furnished to do so, subject to the following conditions:
744#
745# The above copyright notice and this permission notice shall be included
746# in all copies or substantial portions of the Software.
747#
748# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
749# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
750# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
751# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
752# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
753# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
754# OTHER DEALINGS IN THE SOFTWARE.