Coverage for pygeodesy/geohash.py: 98%

286 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-27 20:21 -0400

1 

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

3 

4u'''Geohash en-/decoding. 

5 

6Classes L{Geohash} and L{GeohashError} and several functions to encode, 

7decode and inspect I{geohashes}. 

8 

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

12 

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

18 

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 

36 

37from math import fabs, ldexp, log10, radians 

38 

39__all__ = _ALL_LAZY.geohash 

40__version__ = '24.06.15' 

41 

42_formy = _MODS.into(formy=__name__) 

43 

44 

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

51 

52 @Property_RO 

53 def Borders(self): 

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

55 

56 Bounds4 = (_N_90_0, _N_180_0, _90_0, _180_0) 

57 

58 @Property_RO 

59 def DecodedBase32(self): # inverse GeohashBase32 map 

60 return dict((c, i) for i, c in enumerate(self.GeohashBase32)) 

61 

62 # Geohash-specific base32 map 

63 GeohashBase32 = '0123456789bcdefghjkmnpqrstuvwxyz' # no a, i, j and o 

64 

65 @Property_RO 

66 def Neighbors(self): 

67 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy', 

68 'bc01fg45238967deuvhjyznpkmstqrwx', 

69 '14365h7k9dcfesgujnmqp0r2twvyx8zb', 

70 '238967debc01fg45kmstqrwxuvhjyznp') 

71 

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 

89 

90_GH = _GH() # PYCHOK singleton 

91_MaxPrec = 12 

92 

93 

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 

104 

105 

106def _2center(bounds): 

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

108 ''' 

109 return (favg(bounds.latN, bounds.latS), 

110 favg(bounds.lonE, bounds.lonW)) 

111 

112 

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

119 

120 

121def _2Geohash(geohash): 

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

123 ''' 

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

125 Geohash(geohash) 

126 

127 

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) 

141 

142 

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

150 

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

156 

157 @return: New L{Geohash}. 

158 

159 @raise GeohashError: INValid or non-alphanumeric B{C{cll}}. 

160 

161 @raise TypeError: Invalid B{C{cll}}. 

162 ''' 

163 ll = None 

164 

165 if isinstance(cll, Geohash): 

166 gh = _2geostr(str(cll)) 

167 

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) 

174 

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) 

181 

182 self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=cll)) 

183 self._latlon = ll 

184 return self 

185 

186 @deprecated_property_RO 

187 def ab(self): 

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

189 return self.philam 

190 

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

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

193 

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. 

197 

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

199 

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

201 ''' 

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

203 

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

205 if D not in _GH.Neighbors: 

206 raise GeohashError(direction=direction) 

207 

208 e = 1 if isodd(len(self)) else 0 

209 

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

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

212 if i < 0: 

213 raise GeohashError(geohash=self) 

214 

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) 

219 

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) 

225 

226 @Property_RO 

227 def _bounds(self): 

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

229 ''' 

230 return bounds(self) 

231 

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

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

234 geohash cell. 

235 

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

240 

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) 

248 

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) 

254 

255 def distanceTo(self, other): 

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

257 based the cell sizes. 

258 

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

260 

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

262 

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

264 C{LatLon} or C{str}. 

265 ''' 

266 other = _2Geohash(other) 

267 

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] 

274 

275 @deprecated_method 

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

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

278 return self.distanceTo(other) 

279 

280 distance1 = distance1To 

281 

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) 

286 

287 distance2 = distance2To 

288 

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) 

293 

294 distance3 = distance3To 

295 

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

299 

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

307 

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

311 

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

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

314 

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 

322 

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

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

325 function L{pygeodesy.euclidean}. 

326 

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

330 

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

332 ellipsoid or datum axes). 

333 

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) 

338 

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

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

341 the L{pygeodesy.haversine} formula. 

342 

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

346 

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

348 ellipsoid or datum axes). 

349 

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) 

354 

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) 

362 

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) 

371 

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 

378 

379 @Property_RO 

380 def precision(self): 

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

382 ''' 

383 return len(self) 

384 

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? 

393 

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. 

397 

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

401 

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

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

404 

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) 

409 

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

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

412 the L{pygeodesy.vincentys} formula. 

413 

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

417 

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

419 ellipsoid or datum axes). 

420 

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) 

425 

426 @Property_RO 

427 def N(self): 

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

429 ''' 

430 return self.adjacent(_N_) 

431 

432 @Property_RO 

433 def S(self): 

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

435 ''' 

436 return self.adjacent(_S_) 

437 

438 @Property_RO 

439 def E(self): 

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

441 ''' 

442 return self.adjacent(_E_) 

443 

444 @Property_RO 

445 def W(self): 

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

447 ''' 

