Coverage for pygeodesy/wgrs.py: 97%

190 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-04 12:01 -0400

1 

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

3 

4u'''World Geographic Reference System (WGRS) en-/decoding, aka GEOREF. 

5 

6Class L{Georef} and several functions to encode, decode and inspect WGRS 

7(or GEOREF) references. 

8 

9Transcoded from I{Charles Karney}'s C++ class U{Georef 

10<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Georef.html>}, 

11but with modified C{precision} and extended with C{height} and C{radius}. 

12 

13@see: U{World Geographic Reference System 

14 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}. 

15''' 

16from pygeodesy.basics import isstr, typename 

17from pygeodesy.constants import INT0, _float, _off90, _0_001, \ 

18 _0_5, _1_0, _2_0, _60_0, _1000_0 

19from pygeodesy.dms import parse3llh 

20from pygeodesy.errors import _ValueError, _xattr, _xStrError 

21# from pygeodesy.internals import typename # from .basics 

22from pygeodesy.interns import NN, _0to9_, _AtoZnoIO_, _COMMA_, \ 

23 _height_, _INV_, _radius_, _SPACE_ 

24from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY 

25from pygeodesy.named import _name2__, nameof, Property_RO 

26from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple 

27# from pygeodesy.props import Property_RO # from .named 

28from pygeodesy.streprs import Fmt, _0wd 

29from pygeodesy.units import Height, Int, Lat, Lon, Precision_, \ 

30 Radius, Scalar_, Str 

31from pygeodesy.utily import ft2m, m2ft, m2NM 

32 

33from math import floor 

34 

35__all__ = _ALL_LAZY.wgrs 

36__version__ = '25.04.14' 

37 

38_Base = 10 

39_BaseLen = 4 

40_DegChar = _AtoZnoIO_.tillQ 

41_Digits = _0to9_ 

42_LatOrig = -90 

43_LatTile = _AtoZnoIO_.tillM 

44_LonOrig = -180 

45_LonTile = _AtoZnoIO_ 

46_60B = 60000000000 # == 60_000_000_000 == 60e9 

47_MaxPrec = 11 

48_Tile = 15 # tile size in degrees 

49 

50_MaxLen = _BaseLen + 2 * _MaxPrec 

51_MinLen = _BaseLen - 2 

52 

53_LatOrig_60B = _LatOrig * _60B 

54_LonOrig_60B = _LonOrig * _60B 

55 

56_float_Tile = _float(_Tile) 

57_LatOrig_Tile = _float(_LatOrig) / _Tile 

58_LonOrig_Tile = _float(_LonOrig) / _Tile 

59 

60 

61def _divmod3(x, _Orig_60B): 

62 '''(INTERNAL) Convert B{C{x}} to 3_tuple C{(tile, modulo, fraction)}/ 

63 ''' 

64 i = int(floor(x * _60B)) 

65 i, x = divmod(i - _Orig_60B, _60B) 

66 xt, xd = divmod(i, _Tile) 

67 return xt, xd, x 

68 

69 

70def _2fll(lat, lon): 

71 '''(INTERNAL) Convert lat, lon. 

72 ''' 

73 # lat, lon = parseDMS2(lat, lon) 

74 return (Lat(lat, Error=WGRSError), 

75 Lon(lon, Error=WGRSError)) 

76 

77 

78def _2geostr2(georef): 

79 '''(INTERNAL) Check a georef string. 

80 ''' 

81 try: 

82 n, g = len(georef), georef.upper() 

83 p, o = divmod(n, 2) 

84 if o or n < _MinLen or n > _MaxLen \ 

85 or g.startswith(_INV_) or not g.isalnum(): 

86 raise ValueError() 

87 return g, _2Precision(p - 1) 

88 

89 except (AttributeError, TypeError, ValueError) as x: 

90 raise WGRSError(typename(Georef), georef, cause=x) 

91 

92 

93def _2Precision(precision): 

94 '''(INTERNAL) Return a L{Precision_} instance. 

95 ''' 

96 return Precision_(precision, Error=WGRSError, low=0, high=_MaxPrec) 

97 

98 

99class WGRSError(_ValueError): 

100 '''World Geographic Reference System (WGRS) encode, decode or other L{Georef} issue. 

101 ''' 

102 pass 

103 

104 

105class Georef(Str): 

106 '''Georef class, a named C{str}. 

107 ''' 

108 # no str.__init__ in Python 3 

