I would like to use contextily
to add OSM layer to a map I draw with GeoPandas. But I have kind of a random error when using the zoom
in automatic mode (default).
MCVE
Here is a MCVE:
df = pd.DataFrame({"key": ["A"], "lon": [3.6], "lat": [43.4]})
point = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs=4326).to_crs(crs="ESRI:102014")
circle = point.geometry.buffer(3000)
axe = point.plot(color="red")
circle.plot(alpha=0.35, color="red", ax=axe)
ctx.add_basemap(axe, zoom="auto", crs=point.crs)
Miserably fails because the URL of the tile does not exists:
---------------------------------------------------------------------------
HTTPError Traceback (most recent call last)
File ~/.local/lib/python3.10/site-packages/contextily/tile.py:396, in _retryer(tile_url, wait, max_retries)
395 request = requests.get(tile_url, headers={"user-agent": USER_AGENT})
--> 396 request.raise_for_status()
397 except requests.HTTPError:
File /usr/local/lib/python3.10/dist-packages/requests/models.py:1021, in Response.raise_for_status(self)
1020 if http_error_msg:
-> 1021 raise HTTPError(http_error_msg, response=self)
HTTPError: 404 Client Error: Not Found for url: https://stamen-tiles-a.a.ssl.fastly.net/terrain/14/8353/5993.png
During handling of the above exception, another exception occurred:
HTTPError Traceback (most recent call last)
Cell In [9], line 3
1 axe = point.plot(color="red")
2 circle.plot(alpha=0.35, color="red", ax=axe)
----> 3 ctx.add_basemap(axe, zoom="auto", crs=point.crs)
File ~/.local/lib/python3.10/site-packages/contextily/plotting.py:121, in add_basemap(ax, zoom, source, interpolation, attribution, attribution_size, reset_extent, crs, resampling, **extra_imshow_args)
117 left, right, bottom, top = _reproj_bb(
118 left, right, bottom, top, crs, {"init": "epsg:3857"}
119 )
120 # Download image
--> 121 image, extent = bounds2img(
122 left, bottom, right, top, zoom=zoom, source=source, ll=False
123 )
124 # Warping
125 if crs is not None:
File ~/.local/lib/python3.10/site-packages/contextily/tile.py:222, in bounds2img(w, s, e, n, zoom, source, ll, wait, max_retries)
220 x, y, z = t.x, t.y, t.z
221 tile_url = provider.build_url(x=x, y=y, z=z)
--> 222 image = _fetch_tile(tile_url, wait, max_retries)
223 tiles.append(t)
224 arrays.append(image)
File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:594, in MemorizedFunc.__call__(self, *args, **kwargs)
593 def __call__(self, *args, **kwargs):
--> 594 return self._cached_call(args, kwargs)[0]
File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:537, in MemorizedFunc._cached_call(self, args, kwargs, shelving)
534 must_call = True
536 if must_call:
--> 537 out, metadata = self.call(*args, **kwargs)
538 if self.mmap_mode is not None:
539 # Memmap the output at the first call to be consistent with
540 # later calls
541 if self._verbose:
File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:779, in MemorizedFunc.call(self, *args, **kwargs)
777 if self._verbose > 0:
778 print(format_call(self.func, args, kwargs))
--> 779 output = self.func(*args, **kwargs)
780 self.store_backend.dump_item(
781 [func_id, args_id], output, verbose=self._verbose)
783 duration = time.time() - start_time
File ~/.local/lib/python3.10/site-packages/contextily/tile.py:252, in _fetch_tile(tile_url, wait, max_retries)
250 @memory.cache
251 def _fetch_tile(tile_url, wait, max_retries):
--> 252 request = _retryer(tile_url, wait, max_retries)
253 with io.BytesIO(request.content) as image_stream:
254 image = Image.open(image_stream).convert("RGBA")
File ~/.local/lib/python3.10/site-packages/contextily/tile.py:399, in _retryer(tile_url, wait, max_retries)
397 except requests.HTTPError:
398 if request.status_code == 404:
--> 399 raise requests.HTTPError(
400 "Tile URL resulted in a 404 error. "
401 "Double-check your tile url:\n{}".format(tile_url)
402 )
403 elif request.status_code == 104:
404 if max_retries > 0:
HTTPError: Tile URL resulted in a 404 error. Double-check your tile url:
https://stamen-tiles-a.a.ssl.fastly.net/terrain/14/8353/5993.png
What is interesting is that it seems related to the level of details the tile has. Because if I chose a point with more complex geometries in it, it works:
df = pd.DataFrame({"key": ["A"], "lon": [3.7], "lat": [43.8]})
Or if I reduce the zoom by one unit with the original point:
ctx.add_basemap(axe, zoom=13, crs=point.crs)
It also works (zoom=14
is the value that make it crashes for some points).
Analysis
It seems there is some issue with some tiles which are not rendered or mapped to an expected URL contextily
generates, hence the 404.
The problem seems to be related to the level of details present on the tile itself, because:
- Choosing a tile with a lot of details makes automatic zoom to work;
- Reducing the zoom manually also makes the URL valid but with less resolution.
Question
I have not enough insight to discriminate if it is a contextily
bug or if its related to the tile provider. How can I still use the automatic zoom mode while preventing the 404 error? Is there something I can do with the definition of the provider? Why contextily fails to fetch some tiles in auto zoom mode?