Files
lidar_rendu/docs/MAPS.md

19 KiB
Raw Blame History

Carte à tuiles XYZ (image lidar-maps)

Carte « slippy » classique — même schéma de tuiles que Google Maps et OpenStreetMap — servie par une image légère dédiée. Les rendus du pipeline deviennent ainsi un fond d'imagerie réutilisable dans JOSM, iD, QGIS, uMap, MapLibre ou OsmAnd, en plus de l'interface de consultation fournie.

C'est la SEULE interface web du projet (l'ancienne webapp historique a été retirée) : elle embarque aussi la génération de tuiles — locale sur l'image complète (worker, ./run.sh --serve), déléguée via LIDAR_GENERATION_URL sur la machine légère (cf. docs/DEPLOY_WEBAPP.md).

docker compose -f docker-compose.maps.yml up -d --build   # http://localhost:8975/
docker compose -f docker-compose.maps.yml logs -f maps
./run.sh --serve-maps            # équivalent en conteneur au premier plan

Générer des tuiles depuis la carte

  • + Zone (barre d'outils) — dessiner un rectangle : les dalles LHD de 1 km qui l'intersectent sont téléchargées depuis la géoplateforme IGN puis traitées (0,2 m, options ci-dessous) ; les zones et clics successifs s'additionnent ;
  • ⤒ Compléter — toutes les dalles déjà présentes dans input/ qui manquent au moins une des couches demandées ;
  • ↻ Générer cette dalle (fiche d'infos au clic) — une dalle précise, même sans données existantes.

Options du run : couches visées (défaut : les couches du panneau) et régénération forcée. La classification du sol (IGN, sol seul) et le raccord des bords (bande de 100 m prise aux dalles voisines) sont imposés : aucun réglage. Une demande lancée pendant un run part en file d'attente (persistée, jamais de coupure du travail en place) ; la progression s'affiche dalle par dalle (cadres orange = rendu en cours, bleu = en attente, rouge = échec) avec journal et bouton Arrêter (SIGTERM puis SIGKILL). Pendant le run, chaque dalle terminée apparaît sur la carte : l'inventaire est mis à jour après chaque dalle et la maintenance de pyramide traite ses tuiles en priorité (surveillance toutes les 10 s).

Les boutons sont masqués si le navigateur vient d'une IP hors LIDAR_REGEN_CIDR (défaut : localhost + plages privées RFC1918) ou si aucun backend de génération n'existe (image légère sans LIDAR_GENERATION_URL). Sur la machine légère, tout est transmis au worker (LIDAR_GENERATION_URL), qui exécute le pipeline complet ; ses dalles sont rapatriées à la demande par l'inventaire /api/tiles + les statiques versionnées (LIDAR_SOURCE_URL).

Contrat de tuilage

Point Valeur
Projection EPSG:3857, schéma XYZ OSM (origine nord-ouest, y vers le sud)
URL canonique /tiles/{couche}/{z}/{x}/{y}.png — 256 px, PNG RGBA
Variante haute densité /tiles/{couche}/{z}/{x}/{y}@2x.webp — 512 px (interface interne)
Zooms 5 → 19 natif (0,2 m/px ≈ z19 en France) ; au-delà, sur-zoom côté client
Hors emprise tuile entièrement transparente (superposable), en-tête X-Tile-Empty: 1
En attente (cache seule) tuile transparente, en-têtes X-Tile-Empty: 1 + X-Tile-Pending: 1, jamais mémorisée par le navigateur
CORS Access-Control-Allow-Origin: * sur /tiles/*
Attribution LIDAR_ATTRIBUTION, défaut « LiDAR HD © IGN — Licence Ouverte 2.0 »

Découverte : /tiles/{couche}.json (TileJSON 3.0.0), /tiles/wmts.xml (WMTS 1.0.0, grille GoogleMapsCompatible), /tiles/josm.imagery.xml (toutes les couches d'un coup dans JOSM).

Utiliser les tuiles ailleurs

  • JOSM — Imagery → Imagery preferences → + TMS, coller http://<hôte>:8975/tiles/slope/{zoom}/{x}/{y}.png. Pour tout ajouter d'un coup : Imagery preferences → Offline/Custom → Add imagery source XML avec http://<hôte>:8975/tiles/josm.imagery.xml.
  • iD — Fond de carte → Personnalisé, coller http://<hôte>:8975/tiles/slope/{z}/{x}/{y}.png.
  • QGIS — XYZ Tiles → Nouvelle connexion (même URL, zoom max 19), ou WMS/WMTS → Nouveau avec http://<hôte>:8975/tiles/wmts.xml.
  • uMap / MapLibre / Leaflet — même gabarit XYZ, ou le TileJSON.
  • OsmAnd — source de tuiles en ligne, gabarit XYZ, zoom max 19.

Le bouton « Utiliser dans JOSM / QGIS » de la carte affiche et copie ces URL pour la couche choisie.

Pyramide complète générée d'avance

Par défaut, tous les niveaux jusqu'au natif (19 en numérotation OSM standard = 0,2 m/px, servi en 18@2x par l'interface) sont écrits sur disque en AVIF et générés d'avance par la maintenance de fond (sur une carte alimentée par l'amont : téléchargés depuis LIDAR_MAPS_URL). Aucun niveau n'est rendu à la volée : le rendu à la demande d'un Raspberry Pi (~170 ms par tuile, 3 à la fois) donnait 1,5 à 6 s par écran aux zooms 17–19. L'interface est plafonnée au zoom 19 (1 px écran = 1 px LiDAR) : jamais de tuile agrandie. Ordre de grandeur mesuré : ~110 000 tuiles @2x pour 3 240 dalles, dont 81 000 au niveau natif, ~5 Go.

Stockage réduit (disque compté) : LIDAR_TILE_CACHE_MAX_Z plus bas et/ou LIDAR_TILE_EVEN_LEVELS=1 (niveaux pairs seuls ; l'interface réduit alors les tuiles du niveau supérieur aux zooms impairs, les autres niveaux sont rendus à la volée avec un cache mémoire LIDAR_TILE_MEMORY_CACHE_MB).

Une carte alimentée par un serveur de dalles amont (LIDAR_SOURCE_URL, cas du Pi) rapatrie et garde localement les sources de chaque dalle dès qu'elle apparaît (vignettes et quadrants 500 m ; la dalle entière, doublon de ses quadrants, n'est pas rapatriée), sans attendre de visite. Tous les niveaux restent disponibles quand le conteneur de rendu est éteint ; une source manquée pendant qu'il était éteint est reprise au scan suivant.

Fiche de dalle et rose des vents

Un clic sur la carte sélectionne la dalle LiDAR HD sous le curseur (cadre jaune en pointillés), qu'elle soit générée ou non. La fiche s'affiche tout de suite avec le nom, l'emprise Lambert 93, puis, si la dalle est rendue, la résolution, la date de génération et le recalage vertical des passes (faisceaux, décalages, correction des lignes). Les informations IGN arrivent à part (GET /api/map/ign?col&row, catalogue STAC mis en cache dans output/ign_meta/) : date et heure du scan LiDAR, capteurs, mission, opérateur, date d'édition, procédé de classement, nombre de points et lien de téléchargement du nuage .copc.laz sur la géoplateforme. Un catalogue lent ou injoignable n'empêche jamais la sélection.

Quand le relief orienté est affiché, une rose des vents donne la couleur de chaque orientation de pente (même formule CIELAB que le rendu) ; la clarté porte le relief local (clair = bosse, sombre = creux).

Affichage : relief et précision

La carte ne sert que les couches de PANEL_VIZ (index.py) : le relief orienté (couche d'affichage principal, qui fusionne openness locale et orientation des pentes) et la précision (densite_sol : densité des points sol retenus pour le MNT). Les autres visualisations présentes sur disque ne sont ni listées ni servies en tuiles.

Il n'y a plus de pile de couches. Le panneau propose trois modes, un clic chacun, ou la touche P pour passer au suivant :

  • Relief — le relief orienté seul ;
  • Précision — la densité seule, en 16 gris (échelle log fixe : niveau k à partir de 0,25 × 2^(k/2) pts/m², noir ≤ 0,35 ou aucun point, blanc ≥ 45) ; se lit comme une carte de fiabilité géométrique ;
  • Les deux — la précision en « produit » (mix-blend-mode: multiply) sur le relief, opacité réglable (60 % par défaut) : les zones où le relief est interpolé s'assombrissent sans masquer le relief.

La légende de la précision (16 paliers, info-bulle en pts/m² sur chaque palier) s'affiche dès que la précision est visible. Les couches LiDAR vivent dans un conteneur isolé (isolation: isolate) : le produit n'agit que sur le relief, jamais sur le fond de carte.

Le lien de partage transporte la couche principale, le mode et l'opacité : #z/lat/lng&M=relief_oriente&P=both:60&B=1:85:1. Les anciens liens de la pile (&L=…) s'ouvrent sans erreur sur l'état courant.

Figer la configuration

Le bouton ★ Définir par défaut enregistre l'affichage courant — couche principale, mode, opacité de la précision, fond de carte — dans output/.map-defaults.json. Tout navigateur sans réglage local part alors de cette configuration ; ↺ Réinitialiser oublie l'état local et y revient.

curl http://localhost:8975/api/map/defaults              # configuration servie
curl -X DELETE http://localhost:8975/api/map/defaults    # retour au registre

Sans fichier enregistré, les défauts viennent du registre du pipeline (DEFAULT_VIZ, PRECISION_VIZ, DEFAULT_VIEW_MODE, DEFAULT_PRECISION_OPACITY dans index.py). Les valeurs reçues sont filtrées : couche inconnue (ou la précision elle-même) refusée comme principale, mode validé, opacités bornées à 0–1. Un fichier de l'ancienne pile (order/on/blend) est ignoré, sauf le fond.

Licence — LiDAR HD est diffusé sous Licence Ouverte 2.0 : l'attribution IGN est obligatoire et doit rester visible chez le client. Avant d'utiliser ces rendus comme calque de saisie dans OpenStreetMap, vérifier la position de la communauté (OSM-FR) sur la source concernée.

Comment une tuile est fabriquée

  1. L'emprise de la tuile est convertie en Lambert 93 (échantillonnage 5×5 du contour : les bords ne sont pas droits en L93).
  2. Les dalles de 1 km qui l'intersectent sont retrouvées par leur nom (LHD_FXX_{col}_{row} → X ∈ [col, col+1] km, Y ∈ [row−1, row] km).
  3. Pour chacune, le palier source le plus grossier suffisant est choisi parmi ceux que le pipeline produit déjà : vignette index_thumbs (≈3,9 m/px), vignette intermédiaire _mid (1,56 m/px), quadrants index_subtiles (2500 px, 4× moins à décoder que la dalle) puis la dalle. Les trois dossiers sont scannés indépendamment : un cache partiel — une machine légère ne rapatrie que quadrants et vignettes, jamais les dalles entières — reste entièrement exploitable.
  4. La fenêtre utile est découpée puis reprojetée par transformation projective (Image.transform(..., PERSPECTIVE)), dalle par dalle : le calage mesuré est inférieur au pixel.
  5. Le résultat est encodé (PNG ou WebP) et écrit dans output/index_xyz/{couche}/{z}/{x}/{y}[@2x].{ext}.

Aucune dépendance GDAL/PDAL : Pillow + pyproj uniquement.

Cache et péremption

  • Une tuile en cache est resservie tant qu'aucune dalle contributrice n'est plus récente qu'elle : régénérer une dalle n'invalide que ses tuiles.
  • Une tuile sans donnée est mémorisée par un marqueur .empty — jamais recalculée.
  • Les images sources décodées sont gardées dans un petit cache LRU : le décodage AVIF domine le coût, les tuiles voisines le réutilisent.
  • Côté navigateur, l'interface ajoute ?v=<stamp> (mtime la plus récente) et reçoit alors un cache immuable ; les clients OSM utilisent l'URL nue, servie en revalidation courte.

Mesures (dalles réelles 0,2 m, bloc 3×3 km, conteneur sans GPU)

premier rendu depuis le cache
z10–z14 50–200 ms 3–9 ms
z16–z18 (résolution native) 46–130 ms 3–9 ms

Poids d'une tuile de pente à z17, mesuré sur la même tuile :

encodage poids fidélité
PNG RGBA (défaut) 210 Ko sans perte
PNG palettisé (LIDAR_TILE_PNG_PALETTE=1) 55 Ko écart moyen 5,8 niveaux
WebP q78 (…/{y}.webp) 38 Ko avec perte, visuellement propre

Le PNG canonique reste sans perte par défaut : ces rendus servent à l'interprétation, pas à l'illustration. Pour un usage en fond d'imagerie où le débit compte, deux leviers : demander l'URL .webp (les clients qui la supportent : QGIS, iD, MapLibre, uMap) ou activer LIDAR_TILE_PNG_PALETTE=1.

Contrôles de calage effectués sur données réelles : un chemin OSM se superpose exactement à la trace visible dans la couche de pente, et l'écart de couture entre tuiles voisines reste inférieur au bruit naturel du terrain (écart mesuré 35–53 niveaux contre une médiane de 49–53 entre deux colonnes voisines prises au hasard dans la même tuile).

Charge et petites machines

Le service est borné à chaque étage, rien n'est illimité :

Étage Défaut Réglage
Rendus simultanés 2 LIDAR_TILE_WORKERS
Téléchargements de dalles simultanés 2 LIDAR_TILE_FETCH_WORKERS
Même tuile demandée en parallèle 1 rendu, les autres attendent son résultat —
Mémoire des sources décodées 192 Mo LIDAR_TILE_SOURCE_CACHE_MB

Les requêtes en excès attendent le sémaphore avant tout décodage : elles ne consomment ni CPU ni mémoire. Un navigateur en HTTP/1.1 n'ouvre de toute façon que 6 connexions par origine, toutes couches confondues ; derrière un proxy HTTP/2 ce plafond disparaît et seuls ces réglages tiennent la charge.

Le pré-chauffage (/api/map/warm) est séquentiel : il ne peut pas saturer la machine, seulement prendre du temps.

Sur un Raspberry Pi 2 Go, LIDAR_TILE_WORKERS=1 et LIDAR_TILE_SOURCE_CACHE_MB=64 restent confortables.

Cache seule + maintenance de fond (petit Raspberry Pi)

Sur une machine qui ne doit jamais calculer au fil de la navigation (un Pi qui tient aussi d'autres services), deux variables inversent la charge :

environment:
  - LIDAR_TILE_CACHE_ONLY=1     # la navigation ne rend plus rien
  - LIDAR_TILE_BACKGROUND=1     # une tâche de fond entretient la pyramide
  • LIDAR_TILE_CACHE_ONLY=1 — une tuile absente ou périmée est servie transparente avec X-Tile-Pending: 1 et Cache-Control: no-store (le navigateur la redemande : dès que la maintenance l'a rendue, elle apparaît). Plus aucun rendu local sur le chemin des requêtes ; avec LIDAR_MAPS_URL, une tuile manquante (pas encore passée par la maintenance) y est rapatriée — un téléchargement de quelques dizaines de Ko, mis en cache — puis servie : la navigation couvre tous les zooms sans jamais calculer sur la petite machine.
  • LIDAR_TILE_BACKGROUND=1 — un sondeur rescane les dalles à intervalle régulier (LIDAR_TILE_BACKGROUND_INTERVAL, 120 s) ; chaque dalle nouvelle ou régénérée (le worker vient de produire, le cache à la demande vient de rapatrier) met sa pyramide en file d'attente. Des rendeurs à basse priorité (os.nice) la vident au rythme d'une tuile par LIDAR_TILE_BACKGROUND_PAUSE seconde (1 s). Premier démarrage : TOUTES les dalles sont inconnues, la pyramide se reconstruit entière — les tuiles déjà fraîches ne coûtent qu'un stat. Les niveaux partent en file du plus petit zoom au plus grand : la carte se remplit d'abord grossièrement.
Variable Défaut Rôle
LIDAR_TILE_BACKGROUND_MAX_Z natif (18 en @2x, 19 en 256 px) niveau maximal entretenu en fond, numérotation des URL
LIDAR_TILE_BACKGROUND_SCALE 2 tuiles entretenues : 2 = 512 px (celles de l'interface)
LIDAR_TILE_BACKGROUND_FMT webp format des tuiles entretenues (celui de l'interface)
LIDAR_TILE_BACKGROUND_PAUSE 1.0 pause (s) entre deux rendus — le levier de la discrétion
LIDAR_TILE_BACKGROUND_INTERVAL 120 secondes entre deux scans des dalles
LIDAR_TILE_BACKGROUND_QUEUE_MAX 65536 file d'attente bornée — chaque dalle mémorise la première tuile refusée faute de place et reprend de là aux scans suivants, jusqu'à ce que toute sa pyramide soit passée

Pilotage : GET /api/map/background (état, compteurs, file), POST /api/map/background (scan immédiat), POST /api/map/warm (pré-calcul manuel exhaustif, tous zooms/formats, cf. ci-dessous). Enfin, plafonner le conteneur lui-même (mem_limit + memswap_limit dans un override compose) garantit qu'un rendu déréglé ne peut plus emporter la machine : le tueur OOM ne toucherait que lidar-maps.

Pré-chauffage

curl -X POST http://localhost:8975/api/map/warm \
  -H 'Content-Type: application/json' \
  -d '{"layers": ["slope"], "z_min": 10, "z_max": 16}'
curl http://localhost:8975/api/map/warm     # avancement / compte rendu

Sans bounds, l'emprise des dalles disponibles est utilisée. Utile après un gros run pour que la première consultation soit instantanée.

Deux machines

Deux amonts complémentaires, selon ce que la machine locale possède.

LIDAR_SOURCE_URL désigne la webapp du pipeline (port 8973) : l'inventaire des dalles vient de son /api/tiles et chaque image source est rapatriée au premier rendu qui en a besoin. Le conteneur carte démarre alors sans aucune donnée locale et sert tout le catalogue distant :

docker run -d --name lidar-maps --network host \
  -e LIDAR_SOURCE_URL=http://127.0.0.1:8973 \
  -e LIDAR_OUTPUT_DIR=/data/output -e LIDAR_PORT=8975 \
  -v "$PWD/output-maps:/data/output" --user "$(id -u):$(id -g)" lidar-maps

Mesuré sur 849 dalles réelles servies par un tunnel SSH : première tuile d'une zone 0,9–3,2 s (téléchargement d'un quadrant de 1,8 Mo compris), puis ~1 ms depuis le cache local, qui ne garde que ce qui a été consulté.

LIDAR_MAPS_URL désigne un serveur de tuiles amont (machine de traitement) : une tuile absente localement y est rapatriée (quelques dizaines de Ko) puis mise en cache — au lieu de rapatrier les dalles entières. Un disjoncteur suspend les tentatives 2 minutes après 3 échecs consécutifs : carte consultable même worker éteint.

Variables d'environnement

Variable Défaut Rôle
LIDAR_OUTPUT_DIR /data/output dalles lues, cache index_xyz/ écrit
LIDAR_PORT 8975 port d'écoute
LIDAR_SOURCE_URL — webapp du pipeline : inventaire + dalles rapatriées à la demande
LIDAR_SOURCE_TOKEN — jeton si la webapp amont exige LIDAR_API_TOKEN
LIDAR_MAPS_URL — serveur de tuiles amont (carte déportée)
LIDAR_ATTRIBUTION LiDAR HD © IGN mention servie (TileJSON, WMTS, JOSM, carte)
LIDAR_TILE_PNG_PALETTE — 1 : PNG palettisé (~4× plus léger, écart ~6 niveaux)
LIDAR_TILE_SOURCE_CACHE_MB 192 budget mémoire du cache d'images sources décodées
LIDAR_TILE_WORKERS 2 rendus de tuiles simultanés
LIDAR_TILE_FETCH_WORKERS 2 téléchargements de dalles simultanés (mode LIDAR_SOURCE_URL)
LIDAR_SSL_CERTFILE / LIDAR_SSL_KEYFILE — HTTPS direct (GPS sur téléphone)

Dépannage

  • Carte vide, layers: 0 (curl /healthz) : aucun rendu dans output/visualisations/, ou dossier monté au mauvais endroit.
  • Tuiles transparentes partout : zoom hors plage (5–19) ou zone sans dalle ; l'en-tête X-Tile-Empty le confirme.
  • Première consultation lente : normal, chaque tuile est rendue une fois — pré-chauffer (ci-dessus).
  • JOSM refuse l'URL : utiliser le gabarit {zoom}/{x}/{y} (JOSM) et non {z}/{x}/{y} (Leaflet/QGIS).
  • Couche absente du menu : elle n'existe pas sur disque pour ces dalles — /api/map/meta liste ce qui est réellement disponible.

Références

  • lidar_pipeline/tiles.py — grille, reprojection, cache, pré-chauffage.
  • lidar_pipeline/mapserve.py — routes tuiles/TileJSON/WMTS/JOSM et API carte.
  • lidar_pipeline/mapui.py — interface (Leaflet L.tileLayer, LOD natif).
  • Dockerfile.maps, docker-compose.maps.yml, ./run.sh --serve-maps.