diff --git a/lidar_pipeline/ign.py b/lidar_pipeline/ign.py index dd4b128..6d1d985 100644 --- a/lidar_pipeline/ign.py +++ b/lidar_pipeline/ign.py @@ -76,6 +76,8 @@ def _lat_lon_to_px(lat, lon, zoom, tile_size=256): def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15): """Download IGN WMTS tiles for the given bounds using Web Mercator (PM). + If the first tile returns 404, automatically retries at lower zoom levels. + Args: min_x, max_x, min_y, max_y: Bounds in Lambert 93. layer: IGN WMTS layer name. @@ -106,76 +108,100 @@ def download_ign_tiles(min_x, max_x, min_y, max_y, layer, zoom_level=15): tile_matrix_set = "PM" tile_size = 256 - col_min, row_min = _lat_lon_to_tile(nw_lat, nw_lon, zoom_level) - col_max, row_max = _lat_lon_to_tile(se_lat, se_lon, zoom_level) + # Try downloading at the requested zoom level; fall back to lower zooms on 404 + min_zoom = 15 + for zoom in range(zoom_level, min_zoom - 1, -1): + col_min, row_min = _lat_lon_to_tile(nw_lat, nw_lon, zoom) + col_max, row_max = _lat_lon_to_tile(se_lat, se_lon, zoom) - nw_px_x, nw_px_y = _lat_lon_to_px(nw_lat, nw_lon, zoom_level) - se_px_x, se_px_y = _lat_lon_to_px(se_lat, se_lon, zoom_level) + nw_px_x, nw_px_y = _lat_lon_to_px(nw_lat, nw_lon, zoom) + se_px_x, se_px_y = _lat_lon_to_px(se_lat, se_lon, zoom) - out_width = int(se_px_x - nw_px_x) - out_height = int(se_px_y - nw_px_y) + out_width = int(se_px_x - nw_px_x) + out_height = int(se_px_y - nw_px_y) - if out_width <= 0 or out_height <= 0 or out_width > 10000 or out_height > 10000: - logger.warning(f" Image IGN trop grande: {out_width}x{out_height}px — abandon") - return None + if out_width <= 0 or out_height <= 0 or out_width > 10000 or out_height > 10000: + logger.warning(f" Image IGN trop grande: {out_width}x{out_height}px — zoom {zoom} abandon") + continue - total_tiles = (col_max - col_min + 1) * (row_max - row_min + 1) - logger.info(f" Zoom {zoom_level}: {total_tiles} tuiles à télécharger ({out_width}x{out_height}px)") + total_tiles = (col_max - col_min + 1) * (row_max - row_min + 1) + if zoom < zoom_level: + logger.info(f" ↓ Retry zoom {zoom_level}→{zoom}: {total_tiles} tuiles ({out_width}x{out_height}px)") + else: + logger.info(f" Zoom {zoom}: {total_tiles} tuiles à télécharger ({out_width}x{out_height}px)") - composite = np.full((out_height, out_width, 3), 255, dtype=np.uint8) + composite = np.full((out_height, out_width, 3), 255, dtype=np.uint8) - tiles_downloaded = 0 - fmt = "image/png" if 'PLAN' in layer else "image/jpeg" + tiles_downloaded = 0 + tiles_404 = 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_level}&TILECOL={col}&TILEROW={row}" - f"&FORMAT={fmt}" - ) + 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}" + ) - 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() - tile_img = PILImage.open(io.BytesIO(tile_data)).convert('RGB') - tile_arr = np.array(tile_img) + 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() + 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 + 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) + 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) + 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 + 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 + 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 Exception as e: - if tiles_downloaded == 0 and col == col_min and row == row_min: - logger.error(f" ✗ Erreur tuile IGN: {e}") + 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) + break - logger.info(f" → {tiles_downloaded} tuiles IGN téléchargées ({layer})") - if tiles_downloaded == 0: - return None - return composite + if tiles_404 > 0 and tiles_downloaded == 0: + # No tiles at this zoom, try lower + continue + + logger.info(f" → {tiles_downloaded} tuiles IGN téléchargées ({layer})") + if tiles_downloaded == 0: + continue + return composite + + logger.error(" ✗ Aucun zoom disponible pour cette zone") + return None def generate_ign_overlay(dem_file, basename, vis_dir, resolution, layer, title, legend_label, description, out_suffix):