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>
|
<button id="layerRefresh" class="icon-btn" hidden title="Nouvelles couches disponibles">↻</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 id="vizLayers"></div>
|
||||||
</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 { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.layer-sub-actions .icon-btn { font-size: 10.5px; padding: 1px 6px; }
|
.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 —
|
/* 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
|
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 =
|
document.getElementById('osmRow').querySelector('.layer-name').title =
|
||||||
'Cliquer : fond de carte clair / assombri';
|
'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)
|
// Barre latérale : repli + onglets (Couches / Génération)
|
||||||
const sidebar = document.getElementById('sidebar');
|
const sidebar = document.getElementById('sidebar');
|
||||||
function setSidebarCollapsed(collapsed) {
|
function setSidebarCollapsed(collapsed) {
|
||||||
|
|||||||
@ -274,6 +274,13 @@ def test_build_index_generates_html(tmp_path):
|
|||||||
# le ↻ resynchronise puis reconstruit la carte (mode webapp distante).
|
# le ↻ resynchronise puis reconstruit la carte (mode webapp distante).
|
||||||
assert 'id="layerRefresh"' in content
|
assert 'id="layerRefresh"' in content
|
||||||
assert "/api/layers" in app_js and "/api/sync" in app_js
|
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
|
# Partage de la vue : bouton « Partager » + lien contenant vue, couches
|
||||||
# et transparences (hash #zoom,lat,lng&L=...&O=...) — le destinataire
|
# et transparences (hash #zoom,lat,lng&L=...&O=...) — le destinataire
|
||||||
# reçoit l'état exact, indépendamment de son localStorage local.
|
# reçoit l'état exact, indépendamment de son localStorage local.
|
||||||
|
|||||||
Reference in New Issue
Block a user