From bae16923fc21b940eca46cee691d55496538734b Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 20:56:19 +0200 Subject: [PATCH] =?UTF-8?q?Corriger=20la=20revue=20finale=20:=20onglets=20?= =?UTF-8?q?non=20tronqu=C3=A9s,=20fiche=20de=20dalle=20=C3=A0=20jour,=20cl?= =?UTF-8?q?ics=20et=20lien=20fiables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Onglet Dalle libellé fixe (col·row dans le title), Affichage/Export PDF/ Génération raccourcis pour tenir dans le panneau ; fiche de dalle rechargée après un run sans désélection et dès que /api/status révèle le bouton Régénérer ; double-clic et réponses en retard ignorés sur la sélection ; curseur d'intensité visible hors mode précision ; lien complet sans &I remis à 1× ; applyCompareClip sort tôt hors mode Comparer ; docs/MAPS.md et AGENTS.md à jour (modes Comparer, &I=, « Comment lire »). Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 2 +- docs/MAPS.md | 49 ++++++++------ lidar_pipeline/tests/test_mapserve.py | 93 +++++++++++++++++++++++++++ lidar_pipeline/web/map.html | 18 +++--- lidar_pipeline/web/map.js | 42 ++++++++++-- 5 files changed, 171 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 05506bd..aac6813 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ ## 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 `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 / Dalle / 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 la **sélectionne** (contour) 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. Échap désélectionne la dalle 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–5 (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, repli en bande d'icônes), `lidar-theme`. +- **Interface en panneau unique** (`web/map.{html,css,js}`) : un seul panneau à onglets Affichage / Dalle / 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 la **sélectionne** (contour) 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. Échap désélectionne la dalle 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–5 (onglets, sans effet si l'onglet est masqué), P (mode d'affichage suivant), Échap. Intensité du relief (`&I=`, contraste 0,5–2×, défaut 1×) : réglage de confort personnel mémorisé dans `lidarMapView_v2.intensity` et partagé dans le lien, jamais figé comme défaut serveur (`★ Définir par défaut` l'ignore). 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, repli en bande d'icônes), `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). diff --git a/docs/MAPS.md b/docs/MAPS.md index 3d00142..a3085f8 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -160,25 +160,39 @@ chacun, ou la touche **P** pour passer au suivant : - **Précision** — la densité seule, en 16 gris (échelle log fixe : niveau k à partir de 0,25 × 2^(k/2) pts/m², noir ≤ 0,35 ou aucun point, blanc ≥ 45) ; se lit comme une carte de fiabilité géométrique ; -- **Les deux** — la précision en « produit » (`mix-blend-mode: multiply`) sur - le relief, opacité réglable (60 % par défaut) : les zones où le relief est - interpolé s'assombrissent sans masquer le relief. +- **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. + +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). La légende de la précision (16 paliers, info-bulle en pts/m² sur chaque -palier) s'affiche dès que la précision est visible. Les couches LiDAR vivent -dans un conteneur **isolé** (`isolation: isolate`) : le produit n'agit que sur -le relief, jamais sur le fond de carte. +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. -Le lien de partage transporte la couche principale, le mode et l'opacité : -`#z/lat/lng&M=relief_oriente&P=both:60&B=1:85:1`. Les anciens liens de la pile -(`&L=…`) s'ouvrent sans erreur sur l'état courant. +Le lien de partage transporte la couche principale, le mode, la comparaison et +l'intensité : +`#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. ### Figer la configuration Le bouton **★ Définir par défaut** enregistre l'affichage courant — couche -principale, mode, opacité de la précision, fond de carte — dans -`output/.map-defaults.json`. Tout navigateur sans réglage local part alors de -cette configuration ; **↺ Réinitialiser** oublie l'état local et y revient. +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. ```bash curl http://localhost:8975/api/map/defaults # configuration servie @@ -186,11 +200,10 @@ curl -X DELETE http://localhost:8975/api/map/defaults # retour au registre ``` Sans fichier enregistré, les défauts viennent du registre du pipeline -(`DEFAULT_VIZ`, `PRECISION_VIZ`, `DEFAULT_VIEW_MODE`, -`DEFAULT_PRECISION_OPACITY` dans `index.py`). Les valeurs reçues sont -filtrées : couche inconnue (ou la précision elle-même) refusée comme -principale, mode validé, opacités bornées à 0–1. Un fichier de l'ancienne pile -(`order`/`on`/`blend`) est ignoré, sauf le fond. +(`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. ## Export PDF (planche d'impression terrain) @@ -203,7 +216,7 @@ précédente si elle est encore visible), la carte zoome pour le montrer en entier au-dessus du panneau, puis on navigue librement sans qu'il bouge. On le déplace en faisant glisser sa **poignée ✥** (souris ou doigt) ; au relâchement, la géométrie Lambert 93 exacte est recalculée (`GET /api/export/frame`) — un -relâchement suivi d'un clic immédiat n'ouvre pas la bulle de dalle (garde de +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 diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 9473c70..41a99cc 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -1694,3 +1694,96 @@ def test_ui_panel_restores_tab_and_ignores_collapse_on_phone(): assert "PANEL.collapsed && !isPhone()" in render assert "toggle('collapsed', PANEL.collapsed)" not in render assert "toggle('panel-collapsed', PANEL.collapsed)" not in render + + +def test_ui_tile_tab_label_stays_dalle_col_row_in_title(): + """Revue finale (finding 2) : 5 onglets dans 340 px — le libellé de + l'onglet Dalle reste toujours « Dalle » (jamais tronqué), col · row va + dans le title de l'onglet (survol) et #tileTitle (déjà dans la fiche). + Affichage/Export PDF/Génération raccourcis pour tenir, Partager gardé.""" + from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS + assert 'Vue' in _MAP_HTML + assert 'PDF' in _MAP_HTML + assert 'Générer' in _MAP_HTML + assert 'Dalle' in _MAP_HTML + assert 'data-tab="share"' in _MAP_HTML and '>Partager' in _MAP_HTML + # Le title complet (survol) reste explicite malgré le libellé court + assert 'title="Affichage (1)"' in _MAP_HTML + assert 'title="Export PDF (3)"' in _MAP_HTML + assert 'title="Génération (4)"' in _MAP_HTML + assert "function tileTabTitle(" in _MAP_JS + assert "textContent = tileTabLabel" not in _MAP_JS # plus de libellé dynamique + assert "button[data-tab=\"tile\"]').title = tileTabTitle()" in _MAP_JS + + +def test_ui_tile_card_refetched_after_gen_run(): + """Finding 3 : la fiche de dalle sélectionnée avant/pendant un run devient + périmée (résolution, date, couches...) — genAfterRun la recharge par + api/map/tile sans jamais désélectionner (pas de clearTileSelection).""" + from lidar_pipeline.mapui import _MAP_JS + fn = _MAP_JS[_MAP_JS.index("function genAfterRun("):_MAP_JS.index("function genUpdateStatus(")] + assert "SELECTED_TILE" in fn and "lastTileQuery" in fn + assert "api/map/tile?lat=" in fn + assert "selectTile(d, lastTileQuery)" in fn + assert "clearTileSelection" not in fn + + +def test_ui_double_click_and_stale_click_response_ignored(): + """M1/M2 : un double-clic (zoom Leaflet, deux clics simples dont le + second a detail > 1) ne doit pas re-basculer la sélection posée par le + premier ; une réponse arrivée après un clic plus récent (jeton de + séquence) est ignorée plutôt que d'écraser l'état.""" + from lidar_pipeline.mapui import _MAP_JS + click = _MAP_JS[_MAP_JS.index("map.on('click'"):] + body = click[:click.index("});") + 3] + assert "e.originalEvent && e.originalEvent.detail > 1" in body + assert "let tileClickSeq = 0;" in _MAP_JS + assert "const seq = ++tileClickSeq;" in body + assert "if (seq !== tileClickSeq) return;" in body + + +def test_ui_regen_button_appears_when_gen_status_resolves_late(): + """M3 : une dalle sélectionnée avant que /api/status ne réponde ne doit + pas rester sans bouton Régénérer — la fiche est ré-émise dès que GEN est + connu, dans le même bloc qui pose GEN.available/allowed.""" + from lidar_pipeline.mapui import _MAP_JS + boot = _MAP_JS[_MAP_JS.index("fetch('api/map/meta').then(r => r.json()).then(meta => {"):] + status_block = boot[:boot.index("}).catch(() => {\n toast('Serveur de tuiles injoignable.'")] + assert "GEN.available = !!d.available;" in status_block + assert "if (SELECTED_TILE) renderTabTile();" in status_block + + +def test_ui_intensity_row_visible_outside_precision_block(): + """M4 (liaison de spec) : #intensityRow est un frère de #precBlock, pas un + enfant — visible dès l'onglet Affichage même sans couche de précision, + masqué seulement en mode précision (renderPanel).""" + from lidar_pipeline.mapui import _MAP_HTML + assert '