Rendre robustes les tuiles WMTS IGN : retry, urlencode, User-Agent, latitude bornée
This commit is contained in:
@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user