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

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

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

26 _NW_, _radius_, _S_, _SE_, _SPACE_, _SW_, _W_, \

27 _width_ # _INV_

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

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

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

31 PhiLam2Tuple

32 from pygeodesy.props import deprecated_function, deprecated_method, \

33 deprecated_property_RO, Property_RO, \

34 property_RO, property_ROver

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

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

37

38 from math import fabs, ldexp, log10, radians

39

40__all__ = _ALL_LAZY.geohash

41__version__ = '24.10.12'

42

43_formy = _MODS.into(formy=__name__)

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

45_MaxPrec = 12

46

47

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

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

50 '''

51 if LatLon is None:

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

53 else:

54 kwds = _xkwds(LatLon_kwds, **name)

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

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

57 return r

58

59

60 def _2center(bounds):

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

62 '''

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

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

65

66

67 def _2dab(d, a, b):

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

69 '''

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

71

72

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

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

75 '''

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

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

78 Lon_(lon, Error=GeohashError))

79

80

81 def _2Geohash(geohash):

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

83 '''

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

85 Geohash(geohash)

86

87

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

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

90 '''

91 lat, a = _2mid_ndigits(n, s)

92 lon, b = _2mid_ndigits(e, w)

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

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

95

96

97 def _2mid(a, b):

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

99 '''

100 return (a + b) * _0_5 # favg

101

102

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

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

105 '''

106 # round to near centre without excessive

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

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

109

110

111 def _2Precision(p):

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

113 '''

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

115

116

117 def _2res(res, **prec):

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

119 '''

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

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

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

123

124

125 class _GH(object):

126 '''(INTERNAL) Lazily defined constants.

127 '''

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

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

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

131

132 @property_ROver

133 def Borders(self):

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

135

136 @property_ROver

137 def DecodeB32(self): # inverse EncodeB32 map

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

139

140 def decode2(self, geohash):

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

142 '''

143 swne = self.swne4(geohash)

144 return _2latlon(*swne)

145

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

147 EncodeB32 = '0123456789bcdefghjkmnpqrstuvwxyz'

148

149 def encode(self, *lat_lon_prec_eps):

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

151 '''

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

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

154 E, d, _mid = self.EncodeB32, True, _2mid

155 for _ in range(prec):

156 i = 0

157 for _ in range(5): # len(_MASK5)

158 i += i

159 if d: # bisect longitude

160 a = _mid(e, w)

161 if lon < a:

162 e = a

163 else:

164 w = a

165 i += 1

166 else: # bisect latitude

167 a = _mid(n, s)

168 if lat < a:

169 n = a

170 else:

171 s = a

172 i += 1

173 d = not d

174 yield E[i]

175 if eps > 0: # infer prec

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

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

178 break

179

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

181

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

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

184 '''

185 lat, lon = _2fll(lat, lon)

186 if prec:

187 p, e = _2Precision(prec), 0

188 else: # infer precision by refining geohash

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

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

191

192 @property_ROver

193 def _LatLon2Tuple(self):

194

195 class _LatLon2Tuple(_NamedTuple):

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

197 _Names_ = LatLon2Tuple._Names_

198 _Units_ = Meter, Meter

199

200 return _LatLon2Tuple

201

202 @property_ROver

203 def Neighbors(self):

204 return self._4d('14365h7k9dcfesgujnmqp0r2twvyx8zb',

205 '238967debc01fg45kmstqrwxuvhjyznp',

206 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',

207 'bc01fg45238967deuvhjyznpkmstqrwx')

208

209 @property_ROver

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

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

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

213 T = Sizes3Tuple

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

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

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

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

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

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

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

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

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

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

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

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

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

227

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

229

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

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

232 '''

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

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

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

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

237 D, d, _mid = self.DecodeB32, True, _2mid

238 try:

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

240 i = D[c]

241 for m in mask5:

242 if d: # longitude

243 a = _mid(e, w)

244 if (i & m):

245 w = a

246 else:

247 e = a

248 else: # latitude

249 a = _mid(n, s)

250 if (i & m):

251 s = a

252 else:

253 n = a

254 d = not d

255 except KeyError:

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

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

258 return s, w, n, e

259

