Regrouper les réglages de la carte dans un panneau unique à onglets, volet en bas sur téléphone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -850,7 +850,7 @@ def test_ui_offers_generation_controls():
|
|||||||
"""L'interface embarque les contrôles de génération (+ Zone, options, run)."""
|
"""L'interface embarque les contrôles de génération (+ Zone, options, run)."""
|
||||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
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="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="genStop"' in _MAP_HTML and 'id="genQueueClear"' in _MAP_HTML
|
||||||
assert 'id="genViz"' in _MAP_HTML
|
assert 'id="genViz"' in _MAP_HTML
|
||||||
# Classification et raccord des bords imposés : plus de réglage
|
# 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():
|
def test_ui_tile_card_and_wind_rose():
|
||||||
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
|
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 "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°)
|
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():
|
def test_map_ui_has_pdf_export():
|
||||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||||
for id_ in ("btnPrint", "printcard", "printPaper", "printOrient", "printScale",
|
for id_ in ("tab-print", "printPaper", "printOrient", "printScale",
|
||||||
"printTitle", "printGo", "printMsg", "printClose", "printInfo"):
|
"printTitle", "printGo", "printMsg", "printInfo"):
|
||||||
assert f'id="{id_}"' in _MAP_HTML, id_
|
assert f'id="{id_}"' in _MAP_HTML, id_
|
||||||
for scale in ("1000", "2000", "5000", "10000"):
|
for scale in ("1000", "2000", "5000", "10000"):
|
||||||
assert f'value="{scale}"' in _MAP_HTML
|
assert f'value="{scale}"' in _MAP_HTML
|
||||||
@ -1338,9 +1338,14 @@ def test_map_ui_has_pdf_export():
|
|||||||
assert "PRINT.center" in _MAP_JS
|
assert "PRINT.center" in _MAP_JS
|
||||||
for id_ in ("printHere", "printFit"):
|
for id_ in ("printHere", "printFit"):
|
||||||
assert f'id="{id_}"' in _MAP_HTML, id_
|
assert f'id="{id_}"' in _MAP_HTML, id_
|
||||||
assert "#printcard" in _MAP_CSS
|
# onglet toujours proposé (indépendant de /api/status.available) ; plus de
|
||||||
# bouton toujours visible : indépendant de /api/status.available
|
# bouton ⎙ ni de carte hors du panneau
|
||||||
assert 'id="btnPrint"' in _MAP_HTML and 'id="btnPrint" hidden' not in _MAP_HTML
|
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('<nav id="tools">')[1].split("</nav>")[0]
|
||||||
|
assert "⎙" not in tools
|
||||||
|
assert "e.detail.visible === 'print'" in _MAP_JS
|
||||||
|
|
||||||
|
|
||||||
def test_ui_tokens_themes_and_components():
|
def test_ui_tokens_themes_and_components():
|
||||||
@ -1371,3 +1376,30 @@ def test_ui_tokens_themes_and_components():
|
|||||||
rules = rules[rules.index("}") + 1:]
|
rules = rules[rules.index("}") + 1:]
|
||||||
for hard in ("#0b0e13", "#10141b", "#f4a261", "#4cc9f0", "#e76f51", "rgba(90, 160, 255"):
|
for hard in ("#0b0e13", "#10141b", "#f4a261", "#4cc9f0", "#e76f51", "rgba(90, 160, 255"):
|
||||||
assert hard not in rules, hard
|
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
|
||||||
|
|||||||
@ -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);
|
border: 1px solid var(--border); border-radius: var(--r-md);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
|
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
|
||||||
#layercard { left: 12px; bottom: 12px; width: 320px; max-width: calc(100vw - 24px); }
|
/* Fiche de dalle : à droite du panneau (ordinateur), en haut (téléphone). */
|
||||||
#tilecard { left: 12px; top: 12px; width: 340px; max-width: calc(100vw - 24px);
|
#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; }
|
max-height: calc(100vh - 24px); overflow-y: auto; }
|
||||||
.info-sec { margin-top: 10px; }
|
.info-sec { margin-top: 10px; }
|
||||||
.info-sec h4 { margin: 0 0 4px; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em;
|
.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);
|
.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; }
|
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); }
|
.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;
|
.rose-cap { font-size: var(--fs-xs); color: var(--text); line-height: 1.3; }
|
||||||
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 span { color: var(--muted); font-size: var(--fs-xs); }
|
.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-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-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; }
|
.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); }
|
.card-foot { display: flex; gap: 8px; padding: 8px 4px 2px; margin-top: 6px; border-top: 1px solid var(--border); }
|
||||||
|
|
||||||
/* --- composants communs --------------------------------------------------- */
|
/* --- 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: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 { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
|
||||||
.btn-primary:hover { filter: brightness(1.08); }
|
.btn-primary:hover { filter: brightness(1.08); }
|
||||||
|
.btn.accent, .btn.busy { border-color: var(--accent); color: var(--accent); }
|
||||||
.btn-ghost { background: transparent; }
|
.btn-ghost { background: transparent; }
|
||||||
.btn-icon { min-height: 0; padding: var(--sp-1) var(--sp-2); border: none; background: none;
|
.btn-icon { min-height: 0; padding: var(--sp-1) var(--sp-2); border: none; background: none;
|
||||||
color: var(--muted); font-size: var(--fs-lg); }
|
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.on { color: var(--accent); }
|
||||||
.eye svg { width: 16px; height: 16px; }
|
.eye svg { width: 16px; height: 16px; }
|
||||||
.layer-name { font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.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; }
|
.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);
|
#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); }
|
border: 1px solid var(--border); box-shadow: var(--shadow-sm); }
|
||||||
/* Indicateur de chargement : tuiles restantes pour finir la vue courante
|
/* Indicateur de chargement : tuiles restantes pour finir la vue courante
|
||||||
(fond + LiDAR), barre de progression du lot en cours. */
|
(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);
|
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);
|
background: var(--surface); border: 1px solid var(--border); font-size: var(--fs-sm);
|
||||||
box-shadow: var(--shadow-sm); pointer-events: none;
|
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; }
|
overflow: hidden; flex: none; }
|
||||||
.tl-bar span { display: block; height: 100%; width: 0; background: var(--accent);
|
.tl-bar span { display: block; height: 100%; width: 0; background: var(--accent);
|
||||||
transition: width 0.2s ease; }
|
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);
|
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; }
|
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; }
|
.leaflet-control-attribution a { color: var(--accent) !important; }
|
||||||
|
|
||||||
/* --- génération de tuiles ------------------------------------------------ */
|
/* --- 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-msg { font-size: var(--fs-sm); margin-left: 8px; }
|
||||||
.print-handle { display: flex; align-items: center; justify-content: center; cursor: move;
|
.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;
|
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); }
|
border-radius: var(--r-full); box-shadow: 0 1px 6px rgba(0,0,0,0.5); }
|
||||||
body.drawing { cursor: crosshair !important; }
|
body.drawing { cursor: crosshair !important; }
|
||||||
body.drawing .leaflet-container { 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;
|
.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); }
|
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);
|
.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-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-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; }
|
.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) {
|
@media (max-width: 720px) {
|
||||||
#layercard { width: calc(100vw - 24px); }
|
body, body.panel-collapsed { --panel-w: 0px; }
|
||||||
#tilecard { width: calc(100vw - 24px); }
|
#panel, #panel.collapsed { left: 0; right: 0; top: auto; bottom: 0; width: auto;
|
||||||
#gencard { width: calc(100vw - 24px); }
|
border-radius: var(--r-md) var(--r-md) 0 0; border-bottom: none; transition: height 0.2s ease; }
|
||||||
#printcard { width: calc(100vw - 84px); }
|
#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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,53 +11,106 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
||||||
<div id="layercard" class="card">
|
<aside id="panel" data-sheet="half" aria-label="Réglages de la carte">
|
||||||
<div class="card-head">
|
<div id="sheetGrip" class="sheet-grip" title="Glisser pour agrandir ou réduire"><span></span></div>
|
||||||
<button id="cardToggle" class="btn-icon" title="Replier / déplier">▾</button>
|
<div class="panel-head">
|
||||||
<span class="card-title">Couches</span>
|
<nav id="panelTabs" class="panel-tabs" role="tablist">
|
||||||
<span id="stat" class="card-stat"></span>
|
<button role="tab" data-tab="view" title="Affichage (1)"><span class="tab-ico">◧</span><span class="tab-lbl">Affichage</span></button>
|
||||||
|
<button role="tab" data-tab="print" title="Export PDF (2)"><span class="tab-ico">⎙</span><span class="tab-lbl">Export PDF</span></button>
|
||||||
|
<button role="tab" data-tab="gen" title="Génération (3)" hidden><span class="tab-ico">+</span><span class="tab-lbl">Génération</span></button>
|
||||||
|
<button role="tab" data-tab="share" title="Partager (4)"><span class="tab-ico">⎘</span><span class="tab-lbl">Partager</span></button>
|
||||||
|
</nav>
|
||||||
|
<button id="panelCollapse" class="btn-icon" title="Replier le panneau">‹</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div id="panelBody" class="panel-body">
|
||||||
<div class="base-row" id="baseRow">
|
<section id="tab-view" role="tabpanel" hidden>
|
||||||
<button class="eye on" id="baseToggle" title="Afficher / masquer le fond de carte"></button>
|
<div class="base-row" id="baseRow">
|
||||||
<span class="layer-name" id="baseName" title="Cliquer : fond clair / assombri">Fond OpenStreetMap</span>
|
<button class="eye on" id="baseToggle" title="Afficher / masquer le fond de carte"></button>
|
||||||
<span class="pct" id="basePct"></span>
|
<span class="layer-name" id="baseName" title="Cliquer : fond clair / assombri">Fond OpenStreetMap</span>
|
||||||
<div class="base-ctl">
|
<span class="pct" id="basePct"></span>
|
||||||
<input type="range" id="baseOpacity" min="0" max="100" step="1" title="Opacité du fond">
|
<div class="base-ctl">
|
||||||
|
<input type="range" id="baseOpacity" min="0" max="100" step="1" title="Opacité du fond">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="view-row">
|
||||||
<div class="view-row">
|
<span class="view-label">Affichage</span>
|
||||||
<span class="view-label">Affichage</span>
|
<span class="layer-name" id="mainName"></span>
|
||||||
<span class="layer-name" id="mainName"></span>
|
<select id="mainSel" class="field" title="Couche d'affichage principal" hidden></select>
|
||||||
<select id="mainSel" class="field" title="Couche d'affichage principal" hidden></select>
|
|
||||||
</div>
|
|
||||||
<div id="precBlock" hidden>
|
|
||||||
<div class="seg" id="modeSeg" role="group" aria-label="Relief, précision ou les deux (touche P)">
|
|
||||||
<button type="button" data-mode="relief" title="Relief seul (touche P : mode suivant)">Relief</button>
|
|
||||||
<button type="button" data-mode="precision" title="Précision géométrique seule : densité de points sol">Précision</button>
|
|
||||||
<button type="button" data-mode="both" title="Précision en produit sur le relief : les zones peu fiables s'assombrissent">Les deux</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="prec-ctl" id="precCtl" hidden>
|
<div id="precBlock" hidden>
|
||||||
<span>Opacité de la précision</span>
|
<div class="seg" id="modeSeg" role="group" aria-label="Relief, précision ou les deux (touche P)">
|
||||||
<input type="range" id="precOpacity" min="0" max="100" step="1" title="Opacité de la précision sur le relief">
|
<button type="button" data-mode="relief" title="Relief seul (touche P : mode suivant)">Relief</button>
|
||||||
<span class="pct" id="precPct"></span>
|
<button type="button" data-mode="precision" title="Précision géométrique seule : densité de points sol">Précision</button>
|
||||||
|
<button type="button" data-mode="both" title="Précision en produit sur le relief : les zones peu fiables s'assombrissent">Les deux</button>
|
||||||
|
</div>
|
||||||
|
<div class="prec-ctl" id="precCtl" hidden>
|
||||||
|
<span>Opacité de la précision</span>
|
||||||
|
<input type="range" id="precOpacity" min="0" max="100" step="1" title="Opacité de la précision sur le relief">
|
||||||
|
<span class="pct" id="precPct"></span>
|
||||||
|
</div>
|
||||||
|
<div class="prec-legend" id="precLegend" hidden>
|
||||||
|
<div class="prec-bar" id="precBar"></div>
|
||||||
|
<div class="prec-ticks"><span>≤ 0,35</span><span>4</span><span>≥ 45 pts/m²</span></div>
|
||||||
|
<div class="prec-cap">Points sol par m² : sombre = relief interpolé, noir = aucun point</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="prec-legend" id="precLegend" hidden>
|
<div class="legend" id="legendBlock" title="Relief orienté : la teinte donne l'orientation de la pente, la clarté le relief local">
|
||||||
<div class="prec-bar" id="precBar"></div>
|
<canvas id="roseCanvas" width="96" height="96"></canvas>
|
||||||
<div class="prec-ticks"><span>≤ 0,35</span><span>4</span><span>≥ 45 pts/m²</span></div>
|
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
||||||
<div class="prec-cap">Points sol par m² : sombre = relief interpolé, noir = aucun point</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class="card-foot">
|
<section id="tab-print" role="tabpanel" hidden>
|
||||||
<button id="btnShare" class="btn" title="Copier un lien vers cette vue">⎘ Partager la vue</button>
|
<div class="panel-stat" id="printInfo"></div>
|
||||||
<button id="btnUse" class="btn" title="Utiliser ces rendus comme fond d'imagerie (JOSM, QGIS, iD…)">⤓ Utiliser dans JOSM / QGIS</button>
|
<div class="row"><select id="printPaper" class="field" title="Format papier">
|
||||||
</div>
|
<option value="A4" selected>A4</option><option value="A3">A3</option></select>
|
||||||
<div class="card-foot">
|
<select id="printOrient" class="field" title="Orientation">
|
||||||
<button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, opacité de la précision, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button>
|
<option value="paysage" selected>Paysage</option><option value="portrait">Portrait</option></select>
|
||||||
<button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button>
|
<select id="printScale" class="field" title="Échelle d'impression">
|
||||||
</div>
|
<option value="1000">1:1 000</option><option value="2000" selected>1:2 000</option>
|
||||||
|
<option value="5000">1:5 000</option><option value="10000">1:10 000</option></select></div>
|
||||||
|
<div class="row"><input id="printTitle" class="field" type="text" maxlength="120"
|
||||||
|
placeholder="Titre (optionnel)" title="Titre imprimé dans le cartouche"></div>
|
||||||
|
<p class="hint">Faites glisser la poignée ✥ pour placer le cadre jaune sur la zone à imprimer.</p>
|
||||||
|
<div class="row"><button id="printHere" class="btn"
|
||||||
|
title="Placer le cadre au centre de la vue">⌖ Centrer ici</button>
|
||||||
|
<button id="printFit" class="btn" title="Zoomer sur le cadre">⤢ Voir le cadre</button></div>
|
||||||
|
<div class="card-foot"><button id="printGo" class="btn btn-primary">Exporter le PDF</button>
|
||||||
|
<span id="printMsg" class="print-msg"></span></div>
|
||||||
|
</section>
|
||||||
|
<section id="tab-gen" role="tabpanel" hidden>
|
||||||
|
<div class="row"><button id="btnZone" class="btn" title="Générer une nouvelle zone : dessiner un rectangle, téléchargement IGN + traitement">+ Zone</button>
|
||||||
|
<button id="btnComplete" class="btn" title="Compléter : traiter toutes les dalles déjà téléchargées qui manquent des couches choisies">⤒ Compléter</button></div>
|
||||||
|
<div class="panel-stat" id="genBadge"></div>
|
||||||
|
<div id="genMsg" class="meta">Dessinez une zone (+) ou complétez les dalles existantes (⤒).</div>
|
||||||
|
<div id="genOpts">
|
||||||
|
<div class="gen-row"><span class="gen-lbl">Couches</span>
|
||||||
|
<select id="genViz" class="field" multiple size="4" title="Laisser vide pour les couches du panneau"></select></div>
|
||||||
|
<label class="chk"><input type="checkbox" id="genRegen"> Régénérer les tuiles existantes</label>
|
||||||
|
<div class="meta">Sol : classification IGN · bords raccordés aux dalles voisines (bande 100 m)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-foot" id="genActions">
|
||||||
|
<button id="genGo" class="btn btn-primary" title="Télécharge (géoplateforme IGN) puis traite les dalles sélectionnées">Générer</button>
|
||||||
|
<button id="genClear" title="Vider la sélection de dalles">Effacer la sélection</button>
|
||||||
|
</div>
|
||||||
|
<div id="genRun" hidden>
|
||||||
|
<div class="meta" id="genProgress"></div>
|
||||||
|
<pre id="genLog" class="gen-log"></pre>
|
||||||
|
<div class="card-foot">
|
||||||
|
<button id="genStop" title="SIGTERM au pipeline (nettoyage propre), SIGKILL en repli">Arrêter</button>
|
||||||
|
<button id="genQueueClear" title="Retirer les demandes en attente (le run en cours continue)">Vider la file</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="tab-share" role="tabpanel" hidden>
|
||||||
|
<div class="row"><button id="btnShare" class="btn btn-primary" title="Copier un lien vers cette vue">⎘ Copier le lien de la vue</button></div>
|
||||||
|
<h4 class="sec-title">Fond d'imagerie XYZ (JOSM, QGIS, iD…)</h4>
|
||||||
|
<div id="useBody"></div>
|
||||||
|
<h4 class="sec-title">Configuration par défaut</h4>
|
||||||
|
<div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, opacité de la précision, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button>
|
||||||
|
<button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
|
|
||||||
<div id="tilecard" class="card" hidden>
|
<div id="tilecard" class="card" hidden>
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
@ -67,83 +120,11 @@
|
|||||||
<div class="card-body" id="tileBody"></div>
|
<div class="card-body" id="tileBody"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="rose" class="card" hidden title="Relief orienté : la teinte donne l'orientation de la pente, la clarté le relief local">
|
|
||||||
<canvas id="roseCanvas" width="132" height="132"></canvas>
|
|
||||||
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="usecard" class="card" hidden>
|
|
||||||
<div class="card-head">
|
|
||||||
<span class="card-title">Fond d'imagerie XYZ</span>
|
|
||||||
<button id="useClose" class="btn-icon" title="Fermer">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="useBody"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="printcard" class="card" hidden>
|
|
||||||
<div class="card-head">
|
|
||||||
<span class="card-title">Export PDF</span>
|
|
||||||
<span id="printInfo" class="card-stat"></span>
|
|
||||||
<button id="printClose" class="btn-icon" title="Fermer">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row"><select id="printPaper" class="field" title="Format papier">
|
|
||||||
<option value="A4" selected>A4</option><option value="A3">A3</option></select>
|
|
||||||
<select id="printOrient" class="field" title="Orientation">
|
|
||||||
<option value="paysage" selected>Paysage</option><option value="portrait">Portrait</option></select>
|
|
||||||
<select id="printScale" class="field" title="Échelle d'impression">
|
|
||||||
<option value="1000">1:1 000</option><option value="2000" selected>1:2 000</option>
|
|
||||||
<option value="5000">1:5 000</option><option value="10000">1:10 000</option></select></div>
|
|
||||||
<div class="row"><input id="printTitle" class="field" type="text" maxlength="120"
|
|
||||||
placeholder="Titre (optionnel)" title="Titre imprimé dans le cartouche"></div>
|
|
||||||
<p class="hint">Faites glisser la poignée ✥ pour placer le cadre jaune sur la zone à imprimer.</p>
|
|
||||||
<div class="row"><button id="printHere" class="btn"
|
|
||||||
title="Placer le cadre au centre de la vue">⌖ Centrer ici</button>
|
|
||||||
<button id="printFit" class="btn" title="Zoomer sur le cadre">⤢ Voir le cadre</button></div>
|
|
||||||
<div class="card-foot"><button id="printGo" class="btn btn-primary">Exporter le PDF</button>
|
|
||||||
<span id="printMsg" class="print-msg"></span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="gencard" class="card" hidden>
|
|
||||||
<div class="card-head">
|
|
||||||
<span class="card-title">Génération</span>
|
|
||||||
<span id="genBadge" class="card-stat"></span>
|
|
||||||
<button id="genClose" class="btn-icon" title="Fermer">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="genMsg" class="meta">Dessinez une zone (+) ou complétez les dalles existantes (⤒).</div>
|
|
||||||
<div id="genOpts">
|
|
||||||
<div class="gen-row"><span class="gen-lbl">Couches</span>
|
|
||||||
<select id="genViz" class="field" multiple size="4" title="Laisser vide pour les couches du panneau"></select></div>
|
|
||||||
<label class="chk"><input type="checkbox" id="genRegen"> Régénérer les tuiles existantes</label>
|
|
||||||
<div class="meta">Sol : classification IGN · bords raccordés aux dalles voisines (bande 100 m)</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-foot" id="genActions">
|
|
||||||
<button id="genGo" class="btn btn-primary" title="Télécharge (géoplateforme IGN) puis traite les dalles sélectionnées">Générer</button>
|
|
||||||
<button id="genClear" title="Vider la sélection de dalles">Effacer la sélection</button>
|
|
||||||
</div>
|
|
||||||
<div id="genRun" hidden>
|
|
||||||
<div class="meta" id="genProgress"></div>
|
|
||||||
<pre id="genLog" class="gen-log"></pre>
|
|
||||||
<div class="card-foot">
|
|
||||||
<button id="genStop" title="SIGTERM au pipeline (nettoyage propre), SIGKILL en repli">Arrêter</button>
|
|
||||||
<button id="genQueueClear" title="Retirer les demandes en attente (le run en cours continue)">Vider la file</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav id="gentools">
|
|
||||||
<button id="btnZone" title="Générer une nouvelle zone : dessiner un rectangle, téléchargement IGN + traitement" hidden>+ Zone</button>
|
|
||||||
<button id="btnComplete" title="Compléter : traiter toutes les dalles déjà téléchargées qui manquent des couches choisies" hidden>⤒ Compléter</button>
|
|
||||||
</nav>
|
|
||||||
<nav id="tools">
|
<nav id="tools">
|
||||||
<button id="zoomIn" title="Zoomer">+</button>
|
<button id="zoomIn" title="Zoomer">+</button>
|
||||||
<button id="zoomOut" title="Dézoomer">−</button>
|
<button id="zoomOut" title="Dézoomer">−</button>
|
||||||
<button id="zoomFit" title="Recadrer sur les données">⤢</button>
|
<button id="zoomFit" title="Recadrer sur les données">⤢</button>
|
||||||
<button id="gps" title="Centrer sur ma position">⌖</button>
|
<button id="gps" title="Centrer sur ma position">⌖</button>
|
||||||
<button id="btnPrint" title="Exporter la zone en PDF (impression terrain)">⎙</button>
|
|
||||||
<button id="btnTheme" class="btn-icon" title="Thème">◐</button>
|
<button id="btnTheme" class="btn-icon" title="Thème">◐</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="toast" class="toast" hidden></div>
|
<div id="toast" class="toast" hidden></div>
|
||||||
|
|||||||
@ -65,9 +65,18 @@ function applyTheme(pref) {
|
|||||||
const btn = el('btnTheme');
|
const btn = el('btnTheme');
|
||||||
if (btn) { btn.textContent = THEME_ICON[p]; btn.title = THEME_TITLE[p] + ' (cliquer pour changer)'; }
|
if (btn) { btn.textContent = THEME_ICON[p]; btn.title = THEME_TITLE[p] + ' (cliquer pour changer)'; }
|
||||||
if (typeof applyLayers === 'function' && STATE) applyLayers(); // fond OSM assombri
|
if (typeof applyLayers === 'function' && STATE) applyLayers(); // fond OSM assombri
|
||||||
|
redrawRose();
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rose de la légende : ses libellés cardinaux prennent la couleur du texte,
|
||||||
|
// elle est donc redessinée à chaque changement de thème (si déjà dessinée).
|
||||||
|
function redrawRose() {
|
||||||
|
if (!drawRose.done) return;
|
||||||
|
drawRose.done = false;
|
||||||
|
drawRose();
|
||||||
|
}
|
||||||
|
|
||||||
function cssVar(name) {
|
function cssVar(name) {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||||
}
|
}
|
||||||
@ -580,15 +589,17 @@ function labToRgb(L, a, b) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROSE_SIZE = 96; // taille affichée (px CSS), cf. .legend canvas
|
||||||
|
|
||||||
function drawRose() {
|
function drawRose() {
|
||||||
const cv = el('roseCanvas');
|
const cv = el('roseCanvas');
|
||||||
if (!cv || drawRose.done) return;
|
if (!cv || drawRose.done) return;
|
||||||
drawRose.done = true;
|
drawRose.done = true;
|
||||||
const dpr = window.devicePixelRatio || 1, size = 132;
|
const dpr = window.devicePixelRatio || 1, size = ROSE_SIZE, k = size / 132;
|
||||||
cv.width = size * dpr; cv.height = size * dpr;
|
cv.width = size * dpr; cv.height = size * dpr;
|
||||||
const ctx = cv.getContext('2d');
|
const ctx = cv.getContext('2d');
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
const c = size / 2, rOut = 50, rIn = 22, L = 64;
|
const c = size / 2, rOut = 50 * k, rIn = 22 * k, L = 64;
|
||||||
const chroma = 60 * Math.min(1, L * (100 - L) / 2500);
|
const chroma = 60 * Math.min(1, L * (100 - L) / 2500);
|
||||||
const img = ctx.createImageData(size * dpr, size * dpr);
|
const img = ctx.createImageData(size * dpr, size * dpr);
|
||||||
for (let py = 0; py < size * dpr; py++) {
|
for (let py = 0; py < size * dpr; py++) {
|
||||||
@ -604,14 +615,15 @@ function drawRose() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(img, 0, 0);
|
ctx.putImageData(img, 0, 0);
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
ctx.font = '600 11px system-ui, sans-serif';
|
|
||||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
|
const text = cssVar('--text') || '#1d232b';
|
||||||
const labels = [['N', 0], ['NE', 45], ['E', 90], ['SE', 135], ['S', 180], ['SO', 225], ['O', 270], ['NO', 315]];
|
const labels = [['N', 0], ['NE', 45], ['E', 90], ['SE', 135], ['S', 180], ['SO', 225], ['O', 270], ['NO', 315]];
|
||||||
for (const [t, deg] of labels) {
|
for (const [t, deg] of labels) {
|
||||||
const a = deg * Math.PI / 180, rr = t.length > 1 ? 36 : 58;
|
const a = deg * Math.PI / 180, rr = (t.length > 1 ? 36 : 58) * k;
|
||||||
const x = c + rr * Math.sin(a), y = c - rr * Math.cos(a);
|
const x = c + rr * Math.sin(a), y = c - rr * Math.cos(a);
|
||||||
ctx.fillStyle = t.length > 1 ? 'rgba(20,20,20,0.85)' : 'rgba(255,255,255,0.95)';
|
// intercardinaux sur l'anneau coloré : sombres ; cardinaux hors anneau : couleur du texte
|
||||||
if (t.length > 1) { ctx.font = '600 9px system-ui, sans-serif'; } else { ctx.font = '700 11px system-ui, sans-serif'; }
|
ctx.fillStyle = t.length > 1 ? 'rgba(20,20,20,0.85)' : text;
|
||||||
|
ctx.font = t.length > 1 ? '600 8px system-ui, sans-serif' : '700 10px system-ui, sans-serif';
|
||||||
ctx.fillText(t, x, y);
|
ctx.fillText(t, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -619,7 +631,7 @@ function drawRose() {
|
|||||||
function updateRose() {
|
function updateRose() {
|
||||||
const on = !!(STATE && STATE.main === 'relief_oriente' && effectiveMode() !== 'precision'
|
const on = !!(STATE && STATE.main === 'relief_oriente' && effectiveMode() !== 'precision'
|
||||||
&& LAYERS.get('relief_oriente'));
|
&& LAYERS.get('relief_oriente'));
|
||||||
el('rose').hidden = !on;
|
el('legendBlock').hidden = !on;
|
||||||
if (on) drawRose();
|
if (on) drawRose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,7 +666,6 @@ function renderUseCard() {
|
|||||||
} else { window.prompt('URL XYZ :', text); }
|
} else { window.prompt('URL XYZ :', text); }
|
||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
el('usecard').hidden = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- génération de dalles (API /api/status, /api/preview, /api/generate) ---
|
// --- génération de dalles (API /api/status, /api/preview, /api/generate) ---
|
||||||
@ -693,7 +704,8 @@ function genApi(method, url, body) {
|
|||||||
|
|
||||||
function genOpenCard(msg) {
|
function genOpenCard(msg) {
|
||||||
if (msg !== undefined) el('genMsg').textContent = msg;
|
if (msg !== undefined) el('genMsg').textContent = msg;
|
||||||
el('gencard').hidden = false;
|
// Onglet déjà visible : pas de openTab, qui changerait la hauteur du volet.
|
||||||
|
if (panelVisibleTab() !== 'gen') openTab('gen');
|
||||||
}
|
}
|
||||||
|
|
||||||
function genFillViz() {
|
function genFillViz() {
|
||||||
@ -862,6 +874,7 @@ function genAfterRun() {
|
|||||||
renderPanel();
|
renderPanel();
|
||||||
renderBaseRow();
|
renderBaseRow();
|
||||||
applyLayers();
|
applyLayers();
|
||||||
|
renderUseCard();
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
fetch('api/map/background', { method: 'POST' }).catch(() => {});
|
fetch('api/map/background', { method: 'POST' }).catch(() => {});
|
||||||
}
|
}
|
||||||
@ -915,10 +928,6 @@ function genPollSoon() {
|
|||||||
|
|
||||||
el('btnZone').addEventListener('click', () => genSetDrawing(!genDrawing));
|
el('btnZone').addEventListener('click', () => genSetDrawing(!genDrawing));
|
||||||
el('btnComplete').addEventListener('click', genComplete);
|
el('btnComplete').addEventListener('click', genComplete);
|
||||||
el('genClose').addEventListener('click', () => {
|
|
||||||
el('gencard').hidden = true;
|
|
||||||
if (genDrawing) genSetDrawing(false);
|
|
||||||
});
|
|
||||||
el('genClear').addEventListener('click', () => {
|
el('genClear').addEventListener('click', () => {
|
||||||
genSel.clear();
|
genSel.clear();
|
||||||
genCorners.clear();
|
genCorners.clear();
|
||||||
@ -971,19 +980,15 @@ function printLoad() {
|
|||||||
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
|
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recadre sur le cadre en laissant libre la bande basse couverte par les
|
// Recadre sur le cadre en laissant libre la place du panneau (colonne à
|
||||||
// cartes (couches, export…) : sinon le bas du cadre passe dessous.
|
// gauche sur ordinateur, volet en bas sur téléphone) : sinon il passe dessous.
|
||||||
function printFit() {
|
function printFit() {
|
||||||
if (!PRINT.corners) return;
|
if (!PRINT.corners) return;
|
||||||
const box = map.getContainer().getBoundingClientRect();
|
const r = el('panel').getBoundingClientRect();
|
||||||
let bottom = 0;
|
const panelWidth = isPhone() ? 0 : r.right;
|
||||||
for (const c of document.querySelectorAll('.card')) {
|
const sheetHeight = isPhone() ? r.height : 0;
|
||||||
if (c.hidden || !c.offsetParent) continue;
|
|
||||||
const r = c.getBoundingClientRect();
|
|
||||||
if (r.top > box.top + box.height / 2) bottom = Math.max(bottom, box.bottom - r.top);
|
|
||||||
}
|
|
||||||
map.fitBounds(L.latLngBounds(PRINT.corners),
|
map.fitBounds(L.latLngBounds(PRINT.corners),
|
||||||
{ paddingTopLeft: [30, 60], paddingBottomRight: [70, bottom + 20] });
|
{ paddingTopLeft: [panelWidth + 30, 60], paddingBottomRight: [70, sheetHeight + 20] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function printDraw(corners) {
|
function printDraw(corners) {
|
||||||
@ -1046,10 +1051,11 @@ function printCenterHere() {
|
|||||||
printRefreshFrame();
|
printRefreshFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
|
||||||
|
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
|
||||||
function printSetOpen(on) {
|
function printSetOpen(on) {
|
||||||
|
if (PRINT.on === on) return;
|
||||||
PRINT.on = on;
|
PRINT.on = on;
|
||||||
el('printcard').hidden = !on;
|
|
||||||
el('btnPrint').classList.toggle('accent', on);
|
|
||||||
if (on) {
|
if (on) {
|
||||||
// cadre gardé s'il est encore dans la vue, sinon posé au centre
|
// cadre gardé s'il est encore dans la vue, sinon posé au centre
|
||||||
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
|
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
|
||||||
@ -1089,8 +1095,6 @@ function printExport() {
|
|||||||
.finally(() => { btn.disabled = false; });
|
.finally(() => { btn.disabled = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
el('btnPrint').addEventListener('click', () => printSetOpen(!PRINT.on));
|
|
||||||
el('printClose').addEventListener('click', () => printSetOpen(false));
|
|
||||||
el('printGo').addEventListener('click', printExport);
|
el('printGo').addEventListener('click', printExport);
|
||||||
el('printHere').addEventListener('click', printCenterHere);
|
el('printHere').addEventListener('click', printCenterHere);
|
||||||
el('printFit').addEventListener('click', printFit);
|
el('printFit').addEventListener('click', printFit);
|
||||||
@ -1100,6 +1104,115 @@ for (const id of ['printPaper', 'printOrient', 'printScale']) {
|
|||||||
el('printTitle').addEventListener('change', printSave);
|
el('printTitle').addEventListener('change', printSave);
|
||||||
printLoad();
|
printLoad();
|
||||||
|
|
||||||
|
document.addEventListener('panel:tab', (e) => {
|
||||||
|
printSetOpen(e.detail.visible === 'print');
|
||||||
|
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- panneau à onglets --------------------------------------------------------
|
||||||
|
const PANEL_LS = 'lidar-panel';
|
||||||
|
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, lsGet(PANEL_LS, {}));
|
||||||
|
const phoneQuery = window.matchMedia('(max-width: 720px)');
|
||||||
|
const isPhone = () => phoneQuery.matches;
|
||||||
|
|
||||||
|
// Onglets proposés : Génération seulement si le générateur répond et que ce
|
||||||
|
// navigateur a le droit de lancer (même règle que les anciens boutons).
|
||||||
|
function panelTabs() {
|
||||||
|
return ['view', 'print', 'gen', 'share'].filter(t => t !== 'gen' || (GEN.available && GEN.allowed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Onglet réellement visible, ou null (panneau replié, volet fermé).
|
||||||
|
function panelVisibleTab() {
|
||||||
|
if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null;
|
||||||
|
return PANEL.tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelSave() { lsSet(PANEL_LS, { tab: PANEL.tab, collapsed: PANEL.collapsed, sheet: PANEL.sheet }); }
|
||||||
|
|
||||||
|
function panelRender() {
|
||||||
|
const tabs = panelTabs();
|
||||||
|
if (!tabs.includes(PANEL.tab)) PANEL.tab = 'view';
|
||||||
|
el('panelTabs').querySelectorAll('button[data-tab]').forEach(b => {
|
||||||
|
b.hidden = !tabs.includes(b.dataset.tab);
|
||||||
|
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
|
||||||
|
});
|
||||||
|
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
|
||||||
|
const panel = el('panel');
|
||||||
|
panel.classList.toggle('collapsed', PANEL.collapsed);
|
||||||
|
panel.dataset.sheet = PANEL.sheet;
|
||||||
|
document.body.classList.toggle('panel-collapsed', PANEL.collapsed);
|
||||||
|
// Hauteur couverte par le volet, que l'échelle, l'attribution et les messages
|
||||||
|
// dégagent ; volet plein : la carte est cachée, rien à dégager (sinon ils
|
||||||
|
// remonteraient en haut de l'écran, sur les outils).
|
||||||
|
const sheetPx = isPhone() && PANEL.sheet !== 'full' ? panel.getBoundingClientRect().height : 0;
|
||||||
|
document.body.style.setProperty('--sheet-h', sheetPx + 'px');
|
||||||
|
document.dispatchEvent(new CustomEvent('panel:tab', { detail: { tab: PANEL.tab, visible: panelVisibleTab() } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTab(tab) {
|
||||||
|
if (!panelTabs().includes(tab)) return;
|
||||||
|
const before = PANEL.sheet;
|
||||||
|
// Onglet actif retouché sur téléphone : le volet change de hauteur.
|
||||||
|
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
|
||||||
|
PANEL.sheet = PANEL.sheet === 'half' ? 'full' : 'half';
|
||||||
|
} else if (isPhone() && PANEL.sheet === 'closed') {
|
||||||
|
PANEL.sheet = 'half';
|
||||||
|
}
|
||||||
|
const resized = isPhone() && PANEL.sheet !== before;
|
||||||
|
PANEL.tab = tab;
|
||||||
|
PANEL.collapsed = false;
|
||||||
|
panelSave(); panelRender();
|
||||||
|
if (resized) setTimeout(panelRender, 220); // hauteur finale après la transition CSS
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCollapsed(on) { PANEL.collapsed = !!on; panelSave(); panelRender(); map.invalidateSize(); }
|
||||||
|
|
||||||
|
function setSheet(state) {
|
||||||
|
if (!['closed', 'half', 'full'].includes(state)) return;
|
||||||
|
PANEL.sheet = state; panelSave();
|
||||||
|
panelRender();
|
||||||
|
setTimeout(panelRender, 220); // hauteur finale après la transition CSS
|
||||||
|
}
|
||||||
|
|
||||||
|
el('panelTabs').querySelectorAll('button[data-tab]').forEach(b =>
|
||||||
|
b.addEventListener('click', () => openTab(b.dataset.tab)));
|
||||||
|
el('panelCollapse').addEventListener('click', () => setCollapsed(!PANEL.collapsed));
|
||||||
|
phoneQuery.addEventListener('change', panelRender);
|
||||||
|
window.addEventListener('resize', panelRender); // hauteur du volet en vh (rotation)
|
||||||
|
|
||||||
|
// Poignée du volet : glisser vers le haut / le bas, relâcher sur la hauteur
|
||||||
|
// la plus proche ; un simple toucher fait monter d'un cran.
|
||||||
|
(function sheetDrag() {
|
||||||
|
const grip = el('sheetGrip');
|
||||||
|
let start = null;
|
||||||
|
grip.addEventListener('pointerdown', (e) => {
|
||||||
|
start = { y: e.clientY, h: el('panel').getBoundingClientRect().height };
|
||||||
|
grip.setPointerCapture(e.pointerId);
|
||||||
|
el('panel').style.transition = 'none';
|
||||||
|
});
|
||||||
|
grip.addEventListener('pointermove', (e) => {
|
||||||
|
if (!start) return;
|
||||||
|
const h = Math.max(72, Math.min(window.innerHeight - 16, start.h + start.y - e.clientY));
|
||||||
|
el('panel').style.height = h + 'px';
|
||||||
|
});
|
||||||
|
const end = (e) => {
|
||||||
|
if (!start) return;
|
||||||
|
const moved = Math.abs(e.clientY - start.y) > 6;
|
||||||
|
const h = el('panel').getBoundingClientRect().height;
|
||||||
|
el('panel').style.height = ''; el('panel').style.transition = '';
|
||||||
|
start = null;
|
||||||
|
if (!moved) { setSheet(PANEL.sheet === 'closed' ? 'half' : PANEL.sheet === 'half' ? 'full' : 'half'); return; }
|
||||||
|
const stops = { closed: 72, half: window.innerHeight * 0.45, full: window.innerHeight - 16 };
|
||||||
|
const best = Object.entries(stops).sort((a, b) => Math.abs(a[1] - h) - Math.abs(b[1] - h))[0][0];
|
||||||
|
setSheet(best);
|
||||||
|
};
|
||||||
|
grip.addEventListener('pointerup', end);
|
||||||
|
grip.addEventListener('pointercancel', end);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Fiche de dalle fermée par Échap (provisoire : la Task 4 la remplace).
|
||||||
|
function closeTileViews() { if (el('tilecard').hidden) return false; el('tilecard').hidden = true; return true; }
|
||||||
|
|
||||||
// --- démarrage ------------------------------------------------------------
|
// --- démarrage ------------------------------------------------------------
|
||||||
function fitData() {
|
function fitData() {
|
||||||
if (META.bounds) {
|
if (META.bounds) {
|
||||||
@ -1119,13 +1232,14 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
|||||||
// L'état retenu (défauts du serveur, puis lien partagé) est persisté tout de
|
// L'état retenu (défauts du serveur, puis lien partagé) est persisté tout de
|
||||||
// suite : ce qui est à l'écran est ce qui sera rechargé.
|
// suite : ce qui est à l'écran est ce qui sera rechargé.
|
||||||
saveState();
|
saveState();
|
||||||
el('stat').textContent = META.layers.length + ' couche(s)';
|
|
||||||
const hash = parseHash();
|
const hash = parseHash();
|
||||||
if (hash) map.setView([hash.lat, hash.lng], hash.zoom);
|
if (hash) map.setView([hash.lat, hash.lng], hash.zoom);
|
||||||
else fitData();
|
else fitData();
|
||||||
buildLayers();
|
buildLayers();
|
||||||
renderPanel();
|
renderPanel();
|
||||||
renderBaseRow();
|
renderBaseRow();
|
||||||
|
panelRender();
|
||||||
|
renderUseCard();
|
||||||
genFillViz();
|
genFillViz();
|
||||||
// Droits de génération : boutons affichés si le serveur peut générer
|
// Droits de génération : boutons affichés si le serveur peut générer
|
||||||
// (pipeline local ou LIDAR_GENERATION_URL) ET que le navigateur vient du
|
// (pipeline local ou LIDAR_GENERATION_URL) ET que le navigateur vient du
|
||||||
@ -1135,8 +1249,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
|||||||
GEN.available = !!d.available;
|
GEN.available = !!d.available;
|
||||||
GEN.allowed = d.regen_allowed !== false;
|
GEN.allowed = d.regen_allowed !== false;
|
||||||
const show = GEN.available && GEN.allowed;
|
const show = GEN.available && GEN.allowed;
|
||||||
el('btnZone').hidden = !show;
|
panelRender();
|
||||||
el('btnComplete').hidden = !show;
|
|
||||||
if (show) { genUpdateStatus(d); genPollNow(); }
|
if (show) { genUpdateStatus(d); genPollNow(); }
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}).catch(() => toast('Serveur de tuiles injoignable.', 10000));
|
}).catch(() => toast('Serveur de tuiles injoignable.', 10000));
|
||||||
@ -1166,14 +1279,10 @@ map.on('click', (e) => {
|
|||||||
.then(d => showTileInfo(d, e.latlng));
|
.then(d => showTileInfo(d, e.latlng));
|
||||||
});
|
});
|
||||||
|
|
||||||
el('cardToggle').addEventListener('click', () =>
|
|
||||||
el('layercard').classList.toggle('collapsed'));
|
|
||||||
el('tileClose').addEventListener('click', () => {
|
el('tileClose').addEventListener('click', () => {
|
||||||
el('tilecard').hidden = true;
|
el('tilecard').hidden = true;
|
||||||
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||||
});
|
});
|
||||||
el('useClose').addEventListener('click', () => { el('usecard').hidden = true; });
|
|
||||||
el('btnUse').addEventListener('click', renderUseCard);
|
|
||||||
el('btnShare').addEventListener('click', () => {
|
el('btnShare').addEventListener('click', () => {
|
||||||
const url = location.origin + location.pathname + viewHash(true);
|
const url = location.origin + location.pathname + viewHash(true);
|
||||||
history.replaceState(null, '', url);
|
history.replaceState(null, '', url);
|
||||||
@ -1245,7 +1354,11 @@ el('btnTheme').addEventListener('click', () => {
|
|||||||
lsSet(THEME_LS, THEME);
|
lsSet(THEME_LS, THEME);
|
||||||
});
|
});
|
||||||
if (darkQuery && darkQuery.addEventListener) {
|
if (darkQuery && darkQuery.addEventListener) {
|
||||||
darkQuery.addEventListener('change', () => { if (THEME === 'auto' && STATE) applyLayers(); });
|
darkQuery.addEventListener('change', () => {
|
||||||
|
if (THEME !== 'auto') return;
|
||||||
|
if (STATE) applyLayers();
|
||||||
|
redrawRose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
el('zoomIn').addEventListener('click', () => map.zoomIn(1));
|
el('zoomIn').addEventListener('click', () => map.zoomIn(1));
|
||||||
el('zoomOut').addEventListener('click', () => map.zoomOut(1));
|
el('zoomOut').addEventListener('click', () => map.zoomOut(1));
|
||||||
@ -1273,16 +1386,26 @@ el('gps').addEventListener('click', () => {
|
|||||||
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
|
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
// P : relief → précision → les deux → relief (hors saisie de texte)
|
|
||||||
const typing = e.target && e.target.closest && e.target.closest('input, select, textarea');
|
const typing = e.target && e.target.closest && e.target.closest('input, select, textarea');
|
||||||
if ((e.key === 'p' || e.key === 'P') && !typing && !e.ctrlKey && !e.metaKey && !e.altKey
|
if (typing || e.ctrlKey || e.metaKey || e.altKey) {
|
||||||
&& META && STATE && precisionKey()) {
|
if (e.key === 'Escape' && typing) e.target.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// P : relief → précision → les deux → relief
|
||||||
|
if ((e.key === 'p' || e.key === 'P') && META && STATE && precisionKey()) {
|
||||||
const i = VIEW_KEYS.indexOf(STATE.mode);
|
const i = VIEW_KEYS.indexOf(STATE.mode);
|
||||||
setMode(VIEW_KEYS[(i + 1) % VIEW_KEYS.length], true);
|
setMode(VIEW_KEYS[(i + 1) % VIEW_KEYS.length], true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 1–4 : onglets du panneau (Génération ignorée si masquée)
|
||||||
|
if (/^[1-4]$/.test(e.key)) {
|
||||||
|
const tab = ['view', 'print', 'gen', 'share'][Number(e.key) - 1];
|
||||||
|
if (panelTabs().includes(tab)) openTab(tab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
el('tilecard').hidden = true; el('usecard').hidden = true;
|
if (genDrawing) { genSetDrawing(false); return; }
|
||||||
if (genDrawing) genSetDrawing(false);
|
if (closeTileViews()) return; // bulle / fiche de dalle (Task 4)
|
||||||
|
if (isPhone()) setSheet('closed'); else setCollapsed(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user