Corriger la revue stage1 de la refonte : fiche de dalle, thème du fond, cadre d'export, tokens de couleur

Retour/Échap depuis la fiche détaillée ne retouche plus la hauteur du volet
téléphone (openTab remplacé par un rétablissement direct de l'onglet) ; le
fond OSM suit le thème sauf choix explicite (migration du localStorage et des
défauts serveur en v3, plus de dark:true hérité de l'ex-webapp) et part du
bon thème dès sa création (plus de flash sombre) ; l'onglet Export mémorisé ne
recadre plus la vue au premier rendu (lien partagé préservé) et referme son
cadre pendant que la fiche détaillée est ouverte ; la qualité affichée suit le
nom réel de la dalle rendue ; un second clic referme la fiche au lieu de
déplacer son contour derrière elle ; couleurs en dur restantes tokenisées
(--tile-sel, --gps, .gen-log) et lisibles en thème clair ; localStorage
corrompu (lidar-panel, lidar-print) n'y casse plus l'interface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 18:52:30 +02:00
parent 56c9fa0ccd
commit 72b10cd228
5 changed files with 249 additions and 25 deletions

View File

@ -44,6 +44,10 @@ const PREC_LEVELS = 16;
let META = null;
let STATE = null;
// Vrai jusqu'au premier rendu du panneau au démarrage : évite qu'un onglet
// Export mémorisé recadre la vue par-dessus un lien partagé ou l'URL initiale
// (cf. printSetOpen).
let BOOTSTRAPPING = true;
// --- thème ------------------------------------------------------------------
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
@ -174,7 +178,10 @@ ZoomAnimTileLayer.addInitHook(function () {
});
const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: 'base-dark',
// Classe posée dès la création d'après le thème courant (pas un « dark » figé) :
// sinon le fond flashe sombre en thème clair le temps que /api/map/meta réponde
// (et pour de bon si l'appel échoue). applyLayers() la corrige ensuite.
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: themeIsDark() ? 'base-dark' : '',
// Sinon _setView recrée un niveau en cours d'animation malgré noUpdate.
updateWhenZooming: false,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
@ -210,7 +217,9 @@ function defaultState(meta) {
precOpacity: clamp01(meta.default_precision_opacity, 0.6),
base: { on: b.on !== undefined ? !!b.on : true,
opacity: b.opacity !== undefined ? Number(b.opacity) : 0.85,
dark: b.dark !== undefined ? !!b.dark : null },
// null = suit le thème (choix explicite seulement si b.dark est déjà
// un booléen posé côté serveur).
dark: b.dark == null ? null : !!b.dark },
};
}
@ -219,10 +228,15 @@ function loadState(meta) {
let st = def;
const s = lsGet(LS_KEY, null);
if (s) {
// Ancien format (avant v3) : l'ex-webapp persistait base.dark:true à chaque
// sauvegarde, jamais un vrai choix de l'utilisateur — ignoré ici (le fond
// suit alors le thème) ; v3 seulement écrit un dark explicite (voir saveState).
const sBase = Object.assign({}, s.base || {});
if (s.v !== 3) delete sBase.dark;
st = { main: validMain(meta, s.main) || def.main,
mode: validMode(s.mode) || def.mode,
precOpacity: clamp01(s.precOpacity, def.precOpacity),
base: Object.assign({}, def.base, s.base || {}) };
base: Object.assign({}, def.base, sBase) };
}
// Lien partagé : prime sur l'état local (les anciens liens &L=… de la
// pile de couches sont ignorés, la vue s'ouvre alors sur l'état courant).
@ -236,7 +250,9 @@ function loadState(meta) {
}
function saveState() {
lsSet(LS_KEY, STATE);
// v: 3 marque un format où base.dark, quand posé, est un vrai choix de
// l'utilisateur (voir loadState) — jamais mélangé à STATE lui-même.
lsSet(LS_KEY, Object.assign({ v: 3 }, STATE));
}
// --- hash de vue ----------------------------------------------------------
@ -488,7 +504,7 @@ function selectTileOutline(data) {
if (!data || !data.corners) return;
// Panneau dédié au-dessus de la pile LiDAR : dans l'overlayPane par défaut
// (z-index 400, comme lidarStack), le contour passait sous les tuiles.
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: '#ffd24a', weight: 2,
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: cssVar('--tile-sel'), weight: 2,
fill: false, dashArray: '6 4',
interactive: false }).addTo(map);
}
@ -586,6 +602,10 @@ let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
function showTilePopup(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>'
@ -614,6 +634,10 @@ function openTileDetails() {
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);
@ -627,12 +651,22 @@ function hideTileView() {
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
}
// 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();
panelRender();
}
// 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) {
hideTileView();
openTab(tileReturnTab || 'view');
restoreTabAfterDetails();
return true;
}
return false;
@ -1039,12 +1073,18 @@ function printSave() {
}
function printLoad() {
const s = lsGet(PRINT_LS, {});
const raw = lsGet(PRINT_LS, {});
// Valeur corrompue (clé partagée avec une autre appli, quota dépassé
// pendant l'écriture…) : objet non conforme ignoré plutôt que planté.
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
['scale', 'printScale'], ['title', 'printTitle']]) {
if (s[k] != null) el(id).value = s[k];
if (typeof s[k] === 'string') el(id).value = s[k];
}
if (Array.isArray(s.center) && s.center.length === 2 &&
s.center.every((n) => typeof n === 'number' && Number.isFinite(n))) {
PRINT.center = L.latLng(s.center);
}
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
}
// Recadre sur le cadre en laissant libre la place du panneau (colonne à
@ -1122,13 +1162,16 @@ function printCenterHere() {
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
function printSetOpen(on) {
// fit : recadrer la vue sur le cadre une fois posé — faux au tout premier
// rendu (onglet Export mémorisé) pour ne pas écraser le zoom d'un lien
// partagé ou de l'URL au chargement.
function printSetOpen(on, fit) {
if (PRINT.on === on) return;
PRINT.on = on;
if (on) {
// cadre gardé s'il est encore dans la vue, sinon posé au centre
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
printRefreshFrame(true);
printRefreshFrame(fit !== false);
} else {
if (PRINT.rect) PRINT.rect.remove();
if (PRINT.handle) PRINT.handle.remove();
@ -1174,13 +1217,25 @@ el('printTitle').addEventListener('change', printSave);
printLoad();
document.addEventListener('panel:tab', (e) => {
printSetOpen(e.detail.visible === 'print');
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
});
// --- panneau à onglets --------------------------------------------------------
const PANEL_LS = 'lidar-panel';
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, lsGet(PANEL_LS, {}));
// Ne retient que les clés connues, du bon type et (pour sheet) dans la liste
// valide : une valeur corrompue casserait sinon le CSS (panel.dataset.sheet
// inconnu de map.css) ou l'onglet affiché.
function _sanitizedPanelLS() {
const raw = lsGet(PANEL_LS, {});
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
const out = {};
if (typeof s.tab === 'string') out.tab = s.tab;
if (typeof s.collapsed === 'boolean') out.collapsed = s.collapsed;
if (['closed', 'half', 'full'].includes(s.sheet)) out.sheet = s.sheet;
return out;
}
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, _sanitizedPanelLS());
// want = onglet choisi (mémorisé) ; tab = onglet affiché. Génération encore
// inconnue au démarrage (api/status) : Affichage en attendant, puis l'onglet
// choisi revient dès qu'il est proposé.
@ -1314,6 +1369,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
renderPanel();
renderBaseRow();
panelRender();
BOOTSTRAPPING = false;
renderUseCard();
genFillViz();
// Droits de génération : boutons affichés si le serveur peut générer
@ -1449,7 +1505,7 @@ el('gps').addEventListener('click', () => {
map.setView(ll, Math.max(map.getZoom(), 16));
if (window._gpsMarker) window._gpsMarker.remove();
window._gpsMarker = L.circleMarker(ll, {
radius: 6, color: '#39d98a', fillColor: '#39d98a', fillOpacity: 0.9
radius: 6, color: cssVar('--gps'), fillColor: cssVar('--gps'), fillOpacity: 0.9
}).addTo(map);
}, (err) => {
btn.classList.remove('busy');