Coverage for pygeodesy/utm.py: 97%

261 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-01 11:43 -0400

1 

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

3 

4u'''I{Veness}' Universal Transverse Mercator (UTM) projection. 

5 

6Classes L{Utm} and L{UTMError} and functions L{parseUTM5}, L{toUtm8} and 

7L{utmZoneBand5}. 

8 

9Pure Python implementation of UTM / WGS-84 conversion functions using 

10an ellipsoidal earth model, transcoded from JavaScript originals by 

11I{(C) Chris Veness 2011-2016} 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>}. 

14 

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. 

21 

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

28 

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

35 

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

60 

61from math import asinh, atanh, atan2, cos, cosh, degrees, fabs, \ 

62 radians, sin, sinh, tan, tanh 

63# import operator as _operator # from .fmath 

64 

65__all__ = _ALL_LAZY.utm 

66__version__ = '24.05.31' 

67 

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 

74 

75 

76class UTMError(_ValueError): 

77 '''Universal Transverse Mercator (UTM parse or other L{Utm} issue. 

78 ''' 

79 pass 

80 

81 

82class _Kseries(object): 

83 '''(INTERNAL) Alpha or Beta Krüger series. 

84 

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 

91 

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 

99 

100 self._ab = AB 

101 self._pq = _m2(_x, j2, AB) 

102# assert len(self._ab) == len(self._pq) == n 

103 

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 

108 

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 

114 

115 def xs(self, x0): 

116 '''(INTERNAL) Eta summation (C{float}). 

117 ''' 

118 return fdot3(self._ab, self._cy, self._shx, start=x0) 

119 

120 def ys(self, y0): 

121 '''(INTERNAL) Ksi summation (C{float}). 

122 ''' 

123 return fdot3(self._ab, self._sy, self._chx, start=y0) 

124 

125 def ps(self, p0): 

126 '''(INTERNAL) P summation (C{float}). 

127 ''' 

128 return fdot3(self._pq, self._cy, self._chx, start=p0) 

129 

130 def qs(self, q0): 

131 '''(INTERNAL) Q summation (C{float}). 

132 ''' 

133 return fdot3(self._pq, self._sy, self._shx, start=q0) 

134 

135 

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) 

145 

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. 

150 

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

167 

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

169 

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 

178 

179 self._zone, B, _ = _to3zBlat(zone, band) 

180 

181 h = str(hemisphere)[:1].upper() 

182 if h not in _NS_: 

183 raise self._Error(hemisphere=hemisphere) 

184 self._hemisphere = h 

185 

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) 

197 

198 UtmUpsBase.__init__(self, e, n, band=B, datum=datum, falsed=falsed, 

199 gamma=gamma, scale=scale) 

200 

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 

208 

209 def __repr__(self): 

210 return self.toRepr(B=True) 

211 

212 def __str__(self): 

213 return self.toStr() 

214 

215 def _xcopy2(self, Xtm, **name): 

216 '''(INTERNAL) Make copy as an B{C{Xtm}} instance. 

217 

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

224 

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 

232 

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. 

237 

238 @raise TypeError: Invalid B{C{band}}. 

239 

240 @raise ValueError: Invalid B{C{band}}. 

241 ''' 

242 self._band1(band) 

243 

244 @Property_RO 

245 def _etm(self): 

246 '''(INTERNAL) Cache for method L{toEtm}. 

247 ''' 

248 return self._xcopy2(_MODS.etm.Etm) 

249 

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) 

260 

261 def parse(self, strUTM, **name): 

262 '''Parse a string to a similar L{Utm} instance. 

263 

264 @arg strUTM: The UTM coordinate (C{str}), see function L{parseUTM5}. 

265 @kwarg name: Optional instance name (C{str}), overriding this name. 

266 

267 @return: The similar instance (L{Utm}). 

268 

269 @raise UTMError: Invalid B{C{strUTM}}. 

270 

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

275 

276 @deprecated_method 

277 def parseUTM(self, strUTM): # PYCHOK no cover 

278 '''DEPRECATED, use method L{Utm.parse}.''' 

279 return self.parse(strUTM) 

280 

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 

286 

287 def toEtm(self): 

288 '''Copy this UTM to an ETM coordinate. 

