1

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

3

4 u'''Geohash en-/decoding.

5

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

7 encode, decode and inspect I{geohashes}.

8

9 Transcoded from JavaScript originals by I{(C) Chris Veness 2011-2015}

10 and published under the same MIT Licence**, see U{Geohashes

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

12

13 See also U{Geohash<https://WikiPedia.org/wiki/Geohash>},

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

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

16 U{Geohash-Javascript<https://GitHub.com/DaveTroy/geohash-js>}.

17'''

18

19 from pygeodesy.basics import isodd, isstr, map2

20 from 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!

22 from pygeodesy.dms import parse3llh # parseDMS2

23 from pygeodesy.errors import _ValueError, _xkwds

24 from pygeodesy.fmath import favg

25 from pygeodesy.formy import equirectangular_ as _equirectangular_, \

26 equirectangular, euclidean, haversine, vincentys

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

28 _S_, _SE_, _SW_, _W_

29 from pygeodesy.lazily import _ALL_LAZY, _ALL_OTHER

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

31 from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \

32 LatLon2Tuple, PhiLam2Tuple

33 from pygeodesy.props import deprecated_function, deprecated_method, \

34 deprecated_property_RO, Property_RO

35 from pygeodesy.streprs import fstr

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

37 _xStrError

38

39 from math import fabs, ldexp, log10, radians

40

41__all__ = _ALL_LAZY.geohash

42__version__ = '23.03.19'

43

44

45 class _GH(object):

46 '''(INTERNAL) Lazily defined constants.

47 '''

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

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

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

51

52 @Property_RO

53 def Borders(self):

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

55

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

57

58 @Property_RO

59 def DecodedBase32(self): # inverse GeohashBase32 map

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

61

62 # Geohash-specific base32 map

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

64

65 @Property_RO

66 def Neighbors(self):

67 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy',

68 'bc01fg45238967deuvhjyznpkmstqrwx',

69 '14365h7k9dcfesgujnmqp0r2twvyx8zb',

70 '238967debc01fg45kmstqrwxuvhjyznp')

71

72 @Property_RO

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

88

89_GH = _GH() # PYCHOK singleton

90_MaxPrec = 12

91

92

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

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

95 '''

96 if LatLon is None:

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

98 else:

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

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

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

102 return r # _xnamed(r, name)

103

104

105 def _2center(bounds):

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

107 '''

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

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

110

111

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

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

114 '''

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

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

117 Lon(lon, Error=GeohashError))

118

119

120 def _2Geohash(geohash):

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

122 '''

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

124 Geohash(geohash)

125

126

127 def _2geostr(geohash):

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

129 '''

130 try:

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

132 raise ValueError

133 geostr = geohash.lower()

134 for c in geostr:

135 if c not in _GH.DecodedBase32:

136 raise ValueError

137 return geostr

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

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

140

141

142 class Geohash(Str):

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

144 '''

145 # no str.__init__ in Python 3

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

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

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

149

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

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

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

153 some examples.

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

155

156 @return: New L{Geohash}.

157

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

159

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

161 '''

162 ll = None

163

164 if isinstance(cll, Geohash):

165 gh = _2geostr(str(cll))

166

167 elif isstr(cll):

168 if _COMMA_ in cll:

169 ll = _2fll(*parse3llh(cll))

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

171 else:

172 gh = _2geostr(cll)

173

174 else: # assume LatLon

175 try:

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

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

178 except AttributeError:

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

180

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

182 self._latlon = ll

183 return self

184

185 @deprecated_property_RO

186 def ab(self):

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

188 return self.philam

189

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

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

192

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

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

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

196

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

198

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

200 '''

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

202

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

204 if D not in _GH.Neighbors:

205 raise GeohashError(direction=direction)

206

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

208

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

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

211 if i < 0:

212 raise GeohashError(geohash=self)

213

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

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

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

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

218

219 n = name or self.name

220 if n:

221 n = _DOT_(n, D)

222 # append letter for direction to parent

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

224

225 @Property_RO

226 def _bounds(self):

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

228 '''

229 return bounds(self)

230

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

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

233 geohash cell.

234

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

236 or C{None}.

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

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

239

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

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

242 C{B{LatLon} is None},

243 '''

244 r = self._bounds

245 return r if LatLon is None else \

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

247

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

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

250 '''

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

252 return func_(*lls, **kwds)

253

254 def distanceTo(self, other):

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

256 based the cell sizes.

257

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

259

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

261

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

263 C{LatLon} or C{str}.

264 '''

265 other = _2Geohash(other)

266

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

268 if n:

