Coverage for pygeodesy/ups.py: 97%

162 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-10 16:55 -0500

1 

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

3 

4u'''I{Karney}'s Universal Polar Stereographic (UPS) projection. 

5 

6Classes L{Ups} and L{UPSError} and functions L{parseUPS5}, L{toUps8} and L{upsZoneBand5}. 

7 

8A pure Python implementation, partially transcoded from I{Karney}'s C++ class U{PolarStereographic 

9<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1PolarStereographic.html>}. 

10 

11The U{UPS<https://WikiPedia.org/wiki/Universal_polar_stereographic_coordinate_system>} system is used 

12in conjuction with U{UTM<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>} 

13for locations on the polar regions of the earth. UPS covers areas south of 79.5°S and north of 83.5°N, 

14slightly overlapping the UTM range from 80°S to 84°N by 30' at each end. 

15 

16Env variable C{PYGEODESY_UPS_POLES} determines the UPS zones I{at} latitude 90°S and 90°N. By default, 

17the encoding follows I{Karney}'s and U{Appendix B-3 of DMA TM8358.1<https://Web.Archive.org/web/ 

1820161226192038/http://earth-info.nga.mil/GandG/publications/tm8358.1/pdf/TM8358_1.pdf>}, using only 

19zones C{'B'} respectively C{'Z'} and digraph C{'AN'}. If C{PYGEODESY_UPS_POLES} is set to anything 

20other than C{"std"}, zones C{'A'} and C{'Y'} are used for negative, west longitudes I{at} latitude 

2190°S respectively 90°N (for backward compatibility). 

22''' 

23 

24# from pygeodesy.basics import neg as _neg # from .dms 

25from pygeodesy.constants import EPS, EPS0, _EPSmin as _Tol90, \ 

26 isnear90, _0_0, _0_5, _1_0, _2_0 

27from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

28from pygeodesy.dms import degDMS, _neg, parseDMS2 

29from pygeodesy.errors import RangeError, _ValueError, _xkwds_pop2 

30from pygeodesy.fmath import hypot, hypot1, sqrt0 

31from pygeodesy.internals import _getPYGEODESY, _under 

32from pygeodesy.interns import NN, _COMMASPACE_, _inside_, _N_, \ 

33 _pole_, _range_, _S_, _scale0_, \ 

34 _SPACE_, _std_, _to_, _UTM_ 

35# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .named 

36from pygeodesy.named import nameof, _ALL_LAZY, _MODS 

37from pygeodesy.namedTuples import EasNor2Tuple, UtmUps5Tuple, \ 

38 UtmUps8Tuple, UtmUpsLatLon5Tuple 

39from pygeodesy.props import deprecated_method, property_doc_, \ 

40 Property_RO, _update_all 

41# from pygeodesy.streprs import Fmt # from .utmupsBase 

42from pygeodesy.units import Float, Float_, Meter, Lat 

43from pygeodesy.utily import atan1d, atan2, degrees180, sincos2d 

44from pygeodesy.utmupsBase import Fmt, _LLEB, _hemi, _parseUTMUPS5, _to4lldn, \ 

45 _to3zBhp, _to3zll, _UPS_BANDS as _Bands, \ 

46 _UPS_LAT_MAX, _UPS_LAT_MIN, _UPS_ZONE, \ 

47 _UPS_ZONE_STR, UtmUpsBase 

48 

49from math import fabs, radians, tan # as _tan 

50 

51__all__ = _ALL_LAZY.ups 

52__version__ = '24.11.26' 

53 

54_BZ_UPS = _getPYGEODESY('UPS_POLES', _std_) == _std_ 

55_Falsing = Meter(2000e3) # false easting and northing (C{meter}) 

56_K0_UPS = Float(_K0_UPS= 0.994) # scale factor at central meridian 

57_K1_UPS = Float(_K1_UPS=_1_0) # rescale point scale factor 

58 

59 

60class UPSError(_ValueError): 

61 '''Universal Polar Stereographic (UPS) parse or other L{Ups} issue. 

62 ''' 

63 pass 

64 

65 

66class Ups(UtmUpsBase): 

67 '''Universal Polar Stereographic (UPS) coordinate. 

68 ''' 