109 def __new__(cls, lat_gll, lon=None, height=None, precision=3, name=NN): 

110 '''New L{Georef} from an other L{Georef} instance or georef 

111 C{str} or from a C{LatLon} instance or lat-/longitude C{str}. 

112 

113 @arg lat_gll: Latitude (C{degrees90}), a georef (L{Georef}, 

114 C{str}) or a location (C{LatLon}, C{LatLon*Tuple}). 

115 @kwarg lon: Logitude (C{degrees180)}, required if B{C{lat_gll}} 

116 is C{degrees90}, ignored otherwise. 

117 @kwarg height: Optional height in C{meter}, used if B{C{lat_gll}} 

118 is a location. 

119 @kwarg precision: The desired georef resolution and length (C{int} 

120 0..11), see L{encode<pygeodesy.wgrs.encode>}. 

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

122 

123 @return: New L{Georef}. 

124 

125 @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}. 

126 

127 @raise TypeError: Invalid B{C{lat_gll}} or B{C{lon}}. 

128 

129 @raise WGRSError: INValid B{C{lat_gll}}. 

130 ''' 

131 if lon is None: 

132 if isinstance(lat_gll, Georef): 

133 g, ll, p = str(lat_gll), lat_gll.latlon, lat_gll.precision 

134 elif isstr(lat_gll): 

135 if _COMMA_ in lat_gll or _SPACE_ in lat_gll: 

136 lat, lon, h = parse3llh(lat_gll, height=height) 

137 g, ll, p = _encode3(lat, lon, precision, h=h) 

138 else: 

139 g, ll = lat_gll.upper(), None 

140 try: 

141 _, p = _2geostr2(g) # validate 

142 except WGRSError: # R00H00? 

143 p = None # = decode5(g).precision? 

144 else: # assume LatLon 

145 try: 

146 g, ll, p = _encode3(lat_gll.lat, lat_gll.lon, precision, 

147 h=_xattr(lat_gll, height=height)) 

148 except AttributeError: 

149 raise _xStrError(Georef, gll=lat_gll) # Error=WGRSError 

150 else: 

151 g, ll, p = _encode3(lat_gll, lon, precision, h=height) 

152 

153 self = Str.__new__(cls, g, name=name or nameof(lat_gll)) 

154 self._latlon = ll 

155 self._precision = p 

156 return self 

157 

158 @Property_RO 

159 def decoded3(self): 

160 '''Get this georef's attributes (L{LatLonPrec3Tuple}). 

161 ''' 

162 lat, lon = self.latlon 

163 return LatLonPrec3Tuple(lat, lon, self.precision, name=self.name) 

164 

165 @Property_RO 

166 def decoded5(self): 

167 '''Get this georef's attributes (L{LatLonPrec5Tuple}) with 

168 height and radius set to C{None} if missing. 

169 ''' 

170 return self.decoded3.to5Tuple(self.height, self.radius) 

171 

172 @Property_RO 

173 def _decoded5(self): 

174 '''(INTERNAL) Initial L{LatLonPrec5Tuple}. 

175 ''' 

176 return decode5(self) 

177 

178 @Property_RO 

179 def height(self): 

180 '''Get this georef's height in C{meter} or C{None} if missing. 

181 ''' 

182 return self._decoded5.height 

183 

184 @Property_RO 

185 def latlon(self): 

186 '''Get this georef's (center) lat- and longitude (L{LatLon2Tuple}). 

187 ''' 

188 lat, lon = self._latlon or self._decoded5[:2] 

189 return LatLon2Tuple(lat, lon, name=self.name) 

190 

191 @Property_RO 

192 def latlonheight(self): 

193 '''Get this georef's (center) lat-, longitude and height (L{LatLon3Tuple}), 

194 with height set to C{INT0} if missing. 

195 ''' 

196 return self.latlon.to3Tuple(self.height or INT0) 

197 

198 @Property_RO 

199 def precision(self): 

200 '''Get this georef's precision (C{int}). 

201 ''' 

202 p = self._precision 

203 return self._decoded5.precision if p is None else p 

204 

205 @Property_RO 

206 def radius(self): 

207 '''Get this georef's radius in C{meter} or C{None} if missing. 

208 ''' 

209 return self._decoded5.radius 

210 

211 def toLatLon(self, LatLon=None, height=None, **name_LatLon_kwds): 