269 for n in range(n):

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

271 break

272 return _GH.Sizes[n][2]

273

274 @deprecated_method

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

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

277 return self.distanceTo(other)

278

279 distance1 = distance1To

280

281 @deprecated_method

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

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

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

285

286 distance2 = distance2To

287

288 @deprecated_method

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

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

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

292

293 distance3 = distance3To

294

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

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

297 using function L{pygeodesy.equirectangular}.

298

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

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

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

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

303 @kwarg adjust: Adjust the wrapped, unrolled longitudinal

304 delta by the cosine of the mean latitude

305 C{bool}).

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

307

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

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

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

311

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

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

314

315 @see: U{Local, flat earth approximation

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

317 '''

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

319 kwds = dict(adjust=adjust, limit=None, wrap=wrap)

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

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

322

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

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

325 using function L{pygeodesy.euclidean}.

326

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

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

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

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

331 @kwarg adjust: Adjust the wrapped, unrolled longitudinal

332 delta by the cosine of the mean latitude

333 C{bool}).

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

335

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

337 ellipsoid or datum axes).

338

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

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

341 '''

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

343 adjust=adjust, wrap=wrap)

344

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

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

347 the L{pygeodesy.haversine} formula.

348

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

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

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

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

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

354

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

356 ellipsoid or datum axes).

357

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

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

360 '''

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

362

363 @Property_RO

364 def latlon(self):

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

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

367 '''

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

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

370

371 @Property_RO

372 def neighbors(self):

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

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

375 '''

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

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

378 name=self.name)

379

380 @Property_RO

381 def philam(self):

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

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

384 '''

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

386

387 @Property_RO

388 def precision(self):

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

390 '''

391 return len(self)

392

393 @Property_RO

394 def sizes(self):

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

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

397 '''

398 z = _GH.Sizes

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

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

401

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

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

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

405

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

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

408 keyword arguments, ignored if

409 C{B{LatLon} is None}.

410

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

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

413 is C{None}.

414

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

416

417 @example:

418

419 >>> from sphericalTrigonometry import LatLon

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

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

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

423 '''

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

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

426

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

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

429 the L{pygeodesy.vincentys} formula.

430

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

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

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

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

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

436

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

438 ellipsoid or datum axes).

439

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

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

442 '''

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

444

445 @Property_RO

446 def N(self):

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

448 '''

449 return self.adjacent(_N_)

450

451 @Property_RO

452 def S(self):

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

454 '''

455 return self.adjacent(_S_)

456

457 @Property_RO

458 def E(self):

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

460 '''

461 return self.adjacent(_E_)

462

463 @Property_RO

464 def W(self):

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

466 '''

467 return self.adjacent(_W_)

468

469 @Property_RO

470 def NE(self):

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

472 '''

473 return self.N.E

474

475 @Property_RO

476 def NW(self):

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

478 '''

479 return self.N.W

480

481 @Property_RO

482 def SE(self):

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

484 '''

485 return self.S.E

486

487 @Property_RO

488 def SW(self):

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

490 '''

491 return self.S.W

492

493

494 class GeohashError(_ValueError):

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

496 '''

497 pass

498

499

500 class Neighbors8Dict(_NamedDict):

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

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

503 '''

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

505

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

507 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

509

510

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

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

513

514

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

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

517

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

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

520 or C{None}.

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

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

523

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

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

526 lonW, latN, lonE)}.

527

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

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

530 B{C{LatLon_kwds}}.

531

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

533

534 @example:

535

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

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

538 '''

539 gh = _2Geohash(geohash)

540 if len(gh) < 1:

541 raise GeohashError(geohash=geohash)

542

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

544 try:

545 d = True

546 for c in gh.lower():

547 i = _GH.DecodedBase32[c]

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

549 if d: # longitude

550 if i & m:

551 w = favg(w, e)

552 else:

553 e = favg(w, e)

554 else: # latitude

555 if i & m:

556 s = favg(s, n)

557 else:

558 n = favg(s, n)

559 d = not d

560 except KeyError:

561 raise GeohashError(geohash=geohash)

562

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

564 name=nameof(geohash))

565

566

567 def _bounds3(geohash):

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

569 '''

570 b = bounds(geohash)

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

572

573

574 def decode(geohash):

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

576 centre of) geohash cell to reasonable precision.

577

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

579

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

581

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

583 C{LatLon} or C{str}.

584

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

586

587 @example:

588

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

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

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

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

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

