Coverage for pygeodesy/geohash.py: 97%
292 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-02 14:35 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-02 14:35 -0400
2# -*- coding: utf-8 -*-
4u'''Geohash en-/decoding.
6Classes L{Geohash} and L{GeohashError} and several functions to encode,
7decode 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>}, U{Geohash
14<https://GitHub.com/vinsci/geohash>}, U{PyGeohash
15<https://PyPI.org/project/pygeohash>} and U{Geohash-Javascript
16<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
25# from pygeodesy import formy as _formy # _MODS
26from pygeodesy.interns import NN, _COMMA_, _DOT_, _E_, _N_, _NE_, _NW_, \
27 _S_, _SE_, _SW_, _W_
28from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER
29from pygeodesy.named import _NamedDict, _NamedTuple, nameof, _xnamed
30from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \
31 LatLon2Tuple, PhiLam2Tuple
32from pygeodesy.props import deprecated_function, deprecated_method, \
33 deprecated_property_RO, Property_RO, property_RO
34from pygeodesy.streprs import fstr
35from pygeodesy.units import Degrees_, Int, Lat, Lon, Precision_, Str, \
36 _xStrError
38from math import fabs, ldexp, log10, radians
40__all__ = _ALL_LAZY.geohash
41__version__ = '23.12.18'
44class _GH(object):
45 '''(INTERNAL) Lazily defined constants.
46 '''
47 def _4d(self, n, e, s, w): # helper
48 return dict(N=(n, e), S=(s, w),
49 E=(e, n), W=(w, s))
51 @Property_RO
52 def Borders(self):
53 return self._4d('prxz', 'bcfguvyz', '028b', '0145hjnp')
55 Bounds4 = (_N_90_0, _N_180_0, _90_0, _180_0)
57 @Property_RO
58 def DecodedBase32(self): # inverse GeohashBase32 map
59 return dict((c, i) for i, c in enumerate(self.GeohashBase32))
61 # Geohash-specific base32 map
62 GeohashBase32 = '0123456789bcdefghjkmnpqrstuvwxyz' # no a, i, j and o
64 @Property_RO
65 def Neighbors(self):
66 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy',
67 'bc01fg45238967deuvhjyznpkmstqrwx',
68 '14365h7k9dcfesgujnmqp0r2twvyx8zb',
69 '238967debc01fg45kmstqrwxuvhjyznp')
71 @Property_RO
72 def Sizes(self): # lat-, lon and radial size (in meter)
73 # ... where radial = sqrt(latSize * lonWidth / PI)
74 return (_floatuple(20032e3, 20000e3, 11292815.096), # 0
75 _floatuple( 5003e3, 5000e3, 2821794.075), # 1
76 _floatuple( 650e3, 1225e3, 503442.397), # 2
77 _floatuple( 156e3, 156e3, 88013.575), # 3
78 _floatuple( 19500, 39100, 15578.683), # 4
79 _floatuple( 4890, 4890, 2758.887), # 5
80 _floatuple( 610, 1220, 486.710), # 6
81 _floatuple( 153, 153, 86.321), # 7
82 _floatuple( 19.1, 38.2, 15.239), # 8
83 _floatuple( 4.77, 4.77, 2.691), # 9
84 _floatuple( 0.596, 1.19, 0.475), # 10
85 _floatuple( 0.149, 0.149, 0.084), # 11
86 _floatuple( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec
88_GH = _GH() # PYCHOK singleton
89_MaxPrec = 12
92def _2bounds(LatLon, LatLon_kwds, s, w, n, e, name=NN):
93 '''(INTERNAL) Return SW and NE bounds.
94 '''
95 if LatLon is None:
96 r = Bounds4Tuple(s, w, n, e, name=name)
97 else:
98 sw = _xnamed(LatLon(s, w, **LatLon_kwds), name)
99 ne = _xnamed(LatLon(n, e, **LatLon_kwds), name)
100 r = Bounds2Tuple(sw, ne, name=name)
101 return r # _xnamed(r, name)
104def _2center(bounds):
105 '''(INTERNAL) Return the C{bounds} center.
106 '''
107 return (favg(bounds.latN, bounds.latS),
108 favg(bounds.lonE, bounds.lonW))
111def _2fll(lat, lon, *unused):
112 '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
113 '''
114 # lat, lon = parseDMS2(lat, lon)
115 return (Lat(lat, Error=GeohashError),
116 Lon(lon, Error=GeohashError))
119def _2Geohash(geohash):
120 '''(INTERNAL) Check or create a Geohash instance.
121 '''
122 return geohash if isinstance(geohash, Geohash) else \
123 Geohash(geohash)
126def _2geostr(geohash):
127 '''(INTERNAL) Check a geohash string.
128 '''
129 try:
130 if not (0 < len(geohash) <= _MaxPrec):
131 raise ValueError
132 geostr = geohash.lower()
133 for c in geostr:
134 if c not in _GH.DecodedBase32:
135 raise ValueError
136 return geostr
137 except (AttributeError, TypeError, ValueError) as x:
138 raise GeohashError(Geohash.__name__, geohash, cause=x)
141class Geohash(Str):
142 '''Geohash class, a named C{str}.
143 '''
144 # no str.__init__ in Python 3
145 def __new__(cls, cll, precision=None, name=NN):
146 '''New L{Geohash} from an other L{Geohash} instance or C{str}
147 or from a C{LatLon} instance or C{str}.
149 @arg cll: Cell or location (L{Geohash}, C{LatLon} or C{str}).
150 @kwarg precision: Optional, the desired geohash length (C{int}
151 1..12), see function L{geohash.encode} for
152 some examples.
153 @kwarg name: Optional name (C{str}).
155 @return: New L{Geohash}.
157 @raise GeohashError: INValid or non-alphanumeric B{C{cll}}.
159 @raise TypeError: Invalid B{C{cll}}.
160 '''
161 ll = None
163 if isinstance(cll, Geohash):
164 gh = _2geostr(str(cll))
166 elif isstr(cll):
167 if _COMMA_ in cll:
168 ll = _2fll(*parse3llh(cll))
169 gh = encode(*ll, precision=precision)
170 else:
171 gh = _2geostr(cll)
173 else: # assume LatLon
174 try:
175 ll = _2fll(cll.lat, cll.lon)
176 gh = encode(*ll, precision=precision)
177 except AttributeError:
178 raise _xStrError(Geohash, cll=cll, Error=GeohashError)
180 self = Str.__new__(cls, gh, name=name or nameof(cll))
181 self._latlon = ll
182 return self
184 @deprecated_property_RO
185 def ab(self):
186 '''DEPRECATED, use property C{philam}.'''
187 return self.philam
189 def adjacent(self, direction, name=NN):
190 '''Determine the adjacent cell in the given compass direction.
192 @arg direction: Compass direction ('N', 'S', 'E' or 'W').
193 @kwarg name: Optional name (C{str}), otherwise the name
194 of this cell plus C{.D}irection.
196 @return: Geohash of adjacent cell (L{Geohash}).
198 @raise GeohashError: Invalid geohash or B{C{direction}}.
199 '''
200 # based on <https://GitHub.com/DaveTroy/geohash-js>
202 D = direction[:1].upper()
203 if D not in _GH.Neighbors:
204 raise GeohashError(direction=direction)
206 e = 1 if isodd(len(self)) else 0
208 c = self[-1:] # last hash char
209 i = _GH.Neighbors[D][e].find(c)
210 if i < 0:
211 raise GeohashError(geohash=self)
213 p = self[:-1] # hash without last char
214 # check for edge-cases which don't share common prefix
215 if p and (c in _GH.Borders[D][e]):
216 p = Geohash(p).adjacent(D)
218 n = name or self.name
219 if n:
220 n = _DOT_(n, D)
221 # append letter for direction to parent
222 return Geohash(p + _GH.GeohashBase32[i], name=n)
224 @Property_RO
225 def _bounds(self):
226 '''(INTERNAL) Cache for L{bounds}.
227 '''
228 return bounds(self)
230 def bounds(self, LatLon=None, **LatLon_kwds):
231 '''Return the lower-left SW and upper-right NE bounds of this
232 geohash cell.
234 @kwarg LatLon: Optional class to return I{bounds} (C{LatLon})
235 or C{None}.
236 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
237 arguments, ignored if B{C{LatLon}} is C{None}.
239 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
240 or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if
241 C{B{LatLon} is None},
242 '''
243 r = self._bounds
244 return r if LatLon is None else \
245 _2bounds(LatLon, LatLon_kwds, *r, name=self.name)
247 def _distanceTo(self, func_, other, **kwds):
248 '''(INTERNAL) Helper for distances, see C{.formy._distanceTo*}.
249 '''
250 lls = self.latlon + _2Geohash(other).latlon
251 return func_(*lls, **kwds)
253 def distanceTo(self, other):
254 '''Estimate the distance between this and an other geohash
255 based the cell sizes.
257 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
259 @return: Approximate distance (C{meter}).
261 @raise TypeError: The B{C{other}} is not a L{Geohash},
262 C{LatLon} or C{str}.
263 '''
264 other = _2Geohash(other)
266 n = min(len(self), len(other), len(_GH.Sizes))
267 if n:
268 for n in range(n):
269 if self[n] != other[n]:
270 break
271 return _GH.Sizes[n][2]
273 @deprecated_method
274 def distance1To(self, other): # PYCHOK no cover
275 '''DEPRECATED, use method L{distanceTo}.'''
276 return self.distanceTo(other)
278 distance1 = distance1To
280 @deprecated_method
281 def distance2To(self, other, radius=R_M, adjust=False, wrap=False): # PYCHOK no cover
282 '''DEPRECATED, use method L{equirectangularTo}.'''
283 return self.equirectangularTo(other, radius=radius, adjust=adjust, wrap=wrap)
285 distance2 = distance2To
287 @deprecated_method
288 def distance3To(self, other, radius=R_M, wrap=False): # PYCHOK no cover
289 '''DEPRECATED, use method L{haversineTo}.'''
290 return self.haversineTo(other, radius=radius, wrap=wrap)
292 distance3 = distance3To
294 def equirectangularTo(self, other, radius=R_M, **adjust_limit_wrap):
295 '''Approximate the distance between this and an other geohash
296 using function L{pygeodesy.equirectangular}.
298 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
299 @kwarg radius: Mean earth radius, ellipsoid or datum
300 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
301 L{Datum} or L{a_f2Tuple}) or C{None}.
302 @kwarg adjust_limit_wrap: Optional keyword arguments for
303 function L{pygeodesy.equirectangular_},
304 overriding defaults C{B{adjust}=False,
305 B{limit}=None} and C{B{wrap}=False}.
307 @return: Distance (C{meter}, same units as B{C{radius}} or the
308 ellipsoid or datum axes or C{radians I{squared}} if
309 B{C{radius}} is C{None} or C{0}).
311 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
312 or C{str} or invalid B{C{radius}}.
314 @see: U{Local, flat earth approximation
315 <https://www.EdWilliams.org/avform.htm#flat>}, functions
316 '''
317 lls = self.latlon + _2Geohash(other).latlon
318 kwds = _xkwds(adjust_limit_wrap, adjust=False, limit=None, wrap=False)
319 m = self._formy
320 return m.equirectangular( *lls, radius=radius, **kwds) if radius else \
321 m.equirectangular_(*lls, **kwds).distance2
323 def euclideanTo(self, other, **radius_adjust_wrap):
324 '''Approximate the distance between this and an other geohash using
325 function L{pygeodesy.euclidean}.
327 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
328 @kwarg radius_adjust_wrap: Optional keyword arguments for function
329 L{pygeodesy.euclidean}.
331 @return: Distance (C{meter}, same units as B{C{radius}} or the
332 ellipsoid or datum axes).
334 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
335 or C{str} or invalid B{C{radius}}.
336 '''
337 return self._distanceTo(self._formy.euclidean, other, **radius_adjust_wrap)
339 @property_RO
340 def _formy(self):
341 '''(INTERNAL) Get the C{.formy} module, I{once}.
342 '''
343 Geohash._formy = f = _MODS.formy # overwrite property_RO
344 return f
346 def haversineTo(self, other, **radius_wrap):
347 '''Compute the distance between this and an other geohash using
348 the L{pygeodesy.haversine} formula.
350 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
351 @kwarg radius_wrap: Optional keyword arguments for function
352 L{pygeodesy.haversine}.
354 @return: Distance (C{meter}, same units as B{C{radius}} or the
355 ellipsoid or datum axes).
357 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
358 or C{str} or invalid B{C{radius}}.
359 '''
360 return self._distanceTo(self._formy.haversine, other, **radius_wrap)
362 @Property_RO
363 def latlon(self):
364 '''Get the lat- and longitude of (the approximate center of)
365 this geohash as a L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
366 '''
367 lat, lon = self._latlon or _2center(self.bounds())
368 return LatLon2Tuple(lat, lon, name=self.name)
370 @Property_RO
371 def neighbors(self):
372 '''Get all 8 adjacent cells as a L{Neighbors8Dict}C{(N, NE,
373 E, SE, S, SW, W, NW)} of L{Geohash}es.
374 '''
375 return Neighbors8Dict(N=self.N, NE=self.NE, E=self.E, SE=self.SE,
376 S=self.S, SW=self.SW, W=self.W, NW=self.NW,
377 name=self.name)
379 @Property_RO
380 def philam(self):
381 '''Get the lat- and longitude of (the approximate center of)
382 this geohash as a L{PhiLam2Tuple}C{(phi, lam)} in C{radians}.
383 '''
384 return PhiLam2Tuple(map2(radians, self.latlon), name=self.name) # *map2
386 @Property_RO
387 def precision(self):
388 '''Get this geohash's precision (C{int}).
389 '''
390 return len(self)
392 @Property_RO
393 def sizes(self):
394 '''Get the lat- and longitudinal size of this cell as
395 a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}).
396 '''
397 z = _GH.Sizes
398 n = min(len(z) - 1, max(self.precision, 1))
399 return LatLon2Tuple(z[n][:2], name=self.name) # *z XXX Height, Width?
401 def toLatLon(self, LatLon=None, **LatLon_kwds):
402 '''Return (the approximate center of) this geohash cell
403 as an instance of the supplied C{LatLon} class.
405 @arg LatLon: Class to use (C{LatLon}) or C{None}.
406 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
407 keyword arguments, ignored if
408 C{B{LatLon} is None}.
410 @return: This geohash location (B{C{LatLon}}) or a
411 L{LatLon2Tuple}C{(lat, lon)} if B{C{LatLon}}
412 is C{None}.
414 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
415 '''
416 return self.latlon if LatLon is None else _xnamed(LatLon(
417 *self.latlon, **LatLon_kwds), self.name)
419 def vincentysTo(self, other, **radius_wrap):
420 '''Compute the distance between this and an other geohash using
421 the L{pygeodesy.vincentys} formula.
423 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
424 @kwarg radius_wrap: Optional keyword arguments for function
425 L{pygeodesy.vincentys}.
427 @return: Distance (C{meter}, same units as B{C{radius}} or the
428 ellipsoid or datum axes).
430 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
431 or C{str} or invalid B{C{radius}}.
432 '''
433 return self._distanceTo(self._formy.vincentys, other, **radius_wrap)
435 @Property_RO
436 def N(self):
437 '''Get the cell North of this (L{Geohash}).
438 '''
439 return self.adjacent(_N_)
441 @Property_RO
442 def S(self):
443 '''Get the cell South of this (L{Geohash}).
444 '''
445 return self.adjacent(_S_)
447 @Property_RO
448 def E(self):
449 '''Get the cell East of this (L{Geohash}).
450 '''
451 return self.adjacent(_E_)
453 @Property_RO
454 def W(self):
455 '''Get the cell West of this (L{Geohash}).
456 '''
457 return self.adjacent(_W_)
459 @Property_RO
460 def NE(self):
461 '''Get the cell NorthEast of this (L{Geohash}).
462 '''
463 return self.N.E
465 @Property_RO
466 def NW(self):
467 '''Get the cell NorthWest of this (L{Geohash}).
468 '''
469 return self.N.W
471 @Property_RO
472 def SE(self):
473 '''Get the cell SouthEast of this (L{Geohash}).
474 '''
475 return self.S.E
477 @Property_RO
478 def SW(self):
479 '''Get the cell SouthWest of this (L{Geohash}).
480 '''
481 return self.S.W
484class GeohashError(_ValueError):
485 '''Geohash encode, decode or other L{Geohash} issue.
486 '''
487 pass
490class Neighbors8Dict(_NamedDict):
491 '''8-Dict C{(N, NE, E, SE, S, SW, W, NW)} of L{Geohash}es,
492 providing key I{and} attribute access to the items.
493 '''
494 _Keys_ = (_N_, _NE_, _E_, _SE_, _S_, _SW_, _W_, _NW_)
496 def __init__(self, **kwds): # PYCHOK no *args
497 kwds = _xkwds(kwds, **_Neighbors8Defaults)
498 _NamedDict.__init__(self, **kwds) # name=...
501_Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
502 len(Neighbors8Dict._Keys_))) # XXX frozendict
505def bounds(geohash, LatLon=None, **LatLon_kwds):
506 '''Returns the lower-left SW and upper-right NE corners of a geohash.
508 @arg geohash: To be bound (L{Geohash}).
509 @kwarg LatLon: Optional class to return the bounds (C{LatLon})
510 or C{None}.
511 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
512 arguments, ignored if C{B{LatLon} is None}.
514 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
515 or if B{C{LatLon}} is C{None}, a L{Bounds4Tuple}C{(latS,
516 lonW, latN, lonE)}.
518 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon}
519 or C{str} or invalid B{C{LatLon}} or invalid
520 B{C{LatLon_kwds}}.
522 @raise GeohashError: Invalid or C{null} B{C{geohash}}.
523 '''
524 gh = _2Geohash(geohash)
525 if len(gh) < 1:
526 raise GeohashError(geohash=geohash)
528 s, w, n, e = _GH.Bounds4
529 try:
530 d, _avg = True, favg
531 for c in gh.lower():
532 i = _GH.DecodedBase32[c]
533 for m in (16, 8, 4, 2, 1):
534 if d: # longitude
535 if i & m:
536 w = _avg(w, e)
537 else:
538 e = _avg(w, e)
539 else: # latitude
540 if i & m:
541 s = _avg(s, n)
542 else:
543 n = _avg(s, n)
544 d = not d
545 except KeyError:
546 raise GeohashError(geohash=geohash)
548 return _2bounds(LatLon, LatLon_kwds, s, w, n, e,
549 name=nameof(geohash))
552def _bounds3(geohash):
553 '''(INTERNAL) Return 3-tuple C{(bounds, height, width)}.
554 '''
555 b = bounds(geohash)
556 return b, (b.latN - b.latS), (b.lonE - b.lonW)
559def decode(geohash):
560 '''Decode a geohash to lat-/longitude of the (approximate
561 centre of) geohash cell to reasonable precision.
563 @arg geohash: To be decoded (L{Geohash}).
565 @return: 2-Tuple C{(latStr, lonStr)}, both C{str}.
567 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
568 C{LatLon} or C{str}.
570 @raise GeohashError: Invalid or null B{C{geohash}}.
571 '''
572 b, h, w = _bounds3(geohash)
573 lat, lon = _2center(b)
575 # round to near centre without excessive precision to
576 # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros
577 return (fstr(lat, prec=int(2 - log10(h))),
578 fstr(lon, prec=int(2 - log10(w)))) # strs!
581def decode2(geohash, LatLon=None, **LatLon_kwds):
582 '''Decode a geohash to lat-/longitude of the (approximate center
583 of) geohash cell to reasonable precision.
585 @arg geohash: To be decoded (L{Geohash}).
586 @kwarg LatLon: Optional class to return the location (C{LatLon})
587 or C{None}.
588 @kwarg LatLon_kwds: Optional, addtional B{C{LatLon}} keyword
589 arguments, ignored if C{B{LatLon} is None}.
591 @return: L{LatLon2Tuple}C{(lat, lon)}, both C{degrees} if
592 C{B{LatLon} is None}, otherwise a B{C{LatLon}} instance.
594 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
595 C{LatLon} or C{str}.
597 @raise GeohashError: Invalid or null B{C{geohash}}.
598 '''
599 t = map2(float, decode(geohash))
600 r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t
601 return _xnamed(r, decode2.__name__)
604def decode_error(geohash):
605 '''Return the relative lat-/longitude decoding errors for
606 this geohash.
608 @arg geohash: To be decoded (L{Geohash}).
610 @return: A L{LatLon2Tuple}C{(lat, lon)} with the lat- and
611 longitudinal errors in (C{degrees}).
613 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
614 C{LatLon} or C{str}.
616 @raise GeohashError: Invalid or null B{C{geohash}}.
617 '''
618 _, h, w = _bounds3(geohash)
619 return LatLon2Tuple(h * _0_5, # Height error
620 w * _0_5) # Width error
623def distance_(geohash1, geohash2):
624 '''Estimate the distance between two geohash (from the cell sizes).
626 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
627 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
629 @return: Approximate distance (C{meter}).
631 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
632 L{Geohash}, C{LatLon} or C{str}.
633 '''
634 return _2Geohash(geohash1).distanceTo(geohash2)
637@deprecated_function
638def distance1(geohash1, geohash2):
639 '''DEPRECATED, used L{geohash.distance_}.'''
640 return distance_(geohash1, geohash2)
643@deprecated_function
644def distance2(geohash1, geohash2):
645 '''DEPRECATED, used L{geohash.equirectangular_}.'''
646 return equirectangular_(geohash1, geohash2)
649@deprecated_function
650def distance3(geohash1, geohash2):
651 '''DEPRECATED, used L{geohash.haversine_}.'''
652 return haversine_(geohash1, geohash2)
655def encode(lat, lon, precision=None):
656 '''Encode a lat-/longitude as a C{geohash}, either to the specified
657 precision or if not provided, to an automatically evaluated
658 precision.
660 @arg lat: Latitude (C{degrees}).
661 @arg lon: Longitude (C{degrees}).
662 @kwarg precision: Optional, the desired geohash length (C{int}
663 1..12).
665 @return: The C{geohash} (C{str}).
667 @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
668 '''
669 lat, lon = _2fll(lat, lon)
671 if precision is None:
672 # Infer precision by refining geohash until
673 # it matches precision of supplied lat/lon.
674 for p in range(1, _MaxPrec + 1):
675 gh = encode(lat, lon, p)
676 ll = map2(float, decode(gh))
677 if fabs(lat - ll[0]) < EPS and \
678 fabs(lon - ll[1]) < EPS:
679 return gh
680 p = _MaxPrec
681 else:
682 p = Precision_(precision, Error=GeohashError, low=1, high=_MaxPrec)
684 b = i = 0
685 d, gh = True, []
686 s, w, n, e = _GH.Bounds4
688 _avg = favg
689 while p > 0:
690 i += i
691 if d: # bisect longitude
692 m = _avg(e, w)
693 if lon < m:
694 e = m
695 else:
696 w = m
697 i += 1
698 else: # bisect latitude
699 m = _avg(n, s)
700 if lat < m:
701 n = m
702 else:
703 s = m
704 i += 1
705 d = not d
707 b += 1
708 if b == 5:
709 # 5 bits gives a character:
710 # append it and start over
711 gh.append(_GH.GeohashBase32[i])
712 b = i = 0
713 p -= 1
715 return NN.join(gh)
718def equirectangular_(geohash1, geohash2, radius=R_M):
719 '''Approximate the distance between two geohashes using the
720 L{pygeodesy.equirectangular} formula.
722 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
723 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
724 @kwarg radius: Mean earth radius (C{meter}) or C{None}, see method
725 L{Geohash.equirectangularTo}.
727 @return: Approximate distance (C{meter}, same units as B{C{radius}}),
728 see method L{Geohash.equirectangularTo}.
730 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
731 L{Geohash}, C{LatLon} or C{str}.
732 '''
733 return _2Geohash(geohash1).equirectangularTo(geohash2, radius=radius)
736def euclidean_(geohash1, geohash2, **radius_adjust_wrap):
737 '''Approximate the distance between two geohashes using the
738 L{pygeodesy.euclidean} formula.
740 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
741 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
742 @kwarg radius_adjust_wrap: Optional keyword arguments for function
743 L{pygeodesy.euclidean}.
745 @return: Approximate distance (C{meter}, same units as B{C{radius}}).
747 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
748 L{Geohash}, C{LatLon} or C{str}.
749 '''
750 return _2Geohash(geohash1).euclideanTo(geohash2, **radius_adjust_wrap)
753def haversine_(geohash1, geohash2, **radius_wrap):
754 '''Compute the great-circle distance between two geohashes
755 using the L{pygeodesy.haversine} formula.
757 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
758 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
759 @kwarg radius_wrap: Optional keyword arguments for function
760 L{pygeodesy.haversine}.
762 @return: Great-circle distance (C{meter}, same units as
763 B{C{radius}}).
765 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
766 not a L{Geohash}, C{LatLon} or C{str}.
767 '''
768 return _2Geohash(geohash1).haversineTo(geohash2, **radius_wrap)
771def neighbors(geohash):
772 '''Return the L{Geohash}es for all 8 adjacent cells.
774 @arg geohash: Cell for which neighbors are requested
775 (L{Geohash} or C{str}).
777 @return: A L{Neighbors8Dict}C{(N, NE, E, SE, S, SW, W, NW)}
778 of L{Geohash}es.
780 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
781 C{LatLon} or C{str}.
782 '''
783 return _2Geohash(geohash).neighbors
786def precision(res1, res2=None):
787 '''Determine the L{Geohash} precisions to meet a or both given
788 (geographic) resolutions.
790 @arg res1: The required primary I{(longitudinal)} resolution
791 (C{degrees}).
792 @kwarg res2: Optional, required secondary I{(latitudinal)}
793 resolution (C{degrees}).
795 @return: The L{Geohash} precision or length (C{int}, 1..12).
797 @raise GeohashError: Invalid B{C{res1}} or B{C{res2}}.
799 @see: C++ class U{Geohash
800 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
801 '''
802 r = Degrees_(res1=res1, low=_0_0, Error=GeohashError)
803 if res2 is None:
804 t = r, r
805 for p in range(1, _MaxPrec):
806 if resolution2(p, None) <= t:
807 return p
809 else:
810 t = r, Degrees_(res2=res2, low=_0_0, Error=GeohashError)
811 for p in range(1, _MaxPrec):
812 if resolution2(p, p) <= t:
813 return p
815 return _MaxPrec
818class Resolutions2Tuple(_NamedTuple):
819 '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
820 secondary I{(latitudinal)} resolution, both in C{degrees}.
821 '''
822 _Names_ = ('res1', 'res2')
823 _Units_ = ( Degrees_, Degrees_)
826def resolution2(prec1, prec2=None):
827 '''Determine the (geographic) resolutions of given L{Geohash}
828 precisions.
830 @arg prec1: The given primary I{(longitudinal)} precision
831 (C{int} 1..12).
832 @kwarg prec2: Optional, secondary I{(latitudinal)} precision
833 (C{int} 1..12).
835 @return: L{Resolutions2Tuple}C{(res1, res2)} with the
836 (geographic) resolutions C{degrees}, where C{res2}
837 B{C{is}} C{res1} if no B{C{prec2}} is given.
839 @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
841 @see: I{Karney}'s C++ class U{Geohash
842 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
843 '''
844 res1, res2 = _360_0, _180_0 # note ... lon, lat!
846 if prec1:
847 p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec))
848 res1 = res2 = ldexp(res1, -(p - p // 2))
850 if prec2:
851 p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec))
852 res2 = ldexp(res2, -(p // 2))
854 return Resolutions2Tuple(res1, res2)
857def sizes(geohash):
858 '''Return the lat- and longitudinal size of this L{Geohash} cell.
860 @arg geohash: Cell for which size are required (L{Geohash} or
861 C{str}).
863 @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal
864 height and longitudinal width in (C{meter}).
866 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
867 C{LatLon} or C{str}.
868 '''
869 return _2Geohash(geohash).sizes
872def vincentys_(geohash1, geohash2, **radius_wrap):
873 '''Compute the distance between two geohashes using the
874 L{pygeodesy.vincentys} formula.
876 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
877 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
878 @kwarg radius_wrap: Optional keyword arguments for function
879 L{pygeodesy.vincentys}.
881 @return: Distance (C{meter}, same units as B{C{radius}}).
883 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
884 L{Geohash}, C{LatLon} or C{str}.
885 '''
886 return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap)
889__all__ += _ALL_OTHER(bounds, # functions
890 decode, decode2, decode_error, distance_,
891 encode, equirectangular_, euclidean_, haversine_,
892 neighbors, precision, resolution2, sizes, vincentys_)
894# **) MIT License
895#
896# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
897#
898# Permission is hereby granted, free of charge, to any person obtaining a
899# copy of this software and associated documentation files (the "Software"),
900# to deal in the Software without restriction, including without limitation
901# the rights to use, copy, modify, merge, publish, distribute, sublicense,
902# and/or sell copies of the Software, and to permit persons to whom the
903# Software is furnished to do so, subject to the following conditions:
904#
905# The above copyright notice and this permission notice shall be included
906# in all copies or substantial portions of the Software.
907#
908# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
909# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
910# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
911# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
912# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
913# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
914# OTHER DEALINGS IN THE SOFTWARE.