Coverage for pygeodesy/gars.py: 97%

143 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-06-07 08:37 -0400

1 

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

3 

4u'''I{Global Area Reference System} (GARS) en-/decoding. 

5 

6Classes L{Garef} and L{GARSError} and several functions to encode, 

7decode and inspect I{Global Area Reference System} (GARS) references. 

8 

9Transcoded from C++ class U{GARS 

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

11by I{Charles Karney}. See also U{Global Area Reference System 

12<https://WikiPedia.org/wiki/Global_Area_Reference_System>} and U{NGA (GARS) 

13<https://Earth-Info.NGA.mil/GandG/coordsys/grids/gars.html>}. 

14''' 

15 

16# from pygeodesy.basics import isstr # from .dms 

17from pygeodesy.constants import _off90, _1_over, _0_5, \ 

18 _1_0 # PYCHOK used! 

19from pygeodesy.dms import Fmt, isstr, parse3llh # parseDMS2 

20from pygeodesy.errors import _ValueError, _xkwds 

21from pygeodesy.interns import NN, _0to9_, _AtoZnoIO_, _COMMA_ 

22from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER 

23from pygeodesy.named import nameof, Property_RO 

24from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple 

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

26# from pygeodesy.streprs import Fmt # from .dms 

27from pygeodesy.units import Int_, Lat, Lon, Precision_, Scalar_, \ 

28 Str, _xStrError 

29 

30from math import floor 

31 

32__all__ = _ALL_LAZY.gars 

33__version__ = '22.09.29' 

34 

35_Digits = _0to9_ 

36_LatLen = 2 

37_LatOrig = -90 

38_Letters = _AtoZnoIO_ 

39_LonLen = 3 

40_LonOrig = -180 

41_MaxPrec = 2 

42 

43_MinLen = _LonLen + _LatLen 

44_MaxLen = _MinLen + _MaxPrec 

45 

46_M1 = _M2 = 2 

47_M3 = 3 

48_M4 = _M1 * _M2 * _M3 

49 

50_LatOrig_M4 = _LatOrig * _M4 

51_LatOrig_M1 = _LatOrig * _M1 

52_LonOrig_M4 = _LonOrig * _M4 

53_LonOrig_M1_1 = _LonOrig * _M1 - 1 

54 

55_Resolutions = _1_over(_M1), _1_over(_M1 * _M2), _1_over(_M4) 

56 

57 

58def _2divmod2(ll, _Orig_M4): 

59 x = int(floor(ll * _M4)) - _Orig_M4 

60 i = (x * _M1) // _M4 

61 x -= i * _M4 // _M1 

62 return i, x 

63 

64 

65def _2fll(lat, lon, *unused): 

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

67 ''' 

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

69 return (Lat(lat, Error=GARSError), 

70 Lon(lon, Error=GARSError)) 

71 

72 

73# def _2Garef(garef): 

74# '''(INTERNAL) Check or create a L{Garef} instance. 

75# ''' 

76# if not isinstance(garef, Garef): 

77# try: 

78# garef = Garef(garef) 

79# except (TypeError, ValueError): 

80# raise _xStrError(Garef, Str, garef=garef) 

81# return garef 

82 

83 

84def _2garstr2(garef): 

85 '''(INTERNAL) Check a garef string. 

86 ''' 

87 try: 

88 n, garstr = len(garef), garef.upper() 

89 if n < _MinLen or n > _MaxLen \ 

90 or garstr[:3] == 'INV' \ 

91 or not garstr.isalnum(): 

92 raise ValueError 

93 return garstr, _2Precision(n - _MinLen) 

94 

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

96 raise GARSError(Garef.__name__, garef, cause=x) 

97 

98 

99def _2Precision(precision): 

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

101 ''' 

102 return Precision_(precision, Error=GARSError, low=0, high=_MaxPrec) 

103 

104 

105class GARSError(_ValueError): 

106 '''Global Area Reference System (GARS) encode, decode or other L{Garef} issue. 

