Coverage for pygeodesy/geohash.py: 97%

292 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-02 14:35 -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 _NamedDict, _NamedTuple, nameof, _xnamed 

30from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \ 

31 LatLon2Tuple, PhiLam2Tuple 

32from pygeodesy.props import deprecated_function, deprecated_method, \ 

33 deprecated_property_RO, 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__ = '23.12.18' 

42 

43 

44class _GH(object): 

45 '''(INTERNAL) Lazily defined constants. 

46 ''' 

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

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

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

50 

51 @Property_RO 

52 def Borders(self): 

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

54 

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

56 

57 @Property_RO 

58 def DecodedBase32(self): # inverse GeohashBase32 map 

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

60 

61 # Geohash-specific base32 map 

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

63 

64 @Property_RO 

65 def Neighbors(self): 

66 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy', 

67 'bc01fg45238967deuvhjyznpkmstqrwx', 

68 '14365h7k9dcfesgujnmqp0r2twvyx8zb', 

69 '238967debc01fg45kmstqrwxuvhjyznp') 

70 

71 @Property_RO 

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

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

74 return (_floatuple(20032e3, 20000e3, 11292815.096), # 0 

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

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

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

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

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

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

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

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

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

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

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

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

87 

88_GH = _GH() # PYCHOK singleton 

89_MaxPrec = 12 

90 

91 

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

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

94 ''' 

95 if LatLon is None: 

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

97 else: 

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

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

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

101 return r # _xnamed(r, name) 

102 

103 

104def _2center(bounds): 

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

106 ''' 

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

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

109 

110 

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

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

113 ''' 

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

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

116 Lon(lon, Error=GeohashError)) 

117 

118 

119def _2Geohash(geohash): 

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

121 ''' 

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

123 Geohash(geohash) 

124 

125 

126def _2geostr(geohash): 

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

128 ''' 

129 try: 

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

131 raise ValueError 

132 geostr = geohash.lower() 

133 for c in geostr: 

134 if c not in _GH.DecodedBase32: 

135 raise ValueError 

136 return geostr 

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

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

139 

140 

141class Geohash(Str): 

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

143 ''' 

144 # no str.__init__ in Python 3 

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

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

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

148 

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

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

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

152 some examples. 

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

154 

155 @return: New L{Geohash}. 

156 

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

158 

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

160 ''' 

161 ll = None 

162 

163 if isinstance(cll, Geohash): 

164 gh = _2geostr(str(cll)) 

165 

166 elif isstr(cll): 

167 if _COMMA_ in cll: 

168 ll = _2fll(*parse3llh(cll)) 

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

170 else: 

171 gh = _2geostr(cll) 

172 

173 else: # assume LatLon 

174 try: 

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

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

177 except AttributeError: 

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

179 

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

181 self._latlon = ll 

182 return self 

183 

184 @deprecated_property_RO 

185 def ab(self): 

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

187 return self.philam 

188 

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

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

191 

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

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

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

195 

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

197 

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

199 ''' 

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

201 

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

203 if D not in _GH.Neighbors: 

204 raise GeohashError(direction=direction) 

205 

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

207 

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

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

210 if i < 0: 

211 raise GeohashError(geohash=self) 

212 

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

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

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

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

217 

218 n = name or self.name 

219 if n: 

220 n = _DOT_(n, D) 

221 # append letter for direction to parent 

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

223 

224 @Property_RO 

225 def _bounds(self): 

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

227 ''' 

228 return bounds(self) 

229 

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

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

232 geohash cell. 

233 

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

235 or C{None}. 

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

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

238 

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

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

241 C{B{LatLon} is None}, 

242 ''' 

243 r = self._bounds 

244 return r if LatLon is None else \ 

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

246 

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

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

249 ''' 

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

251 return func_(*lls, **kwds) 

252 

253 def distanceTo(self, other): 

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

255 based the cell sizes. 

256 

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

258 

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

260 

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

262 C{LatLon} or C{str}. 

263 ''' 

264 other = _2Geohash(other) 

265 

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

267 if n: 

268 for n in range(n): 

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

270 break 

271 return _GH.Sizes[n][2] 

272 

273 @deprecated_method 

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

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

276 return self.distanceTo(other) 

277 

278 distance1 = distance1To 

279 

280 @deprecated_method 

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

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

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

284 

285 distance2 = distance2To 

286 

287 @deprecated_method 

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

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

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

291 

292 distance3 = distance3To 

293 

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

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

296 using function L{pygeodesy.equirectangular}. 

297 

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

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

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

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

302 @kwarg adjust_limit_wrap: Optional keyword arguments for 

303 function L{pygeodesy.equirectangular_}, 

304 overriding defaults C{B{adjust}=False, 

305 B{limit}=None} and C{B{wrap}=False}. 

306 

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

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

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

310 

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

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

313 

314 @see: U{Local, flat earth approximation 

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

316 ''' 

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

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

319 m = self._formy 

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

321 m.equirectangular_(*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(self._formy.euclidean, other, **radius_adjust_wrap) 

338 

339 @property_RO 

340 def _formy(self): 

341 '''(INTERNAL) Get the C{.formy} module, I{once}. 

342 ''' 

343 Geohash._formy = f = _MODS.formy # overwrite property_RO 

344 return f 

345 

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

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

348 the L{pygeodesy.haversine} formula. 

349 

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

351 @kwarg radius_wrap: Optional keyword arguments for function 

352 L{pygeodesy.haversine}. 

353 

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

355 ellipsoid or datum axes). 

356 

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

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

359 ''' 

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

361 

362 @Property_RO 

363 def latlon(self): 

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

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

366 ''' 

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

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

369 

370 @Property_RO 

371 def neighbors(self): 

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

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

374 ''' 

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

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

377 name=self.name) 

378 

379 @Property_RO 

380 def philam(self): 

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

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

383 ''' 

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

385 

386 @Property_RO 

387 def precision(self): 

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

389 ''' 

390 return len(self) 

391 

392 @Property_RO 

393 def sizes(self): 

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

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

396 ''' 

397 z = _GH.Sizes 

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

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

400 

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

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

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

404 

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

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

407 keyword arguments, ignored if 

408 C{B{LatLon} is None}. 

409 

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

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

412 is C{None}. 

413 

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

415 ''' 

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

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

418 

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

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

421 the L{pygeodesy.vincentys} formula. 

422 

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

424 @kwarg radius_wrap: Optional keyword arguments for function 

425 L{pygeodesy.vincentys}. 

426 

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

428 ellipsoid or datum axes). 

