Coverage for pygeodesy/geohash.py: 97%

1

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

3

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

5

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

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

8

9 Originally transcoded from JavaScript originals by I{(C) Chris Veness

10 2011-2024} and published under the same MIT Licence**, see

11 U{Geohashes<https://www.Movable-Type.co.UK/scripts/geohash.html>}.

12

13@see: U{Geohash<https://WikiPedia.org/wiki/Geohash>}, I{Karney}'s C++

14 U{Geohash<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>},

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

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

17 U{geohash-js<https://GitHub.com/DaveTroy/geohash-js>}.

18'''

19

20 from pygeodesy.basics import isstr, map2, _splituple

21 from pygeodesy.constants import EPS, R_M, _0_0, _0_5, _180_0, _360_0, \

22 _90_0, _N_90_0, _N_180_0 # PYCHOK used!

23 from pygeodesy.errors import _ValueError, _xkwds, _xStrError

24# from pygeodesy import formy as _formy # _MODS.into

25 from pygeodesy.interns import NN, _DMAIN_, _DOT_, _E_, _height_, _N_, _NE_, \

26 _NW_, _radius_, _S_, _SE_, _SW_, _W_, _width_ # _INV_

27 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS

28 from pygeodesy.named import _name__, _NamedDict, _NamedTuple, nameof, _xnamed

29 from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, LatLon2Tuple, \

30 PhiLam2Tuple

31 from pygeodesy.props import deprecated_function, deprecated_method, \

32 deprecated_property_RO, Property_RO, \

33 property_RO, property_ROver

34# from pygeodesy.streprs import Fmt, fstr # _MODS

35 from pygeodesy.units import Degrees_, Int, Lat_, Lon_, Meter, Precision_, Str

36

37 from math import fabs, ldexp, log10, radians

38

39 __all__ = _ALL_LAZY.geohash

40 __version__ = '25.09.16'

41

42 _formy = _MODS.into(formy=__name__)

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

44 _MaxPrec = 12

45

46

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

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

49 '''

50 if LatLon is None:

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

52 else:

53 kwds = _xkwds(LatLon_kwds, **name)

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

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

56 return r

57

58

59 def _2center(bounds):

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

61 '''

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

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

64

65

66 def _2dab(d, a, b):

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

68 '''

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

70

71

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

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

74 '''

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

76 return (Lat_(lat, Error=GeohashError),

77 Lon_(lon, Error=GeohashError))

78

79

80 def _2Geohash(geohash):

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

82 '''

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

84 Geohash(geohash)

85

86

87 def _2latlon(s, w, n, e, fstr=None):

88 '''(INTERNAL) Get the center C{lat, lon}, rounded.

89 '''

90 lat, a = _2mid_ndigits(n, s)

91 lon, b = _2mid_ndigits(e, w)

92 return (fstr(lat, prec=a), fstr(lon, prec=b)) if fstr else \

93 (round(lat, a), round(lon, b))

94

95

96 def _2mid(a, b):

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

98 '''

99 return (a + b) * _0_5 # favg

100

101

102 def _2mid_ndigits(a, b): # a > b

103 '''(INTERNAL) Return 2-tuple C{(_2mid, ndigits)}.

104 '''

105 # round to near centre without excessive

106 # precision to ⌊2-log10(Δ°)⌋ ndigits

107 return _2mid(a, b), int(2 - log10(a - b))

108

109

110 def _2Precision(p):

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

112 '''

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

114

115

116 def _2res(res, **prec):

117 '''(INTERNAL) Get the C{res}olution for a C{prec}ision.

118 '''

119 p = max(min(Int(Error=GeohashError, **prec), _MaxPrec), 0) * 5

120 x = (p - p // 2) if res > _180_0 else (p // 2)

121 return ldexp(res, -x) if x else res # ldexp == res / float(1 << x)

122

123

124 class _GH(object):

125 '''(INTERNAL) Lazily defined constants.

126 '''

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

128 return dict(S=(s, w), W=(w, s),

129 N=(n, e), E=(e, n))

130

131 @property_ROver

132 def Borders(self):

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

134

135 @property_ROver

136 def DecodeB32(self): # inverse EncodeB32 map

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

138

139 def decode2(self, geohash):

140 '''Decode C{geohash} to 2-tuple C{(lat, lon)}.

141 '''

142 swne = self.swne4(geohash)

143 return _2latlon(*swne)

144

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

146 EncodeB32 = '0123456789bcdefghjkmnpqrstuvwxyz'

147

148 def encode(self, *lat_lon_prec_eps):

149 '''Encode C{lat, lon} to C{prec}ision or C{eps}.

150 '''

151 def _encodes(lat, lon, prec, eps=0):

152 s, w, n, e = self.SWNE4

153 d, _mid = True, _2mid

154 for _ in range(prec):

155 i = 0

156 for _ in _MASK5:

157 i += i

158 if d: # bisect longitude

159 a = _mid(e, w)

160 if lon < a:

161 e = a

162 else:

163 w = a

164 i += 1

165 else: # bisect latitude

166 a = _mid(n, s)

167 if lat < a:

168 n = a

169 else:

170 s = a

171 i += 1

172 d = not d

173 yield self.EncodeB32[i]

174 if eps > 0: # infer prec

175 if _2dab(lon, e, w) < eps and \

176 _2dab(lat, n, s) < eps:

177 break

178

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

180

181 def encode2(self, lat, lon, prec, eps):

182 '''Return 2-tuple C{geohash, (lat, lon))}.

183 '''

184 lat, lon = _2fll(lat, lon)

185 if prec:

186 p, e = _2Precision(prec), 0

187 else: # infer precision by refining geohash

188 p, e = _MaxPrec, max(eps, EPS)

189 return self.encode(lat, lon, p, e), (lat, lon)

190

191 @property_ROver

192 def _LatLon2Tuple(self):

193

194 class _LatLon2Tuple(_NamedTuple):

195 '''DEPRECATED on 2024.07.28, C{(lat, lon)} in B{C{meter}}, use L{Sizes3Tuple}.'''

196 _Names_ = LatLon2Tuple._Names_

197 _Units_ = Meter, Meter

198

199 return _LatLon2Tuple

200

201 @property_ROver

202 def Neighbors(self):

203 return self._4d('14365h7k9dcfesgujnmqp0r2twvyx8zb',

204 '238967debc01fg45kmstqrwxuvhjyznp',

205 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',

206 'bc01fg45238967deuvhjyznpkmstqrwx')

207

208 @property_ROver

209 def Sizes(self): # height, width and radius (in meter)

210 # where radius = sqrt(height * width / PI), the

211 # radius of a circle with area (height * width)

212 T = Sizes3Tuple

213 return (T(20000e3, 20032e3, 11292815.096), # 0

214 T( 5000e3, 5003e3, 2821794.075), # 1

215 T( 650e3, 1225e3, 503442.397), # 2

216 T( 156e3, 156e3, 88013.575), # 3

217 T( 19500, 39100, 15578.683), # 4

218 T( 4890, 4890, 2758.887), # 5

219 T( 610, 1220, 486.710), # 6

220 T( 153, 153, 86.321), # 7

221 T( 19.1, 38.2, 15.239), # 8

222 T( 4.77, 4.77, 2.691), # 9

223 T( 0.596, 1.19, 0.475), # 10

224 T( 0.149, 0.149, 0.084), # 11

225 T( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec

226

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

228

229 def swne4(self, geohash, mask5=_MASK5):

230 '''Decode C{geohash} into 4-tuple C{(s, w, n, e)}.

231 '''

232 nc = len(geohash) if isstr(geohash) else 0

233 if not (0 < nc <= _MaxPrec): # or geohash.startswith(_INV_)

234 raise GeohashError(geohash=geohash, len=nc)

235 s, w, n, e = self.SWNE4

236 d, _mid = True, _2mid

237 try:

238 for j, c in enumerate(geohash.lower()):

239 i = self.DecodeB32[c]

240 for m in mask5:

241 if d: # longitude

242 a = _mid(e, w)

243 if (i & m):

244 w = a

245 else:

246 e = a

247 else: # latitude

248 a = _mid(n, s)

249 if (i & m):

250 s = a

251 else:

252 n = a

253 d = not d

254 except KeyError:

255 c = _MODS.streprs.Fmt.INDEX(repr(c), j)

256 raise GeohashError(geohash=geohash, len=nc, txt=c)

257 return s, w, n, e

258

259 _GH = _GH() # PYCHOK singleton

260

261

262 class Geohash(Str):

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

264 '''

265 # no str.__init__ in Python 3

266 def __new__(cls, lat_ghll, lon=None, precision=None, eps=EPS, **name):

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

268 or from a lat- and longitude.

269

270 @arg lat_ghll: Latitude (C{degrees90}), a geohash (L{Geohash},

271 C{str}) or a location (C{LatLon}, C{LatLon*Tuple}).

272 @kwarg lon: Logitude (C{degrees180)}, required if B{C{lat_ghll}}

273 is C{degrees90}, ignored otherwise.

274 @kwarg precision: The desired geohash length (C{int} 1..12) or

275 C{None} or C{0}, see L{encode<pygeodesy.geohash.encode>}.

276 @kwarg eps: Optional inference tolerance (C{degrees}), see

277 L{encode<pygeodesy.geohash.encode>}.

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

279

280 @return: New L{Geohash}.

281

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

283

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

285

286 @raise TypeError: Invalid B{C{lat_ghll}}.

287 '''

288 if lon is None:

289 if isinstance(lat_ghll, Geohash):

290 gh, ll = str(lat_ghll), lat_ghll.latlon

291 elif isstr(lat_ghll): # "lat, lon" or "geohash"

292 ll = _splituple(lat_ghll)

293 if len(ll) > 1:

294 gh, ll = _GH.encode2(ll[0], ll[1], precision, eps)

295 else:

296 gh, ll = lat_ghll.lower(), None

297 _ = _GH.swne4(gh, mask5=()) # validate

298 else: # assume LatLon

299 try:

300 gh, ll = _GH.encode2(lat_ghll.lat, lat_ghll.lon, precision, eps)

301 except AttributeError:

302 raise _xStrError(Geohash, ghll=lat_ghll, Error=GeohashError)

303 else:

304 gh, ll = _GH.encode2(lat_ghll, lon, precision, eps)

305

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

307 self._latlon = ll

308 return self

309

310 @deprecated_property_RO

311 def ab(self):

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

313 return self.philam

314

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

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

317

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

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

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

321

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

323

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

325 '''

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

327

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

329 if D not in _GH.Neighbors:

330 raise GeohashError(direction=direction)

331

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

333

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

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

336 if i < 0:

337 raise GeohashError(geohash=self)

338

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

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

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

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

343

344 n = self._name__(name)

345 if n:

346 n = _DOT_(n, D)

347 # append letter for direction to parent

348 return Geohash(p + _GH.EncodeB32[i], name=n)

349

350 @Property_RO

351 def _bounds(self):

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

353 '''

354 return bounds(self)

355

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

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

358 geohash cell.

359

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

361 or C{None}.

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

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

364

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

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

367 C{B{LatLon} is None},

368 '''

369 r = self._bounds

370 return r if LatLon is None else \

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

372

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

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

375 '''

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

377 return func_(*lls, **kwds)

378

379 def distanceTo(self, other):

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

381 based the cell sizes.

382

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

384

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

386

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

388 C{LatLon} or C{str}.

389 '''

390 other = _2Geohash(other)

391

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

393 if n:

394 for n in range(n):

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

396 break

397 return _GH.Sizes[n].radius

398

399 @deprecated_method

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

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

402 return self.distanceTo(other)

403

404 distance1 = distance1To

405

406 @deprecated_method

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

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

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

410

411 distance2 = distance2To

412

413 @deprecated_method

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

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

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

417

418 distance3 = distance3To

419

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

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

422 using function L{pygeodesy.equirectangular}.

423

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

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

426 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})

427 or C{None}, see function L{pygeodesy.equirectangular}.

428 @kwarg adjust_limit_wrap: Optional keyword arguments for function

429 L{pygeodesy.equirectangular4}, overriding defaults

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

431

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

433 or datum axes or C{radians I{squared}} if B{C{radius} is None}

434 or C{0}).

435

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

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

438

439 @see: U{Local, flat earth approximation

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

441 '''

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

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

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

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

446

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

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

449 function L{pygeodesy.euclidean}.

450

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

452 @kwarg radius_adjust_wrap: Optional keyword arguments for function

453 L{pygeodesy.euclidean}.

454

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

456 ellipsoid or datum axes).

457

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

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

460 '''

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

462

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

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

465 the L{pygeodesy.haversine} formula.

466

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

468 @kwarg radius_wrap: Optional keyword arguments for function

469 L{pygeodesy.haversine}.

470

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

472 ellipsoid or datum axes).

473

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

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

476 '''

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

478

479 @Property_RO

480 def latlon(self):

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

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

483 '''

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

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

486

487 @Property_RO

488 def neighbors(self):

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

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

491 '''

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

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

494 name=self.name)

495

496 @Property_RO

497 def philam(self):

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

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

500 '''

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

502

503 @Property_RO

504 def precision(self):

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

506 '''

507 return len(self)

508

509 @Property_RO

510 def resolution2(self):

511 '''Get the I{lon-} and I{latitudinal} resolution of this cell

512 in a L{Resolutions2Tuple}C{(res1, res2)}, both in C{degrees}.

513 '''

514 return resolution2(self.precision, self.precision)

515

516 @deprecated_property_RO

517 def sizes(self):

518 '''DEPRECATED on 2024.07.28, use property C{Geohash.sizes3}.'''

519 t = self.sizes3

520 return _GH._LatLon2Tuple(t.height, t.width, name=t.name)

521

522 @Property_RO

523 def sizes3(self):

524 '''Get the lat-, longitudinal and radial size of this cell in

525 a L{Sizes3Tuple}C{(height, width, radius)}, all in C{meter}.

526 '''

527 z = _GH.Sizes

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

529 return Sizes3Tuple(z[n], name=self.name)

530

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

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

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

534

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

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

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

538

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

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

541

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

543 '''

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

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

546

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

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

549 the L{pygeodesy.vincentys} formula.

550

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

552 @kwarg radius_wrap: Optional keyword arguments for function

553 L{pygeodesy.vincentys}.

554

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

556 ellipsoid or datum axes).

557

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

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

560 '''

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

562

563 @Property_RO

564 def E(self):

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

566 '''

567 return self.adjacent(_E_)

568

569 @Property_RO

570 def N(self):

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

572 '''

573 return self.adjacent(_N_)

574

575 @Property_RO

576 def NE(self):

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

578 '''

579 return self.N.E

580

581 @Property_RO

582 def NW(self):

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

584 '''

585 return self.N.W

586

587 @Property_RO

588 def S(self):

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

590 '''

591 return self.adjacent(_S_)

592

593 @Property_RO

594 def SE(self):

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

596 '''

597 return self.S.E

598

599 @Property_RO

600 def SW(self):

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

602 '''

603 return self.S.W

604

605 @Property_RO

606 def W(self):

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

608 '''

609 return self.adjacent(_W_)

610

611

612 class Geohashed(object):

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

614 '''

615 _nn = None, # 1-tuple

616

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

618 '''New L{Geohashed} cache.

619

620 @arg precision: The geohash encoded length (C{int}, 1..12).

621 @kwarg ndigits: Optional number of digits to round C{lat}

622 and C{lon} to cache keys (C{int}, typically

623 C{B{ndigits}=B{precision}}) or C{None} for

624 no rounding.

625 '''

626 self._p = _2Precision(precision)

627 if ndigits is None:

628 self._ab2 = self._ab2float

629 else:

630 self._ab2 = self._ab2round

631 n = Int(ndigits=ndigits)

632 self._nn = n, n

633 self.clear()

634

635 def __len__(self):

636 '''Return the number of I{unigue} geohashes (C{int}).

637 '''

638 d = self._d

639 d = set(d.keys())

640 n = len(d)

641 for e in self._e.values():

642 e = set(e.values())

643 n += len(e - d)

644 return n

645

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

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

648 '''

649 return ll

650

651 def _ab2float(self, *ll):

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

653 '''

654 return map(float, ll)

655

656 def _ab2round(self, *ll):

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

658 '''

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

660

661 def clear(self):

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

663 '''

664 self._e = {}

665 self._d = {}

666

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

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

669

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

671

672 @return: The C{(lat, lon}) pair for C{geohash}.

673 '''

674 try:

675 ll = self._d[geohash]

676 except KeyError:

677 self._d[geohash] = ll = _GH.decode2(geohash)

678 if encoded:

679 a, b = self._ab2(*ll)

680 try:

681 _ = self._e[b][a]

682 except KeyError:

683 self._e.setdefault(b, {})[a] = geohash

684 return ll

685

686 def encoded(self, lat, lon, decoded=False):

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

688

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

690

691 @return: The C{geohash} for pair C{(lat, lon}).

692 '''

693 lat, lon = ll = _2fll(lat, lon)

694 a, b = self._ab2(*ll)

695 try:

696 gh = self._e[b][a]

697 except KeyError:

698 gh = _GH.encode(lat, lon, self._p, 0)

699 self._e.setdefault(b, {})[a] = gh

700 if decoded and gh not in self._d:

701 self._d[gh] = ll

702 return gh

703

704 @property_RO

705 def len2(self):

706 '''Return 2-tuple C{(lencoded, ldecoded)} with the C{len}gths of the

707 C{en-} and C{decoded} cache.

708 '''

709 return sum(len(e) for e in self._e.values()), len(self._d)

710

711 @Property_RO

712 def ndigits(self):

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

714 '''

715 return self._nn[0]

716

717 @Property_RO

718 def precision(self):

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

720 '''

721 return self._p

722

723

724 class GeohashError(_ValueError):

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

726 '''

727 pass

728

729

730 class Neighbors8Dict(_NamedDict):

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

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

733 '''

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

735

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

737 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

739

740

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

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

743

744

745 class Resolutions2Tuple(_NamedTuple):

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

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

748 '''

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

750 _Units_ = ( Degrees_, Degrees_)

751

752 @property_RO

753 def lat(self):

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

755 '''

756 return self[1]

757

758 @property_RO

759 def lon(self):

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

761 '''

762 return self[0]

763

764

765 class Sizes3Tuple(_NamedTuple):

766 '''3-Tuple C{(height, width, radius)} with latitudinal C{height},

767 longitudinal C{width} and area C{radius}, all in C{meter}.

768 '''

769 _Names_ = (_height_, _width_, _radius_)

770 _Units_ = ( Meter, Meter, Meter)

771

772

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

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

775

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

777 @kwarg LatLon: Optional class to return the bounds (C{LatLon}) or C{None}.

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

779 ignored if C{B{LatLon} is None}.

780

781 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)}, each a B{C{LatLon}}

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

783 latN, lonE)}.