212 '''Return (the center of) this georef cell as a C{LatLon}. 

213 

214 @kwarg LatLon: Class to use (C{LatLon}) or C{None}. 

215 @kwarg height: Optional height (C{meter}), overriding this height. 

216 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} (C{str}) and optionally, 

217 additional B{C{LatLon}} keyword arguments, ignored if C{B{LatLon} 

218 is None}. 

219 

220 @return: This georef location (B{C{LatLon}}) or if C{B{LatLon} is None}, 

221 a L{LatLon3Tuple}C{(lat, lon, height)}. 

222 

223 @raise TypeError: Invalid B{C{LatLon}} or B{C{name_LatLon_kwds}}. 

224 ''' 

225 n, kwds = _name2__(name_LatLon_kwds, _or_nameof=self) 

226 h = (self.height or INT0) if height is None else height # _heigHt 

227 r = self.latlon.to3Tuple(h) if LatLon is None else LatLon( 

228 *self.latlon, height=h, **kwds) 

229 return r.renamed(n) if n else r 

230 

231 

232def decode3(georef, center=True): 

233 '''Decode a C{georef} to lat-, longitude and precision. 

234 

235 @arg georef: To be decoded (L{Georef} or C{str}). 

236 @kwarg center: If C{True}, use the georef's center, otherwise 

237 the south-west, lower-left corner (C{bool}). 

238 

239 @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}. 

240 

241 @raise WGRSError: Invalid B{C{georef}}, INValid, non-alphanumeric 

242 or odd length B{C{georef}}. 

243 ''' 

244 def _digit(ll, g, i, m): 

245 d = _Digits.find(g[i]) 

246 if d < 0 or d >= m: 

247 raise _Error(i) 

248 return ll * m + d 

249 

250 def _Error(i): 

251 return WGRSError(Fmt.SQUARE(georef=i), georef) 

252 

253 def _index(chars, g, i): 

254 k = chars.find(g[i]) 

255 if k < 0: 

256 raise _Error(i) 

257 return k 

258 

259 g, precision = _2geostr2(georef) 

260 lon = _index(_LonTile, g, 0) + _LonOrig_Tile 

261 lat = _index(_LatTile, g, 1) + _LatOrig_Tile 

262 

263 u = _1_0 

264 if precision > 0: 

265 lon = lon * _Tile + _index(_DegChar, g, 2) 

266 lat = lat * _Tile + _index(_DegChar, g, 3) 

267 m, p = 6, precision - 1 

268 for i in range(_BaseLen, _BaseLen + p): 

269 lon = _digit(lon, g, i, m) 

270 lat = _digit(lat, g, i + p, m) 

271 u *= m 

272 m = _Base 

273 u *= _Tile 

274 

275 if center: 

276 lon = lon * _2_0 + _1_0 

277 lat = lat * _2_0 + _1_0 

278 u *= _2_0 

279 u = _Tile / u 

280 return LatLonPrec3Tuple(Lat(lat * u, Error=WGRSError), 

281 Lon(lon * u, Error=WGRSError), 

282 precision, name=nameof(georef)) 

283 

284 

285def decode5(georef, center=True): 

286 '''Decode a C{georef} to lat-, longitude, precision, height and radius. 

287 

288 @arg georef: To be decoded (L{Georef} or C{str}). 

289 @kwarg center: If C{True}, use the georef's center, otherwise the 

290 south-west, lower-left corner (C{bool}). 

291 

292 @return: A L{LatLonPrec5Tuple}C{(lat, lon, precision, height, radius)} 

293 where C{height} and/or C{radius} are C{None} if missing. 

294 

295 @raise WGRSError: Invalid B{C{georef}}. 

296 ''' 

297 def _h2m(kft, g_n): 

298 return Height(ft2m(kft * _1000_0), name=g_n, Error=WGRSError) 

299 

300 def _r2m(NM, g_n): 

301 return Radius(NM / m2NM(1), name=g_n, Error=WGRSError) 

302 

303 def _split2(g, Unit, _2m): 

304 n = typename(Unit) 

305 i = max(g.find(n[0]), g.rfind(n[0])) 

306 if i > _BaseLen: 

307 return g[:i], _2m(int(g[i+1:]), _SPACE_(georef, n)) 

308 else: 

309 return g, None 

310 

311 g = Str(georef, Error=WGRSError) 

312 

313 g, h = _split2(g, Height, _h2m) # H is last 

314 g, r = _split2(g, Radius, _r2m) # R before H 

315 

