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:
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user