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,12 +1,12 @@
"""Qualité des données LiDAR par dalle : densité de points sol et date d'acquisition.
"""Per-tile LiDAR data quality: ground point density and acquisition date.
Un sidecar JSON par dalle (output/quality/{basename}.json) est écrit par le
pipeline, recopié dans l'inventaire (index_tiles.json, table `quality`) et
conservé sur les machines légères : l'encart qualité de l'export PDF le lit
depuis le disque local, amont joignable ou non.
One JSON sidecar per tile (output/quality/{basename}.json) is written by the
pipeline, copied into the inventory (index_tiles.json, `quality` table) and
kept on the lightweight machines: the quality inset of the PDF export reads it
from the local disk, whether the upstream is reachable or not.
numpy n'est importé que par le calcul : l'image légère (sans numpy) lit et
écrit les sidecars.
numpy is only imported by the computation: the lightweight image (without
numpy) reads and writes the sidecars.
"""
import json
@ -20,32 +20,35 @@ logger = logging.getLogger("lidar")
QUALITY_VERSION = 1
QUALITY_DIRNAME = "quality"
DENSITY_CELL_M = 50.0 # maille de la grille de densité (20 × 20 par dalle)
EMPTY_PIXEL_M = 0.2 # pixel du MNT : part sans point sol ≈ part interpolée
DENSITY_CELL_M = 50.0 # density grid cell size (20 × 20 per tile)
EMPTY_PIXEL_M = 0.2 # DTM pixel: share without ground point ≈ interpolated share
_GPS_EPOCH = datetime(1980, 1, 6, tzinfo=timezone.utc)
_GPS_ADJUSTED_OFFSET = 1e9 # temps GPS ajusté standard = secondes GPS − 1e9
_GPS_ADJUSTED_OFFSET = 1e9 # standard adjusted GPS time = GPS seconds − 1e9
def gps_adjusted_to_date(t):
"""Temps GPS ajusté standard → date ISO (UTC ; secondes intercalaires
négligées : précision au jour)."""
"""Standard adjusted GPS time → ISO date (UTC; leap seconds ignored:
accurate to the day)."""
return (_GPS_EPOCH + timedelta(seconds=float(t) + _GPS_ADJUSTED_OFFSET)).date().isoformat()
def compute_quality(x, y, gps_time, bounds, gps_adjusted=True, header_date=None):
"""Mesure la qualité d'une dalle à partir de ses points sol.
"""Measure the quality of a tile from its ground points.
Args:
x, y: coordonnées L93 des points sol.
gps_time: temps GPS des points (ou None).
bounds: emprise nominale (min_x, min_y, max_x, max_y) ; les points
hors emprise (bande de raccord) sont ignorés.
gps_adjusted: True si gps_time est en temps GPS ajusté standard.
header_date: date ISO de l'en-tête LAS (repli des dates).
x, y: L93 coordinates of the ground points.
gps_time: GPS time of the points (or None).
bounds: nominal footprint (min_x, min_y, max_x, max_y); points
outside it (edge buffer) are ignored.
gps_adjusted: True if gps_time is standard adjusted GPS time.
header_date: ISO date from the LAS header (date fallback).
Returns:
dict JSON-sérialisable (cf. spec, section 1).
JSON-serializable dict: version, ground_density (pts/m²),
density_grid (pts/m² per 50 m cell, rows north to south),
empty_fraction (share of 0.2 m pixels without a ground point),
acq_start / acq_end (ISO dates) and acq_source ("gps", "header" or None).
"""
import numpy as np
@ -98,12 +101,12 @@ def compute_quality(x, y, gps_time, bounds, gps_adjusted=True, header_date=None)
def quality_path(output_dir, basename):
"""Chemin du sidecar qualité d'une dalle."""
"""Path of a tile's quality sidecar."""
return Path(output_dir) / QUALITY_DIRNAME / f"{basename}.json"
def write_quality(output_dir, basename, data):
"""Écrit le sidecar (atomique) ; False si le contenu est déjà identique."""
"""Write the sidecar (atomically); False if the content is already identical."""
path = quality_path(output_dir, basename)
payload = json.dumps(data, ensure_ascii=False, sort_keys=True)
try:
@ -127,7 +130,7 @@ def write_quality(output_dir, basename, data):
def read_quality(output_dir, basename):
"""Sidecar d'une dalle, ou None (absent, illisible, autre version)."""
"""A tile's sidecar, or None (missing, unreadable, other version)."""
try:
data = json.loads(quality_path(output_dir, basename).read_text(encoding="utf-8"))
except (OSError, ValueError):
@ -138,7 +141,7 @@ def read_quality(output_dir, basename):
def load_quality_table(output_dir):
"""Tous les sidecars valides : {basename: sidecar}."""
"""All valid sidecars: {basename: sidecar}."""
folder = Path(output_dir) / QUALITY_DIRNAME
table = {}
if not folder.is_dir():
@ -154,7 +157,7 @@ _LAZ_EXTS = (".copc.laz", ".copc.las", ".laz", ".las")
def _laz_basename(path):
"""Nom de dalle sans extension LiDAR (.copc.laz compris)."""
"""Tile name without its LiDAR extension (.copc.laz included)."""
name = Path(path).name
for ext in _LAZ_EXTS:
if name.lower().endswith(ext):
@ -163,7 +166,7 @@ def _laz_basename(path):
def _cell_bounds_of(basename):
"""Emprise nominale 1 km d'une dalle LHD, ou None hors motif LHD."""
"""Nominal 1 km footprint of an LHD tile, or None outside the LHD pattern."""
from .index import parse_basename_coords
coords = parse_basename_coords(basename)
if coords is None:
@ -173,9 +176,9 @@ def _cell_bounds_of(basename):
def quality_from_las(las_path, bounds, codes=None):
"""Qualité mesurée depuis un LAS/LAZ ; None en cas d'échec (jamais d'exception).
"""Quality measured from a LAS/LAZ; None on failure (never raises).
codes : classes à retenir (retours ≥ 1) ; None = fichier déjà filtré sol.
codes: classes to keep (returns ≥ 1); None = file already filtered to ground.
"""
try:
import laspy
@ -195,16 +198,16 @@ def quality_from_las(las_path, bounds, codes=None):
return compute_quality(np.asarray(las.x)[keep], np.asarray(las.y)[keep], gps,
bounds, gps_adjusted=adjusted,
header_date=created.isoformat() if created else None)
except Exception as e: # noqa: BLE001 — qualité best-effort
logger.warning(f" Mesure de qualité impossible ({Path(las_path).name}) : {e}")
except Exception as e: # noqa: BLE001 — best-effort quality
logger.warning(f" Could not measure quality ({Path(las_path).name}): {e}")
return None
def ensure_quality(las_path, basename, output_dir, codes=None):
"""Garantit le sidecar qualité d'une dalle (calcul si absent ou périmé).
"""Ensure a tile's quality sidecar exists (computed if missing or outdated).
Returns:
True si un sidecar valide existe à la sortie.
True if a valid sidecar exists on return.
"""
if read_quality(output_dir, basename) is not None:
return True
@ -217,16 +220,16 @@ def ensure_quality(las_path, basename, output_dir, codes=None):
try:
write_quality(output_dir, basename, data)
except OSError as e:
logger.warning(f" Écriture du sidecar qualité impossible : {e}")
logger.warning(f" Could not write the quality sidecar: {e}")
return False
return True
def backfill_quality(input_dir, output_dir, codes=(2,)):
"""Rattrapage : sidecar qualité de chaque LAZ LHD d'input_dir qui n'en a pas.
"""Backfill: quality sidecar for every LHD LAZ in input_dir that lacks one.
Returns:
Nombre de sidecars écrits.
Number of sidecars written.
"""
files = sorted(p for p in Path(input_dir).iterdir()
if p.is_file() and p.name.lower().endswith(_LAZ_EXTS))
@ -235,7 +238,7 @@ def backfill_quality(input_dir, output_dir, codes=(2,)):
base = _laz_basename(laz)
if read_quality(output_dir, base) is not None or _cell_bounds_of(base) is None:
continue
logger.info(f"Qualité [{i}/{len(files)}] {base}")
logger.info(f"Quality [{i}/{len(files)}] {base}")
if ensure_quality(laz, base, output_dir, codes):
written += 1
return written