Coverage for pygeodesy/geohash.py: 96%

283 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-04-02 08:40 -0400

1 

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

3 

4u'''Geohash en-/decoding. 

5 

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

7encode, decode 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>}, 

14U{Geohash<https://GitHub.com/vinsci/geohash>}, 

15U{PyGeohash<https://PyPI.org/project/pygeohash>} and 

16U{Geohash-Javascript<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 

25from pygeodesy.formy import equirectangular_ as _equirectangular_, \ 

26 equirectangular, euclidean, haversine, vincentys 

27from pygeodesy.interns import NN, _COMMA_, _DOT_, _E_, _N_, _NE_, _NW_, \ 

28 _S_, _SE_, _SW_, _W_ 

29from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER 

30from pygeodesy.named import _NamedDict, _NamedTuple, nameof, _xnamed 

31from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \ 

32 LatLon2Tuple, PhiLam2Tuple 

33from pygeodesy.props import deprecated_function, deprecated_method, \ 

34 deprecated_property_RO, Property_RO 

35from pygeodesy.streprs import fstr 

36from pygeodesy.units import Degrees_, Int, Lat, Lon, Precision_, Str, \ 

37 _xStrError 

38 

39from math import fabs, ldexp, log10, radians 

40 

41__all__ = _ALL_LAZY.geohash 

42__version__ = '23.03.19' 

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 return (_floatuple(20032e3, 20000e3, 11292815.096), # 0 

76 _floatuple( 5003e3, 5000e3, 2821794.075), # 1 

77 _floatuple( 650e3, 1225e3, 503442.397), # 2 

78 _floatuple( 156e3, 156e3, 88013.575), # 3 

79 _floatuple( 19500, 39100, 15578.683), # 4 

80 _floatuple( 4890, 4890, 2758.887), # 5 

81 _floatuple( 610, 1220, 486.710), # 6 

82 _floatuple( 153, 153, 86.321), # 7 

83 _floatuple( 19.1, 38.2, 15.239), # 8 

84 _floatuple( 4.77, 4.77, 2.691), # 9 

85 _floatuple( 0.596, 1.19, 0.475), # 10 

86 _floatuple( 0.149, 0.149, 0.084), # 11 

87 _floatuple( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec 

88 

89_GH = _GH() # PYCHOK singleton 

90_MaxPrec = 12 

91 

92 

93def _2bounds(LatLon, LatLon_kwds, s, w, n, e, name=NN): 

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

95 ''' 

96 if LatLon is None: 

97 r = Bounds4Tuple(s, w, n, e, name=name) 

98 else: 

99 sw = _xnamed(LatLon(s, w, **LatLon_kwds), name) 

100 ne = _xnamed(LatLon(n, e, **LatLon_kwds), name) 

101 r = Bounds2Tuple(sw, ne, name=name) 

102 return r # _xnamed(r, name) 

103 

104 

105def _2center(bounds): 

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

107 ''' 

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

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

110 

111 

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

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

114 ''' 

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

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

117 Lon(lon, Error=GeohashError)) 

118 

119 

120def _2Geohash(geohash): 

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

122 ''' 

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

124 Geohash(geohash) 

125 

126 

127def _2geostr(geohash): 

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

129 ''' 

130 try: 

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

132 raise ValueError 

133 geostr = geohash.lower() 

134 for c in geostr: 

135 if c not in _GH.DecodedBase32: 

136 raise ValueError 

137 return geostr 

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

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

140 

141 

142class Geohash(Str): 

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

144 ''' 

145 # no str.__init__ in Python 3 

146 def __new__(cls, cll, precision=None, name=NN): 

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

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

149 

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

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

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

153 some examples. 

154 @kwarg name: Optional name (C{str}). 

155 

156 @return: New L{Geohash}. 

157 

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

159 

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

161 ''' 

162 ll = None 

163 

164 if isinstance(cll, Geohash): 

165 gh = _2geostr(str(cll)) 

166 

167 elif isstr(cll): 

168 if _COMMA_ in cll: 

169 ll = _2fll(*parse3llh(cll)) 

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

