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,76 +108,100 @@ 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
fmt = "image/png" if 'PLAN' in layer else "image/jpeg" tiles_404 = 0
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):
for row in range(row_min, row_max + 1): for row in range(row_min, row_max + 1):
url = ( url = (
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}"
) )
try: try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=10) as response: with urllib.request.urlopen(req, timeout=10) as response:
tile_data = response.read() tile_data = response.read()
tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB') tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB')
tile_arr = np.array(tile_img) tile_arr = np.array(tile_img)
tile_origin_x = col * tile_size tile_origin_x = col * tile_size
tile_origin_y = row * tile_size tile_origin_y = row * tile_size
px_x = int(tile_origin_x - nw_px_x) px_x = int(tile_origin_x - nw_px_x)
px_y = int(tile_origin_y - nw_px_y) px_y = int(tile_origin_y - nw_px_y)
x_off = max(0, -px_x) x_off = max(0, -px_x)
y_off = max(0, -px_y) y_off = max(0, -px_y)
dst_x_start = max(0, px_x) dst_x_start = max(0, px_x)
dst_y_start = max(0, px_y) dst_y_start = max(0, px_y)
dst_x_end = min(out_width, px_x + tile_size) dst_x_end = min(out_width, px_x + tile_size)
dst_y_end = min(out_height, px_y + tile_size) dst_y_end = min(out_height, px_y + tile_size)
src_x = x_off src_x = x_off
src_y = y_off src_y = y_off
src_w = dst_x_end - dst_x_start src_w = dst_x_end - dst_x_start
src_h = dst_y_end - dst_y_start src_h = dst_y_end - dst_y_start
if src_w > 0 and src_h > 0 and tile_arr.shape[0] >= src_y + src_h and tile_arr.shape[1] >= src_x + src_w: if src_w > 0 and src_h > 0 and tile_arr.shape[0] >= src_y + src_h and tile_arr.shape[1] >= src_x + src_w:
composite[dst_y_start:dst_y_end, dst_x_start:dst_x_end] = \ composite[dst_y_start:dst_y_end, dst_x_start:dst_x_end] = \
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 continue
# Only reach here if inner loop broke (first tile 404)
break
logger.info(f"{tiles_downloaded} tuiles IGN téléchargées ({layer})") if tiles_404 > 0 and tiles_downloaded == 0:
if tiles_downloaded == 0: # No tiles at this zoom, try lower
return None continue
return composite
logger.info(f"{tiles_downloaded} tuiles IGN téléchargées ({layer})")
if tiles_downloaded == 0:
continue
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):