Remplacer la bulle et la fiche détaillée de dalle par un onglet Dalle du panneau
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -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).
|
||||
|
||||
38
docs/MAPS.md
38
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é
|
||||
|
||||
@ -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"'):
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -17,9 +17,10 @@
|
||||
<div class="panel-head">
|
||||
<nav id="panelTabs" class="panel-tabs" role="tablist">
|
||||
<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>
|
||||
<button role="tab" data-tab="tile" title="Dalle (2)"><span class="tab-ico">▦</span><span class="tab-lbl" id="tileTabLbl">Dalle</span></button>
|
||||
<button role="tab" data-tab="print" title="Export PDF (3)"><span class="tab-ico">⎙</span><span class="tab-lbl">Export PDF</span></button>
|
||||
<button role="tab" data-tab="gen" title="Génération (4)" hidden><span class="tab-ico">+</span><span class="tab-lbl">Génération</span></button>
|
||||
<button role="tab" data-tab="share" title="Partager (5)"><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>
|
||||
@ -60,6 +61,13 @@
|
||||
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tab-tile" role="tabpanel" hidden>
|
||||
<p class="hint" id="tileHint">Cliquez une dalle sur la carte pour la sélectionner.</p>
|
||||
<div id="tileCard" hidden>
|
||||
<div class="row tile-head"><span class="sec-title" id="tileTitle">Dalle</span></div>
|
||||
<div id="tileBody"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tab-print" role="tabpanel" hidden>
|
||||
<div class="panel-stat" id="printInfo"></div>
|
||||
<div class="row"><select id="printPaper" class="field" title="Format papier">
|
||||
@ -110,11 +118,6 @@
|
||||
<div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, 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>
|
||||
<section id="tileView" hidden>
|
||||
<div class="row tile-head"><button id="tileBack" class="btn btn-ghost">← Retour</button>
|
||||
<span class="sec-title" id="tileTitle">Dalle</span></div>
|
||||
<div id="tileBody"></div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@ -56,11 +56,6 @@ let STATE = null;
|
||||
// de /api/map/meta (sinon un serveur injoignable au démarrage le laisse
|
||||
// bloqué à vrai pour toujours : l'onglet Export ne recadrerait plus jamais).
|
||||
let BOOTSTRAPPING = true;
|
||||
// Vrai le temps d'un panelRender() qui RÉTABLIT un onglet déjà connu
|
||||
// (restoreTabAfterDetails, retour de la fiche détaillée) plutôt que d'en
|
||||
// ouvrir un nouveau à la demande de l'utilisateur : le cadre d'export ne doit
|
||||
// alors ni recadrer la vue, ni recentrer le cadre lui-même (cf. printSetOpen).
|
||||
let RESTORING_TAB = false;
|
||||
|
||||
// --- thème ------------------------------------------------------------------
|
||||
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
|
||||
@ -688,87 +683,39 @@ function showTileInfo(data, latlng) {
|
||||
.then(ign => { if (showTileInfo.current === key) renderTileCard(data, ign); });
|
||||
}
|
||||
|
||||
let tilePopup = null;
|
||||
let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
|
||||
let SELECTED_TILE = null; // données de la dalle sélectionnée (ou null)
|
||||
|
||||
function showTilePopup(data, latlng) {
|
||||
// Libellé de l'onglet : « Dalle » sans sélection, « Dalle col · row » avec.
|
||||
function tileTabLabel() {
|
||||
return SELECTED_TILE ? 'Dalle ' + SELECTED_TILE.col + ' · ' + SELECTED_TILE.row : 'Dalle';
|
||||
}
|
||||
|
||||
// Rafraîchit l'onglet Dalle (libellé + bascule hint/fiche) qu'il soit visible
|
||||
// ou non : son contenu doit être à jour dès qu'on l'ouvre.
|
||||
function renderTabTile() {
|
||||
el('tileTabLbl').textContent = tileTabLabel();
|
||||
el('tileHint').hidden = !!SELECTED_TILE;
|
||||
el('tileCard').hidden = !SELECTED_TILE;
|
||||
if (SELECTED_TILE) showTileInfo(SELECTED_TILE, lastTileQuery);
|
||||
}
|
||||
|
||||
// Sélectionne une dalle (contour + fiche dans l'onglet Dalle) sans changer
|
||||
// l'onglet affiché.
|
||||
function selectTile(data, latlng) {
|
||||
lastTileQuery = latlng || lastTileQuery;
|
||||
// Un nouveau clic pendant que la fiche détaillée d'une AUTRE dalle est
|
||||
// ouverte : on la referme (sinon le contour bouge sur la carte alors que
|
||||
// la fiche affiche encore l'ancienne dalle).
|
||||
if (!el('tileView').hidden) restoreTabAfterDetails();
|
||||
if (tilePopup) tilePopup.remove();
|
||||
const html = !data
|
||||
? '<div class="tile-pop">Serveur injoignable : réessayez dans un instant.</div>'
|
||||
: '<div class="tile-pop"><b>Dalle ' + data.col + ' · ' + data.row + '</b><br>'
|
||||
+ (data.found ? '' : 'Pas encore de rendu<br>')
|
||||
+ 'Acquisition : ' + fmtAcq(data.quality) + '<br>'
|
||||
+ 'Densité : ' + fmtDensity(data.quality)
|
||||
+ '<div class="row"><button class="btn" id="tilePopDetails">Détails</button></div></div>';
|
||||
tilePopup = L.popup({ maxWidth: 260, autoPanPaddingTopLeft: [isPhone() ? 12 : el('panel').getBoundingClientRect().right + 12, 12] })
|
||||
.setLatLng(latlng).setContent(html).openOn(map);
|
||||
if (!data) return;
|
||||
SELECTED_TILE = data;
|
||||
selectTileOutline(data);
|
||||
showTilePopup.data = data;
|
||||
const btn = el('tilePopDetails');
|
||||
if (btn) btn.addEventListener('click', openTileDetails);
|
||||
tilePopup.on('remove', () => {
|
||||
tilePopup = null;
|
||||
if (el('tileView').hidden && tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||
});
|
||||
renderTabTile();
|
||||
}
|
||||
|
||||
function openTileDetails() {
|
||||
const data = showTilePopup.data;
|
||||
if (!data) return;
|
||||
if (tilePopup) { tilePopup.off('remove'); tilePopup.remove(); tilePopup = null; }
|
||||
tileReturnTab = PANEL.tab;
|
||||
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = true;
|
||||
el('tileView').hidden = false;
|
||||
// La fiche masque l'onglet Export : son cadre et sa poignée n'ont rien à
|
||||
// faire sur la carte tant qu'elle est ouverte (restoreTabAfterDetails les
|
||||
// rétablit via panel:tab en revenant sur l'onglet Export).
|
||||
printSetOpen(false);
|
||||
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
|
||||
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
|
||||
showTileInfo(data, lastTileQuery);
|
||||
}
|
||||
|
||||
// Masque #tileView et retire le contour de sélection, sans toucher aux
|
||||
// onglets (utilisé par closeTileViews et par openTab, qui ne doit pas
|
||||
// se rappeler lui-même via closeTileViews).
|
||||
function hideTileView() {
|
||||
el('tileView').hidden = true;
|
||||
// Désélectionne (re-clic sur la même dalle, Échap) : retire le contour et
|
||||
// repasse l'onglet Dalle sur son indication.
|
||||
function clearTileSelection() {
|
||||
SELECTED_TILE = null;
|
||||
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||
renderTabTile();
|
||||
}
|
||||
|
||||
// Referme la fiche détaillée et rétablit l'onglet d'origine SANS passer par
|
||||
// openTab : celui-ci retouche la hauteur du volet quand l'onglet visé est déjà
|
||||
// l'onglet actif (règle « re-toucher l'onglet actif ») — ce qui n'a pas lieu
|
||||
// d'être ici, on rouvre simplement ce qui était affiché avant.
|
||||
function restoreTabAfterDetails() {
|
||||
hideTileView();
|
||||
PANEL.tab = PANEL.want = tileReturnTab || 'view';
|
||||
panelSave();
|
||||
// Ce rendu RÉTABLIT l'onglet Export tel qu'il était (cadre à sa place,
|
||||
// vue inchangée) : ni recadrage de la vue, ni recentrage du cadre — cf.
|
||||
// printSetOpen, qui lit ce drapeau via le listener panel:tab.
|
||||
RESTORING_TAB = true;
|
||||
panelRender();
|
||||
RESTORING_TAB = false;
|
||||
}
|
||||
|
||||
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
|
||||
function closeTileViews() {
|
||||
if (tilePopup) { tilePopup.remove(); tilePopup = null; return true; }
|
||||
if (!el('tileView').hidden) {
|
||||
restoreTabAfterDetails();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
el('tileBack').addEventListener('click', closeTileViews);
|
||||
|
||||
// --- rose des vents du relief orienté --------------------------------------
|
||||
// Mêmes couleurs que visualizations._relief_lut : teinte CIELAB = orientation
|
||||
// (aspect = orientation boussole + 90°, repère arctan2(dy, dx) du rendu),
|
||||
@ -961,7 +908,6 @@ function genAddCellFromQuery() {
|
||||
genApi('GET', 'api/cell?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng)
|
||||
.then(d => {
|
||||
genAddCells([{ col: d.col, row: d.row, corners: d.corners }]);
|
||||
closeTileViews();
|
||||
genOpenCard('1 dalle sélectionnée — options puis « Générer ».');
|
||||
})
|
||||
.catch(msg => toast('Échec : ' + msg.message, 6000));
|
||||
@ -1324,7 +1270,7 @@ el('printTitle').addEventListener('change', printSave);
|
||||
printLoad();
|
||||
|
||||
document.addEventListener('panel:tab', (e) => {
|
||||
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB);
|
||||
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);
|
||||
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
|
||||
});
|
||||
|
||||
@ -1353,17 +1299,16 @@ 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));
|
||||
return ['view', 'tile', 'print', 'gen', 'share'].filter(t => t !== 'gen' || (GEN.available && GEN.allowed));
|
||||
}
|
||||
|
||||
// Onglet réellement visible, ou null (panneau replié, volet fermé, ou fiche
|
||||
// détaillée affichée par-dessus — auquel cas AUCUN onglet n'est à l'écran,
|
||||
// pas même celui mémorisé : panel:tab doit fermer le cadre d'export s'il
|
||||
// était ouvert, quel que soit ce qui a déclenché ce rendu du panneau — grip
|
||||
// glissée/touchée, redimensionnement, changement phoneQuery, le
|
||||
// setTimeout(panelRender, 220) d'openTab…).
|
||||
// Onglet réellement visible, ou null (panneau replié ou volet fermé —
|
||||
// auquel cas AUCUN onglet n'est à l'écran, pas même celui mémorisé :
|
||||
// panel:tab doit fermer le cadre d'export s'il était ouvert, quel que soit
|
||||
// ce qui a déclenché ce rendu du panneau — grip glissée/touchée,
|
||||
// redimensionnement, changement phoneQuery, le setTimeout(panelRender, 220)
|
||||
// d'openTab…).
|
||||
function panelVisibleTab() {
|
||||
if (!el('tileView').hidden) return null;
|
||||
if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null;
|
||||
return PANEL.tab;
|
||||
}
|
||||
@ -1377,9 +1322,7 @@ function panelRender() {
|
||||
b.hidden = !tabs.includes(b.dataset.tab);
|
||||
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
|
||||
});
|
||||
if (el('tileView').hidden) {
|
||||
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
|
||||
}
|
||||
for (const t of ['view', 'tile', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
|
||||
const panel = el('panel');
|
||||
// Repli en bande d'icônes : ordinateur seulement (le téléphone a son volet).
|
||||
const collapsed = PANEL.collapsed && !isPhone();
|
||||
@ -1396,7 +1339,6 @@ function panelRender() {
|
||||
|
||||
function openTab(tab) {
|
||||
if (!panelTabs().includes(tab)) return;
|
||||
if (!el('tileView').hidden) hideTileView();
|
||||
const before = PANEL.sheet;
|
||||
// Onglet actif retouché sur téléphone : le volet change de hauteur.
|
||||
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
|
||||
@ -1523,13 +1465,22 @@ window.addEventListener('hashchange', () => {
|
||||
|
||||
map.on('moveend zoomend', saveHashSoon);
|
||||
map.on('click', (e) => {
|
||||
// Pas de bulle pendant un tracé de zone ni juste après le relâchement de
|
||||
// la poignée du cadre d'export.
|
||||
// Pas de sélection pendant un tracé de zone ni juste après le relâchement
|
||||
// de la poignée du cadre d'export.
|
||||
if (genDrawing || Date.now() < genIgnoreClickUntil || PRINT.dragging) return;
|
||||
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.catch(() => null)
|
||||
.then(d => showTilePopup(d, e.latlng));
|
||||
.then(d => {
|
||||
if (!d) { toast('Serveur injoignable : réessayez dans un instant.', 5000); return; }
|
||||
// Re-clic sur la dalle déjà sélectionnée : désélection. Sinon la
|
||||
// sélection se déplace, sans changer l'onglet affiché.
|
||||
if (SELECTED_TILE && SELECTED_TILE.col === d.col && SELECTED_TILE.row === d.row) {
|
||||
clearTileSelection();
|
||||
} else {
|
||||
selectTile(d, e.latlng);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
el('btnShare').addEventListener('click', () => {
|
||||
@ -1644,15 +1595,15 @@ document.addEventListener('keydown', (e) => {
|
||||
setMode(VIEW_KEYS[(i + 1) % VIEW_KEYS.length], true);
|
||||
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];
|
||||
// 1–5 : onglets du panneau (Génération ignorée si masquée)
|
||||
if (/^[1-5]$/.test(e.key)) {
|
||||
const tab = ['view', 'tile', 'print', 'gen', 'share'][Number(e.key) - 1];
|
||||
if (panelTabs().includes(tab)) openTab(tab);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (genDrawing) { genSetDrawing(false); return; }
|
||||
if (closeTileViews()) return; // bulle / fiche de dalle (Task 4)
|
||||
if (SELECTED_TILE) { clearTileSelection(); return; }
|
||||
if (isPhone()) setSheet('closed'); else setCollapsed(true);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user