1
1
"""Tools for downloading map tiles from coordinates."""
2
- import io
3
- from urllib .request import urlopen
4
-
5
- import numpy as np
2
+ from __future__ import (absolute_import , division , print_function )
6
3
7
4
import mercantile as mt
5
+ import requests
6
+ import io
7
+ import os
8
+ import numpy as np
8
9
import pandas as pd
9
10
import rasterio as rio
10
- from cartopy .io .img_tiles import _merge_tiles as merge_tiles
11
11
from PIL import Image
12
+ from cartopy .io .img_tiles import _merge_tiles as merge_tiles
12
13
from rasterio .transform import from_origin
13
-
14
14
from . import tile_providers as sources
15
15
16
+
16
17
__all__ = ['bounds2raster' , 'bounds2img' , 'howmany' ]
17
18
18
- def bounds2raster (w , s , e , n , path , zoom = 'auto' ,
19
- url = sources .ST_TERRAIN , ll = False ):
20
- """
19
+
20
+ def bounds2raster (w , s , e , n , path , zoom = 'auto' ,
21
+ url = sources .ST_TERRAIN , ll = False ,
22
+ wait = 0 , max_retries = 2 ):
23
+ '''
21
24
Take bounding box and zoom, and write tiles into a raster file in
22
25
the Spherical Mercator CRS (EPSG:3857)
23
26
@@ -45,14 +48,22 @@ def bounds2raster(w, s, e, n, path, zoom='auto',
45
48
ll : Boolean
46
49
[Optional. Default: False] If True, `w`, `s`, `e`, `n` are
47
50
assumed to be lon/lat as opposed to Spherical Mercator.
51
+ wait : int
52
+ [Optional. Default: 0]
53
+ if the tile API is rate-limited, the number of seconds to wait
54
+ between a failed request and the next try
55
+ max_retries: int
56
+ [Optional. Default: 2]
57
+ total number of rejected requests allowed before contextily
58
+ will stop trying to fetch more tiles from a rate-limited API.
48
59
49
60
Returns
50
61
-------
51
62
img : ndarray
52
63
Image as a 3D array of RGB values
53
64
extent : tuple
54
65
Bounding box [minX, maxX, minY, maxY] of the returned image
55
- """
66
+ '''
56
67
if not ll :
57
68
# Convert w, s, e, n into lon/lat
58
69
w , s = _sm2ll (w , s )
@@ -62,29 +73,31 @@ def bounds2raster(w, s, e, n, path, zoom='auto',
62
73
# Download
63
74
Z , ext = bounds2img (w , s , e , n , zoom = zoom , url = url , ll = True )
64
75
# Write
65
- #---
76
+ # ---
66
77
h , w , b = Z .shape
67
- #--- https://mapbox.github.io/rasterio/quickstart.html#opening-a-dataset-in-writing-mode
78
+ # --- https://mapbox.github.io/rasterio/quickstart.html#opening-a-dataset-in-writing-mode
68
79
minX , maxX , minY , maxY = ext
69
80
x = np .linspace (minX , maxX , w )
70
81
y = np .linspace (minY , maxY , h )
71
82
resX = (x [- 1 ] - x [0 ]) / w
72
83
resY = (y [- 1 ] - y [0 ]) / h
73
84
transform = from_origin (x [0 ] - resX / 2 ,
74
85
y [- 1 ] + resY / 2 , resX , resY )
75
- #---
86
+ # ---
76
87
raster = rio .open (path , 'w' ,
77
88
driver = 'GTiff' , height = h , width = w ,
78
89
count = b , dtype = str (Z .dtype .name ),
79
90
crs = 'epsg:3857' , transform = transform )
80
91
for band in range (b ):
81
- raster .write (Z [:, :, band ], band + 1 )
92
+ raster .write (Z [:, :, band ], band + 1 )
82
93
raster .close ()
83
94
return Z , ext
84
95
96
+
85
97
def bounds2img (w , s , e , n , zoom = 'auto' ,
86
- url = sources .ST_TERRAIN , ll = False ):
87
- """
98
+ url = sources .ST_TERRAIN , ll = False ,
99
+ wait = 0 , max_retries = 2 ):
100
+ '''
88
101
Take bounding box and zoom and return an image with all the tiles
89
102
that compose the map and its Spherical Mercator extent.
90
103
@@ -110,14 +123,22 @@ def bounds2img(w, s, e, n, zoom='auto',
110
123
ll : Boolean
111
124
[Optional. Default: False] If True, `w`, `s`, `e`, `n` are
112
125
assumed to be lon/lat as opposed to Spherical Mercator.
126
+ wait : int
127
+ [Optional. Default: 0]
128
+ if the tile API is rate-limited, the number of seconds to wait
129
+ between a failed request and the next try
130
+ max_retries: int
131
+ [Optional. Default: 2]
132
+ total number of rejected requests allowed before contextily
133
+ will stop trying to fetch more tiles from a rate-limited API.
113
134
114
135
Returns
115
136
-------
116
137
img : ndarray
117
138
Image as a 3D array of RGB values
118
139
extent : tuple
119
140
Bounding box [minX, maxX, minY, maxY] of the returned image
120
- """
141
+ '''
121
142
if not ll :
122
143
# Convert w, s, e, n into lon/lat
123
144
w , s = _sm2ll (w , s )
@@ -128,18 +149,16 @@ def bounds2img(w, s, e, n, zoom='auto',
128
149
for t in mt .tiles (w , s , e , n , [zoom ]):
129
150
x , y , z = t .x , t .y , t .z
130
151
tile_url = url .replace ('tileX' , str (x )).replace ('tileY' , str (y )).replace ('tileZ' , str (z ))
131
- #---
132
- fh = urlopen (tile_url )
133
- im_data = io .BytesIO (fh .read ())
134
- fh .close ()
135
- imgr = Image .open (im_data )
136
- imgr = imgr .convert ('RGB' )
137
- #---
138
- img = np .array (imgr )
152
+ # ---
153
+ request = _retryer (tile_url , wait , max_retries )
154
+ with io .BytesIO (request .content ) as image_stream :
155
+ image = Image .open (image_stream ).convert ('RGB' )
156
+ image = np .asarray (image )
157
+ # ---
139
158
wt , st , et , nt = mt .bounds (t )
140
- xr = np .linspace (wt , et , img .shape [0 ])
141
- yr = np .linspace (st , nt , img .shape [1 ])
142
- tiles .append ([img , xr , yr , 'lower' ])
159
+ xr = np .linspace (wt , et , image .shape [0 ])
160
+ yr = np .linspace (st , nt , image .shape [1 ])
161
+ tiles .append ([image , xr , yr , 'lower' ])
143
162
merged , extent = merge_tiles (tiles )[:2 ]
144
163
# lon/lat extent --> Spheric Mercator
145
164
minX , maxX , minY , maxY = extent
@@ -148,8 +167,46 @@ def bounds2img(w, s, e, n, zoom='auto',
148
167
extent = w , e , s , n
149
168
return merged [::- 1 ], extent
150
169
151
- def howmany (w , s , e , n , zoom , verbose = True , ll = False ):
170
+
171
+ def _retryer (tile_url , wait , max_retries ):
152
172
"""
173
+ Retry a url many times in attempt to get a tile
174
+
175
+ Arguments
176
+ ---------
177
+ tile_url: str
178
+ string that is the target of the web request. Should be
179
+ a properly-formatted url for a tile provider.
180
+ wait : int
181
+ if the tile API is rate-limited, the number of seconds to wait
182
+ between a failed request and the next try
183
+ max_retries: int
184
+ total number of rejected requests allowed before contextily
185
+ will stop trying to fetch more tiles from a rate-limited API.
186
+
187
+ Returns
188
+ -------
189
+ request object containing the web response.
190
+ """
191
+ try :
192
+ request = requests .get (tile_url )
193
+ request .raise_for_status ()
194
+ except requests .HTTPError :
195
+ if request .status_code == 404 :
196
+ raise requests .HTTPError ('Tile URL resulted in a 404 error. '
197
+ 'Double-check your tile url:\n {}' .format (tile_url ))
198
+ elif request .status_code == 104 :
199
+ if max_retries > 0 :
200
+ os .wait (wait )
201
+ max_retries -= 1
202
+ request = _retryer (tile_url , wait , max_retries )
203
+ else :
204
+ raise requests .HTTPError ('Connection reset by peer too many times.' )
205
+ return request
206
+
207
+
208
+ def howmany (w , s , e , n , zoom , verbose = True , ll = False ):
209
+ '''
153
210
Number of tiles required for a given bounding box and a zoom level
154
211
...
155
212
@@ -171,7 +228,7 @@ def howmany(w, s, e, n, zoom, verbose=True, ll=False):
171
228
ll : Boolean
172
229
[Optional. Default: False] If True, `w`, `s`, `e`, `n` are
173
230
assumed to be lon/lat as opposed to Spherical Mercator.
174
- """
231
+ '''
175
232
if not ll :
176
233
# Convert w, s, e, n into lon/lat
177
234
w , s = _sm2ll (w , s )
@@ -180,12 +237,13 @@ def howmany(w, s, e, n, zoom, verbose=True, ll=False):
180
237
zoom = _calculate_zoom (w , s , e , n )
181
238
tiles = len (list (mt .tiles (w , s , e , n , [zoom ])))
182
239
if verbose :
183
- print ("Using zoom level %i, this will download %i tiles" % (zoom ,
184
- tiles ))
240
+ print ("Using zoom level %i, this will download %i tiles" % (zoom ,
241
+ tiles ))
185
242
return tiles
186
243
244
+
187
245
def bb2wdw (bb , rtr ):
188
- """
246
+ '''
189
247
Convert XY bounding box into the window of the tile raster
190
248
...
191
249
@@ -200,20 +258,21 @@ def bb2wdw(bb, rtr):
200
258
-------
201
259
window : tuple
202
260
((row_start, row_stop), (col_start, col_stop))
203
- """
261
+ '''
204
262
rbb = rtr .bounds
205
263
xi = pd .Series (np .linspace (rbb .left , rbb .right , rtr .shape [1 ]))
206
264
yi = pd .Series (np .linspace (rbb .bottom , rbb .top , rtr .shape [0 ]))
207
265
208
266
window = ((rtr .shape [0 ] - yi .searchsorted (bb [3 ])[0 ],
209
267
rtr .shape [0 ] - yi .searchsorted (bb [1 ])[0 ]),
210
- (xi .searchsorted (bb [0 ])[0 ],
268
+ (xi .searchsorted (bb [0 ])[0 ],
211
269
xi .searchsorted (bb [2 ])[0 ])
212
- )
270
+ )
213
271
return window
214
272
273
+
215
274
def _sm2ll (x , y ):
216
- """
275
+ '''
217
276
Transform Spherical Mercator coordinates point into lon/lat
218
277
219
278
NOTE: Translated from the JS implementation in
@@ -231,15 +290,15 @@ def _sm2ll(x, y):
231
290
-------
232
291
ll : tuple
233
292
lon/lat coordinates
234
- """
235
- rMajor = 6378137. # Equatorial Radius, QGS84
293
+ '''
294
+ rMajor = 6378137. # Equatorial Radius, QGS84
236
295
shift = np .pi * rMajor
237
296
lon = x / shift * 180.
238
297
lat = y / shift * 180.
239
- lat = 180. / np .pi * (2. * np .arctan ( np .exp ( lat * np .pi / 180. ) ) -
240
- np .pi / 2. )
298
+ lat = 180. / np .pi * (2. * np .arctan (np .exp (lat * np .pi / 180. )) - np .pi / 2. )
241
299
return lon , lat
242
300
301
+
243
302
def _calculate_zoom (w , s , e , n ):
244
303
"""Automatically choose a zoom level given a desired number of tiles.
245
304
0 commit comments