Coverage for pygeodesy/geohash.py: 97%
368 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
2# -*- coding: utf-8 -*-
4u'''I{Gustavo Niemeyer}’s U{Geohash<https://WikiPedia.org/wiki/Geohash>}.
6Class L{Geohash} and several functions to encode, decode and inspect
7C{geohashes} and optional L{Geohashed} caches.
9Originally transcoded from JavaScript originals by I{(C) Chris Veness
102011-2024} and published under the same MIT Licence**, see
11U{Geohashes<https://www.Movable-Type.co.UK/scripts/geohash.html>}.
13@see: U{Geohash<https://WikiPedia.org/wiki/Geohash>}, I{Karney}'s C++
14 U{Geohash<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>},
15 U{geohash<https://GitHub.com/vinsci/geohash>},
16 U{pygeohash<https://PyPI.org/project/pygeohash>} and
17 U{geohash-js<https://GitHub.com/DaveTroy/geohash-js>}.
18'''
20from pygeodesy.basics import isstr, map2
21from pygeodesy.constants import EPS, R_M, _0_0, _0_5, _180_0, _360_0, \
22 _90_0, _N_90_0, _N_180_0 # PYCHOK used!
23from pygeodesy.errors import _ValueError, _xkwds, _xStrError
24# from pygeodesy import formy as _formy # _MODS
25from pygeodesy.interns import NN, _COMMA_, _DOT_, _E_, _height_, _N_, _NE_, \
26 _NW_, _radius_, _S_, _SE_, _SPACE_, _SW_, _W_, \
27 _width_ # _INV_
28from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
29from pygeodesy.named import _name__, _NamedDict, _NamedTuple, nameof, _xnamed
30from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, LatLon2Tuple, \
31 PhiLam2Tuple
32from pygeodesy.props import deprecated_function, deprecated_method, \
33 deprecated_property_RO, Property_RO, \
34 property_RO, property_ROver
35# from pygeodesy.streprs import Fmt, fstr # _MODS
36from pygeodesy.units import Degrees_, Int, Lat_, Lon_, Meter, Precision_, Str
38from math import fabs, ldexp, log10, radians
40__all__ = _ALL_LAZY.geohash
41__version__ = '24.10.12'
43_formy = _MODS.into(formy=__name__)
44_MASK5 = 16, 8, 4, 2, 1 # PYCHOK used!
45_MaxPrec = 12
48def _2bounds(LatLon, LatLon_kwds, s, w, n, e, **name):
49 '''(INTERNAL) Return SW and NE bounds.
50 '''
51 if LatLon is None:
52 r = Bounds4Tuple(s, w, n, e, **name)
53 else:
54 kwds = _xkwds(LatLon_kwds, **name)
55 r = Bounds2Tuple(LatLon(s, w, **kwds),
56 LatLon(n, e, **kwds), **name)
57 return r
60def _2center(bounds):
61 '''(INTERNAL) Return the C{bounds} center.
62 '''
63 return (_2mid(bounds.latN, bounds.latS),
64 _2mid(bounds.lonE, bounds.lonW))
67def _2dab(d, a, b):
68 '''(INTERNAL) Get delta lat or lon from center.
69 '''
70 return fabs(d - round(*_2mid_ndigits(a, b)))
73def _2fll(lat, lon, *unused):
74 '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
75 '''
76 # lat, lon = parseDMS2(lat, lon)
77 return (Lat_(lat, Error=GeohashError),
78 Lon_(lon, Error=GeohashError))
81def _2Geohash(geohash):
82 '''(INTERNAL) Check or create a Geohash instance.
83 '''
84 return geohash if isinstance(geohash, Geohash) else \
85 Geohash(geohash)
88def _2latlon(s, w, n, e, fstr=None):
89 '''(INTERNAL) Get the center C{lat, lon}, rounded.
90 '''
91 lat, a = _2mid_ndigits(n, s)
92 lon, b = _2mid_ndigits(e, w)
93 return (fstr(lat, prec=a), fstr(lon, prec=b)) if fstr else \
94 (round(lat, a), round(lon, b))
97def _2mid(a, b):
98 '''(INTERNAL) Bisect C{a} to C{b}.
99 '''
100 return (a + b) * _0_5 # favg
103def _2mid_ndigits(a, b): # a > b
104 '''(INTERNAL) Return 2-tuple C{(_2mid, ndigits)}.
105 '''
106 # round to near centre without excessive
107 # precision to ⌊2-log10(Δ°)⌋ ndigits
108 return _2mid(a, b), int(2 - log10(a - b))
111def _2Precision(p):
112 '''(INTERNAL) Get a valid C{Precision}.
113 '''
114 return Precision_(p, low=1, high=_MaxPrec, Error=GeohashError)
117def _2res(res, **prec):
118 '''(INTERNAL) Get the C{res}olution for a C{prec}ision.
119 '''
120 p = max(min(Int(Error=GeohashError, **prec), _MaxPrec), 0) * 5
121 x = (p - p // 2) if res > _180_0 else (p // 2)
122 return ldexp(res, -x) if x else res # ldexp == res / float(1 << x)
125class _GH(object):
126 '''(INTERNAL) Lazily defined constants.
127 '''
128 def _4d(self, s, w, n, e): # helper
129 return dict(S=(s, w), W=(w, s),
130 N=(n, e), E=(e, n))
132 @property_ROver
133 def Borders(self):
134 return self._4d('028b', '0145hjnp', 'prxz', 'bcfguvyz')
136 @property_ROver
137 def DecodeB32(self): # inverse EncodeB32 map
138 return dict((c, i) for i, c in enumerate(self.EncodeB32))
140 def decode2(self, geohash):
141 '''Decode C{geohash} to 2-tuple C{(lat, lon)}.
142 '''
143 swne = self.swne4(geohash)
144 return _2latlon(*swne)
146 # Geohash's base32 codes, no a, i, l and o
147 EncodeB32 = '0123456789bcdefghjkmnpqrstuvwxyz'
149 def encode(self, *lat_lon_prec_eps):
150 '''Encode C{lat, lon} to C{prec}ision or C{eps}.
151 '''
152 def _encodes(lat, lon, prec, eps=0):
153 s, w, n, e = self.SWNE4
154 E, d, _mid = self.EncodeB32, True, _2mid
155 for _ in range(prec):
156 i = 0
157 for _ in range(5): # len(_MASK5)
158 i += i
159 if d: # bisect longitude
160 a = _mid(e, w)
161 if lon < a:
162 e = a
163 else:
164 w = a
165 i += 1
166 else: # bisect latitude
167 a = _mid(n, s)
168 if lat < a:
169 n = a
170 else:
171 s = a
172 i += 1
173 d = not d
174 yield E[i]
175 if eps > 0: # infer prec
176 if _2dab(lon, e, w) < eps and \
177 _2dab(lat, n, s) < eps:
178 break
180 return NN.join(_encodes(*lat_lon_prec_eps))
182 def encode2(self, lat, lon, prec, eps):
183 '''Return 2-tuple C{geohash, (lat, lon))}.
184 '''
185 lat, lon = _2fll(lat, lon)
186 if prec:
187 p, e = _2Precision(prec), 0
188 else: # infer precision by refining geohash
189 p, e = _MaxPrec, max(eps, EPS)
190 return self.encode(lat, lon, p, e), (lat, lon)
192 @property_ROver
193 def _LatLon2Tuple(self):
195 class _LatLon2Tuple(_NamedTuple):
196 '''DEPRECATED on 2024.07.28, C{(lat, lon)} in B{C{meter}}, use L{Sizes3Tuple}.'''
197 _Names_ = LatLon2Tuple._Names_
198 _Units_ = Meter, Meter
200 return _LatLon2Tuple
202 @property_ROver
203 def Neighbors(self):
204 return self._4d('14365h7k9dcfesgujnmqp0r2twvyx8zb',
205 '238967debc01fg45kmstqrwxuvhjyznp',
206 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',
207 'bc01fg45238967deuvhjyznpkmstqrwx')
209 @property_ROver
210 def Sizes(self): # height, width and radius (in meter)
211 # where radius = sqrt(height * width / PI), the
212 # radius of a circle with area (height * width)
213 T = Sizes3Tuple
214 return (T(20000e3, 20032e3, 11292815.096), # 0
215 T( 5000e3, 5003e3, 2821794.075), # 1
216 T( 650e3, 1225e3, 503442.397), # 2
217 T( 156e3, 156e3, 88013.575), # 3
218 T( 19500, 39100, 15578.683), # 4
219 T( 4890, 4890, 2758.887), # 5
220 T( 610, 1220, 486.710), # 6
221 T( 153, 153, 86.321), # 7
222 T( 19.1, 38.2, 15.239), # 8
223 T( 4.77, 4.77, 2.691), # 9
224 T( 0.596, 1.19, 0.475), # 10
225 T( 0.149, 0.149, 0.084), # 11
226 T( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec
228 SWNE4 = (_N_90_0, _N_180_0, _90_0, _180_0)
230 def swne4(self, geohash, mask5=_MASK5):
231 '''Decode C{geohash} into 4-tuple C{(s, w, n, e)}.
232 '''
233 nc = len(geohash) if isstr(geohash) else 0
234 if not (0 < nc <= _MaxPrec): # or geohash.startswith(_INV_)
235 raise GeohashError(geohash=geohash, len=nc)
236 s, w, n, e = self.SWNE4
237 D, d, _mid = self.DecodeB32, True, _2mid
238 try:
239 for j, c in enumerate(geohash.lower()):
240 i = D[c]
241 for m in mask5:
242 if d: # longitude
243 a = _mid(e, w)
244 if (i & m):
245 w = a
246 else:
247 e = a
248 else: # latitude
249 a = _mid(n, s)
250 if (i & m):
251 s = a
252 else:
253 n = a
254 d = not d
255 except KeyError:
256 c = _MODS.streprs.Fmt.INDEX(repr(c), j)
257 raise GeohashError(geohash=geohash, len=nc, txt=c)
258 return s, w, n, e
260_GH = _GH() # PYCHOK singleton
263class Geohash(Str):
264 '''Geohash class, a named C{str}.
265 '''
266 # no str.__init__ in Python 3
267 def __new__(cls, lat_ghll, lon=None, precision=None, eps=EPS, **name):
268 '''New L{Geohash} from an other L{Geohash} instance or geohash C{str}
269 or from a lat- and longitude.
271 @arg lat_ghll: Latitude (C{degrees90}), a geohash (L{Geohash},
272 C{str}) or a location (C{LatLon}, C{LatLon*Tuple}).
273 @kwarg lon: Logitude (C{degrees180)}, required if B{C{lat_ghll}}
274 is C{degrees90}, ignored otherwise.
275 @kwarg precision: The desired geohash length (C{int} 1..12) or
276 C{None} or C{0}, see L{encode<pygeodesy.geohash.encode>}.
277 @kwarg eps: Optional inference tolerance (C{degrees}), see
278 L{encode<pygeodesy.geohash.encode>}.
279 @kwarg name: Optional C{B{name}=NN} (C{str}).
281 @return: New L{Geohash}.
283 @raise GeohashError: Invalid B{C{lat_ghll}}.
285 @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}.
287 @raise TypeError: Invalid B{C{lat_ghll}}.
288 '''
289 if lon is None:
290 if isinstance(lat_ghll, Geohash):
291 gh, ll = str(lat_ghll), lat_ghll.latlon
292 elif isstr(lat_ghll): # "lat, lon" or "geohash"
293 ll = lat_ghll.replace(_COMMA_, _SPACE_).split()
294 if len(ll) > 1:
295 gh, ll = _GH.encode2(ll[0], ll[1], precision, eps)
296 else:
297 gh, ll = lat_ghll.lower(), None
298 _ = _GH.swne4(gh, mask5=()) # validate
299 else: # assume LatLon
300 try:
301 gh, ll = _GH.encode2(lat_ghll.lat, lat_ghll.lon, precision, eps)
302 except AttributeError:
303 raise _xStrError(Geohash, ghll=lat_ghll, Error=GeohashError)
304 else:
305 gh, ll = _GH.encode2(lat_ghll, lon, precision, eps)
307 self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=lat_ghll))
308 self._latlon = ll
309 return self
311 @deprecated_property_RO
312 def ab(self):
313 '''DEPRECATED, use property C{philam}.'''
314 return self.philam
316 def adjacent(self, direction, **name):
317 '''Determine the adjacent cell in the given compass direction.
319 @arg direction: Compass direction ('N', 'S', 'E' or 'W').
320 @kwarg name: Optional C{B{name}=NN} (C{str}) otherwise this
321 cell's name, either extended with C{.D}irection.
323 @return: Geohash of adjacent cell (L{Geohash}).
325 @raise GeohashError: Invalid geohash or B{C{direction}}.
326 '''
327 # based on <https://GitHub.com/DaveTroy/geohash-js>
329 D = direction[:1].upper()
330 if D not in _GH.Neighbors:
331 raise GeohashError(direction=direction)
333 e = len(self) & 1 # int(isodd(len(self)))
335 c = self[-1:] # last hash char
336 i = _GH.Neighbors[D][e].find(c)
337 if i < 0:
338 raise GeohashError(geohash=self)
340 p = self[:-1] # hash without last char
341 # check for edge-cases which don't share common prefix
342 if p and (c in _GH.Borders[D][e]):
343 p = Geohash(p).adjacent(D)
345 n = self._name__(name)
346 if n:
347 n = _DOT_(n, D)
348 # append letter for direction to parent
349 return Geohash(p + _GH.EncodeB32[i], name=n)
351 @Property_RO
352 def _bounds(self):
353 '''(INTERNAL) Cache for L{bounds}.
354 '''
355 return bounds(self)
357 def bounds(self, LatLon=None, **LatLon_kwds):
358 '''Return the lower-left SW and upper-right NE bounds of this
359 geohash cell.
361 @kwarg LatLon: Optional class to return I{bounds} (C{LatLon})
362 or C{None}.
363 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
364 arguments, ignored if C{B{LatLon} is None}.
366 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
367 or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if
368 C{B{LatLon} is None},
369 '''
370 r = self._bounds
371 return r if LatLon is None else \
372 _2bounds(LatLon, LatLon_kwds, *r, name=self.name)
374 def _distanceTo(self, func_, other, **kwds):
375 '''(INTERNAL) Helper for distances, see C{.formy._distanceTo*}.
376 '''
377 lls = self.latlon + _2Geohash(other).latlon
378 return func_(*lls, **kwds)
380 def distanceTo(self, other):
381 '''Estimate the distance between this and an other geohash
382 based the cell sizes.
384 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
386 @return: Approximate distance (C{meter}).
388 @raise TypeError: The B{C{other}} is not a L{Geohash},
389 C{LatLon} or C{str}.
390 '''
391 other = _2Geohash(other)
393 n = min(len(self), len(other), len(_GH.Sizes))
394 if n:
395 for n in range(n):
396 if self[n] != other[n]:
397 break
398 return _GH.Sizes[n].radius
400 @deprecated_method
401 def distance1To(self, other): # PYCHOK no cover
402 '''DEPRECATED, use method L{distanceTo}.'''
403 return self.distanceTo(other)
405 distance1 = distance1To
407 @deprecated_method
408 def distance2To(self, other, radius=R_M, adjust=False, wrap=False): # PYCHOK no cover
409 '''DEPRECATED, use method L{equirectangularTo}.'''
410 return self.equirectangularTo(other, radius=radius, adjust=adjust, wrap=wrap)
412 distance2 = distance2To
414 @deprecated_method
415 def distance3To(self, other, radius=R_M, wrap=False): # PYCHOK no cover
416 '''DEPRECATED, use method L{haversineTo}.'''
417 return self.haversineTo(other, radius=radius, wrap=wrap)
419 distance3 = distance3To
421 def equirectangularTo(self, other, radius=R_M, **adjust_limit_wrap):
422 '''Approximate the distance between this and an other geohash
423 using function L{pygeodesy.equirectangular}.
425 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
426 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
427 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
428 or C{None}, see function L{pygeodesy.equirectangular}.
429 @kwarg adjust_limit_wrap: Optional keyword arguments for function
430 L{pygeodesy.equirectangular4}, overriding defaults
431 C{B{adjust}=False, B{limit}=None} and C{B{wrap}=False}.
433 @return: Distance (C{meter}, same units as B{C{radius}} or the ellipsoid
434 or datum axes or C{radians I{squared}} if B{C{radius} is None}
435 or C{0}).
437 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon} or
438 C{str} or invalid B{C{radius}}.
440 @see: U{Local, flat earth approximation
441 <https://www.EdWilliams.org/avform.htm#flat>}, functions
442 '''
443 lls = self.latlon + _2Geohash(other).latlon
444 kwds = _xkwds(adjust_limit_wrap, adjust=False, limit=None, wrap=False)
445 return _formy.equirectangular( *lls, radius=radius, **kwds) if radius else \
446 _formy.equirectangular4(*lls, **kwds).distance2
448 def euclideanTo(self, other, **radius_adjust_wrap):
449 '''Approximate the distance between this and an other geohash using
450 function L{pygeodesy.euclidean}.
452 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
453 @kwarg radius_adjust_wrap: Optional keyword arguments for function
454 L{pygeodesy.euclidean}.
456 @return: Distance (C{meter}, same units as B{C{radius}} or the
457 ellipsoid or datum axes).
459 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
460 or C{str} or invalid B{C{radius}}.
461 '''
462 return self._distanceTo(_formy.euclidean, other, **radius_adjust_wrap)
464 def haversineTo(self, other, **radius_wrap):
465 '''Compute the distance between this and an other geohash using
466 the L{pygeodesy.haversine} formula.
468 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
469 @kwarg radius_wrap: Optional keyword arguments for function
470 L{pygeodesy.haversine}.
472 @return: Distance (C{meter}, same units as B{C{radius}} or the
473 ellipsoid or datum axes).
475 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
476 or C{str} or invalid B{C{radius}}.
477 '''
478 return self._distanceTo(_formy.haversine, other, **radius_wrap)
480 @Property_RO
481 def latlon(self):
482 '''Get the lat- and longitude of (the approximate center of)
483 this geohash as a L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
484 '''
485 lat, lon = self._latlon or _2center(self.bounds())
486 return LatLon2Tuple(lat, lon, name=self.name)
488 @Property_RO
489 def neighbors(self):
490 '''Get all 8 adjacent cells as a L{Neighbors8Dict}C{(N, NE,
491 E, SE, S, SW, W, NW)} of L{Geohash}es.
492 '''
493 return Neighbors8Dict(N=self.N, NE=self.NE, E=self.E, SE=self.SE,
494 S=self.S, SW=self.SW, W=self.W, NW=self.NW,
495 name=self.name)
497 @Property_RO
498 def philam(self):
499 '''Get the lat- and longitude of (the approximate center of)
500 this geohash as a L{PhiLam2Tuple}C{(phi, lam)} in C{radians}.
501 '''
502 return PhiLam2Tuple(map2(radians, self.latlon), name=self.name) # *map2
504 @Property_RO
505 def precision(self):
506 '''Get this geohash's precision (C{int}).
507 '''
508 return len(self)
510 @Property_RO
511 def resolution2(self):
512 '''Get the I{lon-} and I{latitudinal} resolution of this cell
513 in a L{Resolutions2Tuple}C{(res1, res2)}, both in C{degrees}.
514 '''
515 return resolution2(self.precision, self.precision)
517 @deprecated_property_RO
518 def sizes(self):
519 '''DEPRECATED on 2024.07.28, use property C{Geohash.sizes3}.'''
520 t = self.sizes3
521 return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
523 @Property_RO
524 def sizes3(self):
525 '''Get the lat-, longitudinal and radial size of this cell in
526 a L{Sizes3Tuple}C{(height, width, radius)}, all in C{meter}.
527 '''
528 z = _GH.Sizes
529 n = min(max(self.precision, 1), len(z) - 1)
530 return Sizes3Tuple(z[n], name=self.name)
532 def toLatLon(self, LatLon=None, **LatLon_kwds):
533 '''Return (the approximate center of) this geohash cell
534 as an instance of the supplied C{LatLon} class.
536 @arg LatLon: Class to use (C{LatLon}) or C{None}.
537 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
538 arguments, ignored if C{B{LatLon} is None}.
540 @return: This geohash location (B{C{LatLon}}) or if C{B{LatLon}
541 is None}, a L{LatLon2Tuple}C{(lat, lon)}.
543 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
544 '''
545 return self.latlon if LatLon is None else _xnamed(LatLon(
546 *self.latlon, **LatLon_kwds), self.name)
548 def vincentysTo(self, other, **radius_wrap):
549 '''Compute the distance between this and an other geohash using
550 the L{pygeodesy.vincentys} formula.
552 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
553 @kwarg radius_wrap: Optional keyword arguments for function
554 L{pygeodesy.vincentys}.
556 @return: Distance (C{meter}, same units as B{C{radius}} or the
557 ellipsoid or datum axes).
559 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
560 or C{str} or invalid B{C{radius}}.
561 '''
562 return self._distanceTo(_formy.vincentys, other, **radius_wrap)
564 @Property_RO
565 def E(self):
566 '''Get the cell East of this (L{Geohash}).
567 '''
568 return self.adjacent(_E_)
570 @Property_RO
571 def N(self):
572 '''Get the cell North of this (L{Geohash}).
573 '''
574 return self.adjacent(_N_)
576 @Property_RO
577 def NE(self):
578 '''Get the cell NorthEast of this (L{Geohash}).
579 '''
580 return self.N.E
582 @Property_RO
583 def NW(self):
584 '''Get the cell NorthWest of this (L{Geohash}).
585 '''
586 return self.N.W
588 @Property_RO
589 def S(self):
590 '''Get the cell South of this (L{Geohash}).
591 '''
592 return self.adjacent(_S_)
594 @Property_RO
595 def SE(self):
596 '''Get the cell SouthEast of this (L{Geohash}).
597 '''
598 return self.S.E
600 @Property_RO
601 def SW(self):
602 '''Get the cell SouthWest of this (L{Geohash}).
603 '''
604 return self.S.W
606 @Property_RO
607 def W(self):
608 '''Get the cell West of this (L{Geohash}).
609 '''
610 return self.adjacent(_W_)
613class Geohashed(object):
614 '''A cache of en- and decoded geohashes of one precision.
615 '''
616 _nn = None, # 1-tuple
618 def __init__(self, precision, ndigits=None):
619 '''New L{Geohashed} cache.
621 @arg precision: The geohash encoded length (C{int}, 1..12).
622 @kwarg ndigits: Optional number of digits to round C{lat}
623 and C{lon} to cache keys (C{int}, typically
624 C{B{ndigits}=B{precision}}) or C{None} for
625 no rounding.
626 '''
627 self._p = _2Precision(precision)
628 if ndigits is None:
629 self._ab2 = self._ab2float
630 else:
631 self._ab2 = self._ab2round
632 n = Int(ndigits=ndigits)
633 self._nn = n, n
634 self.clear()
636 def __len__(self):
637 '''Return the number of I{unigue} geohashes (C{int}).
638 '''
639 d = self._d
640 d = set(d.keys())
641 n = len(d)
642 for e in self._e.values():
643 e = set(e.values())
644 n += len(e - d)
645 return n
647 def _ab2(self, *ll): # overwritten
648 '''(INTERNAL) Make encoded keys C{a, b}.
649 '''
650 return ll
652 def _ab2float(self, *ll):
653 '''(INTERNAL) Make encoded keys C{a, b}.
654 '''
655 return map(float, ll)
657 def _ab2round(self, *ll):
658 '''(INTERNAL) Make encoded keys C{a, b}.
659 '''
660 return map(round, ll, self._nn) # strict=True
662 def clear(self):
663 '''Clear the C{en-} and C{decoded} cache.
664 '''
665 self._e = {}
666 self._d = {}
668 def decoded(self, geohash, encoded=False):
669 '''Get and cache the C{(lat, lon)} for C{geohash}, see L{decode<pygeodesy.geohash.decode>}.
671 @kwarg encoded: If C{True}, cache the result as C{encoded}.
673 @return: The C{(lat, lon}) pair for C{geohash}.
674 '''
675 try:
676 ll = self._d[geohash]
677 except KeyError:
678 self._d[geohash] = ll = _GH.decode2(geohash)
679 if encoded:
680 a, b = self._ab2(*ll)
681 try:
682 _ = self._e[b][a]
683 except KeyError:
684 self._e.setdefault(b, {})[a] = geohash
685 return ll
687 def encoded(self, lat, lon, decoded=False):
688 '''Get and cache the C{geohash} for C{(lat, lon)}, see L{encode<pygeodesy.geohash.encode>}.
690 @kwarg decoded: If C{True}, cache the result as C{decoded}.
692 @return: The C{geohash} for pair C{(lat, lon}).
693 '''
694 lat, lon = ll = _2fll(lat, lon)
695 a, b = self._ab2(*ll)
696 try:
697 gh = self._e[b][a]
698 except KeyError:
699 gh = _GH.encode(lat, lon, self._p, 0)
700 self._e.setdefault(b, {})[a] = gh
701 if decoded and gh not in self._d:
702 self._d[gh] = ll
703 return gh
705 @property_RO
706 def len2(self):
707 '''Return 2-tuple C{(lencoded, ldecoded)} with the C{len}gths of the
708 C{en-} and C{decoded} cache.
709 '''
710 return sum(len(e) for e in self._e.values()), len(self._d)
712 @Property_RO
713 def ndigits(self):
714 '''Get the rounding (C{int} or C{None}).
715 '''
716 return self._nn[0]
718 @Property_RO
719 def precision(self):
720 '''Get the C{precision} (C{int}).
721 '''
722 return self._p
725class GeohashError(_ValueError):
726 '''Geohash encode, decode or other L{Geohash} issue.
727 '''
728 pass
731class Neighbors8Dict(_NamedDict):
732 '''8-Dict C{(N, NE, E, SE, S, SW, W, NW)} of L{Geohash}es,
733 providing key I{and} attribute access to the items.
734 '''
735 _Keys_ = (_N_, _NE_, _E_, _SE_, _S_, _SW_, _W_, _NW_)
737 def __init__(self, **kwds): # PYCHOK no *args
738 kwds = _xkwds(kwds, **_Neighbors8Defaults)
739 _NamedDict.__init__(self, **kwds) # name=...
742_Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
743 len(Neighbors8Dict._Keys_))) # XXX frozendict
746class Resolutions2Tuple(_NamedTuple):
747 '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
748 secondary I{(latitudinal)} resolution, both in C{degrees}.
749 '''
750 _Names_ = ('res1', 'res2')
751 _Units_ = ( Degrees_, Degrees_)
753 @property_RO
754 def lat(self):
755 '''Get the secondary, latitudinal resolution (C{degrees}).
756 '''
757 return self[1]
759 @property_RO
760 def lon(self):
761 '''Get the primary, longitudinal resolution (C{degrees}).
762 '''
763 return self[0]
766class Sizes3Tuple(_NamedTuple):
767 '''3-Tuple C{(height, width, radius)} with latitudinal C{height},
768 longitudinal C{width} and area C{radius}, all in C{meter}.
769 '''
770 _Names_ = (_height_, _width_, _radius_)
771 _Units_ = ( Meter, Meter, Meter)
774def bounds(geohash, LatLon=None, **LatLon_kwds):
775 '''Returns the lower-left SW and upper-right NE corners of a geohash.
777 @arg geohash: To be "bound" (L{Geohash}).
778 @kwarg LatLon: Optional class to return the bounds (C{LatLon}) or C{None}.
779 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
780 ignored if C{B{LatLon} is None}.
782 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)}, each a B{C{LatLon}}
783 or if C{B{LatLon} is None}, a L{Bounds4Tuple}C{(latS, lonW,
784 latN, lonE)}.
786 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or
787 C{str} or invalid B{C{LatLon}} or invalid B{C{LatLon_kwds}}.
789 @raise GeohashError: Invalid or C{null} B{C{geohash}}.
790 '''
791 swne = _GH.swne4(geohash)
792 return _2bounds(LatLon, LatLon_kwds, *swne,
793 name=nameof(geohash)) # _or_nameof=geohash
796def decode(geohash):
797 '''Decode a geohash to lat-/longitude of the (approximate
798 centre of) geohash cell to reasonable precision.
800 @arg geohash: To be decoded (L{Geohash}).
802 @return: 2-Tuple C{(latStr, lonStr)}, both C{str}.
804 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
805 C{LatLon} or C{str}.
807 @raise GeohashError: Invalid or null B{C{geohash}}.
808 '''
809 # round to near centre without excessive precision to
810 # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros
811 swne = _GH.swne4(geohash)
812 return _2latlon(*swne, fstr=_MODS.streprs.fstr)
815def decode2(geohash, LatLon=None, **LatLon_kwds):
816 '''Decode a geohash to lat-/longitude of the (approximate center
817 of) geohash cell to reasonable precision.
819 @arg geohash: To be decoded (L{Geohash}).
820 @kwarg LatLon: Optional class to return the location (C{LatLon})
821 or C{None}.
822 @kwarg LatLon_kwds: Optional, addtional B{C{LatLon}} keyword
823 arguments, ignored if C{B{LatLon} is None}.
825 @return: L{LatLon2Tuple}C{(lat, lon)}, both C{degrees} if
826 C{B{LatLon} is None}, otherwise a B{C{LatLon}} instance.
828 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
829 C{LatLon} or C{str}.
831 @raise GeohashError: Invalid or null B{C{geohash}}.
832 '''
833 ll = _GH.decode2(geohash)
834 r = LatLon2Tuple(ll) if LatLon is None else \
835 LatLon( *ll, **LatLon_kwds)
836 return _xnamed(r, name__=decode2)
839@deprecated_function
840def decode_error(geohash):
841 '''DEPRECATED on 2024.07.28, use L{geohash.decode_error2}.'''
842 return decode_error2(geohash)
845def decode_error2(geohash):
846 '''Return the lat- and longitude decoding error for a geohash.
848 @arg geohash: To be decoded (L{Geohash}).
850 @return: A L{LatLon2Tuple}C{(lat, lon)} with the lat- and
851 longitudinal errors in (C{degrees}).
853 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
854 C{LatLon} or C{str}.
856 @raise GeohashError: Invalid or null B{C{geohash}}.
857 '''
858 s, w, n, e = _GH.swne4(geohash)
859 return LatLon2Tuple((n - s) * _0_5, # lat error
860 (e - w) * _0_5) # lon error
863def distance_(geohash1, geohash2):
864 '''Estimate the distance between two geohash (from the cell sizes).
866 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
867 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
869 @return: Approximate distance (C{meter}).
871 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
872 L{Geohash}, C{LatLon} or C{str}.
873 '''
874 return _2Geohash(geohash1).distanceTo(geohash2)
877@deprecated_function
878def distance1(geohash1, geohash2):
879 '''DEPRECATED, use L{geohash.distance_}.'''
880 return distance_(geohash1, geohash2)
883@deprecated_function
884def distance2(geohash1, geohash2):
885 '''DEPRECATED, use L{geohash.equirectangular4}.'''
886 return equirectangular4(geohash1, geohash2)
889@deprecated_function
890def distance3(geohash1, geohash2):
891 '''DEPRECATED, use L{geohash.haversine_}.'''
892 return haversine_(geohash1, geohash2)
895def encode(lat, lon, precision=None, eps=EPS):
896 '''Encode a lat-/longitude as a C{geohash}, either to the specified
897 precision or if not provided, to an inferred precision.
899 @arg lat: Latitude (C{degrees90}).
900 @arg lon: Longitude (C{degrees180}).
901 @kwarg precision: The desired geohash length (C{int} 1..12) or
902 C{None} or C{0} for inferred.
903 @kwarg eps: Optional inference tolerance (C{degrees}), ignored
904 if B{C{precision}} is not C{None} or C{0}.
906 @return: The C{geohash} (C{str}).
908 @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
909 '''
910 gh, _ = _GH.encode2(lat, lon, precision, eps)
911 return gh
914def equirectangular4(geohash1, geohash2, radius=R_M):
915 '''Approximate the distance between two geohashes using the
916 L{pygeodesy.equirectangular} formula.
918 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
919 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
920 @kwarg radius: Mean earth radius (C{meter}) or C{None}, see method
921 L{Geohash.equirectangularTo}.
923 @return: Approximate distance (C{meter}, same units as B{C{radius}}),
924 see method L{Geohash.equirectangularTo}.
926 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
927 L{Geohash}, C{LatLon} or C{str}.
928 '''
929 return _2Geohash(geohash1).equirectangularTo(geohash2, radius=radius)
932def euclidean_(geohash1, geohash2, **radius_adjust_wrap):
933 '''Approximate the distance between two geohashes using the
934 L{pygeodesy.euclidean} formula.
936 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
937 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
938 @kwarg radius_adjust_wrap: Optional keyword arguments for function
939 L{pygeodesy.euclidean}.
941 @return: Approximate distance (C{meter}, same units as B{C{radius}}).
943 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
944 L{Geohash}, C{LatLon} or C{str}.
945 '''
946 return _2Geohash(geohash1).euclideanTo(geohash2, **radius_adjust_wrap)
949def haversine_(geohash1, geohash2, **radius_wrap):
950 '''Compute the great-circle distance between two geohashes
951 using the L{pygeodesy.haversine} formula.
953 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
954 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
955 @kwarg radius_wrap: Optional keyword arguments for function
956 L{pygeodesy.haversine}.
958 @return: Great-circle distance (C{meter}, same units as
959 B{C{radius}}).
961 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
962 not a L{Geohash}, C{LatLon} or C{str}.
963 '''
964 return _2Geohash(geohash1).haversineTo(geohash2, **radius_wrap)
967def neighbors(geohash):
968 '''Return the L{Geohash}es for all 8 adjacent cells.
970 @arg geohash: Cell for which neighbors are requested
971 (L{Geohash} or C{str}).
973 @return: A L{Neighbors8Dict}C{(N, NE, E, SE, S, SW, W, NW)}
974 of L{Geohash}es.
976 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
977 C{LatLon} or C{str}.
978 '''
979 return _2Geohash(geohash).neighbors
982def precision(res1, res2=None):
983 '''Determine the L{Geohash} precisions to meet a or both given
984 (geographic) resolutions.
986 @arg res1: The required primary I{(longitudinal)} resolution
987 (C{degrees}).
988 @kwarg res2: Optional, required secondary I{(latitudinal)}
989 resolution (C{degrees}).
991 @return: The L{Geohash} precision or length (C{int}, 1..12).
993 @raise GeohashError: Invalid B{C{res1}} or B{C{res2}}.
995 @see: C++ class U{Geohash
996 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
997 '''
998 r = Degrees_(res1=res1, low=_0_0, Error=GeohashError)
999 N = res2 is None
1000 t = r, (r if N else Degrees_(res2=res2, low=_0_0, Error=GeohashError))
1001 for p in range(1, _MaxPrec):
1002 if resolution2(p, (None if N else p)) <= t:
1003 return p
1004 return _MaxPrec
1007def resolution2(prec1, prec2=None):
1008 '''Determine the (geographic) resolutions of given L{Geohash}
1009 precisions.
1011 @arg prec1: The given primary I{(longitudinal)} precision
1012 (C{int} 1..12).
1013 @kwarg prec2: Optional, secondary I{(latitudinal)} precision
1014 (C{int} 1..12).
1016 @return: L{Resolutions2Tuple}C{(res1, res2)} with the
1017 (geographic) resolutions in C{degrees}, where
1018 C{res2 B{is} res1} if no B{C{prec2}} is given.
1020 @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
1022 @see: I{Karney}'s C++ class U{Geohash
1023 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
1024 '''
1025 lon = _2res(_360_0, prec1=prec1)
1026 lat = lon if prec2 is None else \
1027 _2res(_180_0, prec2=prec2)
1028 return Resolutions2Tuple(lon, lat)
1031@deprecated_function
1032def sizes(geohash):
1033 '''DEPRECATED on 2024.07.28, use function L{pygeodesy.geohash.sizes3}.'''
1034 t = sizes3(geohash)
1035 return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
1038def sizes3(geohash):
1039 '''Return the lat-, longitudinal and radial size of this L{Geohash} cell.
1041 @arg geohash: Cell for which size are required (L{Geohash} or C{str}).
1043 @return: A L{Sizes3Tuple}C{(height, width, radius)}, all C{meter}.
1045 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or C{str}.
1046 '''
1047 return _2Geohash(geohash).sizes3
1050def vincentys_(geohash1, geohash2, **radius_wrap):
1051 '''Compute the distance between two geohashes using the
1052 L{pygeodesy.vincentys} formula.
1054 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
1055 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
1056 @kwarg radius_wrap: Optional keyword arguments for function
1057 L{pygeodesy.vincentys}.
1059 @return: Distance (C{meter}, same units as B{C{radius}}).
1061 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
1062 L{Geohash}, C{LatLon} or C{str}.
1063 '''
1064 return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap)
1067__all__ += _ALL_DOCS(bounds, # functions
1068 decode, decode2, decode_error2, distance_,
1069 encode, equirectangular4, euclidean_, haversine_,
1070 neighbors, precision, resolution2, sizes3, vincentys_,
1071 decode_error, sizes) # DEPRECATED
1073if __name__ == '__main__':
1075 from pygeodesy.internals import printf, _versions
1076 from timeit import timeit
1078 for f, p in (('encode', _MaxPrec), ('infer', None)):
1080 def _t(prec=p):
1081 i = 0
1082 for lat in range(-90, 90, 3):
1083 for lon in range(-180, 180, 7):
1084 _ = encode(lat, lon, prec)
1085 i += 1
1086 return i
1088 i = _t() # prime
1089 n = 10
1090 t = timeit(_t, number=n) / (i * n)
1091 printf('%s %.3f usec, %s', f, t * 1e6, _versions())
1093# % python3.12 -m pygeodesy.geohash
1094# encode 10.145 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1095# infer 14.780 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1096# or about 6.56 and 74.12 times faster than pygeodesy 24.7.24 and older:
1097# encode 66.524 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
1098# infer 1095.386 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
1100# **) MIT License
1101#
1102# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1103#
1104# Permission is hereby granted, free of charge, to any person obtaining a
1105# copy of this software and associated documentation files (the "Software"),
1106# to deal in the Software without restriction, including without limitation
1107# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1108# and/or sell copies of the Software, and to permit persons to whom the
1109# Software is furnished to do so, subject to the following conditions:
1110#
1111# The above copyright notice and this permission notice shall be included
1112# in all copies or substantial portions of the Software.
1113#
1114# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1115# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1116# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1117# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1118# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1119# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1120# OTHER DEALINGS IN THE SOFTWARE.