Coverage for pygeodesy/utmupsBase.py: 98%

268 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

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

3 

4u'''(INTERNAL) Private class C{UtmUpsBase}, functions and constants 

5for modules L{epsg}, L{etm}, L{mgrs}, L{ups} and L{utm}. 

6''' 

7# make sure int/int division yields float quotient, see .basics 

8from __future__ import division as _; del _ # PYCHOK semicolon 

9 

10from pygeodesy.basics import _isin, isint, isscalar, isstr, neg_, \ 

11 _xinstanceof, _xsubclassof 

12from pygeodesy.constants import _float, _0_0, _0_5, _N_90_0, _180_0 

13from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

14from pygeodesy.dms import degDMS, parseDMS2 

15from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB 

16from pygeodesy.errors import _or, ParseError, _parseX, _ValueError, \ 

17 _xattrs, _xkwds, _xkwds_not 

18# from pygeodesy.fsums import Fsum # _MODS 

19# from pygeodesy.internals import _name__, _under # from .named 

20from pygeodesy.interns import NN, _A_, _B_, _COMMA_, _Error_, _gamma_, \ 

21 _n_a_, _not_, _N_, _NS_, _PLUS_, _scale_, \ 

22 _S_, _SPACE_, _Y_, _Z_ 

23from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS 

24from pygeodesy.named import _name__, _NamedBase, _under 

25from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple 

26from pygeodesy.props import deprecated_method, property_doc_, _update_all, \ 

27 deprecated_property_RO, Property_RO, property_RO 

28from pygeodesy.streprs import Fmt, fstr, _fstrENH2, _xzipairs 

29from pygeodesy.units import Band, Easting, Lat, Northing, Phi, Scalar, Zone 

30from pygeodesy.utily import atan1, _Wrap, wrap360 

31 

32from math import cos, degrees, fabs, sin, tan 

33 

34__all__ = _ALL_LAZY.utmupsBase 

35__version__ = '25.04.25' 

36 

37_UPS_BANDS = _A_, _B_, _Y_, _Z_ # UPS polar bands SE, SW, NE, NW 

38# _UTM_BANDS = _MODS.utm._Bands 

39 

40_UTM_LAT_MAX = _float( 84) # PYCHOK for export (C{degrees}) 

41_UTM_LAT_MIN = _float(-80) # PYCHOK for export (C{degrees}) 

42 

43_UPS_LAT_MAX = _UTM_LAT_MAX - _0_5 # PYCHOK includes 30' UTM overlap 

44_UPS_LAT_MIN = _UTM_LAT_MIN + _0_5 # PYCHOK includes 30' UTM overlap 

45 

46_UPS_LATS = {_A_: _N_90_0, _Y_: _UTM_LAT_MAX, # UPS band bottom latitudes, 

47 _B_: _N_90_0, _Z_: _UTM_LAT_MAX} # PYCHOK see .Mgrs.bandLatitude 

48 

49_UTM_ZONE_MAX = 60 # PYCHOK for export 

50_UTM_ZONE_MIN = 1 # PYCHOK for export 

51_UTM_ZONE_OFF_MAX = 60 # PYCHOK max Central meridian offset (C{degrees}) 

52 

53_UPS_ZONE = _UTM_ZONE_MIN - 1 # PYCHOK for export 

54_UPS_ZONE_STR = Fmt.zone(_UPS_ZONE) # PYCHOK for export 

55 

56_UTMUPS_ZONE_INVALID = -4 # PYCHOK for export too 

57_UTMUPS_ZONE_MAX = _UTM_ZONE_MAX # PYCHOK for export too, by .units.py 

58_UTMUPS_ZONE_MIN = _UPS_ZONE # PYCHOK for export too, by .units.py 

59 

60# _MAX_PSEUDO_ZONE = -1 

61# _MIN_PSEUDO_ZONE = -4 

62# _UTMUPS_ZONE_MATCH = -3 

63# _UTMUPS_ZONE_STANDARD = -1 

64# _UTM = -2 

65 

66 

67class UtmUpsBase(_NamedBase): 

68 '''(INTERNAL) Base class for L{Utm} and L{Ups} coordinates. 

69 ''' 

70 _band = NN # latitude band letter ('A..Z') 

71 _Bands = NN # valid Band letters, see L{Utm} and L{Ups} 

