Coverage for pygeodesy/geohash.py: 98%

286 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -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 

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 _xStrError 

37 

38from math import fabs, ldexp, log10, radians 

39 

40__all__ = _ALL_LAZY.geohash 

41__version__ = '24.06.10' 

42 

43_formy = _MODS.into(formy=__name__) 

44 

45 

46class _GH(object): 

47 '''(INTERNAL) Lazily defined constants. 

48 ''' 

49 def _4d(self, n, e, s, w): # helper 

50 return dict(N=(n, e), S=(s, w), 

51 E=(e, n), W=(w, s)) 

52 

53 @Property_RO 

54 def Borders(self): 

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

56 

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

58 

59 @Property_RO 

60 def DecodedBase32(self): # inverse GeohashBase32 map 

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

62 

63 # Geohash-specific base32 map 

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

65 

66 @Property_RO 

67 def Neighbors(self): 

68 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy', 

69 'bc01fg45238967deuvhjyznpkmstqrwx', 

70 '14365h7k9dcfesgujnmqp0r2twvyx8zb', 

71 '238967debc01fg45kmstqrwxuvhjyznp') 

72 

73 @Property_RO 

74 def Sizes(self): # lat-, lon and radial size (in meter) 

75 # ... where radial = sqrt(latSize * lonWidth / PI) 

76 _t = _floatuple 

