Ajouter la fiche de dalle IGN et la rose des vents, imposer la génération
Un clic sélectionne toujours la dalle sous le curseur (cadre sur la carte), générée ou non : emprise, rendu et recalage des passes s'affichent tout de suite ; la fiche IGN (date et heure du scan LiDAR, capteurs, mission, édition, nombre de points, lien de téléchargement du .copc.laz) arrive à part via /api/map/ign, mise en cache, sans jamais bloquer la sélection. Une rose des vents donne la couleur de chaque orientation de pente du relief orienté, avec la même formule CIELAB que le rendu. La génération impose la classification IGN (sol) et le raccord de 100 m avec les dalles voisines : plus de réglage dans la carte, défauts CLI alignés. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -860,12 +860,86 @@ def map_meta():
|
||||
}
|
||||
|
||||
|
||||
# Fiche IGN (catalogue STAC) mise en cache par dalle : un seul appel réseau,
|
||||
# une dalle non publiée est revérifiée au plus une fois par jour.
|
||||
IGN_META_RETRY_S = 86400
|
||||
|
||||
|
||||
def _ign_tile_meta(col, row):
|
||||
"""Métadonnées IGN d'une dalle (acquisition, lien de téléchargement)."""
|
||||
from .fetch_ign import tile_ign_metadata
|
||||
meta_dir = OUTPUT_DIR / "ign_meta"
|
||||
path = meta_dir / f"{col:04d}_{row:04d}.json"
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if data.get("found") or time.time() - data.get("checked", 0) < IGN_META_RETRY_S:
|
||||
return data
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
try:
|
||||
data = tile_ign_metadata(col, row)
|
||||
except Exception as e:
|
||||
logger.warning(f"Catalogue IGN injoignable pour {col}_{row} : {e}")
|
||||
return {"found": False, "error": "catalogue IGN injoignable"}
|
||||
data["checked"] = time.time()
|
||||
try:
|
||||
meta_dir.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
os.replace(tmp, path)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _calage_summary(basename, suffix):
|
||||
"""Résumé du recalage vertical des passes (sidecar du MNT), si présent."""
|
||||
path = OUTPUT_DIR / "DTM" / f"{basename}_dtm{suffix}_stripalign.json"
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
offsets = data.get("offsets") or {}
|
||||
lines = data.get("lines") or {}
|
||||
return {
|
||||
"beams": sorted(set(offsets) | set(lines), key=lambda k: int(k) if str(k).isdigit() else 0),
|
||||
"offsets_cm": {k: round(v * 100, 1) for k, v in offsets.items()},
|
||||
"lines_rms_cm": {k: round(v.get("rms_m", 0) * 100, 1) for k, v in lines.items()},
|
||||
"model": data.get("line_model"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/map/ign")
|
||||
def map_tile_ign(col: int, row: int):
|
||||
"""Fiche IGN d'une dalle (acquisition, capteurs, lien de téléchargement).
|
||||
|
||||
Séparée de /api/map/tile : la sélection d'une dalle s'affiche tout de
|
||||
suite, même si le catalogue IGN est lent ou injoignable.
|
||||
"""
|
||||
return _ign_tile_meta(col, row)
|
||||
|
||||
|
||||
@app.get("/api/map/tile")
|
||||
def map_tile_info(lat: float, lng: float):
|
||||
"""Dalle LHD sous un point WGS84 + métadonnées de génération."""
|
||||
"""Dalle LHD sous un point WGS84 : emprise, génération, recalage.
|
||||
|
||||
Répond toujours (dalle générée ou non) et sans appel réseau ; la fiche
|
||||
IGN se charge à part (/api/map/ign).
|
||||
"""
|
||||
from .index import _collect_tile_metadata, _res_suffix_str, scan_tiles
|
||||
x, y = tiles_mod.wgs84_to_l93(lng, lat)
|
||||
col, row = int(x // 1000), int(y // 1000) + 1
|
||||
x0, y0, x1, y1 = col * 1000, (row - 1) * 1000, (col + 1) * 1000, row * 1000
|
||||
lons, lats = tiles_mod._transformer("EPSG:2154", "EPSG:4326").transform(
|
||||
[x0, x1, x1, x0], [y1, y1, y0, y0])
|
||||
base = {
|
||||
"col": col,
|
||||
"row": row,
|
||||
"name": f"LHD_FXX_{col:04d}_{row:04d}_PTS_LAMB93_IGN69",
|
||||
"l93_bbox": [x0, y0, x1, y1],
|
||||
# coins NO, NE, SE, SO (lat, lng) : cadre de sélection sur la carte
|
||||
"corners": [[round(la, 7), round(lo, 7)] for lo, la in zip(lons, lats)],
|
||||
}
|
||||
best = None
|
||||
for t in scan_tiles(OUTPUT_DIR / "visualisations"):
|
||||
if (t["col"], t["row"]) != (col, row):
|
||||
@ -873,18 +947,19 @@ def map_tile_info(lat: float, lng: float):
|
||||
if best is None or t["resolution"] < best["resolution"]:
|
||||
best = t
|
||||
if best is None:
|
||||
return {"found": False, "col": col, "row": row}
|
||||
return {"found": False, **base}
|
||||
meta = _collect_tile_metadata(best, OUTPUT_DIR / "DTM")
|
||||
suffix = _res_suffix_str(best["resolution"])
|
||||
return {
|
||||
"found": True,
|
||||
"col": col,
|
||||
"row": row,
|
||||
**base,
|
||||
"name": best["basename"],
|
||||
"resolution": best["resolution"],
|
||||
"method": meta.get("method"),
|
||||
"generated": meta.get("generated"),
|
||||
"layers": sorted(best["viz"]),
|
||||
"suffix": _res_suffix_str(best["resolution"]),
|
||||
"suffix": suffix,
|
||||
"calage": _calage_summary(best["basename"], suffix),
|
||||
}
|
||||
|
||||
|
||||
@ -1116,28 +1191,9 @@ class GenerateRequest(BaseModel):
|
||||
tiles: list = Field(..., description="liste [col, row] (entiers km L93)")
|
||||
regenerate: bool = Field(False, description="Régénérer les tuiles déjà générées "
|
||||
"(visualisations refaites, classification conservée)")
|
||||
reclassify: bool = Field(False,
|
||||
description="Relancer la classification du sol même si "
|
||||
"la méthode demandée est déjà en cache "
|
||||
"(--force-classification). Défaut : conserver "
|
||||
"la classification existante ; choisir une "
|
||||
"autre méthode reclassifie de toute façon "
|
||||
"les tuiles concernées")
|
||||
ground_class: str = Field("ign",
|
||||
description="Méthode de classification du sol : "
|
||||
"auto, ign, smrf, csf")
|
||||
ign_classes: str = Field("sol",
|
||||
description="Classes LAS pour le MNT IGN : liste noms ou "
|
||||
"codes séparés par virgules — sol(2), "
|
||||
"unclassified(1), eau(9), virtuel(66), "
|
||||
"pont(17), sursol(64). Mode pur, "
|
||||
"aucune retouche. (défaut: sol)")
|
||||
edge_buffer: bool = Field(False,
|
||||
description="Raccord des bords : étendre le MNT d'une "
|
||||
"bande de 100 m remplie avec les points sol "
|
||||
"des 8 tuiles voisines — rendus continus "
|
||||
"d'une tuile à l'autre (recadrés sur la "
|
||||
"dalle 1 km exacte)")
|
||||
# Classification (IGN, sol seul) et raccord des bords (bande de 100 m prise
|
||||
# aux dalles voisines) sont imposés : plus de réglage côté carte. Les
|
||||
# anciens champs éventuellement envoyés par un client sont ignorés.
|
||||
viz: list = Field(None,
|
||||
description="Visualisations à générer, noms d'étapes du "
|
||||
"pipeline (ex: aspect, wavelet, slope) ; "
|
||||
@ -1349,15 +1405,13 @@ def _queue_summary():
|
||||
EDGE_BUFFER_METERS = 100.0 # raccord des bords : largeur de bande (m)
|
||||
|
||||
|
||||
def _build_command(tiles, regenerate=False, ground_class="ign", ign_classes="sol",
|
||||
viz=None, reclassify=False, edge_buffer=False):
|
||||
def _build_command(tiles, regenerate=False, viz=None):
|
||||
"""Commande de génération : téléchargement IGN + traitement des fichiers.
|
||||
|
||||
Avec regenerate=True, les visualisations des tuiles déjà présentes sont
|
||||
refaites (--force) mais leur classification est conservée. Avec
|
||||
reclassify=True, --force-classification relance la classification même à
|
||||
méthode inchangée. Avec edge_buffer=True, le MNT est étendu d'une bande de
|
||||
EDGE_BUFFER_METERS remplie avec les points sol des tuiles voisines.
|
||||
Classification IGN (sol seul) et raccord des bords (bande de
|
||||
EDGE_BUFFER_METERS remplie avec les points sol des 8 dalles voisines)
|
||||
toujours appliqués. Avec regenerate=True, les visualisations des tuiles
|
||||
déjà présentes sont refaites (--force), classification conservée.
|
||||
"""
|
||||
from .fetch_ign import tile_filename
|
||||
# -u : sortie non bufferisée — le journal .generation.log doit être
|
||||
@ -1366,14 +1420,11 @@ def _build_command(tiles, regenerate=False, ground_class="ign", ign_classes="sol
|
||||
"-o", str(OUTPUT_DIR),
|
||||
"-r", ",".join(str(r) for r in GENERATE_RESOLUTIONS),
|
||||
"--only", *(viz or _panel_viz_steps()),
|
||||
"--ground-classification", ground_class,
|
||||
"--ign-classes", ign_classes]
|
||||
if edge_buffer:
|
||||
cmd += ["--edge-buffer", f"{EDGE_BUFFER_METERS:g}"]
|
||||
"--ground-classification", "ign",
|
||||
"--ign-classes", "sol",
|
||||
"--edge-buffer", f"{EDGE_BUFFER_METERS:g}"]
|
||||
if regenerate:
|
||||
cmd += ["--force"]
|
||||
if reclassify:
|
||||
cmd += ["--force-classification"]
|
||||
if os.environ.get("LIDAR_GPU", "") == "1":
|
||||
cmd += ["-g", "all"]
|
||||
workers = os.environ.get("LIDAR_WORKERS", "auto").strip()
|
||||
@ -1424,10 +1475,6 @@ def _resolve_request(req):
|
||||
raise HTTPException(400, "aucune tuile fournie")
|
||||
if len(tiles) > MAX_CELLS:
|
||||
raise HTTPException(400, f"trop de tuiles ({len(tiles)}) — max {MAX_CELLS}")
|
||||
if req.ground_class not in GROUND_CLASS_METHODS:
|
||||
raise HTTPException(
|
||||
400, f"méthode de classification invalide : {req.ground_class!r} "
|
||||
f"(attendu : {', '.join(GROUND_CLASS_METHODS)})")
|
||||
# Génération du nord au sud (row décroissante, puis col croissante) : les
|
||||
# workers prennent les tuiles dans l'ordre de soumission et la carte se
|
||||
# remplit de haut en bas pendant le run.
|
||||
@ -1437,11 +1484,7 @@ def _resolve_request(req):
|
||||
|
||||
def _launch_job(tiles, viz, req, qid=None):
|
||||
"""Démarre un run du pipeline. À appeler sous _job_lock, serveur libre."""
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate,
|
||||
ground_class=req.ground_class,
|
||||
ign_classes=req.ign_classes,
|
||||
viz=viz, reclassify=req.reclassify,
|
||||
edge_buffer=req.edge_buffer)
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate, viz=viz)
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_fh = open(LOG_FILE, "w", encoding="utf-8")
|
||||
# Nouveau run : journal d'événements remis à zéro et identifiant de run
|
||||
|
||||
Reference in New Issue
Block a user