Coverage for pygeodesy/etm.py: 92%

410 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-09 11:05 -0400

1 

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

3 

4u'''A pure Python version of I{Karney}'s C{Exact Transverse Mercator} (ETM) projection. 

5 

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. 

9 

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"}. 

15 

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}. 

19 

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. 

23 

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. 

29 

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:: 

34 

35 Lee here Description 

36 

37 x/a xi Northing (unit Earth) 

38 

39 y/a eta Easting (unit Earth) 

40 

41 s/a sigma xi + i * eta 

42 

43 y x Easting 

44 

45 x y Northing 

46 

47 k e Eccentricity 

48 

49 k^2 mu Elliptic function parameter 

50 

51 k'^2 mv Elliptic function complementary parameter 

52 

53 m k Scale 

54 

55 zeta zeta Complex longitude = Mercator = chi in paper 

56 

57 s sigma Complex GK = zeta in paper 

58 

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 

65 

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 

92 

93from math import asinh, degrees, radians, sinh, sqrt 

94 

95__all__ = _ALL_LAZY.etm 

96__version__ = '25.01.12' 

97 

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 

103 

104 

105class ETMError(UTMError): 

106 '''Exact Transverse Mercator (ETM) parse, projection or other 

107 L{Etm} issue or L{ExactTransverseMercator} conversion failure. 

108 ''' 

109 pass 

110 

111 

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. 

116 

117 @note: Conversion of (geodetic) lat- and longitudes to/from L{Etm} 

118 coordinates is 3-4 times slower than to/from L{Utm}. 

119 

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 

125 

126 __init__ = Utm.__init__ 

127 '''New L{Etm} Exact Transverse Mercator coordinate, raising L{ETMError}s. 

128 

129 @see: L{Utm.__init__} for more information. 

130 ''' 

131 

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 

139 

140 @exactTM.setter # PYCHOK setter! 

141 def exactTM(self, exactTM): 

142 '''Set the ETM projection (L{ExactTransverseMercator}). 

143 

144 @raise ETMError: The B{C{exacTM}}'s datum incompatible 

145 with this ETM coordinate's C{datum}. 

146 ''' 

147 _xinstanceof(ExactTransverseMercator, exactTM=exactTM) 

148 

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 

154 

155 def parse(self, strETM, **name): 

156 '''Parse a string to a similar L{Etm} instance. 

157 

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. 

160 

161 @return: The instance (L{Etm}). 

162 

163 @raise ETMError: Invalid B{C{strETM}}. 

164 

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)) 

170 

171 @deprecated_method 

172 def parseETM(self, strETM): # PYCHOK no cover 

173 '''DEPRECATED, use method L{Etm.parse}. 

174 ''' 

175 return self.parse(strETM) 

176 

177 def toLatLon(self, LatLon=None, unfalse=True, **unused): # PYCHOK expected 

178 '''Convert this ETM coordinate to an (ellipsoidal) geodetic point. 

179 

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}). 

183 

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)}. 

186 

187 @raise ETMError: This ETM coordinate's C{exacTM} and this C{datum} are not 

188 compatible or no convergence transforming to lat-/longitude. 

189 

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) 

195 

196 def _toLLEB(self, unfalse=True, **unused): # PYCHOK signature 

197 '''(INTERNAL) Compute (ellipsoidal) lat- and longitude. 

198 ''' 

199 d, xTM = self.datum, self.exactTM 

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)) 

204 

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) 

208 

209 ll = _LLEB(lat, lon, datum=d, name=self.name) # utm._LLEB 

210 self._latlon5args(ll, g, k, _toBand, unfalse, xTM) 

211 

212 def toUtm(self): # PYCHOK signature 

213 '''Copy this ETM to a UTM coordinate. 

214 

215 @return: The UTM coordinate (L{Utm}). 

216 ''' 

217 return self._xcopy2(Utm) 

218 

219 

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} 

237 

238 def __init__(self, datum=_WGS84, lon0=0, k0=_K0_UTM, extendp=False, raiser=False, **name): 

239 '''New L{ExactTransverseMercator} projection. 

