diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py
index 482d950..5553fa7 100644
--- a/lidar_pipeline/tests/test_mapserve.py
+++ b/lidar_pipeline/tests/test_mapserve.py
@@ -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
diff --git a/lidar_pipeline/web/map.css b/lidar_pipeline/web/map.css
index a259a7b..ec7efbb 100644
--- a/lidar_pipeline/web/map.css
+++ b/lidar_pipeline/web/map.css
@@ -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; }
diff --git a/lidar_pipeline/web/map.html b/lidar_pipeline/web/map.html
index 7425449..882c096 100644
--- a/lidar_pipeline/web/map.html
+++ b/lidar_pipeline/web/map.html
@@ -10,6 +10,7 @@