72 _datum = _WGS84 # L{Datum} 

73 _easting = _0_0 # Easting, see B{C{falsed}} (C{meter}) 

74 _Error = None # I{Must be overloaded}, see function C{notOverloaded} 

75 _falsed = True # falsed easting and northing (C{bool}) 

76 _gamma = None # meridian conversion (C{degrees}) 

77 _hemisphere = NN # hemisphere ('N' or 'S'), different from UPS pole 

78 _latlon = None # cached toLatLon (C{LatLon} or C{._toLLEB}) 

79 _northing = _0_0 # Northing, see B{C{falsed}} (C{meter}) 

80 _scale = None # grid or point scale factor (C{scalar}) or C{None} 

81# _scale0 = _K0 # central scale factor (C{scalar}) 

82 _ups = None # cached toUps (L{Ups}) 

83 _utm = None # cached toUtm (L{Utm}) 

84 

85 def __init__(self, easting, northing, band=NN, datum=None, falsed=True, 

86 gamma=None, scale=None): 

87 '''(INTERNAL) New L{UtmUpsBase}. 

88 ''' 

89 E = self._Error 

90 if not E: # PYCHOK no cover 

91 self._notOverloaded(callername=_under(_Error_)) 

92 

93 self._easting = Easting(easting, Error=E) 

94 self._northing = Northing(northing, Error=E) 

95 

96 if band: 

97 self._band1(band) 

98 

99 if not _isin(datum, None, self._datum): 

100 self._datum = _ellipsoidal_datum(datum) # raiser=_datum_, name=band 

101 

102 if not falsed: 

103 self._falsed = False 

104 

105 if gamma is not self._gamma: 

106 self._gamma = Scalar(gamma=gamma, Error=E) 

107 if scale is not self._scale: 

108 self._scale = Scalar(scale=scale, Error=E) 

109 

110 def __repr__(self): 

111 return self.toRepr(B=True) 

112 

113 def __str__(self): 

114 return self.toStr() 

115 

116 def _band1(self, band): 

117 '''(INTERNAL) Re/set the latitudinal or polar band. 

118 ''' 

119 if band: 

120 _xinstanceof(str, band=band) 

121# if not self._Bands: # PYCHOK no cover 

122# self._notOverloaded(callername=_under('Bands')) 

123 if band not in self._Bands: 

124 t = _or(*sorted(set(map(repr, self._Bands)))) 

125 raise self._Error(band=band, txt_not_=t) 

126 self._band = band 

127 elif self._band: # reset 

128 self._band = NN 

129 

130 @deprecated_property_RO 

131 def convergence(self): 

132 '''DEPRECATED, use property C{gamma}.''' 

133 return self.gamma 

134 

135 @property_doc_(''' the (ellipsoidal) datum of this coordinate.''') 

136 def datum(self): 

137 '''Get the datum (L{Datum}). 

138 ''' 

139 return self._datum 

140 

141 @datum.setter # PYCHOK setter! 

142 def datum(self, datum): 

143 '''Set the (ellipsoidal) datum L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). 

144 ''' 

145 d = _ellipsoidal_datum(datum) 

146 if self._datum != d: 

147 _update_all(self) 

148 self._datum = d 

149 

150 @Property_RO 

151 def easting(self): 

152 '''Get the easting (C{meter}). 

153 ''' 

154 return self._easting 

155 

156 @Property_RO 

157 def eastingnorthing(self): 

158 '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)}). 

159 ''' 

160 return EasNor2Tuple(self.easting, self.northing) 

161 

162 def eastingnorthing2(self, falsed=True): 

163 '''Return easting and northing, falsed or unfalsed. 

164 

165 @kwarg falsed: If C{True}, return easting and northing falsed, 

166 otherwise unfalsed (C{bool}). 

167 

168 @return: An L{EasNor2Tuple}C{(easting, northing)} in C{meter}. 

169 ''' 

170 e, n = self.falsed2 

171 if self.falsed and not falsed: 

172 e, n = neg_(e, n) 

173 elif falsed and not self.falsed: 

174 pass 

175 else: 

176 e = n = _0_0 

177 return EasNor2Tuple(Easting( e + self.easting, Error=self._Error), 

178 Northing(n + self.northing, Error=self._Error)) 

179 

180 @Property_RO 

181 def _epsg(self): 

182 '''(INTERNAL) Cache for method L{toEpsg}. 

