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