69# _band = NN # polar band ('A', 'B', 'Y' or 'Z') 

70 _Bands = _Bands # polar Band letters (C{tuple}) 

71 _Error = UPSError # Error class 

72 _pole = NN # UPS projection top/center ('N' or 'S') 

73# _scale = None # point scale factor (C{scalar}) 

74 _scale0 = _K0_UPS # central scale factor (C{scalar}) 

75 

76 def __init__(self, zone=0, pole=_N_, easting=_Falsing, # PYCHOK expected 

77 northing=_Falsing, band=NN, datum=_WGS84, 

78 falsed=True, gamma=None, scale=None, 

79 **name_convergence): 

80 '''New L{Ups} UPS coordinate. 

81 

82 @kwarg zone: UPS zone (C{int}, zero) or zone with/-out I{polar} Band 

83 letter (C{str}, '00', '00A', '00B', '00Y' or '00Z'). 

84 @kwarg pole: Top/center of (stereographic) projection (C{str}, 

85 C{'N[orth]'} or C{'S[outh]'}). 

86 @kwarg easting: Easting, see B{C{falsed}} (C{meter}). 

87 @kwarg northing: Northing, see B{C{falsed}} (C{meter}). 

88 @kwarg band: Optional, I{polar} Band (C{str}, 'A'|'B'|'Y'|'Z'). 

89 @kwarg datum: Optional, this coordinate's datum (L{Datum}, 

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

91 @kwarg falsed: If C{True}, both B{C{easting}} and B{C{northing}} are 

92 falsed (C{bool}). 

93 @kwarg gamma: Optional meridian convergence, bearing off grid North, 

94 clockwise from true North to save (C{degrees}) or C{None}. 

95 @kwarg scale: Optional grid scale factor to save (C{scalar}) or C{None}. 

96 @kwarg name_convergence: Optional C{B{name}=NN} (C{str}) and DEPRECATED 

97 keyword argument C{B{convergence}=None}, use B{C{gamma}}. 

98 

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

100 

101 @raise UPSError: Invalid B{C{zone}}, B{C{pole}}, B{C{easting}}, 

102 B{C{northing}}, B{C{band}}, B{C{convergence}} 

103 or B{C{scale}}. 

104 ''' 

105 if name_convergence: 

106 gamma, name = _xkwds_pop2(name_convergence, convergence=gamma) 

107 if name: 

108 self.name = name 

109 try: 

110 z, B, self._pole = _to3zBhp(zone, band, hemipole=pole) 

111 if z != _UPS_ZONE or (B and (B not in _Bands)): 

112 raise ValueError 

113 except (TypeError, ValueError) as x: 

114 raise UPSError(zone=zone, pole=pole, band=band, cause=x) 

115 

116 UtmUpsBase.__init__(self, easting, northing, band=B, datum=datum, falsed=falsed, 

117 gamma=gamma, scale=scale) 

118 

119 def __eq__(self, other): 

120 return isinstance(other, Ups) and other.zone == self.zone \ 

121 and other.pole == self.pole \ 

122 and other.easting == self.easting \ 

123 and other.northing == self.northing \ 

124 and other.band == self.band \ 

125 and other.datum == self.datum 

126 

127 @property_doc_(''' the I{polar} band.''') 

128 def band(self): 

129 '''Get the I{polar} band (C{'A'|'B'|'Y'|'Z'}). 

130 ''' 

131 if not self._band: 

132 self._toLLEB() 

133 return self._band 

134 

135 @band.setter # PYCHOK setter! 

136 def band(self, band): 

137 '''Set or reset the I{polar} band letter (C{'A'|'B'|'Y'|'Z'}) 

138 or C{None} or C{""} to reset. 

139 

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

141 

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

143 ''' 

144 self._band1(band) 

145 

146 @Property_RO 

147 def falsed2(self): 

148 '''Get the easting and northing falsing (L{EasNor2Tuple}C{(easting, northing)}). 

149 ''' 

150 f = _Falsing if self.falsed else 0 

151 return EasNor2Tuple(f, f) 

152 

153 def parse(self, strUPS, **name): 

