Remplacer la fiche de dalle flottante par une bulle et une fiche détaillée dans le panneau

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 18:17:18 +02:00
parent a9196d0c97
commit 45455aec93
5 changed files with 132 additions and 40 deletions

View File

@ -467,6 +467,17 @@ function fmtDate(v, withTime) {
return d.toLocaleString('fr-FR', opt);
}
function fmtDensity(q) {
if (!q || q.ground_density == null) return 'non renseignée';
return q.ground_density.toFixed(q.ground_density < 10 ? 1 : 0).replace('.', ',') + ' pts sol/m²';
}
function fmtAcq(q) {
if (!q || !q.acq_start) return 'non renseignée';
return q.acq_end && q.acq_end.slice(0, 10) !== q.acq_start.slice(0, 10)
? fmtDate(q.acq_start) + ' → ' + fmtDate(q.acq_end) : fmtDate(q.acq_start);
}
function kv(rows) {
return '<dl class="info-kv">' + rows.filter(r => r[1] != null && r[1] !== '')
.map(r => '<dt>' + r[0] + '</dt><dd>' + r[1] + '</dd>').join('') + '</dl>';
@ -523,7 +534,7 @@ function calageSection(c) {
function renderTileCard(data, ign) {
const regenBtn = (GEN.available && GEN.allowed)
? '<div class="card-foot"><button id="tileRegen" class="btn" title="Ajouter cette dalle à la sélection de génération (téléchargement IGN + traitement)">↻ ' + (data.found ? 'Régénérer' : 'Générer') + ' cette dalle</button></div>'
? '<div class="row"><button id="tileRegen" class="btn" title="Ajouter cette dalle à la sélection de génération (téléchargement IGN + traitement)">↻ ' + (data.found ? 'Régénérer' : 'Générer') + ' cette dalle</button></div>'
: '';
el('tileTitle').textContent = 'Dalle ' + data.col + ' · ' + data.row;
const bb = data.l93_bbox || [];
@ -551,7 +562,6 @@ function renderTileCard(data, ign) {
el('tileBody').innerHTML = html + regenBtn;
const btn = el('tileRegen');
if (btn) btn.addEventListener('click', genAddCellFromQuery);
el('tilecard').hidden = false;
}
function showTileInfo(data, latlng) {
@ -559,10 +569,8 @@ function showTileInfo(data, latlng) {
if (!data) {
el('tileTitle').textContent = 'Dalle';
el('tileBody').innerHTML = '<div class="meta">Serveur injoignable : réessayez dans un instant.</div>';
el('tilecard').hidden = false;
return;
}
selectTileOutline(data);
renderTileCard(data, null);
// Fiche IGN à part : la sélection s'affiche sans attendre le catalogue.
const key = data.col + '_' + data.row;
@ -573,6 +581,54 @@ function showTileInfo(data, latlng) {
.then(ign => { if (showTileInfo.current === key) renderTileCard(data, ign); });
}
let tilePopup = null;
let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
function showTilePopup(data, latlng) {
lastTileQuery = latlng || lastTileQuery;
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);
showTilePopup.data = data;
const btn = el('tilePopDetails');
if (btn) btn.addEventListener('click', openTileDetails);
tilePopup.on('remove', () => { if (el('tileView').hidden && tileSel) { map.removeLayer(tileSel); tileSel = null; } });
}
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;
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
showTileInfo(data, lastTileQuery);
}
// 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) {
el('tileView').hidden = true;
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
openTab(tileReturnTab || 'view');
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),
@ -761,8 +817,8 @@ 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 ».');
el('tilecard').hidden = true;
})
.catch(msg => toast('Échec : ' + msg.message, 6000));
}
@ -949,7 +1005,8 @@ el('genQueueClear').addEventListener('click', () =>
// se déplace librement autour, on le fait glisser à la souris ou au doigt.
// Géométrie exacte calculée par le serveur (projection Lambert 93).
const PRINT_LS = 'lidar-print';
const PRINT = { on: false, rect: null, handle: null, seq: 0, center: null, corners: null, drag: null };
const PRINT = { on: false, rect: null, handle: null, seq: 0, center: null, corners: null, drag: null,
dragging: false };
map.createPane('printFrame').style.zIndex = '690';
function printSettings() {
@ -1026,6 +1083,7 @@ function printRefreshFrame(fit) {
// stables pendant le défilement automatique),
// géométrie exacte redemandée au serveur au relâchement.
function printDragStart() {
PRINT.dragging = true;
PRINT.drag = { start: map.latLngToLayerPoint(PRINT.handle.getLatLng()),
corners: PRINT.corners.map(c => map.latLngToLayerPoint(c)) };
}
@ -1043,6 +1101,7 @@ function printDragEnd() {
PRINT.corners = PRINT.rect.getLatLngs()[0];
printSave();
printRefreshFrame();
setTimeout(() => { PRINT.dragging = false; }, 300);
}
function printCenterHere() {
@ -1140,7 +1199,9 @@ function panelRender() {
b.hidden = !tabs.includes(b.dataset.tab);
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
});
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
if (el('tileView').hidden) {
for (const t of ['view', '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();
@ -1157,6 +1218,7 @@ function panelRender() {
function openTab(tab) {
if (!panelTabs().includes(tab)) return;
el('tileView').hidden = true;
const before = PANEL.sheet;
// Onglet actif retouché sur téléphone : le volet change de hauteur.
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
@ -1216,9 +1278,6 @@ window.addEventListener('resize', panelRender); // hauteur du volet en vh (rot
grip.addEventListener('pointercancel', end);
})();
// Fiche de dalle fermée par Échap (provisoire : la Task 4 la remplace).
function closeTileViews() { if (el('tilecard').hidden) return false; el('tilecard').hidden = true; return true; }
// --- démarrage ------------------------------------------------------------
function fitData() {
if (META.bounds) {
@ -1258,7 +1317,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
panelRender();
if (show) { genUpdateStatus(d); genPollNow(); }
}).catch(() => {});
}).catch(() => toast('Serveur de tuiles injoignable.', 10000));
}).catch(() => { toast('Serveur de tuiles injoignable.', 10000); panelRender(); });
let applyingHash = false;
window.addEventListener('hashchange', () => {
@ -1278,17 +1337,15 @@ window.addEventListener('hashchange', () => {
map.on('moveend zoomend', saveHashSoon);
map.on('click', (e) => {
if (genDrawing || Date.now() < genIgnoreClickUntil) return;
// Pas de bulle 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 => showTileInfo(d, e.latlng));
.then(d => showTilePopup(d, e.latlng));
});
el('tileClose').addEventListener('click', () => {
el('tilecard').hidden = true;
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
});
el('btnShare').addEventListener('click', () => {
const url = location.origin + location.pathname + viewHash(true);
history.replaceState(null, '', url);