Ajouter des préconfigurations de couches (couches, transparences, ordre)

L'onglet Couches propose un sélecteur de presets : cinq presets
intégrés (Relief, Pentes, Cavités, Rugosité, Tout) définissent les
couches visibles, leurs transparences et l'ordre de pile. 💾 enregistre
l'état actuel comme preset personnel (persisté en localStorage), ✕ le
supprime depuis la liste « Mes presets ».
This commit is contained in:
Antoine Jacquin
2026-09-13 23:17:11 +02:00
parent 0d3aa41008
commit d847068288
2 changed files with 133 additions and 0 deletions

View File

@ -1235,6 +1235,13 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<button id="layerRefresh" class="icon-btn" hidden title="Nouvelles couches disponibles">↻</button>
</span>
</div>
<div class="preset-row">
<select id="presetSelect" class="gen-select" title="Préconfigurations : couches visibles, transparences et ordre de pile">
<option value="">Préconfigurations…</option>
</select>
<button id="presetSave" class="icon-btn" title="Enregistrer l'état actuel (couches, transparences, ordre) comme preset">💾</button>
<button id="presetDelete" class="icon-btn" hidden title="Supprimer le preset sélectionné">✕</button>
</div>
<div id="vizLayers"></div>
</div>
@ -1536,6 +1543,12 @@ select, input[type="checkbox"] { accent-color: var(--accent); }
}
.layer-sub-actions { display: inline-flex; align-items: center; gap: 4px; }
.layer-sub-actions .icon-btn { font-size: 10.5px; padding: 1px 6px; }
.preset-row {
display: flex; align-items: center; gap: 4px;
padding: 4px 6px;
}
.preset-row .gen-select { flex: 1; min-width: 0; }
.preset-row .icon-btn { font-size: 10.5px; padding: 1px 6px; }
/* Sliders d'opacité : piste fine dans une zone cliquable haute de 18 px —
le pouce reste saisissable au drag (l'élément lui-même fait toute la
@ -2228,6 +2241,119 @@ document.getElementById('osmRow').querySelector('.layer-name')
document.getElementById('osmRow').querySelector('.layer-name').title =
'Cliquer : fond de carte clair / assombri';
// ============================================================
// Préconfigurations de couches : un preset définit les couches
// visibles, leurs transparences et l'ordre de pile (bas → haut).
// Les presets intégrés couvrent les couches du panneau ; les presets
// personnels (couches + transparences + ordre) sont persistés en
// localStorage et se suppriment depuis la liste.
// ============================================================
const PRESETS_LS = 'lidarPresets_v1';
const BUILTIN_PRESETS = [
{ label: 'Relief', on: ['aspect', 'positive_openness'],
opacity: { aspect: 1, positive_openness: 0.65 } },
{ label: 'Pentes', on: ['aspect', 'slope'],
opacity: { aspect: 0.5, slope: 1 } },
{ label: 'Cavités', on: ['slope', 'positive_openness'],
opacity: { slope: 0.5, positive_openness: 1 } },
{ label: 'Rugosité', on: ['roughness'], opacity: {} },
{ label: 'Tout', on: ['slope', 'aspect', 'positive_openness', 'roughness'],
opacity: { slope: 0.8, aspect: 0.8, positive_openness: 0.55, roughness: 0.8 } },
];
function loadCustomPresets() {
try {
const p = JSON.parse(localStorage.getItem(PRESETS_LS) || '[]');
return Array.isArray(p) ? p : [];
} catch (e) { return []; }
}
function saveCustomPresets(list) {
try { localStorage.setItem(PRESETS_LS, JSON.stringify(list)); } catch (e) { /* privé */ }
}
function applyPreset(p) {
const want = (p.on || []).filter(k => VIZ_META[k]);
const L = state.layers;
const off = L.order.filter(k => !want.includes(k));
L.order = want.slice(); // ordre du preset (bas → haut)
L.order.push(...off); // les autres gardent leur ordre relatif
for (const k of L.order) {
const on = want.includes(k);
L.on[k] = on;
if (on) L.opacity[k] = (p.opacity && k in p.opacity) ? Number(p.opacity[k]) : 1;
}
saveLayers();
renderLayerPanel();
syncTileLayers();
}
const presetSelectEl = document.getElementById('presetSelect');
const presetSaveEl = document.getElementById('presetSave');
const presetDeleteEl = document.getElementById('presetDelete');
function renderPresetSelect(selectedValue) {
presetSelectEl.innerHTML = '';
const none = document.createElement('option');
none.value = '';
none.textContent = 'Préconfigurations…';
presetSelectEl.appendChild(none);
for (const p of BUILTIN_PRESETS) {
const o = document.createElement('option');
o.value = 'builtin:' + p.label;
o.textContent = p.label;
presetSelectEl.appendChild(o);
}
const customs = loadCustomPresets();
if (customs.length) {
const g = document.createElement('optgroup');
g.label = 'Mes presets';
for (const p of customs) {
const o = document.createElement('option');
o.value = 'custom:' + p.id;
o.textContent = p.label;
g.appendChild(o);
}
presetSelectEl.appendChild(g);
}
if (selectedValue) presetSelectEl.value = selectedValue;
presetDeleteEl.hidden = !presetSelectEl.value.startsWith('custom:');
}
presetSelectEl.addEventListener('change', () => {
const v = presetSelectEl.value;
if (!v) return;
let p = null;
if (v.startsWith('builtin:')) p = BUILTIN_PRESETS.find(x => x.label === v.slice(8));
else if (v.startsWith('custom:')) p = loadCustomPresets().find(x => x.id === v.slice(7));
if (p) applyPreset(p);
});
presetSaveEl.addEventListener('click', () => {
const name = prompt('Nom du preset :');
if (!name || !name.trim()) return;
const enabled = state.layers.order.filter(k => state.layers.on[k]);
const p = {
id: Date.now().toString(36),
label: name.trim(),
on: enabled,
opacity: Object.fromEntries(enabled.map(k => [k, state.layers.opacity[k]])),
};
const customs = loadCustomPresets();
customs.push(p);
saveCustomPresets(customs);
renderPresetSelect('custom:' + p.id);
});
presetDeleteEl.addEventListener('click', () => {
const v = presetSelectEl.value;
if (!v.startsWith('custom:')) return;
if (!confirm('Supprimer ce preset ?')) return;
saveCustomPresets(loadCustomPresets().filter(p => p.id !== v.slice(7)));
renderPresetSelect('');
});
renderPresetSelect();
// Barre latérale : repli + onglets (Couches / Génération)
const sidebar = document.getElementById('sidebar');
function setSidebarCollapsed(collapsed) {

View File

@ -274,6 +274,13 @@ def test_build_index_generates_html(tmp_path):
# le ↻ resynchronise puis reconstruit la carte (mode webapp distante).
assert 'id="layerRefresh"' in content
assert "/api/layers" in app_js and "/api/sync" in app_js
# Préconfigurations de couches (onglet Couches) : le sélecteur applique
# des presets intégrés (couches visibles, transparences, ordre de pile)
# et 💾/✕ gèrent les presets personnels persistés en localStorage.
assert 'id="presetSelect"' in content
assert 'id="presetSave"' in content
assert "BUILTIN_PRESETS" in app_js and "applyPreset" in app_js
assert "lidarPresets_v1" in app_js
# Partage de la vue : bouton « Partager » + lien contenant vue, couches
# et transparences (hash #zoom,lat,lng&L=...&O=...) — le destinataire
# reçoit l'état exact, indépendamment de son localStorage local.