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 _NamedDict, _NamedTuple, nameof, _xnamed

30 from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, \

31 LatLon2Tuple, PhiLam2Tuple

32 from pygeodesy.props import deprecated_function, deprecated_method, \

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

42

43

44 class _GH(object):

45 '''(INTERNAL) Lazily defined constants.

46 '''

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

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

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

50

51 @Property_RO

52 def Borders(self):

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

54

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

56

57 @Property_RO

58 def DecodedBase32(self): # inverse GeohashBase32 map

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

60

61 # Geohash-specific base32 map

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

63

64 @Property_RO

65 def Neighbors(self):

66 return self._4d('p0r21436x8zb9dcf5h7kjnmqesgutwvy',

67 'bc01fg45238967deuvhjyznpkmstqrwx',

68 '14365h7k9dcfesgujnmqp0r2twvyx8zb',

69 '238967debc01fg45kmstqrwxuvhjyznp')

70

71 @Property_RO

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

87

88_GH = _GH() # PYCHOK singleton

89_MaxPrec = 12

90

91

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

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

94 '''

95 if LatLon is None:

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

97 else:

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

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

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

101 return r # _xnamed(r, name)

102

103

104 def _2center(bounds):

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

106 '''

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

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

109

110

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

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

113 '''

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

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

116 Lon(lon, Error=GeohashError))

117

118

119 def _2Geohash(geohash):

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

121 '''

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

123 Geohash(geohash)

124

125

126 def _2geostr(geohash):

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

128 '''

129 try:

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

131 raise ValueError

132 geostr = geohash.lower()

133 for c in geostr:

134 if c not in _GH.DecodedBase32:

135 raise ValueError

136 return geostr

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

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

139

140

141 class Geohash(Str):

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

143 '''

144 # no str.__init__ in Python 3

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

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

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

148

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

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

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

152 some examples.

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

154

155 @return: New L{Geohash}.

156

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

158

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

160 '''

161 ll = None

162

163 if isinstance(cll, Geohash):

164 gh = _2geostr(str(cll))

165

166 elif isstr(cll):

167 if _COMMA_ in cll:

168 ll = _2fll(*parse3llh(cll))

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

170 else:

171 gh = _2geostr(cll)

172

173 else: # assume LatLon

174 try:

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

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

177 except AttributeError:

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

179

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

181 self._latlon = ll

182 return self

183

184 @deprecated_property_RO

185 def ab(self):

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

187 return self.philam

188

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

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

191

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

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

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

195

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

197

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

199 '''

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

201

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

203 if D not in _GH.Neighbors:

204 raise GeohashError(direction=direction)

205

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

207

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

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

210 if i < 0:

211 raise GeohashError(geohash=self)

212

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

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

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

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

217

218 n = name or self.name

219 if n:

220 n = _DOT_(n, D)

221 # append letter for direction to parent

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

223

224 @Property_RO

225 def _bounds(self):

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

227 '''

228 return bounds(self)

229

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

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

232 geohash cell.

233

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

235 or C{None}.

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

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

238

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

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

241 C{B{LatLon} is None},

242 '''

243 r = self._bounds

244 return r if LatLon is None else \

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

246

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

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

249 '''

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

251 return func_(*lls, **kwds)

252

253 def distanceTo(self, other):

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

255 based the cell sizes.

256

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

258

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

260

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

262 C{LatLon} or C{str}.

263 '''

264 other = _2Geohash(other)

265

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

267 if n:

268 for n in range(n):

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

270 break

271 return _GH.Sizes[n][2]

272

273 @deprecated_method

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

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

276 return self.distanceTo(other)

277

278 distance1 = distance1To

279

280 @deprecated_method

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

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

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

284

285 distance2 = distance2To

286

287 @deprecated_method

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

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

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

291

292 distance3 = distance3To

293

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

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

296 using function L{pygeodesy.equirectangular}.

297

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

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

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

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

302 @kwarg adjust_limit_wrap: Optional keyword arguments for

303 function L{pygeodesy.equirectangular_},

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

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

306

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

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

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

310

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

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

313

