Coverage for pygeodesy/gars.py: 97%

144 statements  

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

1 

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

3 

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

5 

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

7GARS references. 

8 

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

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

11 

12@see: U{Global Area Reference System 

13 <https://WikiPedia.org/wiki/Global_Area_Reference_System>} and U{NGA 

14 (GARS)<https://Earth-Info.NGA.mil/GandG/coordsys/grids/gars.html>}. 

15''' 

16 

17from pygeodesy.basics import isstr, _splituple 

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

19 _1_0 # PYCHOK used! 

20from pygeodesy.errors import _ValueError, _xkwds, _xStrError 

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

22from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY 

23from pygeodesy.named import _name__, Fmt, Property_RO 

24from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple 

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

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

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

28 

29from math import floor 

30 

31__all__ = _ALL_LAZY.gars 

32__version__ = '25.01.15' 

33 

34_Digits = _0to9_ 

35_LatLen = 2 

36_LatOrig = -90 

37_Letters = _AtoZnoIO_ 

38_LonLen = 3 

39_LonOrig = -180 

40_MaxPrec = 2 

41 

42_MinLen = _LonLen + _LatLen 

43_MaxLen = _MinLen + _MaxPrec 

44 

45_M1 = _M2 = 2 

46_M3 = 3 

47_M4 = _M1 * _M2 * _M3 

48 

49_LatOrig_M4 = _LatOrig * _M4 

50_LatOrig_M1 = _LatOrig * _M1 

51_LonOrig_M4 = _LonOrig * _M4 

52_LonOrig_M1_1 = _LonOrig * _M1 - 1 

53 

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

55 

56 

57def _2divmod2(ll, _Orig_M4): 

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

59 i = (x * _M1) // _M4 

60 x -= i * _M4 // _M1 

61 return i, x 

62 

63 

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

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

66 ''' 

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

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

69 Lon(lon, Error=GARSError)) 

70 

71 

72# def _2Garef(garef): 

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

74# ''' 

75# if not isinstance(garef, Garef): 

76# try: 

77# garef = Garef(garef) 

78# except (TypeError, ValueError): 

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

80# return garef 

81 

82 

83def _2garstr2(garef): 

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

85 ''' 

86 try: 

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

88 if n < _MinLen or n > _MaxLen \ 

89 or garstr.startswith(_INV_) \ 

90 or not garstr.isalnum(): 

91 raise ValueError() 

92 return garstr, _2Precision(n - _MinLen) 

93 

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

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

96 

97 

98def _2Precision(precision): 

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

100 ''' 

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

102 

103 

104class GARSError(_ValueError): 

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

106 ''' 

107 pass 

108 

109 

110class Garef(Str): 

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

112 ''' 

113 # no str.__init__ in Python 3 

114 def __new__(cls, lat_gll, lon=None, precision=1, **name): 

115 '''New L{Garef} from an other L{Garef} instance or garef C{str} 

116 or from a lat- and longitude. 

117 

118 @arg lat_gll: Latitude (C{degrees90}), a garef (L{Garef}, 

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

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

121 is C{degrees90}, ignored otherwise. 

122 @kwarg precision: The desired garef resolution and length (C{int} 

123 0..2), see L{encode<pygeodesy.gars.encode>}. 

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

125 

126 @return: New L{Garef}. 

127 

128 @raise GARSError: INValid B{C{lat_gll}}. 

129 

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

131 

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

133 ''' 

134 if lon is None: 

135 if isinstance(lat_gll, Garef): 

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

137 elif isstr(lat_gll): 

138 ll = _splituple(lat_gll) 

139 if len(ll) > 1: 

140 g, ll, p = _encode3(ll[0], ll[1], precision) 

141 else: 

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

143 _, p = _2garstr2(g) # validate 

144 else: # assume LatLon 

145 try: 

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

147 except AttributeError: 

148 raise _xStrError(Garef, gll=lat_gll, Error=GARSError) 

149 else: 

150 g, ll, p = _encode3(lat_gll, lon, precision) 

151 

152 self = Str.__new__(cls, g, name=_name__(name, _or_nameof=lat_gll)) 

