pygeodesy 24.7.7__py2.py3-none-any.whl → 24.8.4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeodesy/geohash.py CHANGED
@@ -1,93 +1,47 @@
1
1
 
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- u'''Geohash en-/decoding.
4
+ u'''I{Gustavo Niemeyer}’s U{Geohash<https://WikiPedia.org/wiki/Geohash>}.
5
5
 
6
- Classes L{Geohash} and L{GeohashError} and several functions to encode,
7
- decode and inspect I{geohashes}.
6
+ Class L{Geohash} and several functions to encode, decode and inspect
7
+ C{geohashes} and optional L{Geohashed} caches.
8
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>}.
9
+ Originally transcoded from JavaScript originals by I{(C) Chris Veness
10
+ 2011-2015} and published under the same MIT Licence**, see
11
+ U{Geohashes<https://www.Movable-Type.co.UK/scripts/geohash.html>}.
12
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>}.
13
+ @see: U{Geohash<https://WikiPedia.org/wiki/Geohash>}, I{Karney}'s C++
14
+ U{Geohash<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>},
15
+ U{geohash<https://GitHub.com/vinsci/geohash>},
16
+ U{pygeohash<https://PyPI.org/project/pygeohash>} and
17
+ U{geohash-js<https://GitHub.com/DaveTroy/geohash-js>}.
17
18
  '''
18
19
 
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
20
+ from pygeodesy.basics import isstr, map2
21
+ from pygeodesy.constants import EPS, R_M, _0_0, _0_5, _180_0, _360_0, \
22
+ _90_0, _N_90_0, _N_180_0 # PYCHOK used!
23
23
  from pygeodesy.errors import _ValueError, _xkwds, _xStrError
24
- from pygeodesy.fmath import favg
25
24
  # 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
25
+ from pygeodesy.interns import NN, _COMMA_, _DOT_, _E_, _height_, _N_, _NE_, \
26
+ _NW_, _radius_, _S_, _SE_, _SPACE_, _SW_, _W_, \
27
+ _width_
28
+ from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
29
29
  from pygeodesy.named import _name__, _NamedDict, _NamedTuple, nameof, _xnamed
30
30
  from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, LatLon2Tuple, \
31
31
  PhiLam2Tuple
32
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
33
+ deprecated_property_RO, Property_RO, \
34
+ property_RO, property_ROver
35
+ # from pygeodesy.streprs import Fmt, fstr # _MODS
36
+ from pygeodesy.units import Degrees_, Int, Lat_, Lon_, Meter, Precision_, Str
36
37
 
37
38
  from math import fabs, ldexp, log10, radians
38
39
 
39
40
  __all__ = _ALL_LAZY.geohash
40
- __version__ = '24.06.15'
41
+ __version__ = '24.08.02'
41
42
 
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
43
+ _formy = _MODS.into(formy=__name__)
44
+ _MASK5 = 16, 8, 4, 2, 1 # PYCHOK used!
91
45
  _MaxPrec = 12
92
46
 
93
47
 
@@ -106,16 +60,22 @@ def _2bounds(LatLon, LatLon_kwds, s, w, n, e, **name):
106
60
  def _2center(bounds):
107
61
  '''(INTERNAL) Return the C{bounds} center.
108
62
  '''
109
- return (favg(bounds.latN, bounds.latS),
110
- favg(bounds.lonE, bounds.lonW))
63
+ return (_2mid(bounds.latN, bounds.latS),
64
+ _2mid(bounds.lonE, bounds.lonW))
65
+
66
+
67
+ def _2dab(d, a, b):
68
+ '''(INTERNAL) Get delta lat or lon from center.
69
+ '''
70
+ return fabs(d - round(*_2mid_ndigits(a, b)))
111
71
 
112
72
 
113
73
  def _2fll(lat, lon, *unused):
114
74
  '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
115
75
  '''
116
76
  # lat, lon = parseDMS2(lat, lon)
117
- return (Lat(lat, Error=GeohashError),
118
- Lon(lon, Error=GeohashError))
77
+ return (Lat_(lat, Error=GeohashError),
78
+ Lon_(lon, Error=GeohashError))
119
79
 
120
80
 
121
81
  def _2Geohash(geohash):
@@ -125,61 +85,226 @@ def _2Geohash(geohash):
125
85
  Geohash(geohash)
126
86
 
127
87
 
128
- def _2geostr(geohash):
129
- '''(INTERNAL) Check a geohash string.
88
+ def _2latlon(s, w, n, e, fstr=None):
89
+ '''(INTERNAL) Get the center C{lat, lon}, rounded.
130
90
  '''
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)
91
+ lat, a = _2mid_ndigits(n, s)
92
+ lon, b = _2mid_ndigits(e, w)
93
+ return (fstr(lat, prec=a), fstr(lon, prec=b)) if fstr else \
94
+ (round(lat, a), round(lon, b))
95
+
96
+
97
+ def _2mid(a, b):
98
+ '''(INTERNAL) Bisect C{a} to C{b}.
99
+ '''
100
+ return (a + b) * _0_5 # favg
101
+
102
+
103
+ def _2mid_ndigits(a, b): # a > b
104
+ '''(INTERNAL) Return 2-tuple C{(_2mid, ndigits)}.
105
+ '''
106
+ # round to near centre without excessive
107
+ # precision to ⌊2-log10(Δ°)⌋ ndigits
108
+ return _2mid(a, b), int(2 - log10(a - b))
109
+
110
+
111
+ def _2Precision(p):
112
+ '''(INTERNAL) Get a valid C{Precision}.
113
+ '''
114
+ return Precision_(p, low=1, high=_MaxPrec, Error=GeohashError)
115
+
116
+
117
+ def _2res(res, **prec):
118
+ '''(INTERNAL) Get the C{res}olution for a C{prec}ision.
119
+ '''
120
+ p = max(min(Int(Error=GeohashError, **prec), _MaxPrec), 0) * 5
121
+ p = (p - p // 2) if res > _180_0 else (p // 2)
122
+ return ldexp(res, -p) if p else res
123
+
124
+
125
+ class _GH(object):
126
+ '''(INTERNAL) Lazily defined constants.
127
+ '''
128
+ def _4d(self, s, w, n, e): # helper
129
+ return dict(S=(s, w), W=(w, s),
130
+ N=(n, e), E=(e, n))
131
+
132
+ @property_ROver
133
+ def Borders(self):
134
+ return self._4d('028b', '0145hjnp', 'prxz', 'bcfguvyz')
135
+
136
+ @property_ROver
137
+ def DecodeB32(self): # inverse EncodeB32 map
138
+ return dict((c, i) for i, c in enumerate(self.EncodeB32))
139
+
140
+ def decode2(self, geohash):
141
+ '''Decode C{geohash} to 2-tuple C{(lat, lon)}.
142
+ '''
143
+ swne = self.swne4(geohash)
144
+ return _2latlon(*swne)
145
+
146
+ # Geohash's base32 codes, no a, i, l and o
147
+ EncodeB32 = '0123456789bcdefghjkmnpqrstuvwxyz'
148
+
149
+ def encode(self, *lat_lon_prec_eps):
150
+ '''Encode C{lat, lon} to C{prec}ision or C{eps}.
151
+ '''
152
+ def _encodes(lat, lon, prec, eps=0):
153
+ s, w, n, e = self.SWNE4
154
+ E, d, _mid = self.EncodeB32, True, _2mid
155
+ for _ in range(prec):
156
+ i = 0
157
+ for _ in range(5): # len(_MASK5)
158
+ i += i
159
+ if d: # bisect longitude
160
+ a = _mid(e, w)
161
+ if lon < a:
162
+ e = a
163
+ else:
164
+ w = a
165
+ i += 1
166
+ else: # bisect latitude
167
+ a = _mid(n, s)
168
+ if lat < a:
169
+ n = a
170
+ else:
171
+ s = a
172
+ i += 1
173
+ d = not d
174
+ yield E[i]
175
+ if eps > 0: # infer prec
176
+ if _2dab(lon, e, w) < eps and \
177
+ _2dab(lat, n, s) < eps:
178
+ break
179
+
180
+ return NN.join(_encodes(*lat_lon_prec_eps))
181
+
182
+ def encode2(self, lat, lon, prec, eps):
183
+ '''Return 2-tuple C{geohash, (lat, lon))}.
184
+ '''
185
+ lat, lon = _2fll(lat, lon)
186
+ if prec:
187
+ p, e = _2Precision(prec), 0
188
+ else: # infer precision by refining geohash
189
+ p, e = _MaxPrec, max(eps, EPS)
190
+ return self.encode(lat, lon, p, e), (lat, lon)
191
+
192
+ @property_ROver
193
+ def _LatLon2Tuple(self):
194
+
195
+ class _LatLon2Tuple(_NamedTuple):
196
+ '''DEPRECATED on 2024.07.28, C{(lat, lon)} in B{C{meter}}, use L{Sizes3Tuple}.'''
197
+ _Names_ = LatLon2Tuple._Names_
198
+ _Units_ = Meter, Meter
199
+
200
+ return _LatLon2Tuple
201
+
202
+ @property_ROver
203
+ def Neighbors(self):
204
+ return self._4d('14365h7k9dcfesgujnmqp0r2twvyx8zb',
205
+ '238967debc01fg45kmstqrwxuvhjyznp',
206
+ 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',
207
+ 'bc01fg45238967deuvhjyznpkmstqrwx')
208
+
209
+ @property_ROver
210
+ def Sizes(self): # height, width and radius (in meter)
211
+ # where radius = sqrt(height * width / PI), the
212
+ # radius of a circle with area (height * width)
213
+ T = Sizes3Tuple
214
+ return (T(20000e3, 20032e3, 11292815.096), # 0
215
+ T( 5000e3, 5003e3, 2821794.075), # 1
216
+ T( 650e3, 1225e3, 503442.397), # 2
217
+ T( 156e3, 156e3, 88013.575), # 3
218
+ T( 19500, 39100, 15578.683), # 4
219
+ T( 4890, 4890, 2758.887), # 5
220
+ T( 610, 1220, 486.710), # 6
221
+ T( 153, 153, 86.321), # 7
222
+ T( 19.1, 38.2, 15.239), # 8
223
+ T( 4.77, 4.77, 2.691), # 9
224
+ T( 0.596, 1.19, 0.475), # 10
225
+ T( 0.149, 0.149, 0.084), # 11
226
+ T( 0.0186, 0.0372, 0.015)) # 12 _MaxPrec
227
+
228
+ SWNE4 = (_N_90_0, _N_180_0, _90_0, _180_0)
229
+
230
+ def swne4(self, geohash, mask5=_MASK5):
231
+ '''Decode C{geohash} into 4-tuple C{(s, w, n, e)}.
232
+ '''
233
+ nc = len(geohash) if isstr(geohash) else 0
234
+ if not (0 < nc <= _MaxPrec):
235
+ raise GeohashError(geohash=geohash, len=nc)
236
+ s, w, n, e = self.SWNE4
237
+ D, d, _mid = self.DecodeB32, True, _2mid
238
+ try:
239
+ for j, c in enumerate(geohash.lower()):
240
+ i = D[c]
241
+ for m in mask5:
242
+ if d: # longitude
243
+ a = _mid(e, w)
244
+ if (i & m):
245
+ w = a
246
+ else:
247
+ e = a
248
+ else: # latitude
249
+ a = _mid(n, s)
250
+ if (i & m):
251
+ s = a
252
+ else:
253
+ n = a
254
+ d = not d
255
+ except KeyError:
256
+ c = _MODS.streprs.Fmt.INDEX(repr(c), j)
257
+ raise GeohashError(geohash=geohash, len=nc, txt=c)
258
+ return s, w, n, e
259
+
260
+ _GH = _GH() # PYCHOK singleton
141
261
 
142
262
 
143
263
  class Geohash(Str):
144
264
  '''Geohash class, a named C{str}.
145
265
  '''
146
266
  # 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.
267
+ def __new__(cls, lat_ghll, lon=None, precision=None, eps=EPS, **name):
268
+ '''New L{Geohash} from an other L{Geohash} instance or geohash C{str}
269
+ or from a lat- and longitude.
270
+
271
+ @arg lat_ghll: Latitude (C{degrees90}), a geohash (L{Geohash},
272
+ C{str}) or a location (C{LatLon}, C{LatLon*Tuple}).
273
+ @kwarg lon: Logitude (C{degrees180)}, required if B{C{lat_ghll}}
274
+ is C{degrees90}, ignored otherwise.
275
+ @kwarg precision: The desired geohash length (C{int} 1..12) or
276
+ C{None} or C{0}, see L{encode<pygeodesy.geohash.encode>}.
277
+ @kwarg eps: Optional inference tolerance (C{degrees}), see
278
+ L{encode<pygeodesy.geohash.encode>}.
155
279
  @kwarg name: Optional C{B{name}=NN} (C{str}).
156
280
 
157
281
  @return: New L{Geohash}.
158
282
 
159
- @raise GeohashError: INValid or non-alphanumeric B{C{cll}}.
283
+ @raise GeohashError: Invalid B{C{lat_ghll}}.
160
284
 
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)
285
+ @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}.
181
286
 
182
- self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=cll))
287
+ @raise TypeError: Invalid B{C{lat_ghll}}.
288
+ '''
289
+ if lon is None:
290
+ if isinstance(lat_ghll, Geohash):
291
+ gh, ll = str(lat_ghll), lat_ghll.latlon
292
+ elif isstr(lat_ghll): # "lat, lon" or "geohash"
293
+ ll = lat_ghll.replace(_COMMA_, _SPACE_).split()
294
+ if len(ll) > 1:
295
+ gh, ll = _GH.encode2(ll[0], ll[1], precision, eps)
296
+ else:
297
+ gh, ll = lat_ghll.lower(), None
298
+ _ = _GH.swne4(gh, mask5=()) # validate
299
+ else: # assume LatLon
300
+ try:
301
+ gh, ll = _GH.encode2(lat_ghll.lat, lat_ghll.lon, precision, eps)
302
+ except AttributeError:
303
+ raise _xStrError(Geohash, ghll=lat_ghll, Error=GeohashError)
304
+ else:
305
+ gh, ll = _GH.encode2(lat_ghll, lon, precision, eps)
306
+
307
+ self = Str.__new__(cls, gh, name=_name__(name, _or_nameof=lat_ghll))
183
308
  self._latlon = ll
184
309
  return self
185
310
 
@@ -205,7 +330,7 @@ class Geohash(Str):
205
330
  if D not in _GH.Neighbors:
206
331
  raise GeohashError(direction=direction)
207
332
 
208
- e = 1 if isodd(len(self)) else 0
333
+ e = len(self) & 1 # int(isodd(len(self)))
209
334
 
210
335
  c = self[-1:] # last hash char
211
336
  i = _GH.Neighbors[D][e].find(c)
@@ -221,7 +346,7 @@ class Geohash(Str):
221
346
  if n:
222
347
  n = _DOT_(n, D)
223
348
  # append letter for direction to parent
224
- return Geohash(p + _GH.GeohashBase32[i], name=n)
349
+ return Geohash(p + _GH.EncodeB32[i], name=n)
225
350
 
226
351
  @Property_RO
227
352
  def _bounds(self):
@@ -270,7 +395,7 @@ class Geohash(Str):
270
395
  for n in range(n):
271
396
  if self[n] != other[n]:
272
397
  break
273
- return _GH.Sizes[n][2]
398
+ return _GH.Sizes[n].radius
274
399
 
275
400
  @deprecated_method
276
401
  def distance1To(self, other): # PYCHOK no cover
@@ -382,14 +507,20 @@ class Geohash(Str):
382
507
  '''