314 @see: U{Local, flat earth approximation

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

316 '''

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

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

319 m = self._formy

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

321 m.equirectangular_(*lls, **kwds).distance2

322

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

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

325 function L{pygeodesy.euclidean}.

326

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

328 @kwarg radius_adjust_wrap: Optional keyword arguments for function

329 L{pygeodesy.euclidean}.

330

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

332 ellipsoid or datum axes).

333

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

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

336 '''

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

338

339 @property_RO

340 def _formy(self):

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

342 '''

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

344 return f

345

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

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

348 the L{pygeodesy.haversine} formula.

349

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

351 @kwarg radius_wrap: Optional keyword arguments for function

352 L{pygeodesy.haversine}.

353

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

355 ellipsoid or datum axes).

356

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

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

359 '''

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

361

362 @Property_RO

363 def latlon(self):

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

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

366 '''

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

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

369

370 @Property_RO

371 def neighbors(self):

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

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

374 '''

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

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

377 name=self.name)

378

379 @Property_RO

380 def philam(self):

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

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

383 '''

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

385

386 @Property_RO

387 def precision(self):

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

389 '''

390 return len(self)

391

392 @Property_RO

393 def sizes(self):

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

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

396 '''

397 z = _GH.Sizes

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

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

400

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

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

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

404

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

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

407 keyword arguments, ignored if

408 C{B{LatLon} is None}.

409

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

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

412 is C{None}.

413

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

415 '''

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

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

418

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

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

421 the L{pygeodesy.vincentys} formula.

422

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

424 @kwarg radius_wrap: Optional keyword arguments for function

425 L{pygeodesy.vincentys}.

426

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

428 ellipsoid or datum axes).

429

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

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

432 '''

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

434

435 @Property_RO

436 def N(self):

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

438 '''

439 return self.adjacent(_N_)

440

441 @Property_RO

442 def S(self):

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

444 '''

445 return self.adjacent(_S_)

446

447 @Property_RO

448 def E(self):

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

450 '''

451 return self.adjacent(_E_)

452

453 @Property_RO

454 def W(self):

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

456 '''

457 return self.adjacent(_W_)

458

459 @Property_RO

460 def NE(self):

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

462 '''

463 return self.N.E

464

465 @Property_RO

466 def NW(self):

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

468 '''

469 return self.N.W

470

471 @Property_RO

472 def SE(self):

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

474 '''

475 return self.S.E

476

477 @Property_RO

478 def SW(self):

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

480 '''

481 return self.S.W

482

483

484 class GeohashError(_ValueError):

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

486 '''

487 pass

488

489

490 class Neighbors8Dict(_NamedDict):

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

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

493 '''

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

495

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

497 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

499

500

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

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

503

504

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

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

507

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

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

510 or C{None}.

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

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

513

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

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

516 lonW, latN, lonE)}.

517

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

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

520 B{C{LatLon_kwds}}.

521

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

523 '''

524 gh = _2Geohash(geohash)

525 if len(gh) < 1:

526 raise GeohashError(geohash=geohash)

527

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

529 try:

530 d, _avg = True, favg

531 for c in gh.lower():

532 i = _GH.DecodedBase32[c]

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

534 if d: # longitude

535 if i & m:

536 w = _avg(w, e)

537 else:

538 e = _avg(w, e)

539 else: # latitude

540 if i & m:

541 s = _avg(s, n)

542 else:

543 n = _avg(s, n)

544 d = not d

545 except KeyError:

546 raise GeohashError(geohash=geohash)

547

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

549 name=nameof(geohash))

550

551

552 def _bounds3(geohash):

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

554 '''

555 b = bounds(geohash)

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

557

558

559 def decode(geohash):

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

561 centre of) geohash cell to reasonable precision.

562

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

564

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

566

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

568 C{LatLon} or C{str}.

569

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

571 '''

572 b, h, w = _bounds3(geohash)

573 lat, lon = _2center(b)

574

575 # round to near centre without excessive precision to

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

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

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

579

580

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

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

583 of) geohash cell to reasonable precision.

584

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

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

587 or C{None}.

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

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

590

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

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

593

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

595 C{LatLon} or C{str}.

596

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

598 '''

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

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

601 return _xnamed(r, decode2.__name__)

602

603

604 def decode_error(geohash):

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

606 this geohash.

607

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

609

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

611 longitudinal errors in (C{degrees}).

612

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

614 C{LatLon} or C{str}.

615

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

617 '''

618 _, h, w = _bounds3(geohash)

619 return LatLon2Tuple(h * _0_5, # Height error

620 w * _0_5) # Width error

621

622

623 def distance_(geohash1, geohash2):

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

625

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

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

628

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

630

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

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

