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:
Antoine Jacquin
2026-09-27 20:22:07 +02:00
parent 0df93c2f00
commit 90663405f7
6 changed files with 112 additions and 188 deletions

View File

@ -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);
}
});