Ajouter le mode Comparer à barre glissante et retirer le mode « les deux »
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -306,19 +306,17 @@ def test_healthz_and_assets(tmp_path, monkeypatch):
|
||||
|
||||
def test_ui_main_layer_and_precision_modes():
|
||||
"""Une couche d'affichage principal et la précision : relief seul,
|
||||
précision seule, ou précision en « produit » sur le relief.
|
||||
précision seule, ou comparaison de part et d'autre d'une barre glissante.
|
||||
|
||||
Plus de pile réordonnable ni de fusion par couche. Le produit est
|
||||
cantonné à la pile LiDAR par un conteneur isolé (sinon il assombrirait
|
||||
aussi le fond de carte). Bascule rapide au clavier (touche P).
|
||||
Plus de pile réordonnable ni de fusion par couche. Bascule rapide au
|
||||
clavier (touche P).
|
||||
"""
|
||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||
for mode in ('data-mode="relief"', 'data-mode="precision"', 'data-mode="both"'):
|
||||
for mode in ('data-mode="relief"', 'data-mode="precision"', 'data-mode="compare"'):
|
||||
assert mode in _MAP_HTML
|
||||
assert 'id="precOpacity"' in _MAP_HTML and 'id="precLegend"' in _MAP_HTML
|
||||
assert 'id="precLegend"' in _MAP_HTML
|
||||
assert 'id="mainSel"' in _MAP_HTML
|
||||
assert "blend = 'multiply'" in _MAP_JS
|
||||
assert "createPane('lidarStack')" in _MAP_JS and "isolation = 'isolate'" in _MAP_JS
|
||||
assert "createPane('lidarStack')" in _MAP_JS
|
||||
assert "e.key === 'p'" in _MAP_JS
|
||||
# Légende en 16 paliers exacts (échelle log de la densité)
|
||||
assert "const PREC_LEVELS = 16;" in _MAP_JS and "repeat(16, 1fr)" in _MAP_CSS
|
||||
@ -509,6 +507,25 @@ def test_meta_serves_reading_text(tmp_path, monkeypatch):
|
||||
assert layers["densite_sol"]["reading"]
|
||||
|
||||
|
||||
def test_ui_compare_mode_replaces_both():
|
||||
"""Mode « Comparer » : une barre glissante sépare deux couches (relief,
|
||||
précision ou OSM seul) ; l'ancien mode « les deux » (produit) disparaît
|
||||
et un ancien lien P=both s'ouvre en relief."""
|
||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||
assert 'data-mode="compare"' in _MAP_HTML and 'data-mode="both"' not in _MAP_HTML
|
||||
assert 'id="precOpacity"' not in _MAP_HTML and "precOpacity" not in _MAP_JS
|
||||
assert "mixBlendMode" not in _MAP_JS and "multiply" not in _MAP_JS
|
||||
for id_ in ("compareBar", "compareLeft", "compareRight", "compareCtl"):
|
||||
assert f'id="{id_}"' in _MAP_HTML, id_
|
||||
assert "function applyCompareClip(" in _MAP_JS and "clipPath" in _MAP_JS
|
||||
assert "map.on('move', applyCompareClip)" in _MAP_JS
|
||||
assert "'&C=' +" in _MAP_JS
|
||||
# Ancien lien P=both : mode relief
|
||||
parse = _MAP_JS[_MAP_JS.index("function parseHash("):_MAP_JS.index("function viewHash(")]
|
||||
assert "'both'" in parse and "'relief'" in parse
|
||||
assert "#compareBar" in _MAP_CSS and "touch-action: none" in _MAP_CSS
|
||||
|
||||
|
||||
def test_ui_applies_server_defaults():
|
||||
"""L'interface part des défauts du serveur et sait les (re)définir."""
|
||||
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
|
||||
@ -516,7 +533,8 @@ def test_ui_applies_server_defaults():
|
||||
assert "api/map/defaults" in _MAP_JS
|
||||
assert "meta.default_main" in _MAP_JS and "meta.default_mode" in _MAP_JS
|
||||
assert "meta.default_base" in _MAP_JS
|
||||
assert "meta.default_precision_opacity" in _MAP_JS
|
||||
assert "default_precision_opacity" not in _MAP_JS
|
||||
assert "precision_opacity" not in _MAP_JS
|
||||
# Réinitialiser oublie l'état local avant de reprendre celui du serveur
|
||||
assert "lsDel(LS_KEY)" in _MAP_JS
|
||||
|
||||
|
||||
@ -57,6 +57,13 @@
|
||||
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text);
|
||||
font: var(--fs-md)/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
#map { position: absolute; inset: 0; background: var(--bg); }
|
||||
/* Mode Comparer : barre glissante qui sépare les deux couches. */
|
||||
#compareBar { position: fixed; top: 0; bottom: 0; width: 4px; margin-left: -2px; z-index: var(--z-map-ui);
|
||||
background: var(--surface); box-shadow: var(--shadow-sm); cursor: ew-resize; touch-action: none; }
|
||||
#compareBar span { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
width: 36px; height: 36px; border-radius: var(--r-full); display: flex; align-items: center;
|
||||
justify-content: center; background: var(--surface); color: var(--text); border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-sm); font-size: var(--fs-lg); }
|
||||
.leaflet-container { background: var(--bg); font: inherit; }
|
||||
/* Fond OSM assombri pour laisser ressortir les rendus LiDAR. Filtre posé sur
|
||||
le conteneur de la couche, pas sur chaque tuile : même rendu (opérations
|
||||
@ -129,13 +136,11 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
|
||||
gap: var(--sp-2); padding: var(--sp-1) 0; }
|
||||
.base-row .base-ctl { grid-column: 2 / -1; display: grid; grid-template-columns: minmax(0, 1fr) 40px;
|
||||
gap: var(--sp-2); align-items: center; }
|
||||
/* Affichage : une couche principale, la précision seule ou superposée. */
|
||||
/* Affichage : une couche principale, la précision seule ou en comparaison. */
|
||||
.view-row { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
|
||||
gap: 8px; padding: 8px 4px 4px; border-top: 1px solid var(--border); margin-top: 4px; }
|
||||
.view-label { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
|
||||
.view-row .layer-name { color: var(--text); }
|
||||
.prec-ctl { display: grid; grid-template-columns: auto minmax(0, 1fr) 34px; gap: 8px;
|
||||
align-items: center; padding: 6px 4px 0; font-size: var(--fs-xs); color: var(--muted); }
|
||||
.prec-legend { padding: 8px 4px 2px; }
|
||||
.prec-bar { display: grid; grid-template-columns: repeat(16, 1fr); height: 10px;
|
||||
border: 1px solid var(--border); border-radius: 3px; overflow: hidden; }
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<div id="compareBar" hidden title="Glisser pour déplacer la séparation"><span>⇆</span></div>
|
||||
|
||||
<aside id="panel" data-sheet="half" aria-label="Réglages de la carte">
|
||||
<div id="sheetGrip" class="sheet-grip" title="Glisser pour agrandir ou réduire"><span></span></div>
|
||||
@ -38,15 +39,15 @@
|
||||
<select id="mainSel" class="field" title="Couche d'affichage principal" hidden></select>
|
||||
</div>
|
||||
<div id="precBlock" hidden>
|
||||
<div class="seg" id="modeSeg" role="group" aria-label="Relief, précision ou les deux (touche P)">
|
||||
<div class="seg" id="modeSeg" role="group" aria-label="Relief, précision ou comparaison (touche P)">
|
||||
<button type="button" data-mode="relief" title="Relief seul (touche P : mode suivant)">Relief</button>
|
||||
<button type="button" data-mode="precision" title="Précision géométrique seule : densité de points sol">Précision</button>
|
||||
<button type="button" data-mode="both" title="Précision en produit sur le relief : les zones peu fiables s'assombrissent">Les deux</button>
|
||||
<button type="button" data-mode="compare" title="Comparer deux couches de part et d'autre d'une barre glissante (touche P : mode suivant)">Comparer</button>
|
||||
</div>
|
||||
<div class="prec-ctl" id="precCtl" hidden>
|
||||
<span>Opacité de la précision</span>
|
||||
<input type="range" id="precOpacity" min="0" max="100" step="1" title="Opacité de la précision sur le relief">
|
||||
<span class="pct" id="precPct"></span>
|
||||
<div class="row" id="compareCtl" hidden>
|
||||
<select id="compareLeft" class="field" title="Couche à gauche de la barre"></select>
|
||||
<span class="hint">⇆</span>
|
||||
<select id="compareRight" class="field" title="Couche à droite de la barre"></select>
|
||||
</div>
|
||||
<div class="prec-legend" id="precLegend" hidden>
|
||||
<div class="prec-bar" id="precBar"></div>
|
||||
@ -106,7 +107,7 @@
|
||||
<h4 class="sec-title">Fond d'imagerie XYZ (JOSM, QGIS, iD…)</h4>
|
||||
<div id="useBody"></div>
|
||||
<h4 class="sec-title">Configuration par défaut</h4>
|
||||
<div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, opacité de la précision, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button>
|
||||
<div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button>
|
||||
<button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button></div>
|
||||
</section>
|
||||
<section id="tileView" hidden>
|
||||
|
||||
@ -28,16 +28,22 @@ const SVG_EYE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
|
||||
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>';
|
||||
|
||||
// Affichage : une couche principale (relief orienté) et la couche
|
||||
// « précision » (densité de points sol), montrée seule ou superposée en
|
||||
// « produit » (mix-blend-mode multiply) sur la principale : les zones peu
|
||||
// fiables s'assombrissent sans masquer le relief. Touche P : mode suivant.
|
||||
const VIEW_MODES = [['relief', 'Relief'], ['precision', 'Précision'], ['both', 'Les deux']];
|
||||
// « précision » (densité de points sol), montrée seule ou comparée : une
|
||||
// barre glissante sépare deux couches (relief, précision ou OSM seul) de
|
||||
// part et d'autre. Touche P : mode suivant.
|
||||
const VIEW_MODES = [['relief', 'Relief'], ['precision', 'Précision'], ['compare', 'Comparer']];
|
||||
const VIEW_KEYS = VIEW_MODES.map(m => m[0]);
|
||||
const validMode = (v) => (VIEW_KEYS.includes(v) ? v : null);
|
||||
const clamp01 = (v, d) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : d;
|
||||
};
|
||||
const COMPARE_SIDES = [['relief', 'Relief'], ['precision', 'Précision'], ['osm', 'OSM seul']];
|
||||
const validSide = (v) => (COMPARE_SIDES.some(s => s[0] === v) ? v : null);
|
||||
const DEFAULT_COMPARE = { left: 'relief', right: 'precision', pos: 50 };
|
||||
function validCompare(c) {
|
||||
c = c || {};
|
||||
const pos = Number(c.pos);
|
||||
return { left: validSide(c.left) || DEFAULT_COMPARE.left,
|
||||
right: validSide(c.right) || DEFAULT_COMPARE.right,
|
||||
pos: Number.isFinite(pos) ? Math.min(95, Math.max(5, pos)) : DEFAULT_COMPARE.pos };
|
||||
}
|
||||
// Paliers de la précision : niveau k à partir de 0,25 × 2^(k/2) pts/m²
|
||||
// (visualizations.density_levels), gris = colormap « gray » sur 0..15.
|
||||
const PREC_LEVELS = 16;
|
||||
@ -221,7 +227,7 @@ function defaultState(meta) {
|
||||
return {
|
||||
main: validMain(meta, meta.default_main) || mainKeys(meta)[0] || null,
|
||||
mode: validMode(meta.default_mode) || 'relief',
|
||||
precOpacity: clamp01(meta.default_precision_opacity, 0.6),
|
||||
compare: validCompare(null),
|
||||
base: { on: b.on !== undefined ? !!b.on : true,
|
||||
opacity: b.opacity !== undefined ? Number(b.opacity) : 0.85,
|
||||
// null = suit le thème (choix explicite seulement si b.dark est déjà
|
||||
@ -241,8 +247,9 @@ function loadState(meta) {
|
||||
const sBase = Object.assign({}, s.base || {});
|
||||
if (s.v !== 3) delete sBase.dark;
|
||||
st = { main: validMain(meta, s.main) || def.main,
|
||||
mode: validMode(s.mode) || def.mode,
|
||||
precOpacity: clamp01(s.precOpacity, def.precOpacity),
|
||||
// Ancien mode « les deux » (produit) mémorisé : relief.
|
||||
mode: validMode(s.mode === 'both' ? 'relief' : s.mode) || def.mode,
|
||||
compare: validCompare(s.compare),
|
||||
base: Object.assign({}, def.base, sBase) };
|
||||
}
|
||||
// Lien partagé : prime sur l'état local (les anciens liens &L=… de la
|
||||
@ -250,7 +257,8 @@ function loadState(meta) {
|
||||
const hash = parseHash();
|
||||
if (hash) {
|
||||
if (hash.main && validMain(meta, hash.main)) st.main = hash.main;
|
||||
if (hash.view) { st.mode = hash.view.mode; st.precOpacity = hash.view.opacity; }
|
||||
if (hash.view) st.mode = hash.view.mode;
|
||||
if (hash.compare) st.compare = hash.compare;
|
||||
if (hash.base) st.base = hash.base;
|
||||
}
|
||||
return st;
|
||||
@ -263,12 +271,12 @@ function saveState() {
|
||||
}
|
||||
|
||||
// --- hash de vue ----------------------------------------------------------
|
||||
// #z/lat/lng&M=principale&P=mode:opacité&B=on:opacité:sombre
|
||||
// #z/lat/lng&M=principale&P=mode&C=gauche:droite:position&B=on:opacité:sombre
|
||||
function parseHash() {
|
||||
const m = /^#([\d.]+)\/(-?[\d.]+)\/(-?[\d.]+)(.*)$/.exec(location.hash || '');
|
||||
if (!m) return null;
|
||||
const out = { zoom: parseFloat(m[1]), lat: parseFloat(m[2]), lng: parseFloat(m[3]),
|
||||
main: null, view: null, base: null };
|
||||
main: null, view: null, compare: null, base: null };
|
||||
const seg = {};
|
||||
for (const part of m[4].split('&')) {
|
||||
const i = part.indexOf('=');
|
||||
@ -277,8 +285,14 @@ function parseHash() {
|
||||
if (seg.M) out.main = seg.M;
|
||||
if (seg.P) {
|
||||
const f = seg.P.split(':');
|
||||
const mode = validMode(f[0]);
|
||||
if (mode) out.view = { mode, opacity: clamp01(Number(f[1]) / 100, 0.6) };
|
||||
// Ancien mode « les deux » (produit) : s'ouvre en relief. L'opacité qui
|
||||
// suivait (P=mode:opacité) est ignorée.
|
||||
const mode = validMode(f[0] === 'both' ? 'relief' : f[0]);
|
||||
if (mode) out.view = { mode };
|
||||
}
|
||||
if (seg.C) {
|
||||
const f = seg.C.split(':');
|
||||
out.compare = validCompare({ left: f[0], right: f[1], pos: f[2] });
|
||||
}
|
||||
if (seg.B) {
|
||||
const f = seg.B.split(':');
|
||||
@ -295,7 +309,10 @@ function viewHash(full) {
|
||||
let h = '#' + map.getZoom().toFixed(1) + '/' + c.lat.toFixed(6) + '/' + c.lng.toFixed(6);
|
||||
if (full) {
|
||||
if (STATE.main) h += '&M=' + STATE.main;
|
||||
h += '&P=' + STATE.mode + ':' + Math.round(STATE.precOpacity * 100);
|
||||
h += '&P=' + STATE.mode;
|
||||
if (STATE.mode === 'compare') {
|
||||
h += '&C=' + STATE.compare.left + ':' + STATE.compare.right + ':' + Math.round(STATE.compare.pos);
|
||||
}
|
||||
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)
|
||||
+ ':' + (dark ? '1' : '0');
|
||||
@ -335,13 +352,10 @@ const EvenLevelTileLayer = ZoomAnimTileLayer.extend({
|
||||
});
|
||||
|
||||
function buildLayers() {
|
||||
// Conteneur isolé : `isolation: isolate` cantonne le « produit » de la
|
||||
// précision à la couche principale. Sans lui, il assombrirait aussi le fond
|
||||
// OSM. Les panneaux sont réutilisés si déjà créés (rafraîchissement après
|
||||
// un run de génération).
|
||||
// Pile LiDAR commune (z 400). Les panneaux sont réutilisés si déjà créés
|
||||
// (rafraîchissement après un run de génération).
|
||||
const stack = map.getPane('lidarStack') || map.createPane('lidarStack');
|
||||
stack.style.zIndex = 400;
|
||||
stack.style.isolation = 'isolate';
|
||||
META.layers.forEach((info) => {
|
||||
const paneName = 'lidar-' + info.key;
|
||||
const pane = map.getPane(paneName) || map.createPane(paneName, stack);
|
||||
@ -370,6 +384,9 @@ function buildLayers() {
|
||||
errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
|
||||
attribution: META.attribution
|
||||
});
|
||||
// Le conteneur de tuiles n'existe qu'après l'ajout : la découpe de la
|
||||
// comparaison lui est appliquée à ce moment-là.
|
||||
layer.on('add', applyCompareClip);
|
||||
LAYERS.set(info.key, layer);
|
||||
});
|
||||
applyLayers();
|
||||
@ -380,28 +397,23 @@ function effectiveMode() {
|
||||
return precisionKey() ? STATE.mode : 'relief';
|
||||
}
|
||||
|
||||
// Couche affichée pour un côté de la comparaison (null = OSM seul).
|
||||
function sideLayerKey(side) {
|
||||
return side === 'relief' ? STATE.main : side === 'precision' ? precisionKey() : null;
|
||||
}
|
||||
|
||||
function applyLayers() {
|
||||
const prec = precisionKey();
|
||||
const mode = effectiveMode();
|
||||
const wanted = new Set(
|
||||
mode === 'relief' ? [STATE.main]
|
||||
: mode === 'precision' ? [prec]
|
||||
: [sideLayerKey(STATE.compare.left), sideLayerKey(STATE.compare.right)]);
|
||||
for (const [key, layer] of LAYERS) {
|
||||
const pane = map.getPane('lidar-' + key);
|
||||
let show, opacity = 1, blend = 'normal';
|
||||
if (key === prec) {
|
||||
show = mode !== 'relief';
|
||||
if (mode === 'both') { opacity = STATE.precOpacity; blend = 'multiply'; }
|
||||
} else {
|
||||
show = key === STATE.main && mode !== 'precision';
|
||||
}
|
||||
if (pane) {
|
||||
pane.style.zIndex = key === prec ? '402' : '401';
|
||||
pane.style.mixBlendMode = blend;
|
||||
}
|
||||
if (show) {
|
||||
if (!map.hasLayer(layer)) layer.addTo(map);
|
||||
layer.setOpacity(opacity);
|
||||
} else if (map.hasLayer(layer)) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
if (pane) pane.style.zIndex = key === prec ? '402' : '401';
|
||||
if (wanted.has(key)) { if (!map.hasLayer(layer)) layer.addTo(map); layer.setOpacity(1); }
|
||||
else if (map.hasLayer(layer)) map.removeLayer(layer);
|
||||
}
|
||||
if (STATE.base.on) { if (!map.hasLayer(base)) base.addTo(map); }
|
||||
else if (map.hasLayer(base)) map.removeLayer(base);
|
||||
@ -409,9 +421,56 @@ function applyLayers() {
|
||||
const container = base.getContainer && base.getContainer();
|
||||
const dark = STATE.base.dark === null || STATE.base.dark === undefined ? themeIsDark() : STATE.base.dark;
|
||||
if (container) container.classList.toggle('base-dark', dark);
|
||||
applyCompareClip();
|
||||
updateRose();
|
||||
}
|
||||
|
||||
// Découpe des couches de part et d'autre de la barre, en coordonnées de
|
||||
// calque (les conteneurs de tuiles se déplacent avec la carte) : recalculée
|
||||
// à chaque déplacement. Même couche des deux côtés : aucune découpe.
|
||||
function applyCompareClip() {
|
||||
if (!STATE) return;
|
||||
const on = effectiveMode() === 'compare';
|
||||
el('compareBar').hidden = !on;
|
||||
const size = map.getSize();
|
||||
const x = size.x * (on ? STATE.compare.pos : 50) / 100;
|
||||
const nw = map.containerPointToLayerPoint([0, 0]);
|
||||
const se = map.containerPointToLayerPoint(size);
|
||||
const cx = map.containerPointToLayerPoint([x, 0]).x;
|
||||
const leftKey = sideLayerKey(STATE.compare.left), rightKey = sideLayerKey(STATE.compare.right);
|
||||
for (const [key, layer] of LAYERS) {
|
||||
const c = layer.getContainer && layer.getContainer();
|
||||
if (!c) continue;
|
||||
let clip = '';
|
||||
if (on && leftKey !== rightKey) {
|
||||
if (key === leftKey) clip = `polygon(${nw.x}px ${nw.y}px, ${cx}px ${nw.y}px, ${cx}px ${se.y}px, ${nw.x}px ${se.y}px)`;
|
||||
else if (key === rightKey) clip = `polygon(${cx}px ${nw.y}px, ${se.x}px ${nw.y}px, ${se.x}px ${se.y}px, ${cx}px ${se.y}px)`;
|
||||
}
|
||||
c.style.clipPath = clip;
|
||||
}
|
||||
if (on) {
|
||||
const r = map.getContainer().getBoundingClientRect();
|
||||
el('compareBar').style.left = (r.left + x) + 'px';
|
||||
}
|
||||
}
|
||||
map.on('move', applyCompareClip);
|
||||
map.on('resize zoomend', applyCompareClip);
|
||||
|
||||
(function compareDrag() {
|
||||
const bar = el('compareBar');
|
||||
let dragging = false;
|
||||
bar.addEventListener('pointerdown', (e) => { dragging = true; bar.setPointerCapture(e.pointerId); e.preventDefault(); });
|
||||
bar.addEventListener('pointermove', (e) => {
|
||||
if (!dragging) return;
|
||||
const r = map.getContainer().getBoundingClientRect();
|
||||
STATE.compare.pos = Math.min(95, Math.max(5, 100 * (e.clientX - r.left) / r.width));
|
||||
applyCompareClip();
|
||||
});
|
||||
const end = () => { if (dragging) { dragging = false; saveState(); } };
|
||||
bar.addEventListener('pointerup', end);
|
||||
bar.addEventListener('pointercancel', end);
|
||||
})();
|
||||
|
||||
function setMode(mode, announce) {
|
||||
if (!validMode(mode)) return;
|
||||
STATE.mode = mode;
|
||||
@ -458,10 +517,14 @@ function renderPanel() {
|
||||
const mode = effectiveMode();
|
||||
el('modeSeg').querySelectorAll('button').forEach(b =>
|
||||
b.setAttribute('aria-pressed', String(b.dataset.mode === mode)));
|
||||
el('precCtl').hidden = mode !== 'both';
|
||||
el('precOpacity').value = Math.round(STATE.precOpacity * 100);
|
||||
el('precPct').textContent = Math.round(STATE.precOpacity * 100) + '%';
|
||||
el('precLegend').hidden = mode === 'relief';
|
||||
el('compareCtl').hidden = mode !== 'compare';
|
||||
for (const [id, side] of [['compareLeft', 'left'], ['compareRight', 'right']]) {
|
||||
const sel = el(id);
|
||||
sel.innerHTML = COMPARE_SIDES.map(([k, lbl]) => '<option value="' + k + '"'
|
||||
+ (k === STATE.compare[side] ? ' selected' : '') + '>' + lbl + '</option>').join('');
|
||||
}
|
||||
el('precLegend').hidden = mode === 'relief'
|
||||
|| (mode === 'compare' && STATE.compare.left !== 'precision' && STATE.compare.right !== 'precision');
|
||||
}
|
||||
|
||||
function renderBaseRow() {
|
||||
@ -741,7 +804,11 @@ function drawRose() {
|
||||
}
|
||||
|
||||
function updateRose() {
|
||||
const on = !!(STATE && STATE.main === 'relief_oriente' && effectiveMode() !== 'precision'
|
||||
// Visible dès que le relief est affiché : seul, ou d'un côté de la barre.
|
||||
const mode = STATE && effectiveMode();
|
||||
const reliefShown = mode === 'relief'
|
||||
|| (mode === 'compare' && (STATE.compare.left === 'relief' || STATE.compare.right === 'relief'));
|
||||
const on = !!(STATE && STATE.main === 'relief_oriente' && reliefShown
|
||||
&& LAYERS.get('relief_oriente'));
|
||||
el('legendBlock').hidden = !on;
|
||||
if (on) drawRose();
|
||||
@ -1426,7 +1493,7 @@ window.addEventListener('hashchange', () => {
|
||||
if (!h) return;
|
||||
applyingHash = true;
|
||||
map.setView([h.lat, h.lng], h.zoom);
|
||||
if (h.main || h.view || h.base) {
|
||||
if (h.main || h.view || h.base || h.compare) {
|
||||
STATE = loadState(META);
|
||||
saveState(); renderPanel(); renderBaseRow(); applyLayers();
|
||||
}
|
||||
@ -1456,7 +1523,6 @@ el('btnDefault').addEventListener('click', () => {
|
||||
const body = {
|
||||
main: STATE.main,
|
||||
mode: STATE.mode,
|
||||
precision_opacity: STATE.precOpacity,
|
||||
base: STATE.base,
|
||||
};
|
||||
fetch('api/map/defaults', {
|
||||
@ -1465,7 +1531,7 @@ el('btnDefault').addEventListener('click', () => {
|
||||
}).then(r => r.ok ? r.json() : Promise.reject('HTTP ' + r.status))
|
||||
.then(() => {
|
||||
META.default_main = STATE.main; META.default_mode = STATE.mode;
|
||||
META.default_precision_opacity = STATE.precOpacity; META.default_base = STATE.base;
|
||||
META.default_base = STATE.base;
|
||||
toast('Affichage enregistré comme défaut');
|
||||
})
|
||||
.catch(msg => toast('Échec de l\'enregistrement : ' + msg, 6000));
|
||||
@ -1477,13 +1543,12 @@ el('mainSel').addEventListener('change', (e) => {
|
||||
STATE.main = e.target.value;
|
||||
saveState(); renderPanel(); applyLayers();
|
||||
});
|
||||
el('precOpacity').addEventListener('input', (e) => {
|
||||
STATE.precOpacity = Number(e.target.value) / 100;
|
||||
el('precPct').textContent = e.target.value + '%';
|
||||
const layer = LAYERS.get(precisionKey());
|
||||
if (layer && map.hasLayer(layer)) layer.setOpacity(STATE.precOpacity);
|
||||
for (const [id, side] of [['compareLeft', 'left'], ['compareRight', 'right']]) {
|
||||
el(id).addEventListener('change', (e) => {
|
||||
STATE.compare[side] = validSide(e.target.value) || STATE.compare[side];
|
||||
saveState(); renderPanel(); applyLayers();
|
||||
});
|
||||
el('precOpacity').addEventListener('change', saveState);
|
||||
}
|
||||
|
||||
el('btnReset').addEventListener('click', () => {
|
||||
// Oublie l'état local et reprend la configuration servie par le serveur.
|
||||
@ -1552,7 +1617,7 @@ document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && typing) e.target.blur();
|
||||
return;
|
||||
}
|
||||
// P : relief → précision → les deux → relief
|
||||
// P : relief → précision → comparer → relief
|
||||
if ((e.key === 'p' || e.key === 'P') && META && STATE && precisionKey()) {
|
||||
const i = VIEW_KEYS.indexOf(STATE.mode);
|
||||
setMode(VIEW_KEYS[(i + 1) % VIEW_KEYS.length], true);
|
||||
|
||||
Reference in New Issue
Block a user