1

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

3

4 u'''Geohash en-/decoding.

5

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

7 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>}, U{Geohash

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

15<https://PyPI.org/project/pygeohash>} and U{Geohash-Javascript

16<https://GitHub.com/DaveTroy/geohash-js>}.

17'''

18

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 import formy as _formy # _MODS

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

27 _S_, _SE_, _SW_, _W_

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

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 from pygeodesy.streprs import fstr

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

36 _xStrError

37

38 from math import fabs, ldexp, log10, radians

39

40__all__ = _ALL_LAZY.geohash

41__version__ = '24.06.10'

42

43_formy = _MODS.into(formy=__name__)

44

45

46 class _GH(object):

47 '''(INTERNAL) Lazily defined constants.

48 '''

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

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

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

52

53 @Property_RO

54 def Borders(self):

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

56

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

58

59 @Property_RO

60 def DecodedBase32(self): # inverse GeohashBase32 map

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

62

63 # Geohash-specific base32 map

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

65

66 @Property_RO

67 def Neighbors(self):

68 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy',

69 'bc01fg45238967deuvhjyznpkmstqrwx',

70 '14365h7k9dcfesgujnmqp0r2twvyx8zb',

71 '238967debc01fg45kmstqrwxuvhjyznp')

72

73 @Property_RO

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

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

76 _t = _floatuple

77 return (_t(20032e3, 20000e3, 11292815.096), # 0

78 _t( 5003e3, 5000e3, 2821794.075), # 1

79 _t( 650e3, 1225e3, 503442.397), # 2

80 _t( 156e3, 156e3, 88013.575), # 3

81 _t( 19500, 39100, 15578.683), # 4

82 _t( 4890, 4890, 2758.887), # 5

83 _t( 610, 1220, 486.710), # 6

84 _t( 153, 153, 86.321), # 7

85 _t( 19.1, 38.2, 15.239), # 8

86 _t( 4.77, 4.77, 2.691), # 9

87 _t( 0.596, 1.19, 0.475), # 10

88 _t( 0.149, 0.149, 0.084), # 11

89 _t( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec

90

91_GH = _GH() # PYCHOK singleton

92_MaxPrec = 12

93

94

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

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

97 '''

98 if LatLon is None:

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

100 else:

101 kwds = _xkwds(LatLon_kwds, **name)

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

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

104 return r

105

106

107 def _2center(bounds):

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

109 '''

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

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

112

113

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

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

116 '''

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

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

119 Lon(lon, Error=GeohashError))

120

121

122 def _2Geohash(geohash):

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

124 '''

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

126 Geohash(geohash)

127

128

129 def _2geostr(geohash):

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

131 '''

132 try:

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

134 raise ValueError()

135 geostr = geohash.lower()

136 for c in geostr:

137 if c not in _GH.DecodedBase32:

138 raise ValueError()

139 return geostr

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

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

142

143

144 class Geohash(Str):

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

146 '''

147 # no str.__init__ in Python 3

148 def __new__(cls, cll, precision=None, **name):

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

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

151

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

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

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

155 some examples.

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

157

158 @return: New L{Geohash}.

159

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

161

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

163 '''

164 ll = None

165

166 if isinstance(cll, Geohash):

167 gh = _2geostr(str(cll))

168

169 elif isstr(cll):

170 if _COMMA_ in cll:

171 ll = _2fll(*parse3llh(cll))

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

173 else:

174 gh = _2geostr(cll)

175

176 else: # assume LatLon

177 try:

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

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

180 except AttributeError:

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

182

183 self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=cll))

184 self._latlon = ll

185 return self

186

187 @deprecated_property_RO

188 def ab(self):

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

190 return self.philam

191

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

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

194

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

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

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

198

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

200

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

202 '''

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

204

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

206 if D not in _GH.Neighbors:

207 raise GeohashError(direction=direction)

208

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

210

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

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

213 if i < 0:

214 raise GeohashError(geohash=self)

215

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

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

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

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

220

221 n = self._name__(name)

222 if n:

223 n = _DOT_(n, D)

224 # append letter for direction to parent

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

226

227 @Property_RO

228 def _bounds(self):

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

230 '''

231 return bounds(self)

232

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

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

235 geohash cell.

236

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

238 or C{None}.

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

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

241

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

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

244 C{B{LatLon} is None},

245 '''

246 r = self._bounds

247 return r if LatLon is None else \

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

249

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

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

252 '''

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

254 return func_(*lls, **kwds)

255

256 def distanceTo(self, other):

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

258 based the cell sizes.

259

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

261

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

263

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

265 C{LatLon} or C{str}.

266 '''

267 other = _2Geohash(other)

268

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

270 if n:

271 for n in range(n):

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

273 break

274 return _GH.Sizes[n][2]

275

276 @deprecated_method

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

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

279 return self.distanceTo(other)

280

281 distance1 = distance1To

282

283 @deprecated_method

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

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

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

287

288 distance2 = distance2To

289

290 @deprecated_method

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

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

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

294

295 distance3 = distance3To

296

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

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

299 using function L{pygeodesy.equirectangular}.

300

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

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

303 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or

304 L{a_f2Tuple}) or C{None}, see function

305 L{pygeodesy.equirectangular}.

306 @kwarg adjust_limit_wrap: Optional keyword arguments for function

307 L{pygeodesy.equirectangular4}, overriding defaults

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

309

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

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

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

313

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

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

316

317 @see: U{Local, flat earth approximation

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

319 '''

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

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

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

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

324

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

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

327 function L{pygeodesy.euclidean}.

328

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

330 @kwarg radius_adjust_wrap: Optional keyword arguments for function

331 L{pygeodesy.euclidean}.

332

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

334 ellipsoid or datum axes).

335

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

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

338 '''

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

340

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

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

343 the L{pygeodesy.haversine} formula.

344

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

346 @kwarg radius_wrap: Optional keyword arguments for function

347 L{pygeodesy.haversine}.

348

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

350 ellipsoid or datum axes).

351

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

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

354 '''

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

356

357 @Property_RO

358 def latlon(self):

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

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

361 '''

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

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

364

365 @Property_RO

366 def neighbors(self):

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

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

369 '''

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

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

372 name=self.name)

373

374 @Property_RO

375 def philam(self):

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

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

378 '''

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

380

381 @Property_RO

382 def precision(self):

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

384 '''

385 return len(self)

386

387 @Property_RO

388 def sizes(self):

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

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

391 '''

392 z = _GH.Sizes

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

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

395

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

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

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

399

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

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

402 keyword arguments, ignored if

403 C{B{LatLon} is None}.

404

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

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

407 is C{None}.

408

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

410 '''

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

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

413

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

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

416 the L{pygeodesy.vincentys} formula.

417

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

419 @kwarg radius_wrap: Optional keyword arguments for function

420 L{pygeodesy.vincentys}.

421

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

423 ellipsoid or datum axes).

424

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

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

427 '''

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

429

430 @Property_RO

431 def N(self):

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

433 '''

434 return self.adjacent(_N_)

435

436 @Property_RO

437 def S(self):

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

439 '''

440 return self.adjacent(_S_)

441

442 @Property_RO

443 def E(self):

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

445 '''

446 return self.adjacent(_E_)

447

448 @Property_RO

449 def W(self):

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

451 '''

452 return self.adjacent(_W_)

453

454 @Property_RO

455 def NE(self):

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

457 '''

458 return self.N.E

459

460 @Property_RO

461 def NW(self):

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

463 '''

464 return self.N.W

465

466 @Property_RO

467 def SE(self):

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

469 '''

470 return self.S.E

471

472 @Property_RO

473 def SW(self):

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

475 '''

476 return self.S.W

477

478

479 class GeohashError(_ValueError):

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

481 '''

482 pass

483

484

485 class Neighbors8Dict(_NamedDict):

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

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

488 '''

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

490

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

492 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

494

495

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

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

498

499

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

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

502

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

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

505 or C{None}.

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

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

508

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

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

511 lonW, latN, lonE)}.

