pygeodesy 24.7.24__py2.py3-none-any.whl → 24.8.24__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.
Files changed (57) hide show
  1. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/METADATA +20 -19
  2. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/RECORD +57 -57
  3. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/WHEEL +1 -1
  4. pygeodesy/__init__.py +26 -27
  5. pygeodesy/auxilats/auxAngle.py +2 -2
  6. pygeodesy/auxilats/auxDST.py +3 -3
  7. pygeodesy/azimuthal.py +4 -4
  8. pygeodesy/basics.py +3 -3
  9. pygeodesy/cartesianBase.py +6 -6
  10. pygeodesy/constants.py +11 -11
  11. pygeodesy/css.py +5 -5
  12. pygeodesy/ellipsoidalBase.py +18 -15
  13. pygeodesy/ellipsoidalExact.py +2 -2
  14. pygeodesy/ellipsoidalGeodSolve.py +2 -2
  15. pygeodesy/ellipsoidalKarney.py +2 -2
  16. pygeodesy/ellipsoidalNvector.py +2 -2
  17. pygeodesy/ellipsoidalVincenty.py +7 -6
  18. pygeodesy/ellipsoids.py +3 -3
  19. pygeodesy/epsg.py +3 -3
  20. pygeodesy/fmath.py +2 -1
  21. pygeodesy/formy.py +2 -2
  22. pygeodesy/fsums.py +4 -4
  23. pygeodesy/gars.py +66 -58
  24. pygeodesy/geodesici.py +4 -10
  25. pygeodesy/geodesicx/gx.py +3 -3
  26. pygeodesy/geodesicx/gxarea.py +3 -3
  27. pygeodesy/geodsolve.py +3 -3
  28. pygeodesy/geohash.py +491 -267
  29. pygeodesy/geoids.py +298 -316
  30. pygeodesy/heights.py +176 -194
  31. pygeodesy/internals.py +39 -6
  32. pygeodesy/interns.py +2 -3
  33. pygeodesy/karney.py +2 -2
  34. pygeodesy/latlonBase.py +14 -8
  35. pygeodesy/lazily.py +22 -21
  36. pygeodesy/ltp.py +6 -7
  37. pygeodesy/ltpTuples.py +12 -6
  38. pygeodesy/named.py +5 -4
  39. pygeodesy/namedTuples.py +14 -1
  40. pygeodesy/osgr.py +7 -7
  41. pygeodesy/points.py +2 -2
  42. pygeodesy/resections.py +7 -7
  43. pygeodesy/rhumb/solve.py +3 -3
  44. pygeodesy/simplify.py +10 -10
  45. pygeodesy/sphericalBase.py +3 -3
  46. pygeodesy/sphericalTrigonometry.py +2 -2
  47. pygeodesy/streprs.py +3 -3
  48. pygeodesy/triaxials.py +210 -204
  49. pygeodesy/units.py +36 -19
  50. pygeodesy/unitsBase.py +4 -4
  51. pygeodesy/utmupsBase.py +3 -3
  52. pygeodesy/vector2d.py +158 -51
  53. pygeodesy/vector3d.py +13 -52
  54. pygeodesy/vector3dBase.py +81 -63
  55. pygeodesy/webmercator.py +3 -3
  56. pygeodesy/wgrs.py +109 -101
  57. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/top_level.txt +0 -0
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_ # _INV_
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.05'
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.
90
+ '''
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.
130
119
  '''
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)
120
+ p = max(min(Int(Error=GeohashError, **prec), _MaxPrec), 0) * 5
121
+ x = (p - p // 2) if res > _180_0 else (p // 2)
122
+ return ldexp(res, -x) if x else res # ldexp == res / float(1 << x)
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): # or geohash.startswith(_INV_)
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
285
+ @raise RangeError: Invalid B{C{lat_gll}} or B{C{lon}}.
164
286
 
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))
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
@@ -383,13 +508,26 @@ class Geohash(Str):
383
508
  return len(self)
384
509
 
385
510
  @Property_RO
511
+ def resolution2(self):
512
+ '''Get the I{lon-} and I{latitudinal} resolution of this cell
513
+ in a L{Resolutions2Tuple}C{(res1, res2)}, both in C{degrees}.
514
+ '''
515
+ return resolution2(self.precision, self.precision)
516
+
517
+ @deprecated_property_RO
386
518
  def sizes(self):
