Coverage for pygeodesy/gars.py: 98%
142 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
2# -*- coding: utf-8 -*-
4u'''I{Global Area Reference System} (GARS) en-/decoding.
6Classes L{Garef} and L{GARSError} and several functions to encode,
7decode and inspect I{Global Area Reference System} (GARS) references.
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'''
16# from pygeodesy.basics import isstr # from .named
17from pygeodesy.constants import _off90, _1_over, _0_5, \
18 _1_0 # PYCHOK used!
19from pygeodesy.dms import parse3llh, Fmt # 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 _name__, isstr, 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
30from math import floor
32__all__ = _ALL_LAZY.gars
33__version__ = '24.05.24'
35_Digits = _0to9_
36_LatLen = 2
37_LatOrig = -90
38_Letters = _AtoZnoIO_
39_LonLen = 3
40_LonOrig = -180
41_MaxPrec = 2
43_MinLen = _LonLen + _LatLen
44_MaxLen = _MinLen + _MaxPrec
46_M1 = _M2 = 2
47_M3 = 3
48_M4 = _M1 * _M2 * _M3
50_LatOrig_M4 = _LatOrig * _M4
51_LatOrig_M1 = _LatOrig * _M1
52_LonOrig_M4 = _LonOrig * _M4
53_LonOrig_M1_1 = _LonOrig * _M1 - 1
55_Resolutions = _1_over(_M1), _1_over(_M1 * _M2), _1_over(_M4)
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
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))
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
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)
95 except (AttributeError, TypeError, ValueError) as x:
96 raise GARSError(Garef.__name__, garef, cause=x)
99def _2Precision(precision):
100 '''(INTERNAL) Return a L{Precision_} instance.
101 '''
102 return Precision_(precision, Error=GARSError, low=0, high=_MaxPrec)
105class GARSError(_ValueError):
106 '''Global Area Reference System (GARS) encode, decode or other L{Garef} issue.
107 '''
108 pass
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):
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}.
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 C{B{name}=NN} (C{str}).
126 @return: New L{Garef}.
128 @raise RangeError: Invalid B{C{cll}} lat- or longitude.
130 @raise TypeError: Invalid B{C{cll}}.
132 @raise GARSError: INValid or non-alphanumeric B{C{cll}}.
133 '''
134 ll = p = None
136 if isinstance(cll, Garef):
137 g, p = _2garstr2(str(cll))
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()
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
153 self = Str.__new__(cls, g, name=_name__(name, _or_nameof=cll))
154 self._latlon = ll
155 self._precision = p
156 return self
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)
165 @Property_RO
166 def _decoded3(self):
167 '''(INTERNAL) Initial L{LatLonPrec3Tuple}.
168 '''
169 return decode3(self)
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)
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
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.
189 @kwarg LatLon: Class to use (C{LatLon} or C{None}).
190 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
191 keyword arguments.
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))
200def decode3(garef, center=True, **name):
201 '''Decode a C{garef} to lat-, longitude and precision.
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}).
207 @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}.
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))
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
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
235 g, precision = _2garstr2(garef)
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)
244 if center: # ll = (ll * 2 + 1) / 2
245 lon += _0_5
246 lat += _0_5
248 n = _name__(name, _or_nameof=garef)
249 r = _Resolutions[precision] # == 1.0 / unit
250 return LatLonPrec3Tuple(Lat(lat * r, Error=GARSError),
251 Lon(lon * r, Error=GARSError),
252 precision, name=n)
255def encode(lat, lon, precision=1): # MCCABE 14
256 '''Encode a lat-/longitude as a C{garef} of the given precision.
258 @arg lat: Latitude (C{degrees}).
259 @arg lon: Longitude (C{degrees}).
260 @kwarg precision: Optional, the desired C{garef} resolution
261 and length (C{int} 0..2).
263 @return: The C{garef} (C{str}).
265 @raise RangeError: Invalid B{C{lat}} or B{C{lon}}.
267 @raise GARSError: Invalid B{C{precision}}.
269 @note: The C{garef} length is M{precision + 5} and the C{garef}
270 resolution is B{30′} for B{C{precision}} 0, B{15′} for 1
271 and B{5′} for 2, respectively.
272 '''
273 def _digit(x, y, m):
274 return _Digits[m * (m - y - 1) + x + 1],
276 def _str(chars, x, n):
277 s, b = [], len(chars)
278 for i in range(n):
279 x, i = divmod(x, b)
280 s.append(chars[i])
281 return tuple(reversed(s))
283 p = _2Precision(precision)
285 lat, lon = _2fll(lat, lon)
286 lat = _off90(lat)
288 ix, x = _2divmod2(lon, _LonOrig_M4)
289 iy, y = _2divmod2(lat, _LatOrig_M4)
291 g = _str(_Digits, ix + 1, _LonLen) + _str(_Letters, iy, _LatLen)
292 if p > 0:
293 ix, x = divmod(x, _M3)
294 iy, y = divmod(y, _M3)
295 g += _digit(ix, iy, _M2)
296 if p > 1:
297 g += _digit(x, y, _M3)
299 return NN.join(g)
302def precision(res):
303 '''Determine the L{Garef} precision to meet a required (geographic)
304 resolution.
306 @arg res: The required resolution (C{degrees}).
308 @return: The L{Garef} precision (C{int} 0..2).
310 @raise ValueError: Invalid B{C{res}}.
312 @see: Function L{gars.encode} for more C{precision} details.
313 '''
314 r = Scalar_(res=res)
315 for p in range(_MaxPrec):
316 if resolution(p) <= r:
317 return p
318 return _MaxPrec
321def resolution(prec):
322 '''Determine the (geographic) resolution of a given L{Garef} precision.
324 @arg prec: The given precision (C{int}).
326 @return: The (geographic) resolution (C{degrees}).
328 @raise GARSError: Invalid B{C{prec}}.
330 @see: Function L{gars.encode} for more C{precision} details.
331 '''
332 p = Int_(prec=prec, Error=GARSError, low=-1, high=_MaxPrec + 1)
333 return _Resolutions[max(0, min(p, _MaxPrec))]
336__all__ += _ALL_OTHER(decode3, # functions
337 encode, precision, resolution)
339# **) MIT License
340#
341# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
342#
343# Permission is hereby granted, free of charge, to any person obtaining a
344# copy of this software and associated documentation files (the "Software"),
345# to deal in the Software without restriction, including without limitation
346# the rights to use, copy, modify, merge, publish, distribute, sublicense,
347# and/or sell copies of the Software, and to permit persons to whom the
348# Software is furnished to do so, subject to the following conditions:
349#
350# The above copyright notice and this permission notice shall be included
351# in all copies or substantial portions of the Software.
352#
353# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
354# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
355# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
356# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
357# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
358# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
359# OTHER DEALINGS IN THE SOFTWARE.