154 '''Parse a string to a similar L{Ups} instance. 

155 

156 @arg strUPS: The UPS coordinate (C{str}), see function L{parseUPS5}. 

157 @kwarg name: Optional C{B{name}=NN} (C{str}), overriding this name. 

158 

159 @return: The similar instance (L{Ups}). 

160 

161 @raise UTMError: Invalid B{C{strUPS}}. 

162 

163 @see: Functions L{parseUTM5} and L{pygeodesy.parseUTMUPS5}. 

164 ''' 

165 return parseUPS5(strUPS, datum=self.datum, Ups=self.classof, 

166 name=self._name__(name)) 

167 

168 @deprecated_method 

169 def parseUPS(self, strUPS): # PYCHOK no cover 

170 '''DEPRECATED, use method L{parse}.''' 

171 return self.parse(strUPS) 

172 

173 @Property_RO 

174 def pole(self): 

175 '''Get the top/center of (stereographic) projection (C{'N'|'S'} or C{""}). 

176 ''' 

177 return self._pole 

178 

179 def rescale0(self, lat, scale0=_K0_UPS): 

180 '''Set the central scale factor for this UPS projection. 

181 

182 @arg lat: Northern latitude (C{degrees}). 

183 @arg scale0: UPS k0 scale at B{C{lat}} latitude (C{scalar}). 

184 

185 @raise RangeError: If B{C{lat}} outside the valid range and 

186 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

187 

188 @raise UPSError: Invalid B{C{scale}}. 

189 ''' 

190 s0 = Float_(scale0=scale0, Error=UPSError, low=EPS) # <= 1.003 or 1.0016? 

191 u = toUps8(fabs(Lat(lat)), _0_0, datum=self.datum, Ups=_Ups_K1) 

192 k = s0 / u.scale 

193 if self.scale0 != k: 

194 _update_all(self) 

195 self._band = NN # force re-compute 

196 self._latlon = self._utm = None 

197 self._scale0 = Float(scale0=k) 

198 

199 def toLatLon(self, LatLon=None, unfalse=True, **LatLon_kwds): 

200 '''Convert this UPS coordinate to an (ellipsoidal) geodetic point. 

201 

202 @kwarg LatLon: Optional, ellipsoidal class to return the 

203 geodetic point (C{LatLon}) or C{None}. 

204 @kwarg unfalse: Unfalse B{C{easting}} and B{C{northing}} 

205 if falsed (C{bool}). 

206 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword 

207 arguments, ignored if C{B{LatLon} is None}. 

208 

209 @return: This UPS coordinate (B{C{LatLon}}) or if C{B{LatLon} 

210 is None}, a L{LatLonDatum5Tuple}C{(lat, lon, datum, 

211 gamma, scale)}. 

212 

213 @raise TypeError: If B{C{LatLon}} is not ellipsoidal. 

214 

215 @raise UPSError: Invalid meridional radius or H-value. 

216 ''' 

217 if self._latlon and self._latlon._toLLEB_args == (unfalse,): 

218 return self._latlon5(LatLon) 

219 else: 

220 self._toLLEB(unfalse=unfalse) 

221 return self._latlon5(LatLon, **LatLon_kwds) 

222 

223 def _toLLEB(self, unfalse=True): # PYCHOK signature 

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

225 ''' 

226 E = self.datum.ellipsoid # XXX vs LatLon.datum.ellipsoid 

227 

228 x, y = self.eastingnorthing2(falsed=not unfalse) 

229 

230 r = hypot(x, y) 

231 t = (r * E.es_c / (self.scale0 * E.a * _2_0)) if r > 0 else EPS0 

232 t = E.es_tauf(_0_5 / t - _0_5 * t) 

233 a = atan1d(t) 

234 if self._pole == _N_: 

235 b, g = atan2(x, -y), 1 

236 else: 

237 b, g, a = atan2(x, y), -1, _neg(a) 

238 ll = _LLEB(a, degrees180(b), datum=self._datum, name=self.name) 

239 

240 k = _scale(E, r, t) if r > 0 else self.scale0 

241 self._latlon5args(ll, g * b, k, _toBand, unfalse) 

242 

243 def toRepr(self, prec=0, fmt=Fmt.SQUARE, sep=_COMMASPACE_, B=False, cs=False, **unused): # PYCHOK expected 

244 '''Return a string representation of this UPS coordinate. 