107 ''' 

108 pass 

109 

110 

111class Garef(Str): 

112 '''Garef class, a named C{str}. 

113 ''' 

114 # no str.__init__ in Python 3 

115 def __new__(cls, cll, precision=1, name=NN): 

116 '''New L{Garef} from an other L{Garef} instance or garef 

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

118 

119 @arg cll: Cell or location (L{Garef} or C{str}, C{LatLon} 

120 or C{str}). 

121 @kwarg precision: Optional, the desired garef resolution 

122 and length (C{int} 0..2), see function 

123 L{gars.encode} for more details. 

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

125 

126 @return: New L{Garef}. 

127 

128 @raise RangeError: Invalid B{C{cll}} lat- or longitude. 

129 

130 @raise TypeError: Invalid B{C{cll}}. 

131 

132 @raise GARSError: INValid or non-alphanumeric B{C{cll}}. 

133 ''' 

134 ll = p = None 

135 

136 if isinstance(cll, Garef): 

137 g, p = _2garstr2(str(cll)) 

138 

139 elif isstr(cll): 

140 if _COMMA_ in cll: 

141 ll = _2fll(*parse3llh(cll)) 

142 g = encode(*ll, precision=precision) # PYCHOK false 

143 else: 

144 g = cll.upper() 

145 

146 else: # assume LatLon 

147 try: 

148 ll = _2fll(cll.lat, cll.lon) 

149 g = encode(*ll, precision=precision) # PYCHOK false 

150 except AttributeError: 

151 raise _xStrError(Garef, cll=cll) # Error=GARSError 

152 

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

154 self._latlon = ll 

155 self._precision = p 

156 return self 

157 

158 @Property_RO 

159 def decoded3(self): 

160 '''Get this garef'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 _decoded3(self): 

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

168 ''' 

169 return decode3(self) 

170 

171 @Property_RO 

172 def latlon(self): 

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

174 ''' 

175 lat, lon = self._latlon or self._decoded3[:2] 

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

177 

178 @Property_RO 

179 def precision(self): 

180 '''Get this garef's precision (C{int}). 

181 ''' 

182 p = self._precision 

183 return self._decoded3.precision if p is None else p 

184 

185 def toLatLon(self, LatLon, **LatLon_kwds): 

186 '''Return (the center of) this garef cell as an instance 

187 of the supplied C{LatLon} class. 

188 

189 @arg LatLon: Class to use (C{LatLon}). 

190 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} 

191 keyword arguments. 

192 

193 @return: This garef location (B{C{LatLon}}). 

194 

195 @raise GARSError: Invalid B{C{LatLon}}. 

196 ''' 

197 if LatLon is None: 

198 kwds = _xkwds(LatLon_kwds, LatLon=None, name=self.name) 

199 raise GARSError(**kwds) 

200 

201 return self._xnamed(LatLon(*self.latlon, **LatLon_kwds)) 

202 

203 

204def decode3(garef, center=True): 

205 '''Decode a C{garef} to lat-, longitude and precision. 

206 

207 @arg garef: To be decoded (L{Garef} or C{str}). 

208 @kwarg center: If C{True} the center, otherwise the south-west, 

209 lower-left corner (C{bool}). 

210 

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

212 

213 @raise GARSError: Invalid B{C{garef}}, INValid, non-alphanumeric 

214 or bad length B{C{garef}}. 

215 ''' 

216 def _Error(i): 

217 return GARSError(garef=Fmt.SQUARE(repr(garef), i)) 

218 

219 def _ll(chars, g, i, j, lo, hi): 

220 ll, b = 0, len(chars) 

221 for i in range(i, j): 

222 d = chars.find(g[i]) 

223 if d < 0: 

224 raise _Error(i) 

225 ll = ll * b + d 

226 if ll < lo or ll > hi: 

227 raise _Error(j) 

228 return ll 

229 

230 def _ll2(lon, lat, g, i, m): 

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

232 if d < 1 or d > m * m: 

