Coverage for pygeodesy/utmups.py: 98%

87 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-06 16:50 -0400

1 

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

3 

4u'''I{Karney}'s UTM and UPS utilities. 

5 

6Functions L{parseUTMUPS5}, L{toUtmUps8}, L{UtmUps} and L{utmupsZoneBand5} 

7to handle both I{Universal Transverse Mercator} (U{UTM 

8<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>}) 

9and I{Universal Polar Stereographic} (U{UPS 

10<https://WikiPedia.org/wiki/Universal_polar_stereographic_coordinate_system>}) 

11coordinates. 

12 

13A pure Python implementation, partially transcoded from C++ class U{UTMUPS 

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

15by I{Charles Karney}. 

16''' 

17# from pygeodesy.basics import map1 # from .namedTuples 

18# from pygeodesy.datums import _WGS84 # from .utmupsBase 

19from pygeodesy.errors import _IsnotError, RangeError, _ValueError, _xkwds_get 

20from pygeodesy.interns import NN, _easting_, _MGRS_, _northing_, _NS_, \ 

21 _outside_, _range_, _SPACE_, _UPS_, _UTM_ 

22from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

23from pygeodesy.named import modulename 

24from pygeodesy.namedTuples import UtmUps5Tuple, UtmUps8Tuple, map1 

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

26from pygeodesy.units import Northing, _100km 

27from pygeodesy.ups import parseUPS5, toUps8, Ups, UPSError, upsZoneBand5 

28from pygeodesy.utm import parseUTM5, toUtm8, Utm, UTMError, utmZoneBand5 

29from pygeodesy.utmupsBase import Fmt, _to4lldn, _to3zBhp, _UPS_ZONE, \ 

30 _UPS_ZONE_STR, _UTMUPS_ZONE_MIN, \ 

31 _UTMUPS_ZONE_MAX, _WGS84 

32 

33__all__ = _ALL_LAZY.utmups 

34__version__ = '23.05.03' 

35 

36_MGRS_TILE = _100km # in .mgrs.Mgrs.tile 

37 

38_UPS_N_MAX = Northing( 27 * _MGRS_TILE) 

39_UPS_N_MIN = Northing( 13 * _MGRS_TILE) 

40_UPS_S_MAX = Northing( 32 * _MGRS_TILE) 

41_UPS_S_MIN = Northing( 8 * _MGRS_TILE) 

42 

43_UTM_C_MAX = Northing( 9 * _MGRS_TILE) 

44_UTM_C_MIN = Northing( 1 * _MGRS_TILE) 

45_UTM_N_MAX = Northing( 95 * _MGRS_TILE) 

46_UTM_N_MIN = Northing( 0 * _MGRS_TILE) 

47_UTM_S_MAX = Northing(100 * _MGRS_TILE) 

48_UTM_S_MIN = Northing( 10 * _MGRS_TILE) 

49 

50_UTM_N_SHIFT = _UTM_S_MAX - _UTM_N_MIN # South minus North UTM northing 

51 

52 

53class _UpsMinMax(object): # XXX _NamedEnum or _NamedTuple 

54 # UPS ranges for North, South pole 

55 eMax = _UPS_N_MAX, _UPS_S_MAX 

56 eMin = _UPS_N_MIN, _UPS_S_MIN 

57 nMax = _UPS_N_MAX, _UPS_S_MAX 

58 nMin = _UPS_N_MIN, _UPS_S_MIN 

59 

60 

61class _UtmMinMax(object): # XXX _NamedEnum or _NamedTuple 

62 # UTM ranges for Northern, Southern hemisphere 

63 eMax = _UTM_C_MAX, _UTM_C_MAX 

64 eMin = _UTM_C_MIN, _UTM_C_MIN 

65 nMax = _UTM_N_MAX, Northing(_UTM_N_MAX + _UTM_N_SHIFT) 

66 nMin = Northing(_UTM_S_MIN - _UTM_N_SHIFT), _UTM_S_MIN 

67 

68 

69class UTMUPSError(_ValueError): # XXX (UTMError, UPSError) 

70 '''Universal Transverse Mercator/Universal Polar Stereographic 

71 (UTM/UPS) parse, validate or other issue. 

72 ''' 

73 pass 

74 

75 

76def parseUTMUPS5(strUTMUPS, datum=_WGS84, Utm=Utm, Ups=Ups, name=NN): 

77 '''Parse a string representing a UTM or UPS coordinate, consisting 

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

79 

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

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

82 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) 

83 or C{None}. 

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

85 or C{None}. 

86 @kwarg name: Optional instance name (C{str}). 

87 

88 @return: The UTM or UPS instance (B{C{Utm}} or B{C{Ups}}) or 

89 a L{UtmUps5Tuple}C{(zone, hemipole, easting, northing, 

90 band)} if B{C{Utm}} respectively B{C{Ups}} or both are 

91 C{None}. The C{hemipole} is C{'N'|'S'}, the UTM hemisphere 

92 or UPS pole, the UPS projection top/center. 

93 

94 @raise UTMUPSError: Invalid B{C{strUTMUPS}}. 

95 

96 @see: Functions L{pygeodesy.parseUTM5} and L{pygeodesy.parseUPS5}. 

97 ''' 

