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/html/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
16from pygeodesy.basics import isstr
17from pygeodesy.dms import parse3llh  # parseDMS2
18from pygeodesy.errors import _ValueError, _xkwds
19from pygeodesy.interns import EPS1_2, NN, _AtoZnoIO_, \
20                             _floatuple, _0to9_, _0_5, _90_0
21from pygeodesy.interns import _1_0  # PYCHOK used!
22from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER
23from pygeodesy.named import nameof
24from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple
25from pygeodesy.props import Property_RO
26from pygeodesy.streprs import Fmt
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__ = '21.08.24'
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_M_ = _M1 * _M2 * _M3
49
50_LatOrig_M_ = _LatOrig * _M_
51_LonOrig_M_ = _LonOrig * _M_
52
53_LatOrig_M1   = _LatOrig * _M1
54_LonOrig_M1_1 = _LonOrig * _M1 - 1
55
56_Resolutions = _floatuple(*(_1_0 / _ for _ in (_M1, _M1 * _M2, _M_)))
57
58
59def _2divmod2(ll, Orig_M_):
60    x = int(floor(ll * _M_)) - Orig_M_
61    i = (x * _M1) // _M_
62    x -= i * _M_ // _M1
63    return i, x
64
65
66def _2fll(lat, lon, *unused):
67    '''(INTERNAL) Convert lat, lon.
68    '''
69    # lat, lon = parseDMS2(lat, lon)
70    return (Lat(lat, Error=GARSError),
71            Lon(lon, Error=GARSError))
72
73
74# def _2Garef(garef):
75#     '''(INTERNAL) Check or create a L{Garef} instance.
76#     '''
77#     if not isinstance(garef, Garef):
78#         try:
79#             garef = Garef(garef)
80#         except (TypeError, ValueError):
81#             raise _xStrError(Garef, Str, garef=garef)
82#     return garef
83
84
85def _2garstr2(garef):
86    '''(INTERNAL) Check a garef string.
87    '''
88    try:
89        n, garstr = len(garef), garef.upper()
90        if n < _MinLen or n > _MaxLen \
91                       or garstr[:3] == 'INV' \
92                       or not garstr.isalnum():
93            raise ValueError
94        return garstr, _2Precision(n - _MinLen)
95
96    except (AttributeError, TypeError, ValueError) as x:
97        raise GARSError(Garef.__name__, garef, txt=str(x))
98
99
100def _2Precision(precision):
101    '''(INTERNAL) Return a L{Precision_} instance.
102    '''
103    return Precision_(precision, Error=GARSError, low=0, high=_MaxPrec)
104
105
106class GARSError(_ValueError):
107    '''Global Area Reference System (GARS) encode, decode or other L{Garef} issue.
108    '''
109    pass
110
111
112class Garef(Str):
113    '''Garef class, a named C{str}.
114    '''
115    # no str.__init__ in Python 3
116    def __new__(cls, cll, precision=1, name=NN):
117        '''New L{Garef} from an other L{Garef} instance or garef
118           C{str} or from a C{LatLon} instance or lat-/longitude C{str}.
119
120           @arg cll: Cell or location (L{Garef} or C{str}, C{LatLon}
121                     or C{str}).
122           @kwarg precision: Optional, the desired garef resolution
123                             and length (C{int} 0..2), see function
124                             L{gars.encode} for more details.
125           @kwarg name: Optional name (C{str}).
126
127           @return: New L{Garef}.
128
129           @raise RangeError: Invalid B{C{cll}} lat- or longitude.
130
131           @raise TypeError: Invalid B{C{cll}}.
132
133           @raise GARSError: INValid or non-alphanumeric B{C{cll}}.
134        '''
135        ll = p = None
136
137        if isinstance(cll, Garef):
138            g, p = _2garstr2(str(cll))
139
140        elif isstr(cll):
141            if ',' in cll:
142                ll = _2fll(*parse3llh(cll))
143                g  =  encode(*ll, precision=precision)  # PYCHOK false
144            else:
145                g = cll.upper()
146
147        else:  # assume LatLon
148            try:
149                ll = _2fll(cll.lat, cll.lon)
150                g  =  encode(*ll, precision=precision)  # PYCHOK false
151            except AttributeError:
152                raise _xStrError(Garef, cll=cll)  # Error=GARSError
153
154        self = Str.__new__(cls, g, name=name or nameof(cll))
155        self._latlon    = ll
156        self._precision = p
157        return self
158
159    @Property_RO
160    def decoded3(self):
161        '''Get this garef's attributes (L{LatLonPrec3Tuple}).
162        '''
163        lat, lon = self.latlon
164        return LatLonPrec3Tuple(lat, lon, self.precision, name=self.name)
165
166    @Property_RO
167    def _decoded3(self):
168        '''(INTERNAL) Initial L{LatLonPrec5Tuple}.
169        '''
170        return decode3(self)
171
172    @Property_RO
173    def latlon(self):
174        '''Get this garef's (center) lat- and longitude (L{LatLon2Tuple}).
175        '''
176        lat, lon = self._latlon or self._decoded3[:2]
177        return LatLon2Tuple(lat, lon, name=self.name)
178
179    @Property_RO
180    def precision(self):
181        '''Get this garef's precision (C{int}).
182        '''
183        p = self._precision
184        return self._decoded3.precision if p is None else p
185
186    def toLatLon(self, LatLon, **LatLon_kwds):
187        '''Return (the center of) this garef cell as an instance
188           of the supplied C{LatLon} class.
189
190           @arg LatLon: Class to use (C{LatLon}).
191           @kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
192                               keyword arguments.
193
194           @return: This garef location (B{C{LatLon}}).
195
196           @raise GARSError: Invalid B{C{LatLon}}.
197        '''
198        if LatLon is None:
199            kwds = _xkwds(LatLon_kwds, LatLon=None, name=self.name)
200            raise GARSError(**kwds)
201
202        return self._xnamed(LatLon(*self.latlon, **LatLon_kwds))
203
204
205def decode3(garef, center=True):
206    '''Decode a C{garef} to lat-, longitude and precision.
207
208       @arg garef: To be decoded (L{Garef} or C{str}).
209       @kwarg center: If C{True} the center, otherwise the south-west,
210                      lower-left corner (C{bool}).
211
212       @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}.
213
214       @raise GARSError: Invalid B{C{garef}}, INValid, non-alphanumeric
215                         or bad length B{C{garef}}.
216    '''
217    def _Error(i):
218        return GARSError(garef=Fmt.SQUARE(repr(garef), i))
219
220    def _ll(chars, g, i, j, lo, hi):
221        ll, b = 0, len(chars)
222        for i in range(i, j):
223            d = chars.find(g[i])
224            if d < 0:
225                raise _Error(i)
226            ll = ll * b + d
227        if ll < lo or ll > hi:
228            raise _Error(j)
229        return ll
230
231    def _ll2(lon, lat, g, i, m):
232        d = _Digits.find(g[i])
233        if d < 1 or d > m * m:
234            raise _Error(i)
235        d, r = divmod(d - 1, m)
236        lon = lon * m + r
237        lat = lat * m + (m - 1 - d)
238        return lon, lat
239
240    g, precision = _2garstr2(garef)
241
242    lon = _ll(_Digits,  g,       0, _LonLen, 1, 720) + _LonOrig_M1_1
243    lat = _ll(_Letters, g, _LonLen, _MinLen, 0, 359) + _LatOrig_M1
244    if precision > 0:
245        lon, lat = _ll2(lon, lat, g, _MinLen, _M2)
246        if precision > 1:
247            lon, lat = _ll2(lon, lat, g, _MinLen + 1, _M3)
248
249    if center:  # ll = (ll * 2 + 1) / 2
250        lon += _0_5
251        lat += _0_5
252
253    r = _Resolutions[precision]  # == 1.0 / unit
254    return LatLonPrec3Tuple(Lat(lat * r, Error=GARSError),
255                            Lon(lon * r, Error=GARSError),
256                            precision, name=nameof(garef))
257
258
259def encode(lat, lon, precision=1):  # MCCABE 14
260    '''Encode a lat-/longitude as a C{garef} of the given precision.
261
262       @arg lat: Latitude (C{degrees}).
263       @arg lon: Longitude (C{degrees}).
264       @kwarg precision: Optional, the desired C{garef} resolution
265                         and length (C{int} 0..2).
266
267       @return: The C{garef} (C{str}).
268
269       @raise RangeError: Invalid B{C{lat}} or B{C{lon}}.
270
271       @raise GARSError: Invalid B{C{precision}}.
272
273       @note: The C{garef} length is M{precision + 5} and the C{garef}
274              resolution is B{30′} for B{C{precision}} 0, B{15′} for 1
275              and B{5′} for 2, respectively.
276    '''
277    def _digit(x, y, m):
278        return _Digits[m * (m - y - 1) + x + 1],
279
280    def _str(chars, x, n):
281        s, b = [], len(chars)
282        for i in range(n):
283            x, i = divmod(x, b)
284            s.append(chars[i])
285        return tuple(reversed(s))
286
287    p = _2Precision(precision)
288
289    lat, lon = _2fll(lat, lon)
290    if lat == _90_0:
291        lat *= EPS1_2
292
293    ix, x = _2divmod2(lon, _LonOrig_M_)
294    iy, y = _2divmod2(lat, _LatOrig_M_)
295
296    g = _str(_Digits, ix + 1, _LonLen) + _str(_Letters, iy, _LatLen)
297    if p > 0:
298        ix, x = divmod(x, _M3)
299        iy, y = divmod(y, _M3)
300        g += _digit(ix, iy, _M2)
301        if p > 1:
302            g += _digit(x, y, _M3)
303
304    return NN.join(g)
305
306
307def precision(res):
308    '''Determine the L{Garef} precision to meet a required (geographic)
309       resolution.
310
311       @arg res: The required resolution (C{degrees}).
312
313       @return: The L{Garef} precision (C{int} 0..2).
314
315       @raise ValueError: Invalid B{C{res}}.
316
317       @see: Function L{gars.encode} for more C{precision} details.
318    '''
319    r = Scalar_(res=res)
320    for p in range(_MaxPrec):
321        if resolution(p) <= r:
322            return p
323    return _MaxPrec
324
325
326def resolution(prec):
327    '''Determine the (geographic) resolution of a given L{Garef} precision.
328
329       @arg prec: The given precision (C{int}).
330
331       @return: The (geographic) resolution (C{degrees}).
332
333       @raise GARSError: Invalid B{C{prec}}.
334
335       @see: Function L{gars.encode} for more C{precision} details.
336    '''
337    p = Int_(prec=prec, Error=GARSError, low=-1, high=_MaxPrec + 1)
338    return _Resolutions[max(0, min(p, _MaxPrec))]
339
340
341__all__ += _ALL_OTHER(decode3,  # functions
342                      encode, precision, resolution)
343
344# **) MIT License
345#
346# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
347#
348# Permission is hereby granted, free of charge, to any person obtaining a
349# copy of this software and associated documentation files (the "Software"),
350# to deal in the Software without restriction, including without limitation
351# the rights to use, copy, modify, merge, publish, distribute, sublicense,
352# and/or sell copies of the Software, and to permit persons to whom the
353# Software is furnished to do so, subject to the following conditions:
354#
355# The above copyright notice and this permission notice shall be included
356# in all copies or substantial portions of the Software.
357#
358# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
359# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
360# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
361# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
362# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
363# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
364# OTHER DEALINGS IN THE SOFTWARE.
365