383
508
  return len(self)
384
509
 
385
- @Property_RO
510
+ @deprecated_property_RO
386
511
  def sizes(self):
387
- '''Get the lat- and longitudinal size of this cell as
388
- a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}).
512
+ '''DEPRECATED on 2024.07.28, use property C{Geohash.sizes3}.'''
513
+ t = self.sizes3
514
+ return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
515
+
516
+ @Property_RO
517
+ def sizes3(self):
518
+ '''Get the lat-, longitudinal and radial size of this cell as
519
+ a L{Sizes3Tuple}C{(height, width, radius)}, all C{meter}.
389
520
  '''
390
521
  z = _GH.Sizes
391
522
  n = min(len(z) - 1, max(self.precision, 1))
392
- return LatLon2Tuple(z[n][:2], name=self.name) # *z XXX Height, Width?
523
+ return Sizes3Tuple(z[n], name=self.name)
393
524
 
394
525
  def toLatLon(self, LatLon=None, **LatLon_kwds):
395
526
  '''Return (the approximate center of) this geohash cell
@@ -423,18 +554,6 @@ class Geohash(Str):
423
554
  '''
424
555
  return self._distanceTo(_formy.vincentys, other, **radius_wrap)
425
556
 
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
557
  @Property_RO
439
558
  def E(self):
440
559
  '''Get the cell East of this (L{Geohash}).