77 return (_t(20032e3, 20000e3, 11292815.096), # 0 

78 _t( 5003e3, 5000e3, 2821794.075), # 1 

79 _t( 650e3, 1225e3, 503442.397), # 2 

80 _t( 156e3, 156e3, 88013.575), # 3 

81 _t( 19500, 39100, 15578.683), # 4 

82 _t( 4890, 4890, 2758.887), # 5 

83 _t( 610, 1220, 486.710), # 6 

84 _t( 153, 153, 86.321), # 7 

85 _t( 19.1, 38.2, 15.239), # 8 

86 _t( 4.77, 4.77, 2.691), # 9 

87 _t( 0.596, 1.19, 0.475), # 10 

88 _t( 0.149, 0.149, 0.084), # 11 

89 _t( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec 

90 

91_GH = _GH() # PYCHOK singleton 

92_MaxPrec = 12 

93 

94 

95def _2bounds(LatLon, LatLon_kwds, s, w, n, e, **name): 

96 '''(INTERNAL) Return SW and NE bounds. 

97 ''' 

98 if LatLon is None: 

99 r = Bounds4Tuple(s, w, n, e, **name) 

100 else: 

101 kwds = _xkwds(LatLon_kwds, **name) 

102 r = Bounds2Tuple(LatLon(s, w, **kwds), 

103 LatLon(n, e, **kwds), **name) 

104 return r 

105 

106 

107def _2center(bounds): 

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

109 ''' 

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

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

112 

113 

114def _2fll(lat, lon, *unused): 

115 '''(INTERNAL) Convert lat, lon to 2-tuple of floats. 

116 ''' 

117 # lat, lon = parseDMS2(lat, lon) 

118 return (Lat(lat, Error=GeohashError), 

119 Lon(lon, Error=GeohashError)) 

120 

121 

122def _2Geohash(geohash): 

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

124 ''' 

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

126 Geohash(geohash) 

127 

128 

129def _2geostr(geohash): 

130 '''(INTERNAL) Check a geohash string. 

131 ''' 

132 try: 

133 if not (0 < len(geohash) <= _MaxPrec): 

134 raise ValueError() 

135 geostr = geohash.lower() 

136 for c in geostr: 

137 if c not in _GH.DecodedBase32: 

138 raise ValueError() 

139 return geostr 

140 except (AttributeError, TypeError, ValueError) as x: 

141 raise GeohashError(Geohash.__name__, geohash, cause=x) 

142 

143 

144class Geohash(Str): 

145 '''Geohash class, a named C{str}. 

146 ''' 

147 # no str.__init__ in Python 3 

148 def __new__(cls, cll, precision=None, **name): 

149 '''New L{Geohash} from an other L{Geohash} instance or C{str} 

150 or from a C{LatLon} instance or C{str}. 

151 

152 @arg cll: Cell or location (L{Geohash}, C{LatLon} or C{str}). 

153 @kwarg precision: Optional, the desired geohash length (C{int} 

154 1..12), see function L{geohash.encode} for 

155 some examples. 

156 @kwarg name: Optional C{B{name}=NN} (C{str}). 

157 

158 @return: New L{Geohash}. 

159 

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

161 

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

163 ''' 

164 ll = None 

165 

166 if isinstance(cll, Geohash): 

167 gh = _2geostr(str(cll)) 

168 

169 elif isstr(cll): 

170 if _COMMA_ in cll: 

171 ll = _2fll(*parse3llh(cll)) 

172 gh = encode(*ll, precision=precision) 

173 else: 

174 gh = _2geostr(cll) 

175 

176 else: # assume LatLon 

177 try: 

178 ll = _2fll(cll.lat, cll.lon) 

179 gh = encode(*ll, precision=precision) 

180 except AttributeError: 

181 raise _xStrError(Geohash, cll=cll, Error=GeohashError) 

182 

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

184 self._latlon = ll 

185 return self 

186 

187 @deprecated_property_RO 

188 def ab(self): 

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

190 return self.philam 

191 

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

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

194 

195 @arg direction: Compass direction ('N', 'S', 'E' or 'W'). 

196 @kwarg name: Optional C{B{name}=NN} (C{str}) otherwise this 

197 cell's name, either extended with C{.D}irection. 

198 

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

200 

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

202 ''' 

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

204 

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

206 if D not in _GH.Neighbors: 

207 raise GeohashError(direction=direction) 

208 

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

210 

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

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

213 if i < 0: 

214 raise GeohashError(geohash=self) 

215 

216 p = self[:-1] # hash without last char 

217 # check for edge-cases which don't share common prefix 

218 if p and (c in _GH.Borders[D][e]): 

219 p = Geohash(p).adjacent(D) 

220 

221 n = self._name__(name) 

222 if n: 

223 n = _DOT_(n, D) 

224 # append letter for direction to parent 

225 return Geohash(p + _GH.GeohashBase32[i], name=n) 

226 

227 @Property_RO 

228 def _bounds(self): 

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

230 ''' 

231 return bounds(self) 

232 

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

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

235 geohash cell. 

236 

237 @kwarg LatLon: Optional class to return I{bounds} (C{LatLon}) 

238 or C{None}. 

239 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword 

240 arguments, ignored if B{C{LatLon}} is C{None}. 

241 

242 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s 

243 or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if 

244 C{B{LatLon} is None}, 

245 ''' 

246 r = self._bounds 

247 return r if LatLon is None else \ 

248 _2bounds(LatLon, LatLon_kwds, *r, name=self.name) 

249 

250 def _distanceTo(self, func_, other, **kwds): 

251 '''(INTERNAL) Helper for distances, see C{.formy._distanceTo*}. 

252 ''' 

253 lls = self.latlon + _2Geohash(other).latlon 

254 return func_(*lls, **kwds) 

255 

256 def distanceTo(self, other): 

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

258 based the cell sizes. 

259 

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

261 

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

263 

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

265 C{LatLon} or C{str}. 

266 ''' 

267 other = _2Geohash(other) 

268 

269 n = min(len(self), len(other), len(_GH.Sizes)) 

270 if n: 

271 for n in range(n): 

272 if self[n] != other[n]: 

273 break 

274 return _GH.Sizes[n][2] 

275 

276 @deprecated_method 

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

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

279 return self.distanceTo(other) 

280 

281 distance1 = distance1To 

282 

283 @deprecated_method 

284 def distance2To(self, other, radius=R_M, adjust=False, wrap=False): # PYCHOK no cover 

285 '''DEPRECATED, use method L{equirectangularTo}.''' 

286 return self.equirectangularTo(other, radius=radius, adjust=adjust, wrap=wrap) 

287 

288 distance2 = distance2To 

289 

290 @deprecated_method 

291 def distance3To(self, other, radius=R_M, wrap=False): # PYCHOK no cover 

292 '''DEPRECATED, use method L{haversineTo}.''' 

293 return self.haversineTo(other, radius=radius, wrap=wrap) 

294 

295 distance3 = distance3To 

296 

297 def equirectangularTo(self, other, radius=R_M, **adjust_limit_wrap): 

298 '''Approximate the distance between this and an other geohash 

299 using function L{pygeodesy.equirectangular}. 

300 

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

302 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter}, 

303 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or 

304 L{a_f2Tuple}) or C{None}, see function 

305 L{pygeodesy.equirectangular}. 

306 @kwarg adjust_limit_wrap: Optional keyword arguments for function 

307 L{pygeodesy.equirectangular4}, overriding defaults 

308 C{B{adjust}=False, B{limit}=None} and C{B{wrap}=False}. 

309 

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

311 ellipsoid or datum axes or C{radians I{squared}} if 

312 B{C{radius}} is C{None} or C{0}). 

313 

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

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

316 

317 @see: U{Local, flat earth approximation 

318 <https://www.EdWilliams.org/avform.htm#flat>}, functions 

319 ''' 

320 lls = self.latlon + _2Geohash(other).latlon 

321 kwds = _xkwds(adjust_limit_wrap, adjust=False, limit=None, wrap=False) 

322 return _formy.equirectangular( *lls, radius=radius, **kwds) if radius else \ 

323 _formy.equirectangular4(*lls, **kwds).distance2 

324 

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

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

327 function L{pygeodesy.euclidean}. 

328 

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

330 @kwarg radius_adjust_wrap: Optional keyword arguments for function 

331 L{pygeodesy.euclidean}. 

332 

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

334 ellipsoid or datum axes). 

335 

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

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

338 ''' 

339 return self._distanceTo(_formy.euclidean, other, **radius_adjust_wrap) 

340 

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

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

343 the L{pygeodesy.haversine} formula. 

344 

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

346 @kwarg radius_wrap: Optional keyword arguments for function 

347 L{pygeodesy.haversine}. 

348 

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

350 ellipsoid or datum axes). 

351 

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

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

354 ''' 

355 return self._distanceTo(_formy.haversine, other, **radius_wrap) 

356 

357 @Property_RO 

358 def latlon(self): 

359 '''Get the lat- and longitude of (the approximate center of) 

360 this geohash as a L{LatLon2Tuple}C{(lat, lon)} in C{degrees}. 

361 ''' 

362 lat, lon = self._latlon or _2center(self.bounds()) 

363 return LatLon2Tuple(lat, lon, name=self.name) 

364 

365 @Property_RO 

366 def neighbors(self): 

367 '''Get all 8 adjacent cells as a L{Neighbors8Dict}C{(N, NE, 

368 E, SE, S, SW, W, NW)} of L{Geohash}es. 

369 ''' 

370 return Neighbors8Dict(N=self.N, NE=self.NE, E=self.E, SE=self.SE, 

371 S=self.S, SW=self.SW, W=self.W, NW=self.NW, 

372 name=self.name) 

373 

374 @Property_RO 

375 def philam(self): 

376 '''Get the lat- and longitude of (the approximate center of) 

377 this geohash as a L{PhiLam2Tuple}C{(phi, lam)} in C{radians}. 

378 ''' 

379 return PhiLam2Tuple(map2(radians, self.latlon), name=self.name) # *map2 

380 

381 @Property_RO 

382 def precision(self): 

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

384 ''' 

385 return len(self) 

386 

387 @Property_RO 

388 def sizes(self): 

389 '''Get the lat- and longitudinal size of this cell as 

390 a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}). 

