From b24ebcd83a3053af00b63446224098664ff7efa6 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 18:00:59 +0200 Subject: [PATCH] =?UTF-8?q?Regrouper=20les=20r=C3=A9glages=20de=20la=20car?= =?UTF-8?q?te=20dans=20un=20panneau=20unique=20=C3=A0=20onglets,=20volet?= =?UTF-8?q?=20en=20bas=20sur=20t=C3=A9l=C3=A9phone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lidar_pipeline/tests/test_mapserve.py | 46 +++++- lidar_pipeline/web/map.css | 94 ++++++++---- lidar_pipeline/web/map.html | 207 ++++++++++++-------------- lidar_pipeline/web/map.js | 203 ++++++++++++++++++++----- 4 files changed, 362 insertions(+), 188 deletions(-) diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index d62a459..c087208 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -850,7 +850,7 @@ def test_ui_offers_generation_controls(): """L'interface embarque les contrôles de génération (+ Zone, options, run).""" from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS assert 'id="btnZone"' in _MAP_HTML and 'id="btnComplete"' in _MAP_HTML - assert 'id="gencard"' in _MAP_HTML and 'id="genGo"' in _MAP_HTML + assert 'id="tab-gen"' in _MAP_HTML and 'id="genGo"' in _MAP_HTML assert 'id="genStop"' in _MAP_HTML and 'id="genQueueClear"' in _MAP_HTML assert 'id="genViz"' in _MAP_HTML # Classification et raccord des bords imposés : plus de réglage @@ -937,7 +937,7 @@ def test_ign_metadata_cached_and_resilient(tmp_path, monkeypatch): def test_ui_tile_card_and_wind_rose(): from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS - assert 'id="rose"' in _MAP_HTML and 'id="roseCanvas"' in _MAP_HTML + assert 'id="legendBlock"' in _MAP_HTML and 'id="roseCanvas"' in _MAP_HTML assert "api/map/ign?col=" in _MAP_JS and "Télécharger le nuage LiDAR" in _MAP_JS assert "compass + 90" in _MAP_JS # même repère que le rendu (aspect = boussole + 90°) @@ -1325,8 +1325,8 @@ def test_export_pdf_never_delegated(tmp_path, monkeypatch): def test_map_ui_has_pdf_export(): from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS - for id_ in ("btnPrint", "printcard", "printPaper", "printOrient", "printScale", - "printTitle", "printGo", "printMsg", "printClose", "printInfo"): + for id_ in ("tab-print", "printPaper", "printOrient", "printScale", + "printTitle", "printGo", "printMsg", "printInfo"): assert f'id="{id_}"' in _MAP_HTML, id_ for scale in ("1000", "2000", "5000", "10000"): assert f'value="{scale}"' in _MAP_HTML @@ -1338,9 +1338,14 @@ def test_map_ui_has_pdf_export(): assert "PRINT.center" in _MAP_JS for id_ in ("printHere", "printFit"): assert f'id="{id_}"' in _MAP_HTML, id_ - assert "#printcard" in _MAP_CSS - # bouton toujours visible : indépendant de /api/status.available - assert 'id="btnPrint"' in _MAP_HTML and 'id="btnPrint" hidden' not in _MAP_HTML + # onglet toujours proposé (indépendant de /api/status.available) ; plus de + # bouton ⎙ ni de carte hors du panneau + assert 'data-tab="print"' in _MAP_HTML + for gone in ('id="btnPrint"', 'id="printcard"', 'id="printClose"'): + assert gone not in _MAP_HTML, gone + tools = _MAP_HTML.split('")[0] + assert "⎙" not in tools + assert "e.detail.visible === 'print'" in _MAP_JS def test_ui_tokens_themes_and_components(): @@ -1371,3 +1376,30 @@ def test_ui_tokens_themes_and_components(): rules = rules[rules.index("}") + 1:] for hard in ("#0b0e13", "#10141b", "#f4a261", "#4cc9f0", "#e76f51", "rgba(90, 160, 255"): assert hard not in rules, hard + + +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"): + assert f'id="{id_}"' in _MAP_HTML, id_ + for tab in ("view", "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"', + 'id="gentools"', 'id="btnPrint"', 'id="rose"', 'id="btnUse"'): + assert gone not in _MAP_HTML, gone + # Réglages et boutons déplacés, identifiants conservés + for id_ in ("printPaper", "printGo", "genGo", "btnZone", "btnComplete", "btnShare", + "btnDefault", "btnReset", "useBody", "modeSeg", "roseCanvas", "baseOpacity"): + assert f'id="{id_}"' in _MAP_HTML, id_ + assert "const PANEL_LS = 'lidar-panel'" in _MAP_JS + for fn in ("function panelTabs(", "function openTab(", "function setSheet(", + "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) + 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"'): + assert state.split('"')[1] in _MAP_CSS, state diff --git a/lidar_pipeline/web/map.css b/lidar_pipeline/web/map.css index 1becb97..f64be18 100644 --- a/lidar_pipeline/web/map.css +++ b/lidar_pipeline/web/map.css @@ -62,8 +62,8 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(- border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-lg); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } -#layercard { left: 12px; bottom: 12px; width: 320px; max-width: calc(100vw - 24px); } -#tilecard { left: 12px; top: 12px; width: 340px; max-width: calc(100vw - 24px); +/* Fiche de dalle : à droite du panneau (ordinateur), en haut (téléphone). */ +#tilecard { left: max(var(--sp-3), var(--panel-w)); top: 12px; width: 340px; max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); overflow-y: auto; } .info-sec { margin-top: 10px; } .info-sec h4 { margin: 0 0 4px; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em; @@ -74,18 +74,11 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(- .info-dl { display: inline-block; margin-top: 6px; padding: 6px 10px; border-radius: var(--r-sm); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); text-decoration: none; font-weight: 600; } .info-dl:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); } -#rose { right: 12px; bottom: 190px; padding: 8px 8px 6px; display: flex; flex-direction: column; - align-items: center; gap: 2px; z-index: var(--z-map-ui); } -#rose canvas { width: 132px; height: 132px; } -.rose-cap { font-size: var(--fs-xs); text-align: center; color: var(--text); line-height: 1.3; } +.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); } -#usecard { left: 50%; top: 12px; transform: translateX(-50%); width: min(560px, calc(100vw - 24px)); z-index: var(--z-popup); } .card-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); } .card-title { font-size: var(--fs-sm); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); } -.card-stat { margin-left: auto; font-size: var(--fs-xs); color: var(--muted); } .card-body { padding: 6px 8px 8px; max-height: min(60vh, 520px); overflow-y: auto; } -#layercard.collapsed .card-body { display: none; } -#layercard.collapsed #cardToggle { transform: rotate(-90deg); } .card-foot { display: flex; gap: 8px; padding: 8px 4px 2px; margin-top: 6px; border-top: 1px solid var(--border); } /* --- composants communs --------------------------------------------------- */ @@ -96,6 +89,7 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(- .btn:disabled, button:disabled { opacity: 0.5; cursor: default; } .btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; } .btn-primary:hover { filter: brightness(1.08); } +.btn.accent, .btn.busy { border-color: var(--accent); color: var(--accent); } .btn-ghost { background: transparent; } .btn-icon { min-height: 0; padding: var(--sp-1) var(--sp-2); border: none; background: none; color: var(--muted); font-size: var(--fs-lg); } @@ -150,14 +144,23 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius .eye.on { color: var(--accent); } .eye svg { width: 16px; height: 16px; } .layer-name { font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.base-row .layer-name { color: var(--text); cursor: pointer; } +.base-row:not(.on) .layer-name { color: var(--muted); } .pct { font-size: var(--fs-xs); color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; } -#tools { position: fixed; right: 12px; bottom: 12px; z-index: var(--z-panel); display: flex; flex-direction: column; gap: 6px; } +#tools { position: fixed; right: var(--sp-3); bottom: calc(var(--sheet-h) + var(--sp-3)); + z-index: var(--z-map-ui); display: flex; flex-direction: column; gap: var(--sp-2); } +@media (max-width: 720px) { + #tools { top: var(--sp-3); bottom: auto; } +} +/* L'attribution Leaflet reste en bas à droite : les outils passent au-dessus. */ +@media (min-width: 721px) { #tools { bottom: 44px; } } #tools button { width: 38px; height: 38px; padding: 0; font-size: var(--fs-lg); background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-sm); } /* Indicateur de chargement : tuiles restantes pour finir la vue courante (fond + LiDAR), barre de progression du lot en cours. */ -.tileload { position: fixed; left: 50%; top: 12px; transform: translateX(-50%); z-index: var(--z-loading); +.tileload { position: fixed; left: calc(var(--panel-w) + (100vw - var(--panel-w)) / 2); top: auto; + bottom: calc(var(--sheet-h) + var(--sp-3)); transform: translateX(-50%); z-index: var(--z-loading); display: flex; align-items: center; gap: 8px; padding: 5px 12px; border-radius: var(--r-full); background: var(--surface); border: 1px solid var(--border); font-size: var(--fs-sm); box-shadow: var(--shadow-sm); pointer-events: none; @@ -172,7 +175,8 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius overflow: hidden; flex: none; } .tl-bar span { display: block; height: 100%; width: 0; background: var(--accent); transition: width 0.2s ease; } -.toast { position: fixed; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: var(--z-toast); +.toast { position: fixed; left: calc(var(--panel-w) + (100vw - var(--panel-w)) / 2); + bottom: calc(var(--sheet-h) + 56px); transform: translateX(-50%); z-index: var(--z-toast); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 7px 12px; font-size: var(--fs-sm); color: var(--text); max-width: 80vw; text-align: center; } @@ -186,23 +190,12 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius .leaflet-control-attribution a { color: var(--accent) !important; } /* --- génération de tuiles ------------------------------------------------ */ -#gencard { right: 12px; top: 56px; width: 340px; max-width: calc(100vw - 24px); } -#printcard { right: 60px; bottom: 12px; width: 340px; max-width: calc(100vw - 84px); } .print-msg { font-size: var(--fs-sm); margin-left: 8px; } .print-handle { display: flex; align-items: center; justify-content: center; cursor: move; font-size: var(--fs-lg); color: #1b1b1b; background: var(--print); border: 2px solid #1b1b1b; border-radius: var(--r-full); box-shadow: 0 1px 6px rgba(0,0,0,0.5); } body.drawing { cursor: crosshair !important; } body.drawing .leaflet-container { cursor: crosshair !important; } -/* Barre de génération : en HAUT à droite de l'écran, boutons accentués — - c'est l'entrée principale du traitement, elle ne se confond pas avec les - icônes de navigation (zoom/GPS) du coin bas droit. */ -#gentools { position: fixed; top: 12px; right: 12px; z-index: var(--z-panel); display: flex; gap: 8px; } -#gentools button { background: var(--surface); color: var(--accent); border: 1px solid var(--accent); - border-radius: var(--r-md); padding: 8px 14px; font: inherit; font-size: var(--fs-md); font-weight: 600; - box-shadow: var(--shadow-sm); } -#gentools button:hover { background: var(--accent); color: var(--on-accent); } -#gentools button.accent { background: var(--accent); color: var(--on-accent); } .gen-row { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 6px; align-items: center; margin: 4px 0; font-size: var(--fs-xs); color: var(--muted); } .chk { display: flex; gap: 7px; align-items: center; font-size: var(--fs-xs); @@ -221,9 +214,54 @@ body.drawing .leaflet-container { cursor: crosshair !important; } .gen-frame-pending { stroke: var(--gen-pending); fill: color-mix(in srgb, var(--gen-pending) 8%, transparent); stroke-dasharray: 6 4; } .gen-frame-failed { stroke: var(--gen-failed); fill: color-mix(in srgb, var(--gen-failed) 20%, transparent); } .gen-sel { stroke: var(--accent); fill: color-mix(in srgb, var(--accent) 12%, transparent); cursor: pointer; } + +/* --- panneau unique à onglets ------------------------------------------------ + Ordinateur : colonne à gauche, repliable en bande d'icônes. + Téléphone : volet en bas à trois hauteurs (fermé / mi-hauteur / plein). */ +#panel { position: fixed; z-index: var(--z-panel); display: flex; flex-direction: column; + background: var(--surface); color: var(--text); border: 1px solid var(--border); + box-shadow: var(--shadow-lg); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + left: var(--sp-3); top: var(--sp-3); bottom: var(--sp-3); width: 340px; border-radius: var(--r-md); } +.panel-head { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2); + border-bottom: 1px solid var(--border); } +.panel-tabs { display: flex; gap: var(--sp-1); flex: 1; min-width: 0; } +.panel-tabs button { flex: 1; min-width: 0; flex-direction: column; gap: 0; min-height: 44px; + padding: var(--sp-1); border: none; background: transparent; color: var(--muted); } +.panel-tabs button[aria-selected="true"] { background: var(--surface-2); color: var(--accent); } +.tab-ico { font-size: var(--fs-lg); line-height: 1.1; } +.tab-lbl { font-size: var(--fs-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } +.panel-body { flex: 1; overflow-y: auto; padding: var(--sp-3); overscroll-behavior: contain; } +.panel-body section > * + * { margin-top: var(--sp-2); } +.panel-stat { font-size: var(--fs-sm); color: var(--muted); } +.sec-title { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; + color: var(--muted); margin-top: var(--sp-4); } +.legend { display: flex; align-items: center; gap: var(--sp-3); } +.legend canvas { width: 96px; height: 96px; flex: none; } +.sheet-grip { display: none; } +#panel.collapsed { width: 60px; } +#panel.collapsed .panel-body, #panel.collapsed .tab-lbl { display: none; } +#panel.collapsed .panel-head { flex-direction: column; border-bottom: none; } +#panel.collapsed .panel-tabs { flex-direction: column; align-self: stretch; } +#panel.collapsed #panelCollapse { transform: rotate(180deg); } +/* La carte, l'échelle et les messages se décalent de la largeur du panneau. */ +body { --panel-w: 364px; --sheet-h: 0px; } +body.panel-collapsed { --panel-w: 84px; } +.leaflet-bottom.leaflet-left { left: var(--panel-w); } @media (max-width: 720px) { - #layercard { width: calc(100vw - 24px); } - #tilecard { width: calc(100vw - 24px); } - #gencard { width: calc(100vw - 24px); } - #printcard { width: calc(100vw - 84px); } + body, body.panel-collapsed { --panel-w: 0px; } + #panel, #panel.collapsed { left: 0; right: 0; top: auto; bottom: 0; width: auto; + border-radius: var(--r-md) var(--r-md) 0 0; border-bottom: none; transition: height 0.2s ease; } + #panel[data-sheet="closed"] { height: 72px; } + #panel[data-sheet="half"] { height: 45vh; } + #panel[data-sheet="full"] { height: calc(100vh - var(--sp-4)); } + #panel[data-sheet="closed"] .panel-body { display: none; } + #panelCollapse { display: none; } + .sheet-grip { display: flex; justify-content: center; padding: var(--sp-2) 0 0; cursor: grab; touch-action: none; } + .sheet-grip span { width: 36px; height: 4px; border-radius: var(--r-full); background: var(--muted); } + .panel-head { padding: var(--sp-1) var(--sp-2); } + .leaflet-bottom { bottom: var(--sheet-h); } + /* Attribution sur toute la largeur : l'échelle passe au-dessus d'elle, + l'indicateur de chargement en haut de l'écran (à gauche des outils). */ + .leaflet-bottom.leaflet-left .leaflet-control-scale { margin-bottom: 28px; } + .tileload { top: var(--sp-3); bottom: auto; } } diff --git a/lidar_pipeline/web/map.html b/lidar_pipeline/web/map.html index c366fc8..2dceb79 100644 --- a/lidar_pipeline/web/map.html +++ b/lidar_pipeline/web/map.html @@ -11,53 +11,106 @@
-
-
- - Couches - +