diff --git a/lidar_pipeline/ign.py b/lidar_pipeline/ign.py index 56138ab..f7c410d 100644 --- a/lidar_pipeline/ign.py +++ b/lidar_pipeline/ign.py @@ -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,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_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}" - try: - req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) - with urllib.request.urlopen(req, timeout=10) as response: - tile_data = response.read() + # 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 (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) - - 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 - - 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: + 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 + + 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: 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(