240 

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}). 

248 

249 @raise ETMError: Near-spherical B{C{datum}} or C{ellipsoid} or invalid B{C{lon0}} 

250 or B{C{k0}}. 

251 

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}}. 

255 

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 

269 

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 

277 

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 

283 

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}). 

287 

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 

293 

294 @Property_RO 

295 def _e(self): 

296 '''(INTERNAL) Get and cache C{_e}. 

297 ''' 

298 return self._E.e 

299 

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 

305 

306 @property_RO 

307 def ellipsoid(self): 

308 '''Get the ellipsoid (L{Ellipsoid}). 

309 ''' 

310 return self._E 

311 

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 

317 

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 

323 

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 

329 

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 

335 

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 

341 

342 a = equatoradius 

343 

344 @Property_RO 

345 def _e_TAYTOL(self): 

346 '''(INTERNAL) Get and cache C{e * TAYTOL}. 

347 ''' 

348 return self._e * _TAYTOL 

349 

350 @Property_RO 

351 def _Eu(self): 

352 '''(INTERNAL) Get and cache C{Elliptic(_mu)}. 

353 ''' 

354 return _MODS.elliptic.Elliptic(self._mu) 

355 

356 @Property_RO 

357 def _Eu_cE(self): 

358 '''(INTERNAL) Get and cache C{_Eu.cE}. 

359 ''' 

360 return self._Eu.cE 

361 

362 def _Eu_2cE_(self, xi): 

363 '''(INTERNAL) Return C{_Eu.cE * 2 - B{xi}}. 

364 ''' 

365 return self._Eu_cE * _2_0 - xi 

366 

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 

372 

373 @Property_RO 

374 def _Eu_cK(self): 

375 '''(INTERNAL) Get and cache C{_Eu.cK}. 

376 ''' 

377 return self._Eu.cK 

378 

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 

384 

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 

390 

391 @Property_RO 

392 def _Ev(self): 

393 '''(INTERNAL) Get and cache C{Elliptic(_mv)}. 

394 ''' 

395 return _MODS.elliptic.Elliptic(self._mv) 

396 

397 @Property_RO 

398 def _Ev_cK(self): 

399 '''(INTERNAL) Get and cache C{_Ev.cK}. 

400 ''' 

401 return self._Ev.cK 

402 

403 @Property_RO 

404 def _Ev_cKE(self): 

405 '''(INTERNAL) Get and cache C{_Ev.cKE}. 

406 ''' 

407 return self._Ev.cKE 

408 

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 

414 

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 

420 

421 @property_RO 

422 def extendp(self): 

423 '''Get the domain (C{bool}), I{extended} or I{standard}. 

424 ''' 

425 return self._extendp 

426 

427 @property_RO 

428 def flattening(self): 

429 '''Get the C{ellipsoid}'s flattening (C{scalar}). 

430 ''' 

431 return self._E.f 

432 

433 f = flattening 

434 

435 def forward(self, lat, lon, lon0=None, jam=_K_2_4, **name): # MCCABE 13 

436 '''Forward projection, from geographic to transverse Mercator. 

437 

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}). 

445 

446 @return: L{Forward4Tuple}C{(easting, northing, gamma, scale)}. 

447 

448 @see: C{void TMExact::Forward(real lon0, real lat, real lon, 

449 real &x, real &y, 

450 real &gamma, real &k)}. 

451 

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 

467 

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) 

478 

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) 

483 

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)) 

494 

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 

511 

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 

519 

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 

525 

526 @k0.setter # PYCHOK setter! 

527 def k0(self, k0): 

528 '''Set the central scale factor (C{float}), aka I{C{scale0}}. 

529 

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 

536 

537 @Property_RO 

538 def _k0_a(self): 

539 '''(INTERNAL) Get and cache C{k0 * equatoradius}. 

540 ''' 

541 return self.k0 * self.equatoradius 

542 