387
- '''Get the lat- and longitudinal size of this cell as
388
- a L{LatLon2Tuple}C{(lat, lon)} in (C{meter}).
519
+ '''DEPRECATED on 2024.07.28, use property C{Geohash.sizes3}.'''
520
+ t = self.sizes3
521
+ return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
522
+
523
+ @Property_RO
524
+ def sizes3(self):
525
+ '''Get the lat-, longitudinal and radial size of this cell in
526
+ a L{Sizes3Tuple}C{(height, width, radius)}, all in C{meter}.
389
527
  '''
390
528
  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?
529
+ n = min(max(self.precision, 1), len(z) - 1)
530
+ return Sizes3Tuple(z[n], name=self.name)
393
531
 
394
532
  def toLatLon(self, LatLon=None, **LatLon_kwds):
395
533
  '''Return (the approximate center of) this geohash cell
@@ -423,18 +561,6 @@ class Geohash(Str):
423
561
  '''
424
562
  return self._distanceTo(_formy.vincentys, other, **radius_wrap)
425
563
 
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
564
  @Property_RO
439
565
  def E(self):
440
566
  '''Get the cell East of this (L{Geohash}).
@@ -442,10 +568,10 @@ class Geohash(Str):
442
568
  return self.adjacent(_E_)
443
569
 
444
570
  @Property_RO
445
- def W(self):
446
- '''Get the cell West of this (L{Geohash}).
571
+ def N(self):
572
+ '''Get the cell North of this (L{Geohash}).
447
573
  '''
448
- return self.adjacent(_W_)
574
+ return self.adjacent(_N_)
449
575
 
450
576
  @Property_RO
451
577
  def NE(self):
@@ -459,6 +585,12 @@ class Geohash(Str):
459
585
  '''
460
586
  return self.N.W
461
587
 
588
+ @Property_RO
589
+ def S(self):
590
+ '''Get the cell South of this (L{Geohash}).
591
+ '''
592
+ return self.adjacent(_S_)
593
+
462
594
  @Property_RO
463
595
  def SE(self):
464
596
  '''Get the cell SouthEast of this (L{Geohash}).
@@ -471,6 +603,124 @@ class Geohash(Str):
471
603
  '''
472
604
  return self.S.W
473
605
 
606
+ @Property_RO
607
+ def W(self):
608
+ '''Get the cell West of this (L{Geohash}).
609
+ '''
610
+ return self.adjacent(_W_)
611
+
612
+
613
+ class Geohashed(object):
614
+ '''A cache of en- and decoded geohashes of one precision.
615
+ '''
616
+ _nn = None, # 1-tuple
617
+
618
+ def __init__(self, precision, ndigits=None):
619
+ '''New L{Geohashed} cache.
620
+
621
+ @arg precision: The geohash encoded length (C{int}, 1..12).
622
+ @kwarg ndigits: Optional number of digits to round C{lat}
623
+ and C{lon} to cache keys (C{int}, typically
624
+ C{B{ndigits}=B{precision}}) or C{None} for
625
+ no rounding.
626
+ '''
627
+ self._p = _2Precision(precision)
628
+ if ndigits is None:
629
+ self._ab2 = self._ab2float
630
+ else:
631
+ self._ab2 = self._ab2round
632
+ n = Int(ndigits=ndigits)
633
+ self._nn = n, n
634
+ self.clear()
635
+
636
+ def __len__(self):
637
+ '''Return the number of I{unigue} geohashes (C{int}).
638
+ '''
639
+ d = self._d
640
+ d = set(d.keys())
641
+ n = len(d)
642
+ for e in self._e.values():
643
+ e = set(e.values())
644
+ n += len(e - d)
645
+ return n
646
+
647
+ def _ab2(self, *ll): # overwritten
648
+ '''(INTERNAL) Make encoded keys C{a, b}.
649
+ '''
650
+ return ll
651
+
652
+ def _ab2float(self, *ll):
653
+ '''(INTERNAL) Make encoded keys C{a, b}.
654
+ '''
655
+ return map(float, ll)
656
+
657
+ def _ab2round(self, *ll):
658
+ '''(INTERNAL) Make encoded keys C{a, b}.
659
+ '''
660
+ return map(round, ll, self._nn) # strict=True
661
+
662
+ def clear(self):
663
+ '''Clear the C{en-} and C{decoded} cache.
664
+ '''
665
+ self._e = {}
666
+ self._d = {}
667
+
668
+ def decoded(self, geohash, encoded=False):
669
+ '''Get and cache the C{(lat, lon)} for C{geohash}, see L{decode<pygeodesy.geohash.decode>}.
670
+
671
+ @kwarg encoded: If C{True}, cache the result as C{encoded}.
672
+
673
+ @return: The C{(lat, lon}) pair for C{geohash}.
674
+ '''
675
+ try:
676
+ ll = self._d[geohash]
677
+ except KeyError:
678
+ self._d[geohash] = ll = _GH.decode2(geohash)
679
+ if encoded:
680
+ a, b = self._ab2(*ll)
681
+ try:
682
+ _ = self._e[b][a]
683
+ except KeyError:
684
+ self._e.setdefault(b, {})[a] = geohash
685
+ return ll
686
+
687
+ def encoded(self, lat, lon, decoded=False):
688
+ '''Get and cache the C{geohash} for C{(lat, lon)}, see L{encode<pygeodesy.geohash.encode>}.
689
+
690
+ @kwarg decoded: If C{True}, cache the result as C{decoded}.
691
+
692
+ @return: The C{geohash} for pair C{(lat, lon}).
693
+ '''
694
+ lat, lon = ll = _2fll(lat, lon)
695
+ a, b = self._ab2(*ll)
696
+ try:
697
+ gh = self._e[b][a]
698
+ except KeyError:
699
+ gh = _GH.encode(lat, lon, self._p, 0)
700
+ self._e.setdefault(b, {})[a] = gh
701
+ if decoded and gh not in self._d:
702
+ self._d[gh] = ll
703
+ return gh
704
+
705
+ @property_RO
706
+ def len2(self):
707
+ '''Return 2-tuple C{(lencoded, ldecoded)} with the C{len}gths of the
708
+ C{en-} and C{decoded} cache.
709
+ '''
710
+ return sum(len(e) for e in self._e.values()), len(self._d)
711
+
712
+ @Property_RO
713
+ def ndigits(self):
714
+ '''Get the rounding (C{int} or C{None}).
715
+ '''
716
+ return self._nn[0]
717
+
718
+ @Property_RO
719
+ def precision(self):
720
+ '''Get the C{precision} (C{int}).
721
+ '''
722
+ return self._p
723
+
474
724
 