260_GH = _GH() # PYCHOK singleton

261

262

263 class Geohash(Str):

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

265 '''

266 # no str.__init__ in Python 3

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

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

269 or from a lat- and longitude.

270

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

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

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

274 is C{degrees90}, ignored otherwise.

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

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

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

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

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

280

281 @return: New L{Geohash}.

282

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

284

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

286

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

288 '''

289 if lon is None:

290 if isinstance(lat_ghll, Geohash):

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

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

293 ll = lat_ghll.replace(_COMMA_, _SPACE_).split()

294 if len(ll) > 1:

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

296 else:

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

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

299 else: # assume LatLon

300 try:

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

302 except AttributeError:

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

304 else:

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

306

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

308 self._latlon = ll

309 return self

310

311 @deprecated_property_RO

312 def ab(self):

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

314 return self.philam

315

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

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

318

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

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

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

322

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

324

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

326 '''

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

328

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

330 if D not in _GH.Neighbors:

331 raise GeohashError(direction=direction)

332

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

334

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

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

337 if i < 0:

338 raise GeohashError(geohash=self)

339

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

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

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

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

344

345 n = self._name__(name)

346 if n:

347 n = _DOT_(n, D)

348 # append letter for direction to parent

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

350

351 @Property_RO

352 def _bounds(self):

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

354 '''

355 return bounds(self)

356

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

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

359 geohash cell.

360

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

362 or C{None}.

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

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

365

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

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

368 C{B{LatLon} is None},

369 '''

370 r = self._bounds

371 return r if LatLon is None else \

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

373

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

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

376 '''

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

378 return func_(*lls, **kwds)

379

380 def distanceTo(self, other):

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

382 based the cell sizes.

383

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

385

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

387

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

389 C{LatLon} or C{str}.

390 '''

391 other = _2Geohash(other)

392

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

394 if n:

395 for n in range(n):

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

397 break

398 return _GH.Sizes[n].radius

399

400 @deprecated_method

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

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

403 return self.distanceTo(other)

404

405 distance1 = distance1To

406

407 @deprecated_method

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

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

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

411

412 distance2 = distance2To

413

414 @deprecated_method

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

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

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

418

419 distance3 = distance3To

420

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

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

423 using function L{pygeodesy.equirectangular}.

424

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

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

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

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

429 @kwarg adjust_limit_wrap: Optional keyword arguments for function

430 L{pygeodesy.equirectangular4}, overriding defaults

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

432

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

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

435 or C{0}).

436

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

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

439

440 @see: U{Local, flat earth approximation

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

442 '''

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

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

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

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

447

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

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

450 function L{pygeodesy.euclidean}.

451

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

453 @kwarg radius_adjust_wrap: Optional keyword arguments for function

454 L{pygeodesy.euclidean}.

455

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

457 ellipsoid or datum axes).

458

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

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

461 '''

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

463

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

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

466 the L{pygeodesy.haversine} formula.

467

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

469 @kwarg radius_wrap: Optional keyword arguments for function

470 L{pygeodesy.haversine}.

471

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

473 ellipsoid or datum axes).

474

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

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

477 '''

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

479

480 @Property_RO

481 def latlon(self):

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

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

484 '''

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

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

487

488 @Property_RO

489 def neighbors(self):

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

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

492 '''

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

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

495 name=self.name)

496

497 @Property_RO

498 def philam(self):

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

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

501 '''

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

503

504 @Property_RO

505 def precision(self):

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

507 '''

508 return len(self)

509

510 @Property_RO

511 def resolution2(self):

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

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

514 '''

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

516

517 @deprecated_property_RO

518 def sizes(self):

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

520 t = self.sizes3

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

522

523 @Property_RO

524 def sizes3(self):

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

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

527 '''

528 z = _GH.Sizes

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

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

531

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

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

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

535

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

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

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

539

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

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

542

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

544 '''

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

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

547

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

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

550 the L{pygeodesy.vincentys} formula.

551

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

553 @kwarg radius_wrap: Optional keyword arguments for function

554 L{pygeodesy.vincentys}.

555

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

557 ellipsoid or datum axes).

558

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

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

561 '''

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

563

564 @Property_RO

