Add web map with zone generation API, side job queue, and restore historical DTM hole rendering
- webapp.py: FastAPI serving the continuous map (port 8973) with /api/preview, /api/generate and /api/status; tiles are downloaded from IGN and processed in a logged subprocess, tracked live in a side "File de génération" panel that survives page reloads - fetch_ign.py: download missing 1 km LiDAR HD tiles from the IGN geoplateforme before processing - index.py: tile thumbnails and 500 m subtiles are now invalidated by mtime so regenerating a tile refreshes its cached images; progress logging per tile - dtm.py: back to the historical gap handling (small gaps filled by fillnodata only, larger holes left as nodata rendered black); lowest-return floor only via --bare-earth, IGN class selection via --ign-classes - cli.py: positional input now optional (--rebuild-index works alone) - docker-compose.yml: serve (GPU, port 8973) and process services; launch via docker compose only (documented in AGENTS.md/AGENTS.md) - tests: 131 passing, incl. regressions for thumbnail staleness, --rebuild-index without input, and nodata rendering
This commit is contained in:
@ -97,7 +97,10 @@ def main():
|
||||
)
|
||||
parser.add_argument(
|
||||
"input",
|
||||
help="Dossier contenant les fichiers LAZ/LAS"
|
||||
nargs="?",
|
||||
default="/data/input",
|
||||
help="Dossier contenant les fichiers LAZ/LAS (défaut: /data/input ; "
|
||||
"optionnel pour --rebuild-index)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
@ -134,7 +137,16 @@ def main():
|
||||
parser.add_argument(
|
||||
"--force-classification",
|
||||
action="store_true",
|
||||
help="Reclassifier le sol même si le fichier .las existe déjà"
|
||||
help="Reclassifier le sol même si la méthode est inchangée (régénère aussi le DTM "
|
||||
"et les images). Sans ce flag, changer --ground-classification suffit : la "
|
||||
"méthode enregistrée est comparée et un changement déclenche la reclassification."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bare-earth",
|
||||
action="store_true",
|
||||
help="Sol nu : ramener le DTM au retour le plus bas de chaque cellule. "
|
||||
"Requalifie le point le plus bas de chaque colonne en terrain — utile sous "
|
||||
"végétation dense ou en relief raide où la classification du sol sous-couvre le terrain."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-tif",
|
||||
@ -143,9 +155,22 @@ def main():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ground-classification",
|
||||
choices=["auto", "smrf", "csf"],
|
||||
choices=["auto", "ign", "smrf", "csf"],
|
||||
default="auto",
|
||||
help="Méthode de classification du sol : auto (détection), smrf, csf (défaut: auto)"
|
||||
help="Méthode de classification du sol : auto (préfère la pré-classification IGN si "
|
||||
"présente — base rapide — sinon détection SMRF/CSF), ign, smrf, csf. "
|
||||
"Avec ign, le MNT est la rasterisation pure des classes choisies "
|
||||
"(--ign-classes) sans aucune retouche ; avec smrf/csf, il est complété "
|
||||
"par le retour le plus bas par cellule + interpolation des trous. (défaut: auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ign-classes",
|
||||
default="sol",
|
||||
help="Classes LAS extraites pour le MNT avec la classification IGN (méthode "
|
||||
"ign/auto) : liste noms ou codes séparés par virgules — "
|
||||
"sol(2), unclassified(1), eau(9), virtuel(66), pont(17), sursol(64). "
|
||||
"Ex: --ign-classes sol,unclassified. Changer la liste reclassifie les "
|
||||
"dalles concernées. (défaut: sol)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quality",
|
||||
@ -185,6 +210,14 @@ def main():
|
||||
default=None,
|
||||
help="Traiter un ou plusieurs fichiers LAZ/LAS (nom complet sans extension, ex: LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fetch-tiles",
|
||||
nargs="+",
|
||||
default=None,
|
||||
metavar="COL,ROW",
|
||||
help="Télécharger ces dalles LiDAR HD depuis l'IGN avant traitement "
|
||||
"(tuiles non encore générées, ex: --fetch-tiles 1055,6882 1056,6883)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
@ -253,6 +286,21 @@ def main():
|
||||
logger.warning("Aucune tuile traitée trouvée — carte globale non générée")
|
||||
return
|
||||
|
||||
# Téléchargement des dalles IGN manquantes avant le traitement
|
||||
if args.fetch_tiles:
|
||||
from .fetch_ign import fetch_tiles, parse_tile_specs
|
||||
try:
|
||||
specs = parse_tile_specs(args.fetch_tiles)
|
||||
except ValueError as e:
|
||||
logger.error(str(e))
|
||||
return
|
||||
logger.info(f"Téléchargement de {len(specs)} dalle(s) LiDAR HD depuis l'IGN...")
|
||||
fetched = fetch_tiles(args.input, specs, args.output)
|
||||
if fetched:
|
||||
logger.info(f"{len(fetched)} dalle(s) téléchargée(s) — traitement...")
|
||||
else:
|
||||
logger.warning("Aucune dalle téléchargée (déjà présentes ou introuvables)")
|
||||
|
||||
quality = 100 if args.lossless else args.quality
|
||||
# Parse --only and --skip: accept comma-separated values
|
||||
only_viz = None
|
||||
@ -268,8 +316,10 @@ def main():
|
||||
workers=args.workers,
|
||||
force=args.force,
|
||||
ground_method=args.ground_classification,
|
||||
ign_classes=args.ign_classes,
|
||||
force_classify=args.force_classification,
|
||||
keep_tif=args.keep_tif,
|
||||
bare_earth=args.bare_earth,
|
||||
quality=quality,
|
||||
only_viz=only_viz,
|
||||
skip_viz=skip_viz,
|
||||
@ -315,30 +365,8 @@ def main():
|
||||
logger.info(f"Traitement de {len(unique_files)} fichier(s) sélectionné(s)")
|
||||
for laz_file in unique_files:
|
||||
logger.info(f" → {laz_file.name}")
|
||||
for laz_file in unique_files:
|
||||
pipeline.process_file(laz_file)
|
||||
|
||||
# Clean up temporary files
|
||||
logger.info("Nettoyage des fichiers temporaires...")
|
||||
try:
|
||||
if pipeline.temp_dir.exists():
|
||||
shutil.rmtree(pipeline.temp_dir)
|
||||
temp_base = pipeline.output_dir / "temp"
|
||||
if temp_base.exists():
|
||||
shutil.rmtree(temp_base)
|
||||
logger.info(" ✓ Fichiers temporaires supprimés")
|
||||
except Exception as e:
|
||||
logger.warning(f" Note: Impossible de supprimer les fichiers temporaires: {e}")
|
||||
|
||||
# Génère la carte globale après traitement --file
|
||||
if not args.no_index:
|
||||
try:
|
||||
from .index import build_index
|
||||
index_path = build_index(pipeline.output_dir, pipeline.output_format)
|
||||
if index_path:
|
||||
logger.info(f"Carte globale générée : {index_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Index global non généré: {e}")
|
||||
# Réutilise process_all : workers parallèles, résumé, index, nettoyage
|
||||
pipeline.process_all(files=unique_files)
|
||||
else:
|
||||
pipeline.process_all()
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user