Sur une petite machine (Raspberry Pi), LIDAR_TILE_CACHE_ONLY sert les tuiles depuis le cache uniquement (tuile absente = transparente non mémorisable, X-Tile-Pending) et LIDAR_TILE_BACKGROUND fait surveiller les dalles par un sondeur : chaque dalle nouvelle ou régénérée par le worker remet sa pyramide en file, rendue à basse priorité et au ralenti. Scan et état pilotables via /api/map/background, pré-calcul exhaustif toujours via /api/map/warm.
14 KiB
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.
Elle coexiste avec la webapp historique (port 8973), sans rien lui changer :
même dossier output/, génération et export restent là-bas.
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
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 avechttp://<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.
Pile de couches
Chaque couche du panneau porte trois réglages, tous persistés et transportés par le lien de partage :
- ordre — poignée ⠿ en glisser-déposer, ou boutons ▲▼ (les seuls utilisables au doigt). La liste est encadrée par « Haut de pile — devant » et « Bas de pile — derrière ». Pendant un glisser, la destination est explicite : la ligne tirée s'estompe et une barre d'insertion marque le point de chute, au-dessus ou en dessous de la ligne survolée selon la moitié visée ; à l'arrivée, la couche déplacée clignote brièvement et est ramenée dans le champ de vision ;
- opacité — curseur par couche ;
- mode de fusion —
mix-blend-modeCSS : Normal, Produit, Écran, Superposé, Doux, Lumière crue, Différence, Luminosité. La superposition ne se réduit donc pas à de la transparence : « Produit » pose un ombrage sur une rampe de couleur sans la délaver, « Superposé » creuse le contraste local, « Différence » fait ressortir les écarts entre deux couches.
Figer la configuration
Le bouton ★ Définir par défaut enregistre l'état courant — ordre, couches
allumées, opacités, modes de fusion, fond de carte et fusion de la pile — 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_LAYERS, DEFAULT_OPACITY, DEFAULT_BLEND dans index.py). Les
valeurs reçues sont filtrées : couches inconnues écartées, opacités bornées à
0–1, modes de fusion validés.
Les couches LiDAR vivent dans un conteneur isolé (isolation: isolate) :
les fusions agissent entre elles, jamais sur le fond de carte à travers les
zones sans donnée. La fusion de la pile entière sur le fond se règle à
part, en bas du panneau.
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
- L'emprise de la tuile est convertie en Lambert 93 (échantillonnage 5×5 du contour : les bords ne sont pas droits en L93).
- 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). - 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), quadrantsindex_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. - 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. - 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 avecX-Tile-Pending: 1etCache-Control: no-store(le navigateur la redemande : dès que la maintenance l'a rendue, elle apparaît). Plus aucun rendu, ni rapatriement amont, sur le chemin des requêtes.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 parLIDAR_TILE_BACKGROUND_PAUSEseconde (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'unstat. 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 |
16 |
niveau maximal entretenu en fond (le natif 17–19 reste au pré-chauffage manuel) |
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 |
4096 |
file d'attente bornée (au-delà : ignoré jusqu'au prochain scan) |
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 dansoutput/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-Emptyle 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/metaliste 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 (LeafletL.tileLayer, LOD natif).Dockerfile.maps,docker-compose.maps.yml,./run.sh --serve-maps.