Coverage for pygeodesy/wgrs.py: 97%
187 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
2# -*- coding: utf-8 -*-
4u'''World Geographic Reference System (WGRS) en-/decoding.
6Classes L{Georef} and L{WGRSError} and several functions to encode,
7decode and inspect WGRS references.
9Transcoded from I{Charles Karney}'s C++ class U{Georef
10<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Georef.html>},
11but with modified C{precision} and extended with C{height} and C{radius}. See
12also U{World Geographic Reference System
13<https://WikiPedia.org/wiki/World_Geographic_Reference_System>}.
14'''
15# from pygeodesy.basics import isstr # from .named
16from pygeodesy.constants import INT0, _float, _off90, _0_001, \
17 _0_5, _1_0, _2_0, _60_0, _1000_0
18from pygeodesy.dms import parse3llh # parseDMS2
19from pygeodesy.errors import _ValueError, _xattr, _xStrError
20from pygeodesy.interns import NN, _0to9_, _AtoZnoIO_, _COMMA_, \
21 _height_, _radius_, _SPACE_
22from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER
23from pygeodesy.named import _name2__, nameof, isstr, Property_RO
24from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple
25# from pygeodesy.props import Property_RO # from .named
26from pygeodesy.streprs import Fmt, _0wd
27from pygeodesy.units import Height, Int, Lat, Lon, Precision_, \
28 Radius, Scalar_, Str
29from pygeodesy.utily import ft2m, m2ft, m2NM
31from math import floor
33__all__ = _ALL_LAZY.wgrs
34__version__ = '24.06.15'
36_Base = 10
37_BaseLen = 4
38_DegChar = _AtoZnoIO_.tillQ
39_Digits = _0to9_
40_Height_ = Height.__name__
41_INV_ = 'INV' # INValid
42_LatOrig = -90
43_LatTile = _AtoZnoIO_.tillM
44_LonOrig = -180
45_LonTile = _AtoZnoIO_
46_60B = 60000000000 # == 60_000_000_000 == 60e9
47_MaxPrec = 11
48_Radius_ = Radius.__name__
49_Tile = 15 # tile size in degrees
51_MaxLen = _BaseLen + 2 * _MaxPrec
52_MinLen = _BaseLen - 2
54_LatOrig_60B = _LatOrig * _60B
55_LonOrig_60B = _LonOrig * _60B
57_float_Tile = _float(_Tile)
58_LatOrig_Tile = _float(_LatOrig) / _Tile
59_LonOrig_Tile = _float(_LonOrig) / _Tile
62def _divmod3(x, _Orig_60B):
63 '''(INTERNAL) Convert B{C{x}} to 3_tuple C{(tile, modulo, fraction)}/
64 '''
65 i = int(floor(x * _60B))
66 i, x = divmod(i - _Orig_60B, _60B)
67 xt, xd = divmod(i, _Tile)
68 return xt, xd, x
71def _fllh3(lat, lon, height=None):
72 '''(INTERNAL) Convert lat, lon, height.
73 '''
74 # lat, lon = parseDMS2(lat, lon)
75 return (Lat(lat, Error=WGRSError),
76 Lon(lon, Error=WGRSError), height)
79def _geostr2(georef):
80 '''(INTERNAL) Check a georef string.
81 '''
82 try:
83 n, geostr = len(georef), georef.upper()
84 p, o = divmod(n, 2)
85 if o or n < _MinLen or n > _MaxLen \
86 or geostr[:3] == _INV_ \
87 or not geostr.isalnum():
88 raise ValueError
89 return geostr, _Precision(p - 1)
91 except (AttributeError, TypeError, ValueError) as x:
92 raise WGRSError(Georef.__name__, georef, cause=x)
95def _Precision(precision):
96 '''(INTERNAL) Return a L{Precision_} instance.
97 '''
98 return Precision_(precision, Error=WGRSError, low=0, high=_MaxPrec)
101class WGRSError(_ValueError):
102 '''World Geographic Reference System (WGRS) encode, decode or other L{Georef} issue.
103 '''
104 pass
107class Georef(Str):
108 '''Georef class, a named C{str}.
109 '''
110 # no str.__init__ in Python 3
111 def __new__(cls, cll, precision=3, name=NN):
112 '''New L{Georef} from an other L{Georef} instance or georef
113 C{str} or from a C{LatLon} instance or lat-/longitude C{str}.
115 @arg cll: Cell or location (L{Georef} or C{str}, C{LatLon}
116 or C{str}).
117 @kwarg precision: Optional, the desired georef resolution
118 and length (C{int} 0..11), see function
119 L{wgrs.encode} for more details.
120 @kwarg name: Optional name (C{str}).
122 @return: New L{Georef}.
124 @raise RangeError: Invalid B{C{cll}} lat- or longitude.
126 @raise TypeError: Invalid B{C{cll}}.
128 @raise WGRSError: INValid or non-alphanumeric B{C{cll}}.
129 '''
130 ll = p = None
132 if isinstance(cll, Georef):
133 g, p = _geostr2(str(cll))
135 elif isstr(cll):
136 if _COMMA_ in cll:
137 lat, lon, h = _fllh3(*parse3llh(cll, height=None))
138 g = encode(lat, lon, precision=precision, height=h) # PYCHOK false
139 ll = lat, lon # original lat, lon
140 else:
141 g = cll.upper()
143 else: # assume LatLon
144 try:
145 lat, lon, h = _fllh3(cll.lat, cll.lon)
146 except AttributeError:
147 raise _xStrError(Georef, cll=cll) # Error=WGRSError
148 h = _xattr(cll, height=h)
149 g = encode(lat, lon, precision=precision, height=h) # PYCHOK false
150 ll = lat, lon # original lat, lon
152 self = Str.__new__(cls, g, name=name or nameof(cll))
153 self._latlon = ll
154 self._precision = p
155 return self
157 @Property_RO
158 def decoded3(self):
159 '''Get this georef's attributes (L{LatLonPrec3Tuple}).
160 '''
161 lat, lon = self.latlon
162 return LatLonPrec3Tuple(lat, lon, self.precision, name=self.name)
164 @Property_RO
165 def decoded5(self):
166 '''Get this georef's attributes (L{LatLonPrec5Tuple}) with
167 height and radius set to C{None} if missing.
168 '''
169 return self.decoded3.to5Tuple(self.height, self.radius)
171 @Property_RO
172 def _decoded5(self):
173 '''(INTERNAL) Initial L{LatLonPrec5Tuple}.
174 '''
175 return decode5(self)
177 @Property_RO
178 def height(self):
179 '''Get this georef's height in C{meter} or C{None} if missing.
180 '''
181 return self._decoded5.height
183 @Property_RO
184 def latlon(self):
185 '''Get this georef's (center) lat- and longitude (L{LatLon2Tuple}).
186 '''
187 lat, lon = self._latlon or self._decoded5[:2]
188 return LatLon2Tuple(lat, lon, name=self.name)
190 @Property_RO
191 def latlonheight(self):
192 '''Get this georef's (center) lat-, longitude and height (L{LatLon3Tuple}),
193 with height set to C{INT0} if missing.
194 '''
195 return self.latlon.to3Tuple(self.height or INT0)
197 @Property_RO
198 def precision(self):
199 '''Get this georef's precision (C{int}).
200 '''
201 p = self._precision
202 return self._decoded5.precision if p is None else p
204 @Property_RO
205 def radius(self):
206 '''Get this georef's radius in C{meter} or C{None} if missing.
207 '''
208 return self._decoded5.radius
210 def toLatLon(self, LatLon=None, height=None, **name_LatLon_kwds):
211 '''Return (the center of) this georef cell as a C{LatLon}.
213 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.
214 @kwarg height: Optional height (C{meter}), overriding this height.
215 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} (C{str}) and optional,
216 additional B{C{LatLon}} keyword arguments, ignored if
217 C{B{LatLon} is None}.
219 @return: This georef location (B{C{LatLon}}) or if C{B{LatLon} is None},
220 a L{LatLon3Tuple}C{(lat, lon, height)}.
222 @raise TypeError: Invalid B{C{LatLon}} or B{C{name_LatLon_kwds}}.
223 '''
224 n, kwds = _name2__(name_LatLon_kwds, _or_nameof=self)
225 h = (self.height or INT0) if height is None else height # _heigHt
226 r = self.latlon.to3Tuple(h) if LatLon is None else LatLon(
227 *self.latlon, height=h, **kwds)
228 return r.renamed(n) if n else r
231def decode3(georef, center=True):
232 '''Decode a C{georef} to lat-, longitude and precision.
234 @arg georef: To be decoded (L{Georef} or C{str}).
235 @kwarg center: If C{True} the center, otherwise the south-west,
236 lower-left corner (C{bool}).
238 @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}.
240 @raise WGRSError: Invalid B{C{georef}}, INValid, non-alphanumeric
241 or odd length B{C{georef}}.
242 '''
243 def _digit(ll, g, i, m):
244 d = _Digits.find(g[i])
245 if d < 0 or d >= m:
246 raise _Error(i)
247 return ll * m + d
249 def _Error(i):
250 return WGRSError(Fmt.SQUARE(georef=i), georef)
252 def _index(chars, g, i):
253 k = chars.find(g[i])
254 if k < 0:
255 raise _Error(i)
256 return k
258 g, precision = _geostr2(georef)
259 lon = _index(_LonTile, g, 0) + _LonOrig_Tile
260 lat = _index(_LatTile, g, 1) + _LatOrig_Tile
262 u = _1_0
263 if precision > 0:
264 lon = lon * _Tile + _index(_DegChar, g, 2)
265 lat = lat * _Tile + _index(_DegChar, g, 3)
266 m, p = 6, precision - 1
267 for i in range(_BaseLen, _BaseLen + p):
268 lon = _digit(lon, g, i, m)
269 lat = _digit(lat, g, i + p, m)
270 u *= m
271 m = _Base
272 u *= _Tile
274 if center:
275 lon = lon * _2_0 + _1_0
276 lat = lat * _2_0 + _1_0
277 u *= _2_0
278 u = _Tile / u
279 return LatLonPrec3Tuple(Lat(lat * u, Error=WGRSError),
280 Lon(lon * u, Error=WGRSError),
281 precision, name=nameof(georef))
284def decode5(georef, center=True):
285 '''Decode a C{georef} to lat-, longitude, precision, height and radius.
287 @arg georef: To be decoded (L{Georef} or C{str}).
288 @kwarg center: If C{True} the center, otherwise the south-west,
289 lower-left corner (C{bool}).
291 @return: A L{LatLonPrec5Tuple}C{(lat, lon,
292 precision, height, radius)} where C{height} and/or
293 C{radius} are C{None} if missing.
295 @raise WGRSError: Invalid B{C{georef}}, INValid, non-alphanumeric
296 or odd length B{C{georef}}.
297 '''
298 def _h2m(kft, name):
299 return Height(ft2m(kft * _1000_0), name=name, Error=WGRSError)
301 def _r2m(NM, name):
302 return Radius(NM / m2NM(1), name=name, Error=WGRSError)
304 def _split2(g, name, _2m):
305 i = max(g.find(name[0]), g.rfind(name[0]))
306 if i > _BaseLen:
307 return g[:i], _2m(int(g[i+1:]), _SPACE_(georef, name))
308 else:
309 return g, None
311 g = Str(georef, Error=WGRSError)
313 g, h = _split2(g, _Height_, _h2m) # H is last
314 g, r = _split2(g, _Radius_, _r2m) # R before H
316 return decode3(g, center=center).to5Tuple(h, r)
319def encode(lat, lon, precision=3, height=None, radius=None): # MCCABE 14
320 '''Encode a lat-/longitude as a C{georef} of the given precision.
322 @arg lat: Latitude (C{degrees}).
323 @arg lon: Longitude (C{degrees}).
324 @kwarg precision: Optional, the desired C{georef} resolution and length
325 (C{int} 0..11).
326 @kwarg height: Optional, height in C{meter}, see U{Designation of area
327 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}.
328 @kwarg radius: Optional, radius in C{meter}, see U{Designation of area
329 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}.
331 @return: The C{georef} (C{str}).
333 @raise RangeError: Invalid B{C{lat}} or B{C{lon}}.
335 @raise WGRSError: Invalid B{C{precision}}, B{C{height}} or B{C{radius}}.
337 @note: The B{C{precision}} value differs from U{Georef<https://
338 GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Georef.html>}.
339 The C{georef} length is M{2 * (precision + 1)} and the
340 C{georef} resolution is I{15°} for B{C{precision}} 0, I{1°}
341 for 1, I{1′} for 2, I{0.1′} for 3, I{0.01′} for 4, ...
342 M{10**(2 - precision)}.
343 '''
344 def _option(name, m, m2_, K):
345 f = Scalar_(m, name=name, Error=WGRSError)
346 return NN(name[0].upper(), int(m2_(f * K) + _0_5))
348 p = _Precision(precision)
350 lat, lon, _ = _fllh3(lat, lon)
351 lat = _off90(lat)
353 xt, xd, x = _divmod3(lon, _LonOrig_60B)
354 yt, yd, y = _divmod3(lat, _LatOrig_60B)
356 g = _LonTile[xt], _LatTile[yt]
357 if p > 0:
358 g += _DegChar[xd], _DegChar[yd]
359 p -= 1
360 if p > 0:
361 d = pow(_Base, _MaxPrec - p)
362 x = _0wd(p, x // d)
363 y = _0wd(p, y // d)
364 g += x, y
366 if radius is not None: # R before H
367 g += _option(_radius_, radius, m2NM, _1_0),
368 if height is not None: # H is last
369 g += _option(_height_, height, m2ft, _0_001),
371 return NN.join(g) # XXX Georef(''.join(g))
374def precision(res):
375 '''Determine the L{Georef} precision to meet a required (geographic)
376 resolution.
378 @arg res: The required resolution (C{degrees}).
380 @return: The L{Georef} precision (C{int} 0..11).
382 @raise ValueError: Invalid B{C{res}}.
384 @see: Function L{wgrs.encode} for more C{precision} details.
385 '''
386 r = Scalar_(res=res)
387 for p in range(_MaxPrec):
388 if resolution(p) <= r:
389 return p
390 return _MaxPrec
393def resolution(prec):
394 '''Determine the (geographic) resolution of a given L{Georef} precision.
396 @arg prec: The given precision (C{int}).
398 @return: The (geographic) resolution (C{degrees}).
400 @raise ValueError: Invalid B{C{prec}}.
402 @see: Function L{wgrs.encode} for more C{precision} details.
403 '''
404 p = Int(prec=prec, Error=WGRSError)
405 if p > 1:
406 r = _1_0 / (_60_0 * pow(_Base, min(p, _MaxPrec) - 1))
407 elif p < 1:
408 r = _float_Tile
409 else:
410 r = _1_0
411 return r
414__all__ += _ALL_OTHER(decode3, decode5, # functions
415 encode, precision, resolution)
417# **) MIT License
418#
419# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
420#
421# Permission is hereby granted, free of charge, to any person obtaining a
422# copy of this software and associated documentation files (the "Software"),
423# to deal in the Software without restriction, including without limitation
424# the rights to use, copy, modify, merge, publish, distribute, sublicense,
425# and/or sell copies of the Software, and to permit persons to whom the
426# Software is furnished to do so, subject to the following conditions:
427#
428# The above copyright notice and this permission notice shall be included
429# in all copies or substantial portions of the Software.
430#
431# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
432# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
433# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
434# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
435# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
436# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
437# OTHER DEALINGS IN THE SOFTWARE.