@@ -442,10 +561,10 @@ class Geohash(Str):
442
561
  return self.adjacent(_E_)
443
562
 
444
563
  @Property_RO
445
- def W(self):
446
- '''Get the cell West of this (L{Geohash}).
564
+ def N(self):
565
+ '''Get the cell North of this (L{Geohash}).
447
566
  '''
448
- return self.adjacent(_W_)
567
+ return self.adjacent(_N_)
449
568
 
450
569
  @Property_RO
451
570
  def NE(self):
@@ -459,6 +578,12 @@ class Geohash(Str):
459
578
  '''
460
579
  return self.N.W
461
580
 
581
+ @Property_RO
582
+ def S(self):
583
+ '''Get the cell South of this (L{Geohash}).
584
+ '''
585
+ return self.adjacent(_S_)
586
+
462
587
  @Property_RO
463
588
  def SE(self):
464
589
  '''Get the cell SouthEast of this (L{Geohash}).
@@ -471,6 +596,124 @@ class Geohash(Str):
471
596
  '''
472
597
  return self.S.W
473
598
 
599
+ @Property_RO
600
+ def W(self):
601
+ '''Get the cell West of this (L{Geohash}).
602
+ '''
603
+ return self.adjacent(_W_)
604
+
605
+
606
+ class Geohashed(object):
607
+ '''A cache of en- and decoded geohashes of one precision.
608
+ '''
609
+ _nn = None, # 1-tuple
610
+
611
+ def __init__(self, precision, ndigits=None):
612
+ '''New L{Geohashed} cache.
613
+
614
+ @arg precision: The geohash encoded length (C{int}, 1..12).
615
+ @kwarg ndigits: Optional number of digits to round C{lat}
616
+ and C{lon} to cache keys (C{int}, typically
617
+ C{B{ndigits}=B{precision}}) or C{None} for
618
+ no rounding.
619
+ '''
620
+ self._p = _2Precision(precision)
621
+ if ndigits is None:
622
+ self._ab2 = self._ab2float
623
+ else:
624
+ self._ab2 = self._ab2round
625
+ n = Int(ndigits=ndigits)
626
+ self._nn = n, n
627
+ self.clear()
628
+
629
+ def __len__(self):
630
+ '''Return the number of I{unigue} geohashes (C{int}).
631
+ '''
632
+ d = self._d
633
+ d = set(d.keys())
634
+ n = len(d)
635
+ for e in self._e.values():
636
+ e = set(e.values())
637
+ n += len(e - d)
638
+ return n
639
+
640
+ def _ab2(self, *ll): # overwritten
641
+ '''(INTERNAL) Make encoded keys C{a, b}.
642
+ '''
643
+ return ll
644
+
645
+ def _ab2float(self, *ll):
646
+ '''(INTERNAL) Make encoded keys C{a, b}.
647
+ '''
648
+ return map(float, ll)
649
+
650
+ def _ab2round(self, *ll):
651
+ '''(INTERNAL) Make encoded keys C{a, b}.
652
+ '''
653
+ return map(round, ll, self._nn)
654
+
655
+ def clear(self):
656
+ '''Clear the C{en-} and C{decoded} cache.
657
+ '''
658
+ self._e = {}
659
+ self._d = {}
660
+
661
+ def decoded(self, geohash, encoded=False):
662
+ '''Get and cache the C{(lat, lon)} for C{geohash}, see L{decode<pygeodesy.geohash.decode>}.
663
+
664
+ @kwarg encoded: If C{True}, cache the result as C{encoded}.
665
+
666
+ @return: The C{(lat, lon}) pair for C{geohash}.
667
+ '''
668
+ try:
669
+ ll = self._d[geohash]
670
+ except KeyError:
671
+ self._d[geohash] = ll = _GH.decode2(geohash)
672
+ if encoded:
673
+ a, b = self._ab2(*ll)
674
+ try:
675
+ _ = self._e[b][a]
676
+ except KeyError:
677
+ self._e.setdefault(b, {})[a] = geohash
678
+ return ll
679
+
680
+ def encoded(self, lat, lon, decoded=False):
681
+ '''Get and cache the C{geohash} for C{(lat, lon)}, see L{encode<pygeodesy.geohash.encode>}.
682
+
683
+ @kwarg decoded: If C{True}, cache the result as C{decoded}.
684
+
685
+ @return: The C{geohash} for pair C{(lat, lon}).
686
+ '''
687
+ lat, lon = ll = _2fll(lat, lon)
688
+ a, b = self._ab2(*ll)
689
+ try:
690
+ gh = self._e[b][a]
691
+ except KeyError:
692
+ gh = _GH.encode(lat, lon, self._p, 0)
693
+ self._e.setdefault(b, {})[a] = gh
694
+ if decoded and gh not in self._d:
695
+ self._d[gh] = ll
696
+ return gh
697
+
698
+ @property_RO
699
+ def len2(self):
700
+ '''Return 2-tuple C{(lencoded, ldecoded)} with the C{len}gths of the
701
+ C{en-} and C{decoded} cache.
702
+ '''
703
+ return sum(len(e) for e in self._e.values()), len(self._d)
704
+
705
+ @Property_RO
706
+ def ndigits(self):
707
+ '''Get the rounding (C{int} or C{None}).
708
+ '''
709
+ return self._nn[0]
710
+
711
+ @Property_RO
712
+ def precision(self):
713
+ '''Get the C{precision} (C{int}).
714
+ '''
715
+ return self._p
716
+
474
717
 
