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:
@ -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):
|
||||
"""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:
|
||||
min_x, max_x, min_y, max_y: Bounds in Lambert 93.
|
||||
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_size = 256
|
||||
|
||||
col_min, row_min = _lat_lon_to_tile(nw_lat, nw_lon, zoom_level)
|
||||
col_max, row_max = _lat_lon_to_tile(se_lat, se_lon, zoom_level)
|
||||
# Try downloading at the requested zoom level; fall back to lower zooms on 404
|
||||
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)
|
||||
se_px_x, se_px_y = _lat_lon_to_px(se_lat, se_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)
|
||||
|
||||
out_width = int(se_px_x - nw_px_x)
|
||||
out_height = int(se_px_y - nw_px_y)
|
||||
out_width = int(se_px_x - nw_px_x)
|
||||
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:
|
||||
logger.warning(f" Image IGN trop grande: {out_width}x{out_height}px — abandon")
|
||||
return None
|
||||
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 — zoom {zoom} abandon")
|
||||
continue
|
||||
|
||||
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)")
|
||||
total_tiles = (col_max - col_min + 1) * (row_max - row_min + 1)
|
||||
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
|
||||
fmt = "image/png" if 'PLAN' in layer else "image/jpeg"
|
||||
tiles_downloaded = 0
|
||||
tiles_404 = 0
|
||||
fmt = "image/png" if 'PLAN' in layer else "image/jpeg"
|
||||
|
||||
for col in range(col_min, col_max + 1):
|
||||
for row in range(row_min, row_max + 1):
|
||||
url = (
|
||||
f"{wmts_url}?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile"
|
||||
f"&LAYER={layer}&STYLE=normal"
|
||||
f"&TILEMATRIXSET={tile_matrix_set}"
|
||||
f"&TILEMATRIX={zoom_level}&TILECOL={col}&TILEROW={row}"
|
||||
f"&FORMAT={fmt}"
|
||||
)
|
||||
for col in range(col_min, col_max + 1):
|
||||
for row in range(row_min, row_max + 1):
|
||||
url = (
|
||||
f"{wmts_url}?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile"
|
||||
f"&LAYER={layer}&STYLE=normal"
|
||||
f"&TILEMATRIXSET={tile_matrix_set}"
|
||||
f"&TILEMATRIX={zoom}&TILECOL={col}&TILEROW={row}"
|
||||
f"&FORMAT={fmt}"
|
||||
)
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
tile_data = response.read()
|
||||
tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB')
|
||||
tile_arr = np.array(tile_img)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
tile_data = response.read()
|
||||
tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB')
|
||||
tile_arr = np.array(tile_img)
|
||||
|
||||
tile_origin_x = col * tile_size
|
||||
tile_origin_y = row * tile_size
|
||||
tile_origin_x = col * tile_size
|
||||
tile_origin_y = row * tile_size
|
||||
|
||||
px_x = int(tile_origin_x - nw_px_x)
|
||||
px_y = int(tile_origin_y - nw_px_y)
|
||||
px_x = int(tile_origin_x - nw_px_x)
|
||||
px_y = int(tile_origin_y - nw_px_y)
|
||||
|
||||
x_off = max(0, -px_x)
|
||||
y_off = max(0, -px_y)
|
||||
dst_x_start = max(0, px_x)
|
||||
dst_y_start = max(0, px_y)
|
||||
dst_x_end = min(out_width, px_x + tile_size)
|
||||
dst_y_end = min(out_height, px_y + tile_size)
|
||||
x_off = max(0, -px_x)
|
||||
y_off = max(0, -px_y)
|
||||
dst_x_start = max(0, px_x)
|
||||
dst_y_start = max(0, px_y)
|
||||
dst_x_end = min(out_width, px_x + tile_size)
|
||||
dst_y_end = min(out_height, px_y + tile_size)
|
||||
|
||||
src_x = x_off
|
||||
src_y = y_off
|
||||
src_w = dst_x_end - dst_x_start
|
||||
src_h = dst_y_end - dst_y_start
|
||||
src_x = x_off
|
||||
src_y = y_off
|
||||
src_w = dst_x_end - dst_x_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:
|
||||
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]
|
||||
tiles_downloaded += 1
|
||||
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] = \
|
||||
tile_arr[src_y:src_y+src_h, src_x:src_x+src_w]
|
||||
tiles_downloaded += 1
|
||||
|
||||
except Exception as e:
|
||||
if tiles_downloaded == 0 and col == col_min and row == row_min:
|
||||
logger.error(f" ✗ Erreur tuile IGN: {e}")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
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
|
||||
|
||||
logger.info(f" → {tiles_downloaded} tuiles IGN téléchargées ({layer})")
|
||||
if tiles_downloaded == 0:
|
||||
return None
|
||||
return composite
|
||||
if tiles_404 > 0 and tiles_downloaded == 0:
|
||||
# No tiles at this zoom, try lower
|
||||
continue
|
||||
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user