Coverage for pygeodesy/geohash.py: 96%
283 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-12 11:45 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-12 11:45 -0400
2# -*- coding: utf-8 -*-
4u'''Geohash en-/decoding.
6Classes L{Geohash} and L{GeohashError} and several functions to
7encode, decode and inspect I{geohashes}.
9Transcoded from JavaScript originals by I{(C) Chris Veness 2011-2015}
10and published under the same MIT Licence**, see U{Geohashes
11<https://www.Movable-Type.co.UK/scripts/geohash.html>}.
13See also U{Geohash<https://WikiPedia.org/wiki/Geohash>},
14U{Geohash<https://GitHub.com/vinsci/geohash>},
15U{PyGeohash<https://PyPI.org/project/pygeohash>} and
16U{Geohash-Javascript<https://GitHub.com/DaveTroy/geohash-js>}.
17'''
19from pygeodesy.basics import isodd, isstr, map2
20from pygeodesy.constants import EPS, R_M, _floatuple, _0_0, _0_5, _180_0, \
21 _360_0, _90_0, _N_90_0, _N_180_0 # PYCHOK used!
22from pygeodesy.dms import parse3llh # parseDMS2
23from pygeodesy.errors import _ValueError, _xkwds
24from pygeodesy.fmath import favg
25from pygeodesy.formy import equirectangular_ as _equirectangular_, \
26 equirectangular, euclidean, haversine, vincentys
27from pygeodesy.interns import NN, _COMMA_, _DOT_, _E_, _N_, _NE_, _NW_, \
28 _S_, _SE_, _SW_, _W_
29from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER
30from pygeodesy.named import _NamedDict, _NamedTuple, nameof, _xnamed
31from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \
32 LatLon2Tuple, PhiLam2Tuple
33from pygeodesy.props import deprecated_function, deprecated_method, \
34 deprecated_property_RO, Property_RO
35from pygeodesy.streprs import fstr
36from pygeodesy.units import Degrees_, Int, Lat, Lon, Precision_, Str, \
37 _xStrError
39from math import fabs, ldexp, log10, radians
41__all__ = _ALL_LAZY.geohash
42__version__ = '23.03.19'
45class _GH(object):
46 '''(INTERNAL) Lazily defined constants.
47 '''
48 def _4d(self, n, e, s, w): # helper
49 return dict(N=(n, e), S=(s, w),
50 E=(e, n), W=(w, s))
52 @Property_RO
53 def Borders(self):
54 return self._4d('prxz', 'bcfguvyz', '028b', '0145hjnp')
56 Bounds4 = (_N_90_0, _N_180_0, _90_0, _180_0)
58 @Property_RO
59 def DecodedBase32(self): # inverse GeohashBase32 map
60 return dict((c, i) for i, c in enumerate(self.GeohashBase32))
62 # Geohash-specific base32 map
63 GeohashBase32 = '0123456789bcdefghjkmnpqrstuvwxyz' # no a, i, j and o
65 @Property_RO
66 def Neighbors(self):
67 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy',
68 'bc01fg45238967deuvhjyznpkmstqrwx',
69 '14365h7k9dcfesgujnmqp0r2twvyx8zb',
70 '238967debc01fg45kmstqrwxuvhjyznp')
72 @Property_RO
73 def Sizes(self): # lat-, lon and radial size (in meter)
74 # ... where radial = sqrt(latSize * lonWidth / PI)
75 return (_floatuple(20032e3, 20000e3, 11292815.096), # 0
76 _floatuple( 5003e3, 5000e3, 2821794.075), # 1
77 _floatuple( 650e3, 1225e3, 503442.397), # 2
78 _floatuple( 156e3, 156e3, 88013.575), # 3
79 _floatuple( 19500, 39100, 15578.683), # 4
80 _floatuple( 4890, 4890, 2758.887), # 5
81 _floatuple( 610, 1220, 486.710), # 6
82 _floatuple( 153, 153, 86.321), # 7
83 _floatuple( 19.1, 38.2, 15.239), # 8
84 _floatuple( 4.77, 4.77, 2.691), # 9
85 _floatuple( 0.596, 1.19, 0.475), # 10
86 _floatuple( 0.149, 0.149, 0.084), # 11
87 _floatuple( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec
89_GH = _GH() # PYCHOK singleton
90_MaxPrec = 12
93def _2bounds(LatLon, LatLon_kwds, s, w, n, e, name=NN):
94 '''(INTERNAL) Return SW and NE bounds.
95 '''
96 if LatLon is None:
97 r = Bounds4Tuple(s, w, n, e, name=name)
98 else:
99 sw = _xnamed(LatLon(s, w, **LatLon_kwds), name)
100 ne = _xnamed(LatLon(n, e, **LatLon_kwds), name)
101 r = Bounds2Tuple(sw, ne, name=name)
102 return r # _xnamed(r, name)
105def _2center(bounds):
106 '''(INTERNAL) Return the C{bounds} center.
107 '''
108 return (favg(bounds.latN, bounds.latS),
109 favg(bounds.lonE, bounds.lonW))
112def _2fll(lat, lon, *unused):
113 '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
114 '''
115 # lat, lon = parseDMS2(lat, lon)
116 return (Lat(lat, Error=GeohashError),
117 Lon(lon, Error=GeohashError))
120def _2Geohash(geohash):
121 '''(INTERNAL) Check or create a Geohash instance.
122 '''
123 return geohash if isinstance(geohash, Geohash) else \
124 Geohash(geohash)
127def _2geostr(geohash):
128 '''(INTERNAL) Check a geohash string.
129 '''
130 try:
131 if not (0 < len(geohash) <= _MaxPrec):
132 raise ValueError
133 geostr = geohash.lower()
134 for c in geostr:
135 if c not in _GH.DecodedBase32:
136 raise ValueError
137 return geostr
138 except (AttributeError, TypeError, ValueError) as x:
139 raise GeohashError(Geohash.__name__, geohash, cause=x)
142class Geohash(Str):
143 '''Geohash class, a named C{str}.
144 '''
145 # no str.__init__ in Python 3
146 def __new__(cls, cll, precision=None, name=NN):
147 '''New L{Geohash} from an other L{Geohash} instance or C{str}
148 or from a C{LatLon} instance or C{str}.
150 @arg cll: Cell or location (L{Geohash}, C{LatLon} or C{str}).
151 @kwarg precision: Optional, the desired geohash length (C{int}
152 1..12), see function L{geohash.encode} for
153 some examples.
154 @kwarg name: Optional name (C{str}).
156 @return: New L{Geohash}.
158 @raise GeohashError: INValid or non-alphanumeric B{C{cll}}.
160 @raise TypeError: Invalid B{C{cll}}.
161 '''
162 ll = None
164 if isinstance(cll, Geohash):
165 gh = _2geostr(str(cll))
167 elif isstr(cll):
168 if _COMMA_ in cll:
169 ll = _2fll(*parse3llh(cll))
170 gh = encode(*ll, precision=precision)
171 else:
172 gh = _2geostr(cll)
174 else: # assume LatLon
175 try:
176 ll = _2fll(cll.lat, cll.lon)
177 gh = encode(*ll, precision=precision)
178 except AttributeError:
179 raise _xStrError(Geohash, cll=cll, Error=GeohashError)
181 self = Str.__new__(cls, gh, name=name or nameof(cll))
182 self._latlon = ll
183 return self
185 @deprecated_property_RO
186 def ab(self):
187 '''DEPRECATED, use property C{philam}.'''
188 return self.philam
190 def adjacent(self, direction, name=NN):
191 '''Determine the adjacent cell in the given compass direction.
193 @arg direction: Compass direction ('N', 'S', 'E' or 'W').
194 @kwarg name: Optional name (C{str}), otherwise the name
195 of this cell plus C{.D}irection.
197 @return: Geohash of adjacent cell (L{Geohash}).
199 @raise GeohashError: Invalid geohash or B{C{direction}}.
200 '''
201 # based on <https://GitHub.com/DaveTroy/geohash-js>
203 D = direction[:1].upper()
204 if D not in _GH.Neighbors:
205 raise GeohashError(direction=direction)
207 e = 1 if isodd(len(self)) else 0
209 c = self[-1:] # last hash char
210 i = _GH.Neighbors[D][e].find(c)
211 if i < 0:
212 raise GeohashError(geohash=self)
214 p = self[:-1] # hash without last char
215 # check for edge-cases which don't share common prefix
216 if p and (c in _GH.Borders[D][e]):
217 p = Geohash(p).adjacent(D)
219 n = name or self.name
220 if n:
221 n = _DOT_(n, D)
222 # append letter for direction to parent
223 return Geohash(p + _GH.GeohashBase32[i], name=n)
225 @Property_RO
226 def _bounds(self):
227 '''(INTERNAL) Cache for L{bounds}.
228 '''
229 return bounds(self)
231 def bounds(self, LatLon=None, **LatLon_kwds):
232 '''Return the lower-left SW and upper-right NE bounds of this
233 geohash cell.
235 @kwarg LatLon: Optional class to return I{bounds} (C{LatLon})
236 or C{None}.
237 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
238 arguments, ignored if B{C{LatLon}} is C{None}.
240 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
241 or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if
242 C{B{LatLon} is None},
243 '''
244 r = self._bounds
245 return r if LatLon is None else \
246 _2bounds(LatLon, LatLon_kwds, *r, name=self.name)
248 def _distanceTo(self, func_, other, **kwds):
249 '''(INTERNAL) Helper for distances, see C{formy._distanceTo*}.
250 '''
251 lls = self.latlon + _2Geohash(other).latlon
252 return func_(*lls, **kwds)
254 def distanceTo(self, other):
255 '''Estimate the distance between this and an other geohash
256 based the cell sizes.
258 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
260 @return: Approximate distance (C{meter}).
262 @raise TypeError: The B{C{other}} is not a L{Geohash},
263 C{LatLon} or C{str}.
264 '''
265 other = _2Geohash(other)
267 n = min(len(self), len(other), len(_GH.Sizes))
268 if n:
269 for n in range(n):
270 if self[n] != other[n]:
271 break
272 return _GH.Sizes[n][2]
274 @deprecated_method
275 def distance1To(self, other): # PYCHOK no cover
276 '''DEPRECATED, use method L{distanceTo}.'''
277 return self.distanceTo(other)
279 distance1 = distance1To
281 @deprecated_method
282 def distance2To(self, other, radius=R_M, adjust=False, wrap=False): # PYCHOK no cover
283 '''DEPRECATED, use method L{equirectangularTo}.'''
284 return self.equirectangularTo(other, radius=radius, adjust=adjust, wrap=wrap)
286 distance2 = distance2To
288 @deprecated_method
289 def distance3To(self, other, radius=R_M, wrap=False): # PYCHOK no cover
290 '''DEPRECATED, use method L{haversineTo}.'''
291 return self.haversineTo(other, radius=radius, wrap=wrap)
293 distance3 = distance3To
295 def equirectangularTo(self, other, radius=R_M, adjust=False, wrap=False):
296 '''Approximate the distance between this and an other geohash
297 using function L{pygeodesy.equirectangular}.
299 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
300 @kwarg radius: Mean earth radius, ellipsoid or datum
301 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
302 L{Datum} or L{a_f2Tuple}) or C{None}.
303 @kwarg adjust: Adjust the wrapped, unrolled longitudinal
304 delta by the cosine of the mean latitude
305 C{bool}).
306 @kwarg wrap: Wrap and unroll longitudes (C{bool}).
308 @return: Distance (C{meter}, same units as B{C{radius}} or the
309 ellipsoid or datum axes or C{radians I{squared}} if
310 B{C{radius}} is C{None} or C{0}).
312 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
313 or C{str} or invalid B{C{radius}}.
315 @see: U{Local, flat earth approximation
316 <https://www.EdWilliams.org/avform.htm#flat>}, functions
317 '''
318 lls = self.latlon + _2Geohash(other).latlon
319 kwds = dict(adjust=adjust, limit=None, wrap=wrap)
320 return equirectangular( *lls, radius=radius, **kwds) if radius else \
321 _equirectangular_(*lls, **kwds).distance2
323 def euclideanTo(self, other, radius=R_M, adjust=False, wrap=False):
324 '''Approximate the distance between this and an other geohash
325 using function L{pygeodesy.euclidean}.
327 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
328 @kwarg radius: Mean earth radius, ellipsoid or datum
329 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
330 L{Datum} or L{a_f2Tuple}).
331 @kwarg adjust: Adjust the wrapped, unrolled longitudinal
332 delta by the cosine of the mean latitude
333 C{bool}).
334 @kwarg wrap: Wrap and unroll longitudes (C{bool}).
336 @return: Distance (C{meter}, same units as B{C{radius}} or the
337 ellipsoid or datum axes).
339 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
340 or C{str} or invalid B{C{radius}}.
341 '''
342 return self._distanceTo(euclidean, other, radius=radius,
343 adjust=adjust, wrap=wrap)
345 def haversineTo(self, other, radius=R_M, wrap=False):
346 '''Compute the distance between this and an other geohash using
347 the L{pygeodesy.haversine} formula.
349 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
350 @kwarg radius: Mean earth radius, ellipsoid or datum
351 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
352 L{Datum} or L{a_f2Tuple}).
353 @kwarg wrap: Wrap and unroll longitudes (C{bool}).
355 @return: Distance (C{meter}, same units as B{C{radius}} or the
356 ellipsoid or datum axes).
358 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
359 or C{str} or invalid B{C{radius}}.
360 '''
361 return self._distanceTo(haversine, other, radius=radius, wrap=wrap)
363 @Property_RO
364 def latlon(self):
365 '''Get the lat- and longitude of (the approximate center of)
366 this geohash as a L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
367 '''
368 lat, lon = self._latlon or _2center(self.bounds())
369 return LatLon2Tuple(lat, lon, name=self.name)
371 @Property_RO
372 def neighbors(self):
373 '''Get all 8 adjacent cells as a L{Neighbors8Dict}C{(N, NE,
374 E, SE, S, SW, W, NW)} of L{Geohash}es.
375 '''
376 return Neighbors8Dict(N=self.N, NE=self.NE, E=self.E, SE=self.SE,
377 S=self.S, SW=self.SW, W=self.W, NW=self.NW,
378 name=self.name)
380 @Property_RO
381 def philam(self):
382 '''Get the lat- and longitude of (the approximate center of)
383 this geohash as a L{PhiLam2Tuple}C{(phi, lam)} in C{radians}.
384 '''
385 return PhiLam2Tuple(map2(radians, self.latlon), name=self.name) # *map2
387 @Property_RO
388 def precision(self):
389 '''Get this geohash's precision (C{int}).
390 '''
391 return len(self)
393 @Property_RO
394 def sizes(self):
395 '''Get the lat- and longitudinal size of this cell as
396 a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}).
397 '''
398 z = _GH.Sizes
399 n = min(len(z) - 1, max(self.precision, 1))
400 return LatLon2Tuple(z[n][:2], name=self.name) # *z XXX Height, Width?
402 def toLatLon(self, LatLon=None, **LatLon_kwds):
403 '''Return (the approximate center of) this geohash cell
404 as an instance of the supplied C{LatLon} class.
406 @arg LatLon: Class to use (C{LatLon}) or C{None}.
407 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
408 keyword arguments, ignored if
409 C{B{LatLon} is None}.
411 @return: This geohash location (B{C{LatLon}}) or a
412 L{LatLon2Tuple}C{(lat, lon)} if B{C{LatLon}}
413 is C{None}.
415 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
417 @example:
419 >>> from sphericalTrigonometry import LatLon
420 >>> ll = Geohash('u120fxw').toLatLon(LatLon)
421 >>> print(repr(ll)) # LatLon(52°12′17.9″N, 000°07′07.64″E)
422 >>> print(ll) # 52.204971°N, 000.11879°E
423 '''
424 return self.latlon if LatLon is None else _xnamed(LatLon(
425 *self.latlon, **LatLon_kwds), self.name)
427 def vincentysTo(self, other, radius=R_M, wrap=False):
428 '''Compute the distance between this and an other geohash using
429 the L{pygeodesy.vincentys} formula.
431 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
432 @kwarg radius: Mean earth radius, ellipsoid or datum
433 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
434 L{Datum} or L{a_f2Tuple}).
435 @kwarg wrap: Wrap and unroll longitudes (C{bool}).
437 @return: Distance (C{meter}, same units as B{C{radius}} or the
438 ellipsoid or datum axes).
440 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
441 or C{str} or invalid B{C{radius}}.
442 '''
443 return self._distanceTo(vincentys, other, radius=radius, wrap=wrap)
445 @Property_RO
446 def N(self):
447 '''Get the cell North of this (L{Geohash}).
448 '''
449 return self.adjacent(_N_)
451 @Property_RO
452 def S(self):
453 '''Get the cell South of this (L{Geohash}).
454 '''
455 return self.adjacent(_S_)
457 @Property_RO
458 def E(self):
459 '''Get the cell East of this (L{Geohash}).
460 '''
461 return self.adjacent(_E_)
463 @Property_RO
464 def W(self):
465 '''Get the cell West of this (L{Geohash}).
466 '''
467 return self.adjacent(_W_)
469 @Property_RO
470 def NE(self):
471 '''Get the cell NorthEast of this (L{Geohash}).
472 '''
473 return self.N.E
475 @Property_RO
476 def NW(self):
477 '''Get the cell NorthWest of this (L{Geohash}).
478 '''
479 return self.N.W
481 @Property_RO
482 def SE(self):
483 '''Get the cell SouthEast of this (L{Geohash}).
484 '''
485 return self.S.E
487 @Property_RO
488 def SW(self):
489 '''Get the cell SouthWest of this (L{Geohash}).
490 '''
491 return self.S.W
494class GeohashError(_ValueError):
495 '''Geohash encode, decode or other L{Geohash} issue.
496 '''
497 pass
500class Neighbors8Dict(_NamedDict):
501 '''8-Dict C{(N, NE, E, SE, S, SW, W, NW)} of L{Geohash}es,
502 providing key I{and} attribute access to the items.
503 '''
504 _Keys_ = (_N_, _NE_, _E_, _SE_, _S_, _SW_, _W_, _NW_)
506 def __init__(self, **kwds): # PYCHOK no *args
507 kwds = _xkwds(kwds, **_Neighbors8Defaults)
508 _NamedDict.__init__(self, **kwds) # name=...
511_Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
512 len(Neighbors8Dict._Keys_))) # XXX frozendict
515def bounds(geohash, LatLon=None, **LatLon_kwds):
516 '''Returns the lower-left SW and upper-right NE corners of a geohash.
518 @arg geohash: To be bound (L{Geohash}).
519 @kwarg LatLon: Optional class to return the bounds (C{LatLon})
520 or C{None}.
521 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
522 arguments, ignored if C{B{LatLon} is None}.
524 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
525 or if B{C{LatLon}} is C{None}, a L{Bounds4Tuple}C{(latS,
526 lonW, latN, lonE)}.
528 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon}
529 or C{str} or invalid B{C{LatLon}} or invalid
530 B{C{LatLon_kwds}}.
532 @raise GeohashError: Invalid or C{null} B{C{geohash}}.
534 @example:
536 >>> geohash.bounds('u120fxw') # 52.20428467, 0.11810303, 52.20565796, 0.11947632
537 >>> geohash.decode('u120fxw') # '52.205', '0.1188'
538 '''
539 gh = _2Geohash(geohash)
540 if len(gh) < 1:
541 raise GeohashError(geohash=geohash)
543 s, w, n, e = _GH.Bounds4
544 try:
545 d = True
546 for c in gh.lower():
547 i = _GH.DecodedBase32[c]
548 for m in (16, 8, 4, 2, 1):
549 if d: # longitude
550 if i & m:
551 w = favg(w, e)
552 else:
553 e = favg(w, e)
554 else: # latitude
555 if i & m:
556 s = favg(s, n)
557 else:
558 n = favg(s, n)
559 d = not d
560 except KeyError:
561 raise GeohashError(geohash=geohash)
563 return _2bounds(LatLon, LatLon_kwds, s, w, n, e,
564 name=nameof(geohash))
567def _bounds3(geohash):
568 '''(INTERNAL) Return 3-tuple C{(bounds, height, width)}.
569 '''
570 b = bounds(geohash)
571 return b, (b.latN - b.latS), (b.lonE - b.lonW)
574def decode(geohash):
575 '''Decode a geohash to lat-/longitude of the (approximate
576 centre of) geohash cell to reasonable precision.
578 @arg geohash: To be decoded (L{Geohash}).
580 @return: 2-Tuple C{(latStr, lonStr)}, both C{str}.
582 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
583 C{LatLon} or C{str}.
585 @raise GeohashError: Invalid or null B{C{geohash}}.
587 @example:
589 >>> geohash.decode('u120fxw') # '52.205', '0.1188'
590 >>> geohash.decode('sunny') # '23.708', '42.473' Saudi Arabia
591 >>> geohash.decode('fur') # '69.6', '-45.7' Greenland
592 >>> geohash.decode('reef') # '-24.87', '162.95' Coral Sea
593 >>> geohash.decode('geek') # '65.48', '-17.75' Iceland
594 '''
595 b, h, w = _bounds3(geohash)
596 lat, lon = _2center(b)
598 # round to near centre without excessive precision to
599 # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros
600 return (fstr(lat, prec=int(2 - log10(h))),
601 fstr(lon, prec=int(2 - log10(w)))) # strs!
604def decode2(geohash, LatLon=None, **LatLon_kwds):
605 '''Decode a geohash to lat-/longitude of the (approximate
606 centre of) geohash cell to reasonable precision.
608 @arg geohash: To be decoded (L{Geohash}).
609 @kwarg LatLon: Optional class to return the location (C{LatLon})
610 or C{None}.
611 @kwarg LatLon_kwds: Optional, addtional B{C{LatLon}} keyword
612 arguments, ignored if C{B{LatLon} is None}.
614 @return: L{LatLon2Tuple}C{(lat, lon)}, both C{degrees} if
615 C{B{LatLon} is None}, otherwise a B{C{LatLon}} instance.
617 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
618 C{LatLon} or C{str}.
620 @raise GeohashError: Invalid or null B{C{geohash}}.
621 '''
622 t = map2(float, decode(geohash))
623 r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t
624 return _xnamed(r, decode2.__name__)
627def decode_error(geohash):
628 '''Return the relative lat-/longitude decoding errors for
629 this geohash.
631 @arg geohash: To be decoded (L{Geohash}).
633 @return: A L{LatLon2Tuple}C{(lat, lon)} with the lat- and
634 longitudinal errors in (C{degrees}).
636 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
637 C{LatLon} or C{str}.
639 @raise GeohashError: Invalid or null B{C{geohash}}.
641 @example:
643 >>> geohash.decode_error('u120fxw') # 0.00068665, 0.00068665
644 >>> geohash.decode_error('fur') # 0.703125, 0.703125
645 >>> geohash.decode_error('fu') # 2.8125, 5.625
646 >>> geohash.decode_error('f') # 22.5, 22.5
647 '''
648 _, h, w = _bounds3(geohash)
649 return LatLon2Tuple(h * _0_5, # Height error
650 w * _0_5) # Width error
653def distance_(geohash1, geohash2):
654 '''Estimate the distance between two geohash (from the cell sizes).
656 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
657 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
659 @return: Approximate distance (C{meter}).
661 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
662 not a L{Geohash}, C{LatLon} or C{str}.
664 @example:
666 >>> geohash.distance_('u120fxwsh', 'u120fxws0') # 15.239
667 '''
668 return _2Geohash(geohash1).distanceTo(geohash2)
671@deprecated_function
672def distance1(geohash1, geohash2):
673 '''DEPRECATED, used L{geohash.distance_}.'''
674 return distance_(geohash1, geohash2)
677@deprecated_function
678def distance2(geohash1, geohash2):
679 '''DEPRECATED, used L{geohash.equirectangular_}.'''
680 return equirectangular_(geohash1, geohash2)
683@deprecated_function
684def distance3(geohash1, geohash2):
685 '''DEPRECATED, used L{geohash.haversine_}.'''
686 return haversine_(geohash1, geohash2)
689def encode(lat, lon, precision=None):
690 '''Encode a lat-/longitude as a C{geohash}, either to the specified
691 precision or if not provided, to an automatically evaluated
692 precision.
694 @arg lat: Latitude (C{degrees}).
695 @arg lon: Longitude (C{degrees}).
696 @kwarg precision: Optional, the desired geohash length (C{int}
697 1..12).
699 @return: The C{geohash} (C{str}).
701 @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
703 @example:
705 >>> geohash.encode(52.205, 0.119, 7) # 'u120fxw'
706 >>> geohash.encode(52.205, 0.119, 12) # 'u120fxwshvkg'
707 >>> geohash.encode(52.205, 0.1188, 12) # 'u120fxws0jre'
708 >>> geohash.encode(52.205, 0.1188) # 'u120fxw'
709 >>> geohash.encode( 0, 0) # 's00000000000'
710 '''
711 lat, lon = _2fll(lat, lon)
713 if precision is None:
714 # Infer precision by refining geohash until
715 # it matches precision of supplied lat/lon.
716 for p in range(1, _MaxPrec + 1):
717 gh = encode(lat, lon, p)
718 ll = map2(float, decode(gh))
719 if fabs(lat - ll[0]) < EPS and \
720 fabs(lon - ll[1]) < EPS:
721 return gh
722 p = _MaxPrec
723 else:
724 p = Precision_(precision, Error=GeohashError, low=1, high=_MaxPrec)
726 b = i = 0
727 d, gh = True, []
728 s, w, n, e = _GH.Bounds4
730 while p > 0:
731 i += i
732 if d: # bisect longitude
733 m = favg(e, w)
734 if lon < m:
735 e = m
736 else:
737 w = m
738 i += 1
739 else: # bisect latitude
740 m = favg(n, s)
741 if lat < m:
742 n = m
743 else:
744 s = m
745 i += 1
746 d = not d
748 b += 1
749 if b == 5:
750 # 5 bits gives a character:
751 # append it and start over
752 gh.append(_GH.GeohashBase32[i])
753 b = i = 0
754 p -= 1
756 return NN.join(gh)
759def equirectangular_(geohash1, geohash2, radius=R_M):
760 '''Approximate the distance between two geohashes using the
761 L{pygeodesy.equirectangular} formula.
763 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
764 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
765 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
767 @return: Approximate distance (C{meter}, same units as
768 B{C{radius}}).
770 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
771 not a L{Geohash}, C{LatLon} or C{str}.
773 @example:
775 >>> geohash.equirectangular_('u120fxwsh', 'u120fxws0') # 19.0879
776 '''
777 return _2Geohash(geohash1).equirectangularTo(geohash2, radius=radius)
780def haversine_(geohash1, geohash2, radius=R_M):
781 '''Compute the great-circle distance between two geohashes
782 using the L{pygeodesy.haversine} formula.
784 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
785 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
786 @kwarg radius: Mean earth radius (C{meter}).
788 @return: Great-circle distance (C{meter}, same units as
789 B{C{radius}}).
791 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
792 not a L{Geohash}, C{LatLon} or C{str}.
794 @example:
796 >>> geohash.haversine_('u120fxwsh', 'u120fxws0') # 11.6978
797 '''
798 return _2Geohash(geohash1).haversineTo(geohash2, radius=radius)
801def neighbors(geohash):
802 '''Return the L{Geohash}es for all 8 adjacent cells.
804 @arg geohash: Cell for which neighbors are requested
805 (L{Geohash} or C{str}).
807 @return: A L{Neighbors8Dict}C{(N, NE, E, SE, S, SW, W, NW)}
808 of L{Geohash}es.
810 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
811 C{LatLon} or C{str}.
812 '''
813 return _2Geohash(geohash).neighbors
816def precision(res1, res2=None):
817 '''Determine the L{Geohash} precisions to meet a or both given
818 (geographic) resolutions.
820 @arg res1: The required primary I{(longitudinal)} resolution
821 (C{degrees}).
822 @kwarg res2: Optional, required secondary I{(latitudinal)}
823 resolution (C{degrees}).
825 @return: The L{Geohash} precision or length (C{int}, 1..12).
827 @raise GeohashError: Invalid B{C{res1}} or B{C{res2}}.
829 @see: C++ class U{Geohash
830 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
831 '''
832 r = Degrees_(res1=res1, low=_0_0, Error=GeohashError)
833 if res2 is None:
834 t = r, r
835 for p in range(1, _MaxPrec):
836 if resolution2(p, None) <= t:
837 return p
839 else:
840 t = r, Degrees_(res2=res2, low=_0_0, Error=GeohashError)
841 for p in range(1, _MaxPrec):
842 if resolution2(p, p) <= t:
843 return p
845 return _MaxPrec
848class Resolutions2Tuple(_NamedTuple):
849 '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
850 secondary I{(latitudinal)} resolution, both in C{degrees}.
851 '''
852 _Names_ = ('res1', 'res2')
853 _Units_ = ( Degrees_, Degrees_)
856def resolution2(prec1, prec2=None):
857 '''Determine the (geographic) resolutions of given L{Geohash}
858 precisions.
860 @arg prec1: The given primary I{(longitudinal)} precision
861 (C{int} 1..12).
862 @kwarg prec2: Optional, secondary I{(latitudinal)} precision
863 (C{int} 1..12).
865 @return: L{Resolutions2Tuple}C{(res1, res2)} with the
866 (geographic) resolutions C{degrees}, where C{res2}
867 B{C{is}} C{res1} if no B{C{prec2}} is given.
869 @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
871 @see: I{Karney}'s C++ class U{Geohash
872 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
873 '''
874 res1, res2 = _360_0, _180_0 # note ... lon, lat!
876 if prec1:
877 p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec))
878 res1 = res2 = ldexp(res1, -(p - p // 2))
880 if prec2:
881 p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec))
882 res2 = ldexp(res2, -(p // 2))
884 return Resolutions2Tuple(res1, res2)
887def sizes(geohash):
888 '''Return the lat- and longitudinal size of this L{Geohash} cell.
890 @arg geohash: Cell for which size are required (L{Geohash} or
891 C{str}).
893 @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal
894 height and longitudinal width in (C{meter}).
896 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
897 C{LatLon} or C{str}.
898 '''
899 return _2Geohash(geohash).sizes
902__all__ += _ALL_OTHER(bounds, # functions
903 decode, decode2, decode_error, distance_,
904 encode, equirectangular_, haversine_,
905 neighbors, precision, resolution2, sizes)
907# **) MIT License
908#
909# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
910#
911# Permission is hereby granted, free of charge, to any person obtaining a
912# copy of this software and associated documentation files (the "Software"),
913# to deal in the Software without restriction, including without limitation
914# the rights to use, copy, modify, merge, publish, distribute, sublicense,
915# and/or sell copies of the Software, and to permit persons to whom the
916# Software is furnished to do so, subject to the following conditions:
917#
918# The above copyright notice and this permission notice shall be included
919# in all copies or substantial portions of the Software.
920#
921# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
922# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
923# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
924# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
925# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
926# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
927# OTHER DEALINGS IN THE SOFTWARE.