512

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

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

515 B{C{LatLon_kwds}}.

516

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

518 '''

519 gh = _2Geohash(geohash)

520 if len(gh) < 1:

521 raise GeohashError(geohash=geohash)

522

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

524 try:

525 d, _avg = True, favg

526 for c in gh.lower():

527 i = _GH.DecodedBase32[c]

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

529 if d: # longitude

530 a = _avg(w, e)

531 if (i & m):

532 w = a

533 else:

534 e = a

535 else: # latitude

536 a = _avg(s, n)

537 if (i & m):

538 s = a

539 else:

540 n = a

541 d = not d

542 except KeyError:

543 raise GeohashError(geohash=geohash)

544

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

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

547

548

549 def _bounds3(geohash):

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

551 '''

552 b = bounds(geohash)

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

554

555

556 def decode(geohash):

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

558 centre of) geohash cell to reasonable precision.

559

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

561

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

563

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

565 C{LatLon} or C{str}.

566

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

568 '''

569 b, h, w = _bounds3(geohash)

570 lat, lon = _2center(b)

571

572 # round to near centre without excessive precision to

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

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

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

576

577

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

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

580 of) geohash cell to reasonable precision.

581

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

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

584 or C{None}.

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

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

587

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

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

590

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

592 C{LatLon} or C{str}.

593

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

595 '''

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

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

598 return _xnamed(r, name__=decode2)