233 raise _Error(i) 

234 d, r = divmod(d - 1, m) 

235 lon = lon * m + r 

236 lat = lat * m + (m - 1 - d) 

237 return lon, lat 

238 

239 g, precision = _2garstr2(garef) 

240 

241 lon = _ll(_Digits, g, 0, _LonLen, 1, 720) + _LonOrig_M1_1 

242 lat = _ll(_Letters, g, _LonLen, _MinLen, 0, 359) + _LatOrig_M1 

243 if precision > 0: 

244 lon, lat = _ll2(lon, lat, g, _MinLen, _M2) 

245 if precision > 1: 

246 lon, lat = _ll2(lon, lat, g, _MinLen + 1, _M3) 

247 

248 if center: # ll = (ll * 2 + 1) / 2 

249 lon += _0_5 

250 lat += _0_5 

251 

252 r = _Resolutions[precision] # == 1.0 / unit 

253 return LatLonPrec3Tuple(Lat(lat * r, Error=GARSError), 

254 Lon(lon * r, Error=GARSError), 

255 precision, name=nameof(garef)) 

256 

257 

258def encode(lat, lon, precision=1): # MCCABE 14 

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

260 

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

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

263 @kwarg precision: Optional, the desired C{garef} resolution 

264 and length (C{int} 0..2). 

265 

266 @return: The C{garef} (C{str}). 

267 

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

269 

270 @raise GARSError: Invalid B{C{precision}}. 

271 

272 @note: The C{garef} length is M{precision + 5} and the C{garef} 

273 resolution is B{30′} for B{C{precision}} 0, B{15′} for 1 

274 and B{5′} for 2, respectively. 

275 ''' 

276 def _digit(x, y, m): 

277 return _Digits[m * (m - y - 1) + x + 1], 

278 

279 def _str(chars, x, n): 

280 s, b = [], len(chars) 

281 for i in range(n): 

282 x, i = divmod(x, b) 

283 s.append(chars[i]) 

284 return tuple(reversed(s)) 

285 

286 p = _2Precision(precision) 

287 

288 lat, lon = _2fll(lat, lon) 

289 lat = _off90(lat) 

290 

291 ix, x = _2divmod2(lon, _LonOrig_M4) 

292 iy, y = _2divmod2(lat, _LatOrig_M4) 

293 

294 g = _str(_Digits, ix + 1, _LonLen) + _str(_Letters, iy, _LatLen) 

295 if p > 0: 

296 ix, x = divmod(x, _M3) 

297 iy, y = divmod(y, _M3) 

298 g += _digit(ix, iy, _M2) 

299 if p > 1: 

300 g += _digit(x, y, _M3) 

301 

302 return NN.join(g) 

303 

304 

305def precision(res): 

306 '''Determine the L{Garef} precision to meet a required (geographic) 

307 resolution. 

308 

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

310 

311 @return: The L{Garef} precision (C{int} 0..2). 

312 

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

314 

315 @see: Function L{gars.encode} for more C{precision} details. 

316 ''' 

317 r = Scalar_(res=res) 

318 for p in range(_MaxPrec): 

319 if resolution(p) <= r: 

320 return p 

321 return _MaxPrec 

322 

323 

324def resolution(prec): 

325 '''Determine the (geographic) resolution of a given L{Garef} precision. 

326 

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

328 

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

330 

331 @raise GARSError: Invalid B{C{prec}}. 

332 

333 @see: Function L{gars.encode} for more C{precision} details. 

334 ''' 

335 p = Int_(prec=prec, Error=GARSError, low=-1, high=_MaxPrec + 1) 

336 return _Resolutions[max(0, min(p, _MaxPrec))] 

337 

338 

339__all__ += _ALL_OTHER(decode3, # functions 

340 encode, precision, resolution) 

341 

342# **) MIT License 

343# 

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

345# 

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

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

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

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

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

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

352# 

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

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

355# 

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

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

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

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

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

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

362# OTHER DEALINGS IN THE SOFTWARE.