429 

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

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

432 ''' 

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

434 

435 @Property_RO 

436 def N(self): 

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

438 ''' 

439 return self.adjacent(_N_) 

440 

441 @Property_RO 

442 def S(self): 

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

444 ''' 

445 return self.adjacent(_S_) 

446 

447 @Property_RO 

448 def E(self): 

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

450 ''' 

451 return self.adjacent(_E_) 

452 

453 @Property_RO 

454 def W(self): 

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

456 ''' 

457 return self.adjacent(_W_) 

458 

459 @Property_RO 

460 def NE(self): 

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

462 ''' 

463 return self.N.E 

464 

465 @Property_RO 

466 def NW(self): 

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

468 ''' 

469 return self.N.W 

470 

471 @Property_RO 

472 def SE(self): 

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

474 ''' 

475 return self.S.E 

476 

477 @Property_RO 

478 def SW(self): 

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

480 ''' 

481 return self.S.W 

482 

483 

484class GeohashError(_ValueError): 

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

486 ''' 

487 pass 

488 

489 

490class Neighbors8Dict(_NamedDict): 

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

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

493 ''' 

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

495 

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

497 kwds = _xkwds(kwds, **_Neighbors8Defaults) 

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

499 

500 

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

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

503 

504 

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

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

507 

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

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

510 or C{None}. 

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

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

513 

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

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

516 lonW, latN, lonE)}. 

517 

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

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

520 B{C{LatLon_kwds}}. 

521 

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

523 ''' 

524 gh = _2Geohash(geohash) 

525 if len(gh) < 1: 

526 raise GeohashError(geohash=geohash) 

527 

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

529 try: 

530 d, _avg = True, favg 

531 for c in gh.lower(): 

532 i = _GH.DecodedBase32[c] 

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

534 if d: # longitude 

535 if i & m: 

536 w = _avg(w, e) 

537 else: 

538 e = _avg(w, e) 

539 else: # latitude 

540 if i & m: 

541 s = _avg(s, n) 

542 else: 

543 n = _avg(s, n) 

544 d = not d 

545 except KeyError: 

546 raise GeohashError(geohash=geohash) 

547 

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

549 name=nameof(geohash)) 

550 

551 

552def _bounds3(geohash): 

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

554 ''' 

555 b = bounds(geohash) 

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

557 

558 

559def decode(geohash): 

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

561 centre of) geohash cell to reasonable precision. 

562 

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

564 

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

566 

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

568 C{LatLon} or C{str}. 

569 

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

571 ''' 

572 b, h, w = _bounds3(geohash) 

573 lat, lon = _2center(b) 

574 

575 # round to near centre without excessive precision to 

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

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

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

579 

580 

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

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

583 of) geohash cell to reasonable precision. 

584 

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

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

587 or C{None}. 

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

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

590 

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

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

593 

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

595 C{LatLon} or C{str}. 

596 

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

598 ''' 

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

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

601 return _xnamed(r, decode2.__name__) 

602 

603 

604def decode_error(geohash): 

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

606 this geohash. 

607 

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

609 

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

611 longitudinal errors in (C{degrees}). 

612 

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

614 C{LatLon} or C{str}. 

615 

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

617 ''' 

618 _, h, w = _bounds3(geohash) 

619 return LatLon2Tuple(h * _0_5, # Height error 

620 w * _0_5) # Width error 

