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