599

600

601 def decode_error(geohash):

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

603 this geohash.

604

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

606

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

608 longitudinal errors in (C{degrees}).

609

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

611 C{LatLon} or C{str}.

612

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

614 '''

615 _, h, w = _bounds3(geohash)

616 return LatLon2Tuple(h * _0_5, # Height error

617 w * _0_5) # Width error

618

619

620 def distance_(geohash1, geohash2):

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

622

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

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

625

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

627

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

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

630 '''

631 return _2Geohash(geohash1).distanceTo(geohash2)

632

633

634@deprecated_function

635 def distance1(geohash1, geohash2):

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

637 return distance_(geohash1, geohash2)

638

639

640@deprecated_function

641 def distance2(geohash1, geohash2):

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

643 return equirectangular4(geohash1, geohash2)

644

645

646@deprecated_function

647 def distance3(geohash1, geohash2):

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

649 return haversine_(geohash1, geohash2)

650

651

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

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

654 precision or if not provided, to an automatically evaluated

655 precision.

656

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

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

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

660 1..12).

661

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

663

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

665 '''

666 lat, lon = _2fll(lat, lon)

667

668 if precision is None:

669 # Infer precision by refining geohash until

670 # it matches precision of supplied lat/lon.

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

672 gh = encode(lat, lon, p)

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

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

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

676 return gh

677 p = _MaxPrec

678 else:

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

680

681 b = i = 0

682 d, gh = True, []

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

684

685 _avg = favg

686 while p > 0:

687 i += i

688 if d: # bisect longitude

689 m = _avg(e, w)

690 if lon < m:

691 e = m

692 else:

693 w = m

694 i += 1

695 else: # bisect latitude

696 m = _avg(n, s)

697 if lat < m:

698 n = m

699 else:

700 s = m

701 i += 1

702 d = not d

703

704 b += 1

705 if b == 5:

706 # 5 bits gives a character:

707 # append it and start over

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

709 b = i = 0

710 p -= 1

711

712 return NN.join(gh)

713

714

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

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

717 L{pygeodesy.equirectangular} formula.

718

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

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

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

722 L{Geohash.equirectangularTo}.

723

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

725 see method L{Geohash.equirectangularTo}.

726

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

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

729 '''

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

731

732

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

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

735 L{pygeodesy.euclidean} formula.

736

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

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

739 @kwarg radius_adjust_wrap: Optional keyword arguments for function

740 L{pygeodesy.euclidean}.

741

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

743

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

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

746 '''

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

748

749

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

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

752 using the L{pygeodesy.haversine} formula.

753

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

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

756 @kwarg radius_wrap: Optional keyword arguments for function

757 L{pygeodesy.haversine}.

758

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

760 B{C{radius}}).

761

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

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

764 '''

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

766

767

768 def neighbors(geohash):

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

770

771 @arg geohash: Cell for which neighbors are requested

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

773

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

775 of L{Geohash}es.

776

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

778 C{LatLon} or C{str}.

779 '''

780 return _2Geohash(geohash).neighbors

781

782

783 def precision(res1, res2=None):

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

785 (geographic) resolutions.

786

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

788 (C{degrees}).

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

790 resolution (C{degrees}).

791

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

793

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

795

796 @see: C++ class U{Geohash

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

798 '''

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

800 N = res2 is None

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

802 for p in range(1, _MaxPrec):

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

804 return p

805 return _MaxPrec

806

807

808 class Resolutions2Tuple(_NamedTuple):

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

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

811 '''

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

813 _Units_ = ( Degrees_, Degrees_)

814

815

816 def resolution2(prec1, prec2=None):

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

818 precisions.

819

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

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

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

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

824

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

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

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

828

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

830

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

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

833 '''

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

835

836 if prec1:

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

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

839

840 if prec2:

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

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

843

844 return Resolutions2Tuple(res1, res2)

845

846

847 def sizes(geohash):

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

849

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

851 C{str}).

852

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

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

855

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

857 C{LatLon} or C{str}.

858 '''

859 return _2Geohash(geohash).sizes

860

861

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

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

864 L{pygeodesy.vincentys} formula.

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 @kwarg radius_wrap: Optional keyword arguments for function

869 L{pygeodesy.vincentys}.

870

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

872

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

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

875 '''

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

877

878

879__all__ += _ALL_OTHER(bounds, # functions

880 decode, decode2, decode_error, distance_,

881 encode, equirectangular4, euclidean_, haversine_,

882 neighbors, precision, resolution2, sizes, vincentys_)

883

884# **) MIT License

885#

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

887#

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

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

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

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

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

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

894#

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

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

897#

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

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

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

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

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

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

904# OTHER DEALINGS IN THE SOFTWARE.