Coverage for pygeodesy/utmupsBase.py: 97%

237 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-25 12:04 -0400

1 

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

3 

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

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

6''' 

7 

8from pygeodesy.basics import isint, isscalar, isstr, neg_, \ 

9 _xinstanceof, _xsubclassof 

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

11from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

12from pygeodesy.dms import degDMS, parseDMS2 

13from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB 

14from pygeodesy.errors import _or, ParseError, _parseX, _UnexpectedError, \ 

15 _ValueError, _xkwds, _xkwds_not, _xkwds_pop2 

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

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

18 _gamma_, _n_a_, _not_, _N_, _NS_, _PLUS_, \ 

19 _scale_, _SPACE_, _Y_, _Z_ 

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

21from pygeodesy.named import _NamedBase, _xnamed, _name__, _under 

22from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple 

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

24 deprecated_property_RO, Property_RO, property_RO 

25from pygeodesy.streprs import Fmt, fstr, _fstrENH2, _xattrs, _xzipairs 

26from pygeodesy.units import Band, Easting, Northing, Scalar, Zone 

27from pygeodesy.utily import _Wrap, wrap360 

28 

29__all__ = _ALL_LAZY.utmupsBase 

30__version__ = '24.05.19' 

31 

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

33# _UTM_BANDS = _MODS.utm._Bands 

34 

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

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

37 

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

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

40 

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

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

43 

44_UTM_ZONE_MAX = 60 # PYCHOK for export 

45_UTM_ZONE_MIN = 1 # PYCHOK for export 

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

47 

48_UPS_ZONE = _UTM_ZONE_MIN - 1 # PYCHOK for export 

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

50 

51_UTMUPS_ZONE_INVALID = -4 # PYCHOK for export too 

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

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

54 

55# _MAX_PSEUDO_ZONE = -1 

56# _MIN_PSEUDO_ZONE = -4 

57# _UTMUPS_ZONE_MATCH = -3 

58# _UTMUPS_ZONE_STANDARD = -1 

59# _UTM = -2 

60 

61 

62def _hemi(lat, N=0): # imported by .ups, .utm 

63 '''Return the hemisphere letter. 

64 

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

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

67 

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

69 ''' 

70 return _NS_[int(lat < N)] 

71 

72 

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

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

75 ''' 

76 try: 

77 # if lon is not None: 

78 # raise AttributeError 

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

80 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon) 

81 if wrap: 

82 _Wrap.latlon(lat, lon) 

83 d = datum or latlon.datum 

84 except AttributeError: 

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

86 parseDMS2(latlon, lon) # clipped 

87 d = datum or _WGS84 

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

89 

90 

91def _to3zBhp(zone, band, hemipole=NN, Error=_ValueError): # imported by .epsg, .ups, .utm, .utmups 

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

93 

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

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

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

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

98 C{ValueError}. 

99 

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

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

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

103 UTM/UPS bands. 

104 

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

106 ''' 

107 try: 

108 B, z = band, _UTMUPS_ZONE_INVALID 

109 if isscalar(zone): 

110 z = int(zone) 

111 elif zone and isstr(zone): 

112 if zone.isdigit(): 

113 z = int(zone) 

114 elif len(zone) > 1: 

115 B = zone[-1:] 

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

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

118 B = zone 

119 z = _UPS_ZONE 

120 

121 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX: 

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

123 if hp in _NS_ or not hp: 

124 z = Zone(z) 

125 B = Band(B.upper()) 

126 if B.isalpha(): 

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

128 elif not B: 

129 return z, B, hp 

130 

131 raise ValueError # _invalid_ 

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

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

134 

135 

136def _to3zll(lat, lon): # imported by .ups, .utm 

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

138 

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

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

141 

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

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

144 ''' 

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

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

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

148 

149 

150class UtmUpsBase(_NamedBase): 

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

152 ''' 

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

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

155 _datum = _WGS84 # L{Datum} 

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

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

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

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

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

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

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

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

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

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

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

167 

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

169 gamma=None, scale=None, **convergence): 

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

171 ''' 

172 E = self._Error 

173 if not E: # PYCHOK no cover 

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

175 

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

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

178 

179 if band: 

180 self._band1(band) 

181 

182 if datum not in (None, self._datum): 

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

184 

185 if not falsed: 

186 self._falsed = False 

187 

188 if convergence: # for backward compatibility 

189 gamma, kwds = _xkwds_pop2(convergence, convergence=gamma) 

190 if kwds: 

191 raise _UnexpectedError(**kwds) 

192 if gamma is not self._gamma: 

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

194 if scale is not self._scale: 

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

196 

197 def __repr__(self): 

198 return self.toRepr(B=True) 

199 

200 def __str__(self): 

201 return self.toStr() 

202 

203 def _band1(self, band): 

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

205 ''' 

206 if band: 

207 _xinstanceof(str, band=band) 

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

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

210 if band not in self._Bands: 

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

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

213 self._band = band 

214 elif self._band: # reset 

215 self._band = NN 

216 

217 @deprecated_property_RO 

218 def convergence(self): 

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

220 return self.gamma 

221 

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

223 def datum(self): 

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

225 ''' 

226 return self._datum 

227 

228 @datum.setter # PYCHOK setter! 

229 def datum(self, datum): 

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

231 ''' 

232 d = _ellipsoidal_datum(datum) 

233 if self._datum != d: 

234 _update_all(self) 

235 self._datum = d 

236 

237 @Property_RO 

238 def easting(self): 

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

240 ''' 