621 

622 

623def distance_(geohash1, geohash2): 

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

625 

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

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

628 

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

630 

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

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

633 ''' 

634 return _2Geohash(geohash1).distanceTo(geohash2) 

635 

636 

637@deprecated_function 

638def distance1(geohash1, geohash2): 

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

640 return distance_(geohash1, geohash2) 

641 

642 

643@deprecated_function 

644def distance2(geohash1, geohash2): 

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

646 return equirectangular_(geohash1, geohash2) 

647 

648 

649@deprecated_function 

650def distance3(geohash1, geohash2): 

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

652 return haversine_(geohash1, geohash2) 

653 

654 

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

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

657 precision or if not provided, to an automatically evaluated 

658 precision. 

659 

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

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

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

663 1..12). 

664 

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

666 

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

668 ''' 

669 lat, lon = _2fll(lat, lon) 

670 

671 if precision is None: 

672 # Infer precision by refining geohash until 

673 # it matches precision of supplied lat/lon. 

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

675 gh = encode(lat, lon, p) 

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

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

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

679 return gh 

680 p = _MaxPrec 

681 else: 

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

683 

684 b = i = 0 

685 d, gh = True, [] 

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

687 

688 _avg = favg 

689 while p > 0: 

690 i += i 

691 if d: # bisect longitude 

692 m = _avg(e, w) 

693 if lon < m: 

694 e = m 

695 else: 

696 w = m 

697 i += 1 

698 else: # bisect latitude 

699 m = _avg(n, s) 

700 if lat < m: 

701 n = m 

702 else: 

703 s = m 

704 i += 1 

705 d = not d 

706 

707 b += 1 

708 if b == 5: 

709 # 5 bits gives a character: 

710 # append it and start over 

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

712 b = i = 0 

713 p -= 1 

714 

715 return NN.join(gh) 

716 

717 

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

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

720 L{pygeodesy.equirectangular} formula. 

721 

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

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

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

725 L{Geohash.equirectangularTo}. 

726 

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

728 see method L{Geohash.equirectangularTo}. 

729 

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

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

732 ''' 

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

734 

735 

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

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

738 L{pygeodesy.euclidean} formula. 

739 

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

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

742 @kwarg radius_adjust_wrap: Optional keyword arguments for function 

743 L{pygeodesy.euclidean}. 

744 

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

746 

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

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

749 ''' 

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

751 

752 

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

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

755 using the L{pygeodesy.haversine} formula. 

756 

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

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

759 @kwarg radius_wrap: Optional keyword arguments for function 

760 L{pygeodesy.haversine}. 

761 

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

763 B{C{radius}}). 

764 

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

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

767 ''' 

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

769 

770 

771def neighbors(geohash): 

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

773 

774 @arg geohash: Cell for which neighbors are requested 

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

776 

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

778 of L{Geohash}es. 

779 

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

781 C{LatLon} or C{str}. 

782 ''' 

783 return _2Geohash(geohash).neighbors 

784 

785 

786def precision(res1, res2=None): 

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

788 (geographic) resolutions. 

789 

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

791 (C{degrees}). 

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

793 resolution (C{degrees}). 

794 

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

796 

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

798 

799 @see: C++ class U{Geohash 

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

801 ''' 

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

803 if res2 is None: 

804 t = r, r 

805 for p in range(1, _MaxPrec): 

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

807 return p 

808 

809 else: 

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

811 for p in range(1, _MaxPrec): 

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

813 return p 

814 

815 return _MaxPrec 

816 

817 

818class Resolutions2Tuple(_NamedTuple): 

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

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

821 ''' 

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

823 _Units_ = ( Degrees_, Degrees_) 

824 

825 

826def resolution2(prec1, prec2=None): 

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

828 precisions. 

829 

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

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

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

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

834 

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

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

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

838 

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

840 

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

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

843 ''' 

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

845 

846 if prec1: 

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

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

849 

850 if prec2: 

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

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

853 

854 return Resolutions2Tuple(res1, res2) 

855 

856 

857def sizes(geohash): 

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

859 

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

861 C{str}). 

862 

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

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

865 

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

867 C{LatLon} or C{str}. 

868 ''' 

869 return _2Geohash(geohash).sizes 

870 

871 

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

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

874 L{pygeodesy.vincentys} formula. 

875 

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

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

878 @kwarg radius_wrap: Optional keyword arguments for function 

879 L{pygeodesy.vincentys}. 

880 

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

882 

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

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

885 ''' 

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

887 

888 

889__all__ += _ALL_OTHER(bounds, # functions 

890 decode, decode2, decode_error, distance_, 

891 encode, equirectangular_, euclidean_, haversine_, 

892 neighbors, precision, resolution2, sizes, vincentys_) 

893 

894# **) MIT License 

895# 

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

897# 

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

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

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

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

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

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

904# 

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

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

907# 

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

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

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

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

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

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

914# OTHER DEALINGS IN THE SOFTWARE.