543 @property_doc_(''' the central meridian (C{degrees180}).''') 

544 def lon0(self): 

545 '''Get the central meridian (C{degrees180}). 

546 ''' 

547 return self._lon0 

548 

549 @lon0.setter # PYCHOK setter! 

550 def lon0(self, lon0): 

551 '''Set the central meridian (C{degrees180}). 

552 

553 @raise ETMError: Invalid B{C{lon0}}. 

554 ''' 

555 self._lon0 = _norm180(Degrees(lon0=lon0, Error=ETMError)) 

556 

557 @deprecated_property_RO 

558 def majoradius(self): # PYCHOK no cover 

559 '''DEPRECATED, use property C{equatoradius}.''' 

560 return self.equatoradius 

561 

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 

567 

568 @Property_RO 

569 def _3_mv(self): 

570 '''(INTERNAL) Get and cache C{3 / _mv}. 

571 ''' 

572 return _3_0 / self._mv 

573 

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) 

579 

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. 

582 

583 @return: 2-Tuple C{(u, v)}. 

584 

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 

591 

592 _zeta3 = self._zeta3 

593 _zetaDwd2 = self._zetaDwd2 

594 else: # _sigmaInv2 

595 _zeta3 = self._sigma3 

596 _zetaDwd2 = self._sigmaDwd2 

597 

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) 

615 

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 

622 

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 

628 

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) 

635 

636 def reset(self, lat0, lon0): 

637 '''Set the central parallel and meridian. 

638 

639 @arg lat0: Latitude of the central parallel (C{degrees90}). 

640 @arg lon0: Longitude of the central meridian (C{degrees180}). 

641 

642 @return: 2-Tuple C{(lat0, lon0)} of the previous central 

643 parallel and meridian. 

644 

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 

651 

652 def _resets(self, datum): 

653 '''(INTERNAL) Set the ellipsoid and elliptic moduli. 

654 

655 @arg datum: Ellipsoidal datum (C{Datum}). 

656 

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_)) 

665 

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 

669 

670 self._E = E 

671 self._mu = mu 

672 self._mv = mv 

673 

674 def reverse(self, x, y, lon0=None, jam=_K_2_4, **name): 

675 '''Reverse projection, from Transverse Mercator to geographic. 

676 

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}). 

684 

685 @return: L{Reverse4Tuple}C{(lat, lon, gamma, scale)}. 

686 

687 @see: C{void TMExact::Reverse(real lon0, real x, real y, 

688 real &lat, real &lon, 

689 real &gamma, real &k)} 

690 

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) 

705 

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 

712 

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 

717 

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)) 

729 

730 def _scaled2(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv): 

731 '''(INTERNAL) C{scaled}. 

732 

733 @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2} 

734 from C{._zeta3}. 

735 

736 @return: 2-Tuple C{(convergence, scale)}. 

737 

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 

770 

771 def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv): 

772 '''(INTERNAL) C{sigma}. 

773 

774 @return: 3-Tuple C{(xi, eta, d2)}. 

775 

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)}. 

779 

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 

797 

798 def _sigmaDwd2(self, snu, cnu, dnu, snv, cnv, dnv): 

799 '''(INTERNAL) C{sigmaDwd}. 

800 

801 @return: 2-Tuple C{(du, dv)}. 

802 

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) 

817 

818 def _sigmaInv2(self, xi, eta): 

819 '''(INTERNAL) Invert C{sigma} using Newton's method. 

820 

821 @return: 2-Tuple C{(u, v)}. 

822 

823 @see: C{void TMExact::sigmainv(real xi, real eta, 

824 real &u, real &v)}. 

825 

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 

832 

833 def _sigmaInv04(self, xi, eta): 

834 '''(INTERNAL) Starting point for C{sigmaInv}. 

835 

836 @return: 4-Tuple C{(u, v, trip, Case)}. 

837 

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 

852 

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 

865 

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 

871 

872 return u, v, t, C 

873 

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) 

881 

