Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

""" Tools for encoding and decoding geohashes 

 

Geohashes are a way of encoding latitude and longitude information into 

a single value (hash as a str or uint64) by dividing up the world into 

hierarchical quadrants for a given level of precision. The higher the 

precision, the more tiers of quadrants are created. When using just one 

level of precision, for example, divides the globe into 36 zones (a-z, 0-9). 

By adding letters to the hash, the quandrants get increasingly small to the 

point where they are useful as global, unique identifiers for pixel coordinates 

(assuming you pick a precision that creates quadrant cells smaller than your 

image pixel size). 

 

References 

---------- 

.. [1] https://en.wikipedia.org/wiki/Geohash 

.. [2] https://www.movable-type.co.uk/scripts/geohash.html 

 

 

TODO 

---- 

* Implement geohash within this submodule to cut dependencies 

* Make faster using Numba (look for 0.43 to deal with dict / str) 

 

""" 

from functools import singledispatch 

 

_HAS_GEOHASH = True 

try: 

import geohash 

except ImportError: 

_HAS_GEOHASH = False 

 

_HAS_DASK = True 

try: 

import dask.array as da 

except ImportError: 

_HAS_DASK = False 

 

import numpy as np 

import pandas as pd 

from rasterio.crs import CRS 

from rasterio.warp import transform 

 

_HAS_XARRAY = True 

try: 

import xarray as xr 

except ImportError: 

_HAS_XARRAY = False 

 

from ..compat import requires_module 

from ..utils import register_multi_singledispatch 

 

_CRS_4326 = CRS.from_epsg(4326) 

_MEM_TYPES = (np.ndarray, pd.Series, ) 

 

 

# ============================================================================= 

# ENCODE 

@requires_module('geohash') 

@singledispatch 

def geohash_encode(y, x, crs=None, precision=12): 

""" Encode Y/X coordinates into a geohash, reprojecting as needed 

 

Parameters 

---------- 

y : np.ndarray 

Y coordinates 

x : np.ndarray 

X coordinates 

crs : rasterio.crs.CRS, optional 

If Y/X aren't in latitude/longitude (EPSG:4326), then provide their 

coordinate reference system 

precision : int, optional 

Characters of precision for the geohash 

 

Returns 

------- 

np.ndarry 

The geohashes for all Y/X (dtype=``np.dtype(('U', precision))``) 

""" 

raise TypeError('Only works on array types') 

 

 

def _geohash_encode_kernel(y, x, crs=None, precision=12): 

y, x, is_scalar = _guard_scalar_yx(y, x) 

if crs is not None: 

assert isinstance(crs, CRS) 

x, y = transform(crs, _CRS_4326, x, y) 

 

geohashes = [] 

for x_, y_ in zip(x, y): 

gh = geohash.encode(y_, x_, precision=precision) 

geohashes.append(gh) 

geohashes_ = np.asarray(geohashes, dtype=np.dtype(('U', precision))) 

 

if is_scalar: 

return geohashes_[0] 

else: 

return geohashes_ 

 

 

@register_multi_singledispatch(geohash_encode, _MEM_TYPES + (int, float, )) 

def _geohash_encode_mem(y, x, crs=None, precision=12): 

return _geohash_encode_kernel(y, x, crs=crs, precision=precision) 

 

 

if _HAS_DASK: 

@geohash_encode.register(da.Array) 

def _geohash_encode_dask(y, x, crs=None, precision=12): 

dtype_ = np.dtype(('U', precision)) 

sig = '(i),(i)->(i)' if y.shape else '(),()->()' 

 

ans = da.map_blocks(_geohash_encode_kernel, 

y, x, 

dtype=dtype_, 

crs=crs, 

precision=precision) 

return ans 

 

 

if _HAS_XARRAY: 

@geohash_encode.register(xr.DataArray) 

def _geohash_encode_xarray(y, x, crs=None, precision=12): 

dtype_ = np.dtype(('U', precision)) 

ans = geohash_encode(y.data, x.data, crs=crs, precision=precision) 

if np.ndim(ans): 

return xr.DataArray(ans, 

dims=y.dims, 

coords=y.coords, 

name='geohash') 

else: 

return xr.DataArray(ans, name='geohash') 

 

 

@requires_module('geohash') 

@singledispatch 

def geohash_decode(geohashes, crs=None): 

""" Encode Y/X coordinates into a geohash, reprojecting as needed 

 

Parameters 

---------- 

np.ndarry 

The geohashes for all Y/X 

crs : rasterio.crs.CRS, optional 

Reproject Y/X latitude/longitude values for each geohash into this CRS 

before returning (to help with end-to-end encode/decode) 

 

Returns 

------- 

y : np.ndarray 

Y coordinates 

x : np.ndarray 

X coordinates 

""" 

raise TypeError('Only works for array types') 

 

 

def _geohash_decode_kernel(geohashes, crs=None): 

geohashes, is_scalar = _guard_scalar_gh(geohashes) 

lats, lons = [], [] 

for gh in geohashes: 

lat, lon = geohash.decode(gh, delta=False) 

lats.append(lat) 

lons.append(lon) 

 

if crs is not None: 

assert isinstance(crs, CRS) 

x, y = transform(_CRS_4326, crs, lons, lats) 

else: 

x, y = lons, lats 

 

y_ = np.asarray(y, dtype=np.float32) 

x_ = np.asarray(x, dtype=np.float32) 

 

if is_scalar: 

return y_[0], x_[0] 

else: 

return y_, x_ 

 

 

@register_multi_singledispatch(geohash_decode, _MEM_TYPES + (str, )) 

def _geohash_decode_mem(geohashes, crs=None): 

return _geohash_decode_kernel(geohashes, crs=crs) 

 

 

if _HAS_DASK: 

@geohash_decode.register(da.Array) 

def _geohash_decode_dask(geohashes, crs=None): 

sig = '(i)->(i),(i)' if geohashes.shape else '()->(),()' 

y, x = da.apply_gufunc(_geohash_decode_kernel, 

sig, 

geohashes, 

output_dtypes=[np.float32, np.float32], 

crs=crs) 

return y, x 

 

 

if _HAS_XARRAY and _HAS_DASK: 

@geohash_decode.register(xr.DataArray) 

def _geohash_decode_xarray(geohashes, crs=None): 

if isinstance(geohashes.data, da.Array): 

y, x = _geohash_decode_dask(geohashes.data, crs=crs) 

else: 

y, x = _geohash_decode_mem(geohashes.data, crs=crs) 

 

y_ = xr.DataArray(y, name='y', coords=geohashes.coords) 

x_ = xr.DataArray(x, name='x', coords=geohashes.coords) 

 

return y_, x_ 

 

 

def _guard_scalar_yx(y, x): 

is_scalar = not getattr(y, 'shape', ()) 

if is_scalar: 

y = np.atleast_1d(y) 

x = np.atleast_1d(x) 

assert y.shape == x.shape 

assert y.ndim == 1 and x.ndim == 1 

return y, x, is_scalar 

 

 

def _guard_scalar_gh(geohashes): 

is_scalar = not getattr(geohashes, 'shape', ()) 

if is_scalar: 

geohashes = [geohashes] 

return geohashes, is_scalar