391 ''' 

392 z = _GH.Sizes 

393 n = min(len(z) - 1, max(self.precision, 1)) 

394 return LatLon2Tuple(z[n][:2], name=self.name) # *z XXX Height, Width? 

395 

396 def toLatLon(self, LatLon=None, **LatLon_kwds): 

397 '''Return (the approximate center of) this geohash cell 

398 as an instance of the supplied C{LatLon} class. 

399 

400 @arg LatLon: Class to use (C{LatLon}) or C{None}. 

401 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} 

402 keyword arguments, ignored if 

403 C{B{LatLon} is None}. 

404 

405 @return: This geohash location (B{C{LatLon}}) or a 

406 L{LatLon2Tuple}C{(lat, lon)} if B{C{LatLon}} 

407 is C{None}. 

408 

409 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}. 

410 ''' 

411 return self.latlon if LatLon is None else _xnamed(LatLon( 

412 *self.latlon, **LatLon_kwds), self.name) 

413 

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

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

416 the L{pygeodesy.vincentys} formula. 

417 

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

419 @kwarg radius_wrap: Optional keyword arguments for function 

420 L{pygeodesy.vincentys}. 

421 

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

423 ellipsoid or datum axes). 

424 

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

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

427 ''' 

428 return self._distanceTo(_formy.vincentys, other, **radius_wrap) 

