Coverage for pygeodesy/geohash.py: 97%

368 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-06 12:20 -0500

1 

2# -*- coding: utf-8 -*- 

3 

4u'''I{Gustavo Niemeyer}’s U{Geohash<https://WikiPedia.org/wiki/Geohash>}. 

5 

6Class L{Geohash} and several functions to encode, decode and inspect 

7C{geohashes} and optional L{Geohashed} caches. 

8 

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>}. 

12 

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''' 

19 

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 

37 

38from math import fabs, ldexp, log10, radians 

39 

40__all__ = _ALL_LAZY.geohash 

41__version__ = '24.10.12' 

42 

43_formy = _MODS.into(formy=__name__) 

44_MASK5 = 16, 8, 4, 2, 1 # PYCHOK used! 

45_MaxPrec = 12 

46 

47 

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 

58 

59 

60def _2center(bounds): 

61 '''(INTERNAL) Return the C{bounds} center. 

62 ''' 

63 return (_2mid(bounds.latN, bounds.latS), 

64 _2mid(bounds.lonE, bounds.lonW)) 

65 

66 

67def _2dab(d, a, b): 

68 '''(INTERNAL) Get delta lat or lon from center. 

69 ''' 

70 return fabs(d - round(*_2mid_ndigits(a, b))) 

71 

72 

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)) 

79 

80 

81def _2Geohash(geohash): 

82 '''(INTERNAL) Check or create a Geohash instance. 

83 ''' 

84 return geohash if isinstance(geohash, Geohash) else \ 

85 Geohash(geohash) 

86 

87 

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)) 

95 

96 

97def _2mid(a, b): 

98 '''(INTERNAL) Bisect C{a} to C{b}. 

99 ''' 

100 return (a + b) * _0_5 # favg 

101 

102 

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)) 

109 

110 

111def _2Precision(p): 

112 '''(INTERNAL) Get a valid C{Precision}. 

113 ''' 

114 return Precision_(p, low=1, high=_MaxPrec, Error=GeohashError) 

115 

116 

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) 

123 

124 

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)) 

131 

132 @property_ROver 

133 def Borders(self): 

134 return self._4d('028b', '0145hjnp', 'prxz', 'bcfguvyz') 

135 

136 @property_ROver 

137 def DecodeB32(self): # inverse EncodeB32 map 

138 return dict((c, i) for i, c in enumerate(self.EncodeB32)) 

139 

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) 

145 

146 # Geohash's base32 codes, no a, i, l and o 

147 EncodeB32 = '0123456789bcdefghjkmnpqrstuvwxyz' 

148 

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 

179 

180 return NN.join(_encodes(*lat_lon_prec_eps)) 

181 

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) 

191 

192 @property_ROver 

193 def _LatLon2Tuple(self): 

194 

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 

199 

200 return _LatLon2Tuple 

201 

202 @property_ROver 

203 def Neighbors(self): 

204 return self._4d('14365h7k9dcfesgujnmqp0r2twvyx8zb', 

205 '238967debc01fg45kmstqrwxuvhjyznp', 

206 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 

207 'bc01fg45238967deuvhjyznpkmstqrwx') 

208 

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 

227 

228 SWNE4 = (_N_90_0, _N_180_0, _90_0, _180_0) 

229 

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 

259 

260_GH = _GH() # PYCHOK singleton 

261 

262 

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. 

270 

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}). 

280 

281 @return: New L{Geohash}. 

282 

283 @raise GeohashError: Invalid B{C{lat_ghll}}. 

284 

285 @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}. 

286 

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) 

306 

307 self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=lat_ghll)) 

308 self._latlon = ll 

309 return self 

310 

311 @deprecated_property_RO 

312 def ab(self): 

313 '''DEPRECATED, use property C{philam}.''' 

314 return self.philam 

315 

316 def adjacent(self, direction, **name): 

317 '''Determine the adjacent cell in the given compass direction. 

318 

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. 

322 

323 @return: Geohash of adjacent cell (L{Geohash}). 

324 

325 @raise GeohashError: Invalid geohash or B{C{direction}}. 

326 ''' 

327 # based on <https://GitHub.com/DaveTroy/geohash-js> 

328 

329 D = direction[:1].upper() 

330 if D not in _GH.Neighbors: 

331 raise GeohashError(direction=direction) 

332 

333 e = len(self) & 1 # int(isodd(len(self))) 

334 

335 c = self[-1:] # last hash char 

336 i = _GH.Neighbors[D][e].find(c) 

