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, _xStrError

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

37 from math import fabs, ldexp, log10, radians

38

39__all__ = _ALL_LAZY.geohash

40__version__ = '24.06.15'

41

42_formy = _MODS.into(formy=__name__)

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 _t = _floatuple

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

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

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

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

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

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

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

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

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

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

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

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

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

89

90_GH = _GH() # PYCHOK singleton

91_MaxPrec = 12

92

93

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

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

96 '''

97 if LatLon is None:

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

99 else:

100 kwds = _xkwds(LatLon_kwds, **name)

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

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

103 return r

104

105

106 def _2center(bounds):

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

108 '''

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

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

111

112

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

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

115 '''

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

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

118 Lon(lon, Error=GeohashError))

119

120

121 def _2Geohash(geohash):

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

123 '''

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

125 Geohash(geohash)

126

127

128 def _2geostr(geohash):

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

130 '''

131 try:

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

133 raise ValueError()

134 geostr = geohash.lower()

135 for c in geostr:

136 if c not in _GH.DecodedBase32:

137 raise ValueError()

138 return geostr

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

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

141

142

143 class Geohash(Str):

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

145 '''

146 # no str.__init__ in Python 3

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

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

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

150

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

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

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

154 some examples.

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

156

157 @return: New L{Geohash}.

158

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

160

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

162 '''

163 ll = None

164

165 if isinstance(cll, Geohash):

166 gh = _2geostr(str(cll))

167

168 elif isstr(cll):

169 if _COMMA_ in cll:

170 ll = _2fll(*parse3llh(cll))

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

172 else:

173 gh = _2geostr(cll)

174

175 else: # assume LatLon

176 try:

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

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

179 except AttributeError:

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

181

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

183 self._latlon = ll

184 return self

185

186 @deprecated_property_RO

187 def ab(self):

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

189 return self.philam

190

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

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

193

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

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

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

197

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

199

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

201 '''

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

203

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

205 if D not in _GH.Neighbors:

206 raise GeohashError(direction=direction)

207

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

209

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

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

212 if i < 0:

213 raise GeohashError(geohash=self)

214

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

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

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

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

219

220 n = self._name__(name)

221 if n:

222 n = _DOT_(n, D)

223 # append letter for direction to parent

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

225

226 @Property_RO

227 def _bounds(self):

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

229 '''

230 return bounds(self)

231

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

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

234 geohash cell.

235

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

237 or C{None}.

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

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

240

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

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

243 C{B{LatLon} is None},

244 '''

245 r = self._bounds

246 return r if LatLon is None else \

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

248

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

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

251 '''

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

253 return func_(*lls, **kwds)

254

255 def distanceTo(self, other):

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

257 based the cell sizes.

258

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

260

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

262

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

264 C{LatLon} or C{str}.

265 '''

266 other = _2Geohash(other)

267

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

269 if n:

270 for n in range(n):

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

272 break

273 return _GH.Sizes[n][2]

274

275 @deprecated_method

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

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

278 return self.distanceTo(other)

279

280 distance1 = distance1To

281

282 @deprecated_method

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

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

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

286

287 distance2 = distance2To

288

289 @deprecated_method

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

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

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

293

294 distance3 = distance3To

295

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

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

298 using function L{pygeodesy.equirectangular}.

299

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

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

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

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

304 @kwarg adjust_limit_wrap: Optional keyword arguments for function

305 L{pygeodesy.equirectangular4}, overriding defaults

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

307

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

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

310 or C{0}).

311

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

313 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 = _xkwds(adjust_limit_wrap, adjust=False, limit=None, wrap=False)

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

321 _formy.equirectangular4(*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(_formy.euclidean, other, **radius_adjust_wrap)

338

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

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

341 the L{pygeodesy.haversine} formula.

342

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

344 @kwarg radius_wrap: Optional keyword arguments for function

345 L{pygeodesy.haversine}.

346

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

348 ellipsoid or datum axes).

349

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

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

352 '''

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

354

355 @Property_RO

356 def latlon(self):

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

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

359 '''

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

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