475
718
  class GeohashError(_ValueError):
476
719
  '''Geohash encode, decode or other L{Geohash} issue.
@@ -493,6 +736,34 @@ _Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
493
736
  len(Neighbors8Dict._Keys_))) # XXX frozendict
494
737
 
495
738
 
739
+ class Resolutions2Tuple(_NamedTuple):
740
+ '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
741
+ secondary I{(latitudinal)} resolution, both in C{degrees}.
742
+ '''
743
+ _Names_ = ('res1', 'res2')
744
+ _Units_ = ( Degrees_, Degrees_)
745
+
746
+ @property_RO
747
+ def lat(self):
748
+ '''Get the secondary, latitudinal resolution (C{degrees}).
749
+ '''
750
+ return self[1]
751
+
752
+ @property_RO
753
+ def lon(self):
754
+ '''Get the primary, longitudinal resolution (C{degrees}).
755
+ '''
756
+ return self[0]
757
+
758
+
759
+ class Sizes3Tuple(_NamedTuple):
760
+ '''3-Tuple C{(height, width, radius)} with latitudinal C{height},
761
+ longitudinal C{width} and area C{radius}, all in C{meter}.
762
+ '''
763
+ _Names_ = (_height_, _width_, _radius_)
764
+ _Units_ = ( Meter, Meter, Meter)
765
+
766
+
496
767
  def bounds(geohash, LatLon=None, **LatLon_kwds):