594 '''

595 b, h, w = _bounds3(geohash)

596 lat, lon = _2center(b)

597

598 # round to near centre without excessive precision to

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

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

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

602

603

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

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

606 centre of) geohash cell to reasonable precision.

607

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

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

610 or C{None}.

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

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

613

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

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

616

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

618 C{LatLon} or C{str}.

619

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

621 '''

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

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

624 return _xnamed(r, decode2.__name__)

625

626

627 def decode_error(geohash):

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

629 this geohash.

630

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

632

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

634 longitudinal errors in (C{degrees}).

635

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

637 C{LatLon} or C{str}.

638

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

640

641 @example:

642

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

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

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

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

647 '''

648 _, h, w = _bounds3(geohash)

649 return LatLon2Tuple(h * _0_5, # Height error

650 w * _0_5) # Width error

651

652

653 def distance_(geohash1, geohash2):

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

655

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

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

658

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

660

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

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

663

664 @example:

665

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

667 '''

668 return _2Geohash(geohash1).distanceTo(geohash2)

669

670

671@deprecated_function

672 def distance1(geohash1, geohash2):

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

674 return distance_(geohash1, geohash2)

675

676

677@deprecated_function

678 def distance2(geohash1, geohash2):

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

680 return equirectangular_(geohash1, geohash2)

681

682

683@deprecated_function

684 def distance3(geohash1, geohash2):

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

686 return haversine_(geohash1, geohash2)

687

688

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

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

691 precision or if not provided, to an automatically evaluated

692 precision.

693

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

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

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

697 1..12).

698

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

700

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

702

703 @example:

704

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

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

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

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

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

710 '''

711 lat, lon = _2fll(lat, lon)

712

713 if precision is None:

714 # Infer precision by refining geohash until

715 # it matches precision of supplied lat/lon.

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

717 gh = encode(lat, lon, p)

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

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

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

721 return gh

722 p = _MaxPrec

723 else:

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

725

726 b = i = 0

727 d, gh = True, []

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

729

730 while p > 0:

731 i += i

732 if d: # bisect longitude

733 m = favg(e, w)

734 if lon < m:

735 e = m

736 else:

737 w = m

738 i += 1

739 else: # bisect latitude

740 m = favg(n, s)

741 if lat < m:

742 n = m

743 else:

744 s = m

745 i += 1

746 d = not d

747

748 b += 1

749 if b == 5:

750 # 5 bits gives a character:

751 # append it and start over

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

753 b = i = 0

754 p -= 1

755

756 return NN.join(gh)

757

758

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

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

761 L{pygeodesy.equirectangular} formula.

762

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

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

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

766

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

768 B{C{radius}}).

769

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

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

772

773 @example:

774

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

776 '''

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

778

779

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

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

782 using the L{pygeodesy.haversine} formula.

783

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

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

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

787

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

789 B{C{radius}}).

790

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

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

793

794 @example:

795

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

797 '''

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

799

800

801 def neighbors(geohash):

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

803

804 @arg geohash: Cell for which neighbors are requested

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

806

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

808 of L{Geohash}es.

809

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

811 C{LatLon} or C{str}.

812 '''

813 return _2Geohash(geohash).neighbors

814

815

816 def precision(res1, res2=None):

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

818 (geographic) resolutions.

819

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

821 (C{degrees}).

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

823 resolution (C{degrees}).

824

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

826

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

828

829 @see: C++ class U{Geohash

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

831 '''

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

833 if res2 is None:

834 t = r, r

835 for p in range(1, _MaxPrec):

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

837 return p

838

839 else:

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

841 for p in range(1, _MaxPrec):

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

843 return p

844

845 return _MaxPrec

846

847

848 class Resolutions2Tuple(_NamedTuple):

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

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

851 '''

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

853 _Units_ = ( Degrees_, Degrees_)

854

855

856 def resolution2(prec1, prec2=None):

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

858 precisions.

859

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

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

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

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

864

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

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

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

868

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

870

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

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

873 '''

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

875

876 if prec1:

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

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

879

880 if prec2:

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

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

883

884 return Resolutions2Tuple(res1, res2)

885

886

887 def sizes(geohash):

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

889

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

891 C{str}).

892

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

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

895

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

897 C{LatLon} or C{str}.

898 '''

899 return _2Geohash(geohash).sizes

900

901

902__all__ += _ALL_OTHER(bounds, # functions

903 decode, decode2, decode_error, distance_,

904 encode, equirectangular_, haversine_,

905 neighbors, precision, resolution2, sizes)

906

907# **) MIT License

908#

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

910#

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

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

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

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

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

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

917#

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

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

920#

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

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

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

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

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

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

927# OTHER DEALINGS IN THE SOFTWARE.