98 try: 

99 try: 

100 u = parseUTM5(strUTMUPS, datum=datum, Utm=Utm, name=name) 

101 except UTMError: 

102 u = parseUPS5(strUTMUPS, datum=datum, Ups=Ups, name=name) 

103 return u 

104 

105 except (UTMError, UPSError) as x: 

106 raise UTMUPSError(strUTMUPS=strUTMUPS, cause=x) 

107 

108 

109def toUtmUps8(latlon, lon=None, datum=None, falsed=True, Utm=Utm, Ups=Ups, 

110 pole=NN, name=NN, **cmoff): 

111 '''Convert a lat-/longitude point to a UTM or UPS coordinate. 

112 

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

114 geodetic C{LatLon} point. 

115 @kwarg lon: Optional longitude (C{degrees}) or C{None}. 

116 @kwarg datum: Optional datum to use this UTM coordinate, 

117 overriding B{C{latlon}}'s datum (C{Datum}). 

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

119 @kwarg Utm: Optional class to return the UTM coordinate (L{Utm}) 

120 or C{None}. 

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

122 or C{None}. 

123 @kwarg pole: Optional top/center of UPS (stereographic) 

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

125 @kwarg name: Optional name (C{str}). 

126 @kwarg cmoff: DEPRECATED, use B{C{falsed}}. Offset longitude 

127 from zone's central meridian, for UTM only (C{bool}). 

128 

129 @return: The UTM or UPS coordinate (B{C{Utm}} respectively B{C{Ups}}) 

130 or a L{UtmUps8Tuple}C{(zone, hemipole, easting, northing, 

131 band, datum, gamma, scale)} if B{C{Utm}} respectively 

132 B{C{Ups}} is C{None} or B{C{cmoff}} is C{False}. 

133 

134 @raise RangeError: If B{C{lat}} outside the valid UTM or UPS bands 

135 or if B{C{lat}} or B{C{lon}} outside the valid 

136 range and L{pygeodesy.rangerrors} set to C{True}. 

137 

138 @raise TypeError: If B{C{latlon}} is not ellipsoidal or B{C{lon}} 

139 value is missing of B{C{datum}} is invalid. 

140 

141 @raise UTMUPSError: UTM or UPS validation failed. 

142 

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

144 

145 @see: Functions L{pygeodesy.toUtm8} and L{pygeodesy.toUps8}. 

146 ''' 

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

148 z, B, p, lat, lon = utmupsZoneBand5(lat, lon) 

149 

150 f = falsed and _xkwds_get(cmoff, cmoff=True) 

151 if z == _UPS_ZONE: 

152 u = toUps8(lat, lon, datum=d, falsed=f, Ups=Ups, name=name, pole=pole or p) 

153 else: 

154 u = toUtm8(lat, lon, datum=d, falsed=f, Utm=Utm, name=name) 

155 return u 

156 

157 

158def UtmUps(zone, hemipole, easting, northing, band=NN, datum=_WGS84, 

159 falsed=True, name=NN): 

160 '''Class-like function to create a UTM/UPS coordinate. 

161 

162 @kwarg zone: The UTM zone with/-out I{longitudinal} Band or UPS zone C{0} 

163 or C{"00"} with/-out I{polar} Band (C{str} or C{int}). 

164 @kwarg hemipole: UTM hemisphere or UPS top/center of projection 

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

166 @arg easting: Easting, see B{C{falsed}} (C{meter}). 

167 @arg northing: Northing, see B{C{falsed}} (C{meter}). 

168 @kwarg band: Optional, UTM I{latitudinal} C{'C'|'D'|..|'W'|'X'} or UPS 

169 I{polar} Band letter C{'A'|'B'|'Y'|'Z'} Band letter (C{str}). 

170 @kwarg datum: Optional, the coordinate's datum (L{Datum}). 

171 @kwarg falsed: Both B{C{easting}} and B{C{northing}} are falsed (C{bool}). 

172 @kwarg name: Optional name (C{str}). 

173 

174 @return: New UTM or UPS instance (L{Utm} or L{Ups}). 

175 

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

177 

178 @raise UTMUPSError: UTM or UPS validation failed. 

179 

180 @see: Classes L{Utm} and L{Ups} and I{Karney}'s U{UTMUPS 

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

182 ''' 

183 z, B, hp = _to3zBhp(zone, band, hemipole=hemipole) 

184 U = Ups if z in (_UPS_ZONE, _UPS_ZONE_STR) else Utm 

185 return U(z, hp, easting, northing, band=B, datum=datum, 

186 falsed=falsed, name=name) 

187 

188 

