Harmoniser les styles de la carte : réglages communs, thèmes clair et sombre, composants uniques

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 17:39:04 +02:00
parent 75a562d6a4
commit b975ed272a
4 changed files with 279 additions and 168 deletions

View File

@ -6,6 +6,24 @@
const UI_VERSION = "__UI_VERSION__";
const LS_KEY = 'lidarMapView_v2';
const el = (id) => document.getElementById(id);
// --- stockage local ---------------------------------------------------------
// Tous les accès passent ici : en navigation privée (ou stockage bloqué),
// localStorage lève — la carte démarre alors sur ses défauts.
function lsGet(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw == null ? fallback : JSON.parse(raw);
} catch (e) { return fallback; }
}
function lsSet(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* privé */ }
}
function lsDel(key) {
try { localStorage.removeItem(key); } catch (e) { /* privé */ }
}
// --- fin stockage ------------------------------------------------------------
const SVG_EYE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
const SVG_EYE_OFF = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19M14.12 14.12a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
@ -26,6 +44,35 @@ const PREC_LEVELS = 16;
let META = null;
let STATE = null;
// --- thème ------------------------------------------------------------------
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
const THEME_LS = 'lidar-theme';
const THEME_NEXT = { auto: 'light', light: 'dark', dark: 'auto' };
const THEME_ICON = { auto: '◐', light: '☀', dark: '☾' };
const THEME_TITLE = { auto: 'Thème : suit le système', light: 'Thème : clair', dark: 'Thème : sombre' };
const darkQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
function themeIsDark() {
const t = document.documentElement.dataset.theme;
return t === 'dark' || (t !== 'light' && !!(darkQuery && darkQuery.matches));
}
function applyTheme(pref) {
const p = THEME_NEXT[pref] ? pref : 'auto';
if (p === 'auto') delete document.documentElement.dataset.theme;
else document.documentElement.dataset.theme = p;
const btn = el('btnTheme');
if (btn) { btn.textContent = THEME_ICON[p]; btn.title = THEME_TITLE[p] + ' (cliquer pour changer)'; }
if (typeof applyLayers === 'function' && STATE) applyLayers(); // fond OSM assombri
return p;
}
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
let THEME = applyTheme(lsGet(THEME_LS, 'auto'));
const LAYERS = new Map(); // clé → L.TileLayer
const map = L.map('map', {
@ -154,23 +201,20 @@ 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 : true },
dark: b.dark !== undefined ? !!b.dark : null },
};
}
function loadState(meta) {
const def = defaultState(meta);
let st = def;
try {
const raw = localStorage.getItem(LS_KEY);
if (raw) {
const s = JSON.parse(raw);
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 || {}) };
}
} catch (e) { st = def; }
const s = lsGet(LS_KEY, null);
if (s) {
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 || {}) };
}
// 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).
const hash = parseHash();
@ -183,7 +227,7 @@ function loadState(meta) {
}
function saveState() {
try { localStorage.setItem(LS_KEY, JSON.stringify(STATE)); } catch (e) { /* privé */ }
lsSet(LS_KEY, STATE);
}
// --- hash de vue ----------------------------------------------------------
@ -220,8 +264,9 @@ function viewHash(full) {
if (full) {
if (STATE.main) h += '&M=' + STATE.main;
h += '&P=' + STATE.mode + ':' + Math.round(STATE.precOpacity * 100);
const dark = STATE.base.dark === null || STATE.base.dark === undefined ? themeIsDark() : STATE.base.dark;
h += '&B=' + (STATE.base.on ? '1' : '0') + ':' + Math.round(STATE.base.opacity * 100)
+ ':' + (STATE.base.dark ? '1' : '0');
+ ':' + (dark ? '1' : '0');
}
return h;
}
@ -330,7 +375,8 @@ function applyLayers() {
else if (map.hasLayer(base)) map.removeLayer(base);
base.setOpacity(STATE.base.opacity);
const container = base.getContainer && base.getContainer();
if (container) container.classList.toggle('base-dark', STATE.base.dark);
const dark = STATE.base.dark === null || STATE.base.dark === undefined ? themeIsDark() : STATE.base.dark;
if (container) container.classList.toggle('base-dark', dark);
updateRose();
}
@ -468,7 +514,7 @@ function calageSection(c) {
function renderTileCard(data, ign) {
const regenBtn = (GEN.available && GEN.allowed)
? '<div class="card-foot"><button id="tileRegen" class="link-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="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>'
: '';
el('tileTitle').textContent = 'Dalle ' + data.col + ' · ' + data.row;
const bb = data.l93_bbox || [];
@ -583,8 +629,8 @@ function renderUseCard() {
const opts = META.layers.map(l =>
'<option value="' + l.key + '">' + l.label + '</option>').join('');
el('useBody').innerHTML =
'<div class="use-row"><select id="useLayer">' + opts + '</select>' +
'<button id="useCopy">Copier</button></div>' +
'<div class="row"><select id="useLayer" class="field">' + opts + '</select>' +
'<button id="useCopy" class="btn btn-primary">Copier</button></div>' +
'<div class="meta">URL XYZ (JOSM « TMS », QGIS « XYZ Tiles », uMap, MapLibre) :</div>' +
'<div class="mono" id="useUrl"></div>' +
'<div class="meta">TileJSON : <a id="useJson" href="#"></a></div>' +
@ -724,7 +770,7 @@ map.on('mousedown', (e) => {
genDrawStart = e.latlng;
if (genDrawRect) genDrawRect.remove();
genDrawRect = L.rectangle([genDrawStart, genDrawStart], {
pane: 'genDraw', color: '#4cc9f0', weight: 2, dashArray: '6 4',
pane: 'genDraw', color: cssVar('--gen-pending'), weight: 2, dashArray: '6 4',
fillOpacity: 0.05, interactive: false,
}).addTo(map);
});
@ -913,18 +959,16 @@ function printQuery(s, withTitle) {
function printSave() {
const s = printSettings();
if (PRINT.center) s.center = [PRINT.center.lat, PRINT.center.lng];
try { localStorage.setItem(PRINT_LS, JSON.stringify(s)); } catch (e) { /* privé */ }
lsSet(PRINT_LS, s);
}
function printLoad() {
try {
const s = JSON.parse(localStorage.getItem(PRINT_LS) || '{}');
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
['scale', 'printScale'], ['title', 'printTitle']]) {
if (s[k] != null) el(id).value = s[k];
}
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
} catch (e) { /* privé ou illisible : réglages par défaut */ }
const s = lsGet(PRINT_LS, {});
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
['scale', 'printScale'], ['title', 'printTitle']]) {
if (s[k] != null) el(id).value = s[k];
}
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
}
// Recadre sur le cadre en laissant libre la bande basse couverte par les
@ -945,7 +989,7 @@ function printFit() {
function printDraw(corners) {
PRINT.corners = corners;
if (PRINT.rect) PRINT.rect.setLatLngs(corners);
else PRINT.rect = L.polygon(corners, { pane: 'printFrame', color: '#ffd166', weight: 2,
else PRINT.rect = L.polygon(corners, { pane: 'printFrame', color: cssVar('--print'), weight: 2,
dashArray: '8 5', fill: false, interactive: false }).addTo(map);
if (PRINT.handle) { PRINT.handle.setLatLng(PRINT.center); return; }
PRINT.handle = L.marker(PRINT.center, {
@ -1173,7 +1217,7 @@ el('precOpacity').addEventListener('change', saveState);
el('btnReset').addEventListener('click', () => {
// Oublie l'état local et reprend la configuration servie par le serveur.
try { localStorage.removeItem(LS_KEY); } catch (e) { /* privé */ }
lsDel(LS_KEY);
fetch('api/map/meta').then(r => r.json()).then(meta => {
META = meta;
STATE = defaultState(META);
@ -1187,7 +1231,7 @@ el('baseToggle').addEventListener('click', () => {
saveState(); renderBaseRow(); applyLayers();
});
el('baseName').addEventListener('click', () => {
STATE.base.dark = !STATE.base.dark;
STATE.base.dark = !(STATE.base.dark ?? themeIsDark());
saveState(); applyLayers();
});
el('baseOpacity').addEventListener('input', (e) => {
@ -1196,6 +1240,13 @@ el('baseOpacity').addEventListener('input', (e) => {
base.setOpacity(STATE.base.opacity);
});
el('baseOpacity').addEventListener('change', saveState);
el('btnTheme').addEventListener('click', () => {
THEME = applyTheme(THEME_NEXT[THEME]);
lsSet(THEME_LS, THEME);
});
if (darkQuery && darkQuery.addEventListener) {
darkQuery.addEventListener('change', () => { if (THEME === 'auto' && STATE) applyLayers(); });
}
el('zoomIn').addEventListener('click', () => map.zoomIn(1));
el('zoomOut').addEventListener('click', () => map.zoomOut(1));
el('zoomFit').addEventListener('click', fitData);