362

363 @Property_RO

364 def neighbors(self):

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

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

367 '''

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

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

370 name=self.name)

371

372 @Property_RO

373 def philam(self):

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

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

376 '''

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

378

379 @Property_RO

380 def precision(self):

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

382 '''

383 return len(self)

384

385 @Property_RO

386 def sizes(self):

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

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

389 '''

390 z = _GH.Sizes

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

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

393

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

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

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

397

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

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

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

401

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

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

404

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

406 '''

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

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

409

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

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

412 the L{pygeodesy.vincentys} formula.

413

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

415 @kwarg radius_wrap: Optional keyword arguments for function

416 L{pygeodesy.vincentys}.

417

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

419 ellipsoid or datum axes).

420

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

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

423 '''

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

425

426 @Property_RO

427 def N(self):

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

429 '''

430 return self.adjacent(_N_)

431

432 @Property_RO

433 def S(self):

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

435 '''

436 return self.adjacent(_S_)

437

438 @Property_RO

439 def E(self):

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

441 '''

442 return self.adjacent(_E_)

443

444 @Property_RO

445 def W(self):

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

447 '''

448 return self.adjacent(_W_)

449

450 @Property_RO

451 def NE(self):

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

453 '''

454 return self.N.E

455

456 @Property_RO

457 def NW(self):

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

459 '''

460 return self.N.W

461

462 @Property_RO

463 def SE(self):

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

465 '''

466 return self.S.E

467

468 @Property_RO

469 def SW(self):

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

471 '''

472 return self.S.W

473

474

475 class GeohashError(_ValueError):

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

477 '''

478 pass

479

480

481 class Neighbors8Dict(_NamedDict):

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

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

484 '''

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

486

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

488 kwds = _xkwds(kwds, **_Neighbors8Defaults)

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

490

491

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

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

494

495

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

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

498

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

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

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

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

503

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

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

506 latN, lonE)}.

507

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

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

510

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

512 '''

513 gh = _2Geohash(geohash)

514 if len(gh) < 1:

515 raise GeohashError(geohash=geohash)

516

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

518 try:

519 d, _avg = True, favg

520 for c in gh.lower():

521 i = _GH.DecodedBase32[c]

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

523 if d: # longitude

524 a = _avg(w, e)

525 if (i & m):

526 w = a

527 else:

528 e = a

529 else: # latitude

530 a = _avg(s, n)

531 if (i & m):

532 s = a

533 else:

534 n = a

535 d = not d

536 except KeyError:

537 raise GeohashError(geohash=geohash)

538

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

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

541

542

543 def _bounds3(geohash):

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

545 '''

546 b = bounds(geohash)

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

548

549

550 def decode(geohash):

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

552 centre of) geohash cell to reasonable precision.

553

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

555

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

557

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

559 C{LatLon} or C{str}.

560

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

562 '''

563 b, h, w = _bounds3(geohash)

564 lat, lon = _2center(b)

565

566 # round to near centre without excessive precision to

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

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

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

570

571

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

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

574 of) geohash cell to reasonable precision.

575

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

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

578 or C{None}.

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

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

581

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

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

584

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

586 C{LatLon} or C{str}.

587

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

589 '''

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

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

592 return _xnamed(r, name__=decode2)

593

594

595 def decode_error(geohash):

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

597 this geohash.

598

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

600

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

602 longitudinal errors in (C{degrees}).

603

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

605 C{LatLon} or C{str}.

606

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

608 '''

609 _, h, w = _bounds3(geohash)

610 return LatLon2Tuple(h * _0_5, # Height error

611 w * _0_5) # Width error

612

613

614 def distance_(geohash1, geohash2):

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

616

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

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

619

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

621

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

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

624 '''

625 return _2Geohash(geohash1).distanceTo(geohash2)

626

627

628@deprecated_function

629 def distance1(geohash1, geohash2):

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

631 return distance_(geohash1, geohash2)

632

633

634@deprecated_function