289 

290 @return: The ETM coordinate (L{Etm}). 

291 ''' 

292 return self._etm 

293 

294 def toLatLon(self, LatLon=None, eps=EPS, unfalse=True, **LatLon_kwds): 

295 '''Convert this UTM coordinate to an (ellipsoidal) geodetic point. 

296 

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

304 

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

308 

309 @raise TypeError: Invalid B{C{datum}} or B{C{LatLon}} is not ellipsoidal. 

310 

311 @raise UTMError: Invalid meridional radius or H-value. 

312 

313 ''' 

314 if eps < EPS: 

315 eps = EPS # less doesn't converge 

316 

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) 

322 

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) 

327 

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 

338 

339 sy, cy = sincos2(y) 

340 shx = sinh(x) 

341 H = hypot(shx, cy) 

342 if H < EPS0: 

343 raise self._Error(H=H) 

344 

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) 

357 

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) 

363 

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) 

373 

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. 

376 

377 Note that UTM coordinates are rounded, not truncated (unlike 

378 MGRS grid references). 

379 

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

387 

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 B{C{cs}} is 

390 C{True} (C{str}). 

391 ''' 

392 return self._toRepr(fmt, B, cs, prec, sep) 

393 

394 def toStr(self, prec=0, sep=_SPACE_, B=False, cs=False): # PYCHOK expected 

395 '''Return a string representation of this UTM coordinate. 

396 

397 To distinguish from MGRS grid zone designators, a space is 

398 left between the zone and the hemisphere. 

399 

400 Note that UTM coordinates are rounded, not truncated (unlike 

401 MGRS grid references). 

402 

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

410 

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 B{C{cs}} is C{True} (C{str}). 

415 ''' 

416 return self._toStr(self.hemisphere, B, cs, prec, sep) 

417 

418 def toUps(self, pole=NN, eps=EPS, falsed=True, **unused): 

419 '''Convert this UTM coordinate to a UPS coordinate. 

420 

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

427 

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 

437 

438 def toUtm(self, zone, eps=EPS, falsed=True, **unused): 

439 '''Convert this UTM coordinate to a different zone. 

440 

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

446 

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) 

459 

460 @Property_RO 

461 def zone(self): 

462 '''Get the (longitudinal) zone (C{int}, 1..60). 

463 ''' 

464 return self._zone 

465 

466 

467def _cmlon(zone): 

468 '''(INTERNAL) Central meridian longitude (C{degrees180}). 

469 ''' 

470 return (zone * 6) - 183 

471 

472 

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 

483 

484 

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) 

493 

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) 

496 

497 

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

501 

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 

506 (L{Utm}) or C{None}. 

507 @kwarg falsed: Use C{B{falsed}=True} if both easting and 

508 northing are falsed (C{bool}). 

509 @kwarg name: Optional B{C{Utm}} C{B{name}=NN} (C{str}). 

510 

511 @return: The UTM coordinate (B{C{Utm}}) or if B{C{Utm}} 

512 is C{None}, a L{UtmUps5Tuple}C{(zone, hemipole, 

513 easting, northing, band)}. The C{hemipole} is 

514 the C{'N'|'S'} hemisphere. 

515 

516 @raise UTMError: Invalid B{C{strUTM}}. 

517 

518 @raise TypeError: Invalid B{C{datum}}. 

519 ''' 

520 return _parseUTM5(strUTM, datum, Utm, falsed, **name) 

521 

522 

523def toUtm8(latlon, lon=None, datum=None, Utm=Utm, falsed=True, 

524 strict=True, zone=None, **name_cmoff): 

525 '''Convert a lat-/longitude point to a UTM coordinate. 