429 

430 @Property_RO 

431 def N(self): 

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

433 ''' 

434 return self.adjacent(_N_) 

435 

436 @Property_RO 

437 def S(self): 

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

439 ''' 

440 return self.adjacent(_S_) 

441 

442 @Property_RO 

443 def E(self): 

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

445 ''' 

446 return self.adjacent(_E_) 

447 

448 @Property_RO 

449 def W(self): 

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

451 ''' 

452 return self.adjacent(_W_) 

453 

454 @Property_RO 

455 def NE(self): 

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

457 ''' 

458 return self.N.E 

459 

460 @Property_RO 

461 def NW(self): 

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

463 ''' 

464 return self.N.W 

465 

466 @Property_RO 

467 def SE(self): 

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

469 ''' 

470 return self.S.E 

471 

472 @Property_RO 

473 def SW(self): 

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

475 ''' 

476 return self.S.W 

477 

478 

479class GeohashError(_ValueError): 

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

481 ''' 

482 pass 

483 

484 

485class Neighbors8Dict(_NamedDict): 

486 '''8-Dict C{(N, NE, E, SE, S, SW, W, NW)} of L{Geohash}es, 

487 providing key I{and} attribute access to the items. 

488 ''' 

489 _Keys_ = (_N_, _NE_, _E_, _SE_, _S_, _SW_, _W_, _NW_) 

490 

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

492 kwds = _xkwds(kwds, **_Neighbors8Defaults) 

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

494 

495 

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

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

498 

499 

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

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

502 

503 @arg geohash: To be bound (L{Geohash}). 

504 @kwarg LatLon: Optional class to return the bounds (C{LatLon}) 

505 or C{None}. 

506 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword 

507 arguments, ignored if C{B{LatLon} is None}. 

508 

509 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} of B{C{LatLon}}s 

510 or if B{C{LatLon}} is C{None}, a L{Bounds4Tuple}C{(latS, 

511 lonW, latN, lonE)}. 

512 

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

514 or C{str} or invalid B{C{LatLon}} or invalid 

515 B{C{LatLon_kwds}}. 

516 

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

518 ''' 

519 gh = _2Geohash(geohash) 

520 if len(gh) < 1: 

521 raise GeohashError(geohash=geohash) 

522 

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

524 try: 

525 d, _avg = True, favg 

526 for c in gh.lower(): 

527 i = _GH.DecodedBase32[c] 

528 for m in (16, 8, 4, 2, 1): 

529 if d: # longitude 

530 a = _avg(w, e) 

531 if (i & m): 

532 w = a 

533 else: 

534 e = a 

535 else: # latitude 

536 a = _avg(s, n) 

537 if (i & m): 

538 s = a 

539 else: 

540 n = a 

541 d = not d 

542 except KeyError: 

543 raise GeohashError(geohash=geohash) 

544 

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

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

547 

548 

549def _bounds3(geohash): 

550 '''(INTERNAL) Return 3-tuple C{(bounds, height, width)}. 

551 ''' 

552 b = bounds(geohash) 

553 return b, (b.latN - b.latS), (b.lonE - b.lonW) 

554 

555 

556def decode(geohash): 

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

558 centre of) geohash cell to reasonable precision. 

559 

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

561 

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

563 

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

565 C{LatLon} or C{str}. 

566 

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

568 ''' 

569 b, h, w = _bounds3(geohash) 

570 lat, lon = _2center(b) 

571 

572 # round to near centre without excessive precision to 

573 # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros 

574 return (fstr(lat, prec=int(2 - log10(h))), 

575 fstr(lon, prec=int(2 - log10(w)))) # strs! 

576 

577 

578def decode2(geohash, LatLon=None, **LatLon_kwds): 

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

580 of) geohash cell to reasonable precision. 

581 

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

583 @kwarg LatLon: Optional class to return the location (C{LatLon}) 

584 or C{None}. 

585 @kwarg LatLon_kwds: Optional, addtional B{C{LatLon}} keyword 

586 arguments, ignored if C{B{LatLon} is None}. 

587 

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

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

590 

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

592 C{LatLon} or C{str}. 

593 

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

595 ''' 