171 else: 

172 gh = _2geostr(cll) 

173 

174 else: # assume LatLon 

175 try: 

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

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

178 except AttributeError: 

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

180 

181 self = Str.__new__(cls, gh, name=name or nameof(cll)) 

182 self._latlon = ll 

183 return self 

184 

185 @deprecated_property_RO 

186 def ab(self): 

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

188 return self.philam 

189 

190 def adjacent(self, direction, name=NN): 

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

192 

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

194 @kwarg name: Optional name (C{str}), otherwise the name 

195 of this cell plus C{.D}irection. 

196 

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

198 

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

200 ''' 

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

202 

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

204 if D not in _GH.Neighbors: 

205 raise GeohashError(direction=direction) 

206 

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

208 

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

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

211 if i < 0: 

212 raise GeohashError(geohash=self) 

213 

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

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

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

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

218 

219 n = name or self.name 

220 if n: 

221 n = _DOT_(n, D) 

222 # append letter for direction to parent 

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

224 

225 @Property_RO 

226 def _bounds(self): 

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

228 ''' 

229 return bounds(self) 

230 

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

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

233 geohash cell. 

234 

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

236 or C{None}. 

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

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

239 

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

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

242 C{B{LatLon} is None}, 

243 ''' 

244 r = self._bounds 

245 return r if LatLon is None else \ 

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

247 

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

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

250 ''' 

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

252 return func_(*lls, **kwds) 

253 

254 def distanceTo(self, other): 

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

256 based the cell sizes. 

257 

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

259 

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

261 

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

263 C{LatLon} or C{str}. 

264 ''' 

265 other = _2Geohash(other) 

266 

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

268 if n: 

269 for n in range(n): 

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

271 break 

272 return _GH.Sizes[n][2] 

273 

274 @deprecated_method 

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

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

277 return self.distanceTo(other) 

278 

279 distance1 = distance1To 

280 

281 @deprecated_method 

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

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

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

285 

286 distance2 = distance2To 

287 

288 @deprecated_method 

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

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

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

292 

293 distance3 = distance3To 

294 

295 def equirectangularTo(self, other, radius=R_M, adjust=False, wrap=False): 

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

297 using function L{pygeodesy.equirectangular}. 

298 

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

300 @kwarg radius: Mean earth radius, ellipsoid or datum 

301 (C{meter}, L{Ellipsoid}, L{Ellipsoid2}, 

302 L{Datum} or L{a_f2Tuple}) or C{None}. 

303 @kwarg adjust: Adjust the wrapped, unrolled longitudinal 

304 delta by the cosine of the mean latitude 

305 C{bool}). 

306 @kwarg wrap: Wrap and unroll longitudes (C{bool}). 

307 

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

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

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

311 

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

313 or 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 = dict(adjust=adjust, limit=None, wrap=wrap) 

320 return equirectangular( *lls, radius=radius, **kwds) if radius else \ 

321 _equirectangular_(*lls, **kwds).distance2 

322 

323 def euclideanTo(self, other, radius=R_M, adjust=False, wrap=False): 

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

325 using function L{pygeodesy.euclidean}. 

326 

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

328 @kwarg radius: Mean earth radius, ellipsoid or datum 

329 (C{meter}, L{Ellipsoid}, L{Ellipsoid2}, 

330 L{Datum} or L{a_f2Tuple}). 

331 @kwarg adjust: Adjust the wrapped, unrolled longitudinal 

332 delta by the cosine of the mean latitude 

333 C{bool}). 

334 @kwarg wrap: Wrap and unroll longitudes (C{bool}). 

335 

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

337 ellipsoid or datum axes). 

338 

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

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

341 ''' 

342 return self._distanceTo(euclidean, other, radius=radius, 

343 adjust=adjust, wrap=wrap) 

344 

345 def haversineTo(self, other, radius=R_M, wrap=False): 

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

347 the L{pygeodesy.haversine} formula. 

348 

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

350 @kwarg radius: Mean earth radius, ellipsoid or datum 

