Translate the whole project to English and fix outdated comments and help

Comments, docstrings, logs, CLI help, map UI, legends, PDF sheet, scripts,
compose files and AGENTS.md are now English. Data keys stay unchanged
(relief_oriente, densite_sol, visualisations/, API JSON keys, link params).
Wrong comments and help defaults found along the way are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2026-09-27 23:16:45 +02:00
parent cc1c22d2b8
commit fb892ea9f2
52 changed files with 4356 additions and 4323 deletions

View File

@ -1,13 +1,13 @@
"""Téléchargement des dalles LiDAR HD de l'IGN pour les tuiles non générées.
"""Download of IGN LiDAR HD tiles for tiles not yet generated.
Catalogue STAC (à jour) : https://browser.stac.teledetection.fr/collections/lidarhd
API : https://api.stac.teledetection.fr/collections/lidarhd/items
Fichiers (géoplateforme): https://data.geopf.fr/telechargement/download/...
STAC catalogue (current) : https://browser.stac.teledetection.fr/collections/lidarhd
API : https://api.stac.teledetection.fr/collections/lidarhd/items
Files (Géoplateforme) : https://data.geopf.fr/telechargement/download/...
Chaque dalle couvre 1 km × 1 km en Lambert 93 et est nommée par son coin
nord-ouest : LHD_FXX_{col}_{row}_PTS_LAMB93_IGN69.copc.laz
(col = X ouest en km, row = Y nord en km, cf. propriété STAC
"lidarhd:coordonnees_NW" au format "0816-6847").
Each tile covers 1 km × 1 km in Lambert 93 and is named after its north-west
corner: LHD_FXX_{col}_{row}_PTS_LAMB93_IGN69.copc.laz
(col = west X in km, row = north Y in km, see the STAC property
"lidarhd:coordonnees_NW" in the "0816-6847" format).
"""
import json
@ -24,38 +24,38 @@ _HEADERS = {"User-Agent": "Mozilla/5.0 (lidar-archeo-pipeline)"}
def parse_tile_specs(args):
"""Convertit des spécifications "col,row" ou "col:row" en liste de tuples.
"""Convert "col,row" or "col:row" specifications into a list of tuples.
Args:
args: liste de chaînes (ex: ["1055,6882", "1056:6883"]).
args: list of strings (e.g. ["1055,6882", "1056:6883"]).
Returns:
Liste de tuples (col, row).
List of (col, row) tuples.
Raises:
ValueError: si une spécification est mal formée.
ValueError: if a specification is malformed.
"""
specs = []
for raw in args:
text = raw.strip().replace(":", ",").replace(";", ",")
parts = [p.strip() for p in text.split(",") if p.strip()]
if len(parts) != 2:
raise ValueError(f"Spécification de tuile invalide: {raw!r} (attendu: col,row)")
raise ValueError(f"Invalid tile specification: {raw!r} (expected: col,row)")
try:
col, row = int(parts[0]), int(parts[1])
except ValueError:
raise ValueError(f"Spécification de tuile invalide: {raw!r} (col et row doivent être des entiers)")
raise ValueError(f"Invalid tile specification: {raw!r} (col and row must be integers)")
specs.append((col, row))
return specs
def tile_filename(col, row):
"""Nom de fichier LAZ standard d'une dalle (col, row)."""
"""Standard LAZ file name of a (col, row) tile."""
return f"LHD_FXX_{col:04d}_{row:04d}_PTS_LAMB93_IGN69.copc.laz"
def _report_download(output_dir, tile, state, detail=None):
"""Émet un événement de téléchargement pour la file de génération (best-effort)."""
"""Emit a download event for the generation queue (best-effort)."""
if output_dir is None:
return
from .progress import report_event
@ -63,7 +63,7 @@ def _report_download(output_dir, tile, state, detail=None):
def _bbox_wgs84(col, row):
"""Bbox WGS84 de la dalle (col,row) pour la requête STAC (peut être élargie)."""
"""WGS84 bbox of the (col,row) tile for the STAC query (padded)."""
try:
from rasterio.warp import transform as warp_transform
xs = [col * 1000, (col + 1) * 1000, col * 1000, (col + 1) * 1000]
@ -72,7 +72,7 @@ def _bbox_wgs84(col, row):
except Exception:
lons = None
if lons is None:
try: # image légère (carte) : pyproj sans rasterio
try: # lightweight (map) image: pyproj without rasterio
from pyproj import Transformer
tr = Transformer.from_crs("EPSG:2154", "EPSG:4326", always_xy=True)
xs = [col * 1000, (col + 1) * 1000, col * 1000, (col + 1) * 1000]
@ -87,12 +87,12 @@ def _bbox_wgs84(col, row):
for y in ((row - 1) * 1000, row * 1000)]
lons = [p[0] for p in pts]
lats = [p[1] for p in pts]
pad = 0.005 # ~500 m de marge pour éviter les erreurs d'arrondi aux bords
pad = 0.005 # ~500 m margin to absorb rounding errors at the edges
return (min(lons) - pad, min(lats) - pad, max(lons) + pad, max(lats) + pad)
def match_feature(features, col, row):
"""Retourne l'item STAC correspondant à la dalle (col,row), sinon None."""
"""Return the STAC item matching the (col,row) tile, else None."""
want = f"{col:04d}-{row:04d}"
for feature in features:
props = feature.get("properties", {})
@ -102,20 +102,20 @@ def match_feature(features, col, row):
def find_tile_url(col, row, timeout=20, max_pages=5):
"""Cherche l'URL de téléchargement de la dalle (col,row) dans le catalogue STAC.
"""Look up the download URL of the (col,row) tile in the STAC catalogue.
Returns:
URL (str) ou None si la dalle n'est pas (encore) publiée par l'IGN.
URL (str), or None if the tile is not (yet) published by IGN.
"""
feature = find_tile_feature(col, row, timeout=timeout, max_pages=max_pages)
return feature.get("assets", {}).get("data", {}).get("href") if feature else None
def tile_ign_metadata(col, row, timeout=10):
"""Fiche IGN d'une dalle : acquisition, capteurs, édition, téléchargement.
"""IGN record of a tile: acquisition, sensors, edition, download.
Returns:
dict normalisé (found=False si la dalle n'est pas publiée).
Normalised dict (found=False if the tile is not published).
"""
feature = find_tile_feature(col, row, timeout=timeout)
if not feature:
@ -141,10 +141,10 @@ def tile_ign_metadata(col, row, timeout=10):
def find_tile_feature(col, row, timeout=20, max_pages=5):
"""Item STAC de la dalle (col,row), ou None si non publiée.
"""STAC item of the (col,row) tile, or None if not published.
Suit la pagination du catalogue (lien rel=next) : sans elle, une dalle
au-delà de la première page de résultats semblait « introuvable ».
Follows the catalogue pagination (rel=next link): without it, a tile
beyond the first page of results appeared "not found".
"""
w, s, e, n = _bbox_wgs84(col, row)
query = urllib.parse.urlencode({"bbox": f"{w:.6f},{s:.6f},{e:.6f},{n:.6f}", "limit": 50})
@ -165,11 +165,11 @@ def find_tile_feature(col, row, timeout=20, max_pages=5):
def download_file(url, dest_path, timeout=120, chunk=1024 * 1024):
"""Télécharge url vers dest_path en streaming (écriture atomique).
"""Stream url to dest_path (atomic write).
Écrit dans un .part puis renomme : un run interrompu (SIGKILL, panne)
ne laisse jamais un LAZ partiel qu'un run suivant considérerait comme
déjà téléchargé. Retourne la taille en octets.
Writes to a .part file then renames it: an interrupted run (SIGKILL,
crash) never leaves a partial LAZ that a later run would treat as
already downloaded. Returns the size in bytes.
"""
import os
dest_path = Path(dest_path)
@ -186,8 +186,8 @@ def download_file(url, dest_path, timeout=120, chunk=1024 * 1024):
out.write(block)
done += len(block)
elapsed = time.time() - t0
logger.info(f" {done / 1e6:.0f} Mo en {elapsed:.0f}s"
f" ({done / 1e6 / max(elapsed, 0.1):.1f} Mo/s)")
logger.info(f" {done / 1e6:.0f} MB in {elapsed:.0f}s"
f" ({done / 1e6 / max(elapsed, 0.1):.1f} MB/s)")
os.replace(tmp, dest_path)
except BaseException:
tmp.unlink(missing_ok=True)
@ -196,25 +196,26 @@ def download_file(url, dest_path, timeout=120, chunk=1024 * 1024):
def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0.5,), force=False):
"""Télécharge les dalles IGN spécifiées, sauf celles inutiles au traitement.
"""Download the given IGN tiles, except those not needed for processing.
Args:
input_dir: dossier des fichiers LAZ (écriture autorisée requise).
specs: liste de tuples (col, row).
output_dir: dossier de sortie (optionnel) — permet d'ignorer les
tuiles déjà complètes.
only_viz: noms d'étapes des visualisations demandées (ex: ['aspect']).
Si fourni, une tuile n'est ignorée que si elle possède déjà
toutes ces visualisations aux résolutions demandées —
sinon son LAZ est (re)téléchargé pour compléter les
visualisations manquantes.
resolutions: résolutions attendues (m/px) pour considérer une tuile
complète quand only_viz est fourni.
force: True pour télécharger même les tuiles complètes (régénération :
leur LAZ est requis pour retraiter avec --force).
input_dir: LAZ files directory (must be writable).
specs: list of (col, row) tuples.
output_dir: output directory (optional) — used to skip tiles that
are already complete.
only_viz: step names of the requested visualisations (e.g. ['aspect']).
If given, a tile is skipped only if it already has all
these visualisations at the requested resolutions —
otherwise its LAZ is (re)downloaded to fill in the
missing ones. If not given, a tile is skipped as soon as
any visualisation exists for it.
resolutions: expected resolutions (m/px) for a tile to count as
complete when only_viz is given.
force: True to download even complete tiles (regeneration: their
LAZ is needed to reprocess with --force).
Returns:
Liste des chemins téléchargés.
List of downloaded paths.
"""
from .dtm import EDGE_NEIGHBORS_DIRNAME
input_dir = Path(input_dir)
@ -224,9 +225,9 @@ def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0
keys = [step_to_keyword(v) for v in only_viz]
complete = cells_with_all_viz(Path(output_dir) / "visualisations",
keys, resolutions)
# Explicite : jamais de promotion quand on télécharge directement DANS
# edge_neighbors/ (appel de _fetch_edge_neighbors avec input_dir=edge_dir)
# — sinon une dalle voisine pourrait se retrouver déplacée vers elle-même.
# Explicit: never promote when downloading directly INTO edge_neighbors/
# (_fetch_edge_neighbors call with input_dir=edge_dir) — otherwise a
# neighbour tile could end up moved onto itself.
is_edge_dir = input_dir.name == EDGE_NEIGHBORS_DIRNAME
downloaded = []
for col, row in specs:
@ -234,55 +235,55 @@ def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0
tile = name[:-len(".copc.laz")] if name.endswith(".copc.laz") else name
dest = input_dir / name
if dest.exists():
logger.info(f" {name} : déjà présent dans input/ — aucun téléchargement")
_report_download(output_dir, tile, "skip", "déjà dans input/")
logger.info(f" {name}: already in input/ — no download")
_report_download(output_dir, tile, "skip", "already in input/")
continue
if not is_edge_dir:
# dest.exists() ci-dessus ne teste que le nom exact : un ".part"
# voisin n'y correspond jamais, donc .exists() suffit à exclure
# un téléchargement voisin encore en cours.
# dest.exists() above only checks the exact name: a neighbouring
# ".part" never matches it, so .exists() is enough to rule out a
# neighbour download still in progress.
neighbor_copy = input_dir / EDGE_NEIGHBORS_DIRNAME / name
if neighbor_copy.exists():
import os
os.replace(neighbor_copy, dest)
logger.info(f" {name} : déjà téléchargée comme voisine "
f"— déplacée dans input/")
logger.info(f" {name}: already downloaded as a neighbour "
f"— moved to input/")
_report_download(output_dir, tile, "skip",
"déjà téléchargée comme voisine — déplacée dans input/")
"already downloaded as a neighbour — moved to input/")
continue
if output_dir is not None and not force:
if complete is not None:
if (col, row) in complete:
logger.info(f" {name} : visualisations déjà complètes — ignorée")
_report_download(output_dir, tile, "skip", "visualisations déjà complètes")
logger.info(f" {name}: visualisations already complete — skipped")
_report_download(output_dir, tile, "skip", "visualisations already complete")
continue
else:
vis_dir = Path(output_dir) / "visualisations"
if list(vis_dir.glob(f"LHD_FXX_{col:04d}_{row:04d}_PTS*")):
logger.info(f" {name} : visualisations déjà générées — ignorée")
_report_download(output_dir, tile, "skip", "visualisations déjà générées")
logger.info(f" {name}: visualisations already generated — skipped")
_report_download(output_dir, tile, "skip", "visualisations already generated")
continue
logger.info(f" {name} : recherche dans le catalogue IGN...")
_report_download(output_dir, tile, "start", "catalogue IGN")
logger.info(f" {name}: searching the IGN catalogue...")
_report_download(output_dir, tile, "start", "IGN catalogue")
try:
url = find_tile_url(col, row)
except Exception as e:
logger.warning(f" ✗ {name} : erreur catalogue ({e})")
_report_download(output_dir, tile, "fail", f"erreur catalogue : {e}")
logger.warning(f" ✗ {name}: catalogue error ({e})")
_report_download(output_dir, tile, "fail", f"catalogue error: {e}")
continue
if not url:
logger.warning(f" ✗ {name} : introuvable dans le catalogue IGN (zone non publiée ?)")
_report_download(output_dir, tile, "fail", "introuvable dans le catalogue IGN")
logger.warning(f" ✗ {name}: not found in the IGN catalogue (area not published?)")
_report_download(output_dir, tile, "fail", "not found in the IGN catalogue")
continue
logger.info(f" {name} : téléchargement depuis la géoplateforme...")
_report_download(output_dir, tile, "start", "géoplateforme")
logger.info(f" {name}: downloading from the Géoplateforme...")
_report_download(output_dir, tile, "start", "Géoplateforme")
try:
download_file(url, dest)
logger.info(f" ✓ {name} téléchargée")
logger.info(f" ✓ {name} downloaded")
_report_download(output_dir, tile, "ok")
downloaded.append(dest)
except Exception as e:
dest.unlink(missing_ok=True)
logger.warning(f" ✗ {name} : échec du téléchargement ({e})")
_report_download(output_dir, tile, "fail", f"échec du téléchargement : {e}")
logger.warning(f" ✗ {name}: download failed ({e})")
_report_download(output_dir, tile, "fail", f"download failed: {e}")
return downloaded