Coverage for pygeodesy/etm.py: 92%
410 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'''A pure Python version of I{Karney}'s C{Exact Transverse Mercator} (ETM) projection.
6Classes L{Etm}, L{ETMError} and L{ExactTransverseMercator}, transcoded from I{Karney}'s
7C++ class U{TransverseMercatorExact<https://GeographicLib.SourceForge.io/C++/doc/
8classGeographicLib_1_1TransverseMercatorExact.html>}, abbreviated as C{TMExact} below.
10Class L{ExactTransverseMercator} provides C{Exact Transverse Mercator} projections while
11instances of class L{Etm} represent ETM C{(easting, northing)} locations. See also
12I{Karney}'s utility U{TransverseMercatorProj<https://GeographicLib.SourceForge.io/C++/doc/
13TransverseMercatorProj.1.html>} and use C{"python[3] -m pygeodesy.etm ..."} to compare
14the results, see usage C{"python[3] -m pygeodesy.etm -h"}.
16Following is a copy of I{Karney}'s U{TransverseMercatorExact.hpp
17<https://GeographicLib.SourceForge.io/C++/doc/TransverseMercatorExact_8hpp_source.html>}
18file C{Header}.
20Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2024) and licensed
21under the MIT/X11 License. For more information, see the U{GeographicLib<https://
22GeographicLib.SourceForge.io>} documentation.
24The method entails using the U{Thompson Transverse Mercator<https://WikiPedia.org/
25wiki/Transverse_Mercator_projection>} as an intermediate projection. The projections
26from the intermediate coordinates to C{phi, lam} and C{x, y} are given by elliptic
27functions. The inverse of these projections are found by Newton's method with a
28suitable starting guess.
30The relevant section of L.P. Lee's paper U{Conformal Projections Based On Jacobian
31Elliptic Functions<https://DOI.org/10.3138/X687-1574-4325-WM62>} in part V, pp
3267-101. The C++ implementation and notation closely follow Lee, with the following
33exceptions::
35 Lee here Description
37 x/a xi Northing (unit Earth)
39 y/a eta Easting (unit Earth)
41 s/a sigma xi + i * eta
43 y x Easting
45 x y Northing
47 k e Eccentricity
49 k^2 mu Elliptic function parameter
51 k'^2 mv Elliptic function complementary parameter
53 m k Scale
55 zeta zeta Complex longitude = Mercator = chi in paper
57 s sigma Complex GK = zeta in paper
59Minor alterations have been made in some of Lee's expressions in an attempt to
60control round-off. For example, C{atanh(sin(phi))} is replaced by C{asinh(tan(phi))}
61which maintains accuracy near C{phi = pi/2}. Such changes are noted in the code.
62'''
63# make sure int/int division yields float quotient, see .basics
64from __future__ import division as _; del _ # PYCHOK semicolon
66from pygeodesy.basics import map1, neg, neg_, _xinstanceof
67from pygeodesy.constants import EPS, EPS02, PI_2, PI_4, _K0_UTM, \
68 _1_EPS, _0_0, _0_1, _0_5, _1_0, _2_0, \
69 _3_0, _90_0, isnear0, isnear90
70from pygeodesy.constants import _4_0 # PYCHOK used!
71from pygeodesy.datums import _ellipsoidal_datum, _WGS84, _EWGS84
72# from pygeodesy.ellipsoids import _EWGS84 # from .datums
73# from pygeodesy.elliptic import Elliptic # _MODS
74# from pygeodesy.errors import _incompatible # from .named
75# from pygeodesy.fsums import Fsum # from .fmath
76from pygeodesy.fmath import cbrt, hypot, hypot1, hypot2, Fsum
77from pygeodesy.interns import _COMMASPACE_, _near_, _SPACE_, _spherical_
78from pygeodesy.karney import _K_2_4, _copyBit, _diff182, _fix90, \
79 _norm2, _norm180, _tand, _unsigned2
80# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .named
81from pygeodesy.named import callername, _incompatible, _NamedBase, \
82 _ALL_LAZY, _MODS
83from pygeodesy.namedTuples import Forward4Tuple, Reverse4Tuple
84from pygeodesy.props import deprecated_method, deprecated_property_RO, \
85 Property_RO, property_RO, _update_all, \
86 property_doc_, _allPropertiesOf_n
87from pygeodesy.streprs import Fmt, pairs, unstr
88from pygeodesy.units import Degrees, Scalar_
89from pygeodesy.utily import atan1d, atan2, atan2d, _loneg, sincos2
90from pygeodesy.utm import _cmlon, _LLEB, _parseUTM5, _toBand, _toXtm8, \
91 _to7zBlldfn, Utm, UTMError
93from math import asinh, degrees, radians, sinh, sqrt
95__all__ = _ALL_LAZY.etm
96__version__ = '24.11.24'
98_OVERFLOW = _1_EPS**2 # ~2e+31
99_TAYTOL = pow(EPS, 0.6)
100_TAYTOL2 = _TAYTOL * _2_0
101_TOL_10 = EPS * _0_1
102_TRIPS = 21 # C++ 10
105class ETMError(UTMError):
106 '''Exact Transverse Mercator (ETM) parse, projection or other
107 L{Etm} issue or L{ExactTransverseMercator} conversion failure.
108 '''
109 pass
112class Etm(Utm):
113 '''Exact Transverse Mercator (ETM) coordinate, a sub-class of L{Utm},
114 a Universal Transverse Mercator (UTM) coordinate using the
115 L{ExactTransverseMercator} projection for highest accuracy.
117 @note: Conversion of (geodetic) lat- and longitudes to/from L{Etm}
118 coordinates is 3-4 times slower than to/from L{Utm}.
120 @see: Karney's U{Detailed Description<https://GeographicLib.SourceForge.io/
121 C++/doc/classGeographicLib_1_1TransverseMercatorExact.html#details>}.
122 '''
123 _Error = ETMError # see utm.UTMError
124 _exactTM = None
126 __init__ = Utm.__init__
127 '''New L{Etm} Exact Transverse Mercator coordinate, raising L{ETMError}s.
129 @see: L{Utm.__init__} for more information.
130 '''
132 @property_doc_(''' the ETM projection (L{ExactTransverseMercator}).''')
133 def exactTM(self):
134 '''Get the ETM projection (L{ExactTransverseMercator}).
135 '''
136 if self._exactTM is None:
137 self.exactTM = self.datum.exactTM # ExactTransverseMercator(datum=self.datum)
138 return self._exactTM
140 @exactTM.setter # PYCHOK setter!
141 def exactTM(self, exactTM):
142 '''Set the ETM projection (L{ExactTransverseMercator}).
144 @raise ETMError: The B{C{exacTM}}'s datum incompatible
145 with this ETM coordinate's C{datum}.
146 '''
147 _xinstanceof(ExactTransverseMercator, exactTM=exactTM)
149 E = self.datum.ellipsoid
150 if E != exactTM.ellipsoid: # may be None
151 raise ETMError(repr(exactTM), txt=_incompatible(repr(E)))
152 self._exactTM = exactTM
153 self._scale0 = exactTM.k0
155 def parse(self, strETM, **name):
156 '''Parse a string to a similar L{Etm} instance.
158 @arg strETM: The ETM coordinate (C{str}), see function L{parseETM5}.
159 @kwarg name: Optional C{B{name}=NN} (C{str}), overriding this name.
161 @return: The instance (L{Etm}).
163 @raise ETMError: Invalid B{C{strETM}}.
165 @see: Function L{pygeodesy.parseUPS5}, L{pygeodesy.parseUTM5} and
166 L{pygeodesy.parseUTMUPS5}.
167 '''
168 return parseETM5(strETM, datum=self.datum, Etm=self.classof,
169 name=self._name__(name))
171 @deprecated_method
172 def parseETM(self, strETM): # PYCHOK no cover
173 '''DEPRECATED, use method L{Etm.parse}.
174 '''
175 return self.parse(strETM)
177 def toLatLon(self, LatLon=None, unfalse=True, **unused): # PYCHOK expected
178 '''Convert this ETM coordinate to an (ellipsoidal) geodetic point.
180 @kwarg LatLon: Optional, ellipsoidal class to return the geodetic point
181 (C{LatLon}) or C{None}.
182 @kwarg unfalse: Unfalse B{C{easting}} and B{C{northing}} if C{falsed} (C{bool}).
184 @return: This ETM coordinate as (B{C{LatLon}}) or if C{B{LatLon} is None},
185 a L{LatLonDatum5Tuple}C{(lat, lon, datum, gamma, scale)}.
187 @raise ETMError: This ETM coordinate's C{exacTM} and this C{datum} are not
188 compatible or no convergence transforming to lat-/longitude.
190 @raise TypeError: Invalid or non-ellipsoidal B{C{LatLon}}.
191 '''
192 if not self._latlon or self._latlon._toLLEB_args != (unfalse, self.exactTM):
193 self._toLLEB(unfalse=unfalse)
194 return self._latlon5(LatLon)
196 def _toLLEB(self, unfalse=True, **unused): # PYCHOK signature
197 '''(INTERNAL) Compute (ellipsoidal) lat- and longitude.
198 '''
199 xTM, d = self.exactTM, self.datum
200 # double check that this and exactTM's ellipsoid match
201 if xTM._E != d.ellipsoid: # PYCHOK no cover
202 t = repr(d.ellipsoid)
203 raise ETMError(repr(xTM._E), txt=_incompatible(t))
205 e, n = self.eastingnorthing2(falsed=not unfalse)
206 lon0 = _cmlon(self.zone) if bool(unfalse) == self.falsed else None
207 lat, lon, g, k = xTM.reverse(e, n, lon0=lon0)
209 ll = _LLEB(lat, lon, datum=d, name=self.name) # utm._LLEB
210 self._latlon5args(ll, g, k, _toBand, unfalse, xTM)
212 def toUtm(self): # PYCHOK signature
213 '''Copy this ETM to a UTM coordinate.
215 @return: The UTM coordinate (L{Utm}).
216 '''
217 return self._xcopy2(Utm)
220class ExactTransverseMercator(_NamedBase):
221 '''Pure Python version of Karney's C++ class U{TransverseMercatorExact
222 <https://GeographicLib.SourceForge.io/C++/doc/TransverseMercatorExact_8cpp_source.html>},
223 a numerically exact transverse Mercator projection, abbreviated as C{TMExact}.
224 '''
225 _datum = _WGS84 # Datum
226 _E = _EWGS84 # Ellipsoid
227 _extendp = False # use extended domain
228# _iteration = None # _NameBase, ._sigmaInv2 and ._zetaInv2
229 _k0 = _K0_UTM # central scale factor
230 _lat0 = _0_0 # central parallel
231 _lon0 = _0_0 # central meridian
232 _mu = _EWGS84.e2 # 1st eccentricity squared
233 _mv = _EWGS84.e21 # 1 - ._mu
234 _raiser = False # throw Error
235 _sigmaC = None # most recent _sigmaInv04 case C{int}
236 _zetaC = None # most recent _zetaInv04 case C{int}
238 def __init__(self, datum=_WGS84, lon0=0, k0=_K0_UTM, extendp=False, raiser=False, **name):
239 '''New L{ExactTransverseMercator} projection.
241 @kwarg datum: The I{non-spherical} datum or ellipsoid (L{Datum},
242 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
243 @kwarg lon0: Central meridian, default (C{degrees180}).
244 @kwarg k0: Central scale factor (C{float}).
245 @kwarg extendp: If C{True}, use the I{extended} domain, I{standard} otherwise (C{bool}).
246 @kwarg raiser: If C{True}, throw an L{ETMError} for convergence failures (C{bool}).
247 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
249 @raise ETMError: Near-spherical B{C{datum}} or C{ellipsoid} or invalid B{C{lon0}}
250 or B{C{k0}}.
252 @see: U{Constructor TransverseMercatorExact<https://GeographicLib.SourceForge.io/
253 C++/doc/classGeographicLib_1_1TransverseMercatorExact.html>} for more details,
254 especially on B{X{extendp}}.
256 @note: For all 255.5K U{TMcoords.dat<https://Zenodo.org/record/32470>} tests (with
257 C{0 <= lat <= 84} and C{0 <= lon}) the maximum error is C{5.2e-08 .forward}
258 (or 52 nano-meter) easting and northing and C{3.8e-13 .reverse} (or 0.38
259 pico-degrees) lat- and longitude (with Python 3.7.3+, 2.7.16+, PyPy6 3.5.3
260 and PyPy6 2.7.13, all in 64-bit on macOS 10.13.6 High Sierra C{x86_64} and
261 12.2 Monterey C{arm64} and C{"arm64_x86_64"}).
262 '''
263 if extendp:
264 self._extendp = True
265 if name:
266 self.name = name
267 if raiser:
268 self.raiser = True
270 TM = ExactTransverseMercator
271 if datum not in (TM._datum, TM._E, None):
272 self.datum = datum # invokes ._resets
273 if lon0 or lon0 != TM._lon0:
274 self.lon0 = lon0
275 if k0 is not TM._k0:
276 self.k0 = k0
278 @property_doc_(''' the datum (L{Datum}).''')
279 def datum(self):
280 '''Get the datum (L{Datum}) or C{None}.
281 '''
282 return self._datum
284 @datum.setter # PYCHOK setter!
285 def datum(self, datum):
286 '''Set the datum and ellipsoid (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
288 @raise ETMError: Near-spherical B{C{datum}} or C{ellipsoid}.
289 '''
290 d = _ellipsoidal_datum(datum, name=self.name) # raiser=_datum_)
291 self._resets(d)
292 self._datum = d
294 @Property_RO
295 def _e(self):
296 '''(INTERNAL) Get and cache C{_e}.
297 '''
298 return self._E.e
300 @Property_RO
301 def _1_e_90(self): # PYCHOK no cover
302 '''(INTERNAL) Get and cache C{(1 - _e) * 90}.
303 '''
304 return (_1_0 - self._e) * _90_0
306 @property_RO
307 def ellipsoid(self):
308 '''Get the ellipsoid (L{Ellipsoid}).
309 '''
310 return self._E
312 @Property_RO
313 def _e_PI_2(self):
314 '''(INTERNAL) Get and cache C{_e * PI / 2}.
315 '''
316 return self._e * PI_2
318 @Property_RO
319 def _e_PI_4_(self):
320 '''(INTERNAL) Get and cache C{-_e * PI / 4}.
321 '''
322 return -self._e * PI_4
324 @Property_RO
325 def _1_e_PI_2(self):
326 '''(INTERNAL) Get and cache C{(1 - _e) * PI / 2}.
327 '''
328 return (_1_0 - self._e) * PI_2
330 @Property_RO
331 def _1_2e_PI_2(self):
332 '''(INTERNAL) Get and cache C{(1 - 2 * _e) * PI / 2}.
333 '''
334 return (_1_0 - self._e * _2_0) * PI_2
336 @property_RO
337 def equatoradius(self):
338 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).
339 '''
340 return self._E.a
342 a = equatoradius
344 @Property_RO
345 def _e_TAYTOL(self):
346 '''(INTERNAL) Get and cache C{e * TAYTOL}.
347 '''
348 return self._e * _TAYTOL
350 @Property_RO
351 def _Eu(self):
352 '''(INTERNAL) Get and cache C{Elliptic(_mu)}.
353 '''
354 return _MODS.elliptic.Elliptic(self._mu)
356 @Property_RO
357 def _Eu_cE(self):
358 '''(INTERNAL) Get and cache C{_Eu.cE}.
359 '''
360 return self._Eu.cE
362 def _Eu_2cE_(self, xi):
363 '''(INTERNAL) Return C{_Eu.cE * 2 - B{xi}}.
364 '''
365 return self._Eu_cE * _2_0 - xi
367 @Property_RO
368 def _Eu_cE_4(self):
369 '''(INTERNAL) Get and cache C{_Eu.cE / 4}.
370 '''
371 return self._Eu_cE / _4_0
373 @Property_RO
374 def _Eu_cK(self):
375 '''(INTERNAL) Get and cache C{_Eu.cK}.
376 '''
377 return self._Eu.cK
379 @Property_RO
380 def _Eu_cK_cE(self):
381 '''(INTERNAL) Get and cache C{_Eu.cK / _Eu.cE}.
382 '''
383 return self._Eu_cK / self._Eu_cE
385 @Property_RO
386 def _Eu_2cK_PI(self):
387 '''(INTERNAL) Get and cache C{_Eu.cK * 2 / PI}.
388 '''
389 return self._Eu_cK / PI_2
391 @Property_RO
392 def _Ev(self):
393 '''(INTERNAL) Get and cache C{Elliptic(_mv)}.
394 '''
395 return _MODS.elliptic.Elliptic(self._mv)
397 @Property_RO
398 def _Ev_cK(self):
399 '''(INTERNAL) Get and cache C{_Ev.cK}.
400 '''
401 return self._Ev.cK
403 @Property_RO
404 def _Ev_cKE(self):
405 '''(INTERNAL) Get and cache C{_Ev.cKE}.
406 '''
407 return self._Ev.cKE
409 @Property_RO
410 def _Ev_3cKE_4(self):
411 '''(INTERNAL) Get and cache C{_Ev.cKE * 3 / 4}.
412 '''
413 return self._Ev_cKE * 0.75 # _0_75
415 @Property_RO
416 def _Ev_5cKE_4(self):
417 '''(INTERNAL) Get and cache C{_Ev.cKE * 5 / 4}.
418 '''
419 return self._Ev_cKE * 1.25 # _1_25
421 @property_RO
422 def extendp(self):
423 '''Get the domain (C{bool}), I{extended} or I{standard}.
424 '''
425 return self._extendp
427 @property_RO
428 def flattening(self):
429 '''Get the C{ellipsoid}'s flattening (C{scalar}).
430 '''
431 return self._E.f
433 f = flattening
435 def forward(self, lat, lon, lon0=None, jam=_K_2_4, **name): # MCCABE 13
436 '''Forward projection, from geographic to transverse Mercator.
438 @arg lat: Latitude of point (C{degrees}).
439 @arg lon: Longitude of point (C{degrees}).
440 @kwarg lon0: Central meridian (C{degrees180}), overriding
441 the default if not C{None}.
442 @kwarg jam: If C{True}, use the C{Jacobi amplitude}
443 otherwise C{Bulirsch}' function (C{bool}).
444 @kwarg name: Optional C{B{name}=NN} (C{str}).
446 @return: L{Forward4Tuple}C{(easting, northing, gamma, scale)}.
448 @see: C{void TMExact::Forward(real lon0, real lat, real lon,
449 real &x, real &y,
450 real &gamma, real &k)}.
452 @raise ETMError: No convergence, thrown iff property
453 C{B{raiser}=True}.
454 '''
455 lat = _fix90(lat - self._lat0)
456 lon, _ = _diff182((self.lon0 if lon0 is None else lon0), lon)
457 if self.extendp:
458 backside = _lat = _lon = False
459 else: # enforce the parity
460 lat, _lat = _unsigned2(lat)
461 lon, _lon = _unsigned2(lon)
462 backside = lon > 90
463 if backside: # PYCHOK no cover
464 lon = _loneg(lon)
465 if lat == 0:
466 _lat = True
468 # u, v = coordinates for the Thompson TM, Lee 54
469 if lat == 90: # isnear90(lat)
470 u = self._Eu_cK
471 v = self._iteration = self._zetaC = 0
472 elif lat == 0 and lon == self._1_e_90: # PYCHOK no cover
473 u = self._iteration = self._zetaC = 0
474 v = self._Ev_cK
475 else: # tau = tan(phi), taup = sinh(psi)
476 tau, lam = _tand(lat), radians(lon)
477 u, v = self._zetaInv2(self._E.es_taupf(tau), lam)
479 sncndn6 = self._sncndn6(u, v, jam=jam)
480 y, x, _ = self._sigma3(v, *sncndn6)
481 g, k = (lon, self.k0) if isnear90(lat) else \
482 self._zetaScaled(sncndn6, ll=False)
484 if backside:
485 y, g = self._Eu_2cE_(y), _loneg(g)
486 y *= self._k0_a
487 x *= self._k0_a
488 if _lat:
489 y, g = neg_(y, g)
490 if _lon:
491 x, g = neg_(x, g)
492 return Forward4Tuple(x, y, g, k, iteration=self._iteration,
493 name=self._name__(name))
495 def _Inv03(self, psi, dlam, _3_mv_e): # (xi, deta, _3_mv)
496 '''(INTERNAL) Partial C{_zetaInv04} or C{_sigmaInv04}, Case 2
497 '''
498 # atan2(dlam-psi, psi+dlam) + 45d gives arg(zeta - zeta0) in
499 # range [-135, 225). Subtracting 180 (multiplier is negative)
500 # makes range [-315, 45). Multiplying by 1/3 (for cube root)
501 # gives range [-105, 15). In particular the range [-90, 180]
502 # in zeta space maps to [-90, 0] in w space as required.
503 a = atan2(dlam - psi, psi + dlam) / _3_0 - PI_4
504 s, c = sincos2(a)
505 h = hypot(psi, dlam)
506 r = cbrt(h * _3_mv_e)
507 u = r * c
508 v = r * s + self._Ev_cK
509 # Error using this guess is about 0.068 * rad^(5/3)
510 return u, v, h
512 @property_RO
513 def iteration(self):
514 '''Get the most recent C{ExactTransverseMercator.forward}
515 or C{ExactTransverseMercator.reverse} iteration number
516 (C{int}) or C{None} if not available/applicable.
517 '''
518 return self._iteration
520 @property_doc_(''' the central scale factor (C{float}).''')
521 def k0(self):
522 '''Get the central scale factor (C{float}), aka I{C{scale0}}.
523 '''
524 return self._k0 # aka scale0
526 @k0.setter # PYCHOK setter!
527 def k0(self, k0):
528 '''Set the central scale factor (C{float}), aka I{C{scale0}}.
530 @raise ETMError: Invalid B{C{k0}}.
531 '''
532 k0 = Scalar_(k0=k0, Error=ETMError, low=_TOL_10, high=_1_0)
533 if self._k0 != k0:
534 ExactTransverseMercator._k0_a._update(self) # redo ._k0_a
535 self._k0 = k0
537 @Property_RO
538 def _k0_a(self):
539 '''(INTERNAL) Get and cache C{k0 * equatoradius}.
540 '''
541 return self.k0 * self.equatoradius
543 @property_doc_(''' the central meridian (C{degrees180}).''')
544 def lon0(self):
545 '''Get the central meridian (C{degrees180}).
546 '''
547 return self._lon0
549 @lon0.setter # PYCHOK setter!
550 def lon0(self, lon0):
551 '''Set the central meridian (C{degrees180}).
553 @raise ETMError: Invalid B{C{lon0}}.
554 '''
555 self._lon0 = _norm180(Degrees(lon0=lon0, Error=ETMError))
557 @deprecated_property_RO
558 def majoradius(self): # PYCHOK no cover
559 '''DEPRECATED, use property C{equatoradius}.'''
560 return self.equatoradius
562 @Property_RO
563 def _1_mu_2(self):
564 '''(INTERNAL) Get and cache C{_mu / 2 + 1}.
565 '''
566 return self._mu * _0_5 + _1_0
568 @Property_RO
569 def _3_mv(self):
570 '''(INTERNAL) Get and cache C{3 / _mv}.
571 '''
572 return _3_0 / self._mv
574 @Property_RO
575 def _3_mv_e(self):
576 '''(INTERNAL) Get and cache C{3 / (_mv * _e)}.
577 '''
578 return _3_0 / (self._mv * self._e)
580 def _Newton2(self, taup, lam, u, v, C, *psi): # or (xi, eta, u, v)
581 '''(INTERNAL) Invert C{_zetaInv2} or C{_sigmaInv2} using Newton's method.
583 @return: 2-Tuple C{(u, v)}.
585 @raise ETMError: No convergence.
586 '''
587 sca1, tol2 = _1_0, _TOL_10
588 if psi: # _zetaInv2
589 sca1 = sca1 / hypot1(taup) # /= chokes PyChecker
590 tol2 = tol2 / max(psi[0], _1_0)**2
592 _zeta3 = self._zeta3
593 _zetaDwd2 = self._zetaDwd2
594 else: # _sigmaInv2
595 _zeta3 = self._sigma3
596 _zetaDwd2 = self._sigmaDwd2
598 d2, r = tol2, self.raiser
599 _U_2 = Fsum(u).fsum2f_
600 _V_2 = Fsum(v).fsum2f_
601 # min iterations 2, max 6 or 7, mean 3.9 or 4.0
602 _hy2 = hypot2
603 for i in range(1, _TRIPS): # GEOGRAPHICLIB_PANIC
604 sncndn6 = self._sncndn6(u, v)
605 du, dv = _zetaDwd2(*sncndn6)
606 T, L, _ = _zeta3(v, *sncndn6)
607 T = (taup - T) * sca1
608 L -= lam
609 u, dU = _U_2(T * du, L * dv)
610 v, dV = _V_2(T * dv, -L * du)
611 if d2 < tol2:
612 r = False
613 break
614 d2 = _hy2(dU, dV)
616 self._iteration = i
617 if r: # PYCHOK no cover
618 n = callername(up=2, underOK=True)
619 t = unstr(n, taup, lam, u, v, C=C)
620 raise ETMError(Fmt.no_convergence(d2, tol2), txt=t)
621 return u, v
623 @property_doc_(''' raise an L{ETMError} for convergence failures (C{bool}).''')
624 def raiser(self):
625 '''Get the error setting (C{bool}).
626 '''
627 return self._raiser
629 @raiser.setter # PYCHOK setter!
630 def raiser(self, raiser):
631 '''Set the error setting (C{bool}), if C{True} throw an L{ETMError}
632 for convergence failures.
633 '''
634 self._raiser = bool(raiser)
636 def reset(self, lat0, lon0):
637 '''Set the central parallel and meridian.
639 @arg lat0: Latitude of the central parallel (C{degrees90}).
640 @arg lon0: Longitude of the central meridian (C{degrees180}).
642 @return: 2-Tuple C{(lat0, lon0)} of the previous central
643 parallel and meridian.
645 @raise ETMError: Invalid B{C{lat0}} or B{C{lon0}}.
646 '''
647 t = self._lat0, self.lon0
648 self._lat0 = _fix90(Degrees(lat0=lat0, Error=ETMError))
649 self. lon0 = lon0 # lon0.setter
650 return t
652 def _resets(self, datum):
653 '''(INTERNAL) Set the ellipsoid and elliptic moduli.
655 @arg datum: Ellipsoidal datum (C{Datum}).
657 @raise ETMError: Near-spherical B{C{datum}} or C{ellipsoid}.
658 '''
659 E = datum.ellipsoid
660 mu = E.e2 # E.eccentricity1st2
661 mv = E.e21 # _1_0 - mu
662 if isnear0(E.e) or isnear0(mu, eps0=EPS02) \
663 or isnear0(mv, eps0=EPS02): # or sqrt(mu) != E.e
664 raise ETMError(ellipsoid=E, txt=_near_(_spherical_))
666 if self._datum or self._E:
667 _i = ExactTransverseMercator.iteration._uname # property_RO
668 _update_all(self, _i, '_sigmaC', '_zetaC', Base=Property_RO) # _under
670 self._E = E
671 self._mu = mu
672 self._mv = mv
674 def reverse(self, x, y, lon0=None, jam=_K_2_4, **name):
675 '''Reverse projection, from Transverse Mercator to geographic.
677 @arg x: Easting of point (C{meters}).
678 @arg y: Northing of point (C{meters}).
679 @kwarg lon0: Optional central meridian (C{degrees180}),
680 overriding the default (C{iff not None}).
681 @kwarg jam: If C{True}, use the C{Jacobi amplitude}
682 otherwise C{Bulirsch}' function (C{bool}).
683 @kwarg name: Optional C{B{name}=NN} (C{str}).
685 @return: L{Reverse4Tuple}C{(lat, lon, gamma, scale)}.
687 @see: C{void TMExact::Reverse(real lon0, real x, real y,
688 real &lat, real &lon,
689 real &gamma, real &k)}
691 @raise ETMError: No convergence, thrown iff property
692 C{B{raiser}=True}.
693 '''
694 # undoes the steps in .forward.
695 xi = y / self._k0_a
696 eta = x / self._k0_a
697 if self.extendp:
698 backside = _lat = _lon = False
699 else: # enforce the parity
700 eta, _lon = _unsigned2(eta)
701 xi, _lat = _unsigned2(xi)
702 backside = xi > self._Eu_cE
703 if backside: # PYCHOK no cover
704 xi = self._Eu_2cE_(xi)
706 # u, v = coordinates for the Thompson TM, Lee 54
707 if xi or eta != self._Ev_cKE:
708 u, v = self._sigmaInv2(xi, eta)
709 else: # PYCHOK no cover
710 u = self._iteration = self._sigmaC = 0
711 v = self._Ev_cK
713 if v or u != self._Eu_cK:
714 g, k, lat, lon = self._zetaScaled(self._sncndn6(u, v, jam=jam))
715 else: # PYCHOK no cover
716 g, k, lat, lon = _0_0, self.k0, _90_0, _0_0
718 if backside: # PYCHOK no cover
719 lon, g = _loneg(lon), _loneg(g)
720 if _lat:
721 lat, g = neg_(lat, g)
722 if _lon:
723 lon, g = neg_(lon, g)
724 lat += self._lat0
725 lon += self._lon0 if lon0 is None else _norm180(lon0)
726 return Reverse4Tuple(lat, _norm180(lon), g, k, # _fix90(lat)
727 iteration=self._iteration,
728 name=self._name__(name))
730 def _scaled2(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv):
731 '''(INTERNAL) C{scaled}.
733 @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2}
734 from C{._zeta3}.
736 @return: 2-Tuple C{(convergence, scale)}.
738 @see: C{void TMExact::Scale(real tau, real /*lam*/,
739 real snu, real cnu, real dnu,
740 real snv, real cnv, real dnv,
741 real &gamma, real &k)}.
742 '''
743 mu, mv = self._mu, self._mv
744 cnudnv = cnu * dnv
745 # Lee 55.12 -- negated for our sign convention. g gives
746 # the bearing (clockwise from true north) of grid north
747 g = atan2d(mv * cnv * snv * snu, cnudnv * dnu)
748 # Lee 55.13 with nu given by Lee 9.1 -- in sqrt change
749 # the numerator from (1 - snu^2 * dnv^2) to (_mv * snv^2
750 # + cnu^2 * dnv^2) to maintain accuracy near phi = 90
751 # and change the denomintor from (dnu^2 + dnv^2 - 1) to
752 # (_mu * cnu^2 + _mv * cnv^2) to maintain accuracy near
753 # phi = 0, lam = 90 * (1 - e). Similarly rewrite sqrt in
754 # 9.1 as _mv + _mu * c^2 instead of 1 - _mu * sin(phi)^2
755 if d2 > 0:
756 # originally: sec2 = 1 + tau**2 # sec(phi)^2
757 # d2 = (mu * cnu**2 + mv * cnv**2)
758 # q2 = (mv * snv**2 + cnudnv**2) / d2
759 # k = sqrt(mv + mu / sec2) * sqrt(sec2) * sqrt(q2)
760 # = sqrt(mv * sec2 + mu) * sqrt(q2)
761 # = sqrt(mv + mv * tau**2 + mu) * sqrt(q2)
762 k, q2 = _0_0, (snv**2 * mv + cnudnv**2)
763 if q2 > 0:
764 k2 = (tau**2 + _1_0) * mv + mu
765 if k2 > 0:
766 k = sqrt(k2) * sqrt(q2 / d2) * self.k0
767 else:
768 k = _OVERFLOW
769 return g, k
771 def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv):
772 '''(INTERNAL) C{sigma}.
774 @return: 3-Tuple C{(xi, eta, d2)}.
776 @see: C{void TMExact::sigma(real /*u*/, real snu, real cnu, real dnu,
777 real v, real snv, real cnv, real dnv,
778 real &xi, real &eta)}.
780 @raise ETMError: No convergence.
781 '''
782 mu = self._mu * cnu
783 mv = self._mv * cnv
784 # Lee 55.4 writing
785 # dnu^2 + dnv^2 - 1 = _mu * cnu^2 + _mv * cnv^2
786 d2 = cnu * mu + cnv * mv
787 mu *= snu * dnu
788 mv *= snv * dnv
789 if d2 > 0: # /= chokes PyChecker
790 mu = mu / d2
791 mv = mv / d2
792 else:
793 mu, mv = map1(_overflow, mu, mv)
794 xi = self._Eu.fE(snu, cnu, dnu) - mu
795 v -= self._Ev.fE(snv, cnv, dnv) - mv
796 return xi, v, d2
798 def _sigmaDwd2(self, snu, cnu, dnu, snv, cnv, dnv):
799 '''(INTERNAL) C{sigmaDwd}.
801 @return: 2-Tuple C{(du, dv)}.
803 @see: C{void TMExact::dwdsigma(real /*u*/, real snu, real cnu, real dnu,
804 real /*v*/, real snv, real cnv, real dnv,
805 real &du, real &dv)}.
806 '''
807 mu = self._mu
808 snuv = snu * snv
809 # Reciprocal of 55.9: dw / ds = dn(w)^2/_mv,
810 # expanding complex dn(w) using A+S 16.21.4
811 d = (cnv**2 + snuv**2 * mu)**2 * self._mv
812 r = cnv * dnu * dnv
813 i = cnu * snuv * mu
814 du = (r + i) * (r - i) / d # (r**2 - i**2) / d
815 dv = r * i * _2_0 / d
816 return du, neg(dv)
818 def _sigmaInv2(self, xi, eta):
819 '''(INTERNAL) Invert C{sigma} using Newton's method.
821 @return: 2-Tuple C{(u, v)}.
823 @see: C{void TMExact::sigmainv(real xi, real eta,
824 real &u, real &v)}.
826 @raise ETMError: No convergence.
827 '''
828 u, v, t, self._sigmaC = self._sigmaInv04(xi, eta)
829 if not t:
830 u, v = self._Newton2(xi, eta, u, v, self._sigmaC)
831 return u, v
833 def _sigmaInv04(self, xi, eta):
834 '''(INTERNAL) Starting point for C{sigmaInv}.
836 @return: 4-Tuple C{(u, v, trip, Case)}.
838 @see: C{bool TMExact::sigmainv0(real xi, real eta,
839 real &u, real &v)}.
840 '''
841 t = False
842 d = eta - self._Ev_cKE
843 if eta > self._Ev_5cKE_4 or (xi < d and xi < -self._Eu_cE_4):
844 # sigma as a simple pole at
845 # w = w0 = Eu.K() + i * Ev.K()
846 # and sigma is approximated by
847 # sigma = (Eu.E() + i * Ev.KE()) + 1 / (w - w0)
848 u, v = _norm2(xi - self._Eu_cE, -d)
849 u += self._Eu_cK
850 v += self._Ev_cK
851 C = 1
853 elif (eta > self._Ev_3cKE_4 and xi < self._Eu_cE_4) or d > 0:
854 # At w = w0 = i * Ev.K(), we have
855 # sigma = sigma0 = i * Ev.KE()
856 # sigma' = sigma'' = 0
857 # including the next term in the Taylor series gives:
858 # sigma = sigma0 - _mv / 3 * (w - w0)^3
859 # When inverting this, we map arg(w - w0) = [-pi/2, -pi/6]
860 # to arg(sigma - sigma0) = [-pi/2, pi/2] mapping arg =
861 # [-pi/2, -pi/6] to [-pi/2, pi/2]
862 u, v, h = self._Inv03(xi, d, self._3_mv)
863 t = h < _TAYTOL2
864 C = 2
866 else: # use w = sigma * Eu.K/Eu.E (correct in limit _e -> 0)
867 u = v = self._Eu_cK_cE
868 u *= xi
869 v *= eta
870 C = 3
872 return u, v, t, C
874 def _sncndn6(self, u, v, **jam):
875 '''(INTERNAL) Get 6-tuple C{(snu, cnu, dnu, snv, cnv, dnv)}.
876 '''
877 # snu, cnu, dnu = self._Eu.sncndn(u)
878 # snv, cnv, dnv = self._Ev.sncndn(v)
879 return self._Eu.sncndn(u, **jam) + \
880 self._Ev.sncndn(v, **jam)
882 def toStr(self, joined=_COMMASPACE_, **kwds): # PYCHOK signature
883 '''Return a C{str} representation.
885 @kwarg joined: Separator to join the attribute strings
886 (C{str} or C{None} or C{NN} for non-joined).
887 @kwarg kwds: Optional, overriding keyword arguments.
888 '''
889 d = dict(datum=self.datum.name, lon0=self.lon0,
890 k0=self.k0, extendp=self.extendp)
891 if self.name:
892 d.update(name=self.name)
893 t = pairs(d, **kwds)
894 return joined.join(t) if joined else t
896 def _zeta3(self, unused, snu, cnu, dnu, snv, cnv, dnv): # _sigma3 signature
897 '''(INTERNAL) C{zeta}.
899 @return: 3-Tuple C{(taup, lambda, d2)}.
901 @see: C{void TMExact::zeta(real /*u*/, real snu, real cnu, real dnu,
902 real /*v*/, real snv, real cnv, real dnv,
903 real &taup, real &lam)}
904 '''
905 e, cnu2, mv = self._e, cnu**2, self._mv
906 # Lee 54.17 but write
907 # atanh(snu * dnv) = asinh(snu * dnv / sqrt(cnu^2 + _mv * snu^2 * snv^2))
908 # atanh(_e * snu / dnv) = asinh(_e * snu / sqrt(_mu * cnu^2 + _mv * cnv^2))
909 d1 = cnu2 + (snu * snv)**2 * mv
910 if d1 > EPS02: # _EPSmin
911 t1 = snu * dnv / sqrt(d1)
912 else: # like atan(overflow) = pi/2
913 t1, d1 = _overflow(snu), 0
914 d2 = cnu2 * self._mu + cnv**2 * mv
915 if d2 > EPS02: # _EPSmin
916 t2 = sinh(e * asinh(e * snu / sqrt(d2)))
917 else:
918 t2, d2 = _overflow(snu), 0
919 # psi = asinh(t1) - asinh(t2)
920 # taup = sinh(psi)
921 taup = t1 * hypot1(t2) - t2 * hypot1(t1)
922 lam = (atan2(dnu * snv, cnu * cnv) -
923 atan2(cnu * snv * e, dnu * cnv) * e) if d1 and d2 else _0_0
924 return taup, lam, d2
926 def _zetaDwd2(self, snu, cnu, dnu, snv, cnv, dnv):
927 '''(INTERNAL) C{zetaDwd}.
929 @return: 2-Tuple C{(du, dv)}.
931 @see: C{void TMExact::dwdzeta(real /*u*/, real snu, real cnu, real dnu,
932 real /*v*/, real snv, real cnv, real dnv,
933 real &du, real &dv)}.
934 '''
935 cnu2 = cnu**2 * self._mu
936 cnv2 = cnv**2
937 dnuv = dnu * dnv
938 dnuv2 = dnuv**2
939 snuv = snu * snv
940 snuv2 = snuv**2 * self._mu
941 # Lee 54.21 but write (see A+S 16.21.4)
942 # (1 - dnu^2 * snv^2) = (cnv^2 + _mu * snu^2 * snv^2)
943 d = (cnv2 + snuv2)**2 * self._mv # max(d, EPS02)?
944 du = (cnv2 - snuv2) * cnu * dnuv / d
945 dv = (cnu2 + dnuv2) * cnv * snuv / d
946 return du, neg(dv)
948 def _zetaInv2(self, taup, lam):
949 '''(INTERNAL) Invert C{zeta} using Newton's method.
951 @return: 2-Tuple C{(u, v)}.
953 @see: C{void TMExact::zetainv(real taup, real lam,
954 real &u, real &v)}.
956 @raise ETMError: No convergence.
957 '''
958 psi = asinh(taup)
959 u, v, t, self._zetaC = self._zetaInv04(psi, lam)
960 if not t:
961 u, v = self._Newton2(taup, lam, u, v, self._zetaC, psi)
962 return u, v
964 def _zetaInv04(self, psi, lam):
965 '''(INTERNAL) Starting point for C{zetaInv}.
967 @return: 4-Tuple C{(u, v, trip, Case)}.
969 @see: C{bool TMExact::zetainv0(real psi, real lam, # radians
970 real &u, real &v)}.
971 '''
972 if lam > self._1_2e_PI_2:
973 d = lam - self._1_e_PI_2
974 if psi < d and psi < self._e_PI_4_: # PYCHOK no cover
975 # N.B. this branch is normally *not* taken because psi < 0
976 # is converted psi > 0 by .forward. There's a log singularity
977 # at w = w0 = Eu.K() + i * Ev.K(), corresponding to the south
978 # pole, where we have, approximately
979 # psi = _e + i * pi/2 - _e * atanh(cos(i * (w - w0)/(1 + _mu/2)))
980 # Inverting this gives:
981 e = self._e # eccentricity
982 s, c = sincos2((PI_2 - lam) / e)
983 h, r = sinh(_1_0 - psi / e), self._1_mu_2
984 u = self._Eu_cK - r * asinh(s / hypot(c, h))
985 v = self._Ev_cK - r * atan2(c, h)
986 return u, v, False, 1
988 elif psi < self._e_PI_2:
989 # At w = w0 = i * Ev.K(), we have
990 # zeta = zeta0 = i * (1 - _e) * pi/2
991 # zeta' = zeta'' = 0
992 # including the next term in the Taylor series gives:
993 # zeta = zeta0 - (_mv * _e) / 3 * (w - w0)^3
994 # When inverting this, we map arg(w - w0) = [-90, 0]
995 # to arg(zeta - zeta0) = [-90, 180]
996 u, v, h = self._Inv03(psi, d, self._3_mv_e)
997 return u, v, (h < self._e_TAYTOL), 2
999 # Use spherical TM, Lee 12.6 -- writing C{atanh(sin(lam) /
1000 # cosh(psi)) = asinh(sin(lam) / hypot(cos(lam), sinh(psi)))}.
1001 # This takes care of the log singularity at C{zeta = Eu.K()},
1002 # corresponding to the north pole.
1003 s, c = sincos2(lam)
1004 h, r = sinh(psi), self._Eu_2cK_PI
1005 # But scale to put 90, 0 on the right place
1006 u = r * atan2(h, c)
1007 v = r * asinh(s / hypot(h, c))
1008 return u, v, False, 3
1010 def _zetaScaled(self, sncndn6, ll=True):
1011 '''(INTERNAL) Recompute (T, L) from (u, v) to improve accuracy of Scale.
1013 @arg sncndn6: 6-Tuple C{(snu, cnu, dnu, snv, cnv, dnv)}.
1015 @return: 2-Tuple C{(g, k)} if not C{B{ll}} else
1016 4-tuple C{(g, k, lat, lon)}.
1017 '''
1018 t, lam, d2 = self._zeta3(None, *sncndn6)
1019 tau = self._E.es_tauf(t)
1020 g_k = self._scaled2(tau, d2, *sncndn6)
1021 if ll:
1022 g_k += atan1d(tau), degrees(lam)
1023 return g_k # or (g, k, lat, lon)
1025_allPropertiesOf_n(22, ExactTransverseMercator, Property_RO) # PYCHOK assert
1026del _0_1, _allPropertiesOf_n, EPS, _1_EPS, _EWGS84
1029def _overflow(x):
1030 '''(INTERNAL) Like C{copysign0(OVERFLOW, B{x})}.
1031 '''
1032 return _copyBit(_OVERFLOW, x)
1035def parseETM5(strUTM, datum=_WGS84, Etm=Etm, falsed=True, **name):
1036 '''Parse a string representing a UTM coordinate, consisting
1037 of C{"zone[band] hemisphere easting northing"}.
1039 @arg strUTM: A UTM coordinate (C{str}).
1040 @kwarg datum: Optional datum to use (L{Datum}, L{Ellipsoid},
1041 L{Ellipsoid2} or L{a_f2Tuple}).
1042 @kwarg Etm: Optional class to return the UTM coordinate
1043 (L{Etm}) or C{None}.
1044 @kwarg falsed: Both easting and northing are C{falsed} (C{bool}).
1045 @kwarg name: Optional B{C{Etm}} C{B{name}=NN} (C{str}).
1047 @return: The UTM coordinate (B{C{Etm}}) or if C{B{Etm} is None}, a
1048 L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}
1049 with C{hemipole} is the hemisphere C{'N'|'S'}.
1051 @raise ETMError: Invalid B{C{strUTM}}.
1053 @raise TypeError: Invalid or near-spherical B{C{datum}}.
1054 '''
1055 r = _parseUTM5(strUTM, datum, Etm, falsed, Error=ETMError, **name)
1056 return r
1059def toEtm8(latlon, lon=None, datum=None, Etm=Etm, falsed=True,
1060 strict=True, zone=None, **name_cmoff):
1061 '''Convert a geodetic lat-/longitude to an ETM coordinate.
1063 @arg latlon: Latitude (C{degrees}) or an (ellipsoidal) geodetic
1064 C{LatLon} instance.
1065 @kwarg lon: Optional longitude (C{degrees}), required if B{C{latlon}}
1066 is C{degrees}, ignored otherwise.
1067 @kwarg datum: Optional datum for the ETM coordinate, overriding
1068 B{C{latlon}}'s datum (L{Datum}, L{Ellipsoid},
1069 L{Ellipsoid2} or L{a_f2Tuple}).
1070 @kwarg Etm: Optional class to return the ETM coordinate (L{Etm}) or C{None}.
1071 @kwarg falsed: False both easting and northing (C{bool}).
1072 @kwarg strict: Restrict B{C{lat}} to UTM ranges (C{bool}).
1073 @kwarg zone: Optional UTM zone to enforce (C{int} or C{str}).
1074 @kwarg name_cmoff: Optional B{C{Etm}} C{B{name}=NN} (C{str}) and DEPRECATED
1075 keyword argument C{B{cmoff}=True} to offset the longitude from
1076 the zone's central meridian (C{bool}), use B{C{falsed}} instead.
1078 @return: The ETM coordinate as B{C{Etm}} or if C{B{Etm} is None} or not B{C{falsed}},
1079 a L{UtmUps8Tuple}C{(zone, hemipole, easting, northing, band, datum, gamma,
1080 scale)}. The C{hemipole} is the C{'N'|'S'} hemisphere.
1082 @raise ETMError: No convergence transforming to ETM easting and northing.
1084 @raise ETMError: Invalid B{C{zone}} or near-spherical or incompatible B{C{datum}}
1085 or C{ellipsoid}.
1087 @raise RangeError: If B{C{lat}} outside the valid UTM bands or if B{C{lat}} or B{C{lon}}
1088 outside the valid range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
1090 @raise TypeError: Invalid or near-spherical B{C{datum}} or B{C{latlon}} not ellipsoidal.
1092 @raise ValueError: The B{C{lon}} value is missing or B{C{latlon}} is invalid.
1093 '''
1094 z, B, lat, lon, d, f, n = _to7zBlldfn(latlon, lon, datum,
1095 falsed, zone, strict,
1096 ETMError, **name_cmoff)
1097 lon0 = _cmlon(z) if f else None
1098 x, y, g, k = d.exactTM.forward(lat, lon, lon0=lon0)
1100 return _toXtm8(Etm, z, lat, x, y, B, d, g, k, f,
1101 n, latlon, d.exactTM, Error=ETMError)
1104if __name__ == '__main__': # MCCABE 16
1106 def _main():
1108 from pygeodesy import fstr, KTransverseMercator
1109 from pygeodesy.interns import _BAR_, _COLONSPACE_, _DASH_, NN
1110 from pygeodesy.internals import printf, _usage
1111 from sys import argv, exit as _exit
1113 def _error(why, _a=NN):
1114 if _a:
1115 why = 'option %r %s' % (_a, why)
1116 _exit(_COLONSPACE_(_usage(*argv), why))
1118 def _help(*why):
1119 if why:
1120 printf(_COLONSPACE_(_usage(*argv), *why))
1121 _exit(_usage(argv[0], '[-s[eries]', _BAR_, '-t]',
1122 '[-p[recision] <ndigits>]',
1123 '[-f[orward] <lat> <lon>', _BAR_,
1124 '-r[everse] <easting> <northing>', _BAR_,
1125 '<lat> <lon>]', _BAR_,
1126 '-h[elp]'))
1128 def _result(t4):
1129 printf(_COLONSPACE_(tm.classname, fstr(t4, prec=_p, sep=_SPACE_)))
1131 # mimick some of I{Karney}'s utility C{TransverseMercatorProj}
1132 _f = _r = _s = _t = False
1133 _p = -6
1134 args = argv[1:]
1135 while args and args[0].startswith(_DASH_):
1136 _a = args.pop(0)
1137 if len(_a) < 2:
1138 _error('invalid', _a)
1139 elif '-forward'.startswith(_a):
1140 _f, _r = True, False
1141 elif '-reverse'.startswith(_a):
1142 _f, _r = False, True
1143 elif '-precision'.startswith(_a) and args:
1144 _p = int(args.pop(0))
1145 elif '-series'.startswith(_a):
1146 _s, _t = True, False
1147 elif _a == '-t':
1148 _s, _t = False, True
1149 elif '-help'.startswith(_a):
1150 _help()
1151 else:
1152 _error('not supported', _a)
1153 if len(args) < 2:
1154 _help('incomplete')
1156 f2 = map1(float, *args[:2])
1157 tm = KTransverseMercator() if _s else \
1158 ExactTransverseMercator(extendp=_t)
1159 if _f:
1160 t = tm.forward(*f2)
1161 elif _r:
1162 t = tm.reverse(*f2)
1163 else:
1164 t = tm.forward(*f2)
1165 _result(t)
1166 t = tm.reverse(t.easting, t.northing)
1167 _result(t)
1169 _main()
1171# % python3.13 -m pygeodesy.etm -p 12 33.33 44.44
1172# ExactTransverseMercator: 4276926.114803905599 4727193.767015309073 28.375536563148 1.233325101778
1173# ExactTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1175# % python3.13 -m pygeodesy.etm -s -p 12 33.33 44.44
1176# KTransverseMercator: 4276926.114803904667 4727193.767015310004 28.375536563148 1.233325101778
1177# KTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1179# % python3.12 -m pygeodesy.etm -p 12 33.33 44.44
1180# ExactTransverseMercator: 4276926.11480390653 4727193.767015309073 28.375536563148 1.233325101778
1181# ExactTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1183# % python3.12 -m pygeodesy.etm -s -p 12 33.33 44.44
1184# KTransverseMercator: 4276926.114803904667 4727193.767015310004 28.375536563148 1.233325101778
1185# KTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1187# % python2 -m pygeodesy.etm -p 12 33.33 44.44
1188# ExactTransverseMercator: 4276926.11480390653 4727193.767015309073 28.375536563148 1.233325101778
1189# ExactTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1191# % python2 -m pygeodesy.etm -s -p 12 33.33 44.44
1192# KTransverseMercator: 4276926.114803904667 4727193.767015310004 28.375536563148 1.233325101778
1193# KTransverseMercator: 33.33 44.44 28.375536563148 1.233325101778
1195# % echo 33.33 44.44 | .../bin/TransverseMercatorProj
1196# 4276926.114804 4727193.767015 28.375536563148 1.233325101778
1198# **) MIT License
1199#
1200# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1201#
1202# Permission is hereby granted, free of charge, to any person obtaining a
1203# copy of this software and associated documentation files (the "Software"),
1204# to deal in the Software without restriction, including without limitation
1205# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1206# and/or sell copies of the Software, and to permit persons to whom the
1207# Software is furnished to do so, subject to the following conditions:
1208#
1209# The above copyright notice and this permission notice shall be included
1210# in all copies or substantial portions of the Software.
1211#
1212# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1213# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1214# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1215# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1216# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1217# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1218# OTHER DEALINGS IN THE SOFTWARE.