351 (C{meter}, L{Ellipsoid}, L{Ellipsoid2}, 

352 L{Datum} or L{a_f2Tuple}). 

353 @kwarg wrap: Wrap and unroll longitudes (C{bool}). 

354 

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

356 ellipsoid or datum axes). 

357 

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

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

360 ''' 

361 return self._distanceTo(haversine, other, radius=radius, wrap=wrap) 

362 

363 @Property_RO 

364 def latlon(self): 

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

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

367 ''' 

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

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

370 

371 @Property_RO 

372 def neighbors(self): 

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

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

375 ''' 

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

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

378 name=self.name) 

379 

380 @Property_RO 

381 def philam(self): 

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

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

384 ''' 

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

386 

387 @Property_RO 

388 def precision(self): 

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

390 ''' 

391 return len(self) 

392 

393 @Property_RO 

394 def sizes(self): 

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

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

397 ''' 

398 z = _GH.Sizes 

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

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

401 

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

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

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

405 

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

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

408 keyword arguments, ignored if 

409 C{B{LatLon} is None}. 

410 

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

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

413 is C{None}. 

414 

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

416 

417 @example: 

418 

419 >>> from sphericalTrigonometry import LatLon 

420 >>> ll = Geohash('u120fxw').toLatLon(LatLon) 

421 >>> print(repr(ll)) # LatLon(52°12′17.9″N, 000°07′07.64″E) 

422 >>> print(ll) # 52.204971°N, 000.11879°E 

423 ''' 

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

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

426 

427 def vincentysTo(self, other, radius=R_M, wrap=False): 

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

429 the L{pygeodesy.vincentys} formula. 

430 

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

432 @kwarg radius: Mean earth radius, ellipsoid or datum 

433 (C{meter}, L{Ellipsoid}, L{Ellipsoid2}, 

434 L{Datum} or L{a_f2Tuple}). 

435 @kwarg wrap: Wrap and unroll longitudes (C{bool}). 

436 

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

438 ellipsoid or datum axes). 

439 

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

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

442 ''' 

443 return self._distanceTo(vincentys, other, radius=radius, wrap=wrap) 

444 

445 @Property_RO 

446 def N(self): 

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

448 ''' 

449 return self.adjacent(_N_) 

450 

451 @Property_RO 

452 def S(self): 

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

454 ''' 

455 return self.adjacent(_S_) 

456 

457 @Property_RO 

458 def E(self): 

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

460 ''' 

461 return self.adjacent(_E_) 

462 

463 @Property_RO 

464 def W(self): 

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

466 ''' 

467 return self.adjacent(_W_) 

468 

469 @Property_RO 

470 def NE(self): 

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

472 ''' 

473 return self.N.E 

474 

475 @Property_RO 

476 def NW(self): 

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

478 ''' 

479 return self.N.W 

480 

481 @Property_RO 

482 def SE(self): 

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

484 ''' 

485 return self.S.E 

486 

487 @Property_RO 

488 def SW(self): 

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

490 ''' 

491 return self.S.W 

492 

493 

494class GeohashError(_ValueError): 

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

496 ''' 

497 pass 

498 

499 

500class Neighbors8Dict(_NamedDict): 

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

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

503 ''' 

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

505 

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

507 kwds = _xkwds(kwds, **_Neighbors8Defaults) 

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

509 

510 

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

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

513 

514 

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

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

517 

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

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

520 or C{None}. 

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

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

523 

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

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

526 lonW, latN, lonE)}. 

527 

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

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

530 B{C{LatLon_kwds}}. 

531 

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

533 

534 @example: 

535 

536 >>> geohash.bounds('u120fxw') # 52.20428467, 0.11810303, 52.20565796, 0.11947632 

537 >>> geohash.decode('u120fxw') # '52.205', '0.1188' 