475
725
  class GeohashError(_ValueError):
476
726
  '''Geohash encode, decode or other L{Geohash} issue.
@@ -493,6 +743,34 @@ _Neighbors8Defaults = dict(zip(Neighbors8Dict._Keys_, (None,) *
493
743
  len(Neighbors8Dict._Keys_))) # XXX frozendict
494
744
 
495
745
 
746
+ class Resolutions2Tuple(_NamedTuple):
747
+ '''2-Tuple C{(res1, res2)} with the primary I{(longitudinal)} and
748
+ secondary I{(latitudinal)} resolution, both in C{degrees}.
749
+ '''
750
+ _Names_ = ('res1', 'res2')
751
+ _Units_ = ( Degrees_, Degrees_)
752
+
753
+ @property_RO
754
+ def lat(self):
755
+ '''Get the secondary, latitudinal resolution (C{degrees}).
756
+ '''
757
+ return self[1]
758
+
759
+ @property_RO
760
+ def lon(self):
761
+ '''Get the primary, longitudinal resolution (C{degrees}).
762
+ '''
763
+ return self[0]
764
+
765
+
766
+ class Sizes3Tuple(_NamedTuple):
767
+ '''3-Tuple C{(height, width, radius)} with latitudinal C{height},
768
+ longitudinal C{width} and area C{radius}, all in C{meter}.
769
+ '''
770
+ _Names_ = (_height_, _width_, _radius_)
771
+ _Units_ = ( Meter, Meter, Meter)
772
+
773
+
496
774
  def bounds(geohash, LatLon=None, **LatLon_kwds):
497
775
  '''Returns the lower-left SW and upper-right NE corners of a geohash.
498
776
 
@@ -510,43 +788,11 @@ def bounds(geohash, LatLon=None, **LatLon_kwds):
510
788
 
511
789
  @raise GeohashError: Invalid or C{null} B{C{geohash}}.
512
790
  '''
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,
791
+ swne = _GH.swne4(geohash)
792
+ return _2bounds(LatLon, LatLon_kwds, *swne,
540
793
  name=nameof(geohash)) # _or_nameof=geohash
541
794
 
542
795
 
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
796
  def decode(geohash):
551
797
  '''Decode a geohash to lat-/longitude of the (approximate
552
798
  centre of) geohash cell to reasonable precision.