784

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

786 C{str} or invalid B{C{LatLon}} or invalid B{C{LatLon_kwds}}.

787

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

789 '''

790 swne = _GH.swne4(geohash)

791 return _2bounds(LatLon, LatLon_kwds, *swne,

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

793

794

795 def decode(geohash):

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

797 centre of) geohash cell to reasonable precision.

798

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

800

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

802

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

804 C{LatLon} or C{str}.

805

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

807 '''

808 # round to near centre without excessive precision to

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

810 swne = _GH.swne4(geohash)

811 return _2latlon(*swne, fstr=_MODS.streprs.fstr)

812

813

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

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

816 of) geohash cell to reasonable precision.

817

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

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

820 or C{None}.

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

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

823

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

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

826

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

828 C{LatLon} or C{str}.

829

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

831 '''

832 ll = _GH.decode2(geohash)

833 r = LatLon2Tuple(ll) if LatLon is None else \

834 LatLon( *ll, **LatLon_kwds)

835 return _xnamed(r, name__=decode2)

836

837

838@deprecated_function

839 def decode_error(geohash):

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

841 return decode_error2(geohash)

842

843

844 def decode_error2(geohash):

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

846

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

848

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

850 longitudinal errors in (C{degrees}).

851

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

853 C{LatLon} or C{str}.

854

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

856 '''