565 def E(self):

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

567 '''

568 return self.adjacent(_E_)

569

570 @Property_RO

571 def N(self):

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

573 '''

574 return self.adjacent(_N_)

575

576 @Property_RO

577 def NE(self):

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

579 '''

580 return self.N.E

581

582 @Property_RO

583 def NW(self):

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

585 '''

586 return self.N.W

587

588 @Property_RO

589 def S(self):

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

591 '''

592 return self.adjacent(_S_)

593

594 @Property_RO

595 def SE(self):

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

597 '''

598 return self.S.E

599

600 @Property_RO

601 def SW(self):

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

603 '''

604 return self.S.W

605

606 @Property_RO

607 def W(self):

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

609 '''

610 return self.adjacent(_W_)

611

612

613 class Geohashed(object):

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

615 '''

616 _nn = None, # 1-tuple

617

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

619 '''New L{Geohashed} cache.

620

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

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

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

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

625 no rounding.

626 '''

627 self._p = _2Precision(precision)

628 if ndigits is None:

629 self._ab2 = self._ab2float

630 else:

631 self._ab2 = self._ab2round

632 n = Int(ndigits=ndigits)

633 self._nn = n, n

634 self.clear()

635

636 def __len__(self):

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

638 '''

639 d = self._d

640 d = set(d.keys())

641 n = len(d)

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

643 e = set(e.values())

644 n += len(e - d)

645 return n

646

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

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

649 '''

650 return ll

651

652 def _ab2float(self, *ll):

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

654 '''

655 return map(float, ll)

656

657 def _ab2round(self, *ll):

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

659 '''

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

661

662 def clear(self):

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

664 '''

665 self._e = {}

666 self._d = {}

667

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

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

670

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

672

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

674 '''

675 try:

676 ll = self._d[geohash]

677 except KeyError:

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

679 if encoded:

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

681 try:

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

683 except KeyError:

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

685 return ll

686

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

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

689

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

691

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

693 '''

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

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

696 try:

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

698 except KeyError:

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

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

701 if decoded and gh not in self._d:

702 self._d[gh] = ll

703 return gh

704

705 @property_RO

706 def len2(self):

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

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

709 '''

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

711

712 @Property_RO

713 def ndigits(self):

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

715 '''

716 return self._nn[0]

717

718 @Property_RO

719 def precision(self):

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

721 '''

722 return self._p

723

724

725 class GeohashError(_ValueError):

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

727 '''

728 pass

729

730

731 class Neighbors8Dict(_NamedDict):

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

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

734 '''

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

736

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

738 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

740

741

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

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

744

745

746 class Resolutions2Tuple(_NamedTuple):

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

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

749 '''

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

751 _Units_ = ( Degrees_, Degrees_)

752

753 @property_RO

754 def lat(self):

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

756 '''

757 return self[1]

758

759 @property_RO

760 def lon(self):

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

762 '''

763 return self[0]

764

765

766 class Sizes3Tuple(_NamedTuple):

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

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

769 '''

770 _Names_ = (_height_, _width_, _radius_)

771 _Units_ = ( Meter, Meter, Meter)

772

773

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

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

776

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

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

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

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

781

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

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

784 latN, lonE)}.

785

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

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

788

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

790 '''

791 swne = _GH.swne4(geohash)

792 return _2bounds(LatLon, LatLon_kwds, *swne,

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

794

795

796 def decode(geohash):

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

798 centre of) geohash cell to reasonable precision.

799

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

801

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

803

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

805 C{LatLon} or C{str}.

806

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

808 '''

809 # round to near centre without excessive precision to

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

811 swne = _GH.swne4(geohash)

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

813

814

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

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

817 of) geohash cell to reasonable precision.

818

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

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

821 or C{None}.

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

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

824

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

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

827

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

829 C{LatLon} or C{str}.

830

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

832 '''

833 ll = _GH.decode2(geohash)

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

835 LatLon( *ll, **LatLon_kwds)

836 return _xnamed(r, name__=decode2)

837

838

839@deprecated_function

840 def decode_error(geohash):

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

842 return decode_error2(geohash)

843

844

845 def decode_error2(geohash):

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

847

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