337 if i < 0: 

338 raise GeohashError(geohash=self) 

339 

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) 

344 

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) 

350 

351 @Property_RO 

352 def _bounds(self): 

353 '''(INTERNAL) Cache for L{bounds}. 

354 ''' 

355 return bounds(self) 

356 

357 def bounds(self, LatLon=None, **LatLon_kwds): 

358 '''Return the lower-left SW and upper-right NE bounds of this 

359 geohash cell. 

360 

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}. 

365 

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) 

373 

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) 

379 

380 def distanceTo(self, other): 

381 '''Estimate the distance between this and an other geohash 

382 based the cell sizes. 

383 

384 @arg other: The other geohash (L{Geohash}, C{LatLon} or C{str}). 

385 

386 @return: Approximate distance (C{meter}). 

387 

388 @raise TypeError: The B{C{other}} is not a L{Geohash}, 

389 C{LatLon} or C{str}. 

390 ''' 

391 other = _2Geohash(other) 

392 

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 

399 

400 @deprecated_method 

401 def distance1To(self, other): # PYCHOK no cover 

402 '''DEPRECATED, use method L{distanceTo}.''' 

403 return self.distanceTo(other) 

404 

405 distance1 = distance1To 

406 

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) 

411 

412 distance2 = distance2To 

413 

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) 

418 

419 distance3 = distance3To 

420 

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}. 

424 

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}. 

432 

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}). 

436 

437 @raise TypeError: The B{C{other}} is not a L{Geohash}, C{LatLon} or 

438 C{str} or invalid B{C{radius}}. 

439 

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 

447 

448 def euclideanTo(self, other, **radius_adjust_wrap): 

449 '''Approximate the distance between this and an other geohash using 

450 function L{pygeodesy.euclidean}. 

451 

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}. 

455 

456 @return: Distance (C{meter}, same units as B{C{radius}} or the 

457 ellipsoid or datum axes). 

458 

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) 

463 

464 def haversineTo(self, other, **radius_wrap): 

465 '''Compute the distance between this and an other geohash using 

466 the L{pygeodesy.haversine} formula. 

467 

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}. 

471 

472 @return: Distance (C{meter}, same units as B{C{radius}} or the 

473 ellipsoid or datum axes). 

474 

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) 

479 

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) 

487 

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) 

496 

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 

503 

504 @Property_RO 

505 def precision(self): 

506 '''Get this geohash's precision (C{int}). 

507 ''' 

508 return len(self) 

509 

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) 

516 

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) 

522 

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) 

531 

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. 

535 

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}. 

539 

540 @return: This geohash location (B{C{LatLon}}) or if C{B{LatLon} 

541 is None}, a L{LatLon2Tuple}C{(lat, lon)}. 

542 

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) 

547 

548 def vincentysTo(self, other, **radius_wrap): 

549 '''Compute the distance between this and an other geohash using 

550 the L{pygeodesy.vincentys} formula. 

551 

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}. 

555 

556 @return: Distance (C{meter}, same units as B{C{radius}} or the 

557 ellipsoid or datum axes). 

558 

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) 

563 

564 @Property_RO 

565 def E(self): 

566 '''Get the cell East of this (L{Geohash}). 

567 ''' 

568 return self.adjacent(_E_) 

569 

570 @Property_RO 

571 def N(self): 

572 '''Get the cell North of this (L{Geohash}). 

573 ''' 

574 return self.adjacent(_N_) 

575 

576 @Property_RO 

577 def NE(self): 

578 '''Get the cell NorthEast of this (L{Geohash}). 

579 ''' 

580 return self.N.E 

581 

582 @Property_RO 

583 def NW(self): 

584 '''Get the cell NorthWest of this (L{Geohash}). 

585 ''' 

586 return self.N.W 

587 

588 @Property_RO 

589 def S(self): 

590 '''Get the cell South of this (L{Geohash}). 

591 ''' 

592 return self.adjacent(_S_) 

593 

594 @Property_RO 

595 def SE(self): 

596 '''Get the cell SouthEast of this (L{Geohash}). 

597 ''' 

598 return self.S.E 

599 

600 @Property_RO 

601 def SW(self): 

602 '''Get the cell SouthWest of this (L{Geohash}). 

603 ''' 

604 return self.S.W 

605 

606 @Property_RO 

607 def W(self): 

608 '''Get the cell West of this (L{Geohash}). 

609 ''' 

610 return self.adjacent(_W_) 

