diff --git a/AGENTS.md b/AGENTS.md index b05239c..d133b75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ ## Conventions -- **Un seul serveur web : `mapserve.py`** (image `lidar-maps`, port 8975 léger / 8973 worker). L'ancienne webapp (`webapp.py`, `export.py`, index.html/`_APP_JS`) a été supprimée : la génération de tuiles (portée de la webapp historique — `/api/preview`, `/api/generate`, `/api/status`, `/api/stop`, `/api/queue/clear`, `/api/cell`) vit dans `mapserve.py`, l'interface dans `mapui.py` (constantes `_MAP_HTML`/`_MAP_CSS`/`_MAP_JS`, écrites par `write_map_assets()` et bâchées dans les images). Sur l'image légère sans `LIDAR_GENERATION_URL`, `/api/status` répond `available: false` et l'interface masque les boutons. +- **Un seul serveur web : `mapserve.py`** (image `lidar-maps`, port 8975 léger / 8973 worker). L'ancienne webapp (`webapp.py`, `export.py`, index.html/`_APP_JS`) a été supprimée : la génération de tuiles (portée de la webapp historique — `/api/preview`, `/api/generate`, `/api/status`, `/api/stop`, `/api/queue/clear`, `/api/cell`) vit dans `mapserve.py`, l'interface dans `web/map.{html,css,js}` (relus par `mapui.py`, constantes `_MAP_*` conservées, écrites par `write_map_assets()` et bâchées dans les images). Sur l'image légère sans `LIDAR_GENERATION_URL`, `/api/status` répond `available: false` et l'interface masque l'onglet Génération. +- **Interface en panneau unique** (`web/map.{html,css,js}`) : un seul panneau à onglets Affichage / Export PDF / Génération (masqué si le générateur est indisponible ou non autorisé) / Partager, remplaçant l'ancienne pile de blocs empilés. Sous 720 px de large, le panneau devient un volet en bas d'écran à trois hauteurs (`closed`/`half`/`full`, poignée `#sheetGrip` glissée ou simplement touchée, ou onglet actif retouché). Un clic sur une dalle ouvre une bulle (date, densité) → « Détails » bascule une fiche complète dans le panneau (« ← Retour » rouvre l'onglet précédent) ; Échap ferme la bulle, puis la fiche, puis replie le panneau (bande d'icônes sur ordinateur, volet fermé sur téléphone — la bande reste utilisable, un clic sur un onglet redéplie le panneau). Raccourcis clavier : 1–4 (onglets, sans effet si l'onglet est masqué), P (mode d'affichage suivant), Échap. Deux thèmes clair/sombre (`lidar-theme`, `auto` par défaut, suit le système) et tous les réglages communs posés sur `:root` en variables CSS (aucune couleur en dur dans les composants). Clés `localStorage` (via `lsGet`/`lsSet`, silencieux en navigation privée) : `lidarMapView_v2` (vue/couches), `lidar-print` (réglages d'export), `lidar-panel` (onglet/hauteur du volet), `lidar-theme`. - **`index.py` = catalogue + registres partagés** (plus d'interface) : `VIZ_LABELS`/`VIZ_LEGENDS`, défauts d'affichage (`DEFAULT_VIZ`/`PRECISION_VIZ`/`VIEW_MODES`), `PANEL_VIZ`/`KEYWORD_TO_STEP`, `scan_tiles`/`cells_with_all_viz`, vignettes + sous-tuiles + inventaire `index_tiles.json` (`build_index`). L'inventaire est servi par `/api/tiles` de mapserve aux machines légères (`LIDAR_SOURCE_URL`). - **Generation is 0.2 m only** (policy): `/api/generate` (`GENERATE_RESOLUTIONS` in `mapserve.py`), the compose `process` command and the CLI `-r` default all produce 0.2 m exclusively; 0.5 m stays available via explicit `-r 0.5`. Completeness detection (`complete_cells`) requires the viz at 0.2 m only. - **Génération du nord au sud** : les tuiles sont traitées par ligne décroissante (row = nord en km), colonnes croissantes — `find_laz_files` (pipeline.py) pour les passes batch et `_resolve_request` (mapserve.py) pour les runs lancés depuis la carte. Les workers prennent les fichiers dans l'ordre de soumission : la carte se remplit de haut en bas pendant un run (`--file` explicite au CLI = ordre utilisateur préservé). Parallélisme de génération : `LIDAR_WORKERS` (10 dans les compose ; `auto` sinon). @@ -50,7 +51,7 @@ - **Calage vertical des faisceaux de vol** : chaque tuile mélange plusieurs passes (1-2 `PointSourceId` par passe) parfois biaisées verticalement de quelques cm (±2,5 cm mesurés sur 1000_6882). `create_dtm_fast` mesure l'offset robuste de chaque faisceau (points sol, maille 1 m, surface médiane itérée 3×) et retranche les offsets ≥ 0,5 cm (`STRIP_ALIGN_THRESHOLD` dans `dtm.py`) avant rastérisation. Offsets calculés **par tuile** (ils dérivent le long d'une ligne de vol : jamais de table globale), mémoïsés par LAS sol, consignés dans `DTM/*_dtm*_stripalign.json` (sidecar de cache : absent, ou version/seuil/paramètres différents ⇒ régénération du DTM). Désactivable : `--no-strip-align`. - **Gigue intra-faisceau (2ᵉ passe du calage)** : les lignes de balayage successives d'une MÊME passe peuvent être décalées verticalement de façon aléatoire (vibration capteur / bruit haute fréquence de trajectoire) — un offset constant par faisceau n'y suffit pas. `_strip_jitter_offsets` découpe chaque faisceau en fenêtres de temps GPS (`STRIP_JITTER_BIN` = 0,1 s, origine de temps propre à chaque faisceau), mesure l'offset robuste de chaque fenêtre contre la surface médiane des AUTRES faisceaux (maille 1 m partagée, ≥ `STRIP_JITTER_MIN_CELLS` = 40 cellules), lisse la série (médiane glissante `STRIP_JITTER_SMOOTH` = 5 fenêtres), la borne à ± `STRIP_JITTER_MAX` (10 cm) puis l'interpole au temps GPS de chaque point (`_apply_strip_jitter`) ; en recouvrement à deux faisceaux, chacun reçoit une série (chacun absorbe sa part). Requiert la dimension `gps_time` (silencieusement ignorée sinon). Sidecar version 2 (séries dans `jitter`), couverte par `--no-strip-align`. - **Couches produites et servies = `PANEL_VIZ` (`index.py`, aujourd'hui `('relief_oriente', 'densite_sol')`)** : pipeline sans `--only`/`--skip` (`panel_steps()`), génération lancée depuis la carte (`_panel_viz_steps` dans `mapserve.py`) et couches servies (`tiles.available_layers` filtre : panneau, `/tiles/…`, TileJSON, WMTS, JOSM). Les autres visualisations restent calculables avec `--only`. Les tests de la carte lèvent la restriction via `_setup(..., panel=None)`. -- **Précision (`densite_sol`) et affichage de la carte** : `create_dtm_fast` écrit la densité des points sol retenus (bande de raccord comprise) dans `DTM/*_dtm*_density.tif` (mailles 1 m, moyenne 3 × 3, pts/m²) ; `generate_densite_sol` la quantifie en 16 niveaux (`density_levels` : échelle log fixe, niveau k dès 0,25 × 2^(k/2) pts/m²) et la garde à 1 m (dalle 1000², ~200 Ko en WebP sans perte, `rendering.LOSSLESS_GRAY_KEYWORDS`) ; tuiles au plus proche voisin (`tiles.NEAREST_LAYERS`) pour garder 16 gris nets aux zooms 18–19. Interface (`mapui.py`) : plus de pile de couches — une couche principale (`DEFAULT_VIZ`) et trois modes relief / précision / les deux (`VIEW_MODES`, précision en `multiply` à `DEFAULT_PRECISION_OPACITY` 0,6), touche P, lien `&M=…&P=mode:opacité`, défauts figés `{main, mode, precision_opacity, base}`. +- **Précision (`densite_sol`) et affichage de la carte** : `create_dtm_fast` écrit la densité des points sol retenus (bande de raccord comprise) dans `DTM/*_dtm*_density.tif` (mailles 1 m, moyenne 3 × 3, pts/m²) ; `generate_densite_sol` la quantifie en 16 niveaux (`density_levels` : échelle log fixe, niveau k dès 0,25 × 2^(k/2) pts/m²) et la garde à 1 m (dalle 1000², ~200 Ko en WebP sans perte, `rendering.LOSSLESS_GRAY_KEYWORDS`) ; tuiles au plus proche voisin (`tiles.NEAREST_LAYERS`) pour garder 16 gris nets aux zooms 18–19. Interface (`web/map.js`, onglet Affichage) : plus de pile de couches — une couche principale (`DEFAULT_VIZ`) et trois modes relief / précision / les deux (`VIEW_MODES`, précision en `multiply` à `DEFAULT_PRECISION_OPACITY` 0,6), touche P, lien `&M=…&P=mode:opacité`, défauts figés `{main, mode, precision_opacity, base}`. - **Relief orienté (`relief_oriente`, seule couche de la carte)** : image RGB unique (GeoTIFF uint8 3 bandes, rendue telle quelle comme ortho/topo via `RGB_KEYWORDS` dans `rendering.py`) — clarté CIELAB = openness positive **locale** (MNT − gaussienne `RELIEF_DETREND_M` = 10 m, rayons `RELIEF_RADII_M` = 5/10/20 m, 16 directions) 65 % + ombrage 35 % ; teinte = aspect, chroma fixe (`RELIEF_CHROMA`). Échelle log fixe `RELIEF_OPEN_RANGE` (pas de statistique par dalle) et support total 40 m < bande de raccord 100 m : dalles jointives. Rapide : détendance + rayons sur grille décimée ~0,8 m (`RELIEF_GRID_M`), noyau dédié qui n'accumule que la moyenne des angles (`_mean_horizon_*` : CuPy `RawKernel` sur GPU, numba parallèle sur CPU, numpy en repli), colorisation fusionnée (numba) ou vectorisée sans trigonométrie (CuPy) via une table L* × teinte (`_relief_lut`). ~5 s de calcul hors préparation sur CPU 12 cœurs. Tout changement de constante change le rendu : régénérer les dalles (`--only relief_oriente --force`). - **Openness sous-échantillonnée** : `generate_openness` calcule le lancé de rayons (l'étape la plus coûteuse : 532 s/tuile à 0,2 m sur CPU) sur une grille décimée par blocs (`OPENNESS_DOWNSAMPLE = 2` : max par bloc en positive, min en négative — préserve les reliefs qui bornent l'horizon) puis rééchantillonne en bilinéaire. Coût ÷ facteur³ : 532 s → 40 s (×13). Signal archéologique préservé (corr. 0,93 après lissage) ; la texture de bruit sub-métrique disparaît. `--openness-downsample 1` = pleine résolution. SVF et openness anisotrope ne sont PAS concernées. - **Raccord des bords entre tuiles** : les rendus à grand noyau (openness/SVF : rayons 100 m ; LRM : 15 m) tronquent leur fenêtre au bord de dalle — bandes d'artefacts à chaque changement de tuile. `--edge-buffer N` (défaut 100 ; toujours appliqué par la génération depuis la carte, `EDGE_BUFFER_METERS` = 100 m dans `mapserve.py`, plus de case à cocher ; 0 = off en ligne de commande) fait rastériser le DTM sur la **dalle nominale 1 km alignée sur la grille** plus une bande de N m remplie avec les points sol des 8 LAZ voisines (`_neighbor_ground_points` dans `dtm.py` : PDAL en flux, découpe + filtre de classes IGN ; voisine absente = téléchargement automatique depuis le catalogue IGN avant le run, **isolée dans `input/edge_neighbors/`** pour ne pas gonfler le corpus des passes globales (dédupliqué sur tout le lot, `_fetch_edge_neighbors` dans `pipeline.py`) ; introuvable ou échec = bande vide). Les visualisations calculent sur l'emprise étendue puis `rendering.py` (`_core_tile_window`, via `tif_to_crop`/`tif_to_png`) recadre les sorties sur la dalle 1 km exacte lue dans le nom LHD — les AVIF restent des carrés 1 km alignés dans la mosaïque. Tampon consigné dans le tag GeoTIFF `LIDAR_EDGE_BUFFER` du DTM : changer `--edge-buffer` invalide le cache DTM automatiquement (tag absent = 0). Bandes voisines non calées par faisceaux (contexte seul, recadrée hors image finale). Coût : ~7 s de lecture par voisine + ~44 % de pixels en plus à 100 m/0,2 m. Nom hors pattern LHD : option ignorée (bornes d'en-tête, pas de recadrage). diff --git a/docs/MAPS.md b/docs/MAPS.md index 170537e..6f3a576 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -16,16 +16,40 @@ 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** (barre d'outils) — dessiner un rectangle : les dalles LHD de 1 km - qui l'intersectent sont téléchargées depuis la géoplateforme IGN puis +- **+ 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 cette dalle** (fiche d'infos au clic) — une dalle précise, - même sans données existantes. +- **↻ 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 @@ -106,15 +130,18 @@ source manquée pendant qu'il était éteint est reprise au scan suivant. ## Fiche de dalle et rose des vents Un clic sur la carte sélectionne la dalle LiDAR HD sous le curseur (cadre -jaune en pointillés), qu'elle soit générée ou non. La fiche s'affiche tout de -suite avec le nom, l'emprise Lambert 93, puis, si la dalle est rendue, la -résolution, la date de génération et le recalage vertical des passes -(faisceaux, décalages, correction des lignes). Les informations IGN arrivent -à part (`GET /api/map/ign?col&row`, catalogue STAC mis en cache dans -`output/ign_meta/`) : date et heure du scan LiDAR, capteurs, mission, -opérateur, date d'édition, procédé de classement, nombre de points et lien de -téléchargement du nuage `.copc.laz` sur la géoplateforme. Un catalogue lent -ou injoignable n'empêche jamais la sélection. +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é @@ -169,17 +196,19 @@ principale, mode validé, opacités bornées à 0–1. Un fichier de l'ancienne ## Export PDF (planche d'impression terrain) -Le bouton **⎙** ouvre une carte de réglages (format A4/A3, paysage/portrait, -échelle 1:1 000 à 1:10 000, titre optionnel) et affiche un **cadre jaune en -pointillés** qui montre la zone qui sera imprimée. Le cadre est **posé sur le +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 des panneaux, puis on navigue librement sans qu'il bouge. On le +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`). -« ⌖ 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 +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). @@ -420,5 +449,5 @@ consultable même worker éteint. - `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). +- `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`.