Corriger la revue finale : onglets non tronqués, fiche de dalle à jour, clics et lien fiables
Onglet Dalle libellé fixe (col·row dans le title), Affichage/Export PDF/ Génération raccourcis pour tenir dans le panneau ; fiche de dalle rechargée après un run sans désélection et dès que /api/status révèle le bouton Régénérer ; double-clic et réponses en retard ignorés sur la sélection ; curseur d'intensité visible hors mode précision ; lien complet sans &I remis à 1× ; applyCompareClip sort tôt hors mode Comparer ; docs/MAPS.md et AGENTS.md à jour (modes Comparer, &I=, « Comment lire »). Co-Authored-By: Claude Sonnet 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 / 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`.
|
||||
- **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. Intensité du relief (`&I=`, contraste 0,5–2×, défaut 1×) : réglage de confort personnel mémorisé dans `lidarMapView_v2.intensity` et partagé dans le lien, jamais figé comme défaut serveur (`★ Définir par défaut` l'ignore). 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).
|
||||
|
||||
49
docs/MAPS.md
49
docs/MAPS.md
@ -160,25 +160,39 @@ chacun, ou la touche **P** pour passer au suivant :
|
||||
- **Précision** — la densité seule, en 16 gris (échelle log fixe : niveau k à
|
||||
partir de 0,25 × 2^(k/2) pts/m², noir ≤ 0,35 ou aucun point, blanc ≥ 45) ;
|
||||
se lit comme une carte de fiabilité géométrique ;
|
||||
- **Les deux** — la précision en « produit » (`mix-blend-mode: multiply`) sur
|
||||
le relief, opacité réglable (60 % par défaut) : les zones où le relief est
|
||||
interpolé s'assombrissent sans masquer le relief.
|
||||
- **Comparer** — une barre glissante (souris ou doigt) sépare deux couches
|
||||
choisies dans deux menus (relief, précision ou fond OSM seul) de part et
|
||||
d'autre du curseur ; la même couche des deux côtés n'y ajoute aucune
|
||||
découpe.
|
||||
|
||||
L'onglet Affichage porte aussi une **intensité du relief** (curseur 0,5×–2×,
|
||||
1× par défaut) : un contraste de confort posé sur le conteneur de la couche
|
||||
affichée (`contrast()` CSS), mémorisé et partagé dans le lien (`&I=`, écrit
|
||||
seulement si ≠ 1×) mais jamais figé comme défaut serveur ni appliqué au PDF
|
||||
exporté (qui garde le rendu standard). Un bloc **Comment lire la carte**,
|
||||
repliable, reprend pour la ou les couches affichées le texte de lecture de
|
||||
`VIZ_LEGENDS` (aussi servi par `/api/map/meta` et le TileJSON).
|
||||
|
||||
La légende de la précision (16 paliers, info-bulle en pts/m² sur chaque
|
||||
palier) s'affiche dès que la précision est visible. Les couches LiDAR vivent
|
||||
dans un conteneur **isolé** (`isolation: isolate`) : le produit n'agit que sur
|
||||
le relief, jamais sur le fond de carte.
|
||||
palier) s'affiche dès que la précision est visible, seule ou d'un côté de la
|
||||
barre Comparer. Les couches LiDAR vivent dans un conteneur **isolé**
|
||||
(`isolation: isolate`) : la barre ne découpe que les calques LiDAR, jamais le
|
||||
fond de carte.
|
||||
|
||||
Le lien de partage transporte la couche principale, le mode et l'opacité :
|
||||
`#z/lat/lng&M=relief_oriente&P=both:60&B=1:85:1`. Les anciens liens de la pile
|
||||
(`&L=…`) s'ouvrent sans erreur sur l'état courant.
|
||||
Le lien de partage transporte la couche principale, le mode, la comparaison et
|
||||
l'intensité :
|
||||
`#z/lat/lng&M=relief_oriente&P=compare&C=relief:precision:30&I=1.4&B=1:85:1`
|
||||
(`&C=gauche:droite:position%` seulement en mode Comparer). Les anciens liens
|
||||
de la pile (`&L=…`) et de l'ancien mode « les deux » (`&P=both:opacité`,
|
||||
opacité alors ignorée) s'ouvrent sans erreur, en mode relief.
|
||||
|
||||
### Figer la configuration
|
||||
|
||||
Le bouton **★ Définir par défaut** enregistre l'affichage courant — couche
|
||||
principale, mode, opacité de la précision, fond de carte — dans
|
||||
`output/.map-defaults.json`. Tout navigateur sans réglage local part alors de
|
||||
cette configuration ; **↺ Réinitialiser** oublie l'état local et y revient.
|
||||
principale, mode, fond de carte — dans `output/.map-defaults.json` (jamais
|
||||
l'intensité, réglage de confort propre à chaque navigateur). Tout navigateur
|
||||
sans réglage local part alors de cette configuration ; **↺ Réinitialiser**
|
||||
oublie l'état local et y revient.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8975/api/map/defaults # configuration servie
|
||||
@ -186,11 +200,10 @@ curl -X DELETE http://localhost:8975/api/map/defaults # retour au registre
|
||||
```
|
||||
|
||||
Sans fichier enregistré, les défauts viennent du registre du pipeline
|
||||
(`DEFAULT_VIZ`, `PRECISION_VIZ`, `DEFAULT_VIEW_MODE`,
|
||||
`DEFAULT_PRECISION_OPACITY` dans `index.py`). Les valeurs reçues sont
|
||||
filtrées : couche inconnue (ou la précision elle-même) refusée comme
|
||||
principale, mode validé, opacités bornées à 0–1. Un fichier de l'ancienne pile
|
||||
(`order`/`on`/`blend`) est ignoré, sauf le fond.
|
||||
(`DEFAULT_VIZ`, `PRECISION_VIZ`, `DEFAULT_VIEW_MODE` dans `index.py`). Les
|
||||
valeurs reçues sont filtrées : couche inconnue (ou la précision elle-même)
|
||||
refusée comme principale, mode validé, opacité du fond bornée à 0–1. Un
|
||||
fichier de l'ancienne pile (`order`/`on`/`blend`) est ignoré, sauf le fond.
|
||||
|
||||
## Export PDF (planche d'impression terrain)
|
||||
|
||||
@ -203,7 +216,7 @@ précédente si elle est encore visible), la carte zoome pour le montrer en
|
||||
entier au-dessus du panneau, puis on navigue librement sans qu'il bouge. On le
|
||||
déplace en faisant glisser sa **poignée ✥** (souris ou doigt) ; au relâchement,
|
||||
la géométrie Lambert 93 exacte est recalculée (`GET /api/export/frame`) — un
|
||||
relâchement suivi d'un clic immédiat n'ouvre pas la bulle de dalle (garde de
|
||||
relâchement suivi d'un clic immédiat ne sélectionne pas de dalle (garde de
|
||||
300 ms). « ⌖ Centrer ici » le ramène au centre de la vue, « ⤢ Voir le cadre »
|
||||
zoome dessus ; changer de format, d'orientation ou d'échelle recadre la vue.
|
||||
Les réglages et la position du cadre sont mémorisés dans `localStorage` du
|
||||
|
||||
@ -1694,3 +1694,96 @@ def test_ui_panel_restores_tab_and_ignores_collapse_on_phone():
|
||||
assert "PANEL.collapsed && !isPhone()" in render
|
||||
assert "toggle('collapsed', PANEL.collapsed)" not in render
|
||||
assert "toggle('panel-collapsed', PANEL.collapsed)" not in render
|
||||
|
||||
|
||||
def test_ui_tile_tab_label_stays_dalle_col_row_in_title():
|
||||
"""Revue finale (finding 2) : 5 onglets dans 340 px — le libellé de
|
||||
l'onglet Dalle reste toujours « Dalle » (jamais tronqué), col · row va
|
||||
dans le title de l'onglet (survol) et #tileTitle (déjà dans la fiche).
|
||||
Affichage/Export PDF/Génération raccourcis pour tenir, Partager gardé."""
|
||||
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
|
||||
assert '<span class="tab-lbl">Vue</span>' in _MAP_HTML
|
||||
assert '<span class="tab-lbl">PDF</span>' in _MAP_HTML
|
||||
assert '<span class="tab-lbl">Générer</span>' in _MAP_HTML
|
||||
assert '<span class="tab-lbl" id="tileTabLbl">Dalle</span>' in _MAP_HTML
|
||||
assert 'data-tab="share"' in _MAP_HTML and '>Partager</span>' in _MAP_HTML
|
||||
# Le title complet (survol) reste explicite malgré le libellé court
|
||||
assert 'title="Affichage (1)"' in _MAP_HTML
|
||||
assert 'title="Export PDF (3)"' in _MAP_HTML
|
||||
assert 'title="Génération (4)"' in _MAP_HTML
|
||||
assert "function tileTabTitle(" in _MAP_JS
|
||||
assert "textContent = tileTabLabel" not in _MAP_JS # plus de libellé dynamique
|
||||
assert "button[data-tab=\"tile\"]').title = tileTabTitle()" in _MAP_JS
|
||||
|
||||
|
||||
def test_ui_tile_card_refetched_after_gen_run():
|
||||
"""Finding 3 : la fiche de dalle sélectionnée avant/pendant un run devient
|
||||
périmée (résolution, date, couches...) — genAfterRun la recharge par
|
||||
api/map/tile sans jamais désélectionner (pas de clearTileSelection)."""
|
||||
from lidar_pipeline.mapui import _MAP_JS
|
||||
fn = _MAP_JS[_MAP_JS.index("function genAfterRun("):_MAP_JS.index("function genUpdateStatus(")]
|
||||
assert "SELECTED_TILE" in fn and "lastTileQuery" in fn
|
||||
assert "api/map/tile?lat=" in fn
|
||||
assert "selectTile(d, lastTileQuery)" in fn
|
||||
assert "clearTileSelection" not in fn
|
||||
|
||||
|
||||
def test_ui_double_click_and_stale_click_response_ignored():
|
||||
"""M1/M2 : un double-clic (zoom Leaflet, deux clics simples dont le
|
||||
second a detail > 1) ne doit pas re-basculer la sélection posée par le
|
||||
premier ; une réponse arrivée après un clic plus récent (jeton de
|
||||
séquence) est ignorée plutôt que d'écraser l'état."""
|
||||
from lidar_pipeline.mapui import _MAP_JS
|
||||
click = _MAP_JS[_MAP_JS.index("map.on('click'"):]
|
||||
body = click[:click.index("});") + 3]
|
||||
assert "e.originalEvent && e.originalEvent.detail > 1" in body
|
||||
assert "let tileClickSeq = 0;" in _MAP_JS
|
||||
assert "const seq = ++tileClickSeq;" in body
|
||||
assert "if (seq !== tileClickSeq) return;" in body
|
||||
|
||||
|
||||
def test_ui_regen_button_appears_when_gen_status_resolves_late():
|
||||
"""M3 : une dalle sélectionnée avant que /api/status ne réponde ne doit
|
||||
pas rester sans bouton Régénérer — la fiche est ré-émise dès que GEN est
|
||||
connu, dans le même bloc qui pose GEN.available/allowed."""
|
||||
from lidar_pipeline.mapui import _MAP_JS
|
||||
boot = _MAP_JS[_MAP_JS.index("fetch('api/map/meta').then(r => r.json()).then(meta => {"):]
|
||||
status_block = boot[:boot.index("}).catch(() => {\n toast('Serveur de tuiles injoignable.'")]
|
||||
assert "GEN.available = !!d.available;" in status_block
|
||||
assert "if (SELECTED_TILE) renderTabTile();" in status_block
|
||||
|
||||
|
||||
def test_ui_intensity_row_visible_outside_precision_block():
|
||||
"""M4 (liaison de spec) : #intensityRow est un frère de #precBlock, pas un
|
||||
enfant — visible dès l'onglet Affichage même sans couche de précision,
|
||||
masqué seulement en mode précision (renderPanel)."""
|
||||
from lidar_pipeline.mapui import _MAP_HTML
|
||||
assert '<div id="precBlock" hidden>' in _MAP_HTML
|
||||
# #intensityRow ouvre juste après la fermeture de #precBlock (frère, pas
|
||||
# enfant) : rien de precBlock (modeSeg/compareCtl/precLegend) ne le suit.
|
||||
after_prec_close = _MAP_HTML[_MAP_HTML.index('<div class="prec-legend" id="precLegend"'):]
|
||||
close_idx = after_prec_close.index("</div>\n </div>")
|
||||
between = after_prec_close[:close_idx]
|
||||
assert 'id="intensityRow"' not in between
|
||||
assert '<div class="row" id="intensityRow">' in after_prec_close[close_idx:close_idx + 200]
|
||||
|
||||
|
||||
def test_ui_full_shared_link_without_intensity_resets_to_one():
|
||||
"""M5 : un lien complet (couche ou mode dans le hash) sans &I signifie
|
||||
1× (viewHash n'écrit &I que si ≠ 1), jamais l'intensité restée dans le
|
||||
localStorage de ce navigateur."""
|
||||
from lidar_pipeline.mapui import _MAP_JS
|
||||
load = _MAP_JS[_MAP_JS.index("function loadState("):_MAP_JS.index("function saveState(")]
|
||||
assert "else if (hash.main || hash.view) st.intensity = 1;" in load
|
||||
|
||||
|
||||
def test_ui_compare_clip_skips_when_already_off():
|
||||
"""M7 : applyCompareClip sort tôt quand le mode n'est pas Comparer et que
|
||||
les calques sont déjà sans découpe — évite de relire/réécrire le style de
|
||||
tous les calques à chaque déplacement de carte alors que Comparer n'a
|
||||
jamais été ouvert."""
|
||||
from lidar_pipeline.mapui import _MAP_JS
|
||||
clip = _MAP_JS[_MAP_JS.index("function applyCompareClip("):_MAP_JS.index("(function compareDrag(")]
|
||||
assert "let compareClipsCleared = true;" in _MAP_JS
|
||||
assert "if (!on && compareClipsCleared) { el('compareBar').hidden = true; return; }" in clip
|
||||
assert "compareClipsCleared = !on;" in clip
|
||||
|
||||
@ -16,10 +16,10 @@
|
||||
<div id="sheetGrip" class="sheet-grip" title="Glisser pour agrandir ou réduire"><span></span></div>
|
||||
<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="view" title="Affichage (1)"><span class="tab-ico">◧</span><span class="tab-lbl">Vue</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="print" title="Export PDF (3)"><span class="tab-ico">⎙</span><span class="tab-lbl">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érer</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>
|
||||
@ -45,12 +45,6 @@
|
||||
<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="compare" title="Comparer deux couches de part et d'autre d'une barre glissante (touche P : mode suivant)">Comparer</button>
|
||||
</div>
|
||||
<div class="row" id="intensityRow">
|
||||
<label for="reliefIntensity" class="hint">Intensité du relief</label>
|
||||
<input type="range" id="reliefIntensity" min="0.5" max="2" step="0.1" value="1"
|
||||
title="Contraste du relief (1 = rendu standard, le PDF garde le rendu standard)">
|
||||
<span class="panel-stat" id="reliefIntensityVal">1×</span>
|
||||
</div>
|
||||
<div class="row" id="compareCtl" hidden>
|
||||
<select id="compareLeft" class="field" title="Couche à gauche de la barre"></select>
|
||||
<span class="hint">⇆</span>
|
||||
@ -61,6 +55,12 @@
|
||||
<div class="prec-ticks"><span>≤ 0,35</span><span>4</span><span>≥ 45 pts/m²</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="intensityRow">
|
||||
<label for="reliefIntensity" class="hint">Intensité du relief</label>
|
||||
<input type="range" id="reliefIntensity" min="0.5" max="2" step="0.1" value="1"
|
||||
title="Contraste du relief (1 = rendu standard, le PDF garde le rendu standard)">
|
||||
<span class="panel-stat" id="reliefIntensityVal">1×</span>
|
||||
</div>
|
||||
<div class="legend" id="legendBlock" title="Relief orienté : la teinte donne l'orientation de la pente, la clarté le relief local">
|
||||
<canvas id="roseCanvas" width="96" height="96"></canvas>
|
||||
<div class="rose-cap">Pente orientée vers…<br><span>clair = bosse · sombre = creux</span></div>
|
||||
|
||||
@ -267,6 +267,10 @@ function loadState(meta) {
|
||||
if (hash.compare) st.compare = hash.compare;
|
||||
if (hash.base) st.base = hash.base;
|
||||
if (hash.intensity != null) st.intensity = hash.intensity;
|
||||
// Lien complet (couche ou mode dans le hash) sans &I : l'export du lien
|
||||
// n'écrit &I que si ≠ 1 (viewHash), donc son absence signifie 1×, jamais
|
||||
// l'intensité de confort restée dans le localStorage de ce navigateur.
|
||||
else if (hash.main || hash.view) st.intensity = 1;
|
||||
}
|
||||
return st;
|
||||
}
|
||||
@ -463,9 +467,15 @@ function compareBounds() {
|
||||
// à chaque déplacement. Même couche des deux côtés : aucune découpe.
|
||||
// STATE.compare.pos garde la valeur demandée (lien, glisser) ; la séparation
|
||||
// affichée, barre et découpe ensemble, est bornée à compareBounds().
|
||||
// Mode différent de « compare » et calques déjà sans découpe : rien à faire.
|
||||
// Sans cette garde, chaque déplacement de carte (map.on('move', …)) relit et
|
||||
// réécrit le style de tous les calques pour ne poser que des clip-path vides,
|
||||
// à chaque frame, même quand Comparer n'a jamais été ouvert.
|
||||
let compareClipsCleared = true;
|
||||
function applyCompareClip() {
|
||||
if (!STATE) return;
|
||||
const on = effectiveMode() === 'compare';
|
||||
if (!on && compareClipsCleared) { el('compareBar').hidden = true; return; }
|
||||
el('compareBar').hidden = !on;
|
||||
const size = map.getSize();
|
||||
let x = size.x * (on ? STATE.compare.pos : 50) / 100;
|
||||
@ -485,6 +495,7 @@ function applyCompareClip() {
|
||||
}
|
||||
c.style.clipPath = clip;
|
||||
}
|
||||
compareClipsCleared = !on;
|
||||
if (on) el('compareBar').style.left = (bounds.left + x) + 'px';
|
||||
}
|
||||
map.on('move', applyCompareClip);
|
||||
@ -729,15 +740,17 @@ function showTileInfo(data, latlng) {
|
||||
|
||||
let SELECTED_TILE = null; // données de la dalle sélectionnée (ou null)
|
||||
|
||||
// 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';
|
||||
// Le libellé de l'onglet reste « Dalle » (5 onglets dans 340 px : un libellé
|
||||
// variable déborderait) ; col · row va dans le title de l'onglet (survol) et
|
||||
// dans #tileTitle, toujours lisible dans la fiche elle-même.
|
||||
function tileTabTitle() {
|
||||
return (SELECTED_TILE ? 'Dalle ' + SELECTED_TILE.col + ' · ' + SELECTED_TILE.row : 'Dalle') + ' (2)';
|
||||
}
|
||||
|
||||
// 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();
|
||||
document.querySelector('button[data-tab="tile"]').title = tileTabTitle();
|
||||
el('tileHint').hidden = !!SELECTED_TILE;
|
||||
el('tileCard').hidden = !SELECTED_TILE;
|
||||
if (SELECTED_TILE) showTileInfo(SELECTED_TILE, lastTileQuery);
|
||||
@ -1067,6 +1080,14 @@ function genAfterRun() {
|
||||
renderUseCard();
|
||||
}).catch(() => {});
|
||||
fetch('api/map/background', { method: 'POST' }).catch(() => {});
|
||||
// Fiche de dalle restée sélectionnée pendant le run : périmée (résolution,
|
||||
// date de génération, couches…) — on la recharge sans jamais désélectionner.
|
||||
if (SELECTED_TILE && lastTileQuery) {
|
||||
fetch('api/map/tile?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.catch(() => null)
|
||||
.then(d => { if (d) selectTile(d, lastTileQuery); });
|
||||
}
|
||||
}
|
||||
|
||||
function genUpdateStatus(d) {
|
||||
@ -1488,6 +1509,9 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
||||
GEN.allowed = d.regen_allowed !== false;
|
||||
const show = GEN.available && GEN.allowed;
|
||||
panelRender();
|
||||
// La fiche de dalle a pu s'afficher avant que /api/status ne réponde
|
||||
// (bouton « Régénérer » absent, GEN encore inconnu) : la ré-émettre.
|
||||
if (SELECTED_TILE) renderTabTile();
|
||||
if (show) { genUpdateStatus(d); genPollNow(); }
|
||||
}).catch(() => {});
|
||||
}).catch(() => {
|
||||
@ -1516,14 +1540,22 @@ window.addEventListener('hashchange', () => {
|
||||
});
|
||||
|
||||
map.on('moveend zoomend', saveHashSoon);
|
||||
// Jeton de séquence : une réponse arrivée après un clic plus récent (double
|
||||
// clic, requête lente) ne doit jamais écraser l'état de la sélection.
|
||||
let tileClickSeq = 0;
|
||||
map.on('click', (e) => {
|
||||
// Pas de sélection pendant un tracé de zone ni juste après le relâchement
|
||||
// de la poignée du cadre d'export.
|
||||
// de la poignée du cadre d'export. Un double-clic (zoom Leaflet) arrive ici
|
||||
// comme deux clics simples : le second (detail > 1) ne doit pas rebasculer
|
||||
// la sélection qui vient d'être posée par le premier.
|
||||
if (genDrawing || Date.now() < genIgnoreClickUntil || PRINT.dragging) return;
|
||||
if (e.originalEvent && e.originalEvent.detail > 1) return;
|
||||
const seq = ++tileClickSeq;
|
||||
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.catch(() => null)
|
||||
.then(d => {
|
||||
if (seq !== tileClickSeq) return; // clic plus récent déjà traité
|
||||
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é.
|
||||
|
||||
Reference in New Issue
Block a user