448 return self.adjacent(_W_) 

449 

450 @Property_RO 

451 def NE(self): 

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

453 ''' 

454 return self.N.E 

455 

456 @Property_RO 

457 def NW(self): 

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

459 ''' 

460 return self.N.W 

461 

462 @Property_RO 

463 def SE(self): 

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

465 ''' 

466 return self.S.E 

467 

468 @Property_RO 

469 def SW(self): 

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

471 ''' 

472 return self.S.W 

473 

474 

475class GeohashError(_ValueError): 

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

477 ''' 

478 pass 

479 

480 

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

486 

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

488 kwds = _xkwds(kwds, **_Neighbors8Defaults) 

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

490 

491 

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

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

494 

495 

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

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

498 

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

503 

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

507 

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

510 

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) 

516 

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) 

538 

539 return _2bounds(LatLon, LatLon_kwds, s, w, n, e, 

540 name=nameof(geohash)) # _or_nameof=geohash 

541 

542 

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) 

548 

549 

550def decode(geohash): 

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

552 centre of) geohash cell to reasonable precision. 

553 

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

555 

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

557 

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

559 C{LatLon} or C{str}. 

560 

561 @raise GeohashError: Invalid or null B{C{geohash}}. 

562 ''' 

563 b, h, w = _bounds3(geohash) 

564 lat, lon = _2center(b) 

565 

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! 

570 

571 

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. 

575 

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

581 

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

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

584 

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

586 C{LatLon} or C{str}. 

587 

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) 

593 

594 

595def decode_error(geohash): 

596 '''Return the relative lat-/longitude decoding errors for 

597 this geohash. 

598 

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

600 

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

602 longitudinal errors in (C{degrees}). 

603 

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

605 C{LatLon} or C{str}. 

606 

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 

612 

613 

614def distance_(geohash1, geohash2): 

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

616 

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

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

619 

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

621 

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) 

626 

627 

628@deprecated_function 

629def distance1(geohash1, geohash2): 

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

631 return distance_(geohash1, geohash2) 

632 

633 

634@deprecated_function 

635def distance2(geohash1, geohash2): 

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

637 return equirectangular4(geohash1, geohash2) 

638 

639 

640@deprecated_function 

641def distance3(geohash1, geohash2): 

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

643 return haversine_(geohash1, geohash2) 

644 

645 

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. 

650 

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

655 

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

657 

658 @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}. 

659 ''' 

660 lat, lon = _2fll(lat, lon) 

661 

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) 

674 

675 b = i = 0 

676 d, gh = True, [] 

677 s, w, n, e = _GH.Bounds4 

678 

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 

697 

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 

705 

706 return NN.join(gh) 

707 

708 

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

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

711 L{pygeodesy.equirectangular} formula. 

712 

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

717 

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

719 see method L{Geohash.equirectangularTo}. 

720 

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) 

725 

726 

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

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

729 L{pygeodesy.euclidean} formula. 

730 

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

735 

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

737 

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) 

742 

743 

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

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

746 using the L{pygeodesy.haversine} formula. 

747 

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

752 

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

754 B{C{radius}}). 

755 

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) 

760 

761 

762def neighbors(geohash): 

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

764 

765 @arg geohash: Cell for which neighbors are requested 

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

767 

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

769 of L{Geohash}es. 

770 

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

772 C{LatLon} or C{str}. 

773 ''' 

774 return _2Geohash(geohash).neighbors 

775 

776 

777def precision(res1, res2=None): 

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

779 (geographic) resolutions. 

780 

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

782 (C{degrees}). 

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

784 resolution (C{degrees}). 

785 

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

787 

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

789 

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 

800 

801 

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

808 

809 

810def resolution2(prec1, prec2=None): 

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

812 precisions. 

813 

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

818 

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. 

822 

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

824 

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! 

829 

830 if prec1: 

831 p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec)) 

832 res1 = res2 = ldexp(res1, -(p - p // 2)) 

833 

834 if prec2: 

835 p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec)) 

836 res2 = ldexp(res2, -(p // 2)) 

837 

838 return Resolutions2Tuple(res1, res2) 

839 

840 

841def sizes(geohash): 

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

843 

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

845 

846 @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal height and 

847 longitudinal width in (C{meter}). 

848 

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

850 ''' 

851 return _2Geohash(geohash).sizes 

852 

853 

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

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

856 L{pygeodesy.vincentys} formula. 

857 

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

862 

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

864 

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) 

869 

870 

871__all__ += _ALL_OTHER(bounds, # functions 

872 decode, decode2, decode_error, distance_, 

873 encode, equirectangular4, euclidean_, haversine_, 

874 neighbors, precision, resolution2, sizes, vincentys_) 

875 

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.