Files
lidar_rendu/docs/MAPS.md
Antoine Jacquin 56c9fa0ccd Documenter le panneau unique de la carte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:34:28 +02:00

454 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`).
```bash
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
```
## Interface
Un **panneau unique à onglets** (`web/map.{html,css,js}`) regroupe tous les
réglages, à la place de l'ancienne pile de blocs empilés : **Affichage**
(couche principale, mode relief/précision, fond de carte), **Export PDF**,
**Génération** (masqué si le générateur est indisponible ou non autorisé
pour ce navigateur) et **Partager**. Sur ordinateur, le panneau occupe une
colonne fixe, repliable en bande d'icônes (**‹**, toujours utilisable : un
clic sur un onglet redéplie le panneau). Sous 720 px de large, il devient un
**volet en bas d'écran** à trois hauteurs — fermé, mi-hauteur, plein — que
l'on change en glissant la poignée, en la touchant simplement (un cran), ou
en retouchant l'onglet déjà actif.
Un clic sur la carte ouvre une **bulle** sur la dalle LiDAR HD sous le
curseur (date d'acquisition, densité de points sol) ; son bouton **Détails**
bascule une **fiche complète** dans le panneau (emprise, IGN, recalage des
passes) — **← Retour** rouvre l'onglet précédent. Raccourcis clavier :
**1–4** (onglets du panneau, sans effet si l'onglet est masqué), **P**
(mode d'affichage suivant), **Échap** (ferme la bulle, puis la fiche, puis
replie le panneau). Deux thèmes clair/sombre (bouton ◐/☀/☾, `auto` par
défaut = suit le système) ; réglages et état du panneau retenus dans
`localStorage` (`lidarMapView_v2`, `lidar-print`, `lidar-panel`,
`lidar-theme` — silencieusement ignorés en navigation privée).
## Générer des tuiles depuis la carte
- **+ Zone** (onglet Génération) — 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/Régénérer cette dalle** (bouton de la fiche de dalle, bulle →
Détails) — 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) et ouvre une **bulle** — nom, date d'acquisition,
densité de points sol — qu'elle soit générée ou non. Son bouton **Détails**
bascule une fiche complète dans le panneau : nom, 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. **← Retour** (ou Échap) ferme la fiche et rouvre l'onglet
précédent.
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.
```bash
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.
## Export PDF (planche d'impression terrain)
L'onglet **Export PDF** affiche les réglages (format A4/A3, paysage/portrait,
échelle 1:1 000 à 1:10 000, titre optionnel) et, tant qu'il est ouvert, un
**cadre jaune en pointillés** qui montre la zone qui sera imprimée (il
disparaît en changeant d'onglet). Le cadre est **posé sur le
terrain** : il se place au centre de la vue à l'ouverture (ou garde sa position
précédente si elle est encore visible), la carte zoome pour le montrer en
entier au-dessus du panneau, puis on navigue librement sans qu'il bouge. On le
déplace en faisant glisser sa **poignée ✥** (souris ou doigt) ; au relâchement,
la géométrie Lambert 93 exacte est recalculée (`GET /api/export/frame`) — un
relâchement suivi d'un clic immédiat n'ouvre pas la bulle de dalle (garde de
300 ms). « ⌖ Centrer ici » le ramène au centre de la vue, « ⤢ Voir le cadre »
zoome dessus ; changer de format, d'orientation ou d'échelle recadre la vue.
Les réglages et la position du cadre sont mémorisés dans `localStorage` du
navigateur. **Exporter le PDF** télécharge la planche (`GET
/api/export/pdf`), nommée `relief_{x_km}_{y_km}_1-{échelle}.pdf` (centre
Lambert 93 en km, à trois décimales).
La planche (module `lidar_pipeline/export_pdf.py`) est composée directement
en Lambert 93 depuis les sources déjà rendues (pas de passage par les tuiles
XYZ), puis dessinée en vectoriel (texte, grille, légende) avec `reportlab` —
Pillow + pyproj + reportlab uniquement, **sans numpy** : elle tourne aussi
bien sur l'image complète que sur l'image légère du Pi seul. Contenu :
- la carte du **relief orienté**, recadrée à l'échelle demandée (300 dpi en
A4, 250 dpi en A3 — borne la mémoire du Pi, ~36 Mo en A3) ; hors emprise des
dalles disponibles, la zone reste blanche et hachurée ;
- un **quadrillage Lambert 93** (pas 100 m aux échelles 1:1 000/1:2 000, 500 m
au 1:5 000, 1 000 m au 1:10 000) gradué en marge, et les **coins WGS84** de
la zone imprimée aux quatre angles ;
- une **flèche du nord géographique** tenant compte de la convergence du
méridien (la carte est orientée sur le nord du quadrillage L93, pas le nord
géographique — l'écart est indiqué en degrés) ;
- une **échelle graphique** (barre alternée) et l'**échelle numérique** ;
- une **rose des orientations à 8 points** (N/NE/E/SE/S/SO/O/NO), même
formule CIELAB que la rose de la fiche de dalle ;
- le texte de légende du relief orienté, repris de `VIZ_LEGENDS` (source
unique avec l'interface et le TileJSON) ;
- un **encart qualité** : miniature de la densité de points sol (mailles
50 m, classes de couleur), chiffres clés (densité moyenne, maille la plus
faible, part de surface interpolée, période d'acquisition), zones **hachurées
en blanc** là où la qualité n'est pas renseignée, et une ligne « **Donnée
manquante (sans relief)** » listant les dalles de la zone qui n'ont pas
encore été générées ;
- un cartouche : titre (par défaut, liste des dalles couvertes), échelle,
format, dpi, centre L93, taille de la zone, date d'export et mention de
source IGN.
Un seul export PDF à la fois : un second appel pendant qu'un export tourne
reçoit `429` (réessayer). L'export n'est **jamais délégué** à
`LIDAR_GENERATION_URL` : contrairement à la génération de tuiles, la carte
légère seule (Pi sans worker) sait exporter par elle-même, à partir des
sources déjà rapatriées sur disque.
> **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 :
```yaml
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
```bash
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 :
```bash
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/web/map.{html,css,js}` — interface (panneau à onglets, Leaflet `L.tileLayer`, LOD natif), relue par `lidar_pipeline/mapui.py`.
- `Dockerfile.maps`, `docker-compose.maps.yml`, `./run.sh --serve-maps`.