From 90663405f7ac3d9d8c294608cc80491e1431f019 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 20:22:07 +0200 Subject: [PATCH] =?UTF-8?q?Remplacer=20la=20bulle=20et=20la=20fiche=20d?= =?UTF-8?q?=C3=A9taill=C3=A9e=20de=20dalle=20par=20un=20onglet=20Dalle=20d?= =?UTF-8?q?u=20panneau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/MAPS.md | 38 ++++--- lidar_pipeline/tests/test_mapserve.py | 85 ++++++--------- lidar_pipeline/web/map.css | 7 -- lidar_pipeline/web/map.html | 19 ++-- lidar_pipeline/web/map.js | 149 +++++++++----------------- 6 files changed, 112 insertions(+), 188 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 58f90b1..05506bd 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 / 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, 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. 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 6f3a576..3d00142 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -29,12 +29,12 @@ clic sur un onglet redéplie le panneau). Sous 720 px de large, il devient un 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 +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`, @@ -48,8 +48,8 @@ défaut = suit le système) ; réglages et état du panneau retenus dans 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, bulle → - Détails) — 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, onglet + Dalle) — 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 @@ -130,18 +130,16 @@ 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) 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. +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. 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é diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index bb437ba..bae5ccb 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -346,24 +346,26 @@ def test_map_ui_tile_load_indicator_and_selection_pane(): assert "pane: 'tileSel'" in _MAP_JS -def test_ui_tile_popup_and_details_view(): - """La fiche de dalle est une bulle Leaflet + une vue détaillée dans le panneau.""" +def test_ui_tile_tab_replaces_popup_and_details_view(): + """Un clic sur la carte sélectionne la dalle (contour) et remplit + l'onglet Dalle : plus de bulle Leaflet ni de vue « Détails » séparée.""" from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS assert 'id="tilecard"' not in _MAP_HTML - for id_ in ("tileView", "tileBack", "tileTitle", "tileBody"): + assert 'data-tab="tile"' in _MAP_HTML + for id_ in ("tab-tile", "tileHint", "tileCard", "tileTitle", "tileBody", "tileTabLbl"): assert f'id="{id_}"' in _MAP_HTML, id_ - for fn in ("function showTilePopup(", "function openTileDetails(", "function closeTileViews("): + for gone in ("tileView", "tileBack", "showTilePopup", "openTileDetails", + "closeTileViews", "hideTileView", "restoreTabAfterDetails", + "RESTORING_TAB", "tileReturnTab", "L.popup("): + assert gone not in _MAP_JS, gone + assert "leaflet-popup" not in _MAP_CSS and "tile-pop" not in _MAP_CSS + for fn in ("function selectTile(", "function clearTileSelection(", "function renderTabTile("): assert fn in _MAP_JS, fn - assert "L.popup(" in _MAP_JS and "Détails" in _MAP_JS - # Pas de bulle pendant le déplacement du cadre d'export ni un tracé + # Pas de sélection pendant le déplacement du cadre d'export ni un tracé click = _MAP_JS[_MAP_JS.index("map.on('click'"):] assert "genDrawing" in click[:400] and "PRINT.dragging" in click[:400] - # Changer d'onglet pendant que la fiche est ouverte nettoie le contour - # de sélection (pas de fuite de tileSel) : openTab passe par le même - # nettoyage que closeTileViews, sans se rappeler lui-même. - assert "function hideTileView(" in _MAP_JS - open_tab = _MAP_JS[_MAP_JS.index("function openTab("):] - assert "hideTileView()" in open_tab[:200] + # Re-clic sur la dalle déjà sélectionnée : désélection plutôt que bascule + assert "clearTileSelection();" in click[click.index("SELECTED_TILE"):click.index("});")] def test_defaults_roundtrip(tmp_path, monkeypatch): @@ -567,22 +569,15 @@ def test_ui_base_dark_follows_theme_unless_chosen(): assert "className: themeIsDark() ? 'base-dark' : ''" in _MAP_JS -def test_ui_phone_return_and_escape_keep_sheet_height(): - """« ← Retour » et Échap depuis la fiche de dalle rétablissent l'onglet - précédent SANS repasser par openTab (qui retoucherait la hauteur du volet - en re-touchant l'onglet déjà actif) : la fiche ferme via un rétablissement - direct de PANEL.tab/want, partagé par closeTileViews et showTilePopup.""" +def test_ui_escape_deselects_tile_before_collapsing_panel(): + """Échap désélectionne la dalle (si sélection il y a) avant de replier le + panneau ; un tracé de zone en cours est annulé en premier, comme avant.""" from lidar_pipeline.mapui import _MAP_JS - assert "function restoreTabAfterDetails()" in _MAP_JS - restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):] - assert "PANEL.tab = PANEL.want = tileReturnTab || 'view';" in restore[:300] - assert "openTab(" not in restore[:300] - close_tiles = _MAP_JS[_MAP_JS.index("function closeTileViews("):] - assert "restoreTabAfterDetails()" in close_tiles[:250] - # Second clic sur la carte pendant que la fiche est ouverte : referme la - # fiche plutôt que de bouger le contour derrière elle (M5) - popup_fn = _MAP_JS[_MAP_JS.index("function showTilePopup("):] - assert "restoreTabAfterDetails()" in popup_fn[:400] + keydown = _MAP_JS[_MAP_JS.index("document.addEventListener('keydown'"):] + esc = keydown[keydown.rindex("if (e.key === 'Escape')"):] + assert "genSetDrawing(false)" in esc[:200] + assert "clearTileSelection()" in esc[:400] + assert "setSheet('closed')" in esc[:600] and "setCollapsed(true)" in esc[:600] def test_ui_saved_print_tab_does_not_rezoom_shared_link(): @@ -594,36 +589,20 @@ def test_ui_saved_print_tab_does_not_rezoom_shared_link(): après une ouverture explicite ultérieure de l'onglet).""" from lidar_pipeline.mapui import _MAP_JS assert "let BOOTSTRAPPING = true;" in _MAP_JS - assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB);" in _MAP_JS + assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);" in _MAP_JS then_branch = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):] assert then_branch.startswith("panelRender();\n BOOTSTRAPPING = false;") catch_branch = _MAP_JS[_MAP_JS.index(".catch(() => {\n toast('Serveur de tuiles injoignable."):] assert "BOOTSTRAPPING = false;" in catch_branch[:400] -def test_ui_details_view_hides_print_frame(): - """Ouvrir la fiche détaillée pendant que l'onglet Export est affiché - referme le cadre d'export (sinon cadre + poignée restent visibles - derrière la fiche) — et le reste tant que la fiche est ouverte, quel que - soit ce qui redéclenche un rendu du panneau (grip, redimensionnement, - changement phoneQuery…) puisque panelVisibleTab() renvoie null tant que - #tileView n'est pas masqué.""" +def test_ui_tile_selection_does_not_affect_print_frame(): + """La sélection d'une dalle vit à part des onglets : elle ne referme ni + ne rouvre le cadre d'export (contrairement à l'ancienne fiche « Détails », + qui masquait les autres onglets par-dessus la carte).""" from lidar_pipeline.mapui import _MAP_JS - open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):] - assert "printSetOpen(false);" in open_details[:600] - visible_tab = _MAP_JS[_MAP_JS.index("function panelVisibleTab("):] - assert "if (!el('tileView').hidden) return null;" in visible_tab[:300] - - -def test_ui_restoring_tab_does_not_refit_or_recentre_print_frame(): - """Rétablir l'onglet Export (retour de la fiche détaillée) rouvre le - cadre là où il était, sans recadrer la vue ni le recentrer sur l'écran — - au contraire d'une ouverture explicite de l'onglet par l'utilisateur.""" - from lidar_pipeline.mapui import _MAP_JS - assert "let RESTORING_TAB = false;" in _MAP_JS - restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):] - assert "RESTORING_TAB = true;" in restore[:400] - assert "panelRender();\n RESTORING_TAB = false;" in restore[:400] + select_fn = _MAP_JS[_MAP_JS.index("function selectTile("):] + assert "printSetOpen" not in select_fn[:select_fn.index("function clearTileSelection(")] open_fn = _MAP_JS[_MAP_JS.index("function printSetOpen("):] assert "explicit && !map.getBounds().contains(PRINT.center)" in open_fn[:700] assert "printRefreshFrame(!!explicit);" in open_fn[:700] @@ -1658,9 +1637,9 @@ def test_ui_tokens_themes_and_components(): def test_ui_single_tabbed_panel(): from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS for id_ in ("panel", "panelTabs", "panelBody", "panelCollapse", "sheetGrip", - "tab-view", "tab-print", "tab-gen", "tab-share"): + "tab-view", "tab-tile", "tab-print", "tab-gen", "tab-share"): assert f'id="{id_}"' in _MAP_HTML, id_ - for tab in ("view", "print", "gen", "share"): + for tab in ("view", "tile", "print", "gen", "share"): assert f'data-tab="{tab}"' in _MAP_HTML, tab # Plus d'éléments flottants : tout passe dans le panneau for gone in ('id="layercard"', 'id="printcard"', 'id="gencard"', 'id="usecard"', @@ -1675,7 +1654,7 @@ def test_ui_single_tabbed_panel(): "function setCollapsed(", "function panelVisibleTab("): assert fn in _MAP_JS, fn assert "'panel:tab'" in _MAP_JS - # Génération masquée : onglet retiré de la liste (touches 1–4 comprises) + # Génération masquée : onglet retiré de la liste (touches 1–5 comprises) assert "GEN.available && GEN.allowed" in _MAP_JS.split("function panelTabs(")[1].split("}")[0] assert "@media (max-width: 720px)" in _MAP_CSS and "#panel" in _MAP_CSS for state in ('data-sheet="closed"', 'data-sheet="half"', 'data-sheet="full"'): diff --git a/lidar_pipeline/web/map.css b/lidar_pipeline/web/map.css index 9885689..7cc4307 100644 --- a/lidar_pipeline/web/map.css +++ b/lidar_pipeline/web/map.css @@ -85,13 +85,6 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(- .rose-cap { font-size: var(--fs-xs); color: var(--text); line-height: 1.3; } .rose-cap span { color: var(--muted); font-size: var(--fs-xs); } .tile-head .sec-title { margin: 0; flex: 1; } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface); color: var(--text); - box-shadow: var(--shadow-lg); } -.leaflet-popup-content-wrapper { border-radius: var(--r-md); border: 1px solid var(--border); } -.leaflet-popup-content { margin: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); line-height: 1.45; } -.leaflet-container a.leaflet-popup-close-button { color: var(--muted); } -.tile-pop b { font-size: var(--fs-md); } -.tile-pop .row { margin-top: var(--sp-2); } /* --- composants communs --------------------------------------------------- */ .btn, button { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1); diff --git a/lidar_pipeline/web/map.html b/lidar_pipeline/web/map.html index 882c096..e2c60cf 100644 --- a/lidar_pipeline/web/map.html +++ b/lidar_pipeline/web/map.html @@ -17,9 +17,10 @@
@@ -60,6 +61,13 @@
Pente orientée vers…
clair = bosse · sombre = creux
+