596 t = map2(float, decode(geohash)) 

597 r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t 

598 return _xnamed(r, name__=decode2) 

599 

600 

601def decode_error(geohash): 

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

603 this geohash. 

604 

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

606 

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

608 longitudinal errors in (C{degrees}). 

609 

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

611 C{LatLon} or C{str}. 

612 

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

614 ''' 

615 _, h, w = _bounds3(geohash) 

616 return LatLon2Tuple(h * _0_5, # Height error 

617 w * _0_5) # Width error 

618 

619 

620def distance_(geohash1, geohash2): 

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

622 

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

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

625 

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

627 

628 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a 

629 L{Geohash}, C{LatLon} or C{str}. 

630 ''' 

631 return _2Geohash(geohash1).distanceTo(geohash2) 

632 

633 

634@deprecated_function 

635def distance1(geohash1, geohash2): 

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

637 return distance_(geohash1, geohash2) 

638 

639 

640@deprecated_function 

641def distance2(geohash1, geohash2): 

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

643 return equirectangular4(geohash1, geohash2) 

644 

645 

646@deprecated_function 

647def distance3(geohash1, geohash2): 

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

649 return haversine_(geohash1, geohash2) 

650 

651 

652def encode(lat, lon, precision=None): 

653 '''Encode a lat-/longitude as a C{geohash}, either to the specified 

654 precision or if not provided, to an automatically evaluated 

655 precision. 

656 

657 @arg lat: Latitude (C{degrees}). 

658 @arg lon: Longitude (C{degrees}). 

659 @kwarg precision: Optional, the desired geohash length (C{int} 

660 1..12). 

661 

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

663 

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

665 ''' 

666 lat, lon = _2fll(lat, lon) 

667 

668 if precision is None: 

669 # Infer precision by refining geohash until 

670 # it matches precision of supplied lat/lon. 

671 for p in range(1, _MaxPrec + 1): 

672 gh = encode(lat, lon, p) 

673 ll = map2(float, decode(gh)) 

674 if fabs(lat - ll[0]) < EPS and \ 

675 fabs(lon - ll[1]) < EPS: 

676 return gh 

677 p = _MaxPrec 

678 else: 

679 p = Precision_(precision, Error=GeohashError, low=1, high=_MaxPrec) 

680 

681 b = i = 0 

682 d, gh = True, [] 

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

684 

685 _avg = favg 

686 while p > 0: 

687 i += i 

688 if d: # bisect longitude 

689 m = _avg(e, w) 

690 if lon < m: 

691 e = m 

692 else: 

693 w = m 

694 i += 1 

695 else: # bisect latitude 

696 m = _avg(n, s) 

697 if lat < m: 

698 n = m 

699 else: 

700 s = m 

701 i += 1 

702 d = not d 

703 

704 b += 1 

705 if b == 5: 

706 # 5 bits gives a character: 

707 # append it and start over 

708 gh.append(_GH.GeohashBase32[i]) 

709 b = i = 0 

710 p -= 1 

711 

712 return NN.join(gh) 

713 

714 

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

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

717 L{pygeodesy.equirectangular} formula. 

718 

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

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

721 @kwarg radius: Mean earth radius (C{meter}) or C{None}, see method 

722 L{Geohash.equirectangularTo}. 

723 

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

725 see method L{Geohash.equirectangularTo}. 

726 

727 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a 

728 L{Geohash}, C{LatLon} or C{str}. 

729 ''' 

730 return _2Geohash(geohash1).equirectangularTo(geohash2, radius=radius) 

731 

732 

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

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

735 L{pygeodesy.euclidean} formula. 

736 

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

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

739 @kwarg radius_adjust_wrap: Optional keyword arguments for function 

740 L{pygeodesy.euclidean}. 

741 

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

743 

744 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a 

745 L{Geohash}, C{LatLon} or C{str}. 

746 ''' 

747 return _2Geohash(geohash1).euclideanTo(geohash2, **radius_adjust_wrap) 