526 

527 @arg latlon: Latitude (C{degrees}) or an (ellipsoidal) 

528 geodetic C{LatLon} point. 

529 @kwarg lon: Optional longitude (C{degrees}), required 

530 if B{C{latlon}} is in C{degrees}. 

531 @kwarg datum: Optional datum for this UTM coordinate, 

532 overriding B{C{latlon}}'s datum (L{Datum}, 

533 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). 

534 @kwarg Utm: Optional class to return the UTM coordinate 

535 (L{Utm}) or C{None}. 

536 @kwarg falsed: False both easting and northing (C{bool}). 

537 @kwarg strict: Restrict B{C{lat}} to UTM ranges (C{bool}). 

538 @kwarg zone: Optional UTM zone to enforce (C{int} or C{str}). 

539 @kwarg name_cmoff: Optional B{C{Utm}} C{B{name}=NN} (C{str}) and 

540 DEPRECATED C{B{cmoff}=True} to offset the longitude 

541 from the zone's central meridian (C{bool}), use 

542 C{B{falsed}} instead. 

543 

544 @return: The UTM coordinate (B{C{Utm}}) or if B{C{Utm}} is 

545 C{None} or not B{C{falsed}}, a L{UtmUps8Tuple}C{(zone, 

546 hemipole, easting, northing, band, datum, gamma, 

547 scale)}. The C{hemipole} is the C{'N'|'S'} hemisphere. 

548 

549 @raise RangeError: If B{C{lat}} outside the valid UTM bands or if 

550 B{C{lat}} or B{C{lon}} outside the valid range 

551 and L{pygeodesy.rangerrors} set to C{True}. 

552 

553 @raise TypeError: Invalid B{C{datum}} or B{C{latlon}} not ellipsoidal. 

554 

555 @raise UTMError: Invalid B{C{zone}}. 

556 

557 @raise ValueError: If B{C{lon}} value is missing or B{C{latlon}} 

558 is invalid. 

559 

560 @note: Implements Karney’s method, using 8-th order Krüger series, 

561 giving results accurate to 5 nm (or better) for distances 

562 up to 3,900 Km from the central meridian. 

563 ''' 

564 z, B, lat, lon, d, f, n = _to7zBlldfn(latlon, lon, datum, 

565 falsed, zone, strict, 

566 UTMError, **name_cmoff) 

567 d = _ellipsoidal_datum(d, name=n) 

568 E = d.ellipsoid 

569 

570 a, b = radians(lat), radians(lon) 

571 # easting, northing: Karney 2011 Eq 7-14, 29, 35 

572 sb, cb = sincos2(b) 

573 

574 T = tan(a) 

575 T12 = hypot1(T) 

576 S = sinh(E.e * atanh(E.e * T / T12)) 

577 

578 T_ = T * hypot1(S) - S * T12 

579 H = hypot(T_, cb) 

580 

581 y = atan2(T_, cb) # ξ' ksi 

582 x = asinh(sb / H) # η' eta 

583 

584 A0 = E.A * getattr(Utm, _under(_scale0_), _K0_UTM) # Utm is class or None 

585 

586 K = _Kseries(E.AlphaKs, x, y) # Krüger series 

587 y = K.ys(y) * A0 # ξ 

588 x = K.xs(x) * A0 # η 

589 

590 # convergence: Karney 2011 Eq 23, 24 

591 p_ = K.ps(1) 

592 q_ = K.qs(0) 

593 g = degrees(atan2(T_ * tan(b), hypot1(T_)) + atan2(q_, p_)) 

594 # scale: Karney 2011 Eq 25 

595 k = E.e2s(sin(a)) * T12 / H * (A0 / E.a * hypot(p_, q_)) 

596 

597 return _toXtm8(Utm, z, lat, x, y, 

598 B, d, g, k, f, n, latlon, EPS) 

599 

600 

601def _toBand(lat, *unused, **strict_Error): # see ups._toBand 

602 '''(INTERNAL) Get the I{latitudinal} Band (row) letter. 