497
768
  '''Returns the lower-left SW and upper-right NE corners of a geohash.
498
769
 
@@ -510,43 +781,11 @@ def bounds(geohash, LatLon=None, **LatLon_kwds):
510
781
 
511
782
  @raise GeohashError: Invalid or C{null} B{C{geohash}}.
512
783
  '''
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,
784
+ swne = _GH.swne4(geohash)
785
+ return _2bounds(LatLon, LatLon_kwds, *swne,
540
786
  name=nameof(geohash)) # _or_nameof=geohash
541
787
 
542
788
 
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
789
  def decode(geohash):
551
790
  '''Decode a geohash to lat-/longitude of the (approximate
552
791
  centre of) geohash cell to reasonable precision.
@@ -560,13 +799,10 @@ def decode(geohash):
560
799
 
561
800
  @raise GeohashError: Invalid or null B{C{geohash}}.
562
801
  '''
563
- b, h, w = _bounds3(geohash)
564
- lat, lon = _2center(b)
565
-
566
802
  # round to near centre without excessive precision to
567
803
  # ⌊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!
804
+ swne = _GH.swne4(geohash)
805
+ return _2latlon(*swne, fstr=_MODS.streprs.fstr)
570
806
 
571
807
 
572
808
  def decode2(geohash, LatLon=None, **LatLon_kwds):