635 def distance2(geohash1, geohash2):

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

637 return equirectangular4(geohash1, geohash2)

638

639

640@deprecated_function

641 def distance3(geohash1, geohash2):

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

643 return haversine_(geohash1, geohash2)

644

645

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

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

648 precision or if not provided, to an automatically evaluated

649 precision.

650

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

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

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

654 1..12).

655

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

657

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

659 '''

660 lat, lon = _2fll(lat, lon)

661

662 if precision is None:

663 # Infer precision by refining geohash until

664 # it matches precision of supplied lat/lon.

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

666 gh = encode(lat, lon, p)

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

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

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

670 return gh

671 p = _MaxPrec

672 else:

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

674

675 b = i = 0

676 d, gh = True, []

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

678

679 _avg = favg

680 while p > 0:

681 i += i

682 if d: # bisect longitude

683 m = _avg(e, w)

684 if lon < m:

685 e = m

686 else:

687 w = m

688 i += 1

689 else: # bisect latitude

690 m = _avg(n, s)

691 if lat < m:

692 n = m

693 else:

694 s = m

695 i += 1

696 d = not d

697

698 b += 1

699 if b == 5:

700 # 5 bits gives a character:

701 # append it and start over

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

703 b = i = 0

704 p -= 1

705

706 return NN.join(gh)

707

708

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

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

711 L{pygeodesy.equirectangular} formula.

712

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

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

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

716 L{Geohash.equirectangularTo}.

717

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

719 see method L{Geohash.equirectangularTo}.

720

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

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

723 '''

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

725

726

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

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

729 L{pygeodesy.euclidean} formula.

730

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

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

733 @kwarg radius_adjust_wrap: Optional keyword arguments for function

734 L{pygeodesy.euclidean}.

735

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

737

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

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

740 '''

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

742

743

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

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

746 using the L{pygeodesy.haversine} formula.

747

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

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

750 @kwarg radius_wrap: Optional keyword arguments for function

751 L{pygeodesy.haversine}.

752

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

754 B{C{radius}}).

755

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

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

758 '''

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

760

761

762 def neighbors(geohash):

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

764

765 @arg geohash: Cell for which neighbors are requested

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

767

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

769 of L{Geohash}es.

770

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

772 C{LatLon} or C{str}.

773 '''

774 return _2Geohash(geohash).neighbors

775

776

777 def precision(res1, res2=None):

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

779 (geographic) resolutions.

780

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

782 (C{degrees}).

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

784 resolution (C{degrees}).

785

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

787

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

789

790 @see: C++ class U{Geohash

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

792 '''

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

794 N = res2 is None

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

796 for p in range(1, _MaxPrec):

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

798 return p

799 return _MaxPrec

800

801

802 class Resolutions2Tuple(_NamedTuple):

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

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

805 '''

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

807 _Units_ = ( Degrees_, Degrees_)

808

809

810 def resolution2(prec1, prec2=None):

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

812 precisions.

813

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

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

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

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

818

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

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

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

822

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

824

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

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

827 '''

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

829

830 if prec1:

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

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

833

834 if prec2:

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

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

837

838 return Resolutions2Tuple(res1, res2)

839

840

841 def sizes(geohash):

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

843

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

845

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

847 longitudinal width in (C{meter}).

848

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

850 '''

851 return _2Geohash(geohash).sizes

852

853

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

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

856 L{pygeodesy.vincentys} formula.

857

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

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

860 @kwarg radius_wrap: Optional keyword arguments for function

861 L{pygeodesy.vincentys}.

862

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

864

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

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

867 '''

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

869

870

871__all__ += _ALL_OTHER(bounds, # functions

872 decode, decode2, decode_error, distance_,

873 encode, equirectangular4, euclidean_, haversine_,

874 neighbors, precision, resolution2, sizes, vincentys_)

875

876# **) MIT License

877#

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

879#

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

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

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

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

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

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

886#

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

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

889#

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

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

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

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

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

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

896# OTHER DEALINGS IN THE SOFTWARE.