245 

246 Note that UPS coordinates are rounded, not truncated (unlike 

247 MGRS grid references). 

248 

249 @kwarg prec: Number of (decimal) digits, unstripped (C{int}). 

250 @kwarg fmt: Enclosing backets format (C{str}). 

251 @kwarg sep: Optional separator between name:value pairs (C{str}). 

252 @kwarg B: Optionally, include polar band letter (C{bool}). 

253 @kwarg cs: Optionally, include gamma meridian convergence and 

254 point scale factor (C{bool} or non-zero C{int} to 

255 specify the precison like B{C{prec}}). 

256 

257 @return: This UPS as a string with C{00[Band] pole, easting, 

258 northing, [convergence, scale]} as C{"[Z:00[Band], 

259 P:N|S, E:meter, N:meter]"} plus C{", C:DMS, S:float"} 

260 if C{B{cs} is True}, where C{[Band]} is present and 

261 C{'A'|'B'|'Y'|'Z'} only if C{B{B} is True} and 

262 convergence C{DMS} is in I{either} degrees, minutes 

263 I{or} seconds (C{str}). 

264 

265 @note: Pseudo zone zero (C{"00"}) for UPS follows I{Karney}'s U{zone UPS 

266 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}. 

267 ''' 

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

269 

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

271 '''Return a string representation of this UPS coordinate. 

272 

273 Note that UPS coordinates are rounded, not truncated (unlike 

274 MGRS grid references). 

275 

276 @kwarg prec: Number of (decimal) digits, unstripped (C{int}). 

277 @kwarg sep: Optional separator to join (C{str}) or C{None} 

278 to return an unjoined C{tuple} of C{str}s. 

279 @kwarg B: Optionally, include and polar band letter (C{bool}). 

280 @kwarg cs: Optionally, include gamma meridian convergence and 

281 point scale factor (C{bool} or non-zero C{int} to 

282 specify the precison like B{C{prec}}). 

283 

284 @return: This UPS as a string with C{00[Band] pole, easting, 

285 northing, [convergence, scale]} as C{"00[B] N|S 

286 meter meter"} plus C{" DMS float"} if B{C{cs}} is C{True}, 

287 where C{[Band]} is present and C{'A'|'B'|'Y'|'Z'} only 

288 if B{C{B}} is C{True} and convergence C{DMS} is in 

289 I{either} degrees, minutes I{or} seconds (C{str}). 

290 

291 @note: Zone zero (C{"00"}) for UPS follows I{Karney}'s U{zone UPS 

292 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}. 

293 ''' 

294 return self._toStr(self.pole, B, cs, prec, sep) # PYCHOK pole 

295 

296 def toUps(self, pole=NN, **unused): 

297 '''Duplicate this UPS coordinate. 

298 

299 @kwarg pole: Optional top/center of the UPS projection, 

300 (C{str}, 'N[orth]'|'S[outh]'). 

301 

302 @return: A copy of this UPS coordinate (L{Ups}). 

303 

304 @raise UPSError: Invalid B{C{pole}} or attempt to transfer 

305 the projection top/center. 

306 ''' 

307 if self.pole == pole or not pole: 

308 return self.copy() 

309 t = _SPACE_(_pole_, repr(self.pole), _to_, repr(pole)) 

310 raise UPSError('no transfer', txt=t) 

311 

312 def toUtm(self, zone, falsed=True, **unused): 

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

314 

315 @arg zone: The UTM zone (C{int}). 

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

317 

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

319 ''' 

320 u = self._utm 

321 if u is None or u.zone != zone or falsed != bool(u.falsed): 

322 ll = self.toLatLon(LatLon=None, unfalse=True) 

323 utm = _MODS.utm 

324 self._utm = u = utm.toUtm8(ll, Utm=utm.Utm, falsed=falsed, 

325 name=self.name, zone=zone) 

326 return u 

327 

328 @Property_RO 

329 def zone(self): 

330 '''Get the polar pseudo zone (C{0}), like I{Karney}'s U{zone UPS<https:// 

331 GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}. 

332 ''' 

333 return _UPS_ZONE 

334 

335 

336class _Ups_K1(Ups): 

