Add visualization picker to zone generation and unify tile colors
- Web map: multi-select picker in the generation bar (aspect, slope, positive openness, anisotropic openness, wavelet) passed to the API; the layer panel is restricted to the same shortlist (PANEL_VIZ) and a refresh button rebuilds the index when new layers appear on disk; jobs started outside the UI are now adopted into the visible queue - Uniform colors across tiles: openness/anisotropic/sailore now store local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF 0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches - Ray-tracing falls back to CPU when VRAM is exhausted so openness and SVF no longer fail silently on shared GPUs - build_index merges visualizations available at only one resolution into the displayed tile so in-progress layers stay visible - 11 new tests (142 passing)
This commit is contained in:
@ -62,6 +62,12 @@ def assets(file_path: str):
|
||||
GROUND_CLASS_METHODS = ("auto", "ign", "smrf", "csf")
|
||||
|
||||
|
||||
def _viz_step_names():
|
||||
"""Noms des visualisations acceptées (étapes VIZ_STEPS du pipeline)."""
|
||||
from .pipeline import VIZ_STEPS
|
||||
return [name for name, _ in VIZ_STEPS]
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
bbox: list = Field(..., description="[ouest, sud, est, nord] en WGS84")
|
||||
regenerate: bool = Field(False, description="Inclure les tuiles déjà générées")
|
||||
@ -83,6 +89,10 @@ class GenerateRequest(BaseModel):
|
||||
description="Sol nu : DTM au retour le plus bas de "
|
||||
"chaque cellule (requalifie le point le plus "
|
||||
"bas en terrain)")
|
||||
viz: list = Field(None,
|
||||
description="Visualisations à générer, noms d'étapes du "
|
||||
"pipeline (ex: aspect, wavelet, aniso_open) ; "
|
||||
"défaut : aspect")
|
||||
|
||||
|
||||
# --- État du job de génération -------------------------------------------
|
||||
@ -159,6 +169,58 @@ def status():
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/layers")
|
||||
def available_layers():
|
||||
"""Couches proposées dans le panneau (clé → label), sans rebuild de l'index.
|
||||
|
||||
Restreintes à PANEL_VIZ (base aspect + visualisations demandées) pour
|
||||
rester cohérent avec le menu. Permet à la carte de détecter celles
|
||||
apparues depuis le dernier build_index et de proposer une actualisation.
|
||||
"""
|
||||
from .index import PANEL_VIZ, VIZ_LABELS
|
||||
vis = OUTPUT_DIR / "visualisations"
|
||||
found = {}
|
||||
if vis.is_dir():
|
||||
for key in PANEL_VIZ:
|
||||
for ext in ("avif", "webp"):
|
||||
if next(vis.glob(f"*/*_{key}.{ext}"), None) is not None:
|
||||
found[key] = VIZ_LABELS.get(key, key)
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
# --- Rebuild de l'index en arrière-plan -------------------------------------
|
||||
_rebuild = {"running": False, "error": None, "done": None}
|
||||
|
||||
|
||||
@app.post("/api/rebuild")
|
||||
def rebuild_index():
|
||||
"""Régénère la carte (index.html, vignettes, sous-tuiles) en arrière-plan."""
|
||||
if _rebuild["running"]:
|
||||
raise HTTPException(409, "un rebuild est déjà en cours")
|
||||
|
||||
def _run():
|
||||
_rebuild["running"] = True
|
||||
_rebuild["error"] = None
|
||||
try:
|
||||
from .index import build_index
|
||||
build_index(OUTPUT_DIR)
|
||||
except Exception as e: # noqa: BLE001 — remonté à l'UI via l'API
|
||||
_rebuild["error"] = str(e)
|
||||
finally:
|
||||
_rebuild["running"] = False
|
||||
_rebuild["done"] = time.time()
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
return {"demarré": True}
|
||||
|
||||
|
||||
@app.get("/api/rebuild")
|
||||
def rebuild_status():
|
||||
return {"running": _rebuild["running"], "error": _rebuild["error"],
|
||||
"done": _rebuild["done"]}
|
||||
|
||||
|
||||
def _tail_log(n_lines):
|
||||
try:
|
||||
lines = LOG_FILE.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
@ -179,7 +241,7 @@ def preview(req: PreviewRequest):
|
||||
return {"count": len(todo), "capped": capped, "cells": todo}
|
||||
|
||||
|
||||
def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False, ign_classes="sol"):
|
||||
def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False, ign_classes="sol", viz=None):
|
||||
"""Commande de génération : téléchargement IGN + traitement des fichiers.
|
||||
|
||||
La classification du sol est choisie via `ground_class` (défaut : "ign",
|
||||
@ -189,13 +251,16 @@ def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False
|
||||
sans retouche). Avec regenerate=True, force la reclassification et la
|
||||
régénération des visualisations des tuiles déjà présentes. Avec
|
||||
bare_earth=True, le DTM est ramené au retour le plus bas de chaque
|
||||
cellule (sol nu).
|
||||
cellule (sol nu). Avec viz, on choisit les visualisations générées
|
||||
(noms d'étapes du pipeline, ex. ["aspect", "wavelet", "aniso_open"] ;
|
||||
défaut : aspect).
|
||||
"""
|
||||
from .fetch_ign import tile_filename
|
||||
# -u : sortie non bufferisée — le journal .generation.log doit être
|
||||
# lu en temps réel par /api/status (progression affichée dans l'UI).
|
||||
cmd = [sys.executable, "-u", "-m", "lidar_pipeline", str(INPUT_DIR),
|
||||
"-o", str(OUTPUT_DIR), "-r", "0.5,0.2", "--only", "aspect",
|
||||
"-o", str(OUTPUT_DIR), "-r", "0.5,0.2",
|
||||
"--only", *(viz or ["aspect"]),
|
||||
"--ground-classification", ground_class,
|
||||
"--ign-classes", ign_classes]
|
||||
if bare_earth:
|
||||
@ -226,12 +291,19 @@ def generate(req: GenerateRequest):
|
||||
raise HTTPException(
|
||||
400, f"méthode de classification invalide : {req.ground_class!r} "
|
||||
f"(attendu : {', '.join(GROUND_CLASS_METHODS)})")
|
||||
names = _viz_step_names()
|
||||
viz = [v for v in (req.viz or []) if v] or ["aspect"]
|
||||
invalid = [v for v in viz if v not in names]
|
||||
if invalid:
|
||||
raise HTTPException(
|
||||
400, f"visualisation invalide : {', '.join(invalid)} "
|
||||
f"(attendues : {', '.join(names)})")
|
||||
|
||||
with _job_lock:
|
||||
proc = _job["proc"]
|
||||
if proc is not None and proc.poll() is None:
|
||||
raise HTTPException(409, "une génération est déjà en cours")
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate, ground_class=req.ground_class, bare_earth=req.bare_earth, ign_classes=req.ign_classes)
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate, ground_class=req.ground_class, bare_earth=req.bare_earth, ign_classes=req.ign_classes, viz=viz)
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_fh = open(LOG_FILE, "w", encoding="utf-8")
|
||||
_job.update({"proc": None, "started": time.time(), "returncode": None, "cmd": cmd})
|
||||
|
||||
Reference in New Issue
Block a user