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).
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).
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:
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?
This is not the issue with the contextily
. Stamen Terrain (default tiles) does not have an even coverage, so some places have a lot of zoom levels while other less. That is what your observation also shows. So contextily automatically computes the expected tile but cannot know the it is not provided for this place as some other places have the same zoom, so tiles metadata say that level 14 is permitted. If you use different tiles, the same code works.
ctx.add_basemap(axe, zoom="auto", crs=point.crs, source=ctx.providers.OpenStreetMap.Mapnik)