Coverage for pygeodesy/gars.py: 97%

1

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

3

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

5

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

7 GARS references.

8

9 Transcoded 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

17 from pygeodesy.basics import isstr, _splituple, typename

18 from pygeodesy.constants import _off90, _1_over, _0_5

19 from pygeodesy.errors import _ValueError, _xkwds, _xStrError

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

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

22 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY

23 from pygeodesy.named import _name__, Fmt, Property_RO

24 from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple

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

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

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

28

29 from math import floor

30

31 __all__ = _ALL_LAZY.gars

32 __version__ = '25.05.07'

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

57 def _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

64 def _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

83 def _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(typename(Garef), garef, cause=x)

96

97

98 def _2Precision(precision):

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

100 '''

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

102

103

104 class GARSError(_ValueError):

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

106 '''

107 pass

108

109

110 class 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

199 def 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

254 def 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

276 def _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

308 def 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

327 def 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-2026 -- 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.