Coverage for pygeodesy/wgrs.py: 97%
190 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
2# -*- coding: utf-8 -*-
4u'''World Geographic Reference System (WGRS) en-/decoding, aka GEOREF.
6Class L{Georef} and several functions to encode, decode and inspect WGRS
7(or GEOREF) 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}.
13@see: U{World Geographic Reference System
14 <https://WikiPedia.org/wiki/World_Geographic_Reference_System>}.
15'''
16from pygeodesy.basics import isstr, typename
17from pygeodesy.constants import INT0, _float, _off90, _0_001, \
18 _0_5, _1_0, _2_0, _60_0, _1000_0
19from pygeodesy.dms import parse3llh
20from pygeodesy.errors import _ValueError, _xattr, _xStrError
21# from pygeodesy.internals import typename # from .basics
22from pygeodesy.interns import NN, _0to9_, _AtoZnoIO_, _COMMA_, \
23 _height_, _INV_, _radius_, _SPACE_
24from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY
25from pygeodesy.named import _name2__, nameof, Property_RO
26from pygeodesy.namedTuples import LatLon2Tuple, LatLonPrec3Tuple
27# from pygeodesy.props import Property_RO # from .named
28from pygeodesy.streprs import Fmt, _0wd
29from pygeodesy.units import Height, Int, Lat, Lon, Precision_, \
30 Radius, Scalar_, Str
31from pygeodesy.utily import ft2m, m2ft, m2NM
33from math import floor
35__all__ = _ALL_LAZY.wgrs
36__version__ = '25.04.14'
38_Base = 10
39_BaseLen = 4
40_DegChar = _AtoZnoIO_.tillQ
41_Digits = _0to9_
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_Tile = 15 # tile size in degrees
50_MaxLen = _BaseLen + 2 * _MaxPrec
51_MinLen = _BaseLen - 2
53_LatOrig_60B = _LatOrig * _60B
54_LonOrig_60B = _LonOrig * _60B
56_float_Tile = _float(_Tile)
57_LatOrig_Tile = _float(_LatOrig) / _Tile
58_LonOrig_Tile = _float(_LonOrig) / _Tile
61def _divmod3(x, _Orig_60B):
62 '''(INTERNAL) Convert B{C{x}} to 3_tuple C{(tile, modulo, fraction)}/
63 '''
64 i = int(floor(x * _60B))
65 i, x = divmod(i - _Orig_60B, _60B)
66 xt, xd = divmod(i, _Tile)
67 return xt, xd, x
70def _2fll(lat, lon):
71 '''(INTERNAL) Convert lat, lon.
72 '''
73 # lat, lon = parseDMS2(lat, lon)
74 return (Lat(lat, Error=WGRSError),
75 Lon(lon, Error=WGRSError))
78def _2geostr2(georef):
79 '''(INTERNAL) Check a georef string.
80 '''
81 try:
82 n, g = len(georef), georef.upper()
83 p, o = divmod(n, 2)
84 if o or n < _MinLen or n > _MaxLen \
85 or g.startswith(_INV_) or not g.isalnum():
86 raise ValueError()
87 return g, _2Precision(p - 1)
89 except (AttributeError, TypeError, ValueError) as x:
90 raise WGRSError(typename(Georef), georef, cause=x)
93def _2Precision(precision):
94 '''(INTERNAL) Return a L{Precision_} instance.
95 '''
96 return Precision_(precision, Error=WGRSError, low=0, high=_MaxPrec)
99class WGRSError(_ValueError):
100 '''World Geographic Reference System (WGRS) encode, decode or other L{Georef} issue.
101 '''
102 pass
105class Georef(Str):
106 '''Georef class, a named C{str}.
107 '''
108 # no str.__init__ in Python 3
109 def __new__(cls, lat_gll, lon=None, height=None, precision=3, name=NN):
110 '''New L{Georef} from an other L{Georef} instance or georef
111 C{str} or from a C{LatLon} instance or lat-/longitude C{str}.
113 @arg lat_gll: Latitude (C{degrees90}), a georef (L{Georef},
114 C{str}) or a location (C{LatLon}, C{LatLon*Tuple}).
115 @kwarg lon: Logitude (C{degrees180)}, required if B{C{lat_gll}}
116 is C{degrees90}, ignored otherwise.
117 @kwarg height: Optional height in C{meter}, used if B{C{lat_gll}}
118 is a location.
119 @kwarg precision: The desired georef resolution and length (C{int}
120 0..11), see L{encode<pygeodesy.wgrs.encode>}.
121 @kwarg name: Optional name (C{str}).
123 @return: New L{Georef}.
125 @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}.
127 @raise TypeError: Invalid B{C{lat_gll}} or B{C{lon}}.
129 @raise WGRSError: INValid B{C{lat_gll}}.
130 '''
131 if lon is None:
132 if isinstance(lat_gll, Georef):
133 g, ll, p = str(lat_gll), lat_gll.latlon, lat_gll.precision
134 elif isstr(lat_gll):
135 if _COMMA_ in lat_gll or _SPACE_ in lat_gll:
136 lat, lon, h = parse3llh(lat_gll, height=height)
137 g, ll, p = _encode3(lat, lon, precision, h=h)
138 else:
139 g, ll = lat_gll.upper(), None
140 try:
141 _, p = _2geostr2(g) # validate
142 except WGRSError: # R00H00?
143 p = None # = decode5(g).precision?
144 else: # assume LatLon
145 try:
146 g, ll, p = _encode3(lat_gll.lat, lat_gll.lon, precision,
147 h=_xattr(lat_gll, height=height))
148 except AttributeError:
149 raise _xStrError(Georef, gll=lat_gll) # Error=WGRSError
150 else:
151 g, ll, p = _encode3(lat_gll, lon, precision, h=height)
153 self = Str.__new__(cls, g, name=name or nameof(lat_gll))
154 self._latlon = ll
155 self._precision = p
156 return self
158 @Property_RO
159 def decoded3(self):
160 '''Get this georef'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 decoded5(self):
167 '''Get this georef's attributes (L{LatLonPrec5Tuple}) with
168 height and radius set to C{None} if missing.
169 '''
170 return self.decoded3.to5Tuple(self.height, self.radius)
172 @Property_RO
173 def _decoded5(self):
174 '''(INTERNAL) Initial L{LatLonPrec5Tuple}.
175 '''
176 return decode5(self)
178 @Property_RO
179 def height(self):
180 '''Get this georef's height in C{meter} or C{None} if missing.
181 '''
182 return self._decoded5.height
184 @Property_RO
185 def latlon(self):
186 '''Get this georef's (center) lat- and longitude (L{LatLon2Tuple}).
187 '''
188 lat, lon = self._latlon or self._decoded5[:2]
189 return LatLon2Tuple(lat, lon, name=self.name)
191 @Property_RO
192 def latlonheight(self):
193 '''Get this georef's (center) lat-, longitude and height (L{LatLon3Tuple}),
194 with height set to C{INT0} if missing.
195 '''
196 return self.latlon.to3Tuple(self.height or INT0)
198 @Property_RO
199 def precision(self):
200 '''Get this georef's precision (C{int}).
201 '''
202 p = self._precision
203 return self._decoded5.precision if p is None else p
205 @Property_RO
206 def radius(self):
207 '''Get this georef's radius in C{meter} or C{None} if missing.
208 '''
209 return self._decoded5.radius
211 def toLatLon(self, LatLon=None, height=None, **name_LatLon_kwds):
212 '''Return (the center of) this georef cell as a C{LatLon}.
214 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.
215 @kwarg height: Optional height (C{meter}), overriding this height.
216 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} (C{str}) and optionally,
217 additional B{C{LatLon}} keyword arguments, ignored if C{B{LatLon}
218 is None}.
220 @return: This georef location (B{C{LatLon}}) or if C{B{LatLon} is None},
221 a L{LatLon3Tuple}C{(lat, lon, height)}.
223 @raise TypeError: Invalid B{C{LatLon}} or B{C{name_LatLon_kwds}}.
224 '''
225 n, kwds = _name2__(name_LatLon_kwds, _or_nameof=self)
226 h = (self.height or INT0) if height is None else height # _heigHt
227 r = self.latlon.to3Tuple(h) if LatLon is None else LatLon(
228 *self.latlon, height=h, **kwds)
229 return r.renamed(n) if n else r
232def decode3(georef, center=True):
233 '''Decode a C{georef} to lat-, longitude and precision.
235 @arg georef: To be decoded (L{Georef} or C{str}).
236 @kwarg center: If C{True}, use the georef's center, otherwise
237 the south-west, lower-left corner (C{bool}).
239 @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}.
241 @raise WGRSError: Invalid B{C{georef}}, INValid, non-alphanumeric
242 or odd length B{C{georef}}.
243 '''
244 def _digit(ll, g, i, m):
245 d = _Digits.find(g[i])
246 if d < 0 or d >= m:
247 raise _Error(i)
248 return ll * m + d
250 def _Error(i):
251 return WGRSError(Fmt.SQUARE(georef=i), georef)
253 def _index(chars, g, i):
254 k = chars.find(g[i])
255 if k < 0:
256 raise _Error(i)
257 return k
259 g, precision = _2geostr2(georef)
260 lon = _index(_LonTile, g, 0) + _LonOrig_Tile
261 lat = _index(_LatTile, g, 1) + _LatOrig_Tile
263 u = _1_0
264 if precision > 0:
265 lon = lon * _Tile + _index(_DegChar, g, 2)
266 lat = lat * _Tile + _index(_DegChar, g, 3)
267 m, p = 6, precision - 1
268 for i in range(_BaseLen, _BaseLen + p):
269 lon = _digit(lon, g, i, m)
270 lat = _digit(lat, g, i + p, m)
271 u *= m
272 m = _Base
273 u *= _Tile
275 if center:
276 lon = lon * _2_0 + _1_0
277 lat = lat * _2_0 + _1_0
278 u *= _2_0
279 u = _Tile / u
280 return LatLonPrec3Tuple(Lat(lat * u, Error=WGRSError),
281 Lon(lon * u, Error=WGRSError),
282 precision, name=nameof(georef))
285def decode5(georef, center=True):
286 '''Decode a C{georef} to lat-, longitude, precision, height and radius.
288 @arg georef: To be decoded (L{Georef} or C{str}).
289 @kwarg center: If C{True}, use the georef's center, otherwise the
290 south-west, lower-left corner (C{bool}).
292 @return: A L{LatLonPrec5Tuple}C{(lat, lon, precision, height, radius)}
293 where C{height} and/or C{radius} are C{None} if missing.
295 @raise WGRSError: Invalid B{C{georef}}.
296 '''
297 def _h2m(kft, g_n):
298 return Height(ft2m(kft * _1000_0), name=g_n, Error=WGRSError)
300 def _r2m(NM, g_n):
301 return Radius(NM / m2NM(1), name=g_n, Error=WGRSError)
303 def _split2(g, Unit, _2m):
304 n = typename(Unit)
305 i = max(g.find(n[0]), g.rfind(n[0]))
306 if i > _BaseLen:
307 return g[:i], _2m(int(g[i+1:]), _SPACE_(georef, n))
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 I{Karney}'s 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 C{georef}
340 resolution is I{15°} for B{C{precision}} 0:, I{1°} for 1, I{1′} for 2,
341 I{0.1′} for 3, I{0.01′} for 4, ... up to I{10**(2 - precision)′}.
342 '''
343 def _option(name, m, m2_, K):
344 f = Scalar_(m, name=name, Error=WGRSError)
345 return NN(name[0].upper(), int(m2_(f * K) + _0_5))
347 g, _, _ = _encode3(lat, lon, precision)
348 if radius is not None: # R before H
349 g += _option(_radius_, radius, m2NM, _1_0)
350 if height is not None: # H is last
351 g += _option(_height_, height, m2ft, _0_001)
352 return g
355def _encode3(lat, lon, precision, h=None):
356 '''Return 3-tuple C{(georef, (lat, lon), p)}.
357 '''
358 p = _2Precision(precision)
360 lat, lon = _2fll(lat, lon)
362 if h is None:
363 xt, xd, x = _divmod3( lon, _LonOrig_60B)
364 yt, yd, y = _divmod3(_off90(lat), _LatOrig_60B)
366 g = _LonTile[xt], _LatTile[yt]
367 if p > 0:
368 g += _DegChar[xd], _DegChar[yd]
369 p -= 1
370 if p > 0:
371 d = pow(_Base, _MaxPrec - p)
372 x = _0wd(p, x // d)
373 y = _0wd(p, y // d)
374 g += x, y
375 g = NN.join(g)
376 else:
377 g = encode(lat, lon, precision=p, height=h)
379 return g, (lat, lon), p # XXX Georef(''.join(g))
382def precision(res):
383 '''Determine the L{Georef} precision to meet a required (geographic)
384 resolution.
386 @arg res: The required resolution (C{degrees}).
388 @return: The L{Georef} precision (C{int} 0..11).
390 @raise ValueError: Invalid B{C{res}}.
392 @see: Function L{wgrs.encode} for more C{precision} details.
393 '''
394 r = Scalar_(res=res)
395 for p in range(_MaxPrec):
396 if resolution(p) <= r:
397 return p
398 return _MaxPrec
401def resolution(prec):
402 '''Determine the (geographic) resolution of a given L{Georef} precision.
404 @arg prec: The given precision (C{int}).
406 @return: The (geographic) resolution (C{degrees}).
408 @raise ValueError: Invalid B{C{prec}}.
410 @see: Function L{wgrs.encode} for more C{precision} details.
411 '''
412 p = Int(prec=prec, Error=WGRSError)
413 if p > 1:
414 p = min(p, _MaxPrec) - 1
415 r = _1_0 / (pow(_Base, p) * _60_0)
416 elif p < 1:
417 r = _float_Tile
418 else:
419 r = _1_0
420 return r
423__all__ += _ALL_DOCS(decode3, decode5, # functions
424 encode, precision, resolution)
426# **) MIT License
427#
428# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
429#
430# Permission is hereby granted, free of charge, to any person obtaining a
431# copy of this software and associated documentation files (the "Software"),
432# to deal in the Software without restriction, including without limitation
433# the rights to use, copy, modify, merge, publish, distribute, sublicense,
434# and/or sell copies of the Software, and to permit persons to whom the
435# Software is furnished to do so, subject to the following conditions:
436#
437# The above copyright notice and this permission notice shall be included
438# in all copies or substantial portions of the Software.
439#
440# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
441# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
442# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
443# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
444# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
445# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
446# OTHER DEALINGS IN THE SOFTWARE.