1

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

3

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

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

6'''

7

8 from pygeodesy.basics import isint, isscalar, isstr, map1, 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 _xkwds, _xkwds_get, _xkwds_not

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

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

18 _scale_, _SPACE_, _Y_, _Z_, _UNDER

19 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

20 from pygeodesy.named import _NamedBase, nameof, notOverloaded, _xnamed

21 from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple

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

23 deprecated_property_RO, Property_RO, property_RO

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

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

26 from pygeodesy.utily import wrap360

27

28__all__ = ()

29__version__ = '22.10.05'

30

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

32# _UTM_BANDS = _MODS.utm._Bands

33

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

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

36

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

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

39

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

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

42

43_UTM_ZONE_MAX = 60 # PYCHOK for export

44_UTM_ZONE_MIN = 1 # PYCHOK for export

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

46

47_UPS_ZONE = _UTM_ZONE_MIN - 1 # PYCHOK for export

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

49

50_UTMUPS_ZONE_INVALID = -4 # PYCHOK for export too

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

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

53

54# _MAX_PSEUDO_ZONE = -1

55# _MIN_PSEUDO_ZONE = -4

56# _UTMUPS_ZONE_MATCH = -3

57# _UTMUPS_ZONE_STANDARD = -1

58# _UTM = -2

59

60

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

62 '''Return the hemisphere letter.

63

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

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

66

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

68 '''

69 return _NS_[int(lat < N)]

70

71

72 def _to4lldn(latlon, lon, datum, name):

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

74 '''

75 try:

76 # if lon is not None:

77 # raise AttributeError

78 lat, lon = map1(float, latlon.lat, latlon.lon)

79 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon)

80 d = datum or latlon.datum

81 except AttributeError:

82 lat, lon = parseDMS2(latlon, lon)

83 d = datum or _WGS84

84 return lat, lon, d, (name or nameof(latlon))

85

86

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

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

89

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

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

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

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

94 C{ValueError}.

95

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

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

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

99 UTM/UPS bands.

100

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

102 '''

103 try:

104 B, z = band, _UTMUPS_ZONE_INVALID

105 if isscalar(zone):

106 z = int(zone)

107 elif zone and isstr(zone):

108 if zone.isdigit():

109 z = int(zone)

110 elif len(zone) > 1:

111 B = zone[-1:]

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

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

114 B = zone

115 z = _UPS_ZONE

116

117 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX:

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

119 if hp in _NS_ or not hp:

120 z = Zone(z)

121 B = Band(B.upper())

122 if B.isalpha():

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

124 elif not B:

125 return z, B, hp

126

127 raise ValueError # _invalid_

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

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

130

131

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

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

134

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

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

137

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

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

140 '''

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

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

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

144

145

146 class UtmUpsBase(_NamedBase):

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

148 '''

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

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

151 _datum = _WGS84 # L{Datum}

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

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

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

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

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

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

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

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

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

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

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

163

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

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

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

167 '''

168 E = self._Error

169 if not E:

170 notOverloaded(self, callername=_UNDER(_Error_))

171

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

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

174

175 if band:

176 self._band1(band)

177

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

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

180

181 if not falsed:

182 self._falsed = False

183

184 if convergence: # for backward compatibility

185 gamma = _xkwds_get(convergence, convergence=gamma)

186 if gamma is not self._gamma:

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

188 if scale is not self._scale:

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

190

191 def __repr__(self):

192 return self.toRepr(B=True)

193

194 def __str__(self):

195 return self.toStr()

196

197 def _band1(self, band):

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

199 '''

200 if band:

201 _xinstanceof(str, band=band)

202# if not self._Bands:

203# notOverloaded(self, callername=_UNDER('Bands'))

204 if band not in self._Bands:

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

206 raise self._Error(band=band, txt=_not_(t))

207 self._band = band

208 elif self._band: # reset

209 self._band = NN

210

211 @deprecated_property_RO

212 def convergence(self):

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

214 return self.gamma

215

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

217 def datum(self):

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

219 '''

220 return self._datum

221

222 @datum.setter # PYCHOK setter!

223 def datum(self, datum):

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

225 '''

226 d = _ellipsoidal_datum(datum)

227 if self._datum != d:

228 _update_all(self)

229 self._datum = d

230

231 @Property_RO

232 def easting(self):

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

234 '''