857 s, w, n, e = _GH.swne4(geohash)

858 return LatLon2Tuple((n - s) * _0_5, # lat error

859 (e - w) * _0_5) # lon error

860

861

862 def distance_(geohash1, geohash2):

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

864

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

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

867

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

869

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

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

872 '''

873 return _2Geohash(geohash1).distanceTo(geohash2)

874

875

876@deprecated_function

877 def distance1(geohash1, geohash2):

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

879 return distance_(geohash1, geohash2)

880

881

882@deprecated_function

883 def distance2(geohash1, geohash2):

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

885 return equirectangular4(geohash1, geohash2)

886

887

888@deprecated_function

889 def distance3(geohash1, geohash2):

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

891 return haversine_(geohash1, geohash2)

892

893

894 def encode(lat, lon, precision=None, eps=EPS):

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

896 precision or if not provided, to an inferred precision.

897

898 @arg lat: Latitude (C{degrees90}).

899 @arg lon: Longitude (C{degrees180}).

900 @kwarg precision: The desired geohash length (C{int} 1..12) or

901 C{None} or C{0} for inferred.

902 @kwarg eps: Optional inference tolerance (C{degrees}), ignored

903 if B{C{precision}} is not C{None} or C{0}.

904

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

906

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

908 '''

909 gh, _ = _GH.encode2(lat, lon, precision, eps)

