L'ancienne carte n'est pas une carte à tuiles : une div Leaflet par dalle,
rotée en CSS pour coller la grille Lambert 93 sur le Web Mercator, trois
paliers d'images choisis à la main, un plafond d'images pleine résolution et
une mosaïque d'overview pour boucher les trous au dézoom. Des centaines de
nœuds DOM, des AVIF de 2500² à 5000² décodés dans le navigateur, un LOD
maison — et rien de réutilisable hors de cette page.
Nouvelle image légère (Pillow + pyproj, ni GDAL ni PDAL, port 8975) servant
une pyramide XYZ EPSG:3857 au schéma OpenStreetMap, rendue à la demande
depuis les dalles et mise en cache dans output/index_xyz/ :
- tiles.py : grille XYZ, reprojection par transformation projective dalle par
dalle (calage mesuré < 1 px), choix du palier source parmi ceux que le
pipeline produit déjà (vignette, intermédiaire, quadrant, dalle), cache
périmé dès qu'une dalle contributrice est plus récente, marqueur .empty
pour les zones sans donnée, cache d'images sources à budget mémoire ;
- mapserve.py : /tiles/{couche}/{z}/{x}/{y}.png (256 px canonique) et
@2x.webp (512 px, interface), TileJSON, WMTS, josm.imagery.xml, CORS —
les rendus deviennent un fond d'imagerie pour JOSM, iD, QGIS, uMap ;
- mapui.py : une L.tileLayer par couche dans un pane isolé — LOD, cache et
animation natifs de Leaflet ; pile réordonnable (glisser avec barre
d'insertion, ou boutons ▲▼ au doigt), modes de fusion CSS par couche,
configuration figeable comme défaut de tous les navigateurs.
Deux amonts pour les déploiements en deux machines : LIDAR_SOURCE_URL
(webapp du pipeline — inventaire complet, dalles rapatriées à la demande) et
LIDAR_MAPS_URL (autre instance carte). Charge bornée et réglable, taillée
par défaut pour une petite machine : 2 rendus et 2 téléchargements
simultanés, 192 Mo de cache de sources.
Mesuré sur 849 dalles réelles : première tuile d'une zone 0,9–3,2 s
(téléchargement compris), ~1 ms ensuite ; un chemin OSM se superpose
exactement à la trace du rendu de pente, et les coutures entre tuiles
restent sous le bruit naturel du terrain.
L'interface historique (port 8973) n'est pas touchée : génération et export
y restent, les deux cartes coexistent.
La webapp légère bâchait app.js/app.css dans l'image au build : après une
évolution de l'interface, le Pi servait l'ancienne carte sans aucun signal
(tuiles invisibles, vieux fichiers). Désormais l'interface embarque sa
version (hash CSS+JS) que le serveur de données publie via /api/tiles : en
cas de divergence, bandeau « interface périmée » dans la carte + log d'alerte
côté webapp. Les assets sont servis en revalidation (jamais immutable) et
une page ouverte depuis plus de 15 min se recharge seule au retour au premier
plan — l'ancienne interface n'est plus jamais affichée après un déploiement.
Rastérisation des points sol via gpu.bin_mean_2d (bincount 2D CuPy, sémantique
identique au repli scipy binned_statistic_2d) : ~x7 plus rapide sur cette
étape, logs de durée par phase dans create_dtm_fast. safe_gpu_call retente en
CPU sur toute erreur GPU (pas seulement les messages CUDA) : un transfert
échoué en cours de run mélangeait types numpy/cupy et faisait échouer la
visualisation entière. Workers en mono-thread BLAS/OpenMP sur la machine de
traitement (plus de saturation des cœurs pendant les runs).
Les tuiles OpenStreetMap standard ont un fond blanc : au dézoom, les zones
sans données LiDAR laissent apparaître ce blanc à travers l'opacité du pane
(45 %), créant des cadres blancs autour des tuiles. Un filtre CSS
(brightness 0.35 + contrast 1.1 + saturate 0.7) assombrit le fond OSM pour
le fondre dans le thème sombre de la carte.
Les sous-tuiles voisines ne partageaient pas leurs bords (interpolation
bilinéaire évaluée deux fois sur des fractions différentes), créant une ligne
claire entre images au zoom moyen. Les points de bord sont désormais calculés
une fois par dalle et partagés entre sous-tuiles adjacentes.
Le cadre blanc autour des tuiles au dézoom venait du outline CSS dessiné
par-dessus les bords d'images non jointives : supprimé (le survol garde son
halo). La bascule overview ↔ vignettes oscillait d'un cran de zoom, d'où une
hystérésis. Les seuils de paliers sont revus pour ne jamais étirer la
vignette intermédiaire au-delà de 1:1.
La config enregistrée (aspect 74 % en bas, openness positive au-dessus,
fond OSM sombre) devient le preset intégré « Défaut » et les défauts du
panneau. DEFAULT_VIZ reste le relief archéologique (positive_openness).
Le MNT de chaque dalle s'étend d'une bande de 100 m remplie avec les
points sol des 8 tuiles voisines (option « Raccord des bords ») : les
rendus à grand noyau (openness, SVF, LRM) deviennent continus d'une
dalle à l'autre, les images restent recadrées sur le kilomètre exact.
Pendant une génération, la carte encadre les dalles du run : orange
pulsant en cours de rendu, rouge en échec — les coins WGS84 sont portés
par /api/status pour toutes les dalles non terminées.
La file de génération trie les dalles du nord au sud et passe à 10
workers GPU.
Trois chantiers liés à la qualité et au coût des rendus :
- Calage vertical des faisceaux : les passes d'une tuile peuvent être biaisées
de quelques cm (±2,5 cm mesurés sur 1000_6882), créant des marches aux
recouvrements. Les offsets par PointSourceId sont mesurés sur les points
sol de la tuile (réf. médiane itérée) et retranchés ≥ 0,5 cm avant
rastérisation, avec sidecar de cache et application au plancher bare-earth.
- Openness décimée ×2 : lancé de rayons sur grille par blocs (max/min) puis
rééchantillonnage bilinéaire — 532 s → 40 s par tuile à 0,2 m sur CPU,
signal archéologique préservé. Réglable --openness-downsample.
- Génération webapp concentrée sur les couches affichées : les défauts
/api/generate, /api/preview et le sélecteur génèrent le panneau complet
(slope, aspect, pos_open) au lieu d'aspect seul.
- Sur la webapp légère, le rebuild ne voyait que le cache local : la
coquille HTML embarquait un sous-ensemble des tuiles et le premier
chargement n'affichait pas les tuiles récentes du worker — elles
n'arrivaient qu'au sondage /api/tiles suivant (15 s).
- _merge_remote_index() applique au rebuild la même fusion que
/api/tiles (l'index distant prime, le local complète) et réécrit
index_tiles.json ET la liste embarquée dans index.html.
- AGENTS.md : avertissement — le Pi de prod tourne en compose+override
(labels Traefik + /srv/lidar/output), jamais via serve-webapp.sh.
- Le Dockerfile principal (worker, --serve, local-2m) n'avait pas l'étape
de bake : le repli de webapp.py tentait d'écrire webapp_assets/ dans le
paquet installé (dist-packages, propriété de root) et faisait planter
l'import de la webapp sous l'utilisateur lidar.
- Même étape de bake que Dockerfile.webapp, avec cd /tmp pour cibler le
paquet installé (sinon la copie source /app capte l'import via sys.path).
- Le repli de webapp.py ne casse plus l'import si le dossier du paquet
n'est pas inscriptible : réécriture dans un répertoire temporaire.
- Les assets de l'interface étaient écrits dans le volume (output/assets)
et servis d'ici : sur la webapp légère, un app.js périmé restait servi
même après rebuild de l'image.
- app.js/app.css (+ Leaflet vendorisé) sont désormais bâchés dans l'image
au build (Dockerfile.webapp) et servis depuis l'image ; le volume ne
porte que les données (index.html, vignettes, tuiles).
- _write_ui_assets() génère les assets dans un dossier cible ; repli de
régénération au démarrage si non bâchés (dev). build_index continue
d'écrire output/assets/ pour le mode serveur statique.
- Le bouton « Vider » ne retirait que les tâches du côté client : au
rechargement, la « dernière génération » du serveur
(.generation.job.json) était réadoptée et les tâches finies
réappaissaient.
- Les runs retirés sont mémorisés par horodatage serveur (sessionStorage) ;
l'adoption au chargement les ignore désormais.
- Les tâches lancées immédiatement (sans passer par la file) captent
l'horodatage de leur run à la fin, pour être retirables sans réadoption.
- Clic sur la carte pour choisir des dalles précises, même sans données
existantes (via /api/cell) ; les zones dessinées s'ajoutent à la
sélection au lieu de la remplacer
- Une demande lancée pendant un run part en file d'attente côté serveur
et démarre à la fin du travail en cours (plus de refus 409), file
vidable depuis l'interface
- Tuiles et interface toujours fraîches : images servies sans cache
navigateur (revalidation 304), rechargement de la carte seulement une
fois le rebuild de l'index terminé
- serve-webapp.sh : sous-commande update retirée, documentation de
déploiement corrigée en conséquence