153 self._latlon = ll 

154 self._precision = p 

155 return self 

156 

157 @Property_RO 

158 def decoded3(self): 

159 '''Get this garef's attributes (L{LatLonPrec3Tuple}). 

160 ''' 

161 lat, lon = self.latlon 

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

163 

164 @Property_RO 

165 def _decoded3(self): 

166 '''(INTERNAL) Initial L{LatLonPrec3Tuple}. 

167 ''' 

168 return decode3(self) 

169 

170 @Property_RO 

171 def latlon(self): 

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

173 ''' 

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

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

176 

177 @Property_RO 

178 def precision(self): 

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

180 ''' 

181 p = self._precision 

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

183 

184 def toLatLon(self, LatLon=None, **LatLon_kwds): 

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

186 of the supplied C{LatLon} class. 

187 

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

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

190 keyword arguments. 

191 

192 @return: This garef location as B{C{LatLon}} or if 

193 C{B{LatLon} is None} as L{LatLonPrec3Tuple}. 

194 ''' 

195 return self.decoded3 if LatLon is None else LatLon( 

196 *self.latlon, **_xkwds(LatLon_kwds, name=self.name)) 

197 

198 

199def decode3(garef, center=True, **name): 

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

201 

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

203 @kwarg center: If C{True}, use the garef's center, otherwise 

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

205 

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

207 

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

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

210 ''' 

211 def _Error(i): 

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

213 

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

215 ll, b = 0, len(chars) 

216 for i in range(i, j): 

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

218 if d < 0: 

219 raise _Error(i) 

220 ll = ll * b + d 

221 if ll < lo or ll > hi: 

222 raise _Error(j) 

223 return ll 

224 

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

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

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

228 raise _Error(i) 

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

230 lon = lon * m + r 

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

232 return lon, lat 

233 

234 g, precision = _2garstr2(garef) 

235 

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

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

238 if precision > 0: 

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

240 if precision > 1: 

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

242 

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

244 lon += _0_5 

245 lat += _0_5 

246 

247 n = _name__(name, _or_nameof=garef) 

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

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

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

251 precision, name=n) 

252 

253 

254def encode(lat, lon, precision=1): 

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

256 

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

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

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

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

261 

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

263 

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

265 

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

267 

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

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

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

271 ''' 

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

273 return g 

274 

275 

276def _encode3(lat, lon, precision): # MCCABE 14 

277 '''Return 3-tuple C{(garef, (lat, lon), p)}. 

278 ''' 

279 def _digit(x, y, m): 

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

281 

282 def _str(chars, x, n): 

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

284 for i in range(n): 

285 x, i = divmod(x, b) 

286 s.append(chars[i]) 

287 return tuple(reversed(s)) 

288 

289 p = _2Precision(precision) 

290 

291 lat, lon = _2fll(lat, lon) 

292 

293 ix, x = _2divmod2( lon, _LonOrig_M4) 

294 iy, y = _2divmod2(_off90(lat), _LatOrig_M4) 

295 

296 g = _str(_Digits, ix + 1, _LonLen) + \ 

297 _str(_Letters, iy, _LatLen) 

298 if p > 0: 

299 ix, x = divmod(x, _M3) 

300 iy, y = divmod(y, _M3) 

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

302 if p > 1: 

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

304 

305 return NN.join(g), (lat, lon), p 

306 

307 

308def precision(res): 

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

310 resolution. 

311 

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

313 

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

315 

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

317 

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

319 ''' 

320 r = Scalar_(res=res) 

321 for p in range(_MaxPrec): 

322 if resolution(p) <= r: 

323 return p 

324 return _MaxPrec 

325 

326 

327def resolution(prec): 

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

329 

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

331 

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

333 

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

335 

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

337 ''' 

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

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

340 

341 

342__all__ += _ALL_DOCS(decode3, # functions 

343 encode, precision, resolution) 

344 

345# **) MIT License 

346# 

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

348# 

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

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

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

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

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

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

355# 

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

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

358# 

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

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

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

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

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

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

365# OTHER DEALINGS IN THE SOFTWARE.