189def utmupsValidate(coord, falsed=False, MGRS=False, Error=UTMUPSError): 

190 '''Check a UTM or UPS coordinate. 

191 

192 @arg coord: The UTM or UPS coordinate (L{Utm}, L{Etm}, L{Ups} 

193 or C{5+Tuple}). 

194 @kwarg falsed: C{5+Tuple} easting and northing are falsed 

195 (C{bool}), ignored otherwise. 

196 @kwarg MGRS: Increase easting and northing ranges (C{bool}). 

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

198 (L{UTMUPSError}). 

199 

200 @return: C{None} if validation passed. 

201 

202 @raise Error: Validation failed. 

203 

204 @see: Function L{utmupsValidateOK}. 

205 ''' 

206 

207 def _en(en, lo, hi, ename): # U, Error 

208 try: 

209 if lo <= float(en) <= hi: 

210 return 

211 except (TypeError, ValueError): 

212 pass 

213 t = _SPACE_(_outside_, U, _range_, _range_(lo, hi)) 

214 raise Error(ename, en, txt=t) 

215 

216 if isinstance(coord, (Ups, Utm)): 

217 hemi = coord.hemisphere 

218 enMM = coord.falsed 

219 elif isinstance(coord, (UtmUps5Tuple, UtmUps8Tuple)): 

220 hemi = coord.hemipole 

221 enMM = falsed 

222 else: 

223 raise _IsnotError(Error=Error, coord=coord, *map1(modulename, 

224 Utm, Ups, UtmUps5Tuple, UtmUps8Tuple)) 

225 band = coord.band 

226 zone = coord.zone 

227 

228 z, B, h = _to3zBhp(zone, band, hemipole=hemi) 

229 

230 if z == _UPS_ZONE: # UPS 

231 u, U, M = _MODS.ups, _UPS_, _UpsMinMax 

232 else: # UTM 

233 u, U, M = _MODS.utm, _UTM_, _UtmMinMax 

234 

235 if MGRS: 

236 U, s = _MGRS_, _MGRS_TILE 

237 else: 

238 s = 0 

239 

240 i = _NS_.find(h) 

241 if i < 0 or z < _UTMUPS_ZONE_MIN \ 

242 or z > _UTMUPS_ZONE_MAX \ 

243 or B not in u._Bands: 

244 t = Fmt.PAREN(U, repr(_SPACE_(NN(Fmt.zone(z), B), h))) 

245 raise Error(coord=t, zone=zone, band=band, hemisphere=hemi) 

246 

247 if enMM: 

248 _en(coord.easting, M.eMin[i] - s, M.eMax[i] + s, _easting_) # PYCHOK .eMax .eMin 

249 _en(coord.northing, M.nMin[i] - s, M.nMax[i] + s, _northing_) # PYCHOK .nMax .nMin 

250 

251 

252def utmupsValidateOK(coord, falsed=False, ok=True): 

253 '''Check a UTM or UPS coordinate. 

254 

255 @arg coord: The UTM or UPS coordinate (L{Utm}, L{Ups} or C{5+Tuple}). 

256 @kwarg falsed: C{5+Tuple} easting and northing are falsed (C{bool}). 

257 @kwarg ok: Result to return if validation passed (B{C{ok}}). 

258 

259 @return: B{C{ok}} if validation passed, the L{UTMUPSError} otherwise. 

260 

261 @see: Function L{utmupsValidate}. 

262 ''' 

263 try: 

264 utmupsValidate(coord, falsed=falsed) 

265 return ok 

266 except UTMUPSError as x: 

267 return x 

268 

269 

270def utmupsZoneBand5(lat, lon, cmoff=False, name=NN): 

271 '''Return the UTM/UPS zone number, Band letter, hemisphere/pole 

272 and clipped lat- and longitude for a given location. 

273 

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

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

276 @kwarg cmoff: Offset longitude from the zone's central 

277 meridian, for UTM only (C{bool}). 

278 @kwarg name: Optional name (C{str}). 

279 

280 @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole, 

281 lat, lon)} where C{hemipole} is C{'N'|'S'}, the 

282 UTM hemisphere or UPS pole, the UPS projection 

283 top/center. 

284 

285 @raise RangeError: If B{C{lat}} outside the valid UTM or UPS bands 

286 or if B{C{lat}} or B{C{lon}} outside the valid 

287 range and L{pygeodesy.rangerrors} set to C{True}. 

288 

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

290 

291 @see: Functions L{pygeodesy.utmZoneBand5} and L{pygeodesy.upsZoneBand5}. 

292 ''' 

293 try: 

294 return utmZoneBand5(lat, lon, cmoff=cmoff, name=name) 

295 except RangeError: 

296 return upsZoneBand5(lat, lon, name=name) 

297 

298# **) MIT License 

299# 

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

301# 

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

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

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

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

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

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

308# 

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

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

311# 

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

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

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

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

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

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

318# OTHER DEALINGS IN THE SOFTWARE.