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,58 +138,75 @@ 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}"
try: # 2 tentatives : un accident réseau (reset, timeout) ne doit
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) # pas laisser un trou blanc définitif dans la dalle.
with urllib.request.urlopen(req, timeout=10) as response: tile_arr = None
tile_data = response.read() first_tile_404 = False
for attempt in range(2):
try:
req = urllib.request.Request(
url, headers={'User-Agent':
'Mozilla/5.0 (compatible; lidar-archeo-pipeline)'})
with urllib.request.urlopen(req, timeout=10) as response:
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
tile_origin_x = col * tile_size except urllib.error.HTTPError as e:
tile_origin_y = row * tile_size if e.code == 404:
tiles_404 += 1
px_x = int(tile_origin_x - nw_px_x) # If the very first tile is 404, this zoom is unavailable
px_y = int(tile_origin_y - nw_px_y) if col == col_min and row == row_min:
logger.info(f" Zoom {zoom} non disponible (404) — essai zoom inférieur")
x_off = max(0, -px_x) first_tile_404 = True
y_off = max(0, -px_y) else:
dst_x_start = max(0, px_x) tiles_failed += 1
dst_y_start = max(0, px_y) break # erreur HTTP : inutile de réessayer
dst_x_end = min(out_width, px_x + tile_size) except Exception:
dst_y_end = min(out_height, px_y + tile_size) tiles_failed += 1
if attempt == 0:
src_x = x_off time.sleep(0.5)
src_y = y_off if first_tile_404:
src_w = dst_x_end - dst_x_start break
src_h = dst_y_end - dst_y_start if tile_arr is None:
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 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 continue
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)
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
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
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(