@@ -587,14 +823,20 @@ def decode2(geohash, LatLon=None, **LatLon_kwds):
587
823
 
588
824
  @raise GeohashError: Invalid or null B{C{geohash}}.
589
825
  '''
590
- t = map2(float, decode(geohash))
591
- r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t
826
+ ll = _GH.decode2(geohash)
827
+ r = LatLon2Tuple(ll) if LatLon is None else \
828
+ LatLon( *ll, **LatLon_kwds)
592
829
  return _xnamed(r, name__=decode2)
593
830
 
594
831
 
832
+ @deprecated_function
595
833
  def decode_error(geohash):
596
- '''Return the relative lat-/longitude decoding errors for
597
- this geohash.
834
+ '''DEPRECATED on 2024.07.28, use L{geohash.decode_error2}.'''
835
+ return decode_error2(geohash)
836
+
837
+
838
+ def decode_error2(geohash):
839
+ '''Return the lat- and longitude decoding error for a geohash.
598
840
 
599
841
  @arg geohash: To be decoded (L{Geohash}).
600
842
 
@@ -606,9 +848,9 @@ def decode_error(geohash):
606
848
 
607
849
  @raise GeohashError: Invalid or null B{C{geohash}}.
608
850
  '''
609
- _, h, w = _bounds3(geohash)
610
- return LatLon2Tuple(h * _0_5, # Height error
611
- w * _0_5) # Width error
851
+ s, w, n, e = _GH.swne4(geohash)
852
+ return LatLon2Tuple((n - s) * _0_5, # lat error
853
+ (e - w) * _0_5) # lon error
612
854
 