603 ''' 

604 if _UTM_LAT_MIN <= lat < _UTM_LAT_MAX: # [-80, 84) like Veness 

605 return _Bands[int(lat - _UTM_LAT_MIN) >> 3] 

606 elif _xkwds_get(strict_Error, strict=True): 

607 r = _range_(_UTM_LAT_MIN, _UTM_LAT_MAX, ropen=True) 

608 t = _SPACE_(_outside_, _UTM_, _range_, r) 

609 E = _xkwds_get(strict_Error, Error=RangeError) 

610 raise E(lat=degDMS(lat), txt=t) 

611 else: 

612 return NN # None 

613 

614 

615def _toXtm8(Xtm, z, lat, x, y, B, d, g, k, f, # PYCHOK 13+ args 

616 n, latlon, eps_other, Error=UTMError): 

617 '''(INTERNAL) Helper for methods L{toEtm8} and L{toUtm8}. 

618 ''' 

619 h = _hemi(lat) 

620 if f: 

621 x, y = _false2(x, y, h) 

622 if Xtm is None: # DEPRECATED 

623 r = UtmUps8Tuple(z, h, x, y, B, d, g, k, Error=Error, name=n) 

624 else: 

625 r = Xtm(z, h, x, y, band=B, datum=d, falsed=f, 

626 gamma=g, scale=k, name=n) 

627 if isinstance(latlon, _LLEB) and d is latlon.datum: # see ups.toUtm8 

628 r._latlon5args(latlon, g, k, _toBand, f, eps_other) # XXX weakref(latlon)? 

629 elif not r._band: 

630 r._band = _toBand(lat) 

631 return r 

632 

633 

634def _to3zBlat(zone, band, Error=UTMError): # in .mgrs 

635 '''(INTERNAL) Check and return zone, Band and band latitude. 

636 

637 @arg zone: Zone number or string. 

638 @arg band: Band letter. 

639 @arg Error: Exception to raise (L{UTMError}). 

640 

641 @return: 3-Tuple (zone, Band, latitude). 

642 ''' 

643 z, B, _ = _to3zBhp(zone, band, Error=Error) 

644 if not (_UTM_ZONE_MIN <= z <= _UTM_ZONE_MAX or 

645 (_UPS_ZONE == z and Error is MGRSError)): 

646 raise Error(zone=zone) 

647 

648 b = None 

649 if B: 

650 if z == _UPS_ZONE: # polar 

651 try: 

652 b = Lat(_UPS_LATS[B], name=_bandLat_) 

653 except KeyError: 

654 raise Error(band=band or B, zone=z) 

655 else: # UTM 

656 b = _Bands.find(B) 

657 if b < 0: 

658 raise Error(band=band or B, zone=z) 

659 b = Int((b << 3) - 80, name=_bandLat_) 

660 B = Band(B) 

661 elif Error is not UTMError: 

662 raise Error(band=band, txt=MISSING) 

663 

664 return Zone(z), B, b 

665 

666 

667def _to4zBll(lat, lon, cmoff=True, strict=True, Error=RangeError): 

668 '''(INTERNAL) Return zone, Band and lat- and (central) longitude in degrees. 

669 

670 @arg lat: Latitude (C{degrees}). 

671 @arg lon: Longitude (C{degrees}). 

672 @kwarg cmoff: If C{True}, offset B{C{lon}} from the zone's central meridian. 

673 @kwarg strict: Restrict B{C{lat}} to the UTM ranges (C{bool}). 

674 @kwarg Error: Error for out of UTM range B{C{lat}}s. 

675 

676 @return: 4-Tuple (zone, Band, lat, lon). 