538 ''' 

539 gh = _2Geohash(geohash) 

540 if len(gh) < 1: 

541 raise GeohashError(geohash=geohash) 

542 

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

544 try: 

545 d = True 

546 for c in gh.lower(): 

547 i = _GH.DecodedBase32[c] 

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

549 if d: # longitude 

550 if i & m: 

551 w = favg(w, e) 

552 else: 

553 e = favg(w, e) 

554 else: # latitude 

555 if i & m: 

556 s = favg(s, n) 

557 else: 

558 n = favg(s, n) 

559 d = not d 

560 except KeyError: 

561 raise GeohashError(geohash=geohash) 

562 

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

564 name=nameof(geohash)) 

565 

566 

567def _bounds3(geohash): 

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

569 ''' 

570 b = bounds(geohash) 

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

572 

573 

574def decode(geohash): 

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

576 centre of) geohash cell to reasonable precision. 

577 

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

579 

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

581 

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

583 C{LatLon} or C{str}. 

584 

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

586 

587 @example: 

588 

589 >>> geohash.decode('u120fxw') # '52.205', '0.1188' 

590 >>> geohash.decode('sunny') # '23.708', '42.473' Saudi Arabia 

591 >>> geohash.decode('fur') # '69.6', '-45.7' Greenland 

592 >>> geohash.decode('reef') # '-24.87', '162.95' Coral Sea 

593 >>> geohash.decode('geek') # '65.48', '-17.75' Iceland 

594 ''' 

595 b, h, w = _bounds3(geohash) 

596 lat, lon = _2center(b) 

597 

598 # round to near centre without excessive precision to 

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

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

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

602 

603 

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

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

606 centre of) geohash cell to reasonable precision. 

607 

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

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

610 or C{None}. 

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

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

613 

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

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

616 

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

618 C{LatLon} or C{str}. 

619 

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

621 ''' 

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

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

624 return _xnamed(r, decode2.__name__) 

625 

626 

627def decode_error(geohash): 

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

629 this geohash. 

630 

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

632 

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

634 longitudinal errors in (C{degrees}). 

635 

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

637 C{LatLon} or C{str}. 

638 

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

640 

641 @example: 

642 

643 >>> geohash.decode_error('u120fxw') # 0.00068665, 0.00068665 

644 >>> geohash.decode_error('fur') # 0.703125, 0.703125 

645 >>> geohash.decode_error('fu') # 2.8125, 5.625 

646 >>> geohash.decode_error('f') # 22.5, 22.5 

647 ''' 

648 _, h, w = _bounds3(geohash) 

649 return LatLon2Tuple(h * _0_5, # Height error 

650 w * _0_5) # Width error 

651 

652 

653def distance_(geohash1, geohash2): 

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

655 

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

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

658 

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

660 

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

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

663 

664 @example: 

665 

666 >>> geohash.distance_('u120fxwsh', 'u120fxws0') # 15.239 

667 ''' 

668 return _2Geohash(geohash1).distanceTo(geohash2) 

669 

670 

671@deprecated_function 

672def distance1(geohash1, geohash2): 

673 '''DEPRECATED, used L{geohash.distance_}.''' 

674 return distance_(geohash1, geohash2) 

675 

676 

677@deprecated_function 

678def distance2(geohash1, geohash2): 

679 '''DEPRECATED, used L{geohash.equirectangular_}.''' 

680 return equirectangular_(geohash1, geohash2) 

681 

682 

683@deprecated_function 

684def distance3(geohash1, geohash2): 

685 '''DEPRECATED, used L{geohash.haversine_}.''' 

686 return haversine_(geohash1, geohash2) 

687 

688 

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

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

691 precision or if not provided, to an automatically evaluated 

692 precision. 

693 

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

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

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

697 1..12). 

698 

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

700 

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

702 

703 @example: 

704 

705 >>> geohash.encode(52.205, 0.119, 7) # 'u120fxw' 

706 >>> geohash.encode(52.205, 0.119, 12) # 'u120fxwshvkg' 

707 >>> geohash.encode(52.205, 0.1188, 12) # 'u120fxws0jre' 

708 >>> geohash.encode(52.205, 0.1188) # 'u120fxw' 

709 >>> geohash.encode( 0, 0) # 's00000000000' 

