Rendre robustes les tuiles WMTS IGN : retry, urlencode, User-Agent, latitude bornée

This commit is contained in:
Antoine Jacquin
2026-09-18 21:19:23 +02:00
parent 777b0c3f98
commit 394dd27375

View File

@ -59,6 +59,7 @@ def _lat_lon_to_tile(lat, lon, zoom):
"""Convert lat/lon to Web Mercator tile coordinates.""" """Convert lat/lon to Web Mercator tile coordinates."""
n = 2 ** zoom n = 2 ** zoom
col = int((lon + 180) / 360 * n) col = int((lon + 180) / 360 * n)
lat = max(-85.0511, min(85.0511, lat)) # borne Mercator : évite log/cos invalides
lat_rad = math.radians(lat) lat_rad = math.radians(lat)
row = int((1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n) row = int((1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n)
return col, row return col, row
@ -68,6 +69,7 @@ def _lat_lon_to_px(lat, lon, zoom, tile_size=256):
"""Convert lat/lon to Web Mercator pixel coordinates.""" """Convert lat/lon to Web Mercator pixel coordinates."""
n = 2 ** zoom n = 2 ** zoom
px_x = (lon + 180) / 360 * n * tile_size px_x = (lon + 180) / 360 * n * tile_size
lat = max(-85.0511, min(85.0511, lat))
lat_rad = math.radians(lat) lat_rad = math.radians(lat)
px_y = (1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n * tile_size px_y = (1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n * tile_size
return px_x, px_y return px_x, px_y
@ -87,6 +89,8 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15, min_zoo
Returns: Returns:
numpy array (H, W, 3) uint8, or None on failure. numpy array (H, W, 3) uint8, or None on failure.
""" """
import urllib.error
import urllib.parse
import urllib.request import urllib.request
import io import io
from PIL import Image as PILImage from PIL import Image as PILImage
@ -134,24 +138,52 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15, min_zoo
tiles_downloaded = 0 tiles_downloaded = 0
tiles_404 = 0 tiles_404 = 0
tiles_failed = 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):
for row in range(row_min, row_max + 1): for row in range(row_min, row_max + 1):
url = ( params = urllib.parse.urlencode({
f"{wmts_url}?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile" "SERVICE": "WMTS", "VERSION": "1.0.0", "REQUEST": "GetTile",
f"&LAYER={layer}&STYLE=normal" "LAYER": layer, "STYLE": "normal",
f"&TILEMATRIXSET={tile_matrix_set}" "TILEMATRIXSET": tile_matrix_set,
f"&TILEMATRIX={zoom}&TILECOL={col}&TILEROW={row}" "TILEMATRIX": zoom, "TILECOL": col, "TILEROW": row,
f"&FORMAT={fmt}" "FORMAT": fmt,
) })
url = f"{wmts_url}?{params}"
# 2 tentatives : un accident réseau (reset, timeout) ne doit
# pas laisser un trou blanc définitif dans la dalle.
tile_arr = None
first_tile_404 = False
for attempt in range(2):
try: try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) req = urllib.request.Request(
url, headers={'User-Agent':
'Mozilla/5.0 (compatible; lidar-archeo-pipeline)'})
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)
break
except urllib.error.HTTPError as e:
if e.code == 404:
tiles_404 += 1
# If the very first tile is 404, this zoom is unavailable
if col == col_min and row == row_min:
logger.info(f" Zoom {zoom} non disponible (404) — essai zoom inférieur")
first_tile_404 = True
else:
tiles_failed += 1
break # erreur HTTP : inutile de réessayer
except Exception:
tiles_failed += 1
if attempt == 0:
time.sleep(0.5)
if first_tile_404:
break
if tile_arr is None:
continue
tile_origin_x = col * tile_size tile_origin_x = col * tile_size
tile_origin_y = row * tile_size tile_origin_y = row * tile_size
@ -175,17 +207,6 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15, min_zoo
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 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: else:
continue continue
# Only reach here if inner loop broke (first tile 404) # Only reach here if inner loop broke (first tile 404)
@ -195,6 +216,9 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15, min_zoo
# No tiles at this zoom, try lower # No tiles at this zoom, try lower
continue continue
if tiles_failed > 0:
logger.warning(f" {tiles_failed} tuile(s) IGN en échec réseau ({layer}) "
f"— la dalle peut avoir des trous")
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:
continue continue
@ -262,7 +286,7 @@ def generate_ign_overlay(dem_file, basename, vis_dir, resolution, layer, title,
from PIL import Image as PILImage from PIL import Image as PILImage
ign_pil = PILImage.fromarray(result) ign_pil = PILImage.fromarray(result)
ign_resized = ign_pil.resize((width, height), PILImage.LANCZOS) ign_resized = ign_pil.resize((width, height), PILImage.Resampling.LANCZOS)
ign_arr = np.array(ign_resized) ign_arr = np.array(ign_resized)
with rasterio.open( with rasterio.open(