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
|
## 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.
|
- **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`).
|
- **`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.
|
- **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).
|
- **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
|
l'on change en glissant la poignée, en la touchant simplement (un cran), ou
|
||||||
en retouchant l'onglet déjà actif.
|
en retouchant l'onglet déjà actif.
|
||||||
|
|
||||||
Un clic sur la carte ouvre une **bulle** sur la dalle LiDAR HD sous le
|
Un clic sur la carte **sélectionne** la dalle LiDAR HD sous le curseur
|
||||||
curseur (date d'acquisition, densité de points sol) ; son bouton **Détails**
|
(contour en pointillés) et remplit l'onglet **Dalle** (emprise, IGN, recalage
|
||||||
bascule une **fiche complète** dans le panneau (emprise, IGN, recalage des
|
des passes) sans changer l'onglet affiché ; re-cliquer la même dalle
|
||||||
passes) — **← Retour** rouvre l'onglet précédent. Raccourcis clavier :
|
désélectionne, cliquer une autre déplace la sélection. Raccourcis clavier :
|
||||||
**1–4** (onglets du panneau, sans effet si l'onglet est masqué), **P**
|
**1–5** (onglets du panneau, sans effet si l'onglet est masqué), **P**
|
||||||
(mode d'affichage suivant), **Échap** (ferme la bulle, puis la fiche, puis
|
(mode d'affichage suivant), **Échap** (désélectionne la dalle, puis
|
||||||
replie le panneau). Deux thèmes clair/sombre (bouton ◐/☀/☾, `auto` par
|
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
|
défaut = suit le système) ; réglages et état du panneau retenus dans
|
||||||
`localStorage` (`lidarMapView_v2`, `lidar-print`, `lidar-panel`,
|
`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 ;
|
s'additionnent ;
|
||||||
- **⤒ Compléter** — toutes les dalles déjà présentes dans `input/` qui
|
- **⤒ Compléter** — toutes les dalles déjà présentes dans `input/` qui
|
||||||
manquent au moins une des couches demandées ;
|
manquent au moins une des couches demandées ;
|
||||||
- **↻ Générer/Régénérer cette dalle** (bouton de la fiche de dalle, bulle →
|
- **↻ Générer/Régénérer cette dalle** (bouton de la fiche de dalle, onglet
|
||||||
Détails) — une dalle précise, même sans données existantes.
|
Dalle) — une dalle précise, même sans données existantes.
|
||||||
|
|
||||||
Options du run : couches visées (défaut : les couches du panneau) et
|
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
|
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
|
## Fiche de dalle et rose des vents
|
||||||
|
|
||||||
Un clic sur la carte sélectionne la dalle LiDAR HD sous le curseur (cadre
|
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,
|
jaune en pointillés) et remplit l'onglet **Dalle** — nom, emprise Lambert 93,
|
||||||
densité de points sol — qu'elle soit générée ou non. Son bouton **Détails**
|
puis, si la dalle est rendue, la résolution, la date de génération et le
|
||||||
bascule une fiche complète dans le panneau : nom, emprise Lambert 93, puis,
|
recalage vertical des passes (faisceaux, décalages, correction des lignes) —
|
||||||
si la dalle est rendue, la résolution, la date de génération et le recalage
|
qu'elle soit générée ou non, sans changer l'onglet affiché. Les informations
|
||||||
vertical des passes (faisceaux, décalages, correction des lignes). Les
|
IGN arrivent à part (`GET /api/map/ign?col&row`, catalogue STAC mis en cache
|
||||||
informations IGN arrivent à part (`GET /api/map/ign?col&row`, catalogue STAC
|
dans `output/ign_meta/`) : date et heure du scan LiDAR, capteurs, mission,
|
||||||
mis en cache dans `output/ign_meta/`) : date et heure du scan LiDAR,
|
opérateur, date d'édition, procédé de classement, nombre de points et lien de
|
||||||
capteurs, mission, opérateur, date d'édition, procédé de classement, nombre
|
téléchargement du nuage `.copc.laz` sur la géoplateforme. Un catalogue lent
|
||||||
de points et lien de téléchargement du nuage `.copc.laz` sur la
|
ou injoignable n'empêche jamais la sélection. Re-cliquer la même dalle (ou
|
||||||
géoplateforme. Un catalogue lent ou injoignable n'empêche jamais la
|
Échap) désélectionne.
|
||||||
sélection. **← Retour** (ou Échap) ferme la fiche et rouvre l'onglet
|
|
||||||
précédent.
|
|
||||||
|
|
||||||
Quand le relief orienté est affiché, une rose des vents donne la couleur de
|
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é
|
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
|
assert "pane: 'tileSel'" in _MAP_JS
|
||||||
|
|
||||||
|
|
||||||
def test_ui_tile_popup_and_details_view():
|
def test_ui_tile_tab_replaces_popup_and_details_view():
|
||||||
"""La fiche de dalle est une bulle Leaflet + une vue détaillée dans le panneau."""
|
"""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
|
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||||
assert 'id="tilecard"' not in _MAP_HTML
|
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_
|
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 fn in _MAP_JS, fn
|
||||||
assert "L.popup(" in _MAP_JS and "Détails" in _MAP_JS
|
# Pas de sélection pendant le déplacement du cadre d'export ni un tracé
|
||||||
# Pas de bulle pendant le déplacement du cadre d'export ni un tracé
|
|
||||||
click = _MAP_JS[_MAP_JS.index("map.on('click'"):]
|
click = _MAP_JS[_MAP_JS.index("map.on('click'"):]
|
||||||
assert "genDrawing" in click[:400] and "PRINT.dragging" in click[:400]
|
assert "genDrawing" in click[:400] and "PRINT.dragging" in click[:400]
|
||||||
# Changer d'onglet pendant que la fiche est ouverte nettoie le contour
|
# Re-clic sur la dalle déjà sélectionnée : désélection plutôt que bascule
|
||||||
# de sélection (pas de fuite de tileSel) : openTab passe par le même
|
assert "clearTileSelection();" in click[click.index("SELECTED_TILE"):click.index("});")]
|
||||||
# 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]
|
|
||||||
|
|
||||||
|
|
||||||
def test_defaults_roundtrip(tmp_path, monkeypatch):
|
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
|
assert "className: themeIsDark() ? 'base-dark' : ''" in _MAP_JS
|
||||||
|
|
||||||
|
|
||||||
def test_ui_phone_return_and_escape_keep_sheet_height():
|
def test_ui_escape_deselects_tile_before_collapsing_panel():
|
||||||
"""« ← Retour » et Échap depuis la fiche de dalle rétablissent l'onglet
|
"""Échap désélectionne la dalle (si sélection il y a) avant de replier le
|
||||||
précédent SANS repasser par openTab (qui retoucherait la hauteur du volet
|
panneau ; un tracé de zone en cours est annulé en premier, comme avant."""
|
||||||
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."""
|
|
||||||
from lidar_pipeline.mapui import _MAP_JS
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
assert "function restoreTabAfterDetails()" in _MAP_JS
|
keydown = _MAP_JS[_MAP_JS.index("document.addEventListener('keydown'"):]
|
||||||
restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):]
|
esc = keydown[keydown.rindex("if (e.key === 'Escape')"):]
|
||||||
assert "PANEL.tab = PANEL.want = tileReturnTab || 'view';" in restore[:300]
|
assert "genSetDrawing(false)" in esc[:200]
|
||||||
assert "openTab(" not in restore[:300]
|
assert "clearTileSelection()" in esc[:400]
|
||||||
close_tiles = _MAP_JS[_MAP_JS.index("function closeTileViews("):]
|
assert "setSheet('closed')" in esc[:600] and "setCollapsed(true)" in esc[:600]
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
def test_ui_saved_print_tab_does_not_rezoom_shared_link():
|
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)."""
|
après une ouverture explicite ultérieure de l'onglet)."""
|
||||||
from lidar_pipeline.mapui import _MAP_JS
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
assert "let BOOTSTRAPPING = true;" in _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;"):]
|
then_branch = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):]
|
||||||
assert then_branch.startswith("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."):]
|
catch_branch = _MAP_JS[_MAP_JS.index(".catch(() => {\n toast('Serveur de tuiles injoignable."):]
|
||||||
assert "BOOTSTRAPPING = false;" in catch_branch[:400]
|
assert "BOOTSTRAPPING = false;" in catch_branch[:400]
|
||||||
|
|
||||||
|
|
||||||
def test_ui_details_view_hides_print_frame():
|
def test_ui_tile_selection_does_not_affect_print_frame():
|
||||||
"""Ouvrir la fiche détaillée pendant que l'onglet Export est affiché
|
"""La sélection d'une dalle vit à part des onglets : elle ne referme ni
|
||||||
referme le cadre d'export (sinon cadre + poignée restent visibles
|
ne rouvre le cadre d'export (contrairement à l'ancienne fiche « Détails »,
|
||||||
derrière la fiche) — et le reste tant que la fiche est ouverte, quel que
|
qui masquait les autres onglets par-dessus la carte)."""
|
||||||
soit ce qui redéclenche un rendu du panneau (grip, redimensionnement,
|
|
||||||
changement phoneQuery…) puisque panelVisibleTab() renvoie null tant que
|
|
||||||
#tileView n'est pas masqué."""
|
|
||||||
from lidar_pipeline.mapui import _MAP_JS
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):]
|
select_fn = _MAP_JS[_MAP_JS.index("function selectTile("):]
|
||||||
assert "printSetOpen(false);" in open_details[:600]
|
assert "printSetOpen" not in select_fn[:select_fn.index("function clearTileSelection(")]
|
||||||
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]
|
|
||||||
open_fn = _MAP_JS[_MAP_JS.index("function printSetOpen("):]
|
open_fn = _MAP_JS[_MAP_JS.index("function printSetOpen("):]
|
||||||
assert "explicit && !map.getBounds().contains(PRINT.center)" in open_fn[:700]
|
assert "explicit && !map.getBounds().contains(PRINT.center)" in open_fn[:700]
|
||||||
assert "printRefreshFrame(!!explicit);" 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():
|
def test_ui_single_tabbed_panel():
|
||||||
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 ("panel", "panelTabs", "panelBody", "panelCollapse", "sheetGrip",
|
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_
|
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
|
assert f'data-tab="{tab}"' in _MAP_HTML, tab
|
||||||
# Plus d'éléments flottants : tout passe dans le panneau
|
# Plus d'éléments flottants : tout passe dans le panneau
|
||||||
for gone in ('id="layercard"', 'id="printcard"', 'id="gencard"', 'id="usecard"',
|
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("):
|
"function setCollapsed(", "function panelVisibleTab("):
|
||||||
assert fn in _MAP_JS, fn
|
assert fn in _MAP_JS, fn
|
||||||
assert "'panel:tab'" in _MAP_JS
|
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 "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
|
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"'):
|
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 { font-size: var(--fs-xs); 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); }
|
||||||
.tile-head .sec-title { margin: 0; flex: 1; }
|
.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 --------------------------------------------------- */
|
/* --- composants communs --------------------------------------------------- */
|
||||||
.btn, button { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
|
.btn, button { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
|
||||||
|
|||||||
@ -17,9 +17,10 @@
|
|||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<nav id="panelTabs" class="panel-tabs" role="tablist">
|
<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="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="tile" title="Dalle (2)"><span class="tab-ico">▦</span><span class="tab-lbl" id="tileTabLbl">Dalle</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="print" title="Export PDF (3)"><span class="tab-ico">⎙</span><span class="tab-lbl">Export PDF</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="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>
|
</nav>
|
||||||
<button id="panelCollapse" class="btn-icon" title="Replier le panneau">‹</button>
|
<button id="panelCollapse" class="btn-icon" title="Replier le panneau">‹</button>
|
||||||
</div>
|
</div>
|
||||||
@ -60,6 +61,13 @@
|
|||||||
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<section id="tab-print" role="tabpanel" hidden>
|
||||||
<div class="panel-stat" id="printInfo"></div>
|
<div class="panel-stat" id="printInfo"></div>
|
||||||
<div class="row"><select id="printPaper" class="field" title="Format papier">
|
<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>
|
<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>
|
<button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button></div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@ -56,11 +56,6 @@ let STATE = null;
|
|||||||
// de /api/map/meta (sinon un serveur injoignable au démarrage le laisse
|
// de /api/map/meta (sinon un serveur injoignable au démarrage le laisse
|
||||||
// bloqué à vrai pour toujours : l'onglet Export ne recadrerait plus jamais).
|
// bloqué à vrai pour toujours : l'onglet Export ne recadrerait plus jamais).
|
||||||
let BOOTSTRAPPING = true;
|
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 ------------------------------------------------------------------
|
// --- thème ------------------------------------------------------------------
|
||||||
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
|
// 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); });
|
.then(ign => { if (showTileInfo.current === key) renderTileCard(data, ign); });
|
||||||
}
|
}
|
||||||
|
|
||||||
let tilePopup = null;
|
let SELECTED_TILE = null; // données de la dalle sélectionnée (ou null)
|
||||||
let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
|
|
||||||
|
|
||||||
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;
|
lastTileQuery = latlng || lastTileQuery;
|
||||||
// Un nouveau clic pendant que la fiche détaillée d'une AUTRE dalle est
|
SELECTED_TILE = data;
|
||||||
// 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;
|
|
||||||
selectTileOutline(data);
|
selectTileOutline(data);
|
||||||
showTilePopup.data = data;
|
renderTabTile();
|
||||||
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; }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTileDetails() {
|
// Désélectionne (re-clic sur la même dalle, Échap) : retire le contour et
|
||||||
const data = showTilePopup.data;
|
// repasse l'onglet Dalle sur son indication.
|
||||||
if (!data) return;
|
function clearTileSelection() {
|
||||||
if (tilePopup) { tilePopup.off('remove'); tilePopup.remove(); tilePopup = null; }
|
SELECTED_TILE = 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;
|
|
||||||
if (tileSel) { map.removeLayer(tileSel); tileSel = 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é --------------------------------------
|
// --- rose des vents du relief orienté --------------------------------------
|
||||||
// Mêmes couleurs que visualizations._relief_lut : teinte CIELAB = orientation
|
// Mêmes couleurs que visualizations._relief_lut : teinte CIELAB = orientation
|
||||||
// (aspect = orientation boussole + 90°, repère arctan2(dy, dx) du rendu),
|
// (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)
|
genApi('GET', 'api/cell?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng)
|
||||||
.then(d => {
|
.then(d => {
|
||||||
genAddCells([{ col: d.col, row: d.row, corners: d.corners }]);
|
genAddCells([{ col: d.col, row: d.row, corners: d.corners }]);
|
||||||
closeTileViews();
|
|
||||||
genOpenCard('1 dalle sélectionnée — options puis « Générer ».');
|
genOpenCard('1 dalle sélectionnée — options puis « Générer ».');
|
||||||
})
|
})
|
||||||
.catch(msg => toast('Échec : ' + msg.message, 6000));
|
.catch(msg => toast('Échec : ' + msg.message, 6000));
|
||||||
@ -1324,7 +1270,7 @@ el('printTitle').addEventListener('change', printSave);
|
|||||||
printLoad();
|
printLoad();
|
||||||
|
|
||||||
document.addEventListener('panel:tab', (e) => {
|
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);
|
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
|
// 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).
|
// navigateur a le droit de lancer (même règle que les anciens boutons).
|
||||||
function panelTabs() {
|
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
|
// Onglet réellement visible, ou null (panneau replié ou volet fermé —
|
||||||
// détaillée affichée par-dessus — auquel cas AUCUN onglet n'est à l'écran,
|
// auquel cas AUCUN onglet n'est à l'écran, pas même celui mémorisé :
|
||||||
// pas même celui mémorisé : panel:tab doit fermer le cadre d'export s'il
|
// panel:tab doit fermer le cadre d'export s'il était ouvert, quel que soit
|
||||||
// était ouvert, quel que soit ce qui a déclenché ce rendu du panneau — grip
|
// ce qui a déclenché ce rendu du panneau — grip glissée/touchée,
|
||||||
// glissée/touchée, redimensionnement, changement phoneQuery, le
|
// redimensionnement, changement phoneQuery, le setTimeout(panelRender, 220)
|
||||||
// setTimeout(panelRender, 220) d'openTab…).
|
// d'openTab…).
|
||||||
function panelVisibleTab() {
|
function panelVisibleTab() {
|
||||||
if (!el('tileView').hidden) return null;
|
|
||||||
if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null;
|
if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null;
|
||||||
return PANEL.tab;
|
return PANEL.tab;
|
||||||
}
|
}
|
||||||
@ -1377,9 +1322,7 @@ function panelRender() {
|
|||||||
b.hidden = !tabs.includes(b.dataset.tab);
|
b.hidden = !tabs.includes(b.dataset.tab);
|
||||||
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
|
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
|
||||||
});
|
});
|
||||||
if (el('tileView').hidden) {
|
for (const t of ['view', 'tile', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
|
||||||
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
|
|
||||||
}
|
|
||||||
const panel = el('panel');
|
const panel = el('panel');
|
||||||
// Repli en bande d'icônes : ordinateur seulement (le téléphone a son volet).
|
// Repli en bande d'icônes : ordinateur seulement (le téléphone a son volet).
|
||||||
const collapsed = PANEL.collapsed && !isPhone();
|
const collapsed = PANEL.collapsed && !isPhone();
|
||||||
@ -1396,7 +1339,6 @@ function panelRender() {
|
|||||||
|
|
||||||
function openTab(tab) {
|
function openTab(tab) {
|
||||||
if (!panelTabs().includes(tab)) return;
|
if (!panelTabs().includes(tab)) return;
|
||||||
if (!el('tileView').hidden) hideTileView();
|
|
||||||
const before = PANEL.sheet;
|
const before = PANEL.sheet;
|
||||||
// Onglet actif retouché sur téléphone : le volet change de hauteur.
|
// Onglet actif retouché sur téléphone : le volet change de hauteur.
|
||||||
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
|
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
|
||||||
@ -1523,13 +1465,22 @@ window.addEventListener('hashchange', () => {
|
|||||||
|
|
||||||
map.on('moveend zoomend', saveHashSoon);
|
map.on('moveend zoomend', saveHashSoon);
|
||||||
map.on('click', (e) => {
|
map.on('click', (e) => {
|
||||||
// Pas de bulle pendant un tracé de zone ni juste après le relâchement de
|
// Pas de sélection pendant un tracé de zone ni juste après le relâchement
|
||||||
// la poignée du cadre d'export.
|
// de la poignée du cadre d'export.
|
||||||
if (genDrawing || Date.now() < genIgnoreClickUntil || PRINT.dragging) return;
|
if (genDrawing || Date.now() < genIgnoreClickUntil || PRINT.dragging) return;
|
||||||
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
|
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.catch(() => 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', () => {
|
el('btnShare').addEventListener('click', () => {
|
||||||
@ -1644,15 +1595,15 @@ document.addEventListener('keydown', (e) => {
|
|||||||
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)
|
// 1–5 : onglets du panneau (Génération ignorée si masquée)
|
||||||
if (/^[1-4]$/.test(e.key)) {
|
if (/^[1-5]$/.test(e.key)) {
|
||||||
const tab = ['view', 'print', 'gen', 'share'][Number(e.key) - 1];
|
const tab = ['view', 'tile', 'print', 'gen', 'share'][Number(e.key) - 1];
|
||||||
if (panelTabs().includes(tab)) openTab(tab);
|
if (panelTabs().includes(tab)) openTab(tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
if (genDrawing) { genSetDrawing(false); return; }
|
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);
|
if (isPhone()) setSheet('closed'); else setCollapsed(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user