Coverage for pygeodesy/geohash.py: 98%
286 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
2# -*- coding: utf-8 -*-
4u'''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, _xStrError
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 _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
34from pygeodesy.streprs import fstr
35from pygeodesy.units import Degrees_, Int, Lat, Lon, Precision_, Str
37from math import fabs, ldexp, log10, radians
39__all__ = _ALL_LAZY.geohash
40__version__ = '24.06.15'
42_formy = _MODS.into(formy=__name__)
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 _t = _floatuple
76 return (_t(20032e3, 20000e3, 11292815.096), # 0
77 _t( 5003e3, 5000e3, 2821794.075), # 1
78 _t( 650e3, 1225e3, 503442.397), # 2
79 _t( 156e3, 156e3, 88013.575), # 3
80 _t( 19500, 39100, 15578.683), # 4
81 _t( 4890, 4890, 2758.887), # 5
82 _t( 610, 1220, 486.710), # 6
83 _t( 153, 153, 86.321), # 7
84 _t( 19.1, 38.2, 15.239), # 8
85 _t( 4.77, 4.77, 2.691), # 9
86 _t( 0.596, 1.19, 0.475), # 10
87 _t( 0.149, 0.149, 0.084), # 11
88 _t( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec
90_GH = _GH() # PYCHOK singleton
91_MaxPrec = 12
94def _2bounds(LatLon, LatLon_kwds, s, w, n, e, **name):
95 '''(INTERNAL) Return SW and NE bounds.
96 '''
97 if LatLon is None:
98 r = Bounds4Tuple(s, w, n, e, **name)
99 else:
100 kwds = _xkwds(LatLon_kwds, **name)
101 r = Bounds2Tuple(LatLon(s, w, **kwds),
102 LatLon(n, e, **kwds), **name)
103 return r
106def _2center(bounds):
107 '''(INTERNAL) Return the C{bounds} center.
108 '''
109 return (favg(bounds.latN, bounds.latS),
110 favg(bounds.lonE, bounds.lonW))
113def _2fll(lat, lon, *unused):
114 '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
115 '''
116 # lat, lon = parseDMS2(lat, lon)
117 return (Lat(lat, Error=GeohashError),
118 Lon(lon, Error=GeohashError))
121def _2Geohash(geohash):
122 '''(INTERNAL) Check or create a Geohash instance.
123 '''
124 return geohash if isinstance(geohash, Geohash) else \
125 Geohash(geohash)
128def _2geostr(geohash):
129 '''(INTERNAL) Check a geohash string.
130 '''
131 try:
132 if not (0 < len(geohash) <= _MaxPrec):
133 raise ValueError()
134 geostr = geohash.lower()
135 for c in geostr:
136 if c not in _GH.DecodedBase32:
137 raise ValueError()
138 return geostr
139 except (AttributeError, TypeError, ValueError) as x:
140 raise GeohashError(Geohash.__name__, geohash, cause=x)
143class Geohash(Str):
144 '''Geohash class, a named C{str}.
145 '''
146 # no str.__init__ in Python 3
147 def __new__(cls, cll, precision=None, **name):
148 '''New L{Geohash} from an other L{Geohash} instance or C{str}
149 or from a C{LatLon} instance or C{str}.
151 @arg cll: Cell or location (L{Geohash}, C{LatLon} or C{str}).
152 @kwarg precision: Optional, the desired geohash length (C{int}
153 1..12), see function L{geohash.encode} for
154 some examples.
155 @kwarg name: Optional C{B{name}=NN} (C{str}).
157 @return: New L{Geohash}.
159 @raise GeohashError: INValid or non-alphanumeric B{C{cll}}.
161 @raise TypeError: Invalid B{C{cll}}.
162 '''
163 ll = None
165 if isinstance(cll, Geohash):
166 gh = _2geostr(str(cll))
168 elif isstr(cll):
169 if _COMMA_ in cll:
170 ll = _2fll(*parse3llh(cll))
171 gh = encode(*ll, precision=precision)
172 else:
173 gh = _2geostr(cll)
175 else: # assume LatLon
176 try:
177 ll = _2fll(cll.lat, cll.lon)
178 gh = encode(*ll, precision=precision)
179 except AttributeError:
180 raise _xStrError(Geohash, cll=cll, Error=GeohashError)
182 self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=cll))
183 self._latlon = ll
184 return self
186 @deprecated_property_RO
187 def ab(self):
188 '''DEPRECATED, use property C{philam}.'''
189 return self.philam
191 def adjacent(self, direction, **name):
192 '''Determine the adjacent cell in the given compass direction.
194 @arg direction: Compass direction ('N', 'S', 'E' or 'W').
195 @kwarg name: Optional C{B{name}=NN} (C{str}) otherwise this
196 cell's name, either extended with C{.D}irection.
198 @return: Geohash of adjacent cell (L{Geohash}).
200 @raise GeohashError: Invalid geohash or B{C{direction}}.
201 '''
202 # based on <https://GitHub.com/DaveTroy/geohash-js>
204 D = direction[:1].upper()
205 if D not in _GH.Neighbors:
206 raise GeohashError(direction=direction)
208 e = 1 if isodd(len(self)) else 0
210 c = self[-1:] # last hash char
211 i = _GH.Neighbors[D][e].find(c)
212 if i < 0:
213 raise GeohashError(geohash=self)
215 p = self[:-1] # hash without last char
216 # check for edge-cases which don't share common prefix
217 if p and (c in _GH.Borders[D][e]):
218 p = Geohash(p).adjacent(D)
220 n = self._name__(name)
221 if n:
222 n = _DOT_(n, D)
223 # append letter for direction to parent
224 return Geohash(p + _GH.GeohashBase32[i], name=n)
226 @Property_RO
227 def _bounds(self):
228 '''(INTERNAL) Cache for L{bounds}.
229 '''
230 return bounds(self)
232 def bounds(self, LatLon=None, **LatLon_kwds):
233 '''Return the lower-left SW and upper-right NE bounds of this
234 geohash cell.
236 @kwarg LatLon: Optional class to return I{bounds} (C{LatLon})
237 or C{None}.
238 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
239 arguments, ignored if C{B{LatLon} is None}.
241 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s
242 or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if
243 C{B{LatLon} is None},
244 '''
245 r = self._bounds
246 return r if LatLon is None else \
247 _2bounds(LatLon, LatLon_kwds, *r, name=self.name)
249 def _distanceTo(self, func_, other, **kwds):
250 '''(INTERNAL) Helper for distances, see C{.formy._distanceTo*}.
251 '''
252 lls = self.latlon + _2Geohash(other).latlon
253 return func_(*lls, **kwds)
255 def distanceTo(self, other):
256 '''Estimate the distance between this and an other geohash
257 based the cell sizes.
259 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
261 @return: Approximate distance (C{meter}).
263 @raise TypeError: The B{C{other}} is not a L{Geohash},
264 C{LatLon} or C{str}.
265 '''
266 other = _2Geohash(other)
268 n = min(len(self), len(other), len(_GH.Sizes))
269 if n:
270 for n in range(n):
271 if self[n] != other[n]:
272 break
273 return _GH.Sizes[n][2]
275 @deprecated_method
276 def distance1To(self, other): # PYCHOK no cover
277 '''DEPRECATED, use method L{distanceTo}.'''
278 return self.distanceTo(other)
280 distance1 = distance1To
282 @deprecated_method
283 def distance2To(self, other, radius=R_M, adjust=False, wrap=False): # PYCHOK no cover
284 '''DEPRECATED, use method L{equirectangularTo}.'''
285 return self.equirectangularTo(other, radius=radius, adjust=adjust, wrap=wrap)
287 distance2 = distance2To
289 @deprecated_method
290 def distance3To(self, other, radius=R_M, wrap=False): # PYCHOK no cover
291 '''DEPRECATED, use method L{haversineTo}.'''
292 return self.haversineTo(other, radius=radius, wrap=wrap)
294 distance3 = distance3To
296 def equirectangularTo(self, other, radius=R_M, **adjust_limit_wrap):
297 '''Approximate the distance between this and an other geohash
298 using function L{pygeodesy.equirectangular}.
300 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
301 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
302 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
303 or C{None}, see function L{pygeodesy.equirectangular}.
304 @kwarg adjust_limit_wrap: Optional keyword arguments for function
305 L{pygeodesy.equirectangular4}, overriding defaults
306 C{B{adjust}=False, B{limit}=None} and C{B{wrap}=False}.
308 @return: Distance (C{meter}, same units as B{C{radius}} or the ellipsoid
309 or datum axes or C{radians I{squared}} if B{C{radius} is None}
310 or C{0}).
312 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon} or
313 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 = _xkwds(adjust_limit_wrap, adjust=False, limit=None, wrap=False)
320 return _formy.equirectangular( *lls, radius=radius, **kwds) if radius else \
321 _formy.equirectangular4(*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(_formy.euclidean, other, **radius_adjust_wrap)
339 def haversineTo(self, other, **radius_wrap):
340 '''Compute the distance between this and an other geohash using
341 the L{pygeodesy.haversine} formula.
343 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
344 @kwarg radius_wrap: Optional keyword arguments for function
345 L{pygeodesy.haversine}.
347 @return: Distance (C{meter}, same units as B{C{radius}} or the
348 ellipsoid or datum axes).
350 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
351 or C{str} or invalid B{C{radius}}.
352 '''
353 return self._distanceTo(_formy.haversine, other, **radius_wrap)
355 @Property_RO
356 def latlon(self):
357 '''Get the lat- and longitude of (the approximate center of)
358 this geohash as a L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
359 '''
360 lat, lon = self._latlon or _2center(self.bounds())
361 return LatLon2Tuple(lat, lon, name=self.name)
363 @Property_RO
364 def neighbors(self):
365 '''Get all 8 adjacent cells as a L{Neighbors8Dict}C{(N, NE,
366 E, SE, S, SW, W, NW)} of L{Geohash}es.
367 '''
368 return Neighbors8Dict(N=self.N, NE=self.NE, E=self.E, SE=self.SE,
369 S=self.S, SW=self.SW, W=self.W, NW=self.NW,
370 name=self.name)
372 @Property_RO
373 def philam(self):
374 '''Get the lat- and longitude of (the approximate center of)
375 this geohash as a L{PhiLam2Tuple}C{(phi, lam)} in C{radians}.
376 '''
377 return PhiLam2Tuple(map2(radians, self.latlon), name=self.name) # *map2
379 @Property_RO
380 def precision(self):
381 '''Get this geohash's precision (C{int}).
382 '''
383 return len(self)
385 @Property_RO
386 def sizes(self):
387 '''Get the lat- and longitudinal size of this cell as
388 a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}).
389 '''
390 z = _GH.Sizes
391 n = min(len(z) - 1, max(self.precision, 1))
392 return LatLon2Tuple(z[n][:2], name=self.name) # *z XXX Height, Width?
394 def toLatLon(self, LatLon=None, **LatLon_kwds):
395 '''Return (the approximate center of) this geohash cell
396 as an instance of the supplied C{LatLon} class.
398 @arg LatLon: Class to use (C{LatLon}) or C{None}.
399 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
400 arguments, ignored if C{B{LatLon} is None}.
402 @return: This geohash location (B{C{LatLon}}) or if C{B{LatLon}
403 is None}, a L{LatLon2Tuple}C{(lat, lon)}.
405 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
406 '''
407 return self.latlon if LatLon is None else _xnamed(LatLon(
408 *self.latlon, **LatLon_kwds), self.name)
410 def vincentysTo(self, other, **radius_wrap):
411 '''Compute the distance between this and an other geohash using
412 the L{pygeodesy.vincentys} formula.
414 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}).
415 @kwarg radius_wrap: Optional keyword arguments for function
416 L{pygeodesy.vincentys}.
418 @return: Distance (C{meter}, same units as B{C{radius}} or the
419 ellipsoid or datum axes).
421 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon}
422 or C{str} or invalid B{C{radius}}.
423 '''
424 return self._distanceTo(_formy.vincentys, other, **radius_wrap)
426 @Property_RO
427 def N(self):
428 '''Get the cell North of this (L{Geohash}).
429 '''
430 return self.adjacent(_N_)
432 @Property_RO
433 def S(self):
434 '''Get the cell South of this (L{Geohash}).
435 '''
436 return self.adjacent(_S_)
438 @Property_RO
439 def E(self):
440 '''Get the cell East of this (L{Geohash}).
441 '''
442 return self.adjacent(_E_)
444 @Property_RO
445 def W(self):
446 '''Get the cell West of this (L{Geohash}).
447 '''
448 return self.adjacent(_W_)
450 @Property_RO
451 def NE(self):
452 '''Get the cell NorthEast of this (L{Geohash}).
453 '''
454 return self.N.E
456 @Property_RO
457 def NW(self):
458 '''Get the cell NorthWest of this (L{Geohash}).
459 '''
460 return self.N.W
462 @Property_RO
463 def SE(self):
464 '''Get the cell SouthEast of this (L{Geohash}).
465 '''
466 return self.S.E
468 @Property_RO
469 def SW(self):
470 '''Get the cell SouthWest of this (L{Geohash}).
471 '''
472 return self.S.W
475class GeohashError(_ValueError):
476 '''Geohash encode, decode or other L{Geohash} issue.
477 '''
478 pass
481class Neighbors8Dict(_NamedDict):
482 '''8-Dict C{(N, NE, E, SE, S, SW, W, NW)} of L{Geohash}es,
483 providing key I{and} attribute access to the items.
484 '''
485 _Keys_ = (_N_, _NE_, _E_, _SE_, _S_, _SW_, _W_, _NW_)
487 def __init__(self, **kwds): # PYCHOK no *args
488 kwds = _xkwds(kwds, **_Neighbors8Defaults)
489 _NamedDict.__init__(self, **kwds) # name=...
492_Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
493 len(Neighbors8Dict._Keys_))) # XXX frozendict
496def bounds(geohash, LatLon=None, **LatLon_kwds):
497 '''Returns the lower-left SW and upper-right NE corners of a geohash.
499 @arg geohash: To be "bound" (L{Geohash}).
500 @kwarg LatLon: Optional class to return the bounds (C{LatLon}) or C{None}.
501 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
502 ignored if C{B{LatLon} is None}.
504 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)}, each a B{C{LatLon}}
505 or if C{B{LatLon} is None}, a L{Bounds4Tuple}C{(latS, lonW,
506 latN, lonE)}.
508 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or
509 C{str} or invalid B{C{LatLon}} or invalid B{C{LatLon_kwds}}.
511 @raise GeohashError: Invalid or C{null} B{C{geohash}}.
512 '''
513 gh = _2Geohash(geohash)
514 if len(gh) < 1:
515 raise GeohashError(geohash=geohash)
517 s, w, n, e = _GH.Bounds4
518 try:
519 d, _avg = True, favg
520 for c in gh.lower():
521 i = _GH.DecodedBase32[c]
522 for m in (16, 8, 4, 2, 1):
523 if d: # longitude
524 a = _avg(w, e)
525 if (i & m):
526 w = a
527 else:
528 e = a
529 else: # latitude
530 a = _avg(s, n)
531 if (i & m):
532 s = a
533 else:
534 n = a
535 d = not d
536 except KeyError:
537 raise GeohashError(geohash=geohash)
539 return _2bounds(LatLon, LatLon_kwds, s, w, n, e,
540 name=nameof(geohash)) # _or_nameof=geohash
543def _bounds3(geohash):
544 '''(INTERNAL) Return 3-tuple C{(bounds, height, width)}.
545 '''
546 b = bounds(geohash)
547 return b, (b.latN - b.latS), (b.lonE - b.lonW)
550def decode(geohash):
551 '''Decode a geohash to lat-/longitude of the (approximate
552 centre of) geohash cell to reasonable precision.
554 @arg geohash: To be decoded (L{Geohash}).
556 @return: 2-Tuple C{(latStr, lonStr)}, both C{str}.
558 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
559 C{LatLon} or C{str}.
561 @raise GeohashError: Invalid or null B{C{geohash}}.
562 '''
563 b, h, w = _bounds3(geohash)
564 lat, lon = _2center(b)
566 # round to near centre without excessive precision to
567 # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros
568 return (fstr(lat, prec=int(2 - log10(h))),
569 fstr(lon, prec=int(2 - log10(w)))) # strs!
572def decode2(geohash, LatLon=None, **LatLon_kwds):
573 '''Decode a geohash to lat-/longitude of the (approximate center
574 of) geohash cell to reasonable precision.
576 @arg geohash: To be decoded (L{Geohash}).
577 @kwarg LatLon: Optional class to return the location (C{LatLon})
578 or C{None}.
579 @kwarg LatLon_kwds: Optional, addtional B{C{LatLon}} keyword
580 arguments, ignored if C{B{LatLon} is None}.
582 @return: L{LatLon2Tuple}C{(lat, lon)}, both C{degrees} if
583 C{B{LatLon} is None}, otherwise a B{C{LatLon}} instance.
585 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
586 C{LatLon} or C{str}.
588 @raise GeohashError: Invalid or null B{C{geohash}}.
589 '''
590 t = map2(float, decode(geohash))
591 r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t
592 return _xnamed(r, name__=decode2)
595def decode_error(geohash):
596 '''Return the relative lat-/longitude decoding errors for
597 this geohash.
599 @arg geohash: To be decoded (L{Geohash}).
601 @return: A L{LatLon2Tuple}C{(lat, lon)} with the lat- and
602 longitudinal errors in (C{degrees}).
604 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
605 C{LatLon} or C{str}.
607 @raise GeohashError: Invalid or null B{C{geohash}}.
608 '''
609 _, h, w = _bounds3(geohash)
610 return LatLon2Tuple(h * _0_5, # Height error
611 w * _0_5) # Width error
614def distance_(geohash1, geohash2):
615 '''Estimate the distance between two geohash (from the cell sizes).
617 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
618 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
620 @return: Approximate distance (C{meter}).
622 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
623 L{Geohash}, C{LatLon} or C{str}.
624 '''
625 return _2Geohash(geohash1).distanceTo(geohash2)
628@deprecated_function
629def distance1(geohash1, geohash2):
630 '''DEPRECATED, use L{geohash.distance_}.'''
631 return distance_(geohash1, geohash2)
634@deprecated_function
635def distance2(geohash1, geohash2):
636 '''DEPRECATED, use L{geohash.equirectangular4}.'''
637 return equirectangular4(geohash1, geohash2)
640@deprecated_function
641def distance3(geohash1, geohash2):
642 '''DEPRECATED, use L{geohash.haversine_}.'''
643 return haversine_(geohash1, geohash2)
646def encode(lat, lon, precision=None):
647 '''Encode a lat-/longitude as a C{geohash}, either to the specified
648 precision or if not provided, to an automatically evaluated
649 precision.
651 @arg lat: Latitude (C{degrees}).
652 @arg lon: Longitude (C{degrees}).
653 @kwarg precision: Optional, the desired geohash length (C{int}
654 1..12).
656 @return: The C{geohash} (C{str}).
658 @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
659 '''
660 lat, lon = _2fll(lat, lon)
662 if precision is None:
663 # Infer precision by refining geohash until
664 # it matches precision of supplied lat/lon.
665 for p in range(1, _MaxPrec + 1):
666 gh = encode(lat, lon, p)
667 ll = map2(float, decode(gh))
668 if fabs(lat - ll[0]) < EPS and \
669 fabs(lon - ll[1]) < EPS:
670 return gh
671 p = _MaxPrec
672 else:
673 p = Precision_(precision, Error=GeohashError, low=1, high=_MaxPrec)
675 b = i = 0
676 d, gh = True, []
677 s, w, n, e = _GH.Bounds4
679 _avg = favg
680 while p > 0:
681 i += i
682 if d: # bisect longitude
683 m = _avg(e, w)
684 if lon < m:
685 e = m
686 else:
687 w = m
688 i += 1
689 else: # bisect latitude
690 m = _avg(n, s)
691 if lat < m:
692 n = m
693 else:
694 s = m
695 i += 1
696 d = not d
698 b += 1
699 if b == 5:
700 # 5 bits gives a character:
701 # append it and start over
702 gh.append(_GH.GeohashBase32[i])
703 b = i = 0
704 p -= 1
706 return NN.join(gh)
709def equirectangular4(geohash1, geohash2, radius=R_M):
710 '''Approximate the distance between two geohashes using the
711 L{pygeodesy.equirectangular} formula.
713 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
714 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
715 @kwarg radius: Mean earth radius (C{meter}) or C{None}, see method
716 L{Geohash.equirectangularTo}.
718 @return: Approximate distance (C{meter}, same units as B{C{radius}}),
719 see method L{Geohash.equirectangularTo}.
721 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
722 L{Geohash}, C{LatLon} or C{str}.
723 '''
724 return _2Geohash(geohash1).equirectangularTo(geohash2, radius=radius)
727def euclidean_(geohash1, geohash2, **radius_adjust_wrap):
728 '''Approximate the distance between two geohashes using the
729 L{pygeodesy.euclidean} formula.
731 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
732 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
733 @kwarg radius_adjust_wrap: Optional keyword arguments for function
734 L{pygeodesy.euclidean}.
736 @return: Approximate distance (C{meter}, same units as B{C{radius}}).
738 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
739 L{Geohash}, C{LatLon} or C{str}.
740 '''
741 return _2Geohash(geohash1).euclideanTo(geohash2, **radius_adjust_wrap)
744def haversine_(geohash1, geohash2, **radius_wrap):
745 '''Compute the great-circle distance between two geohashes
746 using the L{pygeodesy.haversine} formula.
748 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
749 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
750 @kwarg radius_wrap: Optional keyword arguments for function
751 L{pygeodesy.haversine}.
753 @return: Great-circle distance (C{meter}, same units as
754 B{C{radius}}).
756 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is
757 not a L{Geohash}, C{LatLon} or C{str}.
758 '''
759 return _2Geohash(geohash1).haversineTo(geohash2, **radius_wrap)
762def neighbors(geohash):
763 '''Return the L{Geohash}es for all 8 adjacent cells.
765 @arg geohash: Cell for which neighbors are requested
766 (L{Geohash} or C{str}).
768 @return: A L{Neighbors8Dict}C{(N, NE, E, SE, S, SW, W, NW)}
769 of L{Geohash}es.
771 @raise TypeError: The B{C{geohash}} is not a L{Geohash},
772 C{LatLon} or C{str}.
773 '''
774 return _2Geohash(geohash).neighbors
777def precision(res1, res2=None):
778 '''Determine the L{Geohash} precisions to meet a or both given
779 (geographic) resolutions.
781 @arg res1: The required primary I{(longitudinal)} resolution
782 (C{degrees}).
783 @kwarg res2: Optional, required secondary I{(latitudinal)}
784 resolution (C{degrees}).
786 @return: The L{Geohash} precision or length (C{int}, 1..12).
788 @raise GeohashError: Invalid B{C{res1}} or B{C{res2}}.
790 @see: C++ class U{Geohash
791 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
792 '''
793 r = Degrees_(res1=res1, low=_0_0, Error=GeohashError)
794 N = res2 is None
795 t = r, (r if N else Degrees_(res2=res2, low=_0_0, Error=GeohashError))
796 for p in range(1, _MaxPrec):
797 if resolution2(p, (None if N else p)) <= t:
798 return p
799 return _MaxPrec
802class Resolutions2Tuple(_NamedTuple):
803 '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
804 secondary I{(latitudinal)} resolution, both in C{degrees}.
805 '''
806 _Names_ = ('res1', 'res2')
807 _Units_ = ( Degrees_, Degrees_)
810def resolution2(prec1, prec2=None):
811 '''Determine the (geographic) resolutions of given L{Geohash}
812 precisions.
814 @arg prec1: The given primary I{(longitudinal)} precision
815 (C{int} 1..12).
816 @kwarg prec2: Optional, secondary I{(latitudinal)} precision
817 (C{int} 1..12).
819 @return: L{Resolutions2Tuple}C{(res1, res2)} with the
820 (geographic) resolutions C{degrees}, where C{res2}
821 B{C{is}} C{res1} if no B{C{prec2}} is given.
823 @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
825 @see: I{Karney}'s C++ class U{Geohash
826 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
827 '''
828 res1, res2 = _360_0, _180_0 # note ... lon, lat!
830 if prec1:
831 p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec))
832 res1 = res2 = ldexp(res1, -(p - p // 2))
834 if prec2:
835 p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec))
836 res2 = ldexp(res2, -(p // 2))
838 return Resolutions2Tuple(res1, res2)
841def sizes(geohash):
842 '''Return the lat- and longitudinal size of this L{Geohash} cell.
844 @arg geohash: Cell for which size are required (L{Geohash} or C{str}).
846 @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal height and
847 longitudinal width in (C{meter}).
849 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or C{str}.
850 '''
851 return _2Geohash(geohash).sizes
854def vincentys_(geohash1, geohash2, **radius_wrap):
855 '''Compute the distance between two geohashes using the
856 L{pygeodesy.vincentys} formula.
858 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}).
859 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}).
860 @kwarg radius_wrap: Optional keyword arguments for function
861 L{pygeodesy.vincentys}.
863 @return: Distance (C{meter}, same units as B{C{radius}}).
865 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a
866 L{Geohash}, C{LatLon} or C{str}.
867 '''
868 return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap)
871__all__ += _ALL_OTHER(bounds, # functions
872 decode, decode2, decode_error, distance_,
873 encode, equirectangular4, euclidean_, haversine_,
874 neighbors, precision, resolution2, sizes, vincentys_)
876# **) MIT License
877#
878# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
879#
880# Permission is hereby granted, free of charge, to any person obtaining a
881# copy of this software and associated documentation files (the "Software"),
882# to deal in the Software without restriction, including without limitation
883# the rights to use, copy, modify, merge, publish, distribute, sublicense,
884# and/or sell copies of the Software, and to permit persons to whom the
885# Software is furnished to do so, subject to the following conditions:
886#
887# The above copyright notice and this permission notice shall be included
888# in all copies or substantial portions of the Software.
889#
890# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
891# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
892# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
893# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
894# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
895# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
896# OTHER DEALINGS IN THE SOFTWARE.