Rewrite README and docs in English for GitHub, add MIT license, remove internal files
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
707
docs/MAPS.md
707
docs/MAPS.md
@ -1,414 +1,413 @@
|
||||
# Carte à tuiles XYZ (image `lidar-maps`)
|
||||
# XYZ tile map (`lidar-maps` image)
|
||||
|
||||
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.
|
||||
A classic "slippy" map — the same tile scheme as Google Maps and
|
||||
**OpenStreetMap** — served by a dedicated lightweight image. Pipeline
|
||||
renders thus become a **reusable imagery basemap** in JOSM, iD, QGIS, uMap,
|
||||
MapLibre or OsmAnd, on top of the browsing interface it also provides.
|
||||
|
||||
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`).
|
||||
This is the ONLY web interface in the project (the old historical webapp has
|
||||
been removed): it also embeds **tile generation** — local on the full image
|
||||
(the full pipeline server, port 8973, `./run.sh --serve`), delegated via
|
||||
`LIDAR_GENERATION_URL` on the lightweight machine (see
|
||||
`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
|
||||
./run.sh --serve-maps # equivalent, foreground container
|
||||
```
|
||||
|
||||
## 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.
|
||||
A **single tabbed panel** (`web/map.{html,css,js}`) groups all settings,
|
||||
replacing the old stack of stacked blocks: **Affichage** (Display) (main
|
||||
layer, relief/precision mode, basemap), **Export PDF**, **Génération**
|
||||
(Generation) (hidden if the generator is unavailable or not authorized for
|
||||
that browser) and **Partager** (Share). On desktop, the panel occupies a
|
||||
fixed column, collapsible into an icon strip (**‹**, always usable: clicking
|
||||
a tab redeploys the panel). Below 720 px wide, it becomes a **bottom sheet**
|
||||
with three heights — closed, half, full — changed by dragging the handle, by
|
||||
simply tapping it (one notch), or by tapping the already-active tab again.
|
||||
|
||||
Un clic sur la carte **sélectionne** la dalle LiDAR HD sous le curseur
|
||||
(contour en pointillés) et remplit l'onglet **Dalle** (emprise, IGN, recalage
|
||||
des passes) sans changer l'onglet affiché ; re-cliquer la même dalle
|
||||
désélectionne, cliquer une autre déplace la sélection. Raccourcis clavier :
|
||||
**1–5** (onglets du panneau, sans effet si l'onglet est masqué), **P**
|
||||
(mode d'affichage suivant), **Échap** (désélectionne la dalle, 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).
|
||||
Clicking the map **selects** the LiDAR HD tile under the cursor (dashed
|
||||
outline) and fills the **Dalle** (Tile) tab (footprint, IGN info, pass
|
||||
alignment) without changing the displayed tab; clicking the same tile again
|
||||
deselects it, clicking another moves the selection. Keyboard shortcuts:
|
||||
**1–5** (panel tabs, no effect if the tab is hidden), **P** (next display
|
||||
mode), **Escape** (deselects the tile, then collapses the panel). Two
|
||||
light/dark themes (◐/☀/☾ button, `auto` by default = follows the system);
|
||||
settings and panel state are kept in `localStorage` (`lidarMapView_v2`,
|
||||
`lidar-print`, `lidar-panel`, `lidar-theme` — silently ignored in private
|
||||
browsing).
|
||||
|
||||
## Générer des tuiles depuis la carte
|
||||
## Generating tiles from the map
|
||||
|
||||
- **+ 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, onglet
|
||||
Dalle) — une dalle précise, même sans données existantes.
|
||||
- **+ Zone** (Génération tab) — draw a rectangle: the 1 km LHD tiles it
|
||||
intersects are downloaded from the IGN geoplatform and then processed
|
||||
(0.2 m, options below); successive zones and clicks are additive;
|
||||
- **⤒ Compléter** (Complete) — all tiles already present in `input/` that are
|
||||
missing at least one of the requested layers;
|
||||
- **↻ Générer/Régénérer cette dalle** (Generate/Regenerate this tile) (button
|
||||
on the tile sheet, Dalle tab) — a specific tile, even with no existing data.
|
||||
|
||||
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).
|
||||
Run options: target layers (default: the panel layers) and forced
|
||||
regeneration. Ground classification (IGN, ground only) and edge stitching (a
|
||||
100 m band taken from neighboring tiles) are enforced: no setting available.
|
||||
A request submitted while a run is already in progress goes into a
|
||||
**persisted queue** (never interrupting work in progress); progress is shown
|
||||
tile by tile (orange frames = rendering in progress, blue = waiting, red =
|
||||
failed) with a log and a **Stop** button (SIGTERM then SIGKILL). During a
|
||||
run, each completed tile appears on the map as it finishes: the inventory is
|
||||
updated after each tile and the pyramid maintenance job prioritizes its tiles
|
||||
(polled every 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`).
|
||||
The buttons are hidden if the browser's IP falls outside
|
||||
`LIDAR_REGEN_CIDR` (default: localhost + private RFC1918 ranges) or if no
|
||||
generation backend exists (lightweight image without `LIDAR_GENERATION_URL`).
|
||||
On the lightweight machine, everything is forwarded to the worker
|
||||
(`LIDAR_GENERATION_URL`), which runs the full pipeline; its tiles are then
|
||||
pulled back on demand via the `/api/tiles` inventory plus versioned static
|
||||
assets (`LIDAR_SOURCE_URL`).
|
||||
|
||||
## Contrat de tuilage
|
||||
## Tiling contract
|
||||
|
||||
| Point | Valeur |
|
||||
| Point | Value |
|
||||
|---|---|
|
||||
| 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 » |
|
||||
| Projection | EPSG:3857, OSM XYZ scheme (north-west origin, `y` toward the south) |
|
||||
| Canonical URL | `/tiles/{layer}/{z}/{x}/{y}.png` — 256 px, PNG RGBA |
|
||||
| High-density variant | `/tiles/{layer}/{z}/{x}/{y}@2x.webp` — 512 px (internal interface) |
|
||||
| Zoom levels | 5 → 19 native (0.2 m/px ≈ z19 in France); beyond that, client-side over-zoom |
|
||||
| Outside coverage | fully transparent tile (overlayable), header `X-Tile-Empty: 1` |
|
||||
| Pending (cache-only mode) | transparent tile, headers `X-Tile-Empty: 1` + `X-Tile-Pending: 1`, never cached by the browser |
|
||||
| CORS | `Access-Control-Allow-Origin: *` on `/tiles/*` |
|
||||
| Attribution | `LIDAR_ATTRIBUTION`, default "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).
|
||||
Discovery: `/tiles/{layer}.json` (TileJSON 3.0.0), `/tiles/wmts.xml`
|
||||
(WMTS 1.0.0, `GoogleMapsCompatible` grid), `/tiles/josm.imagery.xml`
|
||||
(all layers at once in JOSM).
|
||||
|
||||
## Utiliser les tuiles ailleurs
|
||||
## Using the tiles elsewhere
|
||||
|
||||
- **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.
|
||||
- **JOSM** — *Imagery → Imagery preferences → + TMS*, paste
|
||||
`http://<host>:8975/tiles/slope/{zoom}/{x}/{y}.png`. To add everything at
|
||||
once: *Imagery preferences → Offline/Custom → Add imagery source XML* with
|
||||
`http://<host>:8975/tiles/josm.imagery.xml`.
|
||||
- **iD** — *Background → Custom*, paste
|
||||
`http://<host>:8975/tiles/slope/{z}/{x}/{y}.png`.
|
||||
- **QGIS** — *XYZ Tiles → New Connection* (same URL, max zoom 19), or
|
||||
*WMS/WMTS → New* with `http://<host>:8975/tiles/wmts.xml`.
|
||||
- **uMap / MapLibre / Leaflet** — same XYZ template, or the TileJSON.
|
||||
- **OsmAnd** — online tile source, XYZ template, max zoom 19.
|
||||
|
||||
Le bouton **« Utiliser dans JOSM / QGIS »** de la carte affiche et copie ces
|
||||
URL pour la couche choisie.
|
||||
The **"Utiliser dans JOSM / QGIS"** ("Use in JOSM / QGIS") button on the map
|
||||
shows and copies these URLs for the selected layer.
|
||||
|
||||
## Pyramide complète générée d'avance
|
||||
## Full pyramid pre-generated
|
||||
|
||||
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.
|
||||
By default, **all** levels up to native (19 in standard OSM numbering =
|
||||
0.2 m/px, served as `18@2x` by the interface) are written to disk in AVIF
|
||||
and pre-generated by the background maintenance job (on a map fed by an
|
||||
upstream source: downloaded from `LIDAR_MAPS_URL`). No level is rendered
|
||||
on the fly: on-demand rendering on a Raspberry Pi (~170 ms per tile, 3 at a
|
||||
time) took 1.5 to 6 s per screen at zoom levels 17–19. The interface is
|
||||
capped at zoom 19 (1 screen pixel = 1 LiDAR pixel): never an upscaled tile.
|
||||
Measured order of magnitude: ~110,000 @2x tiles for 3,240 tiles, of which
|
||||
81,000 at native level, ~5 GB.
|
||||
|
||||
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`).
|
||||
Reduced storage (measured on disk): a lower `LIDAR_TILE_CACHE_MAX_Z` and/or
|
||||
`LIDAR_TILE_EVEN_LEVELS=1` (even levels only; the interface then downsamples
|
||||
the level above's tiles at odd zooms, other levels are rendered on the fly
|
||||
with an in-memory cache, `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.
|
||||
A map fed by an upstream tile-source server (`LIDAR_SOURCE_URL`, the Pi's
|
||||
case) pulls in and **keeps locally** the sources for each tile as soon as it
|
||||
appears (thumbnails and 500 m quadrants; the full tile, a duplicate of its
|
||||
quadrants, is not pulled in), without waiting for a visit. All levels remain
|
||||
available while the rendering container is off; a source missed while it was
|
||||
off is picked up on the next scan.
|
||||
|
||||
## Fiche de dalle et rose des vents
|
||||
## Tile sheet and compass rose
|
||||
|
||||
Un clic sur la carte sélectionne la dalle LiDAR HD sous le curseur (cadre
|
||||
jaune en pointillés) et remplit l'onglet **Dalle** — 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) —
|
||||
qu'elle soit générée ou non, sans changer l'onglet affiché. 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. Re-cliquer la même dalle (ou
|
||||
Échap) désélectionne.
|
||||
Clicking the map selects the LiDAR HD tile under the cursor (dashed yellow
|
||||
frame) and fills the **Dalle** (Tile) tab — name, Lambert 93 footprint, then,
|
||||
if the tile has been rendered, resolution, generation date and vertical pass
|
||||
alignment (beams, offsets, line correction) — whether it has been generated
|
||||
or not, without changing the displayed tab. IGN information arrives
|
||||
separately (`GET /api/map/ign?col&row`, STAC catalog cached in
|
||||
`output/ign_meta/`): LiDAR scan date and time, sensors, mission, operator,
|
||||
edit date, classification process, point count and a download link for the
|
||||
`.copc.laz` point cloud on the geoplatform. A slow or unreachable catalog
|
||||
never blocks selection. Clicking the same tile again (or Escape) deselects
|
||||
it.
|
||||
|
||||
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).
|
||||
When the oriented relief layer is displayed, a compass rose shows the color
|
||||
of each slope orientation (same CIELAB formula as the render); lightness
|
||||
carries the local relief (light = bump, dark = hollow).
|
||||
|
||||
## Affichage : relief et précision
|
||||
## Display: relief and precision
|
||||
|
||||
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.
|
||||
The map serves only the layers in `PANEL_VIZ` (`index.py`): the **relief
|
||||
orienté** (oriented relief, the main display layer, which merges local
|
||||
openness and slope orientation) and the **précision** (precision) layer
|
||||
(`densite_sol`: density of the ground points retained for the DTM). Other
|
||||
visualizations present on disk are neither listed nor served as tiles.
|
||||
|
||||
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 :
|
||||
There is no more layer stack. The panel offers three modes, one click each,
|
||||
or the **P** key to cycle to the next:
|
||||
|
||||
- **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 ;
|
||||
- **Comparer** — une barre glissante (souris ou doigt) sépare deux couches
|
||||
choisies dans deux menus (relief, précision ou fond OSM seul) de part et
|
||||
d'autre du curseur ; la même couche des deux côtés n'y ajoute aucune
|
||||
découpe.
|
||||
- **Relief** — the oriented relief alone;
|
||||
- **Précision** (Precision) — density alone, in 16 shades of gray (fixed log
|
||||
scale: level k starting at 0.25 × 2^(k/2) pts/m², black ≤ 0.35 or no
|
||||
points, white ≥ 45); reads as a geometric-reliability map;
|
||||
- **Comparer** (Compare) — a slider (mouse or touch) separates two layers
|
||||
chosen from two menus (relief, precision, or bare OSM background) on
|
||||
either side of the cursor; the same layer on both sides adds no split.
|
||||
|
||||
L'onglet Affichage porte aussi une **intensité du relief** (curseur 0,5×–2×,
|
||||
1× par défaut) : un contraste de confort posé sur le conteneur de la couche
|
||||
affichée (`contrast()` CSS), mémorisé et partagé dans le lien (`&I=`, écrit
|
||||
seulement si ≠ 1×) mais jamais figé comme défaut serveur ni appliqué au PDF
|
||||
exporté (qui garde le rendu standard). Un bloc **Comment lire la carte**,
|
||||
repliable, reprend pour la ou les couches affichées le texte de lecture de
|
||||
`VIZ_LEGENDS` (aussi servi par `/api/map/meta` et le TileJSON).
|
||||
The Affichage (Display) tab also carries a **relief intensity** slider
|
||||
(0.5×–2×, 1× by default): a comfort contrast applied to the displayed
|
||||
layer's container (CSS `contrast()`), remembered and shared in the link
|
||||
(`&I=`, written only if ≠ 1×) but never fixed as a server default nor
|
||||
applied to the exported PDF (which keeps the standard render). A
|
||||
collapsible **Comment lire la carte** (How to read the map) block reuses,
|
||||
for the displayed layer(s), the reading text from `VIZ_LEGENDS` (also served
|
||||
by `/api/map/meta` and the TileJSON).
|
||||
|
||||
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, seule ou d'un côté de la
|
||||
barre Comparer. Les couches LiDAR vivent dans un conteneur **isolé**
|
||||
(`isolation: isolate`) : la barre ne découpe que les calques LiDAR, jamais le
|
||||
fond de carte.
|
||||
The precision legend (16 levels, tooltip in pts/m² on each level) is shown
|
||||
as soon as precision is visible, either alone or on one side of the Compare
|
||||
slider. LiDAR layers live in an **isolated** container (`isolation:
|
||||
isolate`): the slider only splits the LiDAR layers, never the basemap.
|
||||
|
||||
Le lien de partage transporte la couche principale, le mode, la comparaison et
|
||||
l'intensité :
|
||||
The share link carries the main layer, the mode, the comparison and the
|
||||
intensity:
|
||||
`#z/lat/lng&M=relief_oriente&P=compare&C=relief:precision:30&I=1.4&B=1:85:1`
|
||||
(`&C=gauche:droite:position%` seulement en mode Comparer). Les anciens liens
|
||||
de la pile (`&L=…`) et de l'ancien mode « les deux » (`&P=both:opacité`,
|
||||
opacité alors ignorée) s'ouvrent sans erreur, en mode relief.
|
||||
(`&C=left:right:position%` only in Compare mode). Old links from the layer
|
||||
stack (`&L=…`) and the old "both" mode (`&P=both:opacity`, opacity then
|
||||
ignored) open without error, in relief mode.
|
||||
|
||||
### Figer la configuration
|
||||
### Freezing the configuration
|
||||
|
||||
Le bouton **★ Définir par défaut** enregistre l'affichage courant — couche
|
||||
principale, mode, fond de carte — dans `output/.map-defaults.json` (jamais
|
||||
l'intensité, réglage de confort propre à chaque navigateur). Tout navigateur
|
||||
sans réglage local part alors de cette configuration ; **↺ Réinitialiser**
|
||||
oublie l'état local et y revient.
|
||||
The **★ Définir par défaut** ("Set as default") button saves the current
|
||||
display — main layer, mode, basemap — to `output/.map-defaults.json` (never
|
||||
the intensity, a per-browser comfort setting). Any browser with no local
|
||||
setting then starts from this configuration; **↺ Réinitialiser** (Reset)
|
||||
forgets the local state and reverts to it.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8975/api/map/defaults # configuration servie
|
||||
curl -X DELETE http://localhost:8975/api/map/defaults # retour au registre
|
||||
curl http://localhost:8975/api/map/defaults # served configuration
|
||||
curl -X DELETE http://localhost:8975/api/map/defaults # revert to the registry
|
||||
```
|
||||
|
||||
Sans fichier enregistré, les défauts viennent du registre du pipeline
|
||||
(`DEFAULT_VIZ`, `PRECISION_VIZ`, `DEFAULT_VIEW_MODE` 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é du fond bornée à 0–1. Un
|
||||
fichier de l'ancienne pile (`order`/`on`/`blend`) est ignoré, sauf le fond.
|
||||
With no saved file, defaults come from the pipeline registry (`DEFAULT_VIZ`,
|
||||
`PRECISION_VIZ`, `DEFAULT_VIEW_MODE` in `index.py`). Received values are
|
||||
filtered: an unknown layer (or precision itself) is rejected as the main
|
||||
layer, the mode is validated, basemap opacity is clamped to 0–1. A file from
|
||||
the old stack (`order`/`on`/`blend`) is ignored, except for the basemap.
|
||||
|
||||
## Export PDF (planche d'impression terrain)
|
||||
## PDF export (field print sheet)
|
||||
|
||||
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 ne sélectionne pas 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).
|
||||
The **Export PDF** tab shows the settings (A4/A3 format, landscape/portrait,
|
||||
scale 1:1,000 to 1:10,000, optional title) and, while it stays open, a
|
||||
**dashed yellow frame** showing the area that will be printed (it disappears
|
||||
when switching tabs). The frame is **anchored to the terrain**: it is
|
||||
placed at the center of the view when opened (or keeps its previous
|
||||
position if it's still visible), the map zooms to show it in full above the
|
||||
panel, and one can then navigate freely without it moving. It is moved by
|
||||
dragging its **✥ handle** (mouse or touch); on release, the exact Lambert 93
|
||||
geometry is recomputed (`GET /api/export/frame`) — a release immediately
|
||||
followed by a click does not select a tile (300 ms guard). "⌖ Centrer ici"
|
||||
("Center here") brings it back to the center of the view, "⤢ Voir le cadre"
|
||||
("View the frame") zooms onto it; changing format, orientation or scale
|
||||
recenters the view. Settings and frame position are kept in the browser's
|
||||
`localStorage`. **Exporter le PDF** ("Export PDF") downloads the sheet (`GET
|
||||
/api/export/pdf`), named `relief_{x_km}_{y_km}_1-{scale}.pdf` (Lambert 93
|
||||
center in km, to three decimal places).
|
||||
|
||||
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 :
|
||||
The sheet (module `lidar_pipeline/export_pdf.py`) is composed directly in
|
||||
Lambert 93 from the already-rendered sources (no pass through the XYZ
|
||||
tiles), then drawn vectorially (text, grid, legend) with `reportlab` —
|
||||
Pillow + pyproj + reportlab only, **no numpy**: it runs equally well on the
|
||||
full image and on the Pi's lightweight image alone. Contents:
|
||||
|
||||
- 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.
|
||||
- the **oriented relief** map, cropped to the requested scale (300 dpi at
|
||||
A4, 250 dpi at A3 — bounds Pi memory usage, ~36 MB at A3); outside the
|
||||
available tiles' coverage, the area stays white and hatched;
|
||||
- a **Lambert 93 grid** (100 m spacing at scales 1:1,000/1:2,000, 500 m at
|
||||
1:5,000, 1,000 m at 1:10,000) graduated in the margin, and the printed
|
||||
area's **WGS84 corners** at all four angles;
|
||||
- a **geographic north arrow** accounting for meridian convergence (the map
|
||||
is oriented to the L93 grid north, not geographic north — the difference
|
||||
is shown in degrees);
|
||||
- a **graphic scale bar** (alternating bar) and the **numeric scale**;
|
||||
- an **8-point orientation rose** (N/NE/E/SE/S/SW/W/NW), same CIELAB formula
|
||||
as the tile sheet's rose;
|
||||
- the oriented relief's legend text, reused from `VIZ_LEGENDS` (single
|
||||
source shared with the interface and the TileJSON);
|
||||
- a **quality panel**: a thumbnail of ground-point density (50 m cells,
|
||||
color classes), key figures (average density, weakest cell, share of
|
||||
interpolated area, acquisition period), areas **hatched in white** where
|
||||
quality data is not available, and a "**Donnée manquante (sans relief)**"
|
||||
("Missing data (no relief)") line listing the tiles in the area that
|
||||
haven't been generated yet;
|
||||
- a title block: title (by default, the list of covered tiles), scale,
|
||||
format, dpi, L93 center, area size, export date and IGN source mention.
|
||||
|
||||
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.
|
||||
Only one PDF export at a time: a second call while an export is running gets
|
||||
`429` (retry). Export is **never delegated** to `LIDAR_GENERATION_URL`:
|
||||
unlike tile generation, the lightweight map alone (a Pi with no worker) can
|
||||
export by itself, from the sources already pulled to disk.
|
||||
|
||||
> **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.
|
||||
> **License** — LiDAR HD is distributed under the **Licence Ouverte 2.0**
|
||||
> ("Open License 2.0"): IGN attribution is mandatory and must remain visible
|
||||
> to the end user. Before using these renders as an OpenStreetMap **survey**
|
||||
> layer, check the position of the community (OSM-FR) on the source in
|
||||
> question.
|
||||
|
||||
## Comment une tuile est fabriquée
|
||||
## How a tile is built
|
||||
|
||||
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
|
||||
1. The tile's footprint is converted to Lambert 93 (5×5 sampling of the
|
||||
outline: edges aren't straight in L93).
|
||||
2. The 1 km tiles it intersects are found by their name
|
||||
(`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}`.
|
||||
3. For each one, the coarsest **source tier** that is still sufficient is
|
||||
chosen among those the pipeline already produces: `index_thumbs`
|
||||
thumbnail (≈3.9 m/px), intermediate `_mid` thumbnail (1.56 m/px),
|
||||
`index_subtiles` quadrants (2500 px, 4× less to decode than the full
|
||||
tile), then the full tile. The three folders are scanned
|
||||
**independently**: a partial cache — a lightweight machine that only
|
||||
pulls in quadrants and thumbnails, never full tiles — remains fully
|
||||
usable.
|
||||
4. The useful window is cropped, then reprojected via a **perspective
|
||||
transform** (`Image.transform(..., PERSPECTIVE)`), tile by tile: the
|
||||
measured alignment is sub-pixel.
|
||||
5. The result is encoded (PNG or WebP) and written to
|
||||
`output/index_xyz/{layer}/{z}/{x}/{y}[@2x].{ext}`.
|
||||
|
||||
Aucune dépendance GDAL/PDAL : **Pillow + pyproj** uniquement.
|
||||
No GDAL/PDAL dependency: **Pillow + pyproj** only.
|
||||
|
||||
### Cache et péremption
|
||||
### Cache and expiry
|
||||
|
||||
- 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.
|
||||
- A cached tile is re-served as long as **no contributing tile is more
|
||||
recent than it**: regenerating a tile only invalidates its own tiles.
|
||||
- A tile with no data is memoized with an `.empty` marker — never
|
||||
recomputed.
|
||||
- Decoded source images are kept in a small LRU cache: AVIF decoding
|
||||
dominates the cost, and neighboring tiles reuse it.
|
||||
- Client-side, the interface appends `?v=<stamp>` (most recent mtime) and
|
||||
then gets an immutable cache; OSM clients use the bare URL, served with
|
||||
short revalidation.
|
||||
|
||||
### Mesures (dalles réelles 0,2 m, bloc 3×3 km, conteneur sans GPU)
|
||||
### Measurements (real 0.2 m tiles, 3×3 km block, GPU-less container)
|
||||
|
||||
| | premier rendu | depuis le cache |
|
||||
| | first render | from cache |
|
||||
|---|---|---|
|
||||
| z10–z14 | 50–200 ms | 3–9 ms |
|
||||
| z16–z18 (résolution native) | 46–130 ms | 3–9 ms |
|
||||
| z16–z18 (native resolution) | 46–130 ms | 3–9 ms |
|
||||
|
||||
Poids d'une tuile de pente à z17, mesuré sur la même tuile :
|
||||
Weight of a slope tile at z17, measured on the same tile:
|
||||
|
||||
| encodage | poids | fidélité |
|
||||
| encoding | size | fidelity |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| PNG RGBA (default) | 210 KB | lossless |
|
||||
| Palettized PNG (`LIDAR_TILE_PNG_PALETTE=1`) | 55 KB | average deviation 5.8 levels |
|
||||
| WebP q78 (`…/{y}.webp`) | 38 KB | lossy, visually clean |
|
||||
|
||||
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`.
|
||||
The canonical PNG stays **lossless** by default: these renders are meant for
|
||||
interpretation, not illustration. For use as an imagery basemap where
|
||||
bandwidth matters, two levers: request the `.webp` URL (supported by QGIS,
|
||||
iD, MapLibre, uMap) or enable `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).
|
||||
Alignment checks performed on real data: an OSM path overlays exactly onto
|
||||
the trace visible in the slope layer, and the seam gap between neighboring
|
||||
tiles stays **below the terrain's natural noise** (measured gap 35–53
|
||||
levels against a median of 49–53 between two random neighboring columns
|
||||
within the same tile).
|
||||
|
||||
## Charge et petites machines
|
||||
## Load and small machines
|
||||
|
||||
Le service est borné à chaque étage, rien n'est illimité :
|
||||
The service is bounded at every stage, nothing is unlimited:
|
||||
|
||||
| Étage | Défaut | Réglage |
|
||||
| Stage | Default | Setting |
|
||||
|---|---|---|
|
||||
| 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` |
|
||||
| Concurrent renders | 2 | `LIDAR_TILE_WORKERS` |
|
||||
| Concurrent tile downloads | 2 | `LIDAR_TILE_FETCH_WORKERS` |
|
||||
| Same tile requested in parallel | 1 render, others wait for its result | — |
|
||||
| Decoded source memory | 192 MB | `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.
|
||||
Excess requests wait on the semaphore **before** any decoding: they consume
|
||||
neither CPU nor memory. A browser over HTTP/1.1 only opens 6 connections per
|
||||
origin anyway, across all layers combined; behind an HTTP/2 proxy this cap
|
||||
disappears and only these settings hold the load.
|
||||
|
||||
Le pré-chauffage (`/api/map/warm`) est séquentiel : il ne peut pas saturer la
|
||||
machine, seulement prendre du temps.
|
||||
Pre-warming (`/api/map/warm`) is sequential: it cannot saturate the machine,
|
||||
only take time.
|
||||
|
||||
Sur un Raspberry Pi 2 Go, `LIDAR_TILE_WORKERS=1` et
|
||||
`LIDAR_TILE_SOURCE_CACHE_MB=64` restent confortables.
|
||||
On a 2 GB Raspberry Pi, `LIDAR_TILE_WORKERS=1` and
|
||||
`LIDAR_TILE_SOURCE_CACHE_MB=64` remain comfortable.
|
||||
|
||||
## Cache seule + maintenance de fond (petit Raspberry Pi)
|
||||
## Cache-only + background maintenance (small 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 :
|
||||
On a machine that must never compute while someone is browsing (a Pi that
|
||||
also hosts other services), two variables invert the load:
|
||||
|
||||
```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 # browsing no longer renders anything
|
||||
- LIDAR_TILE_BACKGROUND=1 # a background task maintains the pyramid
|
||||
```
|
||||
|
||||
- **`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.
|
||||
- **`LIDAR_TILE_CACHE_ONLY=1`** — a missing or stale tile is served
|
||||
transparent with `X-Tile-Pending: 1` and `Cache-Control: no-store` (the
|
||||
browser re-requests it: as soon as maintenance has rendered it, it
|
||||
appears). No more local rendering on the request path; with
|
||||
`LIDAR_MAPS_URL`, a missing tile (not yet handled by maintenance) is
|
||||
pulled in from there — a download of a few dozen KB, cached — then
|
||||
served: browsing covers all zoom levels without ever computing on the
|
||||
small machine.
|
||||
- **`LIDAR_TILE_BACKGROUND=1`** — a poller rescans tiles at a regular
|
||||
interval (`LIDAR_TILE_BACKGROUND_INTERVAL`, 120 s); each new or
|
||||
regenerated tile (the worker just produced it, or the on-demand cache just
|
||||
pulled it in) queues its pyramid. Low-priority renderers (`os.nice`) drain
|
||||
it at a rate of one tile per `LIDAR_TILE_BACKGROUND_PAUSE` second (1 s).
|
||||
First start-up: ALL tiles are unknown, the whole pyramid is rebuilt —
|
||||
already-fresh tiles only cost a `stat`. Levels are queued from the
|
||||
smallest zoom to the largest: the map fills in coarsely first.
|
||||
|
||||
| Variable | Défaut | Rôle |
|
||||
| Variable | Default | Role |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
| `LIDAR_TILE_BACKGROUND_MAX_Z` | native (`18` at @2x, `19` at 256 px) | maximum level maintained in the background, URL numbering |
|
||||
| `LIDAR_TILE_BACKGROUND_SCALE` | `2` | tiles maintained: 2 = 512 px (the interface's) |
|
||||
| `LIDAR_TILE_BACKGROUND_FMT` | `webp` | format of maintained tiles (the interface's) |
|
||||
| `LIDAR_TILE_BACKGROUND_PAUSE` | `1.0` | pause (s) between two renders — the discretion lever |
|
||||
| `LIDAR_TILE_BACKGROUND_INTERVAL` | `120` | seconds between two tile scans |
|
||||
| `LIDAR_TILE_BACKGROUND_QUEUE_MAX` | `65536` | bounded queue — each tile remembers the first pyramid tile it was refused for lack of room and resumes from there on the next scans, until its whole pyramid has gone through |
|
||||
|
||||
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`.
|
||||
Control: `GET /api/map/background` (state, counters, queue), `POST
|
||||
/api/map/background` (immediate scan), `POST /api/map/warm` (manual
|
||||
exhaustive pre-computation, all zooms/formats, see below). Finally, capping
|
||||
the container itself (`mem_limit` + `memswap_limit` in a compose override)
|
||||
guarantees a misbehaving render can no longer bring down the machine: the
|
||||
OOM killer would only ever hit `lidar-maps`.
|
||||
|
||||
## Pré-chauffage
|
||||
## Pre-warming
|
||||
|
||||
```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
|
||||
curl http://localhost:8975/api/map/warm # progress / report
|
||||
```
|
||||
|
||||
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.
|
||||
Without `bounds`, the coverage of available tiles is used. Useful after a
|
||||
large run so the first visit is instant.
|
||||
|
||||
## Deux machines
|
||||
## Two machines
|
||||
|
||||
Deux amonts complémentaires, selon ce que la machine locale possède.
|
||||
Two complementary upstreams, depending on what the local machine has.
|
||||
|
||||
`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 :
|
||||
`LIDAR_SOURCE_URL` designates **the full pipeline server** (port 8973): the
|
||||
tile inventory comes from its `/api/tiles`, and each source image is pulled
|
||||
in on the first render that needs it. The map container then starts with no
|
||||
local data at all and serves the entire remote catalog:
|
||||
|
||||
```bash
|
||||
docker run -d --name lidar-maps --network host \
|
||||
@ -417,48 +416,48 @@ docker run -d --name lidar-maps --network host \
|
||||
-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é.
|
||||
Measured on 849 real tiles served over an SSH tunnel: first tile in an area
|
||||
0.9–3.2 s (including downloading a 1.8 MB quadrant), then **~1 ms** from the
|
||||
local cache, which only keeps what has been consulted.
|
||||
|
||||
`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.
|
||||
`LIDAR_MAPS_URL` designates an upstream tile server (the processing
|
||||
machine): a tile missing locally is pulled in from there (a few dozen KB)
|
||||
then cached — instead of pulling in whole tiles. A circuit breaker suspends
|
||||
attempts for 2 minutes after 3 consecutive failures: the map stays usable
|
||||
even with the worker off.
|
||||
|
||||
## Variables d'environnement
|
||||
## Environment variables
|
||||
|
||||
| Variable | Défaut | Rôle |
|
||||
| Variable | Default | Role |
|
||||
|---|---|---|
|
||||
| `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) |
|
||||
| `LIDAR_OUTPUT_DIR` | `/data/output` | tiles read from, `index_xyz/` cache written to |
|
||||
| `LIDAR_PORT` | `8975` | listening port |
|
||||
| `LIDAR_SOURCE_URL` | — | full pipeline server: inventory + tiles pulled in on demand |
|
||||
| `LIDAR_SOURCE_TOKEN` | — | token if the upstream server requires `LIDAR_API_TOKEN` |
|
||||
| `LIDAR_MAPS_URL` | — | upstream tile server (remote map) |
|
||||
| `LIDAR_ATTRIBUTION` | LiDAR HD © IGN | attribution served (TileJSON, WMTS, JOSM, map) |
|
||||
| `LIDAR_TILE_PNG_PALETTE` | — | `1`: palettized PNG (~4× lighter, ~6-level deviation) |
|
||||
| `LIDAR_TILE_SOURCE_CACHE_MB` | `192` | memory budget for the decoded source-image cache |
|
||||
| `LIDAR_TILE_WORKERS` | `2` | concurrent tile renders |
|
||||
| `LIDAR_TILE_FETCH_WORKERS` | `2` | concurrent tile downloads (`LIDAR_SOURCE_URL` mode) |
|
||||
| `LIDAR_SSL_CERTFILE` / `LIDAR_SSL_KEYFILE` | — | direct HTTPS (GPS on a phone) |
|
||||
|
||||
## Dépannage
|
||||
## Troubleshooting
|
||||
|
||||
- **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
|
||||
- **Empty map, `layers: 0`** (`curl /healthz`): no render in
|
||||
`output/visualisations/`, or the folder is mounted at the wrong path.
|
||||
- **Tiles transparent everywhere**: zoom out of range (5–19) or an area with
|
||||
no tiles; the `X-Tile-Empty` header confirms this.
|
||||
- **First visit is slow**: normal, each tile is rendered once — pre-warm
|
||||
(see above).
|
||||
- **JOSM rejects the URL**: use the `{zoom}/{x}/{y}` template (JOSM), not
|
||||
`{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.
|
||||
- **Layer missing from the menu**: it doesn't exist on disk for these tiles —
|
||||
`/api/map/meta` lists what is actually available.
|
||||
|
||||
## Références
|
||||
## References
|
||||
|
||||
- `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`.
|
||||
- `lidar_pipeline/tiles.py` — grid, reprojection, cache, pre-warming.
|
||||
- `lidar_pipeline/mapserve.py` — tile/TileJSON/WMTS/JOSM routes and the map API.
|
||||
- `lidar_pipeline/web/map.{html,css,js}` — interface (tabbed panel, Leaflet `L.tileLayer`, native LOD), re-read by `lidar_pipeline/mapui.py`.
|
||||
- `Dockerfile.maps`, `docker-compose.maps.yml`, `./run.sh --serve-maps`.
|
||||
|
||||
Reference in New Issue
Block a user