http-status-code-404geopandasgeocontextily

Why contextily fails to fetch some tiles in auto zoom mode?


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:

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?


Solution

  • 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)