611 

612 

613class Geohashed(object): 

614 '''A cache of en- and decoded geohashes of one precision. 

615 ''' 

616 _nn = None, # 1-tuple 

617 

618 def __init__(self, precision, ndigits=None): 

619 '''New L{Geohashed} cache. 

620 

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() 

635 

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 

646 

647 def _ab2(self, *ll): # overwritten 

648 '''(INTERNAL) Make encoded keys C{a, b}. 

649 ''' 

650 return ll 

651 

652 def _ab2float(self, *ll): 

653 '''(INTERNAL) Make encoded keys C{a, b}. 

654 ''' 

655 return map(float, ll) 

656 

657 def _ab2round(self, *ll): 

658 '''(INTERNAL) Make encoded keys C{a, b}. 

659 ''' 

660 return map(round, ll, self._nn) # strict=True 

661 

662 def clear(self): 

663 '''Clear the C{en-} and C{decoded} cache. 

664 ''' 

665 self._e = {} 

666 self._d = {} 

667 

668 def decoded(self, geohash, encoded=False): 

669 '''Get and cache the C{(lat, lon)} for C{geohash}, see L{decode<pygeodesy.geohash.decode>}. 

670 

671 @kwarg encoded: If C{True}, cache the result as C{encoded}. 

672 

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 

686 

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>}. 

689 

690 @kwarg decoded: If C{True}, cache the result as C{decoded}. 

691 

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 

704 

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) 

711 

712 @Property_RO 

713 def ndigits(self): 

714 '''Get the rounding (C{int} or C{None}). 

715 ''' 

716 return self._nn[0] 

717 

718 @Property_RO 

719 def precision(self): 

720 '''Get the C{precision} (C{int}). 

721 ''' 

722 return self._p 

723 

724 

725class GeohashError(_ValueError): 

726 '''Geohash encode, decode or other L{Geohash} issue. 

727 ''' 

728 pass 

729 

730 

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_) 

736 

737 def __init__(self, **kwds): # PYCHOK no *args 

738 kwds = _xkwds(kwds, **_Neighbors8Defaults) 

739 _NamedDict.__init__(self, **kwds) # name=... 

740 

741 

742_Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) * 

743 len(Neighbors8Dict._Keys_))) # XXX frozendict 

744 

745 

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_) 

752 

753 @property_RO 

754 def lat(self): 

755 '''Get the secondary, latitudinal resolution (C{degrees}). 

756 ''' 

757 return self[1] 

758 

759 @property_RO 

760 def lon(self): 

761 '''Get the primary, longitudinal resolution (C{degrees}). 

762 ''' 

763 return self[0] 

764 

765 

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) 

772 

773 

774def bounds(geohash, LatLon=None, **LatLon_kwds): 

775 '''Returns the lower-left SW and upper-right NE corners of a geohash. 

776 

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}. 

781 

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)}. 

785 

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}}. 

788 

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 

794 

795 

796def decode(geohash): 

797 '''Decode a geohash to lat-/longitude of the (approximate 

798 centre of) geohash cell to reasonable precision. 

799 

800 @arg geohash: To be decoded (L{Geohash}). 

801 

802 @return: 2-Tuple C{(latStr, lonStr)}, both C{str}. 

803 

804 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, 

805 C{LatLon} or C{str}. 

806 

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) 

813 

814 

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. 

818 

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}. 

824 

825 @return: L{LatLon2Tuple}C{(lat, lon)}, both C{degrees} if 

826 C{B{LatLon} is None}, otherwise a B{C{LatLon}} instance. 

827 

828 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, 

829 C{LatLon} or C{str}. 

830 

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) 

837 

838 

839@deprecated_function 

840def decode_error(geohash): 

841 '''DEPRECATED on 2024.07.28, use L{geohash.decode_error2}.''' 

842 return decode_error2(geohash) 

843 

844 

845def decode_error2(geohash): 

846 '''Return the lat- and longitude decoding error for a geohash. 

847 

848 @arg geohash: To be decoded (L{Geohash}). 

849 

850 @return: A L{LatLon2Tuple}C{(lat, lon)} with the lat- and 

851 longitudinal errors in (C{degrees}). 

852 

853 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, 

854 C{LatLon} or C{str}. 

855 

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 

861 

862 

863def distance_(geohash1, geohash2): 

864 '''Estimate the distance between two geohash (from the cell sizes). 

865 

866 @arg geohash1: First geohash (L{Geohash}, C{LatLon} or C{str}). 