316 return decode3(g, center=center).to5Tuple(h, r) 

317 

318 

319def encode(lat, lon, precision=3, height=None, radius=None): # MCCABE 14 

320 '''Encode a lat-/longitude as a C{georef} of the given precision. 

321 

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

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

324 @kwarg precision: Optional, the desired C{georef} resolution and length 

325 (C{int} 0..11). 

326 @kwarg height: Optional, height in C{meter}, see U{Designation of area 

327 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}. 

328 @kwarg radius: Optional, radius in C{meter}, see U{Designation of area 

329 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}. 

330 

331 @return: The C{georef} (C{str}). 

332 

333 @raise RangeError: Invalid B{C{lat}} or B{C{lon}}. 

334 

335 @raise WGRSError: Invalid B{C{precision}}, B{C{height}} or B{C{radius}}. 

336 

337 @note: The B{C{precision}} value differs from I{Karney}'s U{Georef<https:// 

338 GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Georef.html>}. 

339 The C{georef} length is M{2 * (precision + 1)} and the C{georef} 

340 resolution is I{15°} for B{C{precision}} 0:, I{1°} for 1, I{1′} for 2, 

341 I{0.1′} for 3, I{0.01′} for 4, ... up to I{10**(2 - precision)′}. 

342 ''' 

343 def _option(name, m, m2_, K): 

344 f = Scalar_(m, name=name, Error=WGRSError) 

345 return NN(name[0].upper(), int(m2_(f * K) + _0_5)) 

346 

347 g, _, _ = _encode3(lat, lon, precision) 

348 if radius is not None: # R before H 

349 g += _option(_radius_, radius, m2NM, _1_0) 

350 if height is not None: # H is last 

351 g += _option(_height_, height, m2ft, _0_001) 

352 return g 

353 

354 

355def _encode3(lat, lon, precision, h=None): 

356 '''Return 3-tuple C{(georef, (lat, lon), p)}. 

357 ''' 

358 p = _2Precision(precision) 

359 

360 lat, lon = _2fll(lat, lon) 

361 

362 if h is None: 

363 xt, xd, x = _divmod3( lon, _LonOrig_60B) 

364 yt, yd, y = _divmod3(_off90(lat), _LatOrig_60B) 

365 

366 g = _LonTile[xt], _LatTile[yt] 

367 if p > 0: 

368 g += _DegChar[xd], _DegChar[yd] 

369 p -= 1 

370 if p > 0: 

371 d = pow(_Base, _MaxPrec - p) 

372 x = _0wd(p, x // d) 

373 y = _0wd(p, y // d) 

374 g += x, y 

375 g = NN.join(g) 

376 else: 

377 g = encode(lat, lon, precision=p, height=h) 

378 

379 return g, (lat, lon), p # XXX Georef(''.join(g)) 

380 

381 

382def precision(res): 

383 '''Determine the L{Georef} precision to meet a required (geographic) 

384 resolution. 

385 

386 @arg res: The required resolution (C{degrees}). 

387 

388 @return: The L{Georef} precision (C{int} 0..11). 

389 

390 @raise ValueError: Invalid B{C{res}}. 

391 

392 @see: Function L{wgrs.encode} for more C{precision} details. 

393 ''' 

394 r = Scalar_(res=res) 

395 for p in range(_MaxPrec): 

396 if resolution(p) <= r: 

397 return p 

398 return _MaxPrec 

399 

400 

401def resolution(prec): 

402 '''Determine the (geographic) resolution of a given L{Georef} precision. 

403 

404 @arg prec: The given precision (C{int}). 

405 

406 @return: The (geographic) resolution (C{degrees}). 

407 

408 @raise ValueError: Invalid B{C{prec}}. 

409 

410 @see: Function L{wgrs.encode} for more C{precision} details. 

411 ''' 

412 p = Int(prec=prec, Error=WGRSError) 

413 if p > 1: 

414 p = min(p, _MaxPrec) - 1 

415 r = _1_0 / (pow(_Base, p) * _60_0) 

416 elif p < 1: 

417 r = _float_Tile 

418 else: 

419 r = _1_0 

420 return r 

421 

422 

423__all__ += _ALL_DOCS(decode3, decode5, # functions 

424 encode, precision, resolution) 

425 

426# **) MIT License 

427# 

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

429# 

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

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

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

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

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

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

436# 

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

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

439# 

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

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

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

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

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

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

446# OTHER DEALINGS IN THE SOFTWARE.