910 return gh

911

912

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

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

915 L{pygeodesy.equirectangular} formula.

916

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

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

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

920 L{Geohash.equirectangularTo}.

921

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

923 see method L{Geohash.equirectangularTo}.

924

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

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

927 '''

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

929

930

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

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

933 L{pygeodesy.euclidean} formula.

934

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

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

937 @kwarg radius_adjust_wrap: Optional keyword arguments for function

938 L{pygeodesy.euclidean}.

939

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

941

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

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

944 '''

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

946

947

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

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

950 using the L{pygeodesy.haversine} formula.

951

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

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

954 @kwarg radius_wrap: Optional keyword arguments for function

955 L{pygeodesy.haversine}.

956

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

958 B{C{radius}}).

959

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

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

962 '''

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

964

965

966 def neighbors(geohash):

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

968

969 @arg geohash: Cell for which neighbors are requested

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

971

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

973 of L{Geohash}es.

974

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

976 C{LatLon} or C{str}.

977 '''

978 return _2Geohash(geohash).neighbors

979

980

981 def precision(res1, res2=None):

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

983 (geographic) resolutions.

984

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

986 (C{degrees}).

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

988 resolution (C{degrees}).

989

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

991

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

993

994 @see: C++ class U{Geohash

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

996 '''

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

998 N = res2 is None

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

1000 for p in range(1, _MaxPrec):

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

1002 return p

1003 return _MaxPrec

1004

1005

1006 def resolution2(prec1, prec2=None):

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

1008 precisions.

1009

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

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

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

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

1014

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

1016 (geographic) resolutions in C{degrees}, where

1017 C{res2 B{is} res1} if no B{C{prec2}} is given.

1018

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

1020

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

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

1023 '''

1024 lon = _2res(_360_0, prec1=prec1)

1025 lat = lon if prec2 is None else \

1026 _2res(_180_0, prec2=prec2)

1027 return Resolutions2Tuple(lon, lat)

1028

1029

1030@deprecated_function

1031 def sizes(geohash):

1032 '''DEPRECATED on 2024.07.28, use function L{pygeodesy.geohash.sizes3}.'''

1033 t = sizes3(geohash)

1034 return _GH._LatLon2Tuple(t.height, t.width, name=t.name)

1035

1036

1037 def sizes3(geohash):

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

1039

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

1041

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

1043

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

1045 '''

1046 return _2Geohash(geohash).sizes3

1047

1048

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

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

1051 L{pygeodesy.vincentys} formula.

1052

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

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

1055 @kwarg radius_wrap: Optional keyword arguments for function

1056 L{pygeodesy.vincentys}.

1057

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

1059

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

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

1062 '''

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

1064

1065

1066 __all__ += _ALL_DOCS(bounds, # functions

1067 decode, decode2, decode_error2, distance_,

1068 encode, equirectangular4, euclidean_, haversine_,

1069 neighbors, precision, resolution2, sizes3, vincentys_,

1070 decode_error, sizes) # DEPRECATED

1071

1072 if __name__ == _DMAIN_:

1073

1074 from pygeodesy.internals import printf, _versions

1075 from timeit import timeit

1076

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

1078

1079 def _t(prec=p):

1080 i = 0

1081 for lat in range(-90, 90, 3):

1082 for lon in range(-180, 180, 7):

1083 _ = encode(lat, lon, prec)

1084 i += 1

1085 return i

1086

1087 i = _t() # prime

1088 n = 10

1089 t = timeit(_t, number=n) / (i * n)

1090 printf('%s %.3f usec, %s', f, t * 1e6, _versions())

1091

1092# % python3.12 -m pygeodesy.geohash

1093# encode 10.145 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5

1094# infer 14.780 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5

1095# or about 6.56 and 74.12 times faster than pygeodesy 24.7.24 and older:

1096# encode 66.524 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5

1097# infer 1095.386 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5

1098

1099# **) MIT License

1100#

1101# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1102#

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

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

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

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

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

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

1109#

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

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

1112#

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

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

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

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

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

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

1119# OTHER DEALINGS IN THE SOFTWARE.