Au rechargement de la page, l'index obsolète du worker (16 couches)
polluait le /api/tiles fusionné et mergeTilesData réajoutait les couches
retirées au menu. viz_meta est désormais filtré aux couches du panneau
(PANEL_VIZ) côté serveur, et le panneau client est figé au chargement
(clés VIZ_META initiales) : aucun flux ne peut réintroduire de couche
hors panneau, même avec un index distant périmé.
La carte ne conserve que Pente, Aspect, Openness positive et Rugosité :
PANEL_VIZ est restreint à DEFAULT_LAYERS, le sélecteur de génération
suit le panneau, et ces 4 couches sont allumées par défaut via
default_layers (index.html + index_tiles.json). Les autres couches ne
sont plus ni affichées ni proposées.
Le bouton ⌖ recentre la carte sur la position GPS du téléphone. L'API
Geolocation exige un contexte sécurisé : la webapp sert désormais du HTTP
et le TLS est porté par le reverse proxy (Traefik, labels dans l'override
local) ; le webapp est transparent au proxy (URLs relatives, X-Forwarded-For
pour la restriction LIDAR_REGEN_CIDR). Ajoute le modèle d'override
docker-compose.webapp.override.yml.example, un rebuild d'index à la demande
et met à jour doc/déploiement ; les tuiles sont servies à la demande.
Le fichier de configuration de la webapp distante (montages du cache,
adresse du worker, labels reverse-proxy) dépend de la machine : chaque
clone garde sa copie, ignorée par git. webapp.env.example reste le
modèle versionné.
Bouton « Partager » dans l'onglet Couches : copie un lien URL encodant
la vue (zoom/lat/lng), la pile de couches avec visibilité et opacité,
et le fond OSM (affiché, opacité, mode sombre). Le destinataire reçoit
l'état exact, le lien prime sur son localStorage local. Hash étendu :
#zoom,lat,lng&L=clé:opacité:visible,...&O=on:opacité:dark — le hash
simple #zoom,lat,lng reste rétro-compatible.
_res_suffix_str importait LidarArchaeoPipeline (donc dtm→numpy) juste pour
le nommage des fichiers : sur la webapp légère sans numpy, build_index
échouait avec « No module named 'numpy' » et l'index n'était jamais
régénéré. Logique réimplémentée localement, synchronisée avec
pipeline._res_suffix et _strip_res_suffix.
- POST /api/stop : SIGTERM au pipeline (groupe isolé via start_new_session),
SIGKILL du groupe après 15 s ; forwardé au worker en mode deux machines
- « Régénérer » n'implique plus la reclassification : le DTM en cache est
réutilisé tant que la méthode ne change pas ; nouvelle case « Reclasser le
sol » (--force-classification) et rappel de la classification existante
- Tâche marquée « Interrompu » (returncode négatif ou nul) au lieu d'Échec
Les images (visualisations, vignettes, sous-tuiles) sont rapatriées depuis
la machine de traitement par HTTP au moment où elles sont affichées, puis
servies depuis le disque : une image absente est téléchargée, une image
périmée ( ?v= plus récent que sa mtime, tuile régénérée) est rafraîchie.
/api/tiles fusionne l'index du worker (cache 60 s) avec le cache local, si
bien que les tuiles non cachées apparaissent quand même sur la carte.
Worker hors ligne : disjoncteur 2 min après 3 échecs, index local seul,
/api/status répond running=null — aucune erreur cliente.
- URLs images versionnées (?v=mtime) : une tuile recalculée change d'URL et
force le rechargement navigateur (cache heuristique contourné), y compris
en plein run via /api/tiles ; veille permanente 15 s sur la carte
- simulation locale du mode deux machines (docker-compose.local-2m.yml) :
worker GPU :8974 + webapp légère :8973 au cache séparé output-webapp/
- override webapp pour le Pi 5 (192.168.3.3) : volume /srv/lidar/output,
rsync vers le worker, labels Traefik (proxy/websecure/myresolver)
- purge 0,5 m : worker/process et politique générale passés à 0,2 m seul
- intègre le travail parallèle non commité : export mosaïque multi-dalles
(export.py + /api/export), sous-tuilage intégral des couches, légendes
VIZ_LEGENDS, docs et tests associés (213 tests verts)
- Scope pkill to 'pdal pipeline' (avoids killing unrelated PDAL jobs)
- Inline _strip_lidar_ext in dtm.py to remove circular import from pipeline
- _res_suffix_str now delegates to LidarArchaeoPipeline._res_suffix
Prevents indefinite hang if a worker gets stuck (deadlocked PDAL,
I/O stall). 2-hour safety net for the entire batch; remaining
futures are cancelled on timeout.
Replaces the pure-Python heapq implementation with a compiled binary
min-heap (~200x faster for large grids). Falls back to Python when
numba is unavailable. Uses a flat array view for heap elevation
comparisons to avoid 2D indexing issues in nopython mode.
Single command to start the interactive map container in the background
(auto-restart after reboot), with stop/restart/status/sync/logs/update
subcommands. Configuration lives in webapp.env (template provided,
git-ignored): remote generation URL, shared token, rsync cache command,
periodic sync and allowed network. The SSH key is mounted automatically
for rsync, port conflicts are detected before launch, and update pulls the
repo, rebuilds the image and restarts.
Deploy doc updated: the script is now the recommended install path on the
Raspberry Pi, alongside run.sh (foreground) and docker compose.
index.py rewritten as a continuous Leaflet map: rotated L93 tiles, stackable
visualization layers (per-layer opacity, drag-reorder persisted in
localStorage), tile info panel, live rebuild after each tile during a run.
Leaflet is vendored in assets/vendor/ so the map works fully offline;
georeferencing falls back rasterio -> pyproj -> affine so the lightweight
webapp (no GDAL) is supported.
docker-compose.worker.yml adds the tile generator service (full image + GPU)
that remote webapps call via LIDAR_GENERATION_URL, plus a one-shot process
profile. webapp.py gains LIDAR_AUTO_SYNC_SECONDS periodic cache refresh and
LIDAR_REGEN_CIDR restricting generation to the local network. run.sh
--serve-webapp now mounts ~/.ssh read-only so the rsync sync works.
docs/DEPLOY_WEBAPP.md completed for Raspberry Pi deployment: prerequisites,
git clone install, SSH key setup, first sync, update procedure and
troubleshooting.
La webapp (carte + vignettes) et la génération de tuiles se déploient sur
deux machines : image légère Dockerfile.webapp (FastAPI + Pillow AVIF
natif + pyproj) sur Raspberry Pi, pipeline complet sur la machine de
traitement. LIDAR_GENERATION_URL délègue /api/generate, /api/preview et
/api/status ; /api/sync ramène les tuiles par rsync puis régénère
vignettes et index localement. Token partagé optionnel
(LIDAR_API_TOKEN/LIDAR_REMOTE_TOKEN). Retire du dépôt les journaux
internes (.swival, audit-findings) et les données (data/, notebooks/).
Doc : docs/DEPLOY_WEBAPP.md.
- 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)
- 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
_gpu_candidates was never populated (typo: _candidate_gpus), breaking
num_gpus()/available_gpu_ids() and the multi-GPU round-robin. available_gpu_ids
also indexed tuples with dict syntax (TypeError). disable_gpu() now frees the
memory pool before clearing refs (was leaking VRAM). to_cpu() and safe_gpu_call()
survive disable_gpu() via a persistent _cupy_ndarray type reference so orphaned
GPU arrays are recovered to CPU. log_gpu_status() now uses Device(0) instead of
the host index (CuPy renumbers to 0 after CUDA_VISIBLE_DEVICES).
Small archaeological features (ditches, walls, post-holes) were drowned out
by large-scale topography (100-200m). Fix: replace 100/200m with finer
scales (3, 8, 15m), weight 5-10m heaviest, clip absolute z-score to 3.0
before combination to prevent large-scale outliers from dominating.
GPU multi-processing fix:
- gpu.py: revert to CUDA_VISIBLE_DEVICES approach with lazy CuPy init
(Device.use() caused CUDA_ERROR_NO_BINARY_FOR_GPU on GPU 1)
- CuPy is imported lazily on first to_gpu() call, allowing
CUDA_VISIBLE_DEVICES to be set before CUDA context creation
- nvidia-smi used for GPU count detection (no CUDA import needed)
- pipeline.py: add tip message suggesting -w N when multiple GPUs detected
Rendering improvements:
- Title: split into bold title (14pt) + italic description (10pt)
- North arrow: moved inside data area (top-right) with transparent
background — no longer overlaps title
- Colorbar: full height (compass gap removed), ScalarFormatter with
useOffset=False to prevent scientific notation on small values
Performance:
- rendering.py: save matplotlib figure to BytesIO instead of temp PNG
file — eliminates disk I/O between matplotlib and PIL
- visualizations.py: cap max_dist at 300 for ray-tracing (SVF,
openness, aniso_open) — avoids 500+ iterations at 0.2m resolution
- pipeline.py: deduplicate n_gpus calculation in parallel path
GPU fix:
- Revert to CUDA_VISIBLE_DEVICES approach but with lazy CuPy init
- gpu.py: CuPy is no longer imported at module level; _init_gpu()
imports it lazily on first to_gpu() call. This allows workers to
set CUDA_VISIBLE_DEVICES before CuPy creates a CUDA context.
- gpu.py: detect GPU count via nvidia-smi (no CUDA context needed)
- pipeline.py: each worker sets CUDA_VISIBLE_DEVICES=N before CuPy
init, so each process uses only its assigned GPU
Rendering improvements:
- Title: split into bold title (14pt) + italic description (10pt)
instead of single 15pt bold block
- North arrow: moved inside data area (top-right corner) with
semi-transparent white background for readability over data
- Colorbar: full height (no gap for compass rose), added
ScalarFormatter(useOffset=False) to avoid scientific notation
- Colorbar compass rose gap removed since north arrow is now
inside the data area