Coverage for pygeodesy/utmupsBase.py: 98%

1

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

3

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

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

6'''

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

8 from __future__ import division as _; del _ # noqa: E702 ;

9

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

11 neg_, _xinstanceof, _xsubclassof

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

13 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

14 from pygeodesy.dms import degDMS, parseDMS2

15 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB

16 from 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

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

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

22 _S_, _SPACE_, _Y_, _Z_

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

24 from pygeodesy.named import _name__, _NamedBase, _under

25 from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple

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

27 deprecated_property_RO, Property_RO, property_RO

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

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

30 from pygeodesy.utily import atan1, _Wrap, wrap360

31

32 from math import cos, degrees, fabs, sin, tan # copysign as _copysign

33

34 __all__ = _ALL_LAZY.utmupsBase

35 __version__ = '25.05.12'

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

67 class 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) Return the foot-point latitude in C{radians}.

199 '''

200 F = _MODS.fsums.Fsum

201 E = self.datum.ellipsoid

202 if y is None:

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

204 B = F(E.Llat(lat0), y)

205 if E.isSpherical:

206 r = B.fover(E.a) # == E.b

207

208 elif makris:

209 b = B.fover(E.b)

210 r = fabs(b)

211 if r:

212 e2 = E.e22 # E.e22abs?

213 e4 = E.e4

214

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

216 e2 = F( e2 / 8, -13 / 128 * e4).as_iscalar

217 e4 *= cos(r)**2 / 8

218

219 s = sin(r * 2)

220 r = -r

221 U = F(e1 * r, e2 * s, e4 * r, e4 / 8 * 5 * s**2)

222 r = _copysign(atan1(E.a * tan(float(U)) / E.b), b)

223

224# elif clins: # APRIL-ZJU/clins/include/utils/gps_convert_utils.h

225# n = E.n

226# n2 = n**2

227# n3 = n**3

228# n4 = n**4

229# n5 = n**5

230# A = F(1, n2 / 4, n4 / 64).fmul((E.a + E.b) / 2)

231# r = B.fover(A)

232# R = F(r)

233# if clins: # FootpointLatitude, GPS-Theory-Practice, 1994

234# R += F(3 / 2 * n, -27 / 32 * n3, 269 / 512 * n5).fmul(sin(r * 2))

235# R += F( 21 / 16 * n2, -55 / 32 * n4).fmul(sin(r * 4))

236# R += F( 151 / 96 * n3, -417 / 128 * n5).fmul(sin(r * 6))

237# R += (1097 / 512 * n4) * sin(r * 8)

238# else: # GPS-Theory-Practice, 1992, page 234-235

239# R += F(-3 / 2 * n, 9 / 16 * n3, -3 / 32 * n5).fmul(sin(r * 2))

240# R += F( 15 / 16 * n2, -15 / 32 * n4).fmul(sin(r * 4))

241# R += F( -35 / 48 * n3, 105 / 256 * n4).fmul(sin(r * 6)) # n5?

242# r = float(R)

243

244 else: # PyGeodetics/src/geodetics/footpoint_latitude.py

245 f = E.f

246 f2 = f**2

247 f3 = f**3

248 B0 = F(1, -f / 2, f2 / 16, f3 / 32).fmul(E.a)

249 r = B.fover(B0)

250 R = F(r)

251 R += F(3 / 4 * f, 3 / 8 * f2, 21 / 256 * f3).fmul(sin(r * 2))

252 R += F( 21 / 64 * f2, 21 / 64 * f3).fmul(sin(r * 4))

253 R += (151 / 768 * f3) * sin(r * 6)

254 r = float(R)

255

256 return r

257

258 @Property_RO

259 def gamma(self):

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

261 if not available.

262 '''

263 return self._gamma

264

265 @property_RO

266 def hemisphere(self):

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

268 '''

269 if not self._hemisphere:

270 self._toLLEB()

271 return self._hemisphere

272

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

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

275

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

277 overriding this northing, I{unfalsed}.

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

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

280

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

282

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

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

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

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

287 '''

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

289

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

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

292 '''

293 ll = self._latlon

294 if LatLon is None:

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

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

297 else:

298 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

301 return r

302

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

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

