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,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(
|
||||||
|
|||||||
Reference in New Issue
Block a user