613
855
 
614
856
  def distance_(geohash1, geohash2):
@@ -643,67 +885,23 @@ def distance3(geohash1, geohash2):
643
885
  return haversine_(geohash1, geohash2)
644
886
 
645
887
 
646
- def encode(lat, lon, precision=None):
888
+ def encode(lat, lon, precision=None, eps=EPS):
647
889
  '''Encode a lat-/longitude as a C{geohash}, either to the specified
648
- precision or if not provided, to an automatically evaluated
649
- precision.
890
+ precision or if not provided, to an inferred precision.
650
891
 
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).
892
+ @arg lat: Latitude (C{degrees90}).
893
+ @arg lon: Longitude (C{degrees180}).
894
+ @kwarg precision: The desired geohash length (C{int} 1..12) or
895
+ C{None} or C{0} for inferred.
896
+ @kwarg eps: Optional inference tolerance (C{degrees}), ignored
897
+ if B{C{precision}} is not C{None} or C{0}.
655
898
 
656
899
  @return: The C{geohash} (C{str}).
657
900
 
658
901
  @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
659
902
  '''
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)
903
+ gh, _ = _GH.encode2(lat, lon, precision, eps)
904
+ return gh
707
905
 
708
906
 
709
907
  def equirectangular4(geohash1, geohash2, radius=R_M):
@@ -799,14 +997,6 @@ def precision(res1, res2=None):
799
997
  return _MaxPrec
800
998
 
801
999
 
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
1000
  def resolution2(prec1, prec2=None):