305 '''

306 ll._gamma = g

307 ll._scale = k

308 ll._toLLEB_args = (unfalse,) + other

309 if unfalse:

310 if not self._band:

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

312 if not self._hemisphere:

313 self._hemisphere = _hemi(ll.lat)

314 self._latlon = ll

315

316 @Property_RO

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

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

319 '''

320 return _lowerleft(self, 0)

321

322 @Property_RO

323 def _mgrs(self):

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

325 '''

326 return _toMgrs(self)

327

328 @Property_RO

329 def _mgrs_lowerleft(self):

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

331 '''

332 utmups = self._lowerleft

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

334

335 @Property_RO

336 def northing(self):

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

338 '''

339 return self._northing

340

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

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

343

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

345

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

347 '''

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

349

350 @Property_RO

351 def scale(self):

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

353 '''

354 return self._scale

355

356 @Property_RO

357 def scale0(self):

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

359 '''

360 return self._scale0

361

362 @deprecated_method

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

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

365

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

367 '''

368 return self.eastingnorthing2(falsed=falsed)

369

370 def toEpsg(self):

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

372

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

374

375 @raise EPSGError: See L{Epsg}.

376 '''

377 return self._epsg

378

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

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

381 self._notOverloaded(**kwds)

382

383 def toMgrs(self, center=False):

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

385

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

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

388 meter} (C{scalar}).

389

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

391

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

393

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

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

396 '''

397 return self._mgrs_lowerleft if center is True else (

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

399 self._mgrs) # PYCHOK indent

400

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

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

403 '''

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

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

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

407

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

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

410 '''

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

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

413 if cs:

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

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

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

417 _n_a_ if self.scale is None else

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

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

420

421

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

423 '''Return the hemisphere letter.

424

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

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

427

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

429 '''

430 return _S_ if lat < N else _N_

431

432

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

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

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

436 '''

437 if center:

438 e = n = -center

439 else:

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

441 t = c * 2

442 e = int(utmups.easting % t)

443 n = int(utmups.northing % t)

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

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

446 break

447 else:

448 return utmups # unchanged

449

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

451 gamma=utmups.gamma,

452 scale=utmups.scale, name=utmups.name)

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

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

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

456

457

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

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

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

461

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

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

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

465

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

467 band}).

468

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

470 '''

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

472 u = strUTMUPS.lstrip()

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

474 raise ValueError(_not_(_UPS_ZONE_STR))

475

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

477 if len(u) < 4:

478 raise ValueError(_not_(sep))

479

480 z, h = u[:2]

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

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

483

484 if z.isdigit():

485 z, B = int(z), band

486 else:

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

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

489 # int('') raises ValueError

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

491 break

492 else:

493 raise ValueError(z)

494

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

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

497

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

499 strUTMUPS=strUTMUPS, Error=Error)

500

501

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

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

504 '''

505 try:

506 # if lon is not None:

507 # raise AttributeError

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

509 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon)

510 if wrap:

511 _Wrap.latlon(lat, lon)

512 d = datum or latlon.datum

513 except AttributeError: # TypeError

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

515 parseDMS2(latlon, lon) # clipped

516 d = datum or _WGS84

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

518

519

520 def _toMgrs(utmups):

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

522 '''

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

524

525

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

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

528

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

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

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

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

533 C{ValueError}.

534

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

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

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

538 UTM/UPS bands.

539

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

541 '''

542 try:

543 B, z = band, _UTMUPS_ZONE_INVALID

544 if isscalar(zone):

545 z = int(zone)

546 elif zone and isstr(zone):

547 if zone.isdigit():

548 z = int(zone)

549 elif len(zone) > 1:

550 B = zone[-1:]

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

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

553 B = zone

554 z = _UPS_ZONE

555

556 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX:

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

558 if hp in _NS_ or not hp:

559 z = Zone(z)

560 B = Band(B.upper())

561 if B.isalpha():

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

563 elif not B:

564 return z, B, hp

565

566 raise ValueError() # _invalid_

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

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

569

570

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

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

573

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

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

576

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

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

579 '''

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

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

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

583

584

585 __all__ += _ALL_DOCS(UtmUpsBase)

586

587# **) MIT License

588#

589# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

590#

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

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

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

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

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

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

597#

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

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

600#

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

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

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

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

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

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

607# OTHER DEALINGS IN THE SOFTWARE.