Auto-retry IGN tiles at lower zoom on 404

When zoom 20 tiles are unavailable (rural areas), fall back to zoom 19, 18,
etc. down to 15. Breaks out immediately on first-tile 404 to avoid wasting
requests at unsupported zoom levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-14 02:21:47 +02:00
parent e2bd6b2536
commit f988ddb76d

View File

@ -76,6 +76,8 @@ def _lat_lon_to_px(lat, lon, zoom, tile_size=256):
def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15): def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15):
"""Download IGN WMTS tiles for the given bounds using Web Mercator (PM). """Download IGN WMTS tiles for the given bounds using Web Mercator (PM).
If the first tile returns 404, automatically retries at lower zoom levels.
Args: Args:
min_x, max_x, min_y, max_y: Bounds in Lambert 93. min_x, max_x, min_y, max_y: Bounds in Lambert 93.
layer: IGN WMTS layer name. layer: IGN WMTS layer name.
@ -106,25 +108,32 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15):
tile_matrix_set = "PM" tile_matrix_set = "PM"
tile_size = 256 tile_size = 256
col_min, row_min = _lat_lon_to_tile(nw_lat, nw_lon, zoom_level) # Try downloading at the requested zoom level; fall back to lower zooms on 404
col_max, row_max = _lat_lon_to_tile(se_lat, se_lon, zoom_level) min_zoom = 15
for zoom in range(zoom_level, min_zoom - 1, -1):
col_min, row_min = _lat_lon_to_tile(nw_lat, nw_lon, zoom)
col_max, row_max = _lat_lon_to_tile(se_lat, se_lon, zoom)
nw_px_x, nw_px_y = _lat_lon_to_px(nw_lat, nw_lon, zoom_level) nw_px_x, nw_px_y = _lat_lon_to_px(nw_lat, nw_lon, zoom)
se_px_x, se_px_y = _lat_lon_to_px(se_lat, se_lon, zoom_level) se_px_x, se_px_y = _lat_lon_to_px(se_lat, se_lon, zoom)
out_width = int(se_px_x - nw_px_x) out_width = int(se_px_x - nw_px_x)
out_height = int(se_px_y - nw_px_y) out_height = int(se_px_y - nw_px_y)
if out_width <= 0 or out_height <= 0 or out_width > 10000 or out_height > 10000: if out_width <= 0 or out_height <= 0 or out_width > 10000 or out_height > 10000:
logger.warning(f" Image IGN trop grande: {out_width}x{out_height}px — abandon") logger.warning(f" Image IGN trop grande: {out_width}x{out_height}px — zoom {zoom} abandon")
return None continue
total_tiles = (col_max - col_min + 1) * (row_max - row_min + 1) total_tiles = (col_max - col_min + 1) * (row_max - row_min + 1)
logger.info(f" Zoom {zoom_level}: {total_tiles} tuiles à télécharger ({out_width}x{out_height}px)") if zoom < zoom_level:
logger.info(f" ↓ Retry zoom {zoom_level}{zoom}: {total_tiles} tuiles ({out_width}x{out_height}px)")
else:
logger.info(f" Zoom {zoom}: {total_tiles} tuiles à télécharger ({out_width}x{out_height}px)")
composite = np.full((out_height, out_width, 3), 255, dtype=np.uint8) composite = np.full((out_height, out_width, 3), 255, dtype=np.uint8)
tiles_downloaded = 0 tiles_downloaded = 0
tiles_404 = 0
fmt = "image/png" if 'PLAN' in layer else "image/jpeg" fmt = "image/png" if 'PLAN' in layer else "image/jpeg"
for col in range(col_min, col_max + 1): for col in range(col_min, col_max + 1):
@ -133,7 +142,7 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15):
f"{wmts_url}?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile" f"{wmts_url}?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile"
f"&LAYER={layer}&STYLE=normal" f"&LAYER={layer}&STYLE=normal"
f"&TILEMATRIXSET={tile_matrix_set}" f"&TILEMATRIXSET={tile_matrix_set}"
f"&TILEMATRIX={zoom_level}&TILECOL={col}&TILEROW={row}" f"&TILEMATRIX={zoom}&TILECOL={col}&TILEROW={row}"
f"&FORMAT={fmt}" f"&FORMAT={fmt}"
) )
@ -167,16 +176,33 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15):
tile_arr[src_y:src_y+src_h, src_x:src_x+src_w] tile_arr[src_y:src_y+src_h, src_x:src_x+src_w]
tiles_downloaded += 1 tiles_downloaded += 1
except Exception as e: except urllib.error.HTTPError as e:
if tiles_downloaded == 0 and col == col_min and row == row_min: if e.code == 404:
logger.error(f" ✗ Erreur tuile IGN: {e}") tiles_404 += 1
# If the very first tile is 404, this zoom level is unavailable
if col == col_min and row == row_min:
logger.info(f" Zoom {zoom} non disponible (404) — essai zoom inférieur")
break
continue
except Exception:
continue
else:
continue
# Only reach here if inner loop broke (first tile 404)
break
if tiles_404 > 0 and tiles_downloaded == 0:
# No tiles at this zoom, try lower
continue continue
logger.info(f"{tiles_downloaded} tuiles IGN téléchargées ({layer})") logger.info(f"{tiles_downloaded} tuiles IGN téléchargées ({layer})")
if tiles_downloaded == 0: if tiles_downloaded == 0:
return None continue
return composite return composite
logger.error(" ✗ Aucun zoom disponible pour cette zone")
return None
def generate_ign_overlay(dem_file, basename, vis_dir, resolution, layer, title, legend_label, description, out_suffix): def generate_ign_overlay(dem_file, basename, vis_dir, resolution, layer, title, legend_label, description, out_suffix):
"""Generate an IGN basemap overlay visualization. """Generate an IGN basemap overlay visualization.