882 def toStr(self, joined=_COMMASPACE_, **kwds): # PYCHOK signature 

883 '''Return a C{str} representation. 

884 

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 

895 

896 def _zeta3(self, unused, snu, cnu, dnu, snv, cnv, dnv): # _sigma3 signature 

897 '''(INTERNAL) C{zeta}. 

898 

899 @return: 3-Tuple C{(taup, lambda, d2)}. 

900 

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 

925 

926 def _zetaDwd2(self, snu, cnu, dnu, snv, cnv, dnv): 

927 '''(INTERNAL) C{zetaDwd}. 

928 

929 @return: 2-Tuple C{(du, dv)}. 

930 

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) 

947 

948 def _zetaInv2(self, taup, lam): 

949 '''(INTERNAL) Invert C{zeta} using Newton's method. 

950 

951 @return: 2-Tuple C{(u, v)}. 

952 

953 @see: C{void TMExact::zetainv(real taup, real lam, 

954 real &u, real &v)}. 

955 

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 

963 

964 def _zetaInv04(self, psi, lam): 

965 '''(INTERNAL) Starting point for C{zetaInv}. 

966 

967 @return: 4-Tuple C{(u, v, trip, Case)}. 

968 

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 

987 

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 

998 

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 

1009 

1010 def _zetaScaled(self, sncndn6, ll=True): 

1011 '''(INTERNAL) Recompute (T, L) from (u, v) to improve accuracy of Scale. 

1012 

1013 @arg sncndn6: 6-Tuple C{(snu, cnu, dnu, snv, cnv, dnv)}. 

1014 

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) 

1024 

1025_allPropertiesOf_n(22, ExactTransverseMercator, Property_RO) # PYCHOK assert _ROs = ... 

1026del _0_1, _allPropertiesOf_n, EPS, _1_EPS, _EWGS84 

1027 

1028 

1029def _overflow(x): 

1030 '''(INTERNAL) Like C{copysign0(OVERFLOW, B{x})}. 

1031 ''' 

1032 return _copyBit(_OVERFLOW, x) 

1033 

1034 

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"}. 

1038 

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}). 

1046 

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'}. 

1050 

1051 @raise ETMError: Invalid B{C{strUTM}}. 

1052 

1053 @raise TypeError: Invalid or near-spherical B{C{datum}}. 

1054 ''' 

1055 r = _parseUTM5(strUTM, datum, Etm, falsed, Error=ETMError, **name) 

1056 return r 

1057 

1058 

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. 

1062 

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. 

1077 

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. 

1081 

1082 @raise ETMError: No convergence transforming to ETM easting and northing. 

1083 

1084 @raise ETMError: Invalid B{C{zone}} or near-spherical or incompatible B{C{datum}} 

1085 or C{ellipsoid}. 

1086 

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}. 

1089 

1090 @raise TypeError: Invalid or near-spherical B{C{datum}} or B{C{latlon}} not ellipsoidal. 

1091 

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) 

1099 

1100 return _toXtm8(Etm, z, lat, x, y, B, d, g, k, f, 

1101 n, latlon, d.exactTM, Error=ETMError) 

1102 

1103 

1104if __name__ == '__main__': # MCCABE 16 

1105 

1106 def _main(): 

1107 

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 

1112 

1113 def _error(why, _a=NN): 

1114 if _a: 

1115 why = 'option %r %s' % (_a, why) 

1116 _exit(_COLONSPACE_(_usage(*argv), why)) 

1117 

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]')) 

1127 

1128 def _result(t4): 

1129 printf(_COLONSPACE_(tm.classname, fstr(t4, prec=_p, sep=_SPACE_))) 

1130 

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') 

1155 

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) 

1168 

1169 _main() 

1170 

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 

1174 

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 

1178 

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 

1182 

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 

1186 

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 

1190 

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 

1194 

1195# % echo 33.33 44.44 | .../bin/TransverseMercatorProj 

1196# 4276926.114804 4727193.767015 28.375536563148 1.233325101778 

1197 

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.