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:
@ -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