1

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

3

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

5

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

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

8

9 Transcoded from C++ class U{GARS

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

11 by 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

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

18 _1_0 # PYCHOK used!

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

20 from pygeodesy.errors import _ValueError, _xkwds

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

22 from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER

23 from pygeodesy.named import nameof, 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 .dms

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

28 Str, _xStrError

29

30 from math import floor

31

32__all__ = _ALL_LAZY.gars

33__version__ = '23.10.07'

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

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

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

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

99 def _2Precision(precision):

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

101 '''

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

103

104

105 class GARSError(_ValueError):

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

107 '''

108 pass

109

110

111 class 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{LatLonPrec3Tuple}.

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=None, **LatLon_kwds):

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

187 of the supplied C{LatLon} class.

188

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

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

191 keyword arguments.

192

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

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

195 '''

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

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

198

199

200 def decode3(garef, center=True):

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

202

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

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

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

206

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

208

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

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

211 '''

212 def _Error(i):

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

214

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

216 ll, b = 0, len(chars)

217 for i in range(i, j):

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

219 if d < 0:

220 raise _Error(i)

221 ll = ll * b + d

222 if ll < lo or ll > hi:

223 raise _Error(j)

224 return ll

225

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

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

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

229 raise _Error(i)

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

231 lon = lon * m + r

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

233 return lon, lat

234

235 g, precision = _2garstr2(garef)

236

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

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

239 if precision > 0:

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

241 if precision > 1:

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

243

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

245 lon += _0_5

246 lat += _0_5

247

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

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

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

251 precision, name=nameof(garef))

252

253

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

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 def _digit(x, y, m):

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

274

275 def _str(chars, x, n):

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

277 for i in range(n):

278 x, i = divmod(x, b)

279 s.append(chars[i])

280 return tuple(reversed(s))

281

282 p = _2Precision(precision)

283

284 lat, lon = _2fll(lat, lon)

285 lat = _off90(lat)

286

287 ix, x = _2divmod2(lon, _LonOrig_M4)

288 iy, y = _2divmod2(lat, _LatOrig_M4)

289

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

291 if p > 0:

292 ix, x = divmod(x, _M3)

293 iy, y = divmod(y, _M3)

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

295 if p > 1:

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

297

298 return NN.join(g)

299

300

301 def precision(res):

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

303 resolution.

304

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

306

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

308

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

310

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

312 '''

313 r = Scalar_(res=res)

314 for p in range(_MaxPrec):

315 if resolution(p) <= r:

316 return p

317 return _MaxPrec

318

319

320 def resolution(prec):

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

322

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

324

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

326

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

328

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

330 '''

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

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

333

334

335__all__ += _ALL_OTHER(decode3, # functions

336 encode, precision, resolution)

337

338# **) MIT License

339#

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

341#

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

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

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

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

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

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

348#

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

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

351#

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

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

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

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

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

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

358# OTHER DEALINGS IN THE SOFTWARE.