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

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

9 _xinstanceof, _xsubclassof

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

11 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

12 from pygeodesy.dms import degDMS, parseDMS2

13 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB

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

15 _xattrs, _xkwds, _xkwds_not

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

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

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

19 _S_, _scale_, _SPACE_, _Y_, _Z_

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

21 from pygeodesy.named import _name__, _NamedBase, _under

22 from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple

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

24 deprecated_property_RO, Property_RO, property_RO

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

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

27 from pygeodesy.utily import _Wrap, wrap360

28

29__all__ = _ALL_LAZY.utmupsBase

30__version__ = '24.06.12'

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

62 class UtmUpsBase(_NamedBase):

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

64 '''

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

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

67 _datum = _WGS84 # L{Datum}

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

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

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

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

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

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

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

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

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

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

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

79

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

81 gamma=None, scale=None):

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

83 '''

84 E = self._Error

85 if not E: # PYCHOK no cover

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

87

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

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

90

91 if band:

92 self._band1(band)

93

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

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

96

97 if not falsed:

98 self._falsed = False

99

100 if gamma is not self._gamma:

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

102 if scale is not self._scale:

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

104

105 def __repr__(self):

106 return self.toRepr(B=True)

107

108 def __str__(self):

109 return self.toStr()

110

111 def _band1(self, band):

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

113 '''

114 if band:

115 _xinstanceof(str, band=band)

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

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

118 if band not in self._Bands:

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

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

121 self._band = band

122 elif self._band: # reset

123 self._band = NN

124

125 @deprecated_property_RO

126 def convergence(self):

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

128 return self.gamma

129

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

131 def datum(self):

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

133 '''

134 return self._datum

135

136 @datum.setter # PYCHOK setter!

137 def datum(self, datum):

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

139 '''

140 d = _ellipsoidal_datum(datum)

141 if self._datum != d:

142 _update_all(self)

143 self._datum = d

144

145 @Property_RO

146 def easting(self):

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

148 '''

149 return self._easting

150

151 @Property_RO

152 def eastingnorthing(self):

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

154 '''

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

156

157 def eastingnorthing2(self, falsed=True):

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

159

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

161 (C{bool}), otherwise unfalsed.

162

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

164 '''

165 e, n = self.falsed2

166 if self.falsed and not falsed:

167 e, n = neg_(e, n)

168 elif falsed and not self.falsed:

169 pass

170 else:

171 e = n = _0_0

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

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

174

175 @Property_RO

176 def _epsg(self):

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

178 '''

179 return _MODS.epsg.Epsg(self)

180

181 @Property_RO

182 def falsed(self):

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

184 '''

185 return self._falsed

186

187 @Property_RO

188 def falsed2(self): # PYCHOK no cover

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

190 self._notOverloaded(self)

191

192 @Property_RO

193 def gamma(self):

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

195 if not available.

196 '''

197 return self._gamma

198

199 @property_RO

200 def hemisphere(self):

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

202 '''

203 if not self._hemisphere:

204 self._toLLEB()

205 return self._hemisphere

206

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

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

209 '''

210 ll = self._latlon

211 if LatLon is None:

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

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

214 else:

215 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

218 return r

219

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

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

222 '''

223 ll._gamma = g

224 ll._scale = k

225 ll._toLLEB_args = (unfalse,) + other

226 if unfalse:

227 if not self._band:

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

229 if not self._hemisphere:

230 self._hemisphere = _hemi(ll.lat)

231 self._latlon = ll

232

233 @Property_RO

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

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

236 '''

237 return _lowerleft(self, 0)

238

239 @Property_RO

240 def _mgrs(self):

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

242 '''

243 return _toMgrs(self)

244

245 @Property_RO

246 def _mgrs_lowerleft(self):

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

248 '''

249 utmups = self._lowerleft

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

251

252 @Property_RO

253 def northing(self):

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

255 '''

256 return self._northing

257

258 @Property_RO

259 def scale(self):

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

261 '''

262 return self._scale

263

264 @Property_RO

265 def scale0(self):

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

267 '''

268 return self._scale0

269

270 @deprecated_method

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

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

273

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

275 '''

276 return self.eastingnorthing2(falsed=falsed)

277

278 def toEpsg(self):

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

280

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

282

283 @raise EPSGError: See L{Epsg}.

284 '''

285 return self._epsg

286

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

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

289 self._notOverloaded(**kwds)

290

291 def toMgrs(self, center=False):

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

293

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

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

296 meter} (C{scalar}).

297

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

299

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

301

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

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

304 '''

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

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

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

308

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

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

311 '''

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

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

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

315

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

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

318 '''

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

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

321 if cs:

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

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

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

325 _n_a_ if self.scale is None else

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

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

328

329

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

331 '''Return the hemisphere letter.

332

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

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

335

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

337 '''

338 return _S_ if lat < N else _N_

339

340

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

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

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

344 '''

345 if center:

346 e = n = -center

347 else:

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

349 t = c * 2

350 e = int(utmups.easting % t)

351 n = int(utmups.northing % t)

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

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

354 break

355 else:

356 return utmups # unchanged

357

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

359 gamma=utmups.gamma,

360 scale=utmups.scale, name=utmups.name)

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

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

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

364

365

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

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

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

369

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

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

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

373

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

375 band}).

376

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

378 '''

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

380 u = strUTMUPS.lstrip()

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

382 raise ValueError(_not_(_UPS_ZONE_STR))

383

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

385 if len(u) < 4:

386 raise ValueError(_not_(sep))

387

388 z, h = u[:2]

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

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

391

392 if z.isdigit():

393 z, B = int(z), band

394 else:

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

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

397 # int('') raises ValueError

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

399 break

400 else:

401 raise ValueError(z)

402

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

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

405

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

407 strUTMUPS=strUTMUPS, Error=Error)

408

409

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

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

412 '''

413 try:

414 # if lon is not None:

415 # raise AttributeError

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

417 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon)

418 if wrap:

419 _Wrap.latlon(lat, lon)

420 d = datum or latlon.datum

421 except AttributeError: # TypeError

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

423 parseDMS2(latlon, lon) # clipped

424 d = datum or _WGS84

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

426

427

428 def _toMgrs(utmups):

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

430 '''

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

432

433

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

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

436

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

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

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

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

441 C{ValueError}.

442

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

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

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

446 UTM/UPS bands.

447

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

449 '''

450 try:

451 B, z = band, _UTMUPS_ZONE_INVALID

452 if isscalar(zone):

453 z = int(zone)

454 elif zone and isstr(zone):

455 if zone.isdigit():

456 z = int(zone)

457 elif len(zone) > 1:

458 B = zone[-1:]

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

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

461 B = zone

462 z = _UPS_ZONE

463

464 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX:

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

466 if hp in _NS_ or not hp:

467 z = Zone(z)

468 B = Band(B.upper())

469 if B.isalpha():

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

471 elif not B:

472 return z, B, hp

473

474 raise ValueError() # _invalid_

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

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

477

478

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

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

481

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

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

484

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

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

487 '''

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

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

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

491

492

493__all__ += _ALL_DOCS(UtmUpsBase)

494

495# **) MIT License

496#

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

498#

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

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

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

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

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

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

505#

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

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

508#

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

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

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

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

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

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

515# OTHER DEALINGS IN THE SOFTWARE.