710 ''' 

711 lat, lon = _2fll(lat, lon) 

712 

713 if precision is None: 

714 # Infer precision by refining geohash until 

715 # it matches precision of supplied lat/lon. 

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

717 gh = encode(lat, lon, p) 

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

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

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

721 return gh 

722 p = _MaxPrec 

723 else: 

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

725 

726 b = i = 0 

727 d, gh = True, [] 

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

729 

730 while p > 0: 

731 i += i 

732 if d: # bisect longitude 

733 m = favg(e, w) 

734 if lon < m: 

735 e = m 

736 else: 

737 w = m 

738 i += 1 

739 else: # bisect latitude 

740 m = favg(n, s) 

741 if lat < m: 

742 n = m 

743 else: 

744 s = m 

745 i += 1 

746 d = not d 

747 

748 b += 1 

749 if b == 5: 

750 # 5 bits gives a character: 

751 # append it and start over 

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

753 b = i = 0 

754 p -= 1 

755 

756 return NN.join(gh) 

757 

758 

759def equirectangular_(geohash1, geohash2, radius=R_M): 

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

761 L{pygeodesy.equirectangular} formula. 

762 

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

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

765 @kwarg radius: Mean earth radius (C{meter}) or C{None}. 

766 

767 @return: Approximate distance (C{meter}, same units as 

768 B{C{radius}}). 

769 

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

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

772 

773 @example: 

774 

775 >>> geohash.equirectangular_('u120fxwsh', 'u120fxws0') # 19.0879 

776 ''' 

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

778 

779 

780def haversine_(geohash1, geohash2, radius=R_M): 

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

782 using the L{pygeodesy.haversine} formula. 

783 

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

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

786 @kwarg radius: Mean earth radius (C{meter}). 

787 

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

789 B{C{radius}}). 

790 

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

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

793 

794 @example: 

795 

796 >>> geohash.haversine_('u120fxwsh', 'u120fxws0') # 11.6978 

797 ''' 

798 return _2Geohash(geohash1).haversineTo(geohash2, radius=radius) 

799 

800 

801def neighbors(geohash): 

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

803 

804 @arg geohash: Cell for which neighbors are requested 

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

806 

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

808 of L{Geohash}es. 

809 

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

811 C{LatLon} or C{str}. 

812 ''' 

813 return _2Geohash(geohash).neighbors 

814 

815 

816def precision(res1, res2=None): 

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

818 (geographic) resolutions. 

819 

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

821 (C{degrees}). 

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

823 resolution (C{degrees}). 

824 

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

826 

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

828 

829 @see: C++ class U{Geohash 

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

831 ''' 

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

833 if res2 is None: 

834 t = r, r 

835 for p in range(1, _MaxPrec): 

836 if resolution2(p, None) <= t: 

837 return p 

838 

839 else: 

840 t = r, Degrees_(res2=res2, low=_0_0, Error=GeohashError) 

841 for p in range(1, _MaxPrec): 

842 if resolution2(p, p) <= t: 

843 return p 

844 

845 return _MaxPrec 

846 

847 

848class Resolutions2Tuple(_NamedTuple): 

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

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

851 ''' 

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

853 _Units_ = ( Degrees_, Degrees_) 

854 

855 

856def resolution2(prec1, prec2=None): 

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

858 precisions. 

859 

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

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

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

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

864 

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

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

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

868 

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

870 

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

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

873 ''' 

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

875 

876 if prec1: 

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

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

879 

880 if prec2: 

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

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

883 

884 return Resolutions2Tuple(res1, res2) 

885 

886 

887def sizes(geohash): 

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

889 

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

891 C{str}). 

892 

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

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

895 

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

897 C{LatLon} or C{str}. 

898 ''' 

899 return _2Geohash(geohash).sizes 

900 

901 

902__all__ += _ALL_OTHER(bounds, # functions 

903 decode, decode2, decode_error, distance_, 

904 encode, equirectangular_, haversine_, 

905 neighbors, precision, resolution2, sizes) 

906 

907# **) MIT License 

908# 

909# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

910# 

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

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

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

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

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

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

917# 

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

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

920# 

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

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

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

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

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

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

927# OTHER DEALINGS IN THE SOFTWARE.