@@ -560,13 +806,10 @@ def decode(geohash):
560
806
 
561
807
  @raise GeohashError: Invalid or null B{C{geohash}}.
562
808
  '''
563
- b, h, w = _bounds3(geohash)
564
- lat, lon = _2center(b)
565
-
566
809
  # round to near centre without excessive precision to
567
810
  # ⌊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!
811
+ swne = _GH.swne4(geohash)
812
+ return _2latlon(*swne, fstr=_MODS.streprs.fstr)
570
813
 
571
814
 
572
815
  def decode2(geohash, LatLon=None, **LatLon_kwds):
@@ -587,14 +830,20 @@ def decode2(geohash, LatLon=None, **LatLon_kwds):
587
830
 
588
831
  @raise GeohashError: Invalid or null B{C{geohash}}.
589
832
  '''
590
- t = map2(float, decode(geohash))
591
- r = LatLon2Tuple(t) if LatLon is None else LatLon(*t, **LatLon_kwds) # *t
833
+ ll = _GH.decode2(geohash)
834
+ r = LatLon2Tuple(ll) if LatLon is None else \
835
+ LatLon( *ll, **LatLon_kwds)
592
836
  return _xnamed(r, name__=decode2)
593
837
 
594
838
 
839
+ @deprecated_function
595
840
  def decode_error(geohash):
596
- '''Return the relative lat-/longitude decoding errors for
597
- this geohash.
841
+ '''DEPRECATED on 2024.07.28, use L{geohash.decode_error2}.'''
842
+ return decode_error2(geohash)
843
+
844
+
845
+ def decode_error2(geohash):
846
+ '''Return the lat- and longitude decoding error for a geohash.
598
847
 
599
848
  @arg geohash: To be decoded (L{Geohash}).
600
849
 
@@ -606,9 +855,9 @@ def decode_error(geohash):
606
855
 
607
856
  @raise GeohashError: Invalid or null B{C{geohash}}.
608
857
  '''
609
- _, h, w = _bounds3(geohash)
610
- return LatLon2Tuple(h * _0_5, # Height error
611
- w * _0_5) # Width error
858
+ s, w, n, e = _GH.swne4(geohash)
859
+ return LatLon2Tuple((n - s) * _0_5, # lat error
860
+ (e - w) * _0_5) # lon error
612
861
 
613
862
 
614
863
  def distance_(geohash1, geohash2):
@@ -643,67 +892,23 @@ def distance3(geohash1, geohash2):
643
892
  return haversine_(geohash1, geohash2)
644
893
 
645
894
 
646
- def encode(lat, lon, precision=None):
895
+ def encode(lat, lon, precision=None, eps=EPS):
647
896
  '''Encode a lat-/longitude as a C{geohash}, either to the specified
648
- precision or if not provided, to an automatically evaluated
649
- precision.
897
+ precision or if not provided, to an inferred precision.
650
898
 
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).
899
+ @arg lat: Latitude (C{degrees90}).
900
+ @arg lon: Longitude (C{degrees180}).
901
+ @kwarg precision: The desired geohash length (C{int} 1..12) or
902
+ C{None} or C{0} for inferred.
903
+ @kwarg eps: Optional inference tolerance (C{degrees}), ignored
904
+ if B{C{precision}} is not C{None} or C{0}.
655
905
 
656
906
  @return: The C{geohash} (C{str}).
657
907
 
658
908
  @raise GeohashError: Invalid B{C{lat}}, B{C{lon}} or B{C{precision}}.
659
909
  '''
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)
910
+ gh, _ = _GH.encode2(lat, lon, precision, eps)
911
+ return gh
707
912
 
708
913
 
709
914
  def equirectangular4(geohash1, geohash2, radius=R_M):
@@ -799,14 +1004,6 @@ def precision(res1, res2=None):
799
1004
  return _MaxPrec
800
1005
 
801
1006
 
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
1007
  def resolution2(prec1, prec2=None):
811
1008
  '''Determine the (geographic) resolutions of given L{Geohash}
812
1009
  precisions.
@@ -817,38 +1014,37 @@ def resolution2(prec1, prec2=None):
817
1014
  (C{int} 1..12).
818
1015
 
819
1016
  @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.
1017
+ (geographic) resolutions in C{degrees}, where
1018
+ C{res2 B{is} res1} if no B{C{prec2}} is given.
822
1019
 
823
1020
  @raise GeohashError: Invalid B{C{prec1}} or B{C{prec2}}.