811
1001
  '''Determine the (geographic) resolutions of given L{Geohash}
812
1002
  precisions.
@@ -817,38 +1007,37 @@ def resolution2(prec1, prec2=None):
817
1007
  (C{int} 1..12).
818
1008
 
819
1009
  @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.
1010
+ (geographic) resolutions C{degrees}, where C{res2
1011
+ B{is} res1} if no B{C{prec2}} is given.
822
1012
 
823
1013
  @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
824
1014
 
825
1015
  @see: I{Karney}'s C++ class U{Geohash
826
1016
  <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
827
1017
  '''
828
- res1, res2 = _360_0, _180_0 # note ... lon, lat!
1018
+ lon = _2res(_360_0, prec1=prec1)
1019
+ lat = lon if prec2 is None else \
1020
+ _2res(_180_0, prec2=prec2)
1021
+ return Resolutions2Tuple(lon, lat)
829
1022
 
830
- if prec1:
831
- p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec))
832
- res1 = res2 = ldexp(res1, -(p - p // 2))
833
1023
 
834
- if prec2:
835
- p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec))
836
- res2 = ldexp(res2, -(p // 2))
1024
+ @deprecated_function
1025
+ def sizes(geohash):
1026
+ '''DEPRECATED on 2024.07.28, use function L{pygeodesy.geohash.sizes3}.'''
1027
+ t = sizes3(geohash)
1028
+ return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
837
1029
 
838
- return Resolutions2Tuple(res1, res2)
839
1030
 
840
-
841
- def sizes(geohash):
842
- '''Return the lat- and longitudinal size of this L{Geohash} cell.
1031
+ def sizes3(geohash):
1032
+ '''Return the lat-, longitudinal and radial size of this L{Geohash} cell.
843
1033
 
844
1034
  @arg geohash: Cell for which size are required (L{Geohash} or C{str}).
845
1035
 
846
- @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal height and
847
- longitudinal width in (C{meter}).
1036
+ @return: A L{Sizes3Tuple}C{(height, width, radius)}, all C{meter}.
848
1037
 
849
1038
  @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or C{str}.
850
1039
  '''
851
- return _2Geohash(geohash).sizes
1040
+ return _2Geohash(geohash).sizes3
852
1041
 
853
1042
 
854
1043
  def vincentys_(geohash1, geohash2, **radius_wrap):
@@ -868,10 +1057,38 @@ def vincentys_(geohash1, geohash2, **radius_wrap):
868
1057
  return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap)
869
1058
 
870
1059
 
871
- __all__ += _ALL_OTHER(bounds, # functions
872
- decode, decode2, decode_error, distance_,
873
- encode, equirectangular4, euclidean_, haversine_,
874
- neighbors, precision, resolution2, sizes, vincentys_)
1060
+ __all__ += _ALL_DOCS(bounds, # functions
1061
+ decode, decode2, decode_error2, distance_,
1062
+ encode, equirectangular4, euclidean_, haversine_,
1063
+ neighbors, precision, resolution2, sizes3, vincentys_,
1064
+ decode_error, sizes) # DEPRECATED
1065
+
1066
+ if __name__ == '__main__':
1067
+
1068
+ from pygeodesy.internals import printf, _versions
1069
+ from timeit import timeit
1070
+
1071
+ for f, p in (('encode', _MaxPrec), ('infer', None)):
1072
+
1073
+ def _t(prec=p):
1074
+ i = 0
1075
+ for lat in range(-90, 90, 3):
1076
+ for lon in range(-180, 180, 7):
1077
+ _ = encode(lat, lon, prec)
1078
+ i += 1
1079
+ return i
1080
+
1081
+ i = _t() # prime
1082
+ n = 10
1083
+ t = timeit(_t, number=n) / (i * n)
1084
+ printf('%s %.3f usec, %s', f, t * 1e6, _versions())
1085
+
1086
+ # % python3.12 -m pygeodesy.geohash
1087
+ # encode 10.145 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1088
+ # infer 14.780 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1089
+ # or about 6.56 and 74.12 times faster than pygeodesy 24.7.24 and older:
1090
+ # encode 66.524 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
1091
+ # infer 1095.386 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
875
1092
 
876
1093
  # **) MIT License
877
1094
  #