633 '''

634 return _2Geohash(geohash1).distanceTo(geohash2)

635

636

637@deprecated_function

638 def distance1(geohash1, geohash2):

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

640 return distance_(geohash1, geohash2)

641

642

643@deprecated_function

644 def distance2(geohash1, geohash2):

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

646 return equirectangular_(geohash1, geohash2)

647

648

649@deprecated_function

650 def distance3(geohash1, geohash2):

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

652 return haversine_(geohash1, geohash2)

653

654

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

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

657 precision or if not provided, to an automatically evaluated

658 precision.

659

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

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

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

663 1..12).

664

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

666

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

668 '''

669 lat, lon = _2fll(lat, lon)

670

671 if precision is None:

672 # Infer precision by refining geohash until

673 # it matches precision of supplied lat/lon.

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

675 gh = encode(lat, lon, p)

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

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

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

679 return gh

680 p = _MaxPrec

681 else:

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

683

684 b = i = 0

685 d, gh = True, []

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

687

688 _avg = favg

689 while p > 0:

690 i += i

691 if d: # bisect longitude

692 m = _avg(e, w)

693 if lon < m:

694 e = m

695 else:

696 w = m

697 i += 1

698 else: # bisect latitude

699 m = _avg(n, s)

700 if lat < m:

701 n = m

702 else:

703 s = m

704 i += 1

705 d = not d

706

707 b += 1

708 if b == 5:

709 # 5 bits gives a character:

710 # append it and start over

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

712 b = i = 0

713 p -= 1

714

715 return NN.join(gh)

716

717

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

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

720 L{pygeodesy.equirectangular} formula.

721

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

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

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

725 L{Geohash.equirectangularTo}.

726

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

728 see method L{Geohash.equirectangularTo}.

729

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

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

732 '''

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

734

735

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

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

738 L{pygeodesy.euclidean} formula.

739

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

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

742 @kwarg radius_adjust_wrap: Optional keyword arguments for function

743 L{pygeodesy.euclidean}.

744

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

746

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

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

749 '''

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

751

752

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

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

755 using the L{pygeodesy.haversine} formula.

756

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

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

759 @kwarg radius_wrap: Optional keyword arguments for function

760 L{pygeodesy.haversine}.

761

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

763 B{C{radius}}).

764

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

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

767 '''

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

769

770

771 def neighbors(geohash):

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

773

774 @arg geohash: Cell for which neighbors are requested

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

776

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

778 of L{Geohash}es.

779

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

781 C{LatLon} or C{str}.

782 '''

783 return _2Geohash(geohash).neighbors

784

785

786 def precision(res1, res2=None):

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

788 (geographic) resolutions.

789

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

791 (C{degrees}).

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

793 resolution (C{degrees}).

794

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

796

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

798

799 @see: C++ class U{Geohash

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

801 '''

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

803 if res2 is None:

804 t = r, r

805 for p in range(1, _MaxPrec):

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

807 return p

808

809 else:

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

811 for p in range(1, _MaxPrec):

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

813 return p

814

815 return _MaxPrec

816

817

818 class Resolutions2Tuple(_NamedTuple):

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

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

821 '''

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

823 _Units_ = ( Degrees_, Degrees_)

824

825

826 def resolution2(prec1, prec2=None):

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

828 precisions.

829

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

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

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

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

834

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

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

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

838

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

840

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

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

843 '''

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

845

846 if prec1:

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

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

849

850 if prec2:

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

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

853

854 return Resolutions2Tuple(res1, res2)

855

856

857 def sizes(geohash):

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

859

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

861 C{str}).

862

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

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

865

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

867 C{LatLon} or C{str}.

868 '''

869 return _2Geohash(geohash).sizes

870

871

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

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

874 L{pygeodesy.vincentys} formula.

875

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

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

878 @kwarg radius_wrap: Optional keyword arguments for function

879 L{pygeodesy.vincentys}.

880

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

882

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

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

885 '''

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

887

888

889__all__ += _ALL_OTHER(bounds, # functions

890 decode, decode2, decode_error, distance_,

891 encode, equirectangular_, euclidean_, haversine_,

892 neighbors, precision, resolution2, sizes, vincentys_)

893

894# **) MIT License

895#

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

897#

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

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

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

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

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

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

904#

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

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

907#

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

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

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

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

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

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

914# OTHER DEALINGS IN THE SOFTWARE.