183 ''' 

184 return _MODS.epsg.Epsg(self) 

185 

186 @Property_RO 

187 def falsed(self): 

188 '''Are easting and northing falsed (C{bool})? 

189 ''' 

190 return self._falsed 

191 

192 @Property_RO 

193 def falsed2(self): # PYCHOK no cover 

194 '''I{Must be overloaded}.''' 

195 self._notOverloaded(self) 

196 

197 def _footpoint(self, y, lat0, makris): 

198 '''(INTERNAL) Compute to foot-point latitude in C{radians}. 

199 ''' 

200 if y is None: 

201 _, y = self.eastingnorthing2(falsed=False) 

202 S = _MODS.fsums.Fsum 

203 E = self.datum.ellipsoid 

204 P = S(E.Llat(lat0), y) 

205 if E.isSpherical: 

206 p = P.fover(E.a) 

207 elif makris: 

208 r = P.fover(E.b) 

209 p = fabs(r) 

210 if p: 

211 e2 = E.e22 # E.e22abs? 

212 e4 = E.e4 

213 

214 e1 = S(e2 / 4, -11 / 64 * e4, -1).as_iscalar 

215 e2 = S(e2 / 8, -13 / 128 * e4) .as_iscalar 

216 e4 *= cos(p)**2 / 8 

217 

218 s = sin(p * 2) 

219 p = -p 

220 U = S(e1 * p, e2 * s, e4 * p, (5 / 8 * e4) * s**2) 

221 p = atan1(E.a * tan(float(U)) / E.b) 

222 if r < 0: # copysign(p, y) 

223 p = -p 

224 else: # PyGeodetics 

225 f = E.f 

226 f2 = f**2 

227 f3 = f**3 

228 B0 = S(1, -f / 2, f2 / 16, f3 / 32) * E.a 

229 r = P.fover(B0) 

230 P = S(r, S(3 / 4 * f, 3 / 8 * f2, 21 / 256 * f3) * sin(r * 2), 

231 S( 21 / 64 * f2, 21 / 64 * f3) * sin(r * 4), 

232 151 / 768 * f3 * sin(r * 6)) 

233 p = float(P) 

234 return p 

235 

236 @Property_RO 

237 def gamma(self): 

238 '''Get the meridian convergence (C{degrees}) or C{None} 

239 if not available. 

240 ''' 

241 return self._gamma 

242 

243 @property_RO 

244 def hemisphere(self): 

245 '''Get the hemisphere (C{str}, 'N'|'S'). 

246 ''' 

247 if not self._hemisphere: 

248 self._toLLEB() 

249 return self._hemisphere 

250 

251 def latFootPoint(self, northing=None, lat0=0, makris=False): 

252 '''Compute the foot-point latitude in C{degrees}. 

253 

254 @arg northing: Northing (C{meter}, same units this datum's ellipsoid's axes), 

255 overriding this I{unfalsed} northing. 

256 @kwarg lat0: Geodetic latitude of the meridian's origin (C{degrees}). 

257 @kwarg makris: If C{True}, use C{Makris}' formula, otherwise C{PyGeodetics}'. 

258 

259 @return: Foot-point latitude (C{degrees}). 

260 