849

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

851 longitudinal errors in (C{degrees}).

852

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

854 C{LatLon} or C{str}.

855

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

857 '''

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

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

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

861

862

863 def distance_(geohash1, geohash2):

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

865

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

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

868

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

870

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

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

873 '''

874 return _2Geohash(geohash1).distanceTo(geohash2)

875

876

877@deprecated_function

878 def distance1(geohash1, geohash2):

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

880 return distance_(geohash1, geohash2)

881

882

883@deprecated_function

884 def distance2(geohash1, geohash2):

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

886 return equirectangular4(geohash1, geohash2)

887

888

889@deprecated_function

890 def distance3(geohash1, geohash2):

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

892 return haversine_(geohash1, geohash2)

893

894

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

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

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

898

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

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

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

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

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

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

905

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

907

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

909 '''

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

911 return gh

912

913

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

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

916 L{pygeodesy.equirectangular} formula.

917

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

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

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

921 L{Geohash.equirectangularTo}.

922

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

924 see method L{Geohash.equirectangularTo}.

925

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

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

928 '''

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

930

931

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

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

934 L{pygeodesy.euclidean} formula.

935

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

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

938 @kwarg radius_adjust_wrap: Optional keyword arguments for function

939 L{pygeodesy.euclidean}.

940

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

942

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

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

945 '''

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

947

948

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

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

951 using the L{pygeodesy.haversine} formula.

952

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

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

955 @kwarg radius_wrap: Optional keyword arguments for function

956 L{pygeodesy.haversine}.

957

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

959 B{C{radius}}).

960

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

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

963 '''

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

965

966

967 def neighbors(geohash):

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

969

970 @arg geohash: Cell for which neighbors are requested

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

972

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

974 of L{Geohash}es.

975

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

977 C{LatLon} or C{str}.

978 '''

979 return _2Geohash(geohash).neighbors

980

981

982 def precision(res1, res2=None):

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

984 (geographic) resolutions.

985

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

987 (C{degrees}).

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

989 resolution (C{degrees}).

990

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

992

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

994

995 @see: C++ class U{Geohash

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

997 '''

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

999 N = res2 is None

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

1001 for p in range(1, _MaxPrec):

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

1003 return p

1004 return _MaxPrec

1005

1006

1007 def resolution2(prec1, prec2=None):

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

1009 precisions.

1010

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

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

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

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

1015

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

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

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

1019

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

1021

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

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

1024 '''

1025 lon = _2res(_360_0, prec1=prec1)

1026 lat = lon if prec2 is None else \

1027 _2res(_180_0, prec2=prec2)

1028 return Resolutions2Tuple(lon, lat)

1029

1030

1031@deprecated_function

1032 def sizes(geohash):

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

1034 t = sizes3(geohash)

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

1036

1037

1038 def sizes3(geohash):

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

1040

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

1042

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

1044

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

1046 '''

1047 return _2Geohash(geohash).sizes3

1048

1049

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

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

1052 L{pygeodesy.vincentys} formula.

1053

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

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

1056 @kwarg radius_wrap: Optional keyword arguments for function

1057 L{pygeodesy.vincentys}.

1058

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

1060

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

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

1063 '''

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

1065

1066

1067__all__ += _ALL_DOCS(bounds, # functions

1068 decode, decode2, decode_error2, distance_,

1069 encode, equirectangular4, euclidean_, haversine_,

1070 neighbors, precision, resolution2, sizes3, vincentys_,

1071 decode_error, sizes) # DEPRECATED

1072

1073 if __name__ == '__main__':

1074

1075 from pygeodesy.internals import printf, _versions

1076 from timeit import timeit

1077

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

1079

1080 def _t(prec=p):

1081 i = 0

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

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

1084 _ = encode(lat, lon, prec)

1085 i += 1

1086 return i

1087

1088 i = _t() # prime

1089 n = 10

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

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

1092

1093# % python3.12 -m pygeodesy.geohash

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

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

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

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

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

1099

1100# **) MIT License

1101#

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

1103#

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

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

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

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

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

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

1110#

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

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

1113#

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

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

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

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

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

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

1120# OTHER DEALINGS IN THE SOFTWARE.