748 

749 

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

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

752 using the L{pygeodesy.haversine} formula. 

753 

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

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

756 @kwarg radius_wrap: Optional keyword arguments for function 

757 L{pygeodesy.haversine}. 

758 

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

760 B{C{radius}}). 

761 

762 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is 

763 not a L{Geohash}, C{LatLon} or C{str}. 

764 ''' 

765 return _2Geohash(geohash1).haversineTo(geohash2, **radius_wrap) 

766 

767 

768def neighbors(geohash): 

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

770 

771 @arg geohash: Cell for which neighbors are requested 

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

773 

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

775 of L{Geohash}es. 

776 

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

778 C{LatLon} or C{str}. 

779 ''' 

780 return _2Geohash(geohash).neighbors 

781 

782 

783def precision(res1, res2=None): 

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

785 (geographic) resolutions. 

786 

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

788 (C{degrees}). 

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

790 resolution (C{degrees}). 

791 

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

793 

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

795 

796 @see: C++ class U{Geohash 

797 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}. 

798 ''' 

799 r = Degrees_(res1=res1, low=_0_0, Error=GeohashError) 

800 N = res2 is None 

801 t = r, (r if N else Degrees_(res2=res2, low=_0_0, Error=GeohashError)) 

802 for p in range(1, _MaxPrec): 

803 if resolution2(p, (None if N else p)) <= t: 

804 return p 

805 return _MaxPrec 

806 

807 

808class Resolutions2Tuple(_NamedTuple): 

809 '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and 

810 secondary I{(latitudinal)} resolution, both in C{degrees}. 

811 ''' 

812 _Names_ = ('res1', 'res2') 

813 _Units_ = ( Degrees_, Degrees_) 

814 

815 

816def resolution2(prec1, prec2=None): 

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

818 precisions. 

819 

820 @arg prec1: The given primary I{(longitudinal)} precision 

821 (C{int} 1..12). 

822 @kwarg prec2: Optional, secondary I{(latitudinal)} precision 

823 (C{int} 1..12). 

824 

825 @return: L{Resolutions2Tuple}C{(res1, res2)} with the 

826 (geographic) resolutions C{degrees}, where C{res2} 

827 B{C{is}} C{res1} if no B{C{prec2}} is given. 

828 

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

830 

831 @see: I{Karney}'s C++ class U{Geohash 

832 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}. 

833 ''' 

834 res1, res2 = _360_0, _180_0 # note ... lon, lat! 

835 

836 if prec1: 

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

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

839 

840 if prec2: 

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

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

843 

844 return Resolutions2Tuple(res1, res2) 

845 

846 

847def sizes(geohash): 

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

849 

850 @arg geohash: Cell for which size are required (L{Geohash} or 

851 C{str}). 

852 

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

854 height and longitudinal width in (C{meter}). 

855 

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

857 C{LatLon} or C{str}. 

858 ''' 

859 return _2Geohash(geohash).sizes 

860 

861 

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

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

864 L{pygeodesy.vincentys} formula. 

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 @kwarg radius_wrap: Optional keyword arguments for function 

869 L{pygeodesy.vincentys}. 

870 

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

872 

873 @raise TypeError: If B{C{geohash1}} or B{C{geohash2}} is not a 

874 L{Geohash}, C{LatLon} or C{str}. 

875 ''' 

876 return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap) 

877 

878 

879__all__ += _ALL_OTHER(bounds, # functions 

880 decode, decode2, decode_error, distance_, 

881 encode, equirectangular4, euclidean_, haversine_, 

882 neighbors, precision, resolution2, sizes, vincentys_) 

883 

884# **) MIT License 

885# 

886# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

887# 

888# Permission is hereby granted, free of charge, to any person obtaining a 

889# copy of this software and associated documentation files (the "Software"), 

890# to deal in the Software without restriction, including without limitation 

891# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

892# and/or sell copies of the Software, and to permit persons to whom the 

893# Software is furnished to do so, subject to the following conditions: 

894# 

895# The above copyright notice and this permission notice shall be included 

896# in all copies or substantial portions of the Software. 

897# 

898# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

899# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

900# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

901# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

902# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

903# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

904# OTHER DEALINGS IN THE SOFTWARE.