261 @see: U{PyGeodetics<https://GitHub.com/paarnes/pygeodetics>}, U{FootpointLatitude 

262 <https://GitHub.com/APRIL-ZJU/clins/blob/master/include/utils/gps_convert_utils.h#L143>}, 

263 U{Makris<https://www.TandFonline.com/doi/abs/10.1179/sre.1982.26.205.345>} and 

264 U{Geomatics' Mercator, page 60<https://Geomatics.CC/legacy-files/mercator.pdf>}. 

265 ''' 

266 return Lat(FootPoint=degrees(self._footpoint(northing, lat0, makris))) 

267 

268 def _latlon5(self, LatLon, **LatLon_kwds): 

269 '''(INTERNAL) Get cached C{._toLLEB} as B{C{LatLon}} instance. 

270 ''' 

271 ll = self._latlon 

272 if LatLon is None: 

273 r = LatLonDatum5Tuple(ll.lat, ll.lon, ll.datum, 

274 ll.gamma, ll.scale, name=ll.name) 

275 else: 

276 _xsubclassof(_LLEB, LatLon=LatLon) 

277 r = LatLon(ll.lat, ll.lon, **_xkwds(LatLon_kwds, datum=ll.datum, name=ll.name)) 

278 r = _xattrs(r, ll, _under(_gamma_), _under(_scale_)) 

279 return r 

280 

281 def _latlon5args(self, ll, g, k, _toBand, unfalse, *other): 

282 '''(INTERNAL) See C{._toLLEB} methods, functions C{ups.toUps8} and C{utm._toXtm8} 

283 ''' 

284 ll._gamma = g 

285 ll._scale = k 

286 ll._toLLEB_args = (unfalse,) + other 

287 if unfalse: 

288 if not self._band: 

289 self._band = _toBand(ll.lat, ll.lon) 

290 if not self._hemisphere: 

291 self._hemisphere = _hemi(ll.lat) 

292 self._latlon = ll 

293 

294 @Property_RO 

295 def _lowerleft(self): # by .ellipsoidalBase._lowerleft 

296 '''Get this UTM or UPS C{un}-centered (L{Utm} or L{Ups}) to its C{lowerleft}. 

297 ''' 

298 return _lowerleft(self, 0) 

299 

300 @Property_RO 

301 def _mgrs(self): 

302 '''(INTERNAL) Cache for method L{toMgrs}. 

303 ''' 

304 return _toMgrs(self) 

305 

306 @Property_RO 

307 def _mgrs_lowerleft(self): 

308 '''(INTERNAL) Cache for method L{toMgrs}, I{un}-centered. 

309 ''' 

310 utmups = self._lowerleft 

311 return self._mgrs if utmups is self else _toMgrs(utmups) 

312 

313 @Property_RO 

314 def northing(self): 

315 '''Get the northing (C{meter}). 

316 ''' 

317 return self._northing 

318 

319 def phiFootPoint(self, northing=None, lat0=0, makris=False): 

320 '''Compute the foot-point latitude in C{radians}. 

321 

322 @return: Foot-point latitude (C{radians}). 

323 

324 @see: Method L{latFootPoint<UtmUpsBase.latFootPoint>} for further details. 

325 ''' 

326 return Phi(FootPoint=self._footpoint(northing, lat0, makris)) 

327 

328 @Property_RO 

329 def scale(self): 

330 '''Get the grid scale (C{float}) or C{None}. 

331 ''' 

332 return self._scale 

333 

334 @Property_RO 

335 def scale0(self): 

336 '''Get the central scale factor (C{float}). 

337 ''' 

338 return self._scale0 

339 

340 @deprecated_method 

341 def to2en(self, falsed=True): # PYCHOK no cover 

342 '''DEPRECATED, use method C{eastingnorthing2}. 

343 

344 @return: An L{EasNor2Tuple}C{(easting, northing)}. 

345 ''' 

346 return self.eastingnorthing2(falsed=falsed) 

347 

348 def toEpsg(self): 

349 '''Determine the B{EPSG (European Petroleum Survey Group)} code. 

350 

351 @return: C{EPSG} code (C{int}). 

352 

353 @raise EPSGError: See L{Epsg}. 

354 ''' 

355 return self._epsg 

356 

357 def _toLLEB(self, **kwds): # PYCHOK no cover 

358 '''(INTERNAL) I{Must be overloaded}.''' 

359 self._notOverloaded(**kwds) 

360 

361 def toMgrs(self, center=False): 

362 '''Convert this UTM/UPS coordinate to an MGRS grid reference. 

363 

364 @kwarg center: If C{True}, I{un}-center this UTM or UPS to 

365 its C{lowerleft} (C{bool}) or by C{B{center} 

366 meter} (C{scalar}). 

367 

368 @return: The MGRS grid reference (L{Mgrs}). 

369 

370 @see: Function L{pygeodesy.toMgrs} in module L{mgrs} for more details. 

371 

372 @note: If not specified, the I{latitudinal} C{band} is computed from 

373 the (geodetic) latitude and the C{datum}. 

374 ''' 

375 return self._mgrs_lowerleft if center is True else ( 

376 _toMgrs(_lowerleft(self, center)) if center else 

377 self._mgrs) # PYCHOK indent 

378 

379 def _toRepr(self, fmt, B, cs, prec, sep): # PYCHOK expected 

380 '''(INTERNAL) Return a representation for this ETM/UTM/UPS coordinate. 

381 ''' 

382 t = self.toStr(prec=prec, sep=None, B=B, cs=cs) # hemipole 

383 T = 'ZHENCS'[:len(t)] 

384 return _xzipairs(T, t, sep=sep, fmt=fmt) 

385 

386 def _toStr(self, hemipole, B, cs, prec, sep): 

387 '''(INTERNAL) Return a string for this ETM/UTM/UPS coordinate. 

388 ''' 

389 z = NN(Fmt.zone(self.zone), (self.band if B else NN)) # PYCHOK band 

390 t = (z, hemipole) + _fstrENH2(self, prec, None)[0] 

391 if cs: 

392 prec = cs if isint(cs) else 8 # for backward compatibility 

393 t += (_n_a_ if self.gamma is None else 

394 degDMS(self.gamma, prec=prec, pos=_PLUS_), 

395 _n_a_ if self.scale is None else 

396 fstr(self.scale, prec=prec)) 

397 return t if sep is None else sep.join(t) 

398 

399 

400def _hemi(lat, N=0): # in .ups, .utm 

401 '''Return the hemisphere letter. 

402 

403 @arg lat: Latitude (C{degrees} or C{radians}). 

404 @kwarg N: Minimal North latitude, C{0} or C{_N_}. 

405 

406 @return: C{'N'|'S'} for north-/southern hemisphere. 

407 ''' 

408 return _S_ if lat < N else _N_ 

409 

410 

411def _lowerleft(utmups, center): # in .ellipsoidalBase._lowerleft 

412 '''(INTERNAL) I{Un}-center a B{C{utmups}} to its C{lowerleft} by 

413 C{B{center} meter} or by a I{guess} if B{C{center}} is C{0}. 

414 ''' 

415 if center: 

416 e = n = -center 

417 else: 

418 for c in (50, 500, 5000): 

419 t = c * 2 

420 e = int(utmups.easting % t) 

421 n = int(utmups.northing % t) 

422 if (e == c and _isin(n, c, c - 1)) or \ 

423 (n == c and _isin(e, c, c - 1)): 

424 break 

425 else: 

426 return utmups # unchanged 

427 

428 r = _xkwds_not(None, datum=utmups.datum, 

429 gamma=utmups.gamma, 

430 scale=utmups.scale, name=utmups.name) 

431 return utmups.classof(utmups.zone, utmups.hemisphere, 

432 utmups.easting - e, utmups.northing - n, 

433 band=utmups.band, falsed=utmups.falsed, **r) 

434 

435 

436def _parseUTMUPS5(strUTMUPS, UPS, Error=ParseError, band=NN, sep=_COMMA_): 

437 '''(INTERNAL) Parse a string representing a UTM or UPS coordinate 

438 consisting of C{"zone[band] hemisphere/pole easting northing"}. 

439 

440 @arg strUTMUPS: A UTM or UPS coordinate (C{str}). 

441 @kwarg band: Optional, default Band letter (C{str}). 

442 @kwarg sep: Optional, separator to split (","). 

443 

444 @return: 5-Tuple (C{zone, hemisphere/pole, easting, northing, 

445 band}). 

446 

447 @raise ParseError: Invalid B{C{strUTMUPS}}. 

448 ''' 

449 def _UTMUPS5(strUTMUPS, UPS, band, sep): 

450 u = strUTMUPS.lstrip() 

451 if UPS and not u.startswith(_UPS_ZONE_STR): 

452 raise ValueError(_not_(_UPS_ZONE_STR)) 

453 

454 u = u.replace(sep, _SPACE_).strip().split() 

455 if len(u) < 4: 

456 raise ValueError(_not_(sep)) 

457 

458 z, h = u[:2] 

459 if h[:1].upper() not in _NS_: 

460 raise ValueError(_SPACE_(h, _not_(_NS_))) 

461 

462 if z.isdigit(): 

463 z, B = int(z), band 

464 else: 

465 for i in range(len(z)): 

466 if not z[i].isdigit(): 

467 # int('') raises ValueError 

468 z, B = int(z[:i]), z[i:] 

469 break 

470 else: 

471 raise ValueError(z) 

472 

473 e, n = map(float, u[2:4]) 

474 return z, h.upper(), e, n, B.upper() 

475 

476 return _parseX(_UTMUPS5, strUTMUPS, UPS, band, sep, 

477 strUTMUPS=strUTMUPS, Error=Error) 

478 

479 

480def _to4lldn(latlon, lon, datum, name, wrap=False): 

481 '''(INTERNAL) Return 4-tuple (C{lat, lon, datum, name}). 

482 ''' 

483 try: 

484 # if lon is not None: 

485 # raise AttributeError 

486 lat, lon = float(latlon.lat), float(latlon.lon) 

487 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon) 

488 if wrap: 

489 _Wrap.latlon(lat, lon) 

490 d = datum or latlon.datum 

491 except AttributeError: # TypeError 

492 lat, lon = _Wrap.latlonDMS2(latlon, lon) if wrap else \ 

493 parseDMS2(latlon, lon) # clipped 

494 d = datum or _WGS84 

495 return lat, lon, d, _name__(name, _or_nameof=latlon) 

496 

497 

498def _toMgrs(utmups): 

499 '''(INTERNAL) Convert a L{Utm} or L{Ups} to an L{Mgrs} instance. 

500 ''' 

501 return _MODS.mgrs.toMgrs(utmups, datum=utmups.datum, name=utmups.name) 

502 

503 

504def _to3zBhp(zone, band, hemipole=NN, Error=_ValueError): # in .epsg, .ups, .utm, .utmups 

505 '''Parse UTM/UPS zone, Band letter and hemisphere/pole letter. 

506 

507 @arg zone: Zone with/-out Band (C{scalar} or C{str}). 

508 @kwarg band: Optional I{longitudinal/polar} Band letter (C{str}). 

509 @kwarg hemipole: Optional hemisphere/pole letter (C{str}). 

510 @kwarg Error: Optional error to raise, overriding the default 

511 C{ValueError}. 

512 

513 @return: 3-Tuple (C{zone, Band, hemisphere/pole}) as (C{int, str, 

514 'N'|'S'}) where C{zone} is C{0} for UPS or C{1..60} for 

515 UTM and C{Band} is C{'A'..'Z'} I{NOT} checked for valid 

516 UTM/UPS bands. 

517 

518 @raise ValueError: Invalid B{C{zone}}, B{C{band}} or B{C{hemipole}}. 

519 ''' 

520 try: 

521 B, z = band, _UTMUPS_ZONE_INVALID 

522 if isscalar(zone): 

523 z = int(zone) 

524 elif zone and isstr(zone): 

525 if zone.isdigit(): 

526 z = int(zone) 

527 elif len(zone) > 1: 

528 B = zone[-1:] 

529 z = int(zone[:-1]) 

530 elif zone.upper() in _UPS_BANDS: # single letter 

531 B = zone 

532 z = _UPS_ZONE 

533 

534 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX: 

535 hp = hemipole[:1].upper() 

536 if hp in _NS_ or not hp: 

537 z = Zone(z) 

538 B = Band(B.upper()) 

539 if B.isalpha(): 

540 return z, B, (hp or _hemi(B, _N_)) 

541 elif not B: 

542 return z, B, hp 

543 

544 raise ValueError() # _invalid_ 

545 except (AttributeError, IndexError, TypeError, ValueError) as x: 

546 raise Error(zone=zone, band=B, hemipole=hemipole, cause=x) 

547 

548 

549def _to3zll(lat, lon): # in .ups, .utm 

550 '''Wrap lat- and longitude and determine UTM zone. 

551 

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

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

554 

555 @return: 3-Tuple (C{zone, lat, lon}) as (C{int}, C{degrees90}, 

556 C{degrees180}) where C{zone} is C{1..60} for UTM. 

557 ''' 

558 x = wrap360(lon + _180_0) # use wrap360 to get ... 

559 z = int(x) // 6 + 1 # ... longitudinal UTM zone [1, 60] and ... 

560 return Zone(z), lat, (x - _180_0) # ... -180 <= lon < 180 

561 

562 

563__all__ += _ALL_DOCS(UtmUpsBase) 

564 

565# **) MIT License 

566# 

567# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

568# 

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

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

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

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

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

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

575# 

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

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

578# 

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

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

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

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

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

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

585# OTHER DEALINGS IN THE SOFTWARE.