337 '''(INTERNAL) For method L{Ups.rescale0}. 

338 ''' 

339 _scale0 = _K1_UPS 

340 

341 

342def parseUPS5(strUPS, datum=_WGS84, Ups=Ups, falsed=True, **name): 

343 '''Parse a string representing a UPS coordinate, consisting of 

344 C{"[zone][band] pole easting northing"} where B{C{zone}} is 

345 pseudo zone C{"00"|"0"|""} and C{band} is C{'A'|'B'|'Y'|'Z'|''}. 

346 

347 @arg strUPS: A UPS coordinate (C{str}). 

348 @kwarg datum: Optional datum to use (L{Datum}). 

349 @kwarg Ups: Optional class to return the UPS coordinate (L{Ups}) 

350 or C{None}. 

351 @kwarg falsed: If C{True}, both B{C{easting}} and B{C{northing}} 

352 are falsed (C{bool}). 

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

354 

355 @return: The UPS coordinate (B{C{Ups}}) or if C{B{Ups} is None}, a 

356 L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, band)}. 

357 The C{hemipole} is the C{'N'|'S'} pole, the UPS projection 

358 top/center. 

359 

360 @raise UPSError: Invalid B{C{strUPS}}. 

361 ''' 

362 z, p, e, n, B = _parseUTMUPS5(strUPS, _UPS_ZONE_STR, Error=UPSError) 

363 if z != _UPS_ZONE or (B and B not in _Bands): 

364 raise UPSError(strUPS=strUPS, zone=z, band=B) 

365 

366 r = UtmUps5Tuple(z, p, e, n, B, Error=UPSError, **name) if Ups is None \ 

367 else Ups(z, p, e, n, band=B, falsed=falsed, datum=datum, **name) 

368 return r 

369 

370 

371def _scale(E, rho, tau): 

372 # compute the point scale factor, ala Karney 

373 t = hypot1(tau) 

374 return Float(scale=(rho / E.a) * t * sqrt0(E.e21 + E.e2 / t**2)) 

375 

376 

377def _toBand(lat, lon): # see utm._toBand 

378 '''(INTERNAL) Get the I{polar} Band letter for a (lat, lon). 

379 ''' 

380 return _Bands[(0 if lat < 0 else 2) + (0 if -180 < lon < 0 else 1)] 

381 

382 

383def toUps8(latlon, lon=None, datum=None, Ups=Ups, pole=NN, 

384 falsed=True, strict=True, **name): 

385 '''Convert a lat-/longitude point to a UPS coordinate. 

386 

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

388 C{LatLon} point. 

389 @kwarg lon: Optional longitude (C{degrees}) or C{None} if 

390 B{C{latlon}} is a C{LatLon}. 

391 @kwarg datum: Optional datum for this UPS coordinate, overriding 

392 B{C{latlon}}'s datum (C{Datum}, L{Ellipsoid}, 

393 L{Ellipsoid2} or L{a_f2Tuple}). 

394 @kwarg Ups: Optional class to return the UPS coordinate (L{Ups}) 

395 or C{None}. 

396 @kwarg pole: Optional top/center of (stereographic) projection 

397 (C{str}, C{'N[orth]'} or C{'S[outh]'}). 

398 @kwarg falsed: If C{True}, false both easting and northing (C{bool}). 

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

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

401 

402 @return: The UPS coordinate (B{C{Ups}}) or if C{B{Ups} is None}, a 

403 L{UtmUps8Tuple}C{(zone, hemipole, easting, northing, band, 

404 datum, gamma, scale)} where C{hemipole} is the C{'N'|'S'} 

405 pole, the UPS projection top/center. 

406 

407 @raise RangeError: If B{C{strict}} and B{C{lat}} outside the valid UPS bands 

408 or if B{C{lat}} or B{C{lon}} outside the valid range and 

409 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

410 

411 @raise TypeError: If B{C{latlon}} is not ellipsoidal or if B{C{datum}} is invalid. 

412 

413 @raise ValueError: If B{C{lon}} value is missing or if B{C{latlon}} is invalid. 

414 

415 @see: I{Karney}'s C++ class U{UPS 

416 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UPS.html>}. 

417 ''' 

418 lat, lon, d, name = _to4lldn(latlon, lon, datum, name) 