867 @arg geohash2: Second geohash (L{Geohash}, C{LatLon} or C{str}). 

868 

869 @return: Approximate distance (C{meter}). 

870 

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) 

875 

876 

877@deprecated_function 

878def distance1(geohash1, geohash2): 

879 '''DEPRECATED, use L{geohash.distance_}.''' 

880 return distance_(geohash1, geohash2) 

881 

882 

883@deprecated_function 

884def distance2(geohash1, geohash2): 

885 '''DEPRECATED, use L{geohash.equirectangular4}.''' 

886 return equirectangular4(geohash1, geohash2) 

887 

888 

889@deprecated_function 

890def distance3(geohash1, geohash2): 

891 '''DEPRECATED, use L{geohash.haversine_}.''' 

892 return haversine_(geohash1, geohash2) 

893 

894 

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. 

898 

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}. 

905 

906 @return: The C{geohash} (C{str}). 

907 

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 

912 

913 

914def equirectangular4(geohash1, geohash2, radius=R_M): 

915 '''Approximate the distance between two geohashes using the 

916 L{pygeodesy.equirectangular} formula. 

917 

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}. 

922 

923 @return: Approximate distance (C{meter}, same units as B{C{radius}}), 

924 see method L{Geohash.equirectangularTo}. 

925 

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) 

930 

931 

932def euclidean_(geohash1, geohash2, **radius_adjust_wrap): 

933 '''Approximate the distance between two geohashes using the 

934 L{pygeodesy.euclidean} formula. 

935 

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}. 

940 

941 @return: Approximate distance (C{meter}, same units as B{C{radius}}). 

942 

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) 

947 

948 

949def haversine_(geohash1, geohash2, **radius_wrap): 

950 '''Compute the great-circle distance between two geohashes 

951 using the L{pygeodesy.haversine} formula. 

952 

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}. 

957 

958 @return: Great-circle distance (C{meter}, same units as 

959 B{C{radius}}). 

960 

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) 

965 

966 

967def neighbors(geohash): 

968 '''Return the L{Geohash}es for all 8 adjacent cells. 

969 

970 @arg geohash: Cell for which neighbors are requested 

971 (L{Geohash} or C{str}). 

972 

973 @return: A L{Neighbors8Dict}C{(N, NE, E, SE, S, SW, W, NW)} 

974 of L{Geohash}es. 

975 

976 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, 

977 C{LatLon} or C{str}. 

978 ''' 

979 return _2Geohash(geohash).neighbors 

980 

981 

982def precision(res1, res2=None): 

983 '''Determine the L{Geohash} precisions to meet a or both given 

984 (geographic) resolutions. 

985 

986 @arg res1: The required primary I{(longitudinal)} resolution 

987 (C{degrees}). 

988 @kwarg res2: Optional, required secondary I{(latitudinal)} 

989 resolution (C{degrees}). 

990 

991 @return: The L{Geohash} precision or length (C{int}, 1..12). 

992 

993 @raise GeohashError: Invalid B{C{res1}} or B{C{res2}}. 

994 

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 

1005 

1006 

1007def resolution2(prec1, prec2=None): 

1008 '''Determine the (geographic) resolutions of given L{Geohash} 

1009 precisions. 

1010 

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). 

1015 

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. 

1019 

1020 @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}. 

1021 

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) 

1029 

1030 

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) 

1036 

1037 

1038def sizes3(geohash): 

1039 '''Return the lat-, longitudinal and radial size of this L{Geohash} cell. 

1040 

1041 @arg geohash: Cell for which size are required (L{Geohash} or C{str}). 

1042 

1043 @return: A L{Sizes3Tuple}C{(height, width, radius)}, all C{meter}. 

1044 

1045 @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or C{str}. 

1046 ''' 

1047 return _2Geohash(geohash).sizes3 

1048 

1049 

1050def vincentys_(geohash1, geohash2, **radius_wrap): 

1051 '''Compute the distance between two geohashes using the 

1052 L{pygeodesy.vincentys} formula. 

1053 

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}. 

1058 

1059 @return: Distance (C{meter}, same units as B{C{radius}}). 

1060 

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) 

1065 

1066 

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 

1072 

1073if __name__ == '__main__': 

1074 

1075 from pygeodesy.internals import printf, _versions 

1076 from timeit import timeit 

1077 

1078 for f, p in (('encode', _MaxPrec), ('infer', None)): 

1079 

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 

1087 

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()) 

1092 

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 

1099 

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.