235 return self._easting

236

237 @Property_RO

238 def eastingnorthing(self):

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

240 '''

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

242

243 def eastingnorthing2(self, falsed=True):

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

245

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

247 (C{bool}), otherwise unfalsed.

248

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

250 '''

251 e, n = self.falsed2

252 if self.falsed and not falsed:

253 e, n = neg_(e, n)

254 elif falsed and not self.falsed:

255 pass

256 else:

257 e = n = _0_0

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

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

260

261 @Property_RO

262 def _epsg(self):

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

264 '''

265 return _MODS.epsg.Epsg(self)

266

267 @Property_RO

268 def falsed(self):

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

270 '''

271 return self._falsed

272

273 @Property_RO

274 def falsed2(self): # PYCHOK no cover

275 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}.

276 '''

277 notOverloaded(self)

278

279 @Property_RO

280 def gamma(self):

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

282 if not available.

283 '''

284 return self._gamma

285

286 @property_RO

287 def hemisphere(self):

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

289 '''

290 if not self._hemisphere:

291 self._toLLEB()

292 return self._hemisphere

293

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

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

296 '''

297 ll = self._latlon

298 if LatLon is None:

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

300 ll.gamma, ll.scale)

301 else:

302 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

305 ll, _UNDER(_gamma_), _UNDER(_scale_))

306 return _xnamed(r, ll.name)

307

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

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

310 '''

311 ll._toLLEB_args = (unfalse,) + other

312 if unfalse:

313 if not self._band:

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

315 if not self._hemisphere:

316 self._hemisphere = _hemi(ll.lat)

317 self._latlon = ll

318

319 @Property_RO

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

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

322 '''

323 return _lowerleft(self, 0)

324

325 @Property_RO

326 def _mgrs(self):

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

328 '''

329 return _toMgrs(self)

330

331 @Property_RO

332 def _mgrs_lowerleft(self):

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

334 '''

335 utmups = self._lowerleft

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

337

338 @Property_RO

339 def northing(self):

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

341 '''

342 return self._northing

343

344 @Property_RO

345 def scale(self):

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

347 '''

348 return self._scale

349

350 @Property_RO

351 def scale0(self):

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

353 '''

354 return self._scale0

355

356 @deprecated_method

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

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

359

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

361 '''

362 return self.eastingnorthing2(falsed=falsed)

363

364 def toEpsg(self):

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

366

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

368

369 @raise EPSGError: See L{Epsg}.

370 '''

371 return self._epsg

372

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

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

375 '''

376 notOverloaded(self, **kwds)

377

378 def toMgrs(self, center=False):

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

380

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

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

383 meter} (C{scalar}).

384

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

386

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

388

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

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

391 '''

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

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

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

395

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

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

398 '''

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

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

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

402

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

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

405 '''

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

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

408 if cs:

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

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

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

412 _n_a_ if self.scale is None else

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

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

415

416

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

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

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

420 '''

421 if center:

422 e = n = -center

423 else:

424 c = 5 # center

425 for _ in range(3):

426 c *= 10 # 50, 500, 5000

427 t = c * 2

428 e = int(utmups.easting % t)

429 n = int(utmups.northing % t)

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

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

432 break

433 else:

434 return utmups # unchanged

435

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

437 gamma=utmups.gamma,

438 scale=utmups.scale)

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

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

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

442

443

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

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

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

447

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

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

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

451

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

453 band}).

454

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

456 '''

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

458 u = strUTMUPS.lstrip()

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

460 raise ValueError(_not_(_UPS_ZONE_STR))

461

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

463 if len(u) < 4:

464 raise ValueError(_not_(sep))

465

466 z, h = u[:2]

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

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

469

470 if z.isdigit():

471 z, B = int(z), band

472 else:

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

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

475 # int('') raises ValueError

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

477 break

478 else:

479 raise ValueError(z)

480

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

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

483

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

485 strUTMUPS=strUTMUPS, Error=Error)

486

487

488 def _toMgrs(utmups):

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

490 '''

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

492

493

494__all__ += _ALL_DOCS(UtmUpsBase)

495

496# **) MIT License

497#

498# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

499#

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

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

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

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

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

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

506#

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

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

509#

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

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

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

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

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

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

516# OTHER DEALINGS IN THE SOFTWARE.