419 z, B, p, lat, lon = upsZoneBand5(lat, lon, strict=strict) # PYCHOK UtmUpsLatLon5Tuple 

420 

421 d = _ellipsoidal_datum(d, name=name) 

422 E = d.ellipsoid 

423 

424 p = str(pole or p)[:1].upper() 

425 S = p == _S_ # at south[pole] 

426 

427 a = -lat if S else lat 

428 P = isnear90(a, eps90=_Tol90) # at pole 

429 

430 t = tan(radians(a)) 

431 T = E.es_taupf(t) 

432 r = (hypot1(T) - T) if T < 0 else (_0_0 if P else _1_0 / 

433 (hypot1(T) + T)) 

434 

435 k0 = getattr(Ups, _under(_scale0_), _K0_UPS) # Ups is class or None 

436 r *= k0 * E.a * _2_0 / E.es_c 

437 

438 k = k0 if P else _scale(E, r, t) 

439 g = lon # [-180, 180) from .upsZoneBand5 

440 x, y = sincos2d(g) 

441 x *= r 

442 y *= r 

443 if S: 

444 g = _neg(g) 

445 else: 

446 y = _neg(y) 

447 

448 if falsed: 

449 x += _Falsing 

450 y += _Falsing 

451 

452 n = name or nameof(latlon) 

453 if Ups is None: 

454 r = UtmUps8Tuple(z, p, x, y, B, d, g, k, Error=UPSError, name=n) 

455 else: 

456 if z != _UPS_ZONE and not strict: 

457 z = _UPS_ZONE # ignore UTM zone 

458 r = Ups(z, p, x, y, band=B, datum=d, falsed=falsed, 

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

460 if isinstance(latlon, _LLEB) and d is latlon.datum: # see utm._toXtm8 

461 r._latlon5args(latlon, g, k, _toBand, falsed) # XXX weakref(latlon)? 

462 else: 

463 r._hemisphere = _hemi(lat) 

464 if not r._band: 

465 r._band = _toBand(lat, lon) 

466 return r 

467 

468 

469def upsZoneBand5(lat, lon, strict=True, **name): 

470 '''Return the UTM/UPS zone number, I{polar} Band letter, pole and 

471 clipped lat- and longitude for a given location. 

472 

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

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

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

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

477 

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

479 where C{hemipole} is the C{'N'|'S'} pole, the UPS projection 

480 top/center and C{lon} [-180..180). 

481 

482 @note: The C{lon} is set to C{0} if B{C{lat}} is C{-90} or C{90}, see env 

483 variable C{PYGEODESY_UPS_POLES} in module L{ups<pygeodesy.ups>}. 

484 

485 @raise RangeError: If B{C{strict} is True} and B{C{lat}} within the UTM but not 

486 the UPS range or if B{C{lat}} or B{C{lon}} outside the valid 

487 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

488 

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

490 ''' 

491 z, lat, lon = _to3zll(*parseDMS2(lat, lon)) 

492 if _BZ_UPS and lon < 0 and isnear90(fabs(lat), eps90=_Tol90): # DMA TM8358.1 only ... 

493 lon = 0 # ... zones B and Z at 90°S and 90°N, see also GeoConvert 

494 

495 if lat < _UPS_LAT_MIN: # includes 30' overlap 

496 z, B, p = _UPS_ZONE, _toBand(lat, lon), _S_ 

497 

498 elif lat > _UPS_LAT_MAX: # includes 30' overlap 

499 z, B, p = _UPS_ZONE, _toBand(lat, lon), _N_ 

500 

501 elif strict: 

502 r = _range_(_UPS_LAT_MIN, _UPS_LAT_MAX, prec=1) 

503 t = _SPACE_(_inside_, _UTM_, _range_, r) 

504 raise RangeError(lat=degDMS(lat), txt=t) 

505 

506 else: 

507 B, p = NN, _hemi(lat) 

508 return UtmUpsLatLon5Tuple(z, B, p, lat, lon, Error=UPSError, **name) 

509 

510# **) MIT License 

511# 

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

513# 

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

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

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

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

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

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

520# 

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

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

523# 

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

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

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

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

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

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

530# OTHER DEALINGS IN THE SOFTWARE.