From 4424c2d9c42f68f2218ea16da1a118ef671fe2c8 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Wed, 9 Sep 2026 23:08:36 +0200 Subject: [PATCH] refactor: implement strict map layer stacking order for intersection and cross plan visualizations --- .../trafficlights_intersection_detail.js | 118 +++++++++++++++++- .../trafficlights_import_cross_plan.html | 88 ++++++++++++- .../static/common/layer-switcher-control.js | 42 ++++++- 3 files changed, 243 insertions(+), 5 deletions(-) diff --git a/loko/assets/static/assets/trafficlights_intersection_detail.js b/loko/assets/static/assets/trafficlights_intersection_detail.js index 68229db..11f639d 100644 --- a/loko/assets/static/assets/trafficlights_intersection_detail.js +++ b/loko/assets/static/assets/trafficlights_intersection_detail.js @@ -42,6 +42,104 @@ class CustomPlansControl { } map.addControl(new CustomPlansControl(), 'top-right'); +// ── Helper to determine where to place the orthophoto layer ── +function getOrthoBeforeLayer(m) { + if (!m || !m.getStyle) return undefined; + const style = m.getStyle(); + const styleLayers = (style && style.layers) || []; + + // 1. Bottom-most plan layer: first georef image layer + const imageLayer = styleLayers.find(l => l.id && l.id.startsWith('georef-image-overlay-layer-')); + if (imageLayer) return imageLayer.id; + + // 2. Or first CAD layer + if (m.getLayer('dxf-layout-fills')) return 'dxf-layout-fills'; + if (m.getLayer('dxf-layout-lines')) return 'dxf-layout-lines'; + if (m.getLayer('dxf-layout-symbols')) return 'dxf-layout-symbols'; + + // 3. Or traffic light / marker layer + if (m.getLayer('traffic-light-outline')) return 'traffic-light-outline'; + if (m.getLayer('traffic-light')) return 'traffic-light'; + + return undefined; +} + +// ── Ensure strict layer stacking order ── +// 1. Base vector tiles +// 2. Orthophotos (layer-switcher-ortho) +// 3. Georeferenced image plans (georef-image-overlay-layer-*) +// 4. CAD / DXF layers (fills -> lines -> symbols) +// 5. Traffic light marker (traffic-light-outline -> traffic-light) +// 6. Assets (cables, poles, radars, cabinets, equipment) +function reorderIntersectionLayers() { + if (!map || typeof map.isStyleLoaded !== 'function' || !map.isStyleLoaded()) return; + + const markerBottomId = map.getLayer('traffic-light-outline') + ? 'traffic-light-outline' + : (map.getLayer('traffic-light') ? 'traffic-light' : undefined); + + const style = map.getStyle(); + const styleLayers = (style && style.layers) || []; + const imageLayerIds = styleLayers + .filter(l => l.id && l.id.startsWith('georef-image-overlay-layer-')) + .map(l => l.id); + + const firstCadId = map.getLayer('dxf-layout-fills') + ? 'dxf-layout-fills' + : (map.getLayer('dxf-layout-lines') ? 'dxf-layout-lines' : (map.getLayer('dxf-layout-symbols') ? 'dxf-layout-symbols' : undefined)); + + const firstPlanId = (imageLayerIds.length > 0 ? imageLayerIds[0] : null) || firstCadId; + + // 1. Move orthophoto under any plan layer (or under marker) + const orthoBeforeId = firstPlanId || markerBottomId; + if (map.getLayer('layer-switcher-ortho') && orthoBeforeId && map.getLayer(orthoBeforeId)) { + try { + map.moveLayer('layer-switcher-ortho', orthoBeforeId); + } catch (e) { + console.warn("Could not move layer-switcher-ortho:", e); + } + } + + // 2. Georeferenced image layers under CAD layers (or under marker) + const imageBeforeId = firstCadId || markerBottomId; + if (imageBeforeId && map.getLayer(imageBeforeId)) { + imageLayerIds.forEach(imgId => { + if (map.getLayer(imgId)) { + try { + map.moveLayer(imgId, imageBeforeId); + } catch (e) { + console.warn("Could not move image overlay layer:", e); + } + } + }); + } + + // 3. DXF CAD layers: fills -> lines -> symbols, before markerBottomId + if (markerBottomId && map.getLayer(markerBottomId)) { + ['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(cadId => { + if (map.getLayer(cadId)) { + try { + map.moveLayer(cadId, markerBottomId); + } catch (e) { + console.warn("Could not move CAD layer:", e); + } + } + }); + } + + // 4. Ensure assets stay on top of the intersection marker and plans + const assetLayerIds = ['assets-cables', 'assets-poles', 'assets-radars', 'assets-cabinets', 'assets-equipment']; + assetLayerIds.forEach(assetId => { + if (map.getLayer(assetId)) { + try { + map.moveLayer(assetId); + } catch (e) { + // ignore + } + } + }); +} + // --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button) if (typeof LayerSwitcherControl !== 'undefined') { const layerSwitcher = new LayerSwitcherControl({ @@ -51,7 +149,11 @@ if (typeof LayerSwitcherControl !== 'undefined') { name: 'Orthophotos', icon: 'satellite', group: 'ortho', - beforeLayer: 'traffic-light', + beforeLayer: getOrthoBeforeLayer, + onLayerAdded: () => reorderIntersectionLayers(), + onVisibilityChange: (layerId, isVisible) => { + if (isVisible) reorderIntersectionLayers(); + }, layer: { type: 'raster', paint: { 'raster-opacity': 1 } @@ -440,11 +542,14 @@ map.on('load', () => { }); }); + reorderIntersectionLayers(); + }).catch(e => { console.error('Failed to load asset icons:', e); }); } + reorderIntersectionLayers(); }); @@ -704,7 +809,9 @@ function ensureDxfSourcesAndLayers() { data: { type: "FeatureCollection", features: [] } }); - const beforeId = map.getLayer('traffic-light') ? 'traffic-light' : undefined; + const beforeId = map.getLayer('traffic-light-outline') + ? 'traffic-light-outline' + : (map.getLayer('traffic-light') ? 'traffic-light' : undefined); map.addLayer({ 'id': 'dxf-layout-fills', @@ -762,6 +869,7 @@ function ensureDxfSourcesAndLayers() { } }, beforeId); } + reorderIntersectionLayers(); } // ── Georeferenced Plans & DXF Background Loader ── @@ -792,6 +900,7 @@ function showGeorefImage(doc, active, opacity) { if (map.getLayer(lyrId)) { map.setLayoutProperty(lyrId, 'visibility', 'visible'); map.setPaintProperty(lyrId, 'raster-opacity', opacity); + reorderIntersectionLayers(); } } else { map.addSource(srcId, { @@ -805,6 +914,8 @@ function showGeorefImage(doc, active, opacity) { beforeId = 'dxf-layout-fills'; } else if (map.getLayer('dxf-layout-lines')) { beforeId = 'dxf-layout-lines'; + } else if (map.getLayer('traffic-light-outline')) { + beforeId = 'traffic-light-outline'; } else if (map.getLayer('traffic-light')) { beforeId = 'traffic-light'; } @@ -816,6 +927,8 @@ function showGeorefImage(doc, active, opacity) { 'layout': { 'visibility': 'visible' }, 'paint': { 'raster-opacity': opacity } }, beforeId); + + reorderIntersectionLayers(); } // Zoom/fitBounds to document @@ -873,6 +986,7 @@ function applyDxfVectorFilters() { map.setLayoutProperty(lId, 'visibility', 'visible'); } }); + reorderIntersectionLayers(); } } diff --git a/loko/assets/templates/assets/trafficlights_import_cross_plan.html b/loko/assets/templates/assets/trafficlights_import_cross_plan.html index 3603f6e..665686d 100644 --- a/loko/assets/templates/assets/trafficlights_import_cross_plan.html +++ b/loko/assets/templates/assets/trafficlights_import_cross_plan.html @@ -1195,6 +1195,83 @@ document.addEventListener('DOMContentLoaded', () => { } map.addControl(new CustomPlansControl(), 'top-right'); + // ── Helper to determine where to place the orthophoto layer ── + function getOrthoBeforeLayer(m) { + if (!m || !m.getStyle) return undefined; + const style = m.getStyle(); + const styleLayers = (style && style.layers) || []; + + // 1. Bottom-most plan layer: first georef image layer + const imageLayer = styleLayers.find(l => l.id && l.id.startsWith('georef-image-overlay-layer-')); + if (imageLayer) return imageLayer.id; + + // 2. Or first CAD layer + if (m.getLayer('dxf-layout-fills')) return 'dxf-layout-fills'; + if (m.getLayer('dxf-layout-lines')) return 'dxf-layout-lines'; + if (m.getLayer('dxf-layout-symbols')) return 'dxf-layout-symbols'; + + // 3. Or existing poles layer + if (m.getLayer('existing-poles-layer')) return 'existing-poles-layer'; + + return undefined; + } + + // ── Ensure strict layer stacking order for cross plan import ── + function reorderCrossPlanLayers() { + if (!map || typeof map.isStyleLoaded !== 'function' || !map.isStyleLoaded()) return; + + const markerBottomId = map.getLayer('existing-poles-layer') ? 'existing-poles-layer' : undefined; + + const style = map.getStyle(); + const styleLayers = (style && style.layers) || []; + const imageLayerIds = styleLayers + .filter(l => l.id && l.id.startsWith('georef-image-overlay-layer-')) + .map(l => l.id); + + const firstCadId = map.getLayer('dxf-layout-fills') + ? 'dxf-layout-fills' + : (map.getLayer('dxf-layout-lines') ? 'dxf-layout-lines' : (map.getLayer('dxf-layout-symbols') ? 'dxf-layout-symbols' : undefined)); + + const firstPlanId = (imageLayerIds.length > 0 ? imageLayerIds[0] : null) || firstCadId; + + // 1. Move orthophoto under any plan layer (or under marker) + const orthoBeforeId = firstPlanId || markerBottomId; + if (map.getLayer('layer-switcher-ortho') && orthoBeforeId && map.getLayer(orthoBeforeId)) { + try { + map.moveLayer('layer-switcher-ortho', orthoBeforeId); + } catch (e) { + console.warn("Could not move layer-switcher-ortho:", e); + } + } + + // 2. Georeferenced image layers under CAD layers (or under marker) + const imageBeforeId = firstCadId || markerBottomId; + if (imageBeforeId && map.getLayer(imageBeforeId)) { + imageLayerIds.forEach(imgId => { + if (map.getLayer(imgId)) { + try { + map.moveLayer(imgId, imageBeforeId); + } catch (e) { + console.warn("Could not move image overlay layer:", e); + } + } + }); + } + + // 3. DXF CAD layers: fills -> lines -> symbols, before markerBottomId + if (markerBottomId && map.getLayer(markerBottomId)) { + ['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(cadId => { + if (map.getLayer(cadId)) { + try { + map.moveLayer(cadId, markerBottomId); + } catch (e) { + console.warn("Could not move CAD layer:", e); + } + } + }); + } + } + // --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button) if (typeof LayerSwitcherControl !== 'undefined') { const layerSwitcher = new LayerSwitcherControl({ @@ -1204,7 +1281,11 @@ document.addEventListener('DOMContentLoaded', () => { name: 'Orthophotos', icon: 'satellite', group: 'ortho', - beforeLayer: 'existing-poles-layer', + beforeLayer: getOrthoBeforeLayer, + onLayerAdded: () => reorderCrossPlanLayers(), + onVisibilityChange: (layerId, isVisible) => { + if (isVisible) reorderCrossPlanLayers(); + }, layer: { type: 'raster', paint: { 'raster-opacity': 1 } @@ -1783,6 +1864,7 @@ document.addEventListener('DOMContentLoaded', () => { if (map.getLayer(lyrId)) { map.setLayoutProperty(lyrId, 'visibility', 'visible'); map.setPaintProperty(lyrId, 'raster-opacity', opacity); + reorderCrossPlanLayers(); } } else { map.addSource(srcId, { @@ -1807,6 +1889,8 @@ document.addEventListener('DOMContentLoaded', () => { 'layout': { 'visibility': 'visible' }, 'paint': { 'raster-opacity': opacity } }, beforeId); + + reorderCrossPlanLayers(); } // Zoom/fitBounds to document @@ -1864,6 +1948,7 @@ document.addEventListener('DOMContentLoaded', () => { map.setLayoutProperty(lId, 'visibility', 'visible'); } }); + reorderCrossPlanLayers(); } } @@ -1932,6 +2017,7 @@ document.addEventListener('DOMContentLoaded', () => { } }, beforeId); } + reorderCrossPlanLayers(); } function updateButtonActiveState() { diff --git a/loko/common/static/common/layer-switcher-control.js b/loko/common/static/common/layer-switcher-control.js index e63856e..0eeede0 100644 --- a/loko/common/static/common/layer-switcher-control.js +++ b/loko/common/static/common/layer-switcher-control.js @@ -425,7 +425,12 @@ this.map.addSource(sourceId, sublayer.source); // Determine where to insert the layer - let beforeLayer = layerConfig.beforeLayer; + let beforeLayer = typeof layerConfig.beforeLayer === 'function' + ? layerConfig.beforeLayer(this.map) + : layerConfig.beforeLayer; + if (beforeLayer && !this.map.getLayer(beforeLayer)) { + beforeLayer = undefined; + } if (!beforeLayer) { const layers = this.map.getStyle().layers; for (const layer of layers) { @@ -447,6 +452,13 @@ }; this.map.addLayer(mapLayerConfig, beforeLayer); + + if (layerConfig.onLayerAdded && typeof layerConfig.onLayerAdded === 'function') { + layerConfig.onLayerAdded(mapLayerId, this.map); + } + if (this.map && typeof this.map.fire === 'function') { + this.map.fire('layerswitcher:layerAdded', { layerId: mapLayerId, config: layerConfig }); + } } } @@ -501,8 +513,14 @@ } // Determine where to insert the layer - let beforeLayer = layerConfig.beforeLayer; + let beforeLayer = typeof layerConfig.beforeLayer === 'function' + ? layerConfig.beforeLayer(this.map) + : layerConfig.beforeLayer; + if (beforeLayer && !this.map.getLayer(beforeLayer)) { + beforeLayer = undefined; + } + // If no beforeLayer specified, try to insert before draw layers if (!beforeLayer) { const layers = this.map.getStyle().layers; @@ -531,6 +549,13 @@ state.layerAdded = true; state.visible = true; } + + if (layerConfig.onLayerAdded && typeof layerConfig.onLayerAdded === 'function') { + layerConfig.onLayerAdded(layerId, this.map); + } + if (this.map && typeof this.map.fire === 'function') { + this.map.fire('layerswitcher:layerAdded', { layerId: layerId, config: layerConfig }); + } } /** @@ -578,6 +603,13 @@ if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { this._hideSublayerSelector(layerId); } + + if (layerConfig.onVisibilityChange && typeof layerConfig.onVisibilityChange === 'function') { + layerConfig.onVisibilityChange(mapLayerId, false, this.map); + } + if (this.map && typeof this.map.fire === 'function') { + this.map.fire('layerswitcher:visibilityChange', { layerId: mapLayerId, visible: false }); + } } else { // Handle exclusive layers if (layerConfig.exclusive && layerConfig.group) { @@ -587,6 +619,12 @@ // Show layer if (this.map.getLayer(mapLayerId)) { this.map.setLayoutProperty(mapLayerId, 'visibility', 'visible'); + if (layerConfig.onVisibilityChange && typeof layerConfig.onVisibilityChange === 'function') { + layerConfig.onVisibilityChange(mapLayerId, true, this.map); + } + if (this.map && typeof this.map.fire === 'function') { + this.map.fire('layerswitcher:visibilityChange', { layerId: mapLayerId, visible: true }); + } } else { this._addLayer(layerConfig); }