# 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. ```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 ``` ## 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` | | 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://:8975/tiles/slope/{zoom}/{x}/{y}.png`. Pour tout ajouter d'un coup : *Imagery preferences → Offline/Custom → Add imagery source XML* avec `http://:8975/tiles/josm.imagery.xml`. - **iD** — *Fond de carte → Personnalisé*, coller `http://:8975/tiles/slope/{z}/{x}/{y}.png`. - **QGIS** — *XYZ Tiles → Nouvelle connexion* (même URL, zoom max 19), ou *WMS/WMTS → Nouveau* avec `http://: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-mode` CSS : 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. ```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_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 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=` (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. ## 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/mapui.py` — interface (Leaflet `L.tileLayer`, LOD natif). - `Dockerfile.maps`, `docker-compose.maps.yml`, `./run.sh --serve-maps`.