241 return self._easting 

242 

243 @Property_RO 

244 def eastingnorthing(self): 

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

246 ''' 

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

248 

249 def eastingnorthing2(self, falsed=True): 

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

251 

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

253 (C{bool}), otherwise unfalsed. 

254 

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

256 ''' 

257 e, n = self.falsed2 

258 if self.falsed and not falsed: 

259 e, n = neg_(e, n) 

260 elif falsed and not self.falsed: 

261 pass 

262 else: 

263 e = n = _0_0 

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

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

266 

267 @Property_RO 

268 def _epsg(self): 

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

270 ''' 

271 return _MODS.epsg.Epsg(self) 

272 

273 @Property_RO 

274 def falsed(self): 

275 '''Get easting and northing falsed (C{bool}). 

276 ''' 

277 return self._falsed 

278 

279 @Property_RO 

280 def falsed2(self): # PYCHOK no cover 

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

282 self._notOverloaded(self) 

283 

284 @Property_RO 

285 def gamma(self): 

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

287 if not available. 

288 ''' 

289 return self._gamma 

290 

291 @property_RO 

292 def hemisphere(self): 

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

294 ''' 

295 if not self._hemisphere: 

296 self._toLLEB() 

297 return self._hemisphere 

298 

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

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

301 ''' 

302 ll = self._latlon 

303 if LatLon is None: 

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

305 ll.gamma, ll.scale) 

306 else: 

307 _xsubclassof(_LLEB, LatLon=LatLon) 

308 kwds = _xkwds(LatLon_kwds, datum=ll.datum) 

309 r = _xattrs(LatLon(ll.lat, ll.lon, **kwds), 

310 ll, _under(_gamma_), _under(_scale_)) 

311 return _xnamed(r, ll.name) 

312 

313 def _latlon5args(self, ll, _toBand, unfalse, *other): 

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

315 ''' 

316 ll._toLLEB_args = (unfalse,) + other 

317 if unfalse: 

318 if not self._band: 

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

320 if not self._hemisphere: 

321 self._hemisphere = _hemi(ll.lat) 

322 self._latlon = ll 

323 

324 @Property_RO 

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

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

327 ''' 

328 return _lowerleft(self, 0) 

329 

330 @Property_RO 

331 def _mgrs(self): 

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

333 ''' 

334 return _toMgrs(self) 

335 

336 @Property_RO 

337 def _mgrs_lowerleft(self): 

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

339 ''' 

340 utmups = self._lowerleft 

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

342 

343 @Property_RO 

344 def northing(self): 

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

346 ''' 

347 return self._northing 

348 

349 @Property_RO 

350 def scale(self): 

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

352 ''' 

353 return self._scale 

354 

355 @Property_RO 

356 def scale0(self): 

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

358 ''' 

359 return self._scale0 

360 

361 @deprecated_method 

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

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

364 

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

366 ''' 

367 return self.eastingnorthing2(falsed=falsed) 

368 

369 def toEpsg(self): 

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

371 

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

373 

374 @raise EPSGError: See L{Epsg}. 

375 ''' 

376 return self._epsg 

377 

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

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

380 self._notOverloaded(**kwds) 

381 

382 def toMgrs(self, center=False): 

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

384 

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

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

387 meter} (C{scalar}). 

388 

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

390 

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

392 

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

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

395 ''' 

396 return self._mgrs if center in (False, 0, _0_0) else ( 

397 self._mgrs_lowerleft if center in (True,) else 

398 _toMgrs(_lowerleft(self, center))) # PYCHOK indent 

399 

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

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

402 ''' 

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

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

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

406 

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

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

409 ''' 

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

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

412 if cs: 

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

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

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

416 _n_a_ if self.scale is None else 

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

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

419 

420 

421def _lowerleft(utmups, center): # by .ellipsoidalBase._lowerleft 

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

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

424 ''' 

425 if center: 

426 e = n = -center 

427 else: 

428 c = 5 # center 

429 for _ in range(3): 

430 c *= 10 # 50, 500, 5000 

431 t = c * 2 

432 e = int(utmups.easting % t) 

433 n = int(utmups.northing % t) 

434 if (e == c and n in (c, c - 1)) or \ 

435 (n == c and e in (c, c - 1)): 

436 break 

437 else: 

438 return utmups # unchanged 

439 

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

441 gamma=utmups.gamma, 

442 scale=utmups.scale) 

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

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

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

446 

447 

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

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

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

451 

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

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

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

455 

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

457 band}). 

458 

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

460 ''' 

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

462 u = strUTMUPS.lstrip() 

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

464 raise ValueError(_not_(_UPS_ZONE_STR)) 

465 

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

467 if len(u) < 4: 

468 raise ValueError(_not_(sep)) 

469 

470 z, h = u[:2] 

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

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

473 

474 if z.isdigit(): 

475 z, B = int(z), band 

476 else: 

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

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

479 # int('') raises ValueError 

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

481 break 

482 else: 

483 raise ValueError(z) 

484 

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

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

487 

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

489 strUTMUPS=strUTMUPS, Error=Error) 

490 

491 

492def _toMgrs(utmups): 

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

494 ''' 

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

496 

497 

498__all__ += _ALL_DOCS(UtmUpsBase) 

499 

500# **) MIT License 

501# 

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

503# 

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

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

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

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

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

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

510# 

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

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

513# 

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

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

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

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

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

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

520# OTHER DEALINGS IN THE SOFTWARE.