677 ''' 

678 z, lat, lon = _to3zll(lat, lon) # in .utmupsBase 

679 

680 x = lon - _cmlon(z) # z before Norway/Svalbard 

681 if fabs(x) > _UTM_ZONE_OFF_MAX: 

682 t = _SPACE_(_outside_, _UTM_, _zone_, str(z), _by_, degDMS(x, prec=6)) 

683 raise Error(lon=degDMS(lon), txt=t) 

684 

685 B = _toBand(lat, strict=strict, Error=Error) 

686 if B == _X_: # and 0 <= lon < 42: z = int(lon + 183) // 6 + 1 

687 x = _SvalbardXzone.get(z, None) 

688 if x: # Svalbard/Spitsbergen archipelago 

689 z += 1 if lon >= x else -1 

690 elif B == _V_ and z == 31 and lon >= 3: 

691 z += 1 # SouthWestern Norway 

692 

693 if cmoff: # lon off central meridian 

694 lon -= _cmlon(z) # z I{after} Norway/Svalbard 

695 return Zone(z), (Band(B) if B else None), Lat(lat), Lon(lon) 

696 

697 

698def _to7zBlldfn(latlon, lon, datum, falsed, zone, strict, Error, **name_cmoff): 

699 '''(INTERNAL) Determine 7-tuple (zone, band, lat, lon, datum, 

700 falsed, name) for methods L{toEtm8} and L{toUtm8}. 

701 ''' 

702 f, name = _xkwds_pop2(name_cmoff, cmoff=falsed) # DEPRECATED 

703 lat, lon, d, n = _to4lldn(latlon, lon, datum, name) 

704 z, B, lat, lon = _to4zBll(lat, lon, cmoff=f, strict=strict) 

705 if zone: # re-zone for ETM/UTM 

706 r, _, _ = _to3zBhp(zone, B) 

707 if r != z: 

708 if not _UTM_ZONE_MIN <= r <= _UTM_ZONE_MAX: 

709 raise Error(zone=zone) 

710 if f: # re-offset from central meridian 

711 lon += _cmlon(z) - _cmlon(r) 

712 z = r 

713 return z, B, lat, lon, d, f, n 

714 

715 

716def utmZoneBand5(lat, lon, cmoff=False, **name): 

717 '''Return the UTM zone number, Band letter, hemisphere and 

718 (clipped) lat- and longitude for a given location. 

719 

720 @arg lat: Latitude in degrees (C{scalar} or C{str}). 

721 @arg lon: Longitude in degrees (C{scalar} or C{str}). 

722 @kwarg cmoff: If C{True}, offset longitude from the zone's central 

723 meridian (C{bool}). 

724 @kwarg name: Optional C{B{name}=NN} (C{str}). 

725 

726 @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole, lat, lon)} 

727 where C{hemipole} is the C{'N'|'S'} UTM hemisphere. 

728 

729 @raise RangeError: If B{C{lat}} outside the valid UTM bands or if 

730 B{C{lat}} or B{C{lon}} outside the valid range 

731 and L{pygeodesy.rangerrors} set to C{True}. 

732 

733 @raise ValueError: Invalid B{C{lat}} or B{C{lon}}. 

734 ''' 

735 lat, lon = parseDMS2(lat, lon) 

736 z, B, lat, lon = _to4zBll(lat, lon, cmoff=cmoff) 

737 return UtmUpsLatLon5Tuple(z, B, _hemi(lat), lat, lon, **name) 

738 

739# **) MIT License 

740# 

741# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

742# 

743# Permission is hereby granted, free of charge, to any person obtaining a 

744# copy of this software and associated documentation files (the "Software"), 

745# to deal in the Software without restriction, including without limitation 

746# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

747# and/or sell copies of the Software, and to permit persons to whom the 

748# Software is furnished to do so, subject to the following conditions: 

749# 

750# The above copyright notice and this permission notice shall be included 

751# in all copies or substantial portions of the Software. 

752# 

753# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

754# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

755# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

756# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

757# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

758# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

759# OTHER DEALINGS IN THE SOFTWARE.