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."""
n = 2 ** zoom
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)
row = int((1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n)
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."""
n = 2 ** zoom
px_x = (lon + 180) / 360 * n * tile_size
lat = max(-85.0511, min(85.0511, 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
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:
numpy array (H, W, 3) uint8, or None on failure.
"""
import urllib.error
import urllib.parse
import urllib.request
import io
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_404 = 0
tiles_failed = 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}&TILECOL={col}&TILEROW={row}"
f"&FORMAT={fmt}"
)
params = urllib.parse.urlencode({
"SERVICE": "WMTS", "VERSION": "1.0.0", "REQUEST": "GetTile",
"LAYER": layer, "STYLE": "normal",
"TILEMATRIXSET": tile_matrix_set,
"TILEMATRIX": zoom, "TILECOL": col, "TILEROW": row,
"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:
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:
tile_data = response.read()
tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB')
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_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] = \
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
else:
continue
# 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
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})")
if tiles_downloaded == 0:
continue
@ -262,7 +286,7 @@ def generate_ign_overlay(dem_file, basename, vis_dir, resolution, layer, title,
from PIL import Image as PILImage
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)
with rasterio.open(