824
1021
 
825
1022
  @see: I{Karney}'s C++ class U{Geohash
826
1023
  <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geohash.html>}.
827
1024
  '''
828
- res1, res2 = _360_0, _180_0 # note ... lon, lat!
1025
+ lon = _2res(_360_0, prec1=prec1)
1026
+ lat = lon if prec2 is None else \
1027
+ _2res(_180_0, prec2=prec2)
1028
+ return Resolutions2Tuple(lon, lat)
829
1029
 
830
- if prec1:
831
- p = 5 * max(0, min(Int(prec1=prec1, Error=GeohashError), _MaxPrec))
832
- res1 = res2 = ldexp(res1, -(p - p // 2))
833
1030
 
834
- if prec2:
835
- p = 5 * max(0, min(Int(prec2=prec2, Error=GeohashError), _MaxPrec))
836
- res2 = ldexp(res2, -(p // 2))
1031
+ @deprecated_function
1032
+ def sizes(geohash):
1033
+ '''DEPRECATED on 2024.07.28, use function L{pygeodesy.geohash.sizes3}.'''
1034
+ t = sizes3(geohash)
1035
+ return _GH._LatLon2Tuple(t.height, t.width, name=t.name)
837
1036
 
838
- return Resolutions2Tuple(res1, res2)
839
1037
 
840
-
841
- def sizes(geohash):
842
- '''Return the lat- and longitudinal size of this L{Geohash} cell.
1038
+ def sizes3(geohash):
1039
+ '''Return the lat-, longitudinal and radial size of this L{Geohash} cell.
843
1040
 
844
1041
  @arg geohash: Cell for which size are required (L{Geohash} or C{str}).
845
1042
 
846
- @return: A L{LatLon2Tuple}C{(lat, lon)} with the latitudinal height and
847
- longitudinal width in (C{meter}).
1043
+ @return: A L{Sizes3Tuple}C{(height, width, radius)}, all C{meter}.
848
1044
 
849
1045
  @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon} or C{str}.
850
1046
  '''
851
- return _2Geohash(geohash).sizes
1047
+ return _2Geohash(geohash).sizes3
852
1048
 
853
1049
 
854
1050
  def vincentys_(geohash1, geohash2, **radius_wrap):
@@ -868,10 +1064,38 @@ def vincentys_(geohash1, geohash2, **radius_wrap):
868
1064
  return _2Geohash(geohash1).vincentysTo(geohash2, **radius_wrap)
869
1065
 
870
1066
 
871
- __all__ += _ALL_OTHER(bounds, # functions
872
- decode, decode2, decode_error, distance_,
873
- encode, equirectangular4, euclidean_, haversine_,
874
- neighbors, precision, resolution2, sizes, vincentys_)
1067
+ __all__ += _ALL_DOCS(bounds, # functions
1068
+ decode, decode2, decode_error2, distance_,
1069
+ encode, equirectangular4, euclidean_, haversine_,
1070
+ neighbors, precision, resolution2, sizes3, vincentys_,
1071
+ decode_error, sizes) # DEPRECATED
1072
+
1073
+ if __name__ == '__main__':
1074
+
1075
+ from pygeodesy.internals import printf, _versions
1076
+ from timeit import timeit
1077
+
1078
+ for f, p in (('encode', _MaxPrec), ('infer', None)):
1079
+
1080
+ def _t(prec=p):
1081
+ i = 0
1082
+ for lat in range(-90, 90, 3):
1083
+ for lon in range(-180, 180, 7):
1084
+ _ = encode(lat, lon, prec)
1085
+ i += 1
1086
+ return i
1087
+
1088
+ i = _t() # prime
1089
+ n = 10
1090
+ t = timeit(_t, number=n) / (i * n)
1091
+ printf('%s %.3f usec, %s', f, t * 1e6, _versions())
1092
+
1093
+ # % python3.12 -m pygeodesy.geohash
1094
+ # encode 10.145 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1095
+ # infer 14.780 usec, pygeodesy 24.8.4 Python 3.12.4 64bit arm64 macOS 14.5
1096
+ # or about 6.56 and 74.12 times faster than pygeodesy 24.7.24 and older:
1097
+ # encode 66.524 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
1098
+ # infer 1095.386 usec, pygeodesy 24.7.24 Python 3.12.4 64bit arm64 macOS 14.5
875
1099
 
876
1100
  # **) MIT License
877
1101
  #