const map = new maplibregl.Map({ style: window.MAP_STYLE_LIGHT, center: [feature_lon, feature_lat], zoom: 17, pitch: 45, bearing: -17.6, container: 'map', antialias: true }); // Custom Plans Control in Top Right (first, at the top) class CustomPlansControl { onAdd(map) { this._container = document.createElement('div'); this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; const button = document.createElement('button'); button.type = 'button'; button.id = 'btn-load-georef-plan'; button.className = 'btn btn-xs btn-light bg-white border shadow-sm d-flex align-items-center gap-1'; button.style.border = 'none'; button.style.padding = '8px 12px'; button.style.height = 'auto'; button.style.width = 'auto'; button.style.fontWeight = 'bold'; button.style.fontSize = '0.75rem'; button.innerHTML = ' Plans'; // Toggle bootstrap modal natively button.setAttribute('data-bs-toggle', 'modal'); button.setAttribute('data-bs-target', '#georefPlanModal'); this._container.appendChild(button); return this._container; } onRemove() { this._container.parentNode.removeChild(this._container); this._map = undefined; } } map.addControl(new CustomPlansControl(), 'top-right'); // --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button) if (typeof LayerSwitcherControl !== 'undefined') { const layerSwitcher = new LayerSwitcherControl({ layers: [ { id: 'ortho', name: 'Orthophotos', icon: 'satellite', group: 'ortho', beforeLayer: 'traffic-light', layer: { type: 'raster', paint: { 'raster-opacity': 1 } }, sublayers: [ { id: 'urbis', name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)', default: true, source: { type: 'raster', tiles: [ window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}' ], tileSize: 256, attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS' } }, { id: 'vlaanderen', name: 'Vlaanderen 2023', source: { type: 'raster', tiles: [ 'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}' ], tileSize: 256, attribution: '© Digitaal Vlaanderen - Orthofoto 2023' } } ] } ] }); map.addControl(layerSwitcher, 'top-right'); } // Classic Zoom & Bearing/Compass Control in Top Right (below Orthophotos control) map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'top-right'); map.on('load', () => { const pointData = { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Point', coordinates: [feature_lon, feature_lat] }, properties: { name: '...', status: feature_status } } ] }; // Add the circle as a GeoJSON source for the intersection map.addSource('traffic-light', { type: 'geojson', data: pointData }); if (typeof feature_is_radar !== 'undefined' && feature_is_radar) { // Register cross SDF icons for radar locations const sz = 36; const pad = 6; if (!map.hasImage('cross-sdf')) { const canvas = document.createElement('canvas'); canvas.width = sz; canvas.height = sz; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, sz, sz); ctx.strokeStyle = '#000000'; ctx.lineWidth = 6; ctx.lineCap = 'round'; ctx.beginPath(); // Horizontal ctx.moveTo(pad, sz / 2); ctx.lineTo(sz - pad, sz / 2); // Vertical ctx.moveTo(sz / 2, pad); ctx.lineTo(sz / 2, sz - pad); ctx.stroke(); map.addImage('cross-sdf', { width: sz, height: sz, data: ctx.getImageData(0, 0, sz, sz).data }, { sdf: true }); } if (!map.hasImage('cross-sdf-border')) { const bc = document.createElement('canvas'); bc.width = sz; bc.height = sz; const bctx = bc.getContext('2d'); bctx.clearRect(0, 0, sz, sz); bctx.strokeStyle = '#000000'; bctx.lineWidth = 12; bctx.lineCap = 'round'; bctx.beginPath(); // Horizontal bctx.moveTo(pad, sz / 2); bctx.lineTo(sz - pad, sz / 2); // Vertical bctx.moveTo(sz / 2, pad); bctx.lineTo(sz / 2, sz - pad); bctx.stroke(); map.addImage('cross-sdf-border', { width: sz, height: sz, data: bctx.getImageData(0, 0, sz, sz).data }, { sdf: true }); } map.addLayer({ 'id': 'traffic-light-outline', 'type': 'symbol', 'source': 'traffic-light', 'layout': { 'icon-image': 'cross-sdf-border', 'icon-size': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 22, 1.2], 'icon-allow-overlap': true, 'icon-ignore-placement': true, }, 'paint': { 'icon-color': '#ffffff' } }); map.addLayer({ 'id': 'traffic-light', 'type': 'symbol', 'source': 'traffic-light', 'layout': { 'icon-image': 'cross-sdf', 'icon-size': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 22, 1.2], 'icon-allow-overlap': true, 'icon-ignore-placement': true, }, 'paint': { 'icon-color': [ 'match', ['get', 'status'], 'active', '#0094ff', 'in_preparation', '#f59f00', 'under_repair', '#f76707', 'voluntarily_out_of_service', '#e03131', 'inactive', '#e03131', 'archived', '#6c757d', '#6c757d' ] } }); } else { map.addLayer({ 'id': 'traffic-light', 'type': 'circle', 'source': 'traffic-light', 'paint': { 'circle-radius': { 'base': 1.75, 'stops': [ [12, 2], [22, 300] ] }, 'circle-color': [ 'match', ['get', 'status'], 'active', '#0094ff', 'in_preparation', '#f59f00', 'under_repair', '#f76707', 'voluntarily_out_of_service', '#e03131', 'inactive', '#e03131', 'archived', '#6c757d', '#6c757d' ], 'circle-opacity': 0.7 } }); } // Add assets layer if (typeof assets_geojson !== 'undefined' && assets_geojson.features && assets_geojson.features.length > 0) { console.log('Adding assets layer with', assets_geojson.features.length, 'features'); const iconSize = [ 'interpolate', ['linear'], ['zoom'], 8, 0.1, 14, 0.5, 22, 1 ]; // Normalise the image onto a fixed-size canvas so that icon-size // acts as a true multiplier regardless of the SVG's intrinsic dimensions. function loadImage(url, size = 64) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); const scale = Math.min(size / img.width, size / img.height); const w = img.width * scale; const h = img.height * scale; ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); resolve(ctx.getImageData(0, 0, size, size)); }; img.onerror = (e) => reject(e); img.src = url; }); } function registerBootstrapGlyphSdfIcon(map, iconName, codepoint, size = 32, color = '#333333') { if (map.hasImage(iconName)) return Promise.resolve(); return document.fonts.ready.then(() => { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, size, size); ctx.font = `${Math.round(size * 0.85)}px "bootstrap-icons"`; ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(String.fromCodePoint(parseInt(codepoint, 16)), size / 2, size / 2); const imageData = ctx.getImageData(0, 0, size, size); if (!map.hasImage(iconName)) { map.addImage(iconName, imageData); } }); } Promise.all([ loadImage(traffic_light_icon_url, 64), loadImage(simple_pole_icon_url, 64), loadImage(electrical_cabinet_icon_url, 32), loadImage(radar_preventif_icon_url, 64), registerBootstrapGlyphSdfIcon(map, 'icon-trafficlightsequipment', 'f1d5', 32, '#331110') ]).then(([tlImg, poleImg, cabinetImg, radarImg]) => { map.addImage('traffic-light-icon', tlImg); map.addImage('simple-pole-icon', poleImg); map.addImage('electrical-cabinet-icon', cabinetImg); map.addImage('radar-preventif-icon', radarImg); // Single source for all assets map.addSource('assets', { type: 'geojson', data: assets_geojson }); // Layer for cables map.addLayer({ id: 'assets-cables', type: 'line', source: 'assets', filter: ['==', ['get', 'asset_type'], 'trafficlightcable'], layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': ['coalesce', ['get', 'color'], '#e53935'], 'line-width': 2, 'line-opacity': 0.9, 'line-dasharray': [2.5, 2] } }); // Layer for traffic light poles (standard and radar poles) map.addLayer({ id: 'assets-poles', type: 'symbol', source: 'assets', filter: ['match', ['get', 'asset_type'], ['trafficlightpole', 'trafficlightradarpole'], true, false], layout: { 'icon-image': [ 'match', ['get', 'asset_type'], 'trafficlightradarpole', 'simple-pole-icon', 'traffic-light-icon' ], 'icon-size': iconSize, 'icon-allow-overlap': true, 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 11, 'text-anchor': 'top', 'text-offset': [0, 1.2], 'text-allow-overlap': false, 'text-optional': true }, paint: { 'icon-opacity': 0.9, 'text-color': '#333333', 'text-halo-color': 'rgba(255, 255, 255, 0.85)', 'text-halo-width': 2 } }); // Layer for prevention radars map.addLayer({ id: 'assets-radars', type: 'symbol', source: 'assets', filter: ['==', ['get', 'asset_type'], 'trafficlightradar'], layout: { 'icon-image': 'radar-preventif-icon', 'icon-size': iconSize, 'icon-allow-overlap': true, 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 11, 'text-anchor': 'top', 'text-offset': [0, 1.2], 'text-allow-overlap': false, 'text-optional': true }, paint: { 'icon-opacity': 0.9, 'text-color': '#333333', 'text-halo-color': 'rgba(255, 255, 255, 0.85)', 'text-halo-width': 2 } }); // Layer for electrical cabinets map.addLayer({ id: 'assets-cabinets', type: 'symbol', source: 'assets', filter: ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'], layout: { 'icon-image': 'electrical-cabinet-icon', 'icon-size': iconSize, 'icon-allow-overlap': true, 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 11, 'text-anchor': 'top', 'text-offset': [0, 1.2], 'text-allow-overlap': false, 'text-optional': true }, paint: { 'icon-opacity': 0.9, 'text-color': '#333333', 'text-halo-color': 'rgba(255, 255, 255, 0.85)', 'text-halo-width': 2 } }); // Layer for generic equipment (controllers, detectors, hardwares) map.addLayer({ id: 'assets-equipment', type: 'symbol', source: 'assets', filter: ['match', ['get', 'asset_type'], ['trafficlightcontroller', 'trafficlightdetector', 'trafficlightcontrollerhardware', 'trafficlightnetworkhardware', 'trafficlightvoltagehardware'], true, false], layout: { 'icon-image': 'icon-trafficlightsequipment', 'icon-size': iconSize, 'icon-allow-overlap': true, 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 11, 'text-anchor': 'top', 'text-offset': [0, 1.2], 'text-allow-overlap': false, 'text-optional': true }, paint: { 'icon-opacity': 0.9, 'text-color': '#333333', 'text-halo-color': 'rgba(255, 255, 255, 0.85)', 'text-halo-width': 2 } }); // Click handler for all layers ['assets-poles', 'assets-cabinets', 'assets-radars', 'assets-equipment', 'assets-cables'].forEach(layerId => { map.on('click', layerId, (e) => { if (e.features.length > 0) { const feature = e.features[0]; if (feature.properties.detail_url) { window.location.href = feature.properties.detail_url; } } }); map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); }); }).catch(e => { console.error('Failed to load asset icons:', e); }); } }); const fileInput = document.getElementById('fileInput'); const fileContainer = document.getElementById('fileFieldsContainer'); // Configuration pour l'ancien système (fallback) const documentTypeChoices = { 'as_built': 'As Built', 'cross_plan': 'Plan de croix', 'other': 'Autre', 'programming_file': 'Fichier de programmation', 'connexion_plan': 'Plan de raccordement' }; // Récupérer la configuration du nouveau système de gestion documentaire let documentConfig = { use_managed_documents: false, can_add_unique_tag_documents: false, config_url: null }; let availableTags = []; let availableFolders = []; try { const configEl = document.getElementById('documentConfigData'); if (configEl) { documentConfig = JSON.parse(configEl.textContent); } } catch (e) { console.log('Document config not found, using legacy system'); } // Charger la configuration complète via l'API async function loadDocumentConfig() { if (!documentConfig.config_url) return; try { const response = await fetch(documentConfig.config_url); const data = await response.json(); documentConfig.use_managed_documents = data.use_managed_documents; documentConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents; availableTags = data.tags || []; availableFolders = data.folders || []; // Exposer globalement pour le gestionnaire de fichiers window.availableTags = availableTags; window.availableFolders = availableFolders; } catch (e) { console.log('Failed to load document config:', e); } } // Charger la config au démarrage loadDocumentConfig(); function renderFileFields() { if (!fileContainer) return; fileContainer.innerHTML = ''; const files = Array.from(fileInput.files); files.forEach((file, index) => { const row = document.createElement('div'); row.classList.add('d-flex', 'align-items-center', 'gap-3', 'mb-2', 'p-2', 'border', 'rounded'); // Nom du fichier (en lecture seule) const fileName = document.createElement('span'); fileName.classList.add('flex-grow-1', 'text-truncate'); fileName.style.maxWidth = '200px'; fileName.textContent = file.name; fileName.title = file.name; row.appendChild(fileName); if (documentConfig.use_managed_documents && availableTags && availableTags.length > 0) { // Nouveau système: select pour le tag const tagSelect = document.createElement('select'); tagSelect.name = `tag_${index}`; tagSelect.classList.add('form-select', 'w-auto'); tagSelect.style.minWidth = '150px'; const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- Choisir un tag --'; tagSelect.appendChild(defaultOption); availableTags.forEach(tag => { const option = document.createElement('option'); option.value = tag.slug; option.textContent = tag.name; option.dataset.isUnique = tag.is_unique; // Désactiver les tags uniques si l'utilisateur n'a pas les permissions if (tag.is_unique && !documentConfig.can_add_unique_tag_documents) { option.disabled = true; option.textContent += ' (non autorisé)'; } tagSelect.appendChild(option); }); row.appendChild(tagSelect); // Select pour le répertoire (affiché uniquement pour les tags non-uniques) const folderSelect = document.createElement('select'); folderSelect.name = `folder_${index}`; folderSelect.classList.add('form-select', 'w-auto', 'd-none'); folderSelect.style.minWidth = '180px'; if (availableFolders && availableFolders.length > 0) { availableFolders.forEach(folder => { const option = document.createElement('option'); option.value = folder.id; option.textContent = folder.path; if (folder.is_root) { option.selected = true; } folderSelect.appendChild(option); }); } row.appendChild(folderSelect); // Champ pour les paramètres d'unicité (cables pour crossplan) const uniqueParamInput = document.createElement('input'); uniqueParamInput.type = 'text'; uniqueParamInput.name = `unique_param_${index}`; uniqueParamInput.classList.add('form-control', 'w-auto', 'd-none'); uniqueParamInput.style.minWidth = '120px'; uniqueParamInput.placeholder = 'Ex: A,B'; row.appendChild(uniqueParamInput); // Afficher/masquer le sélecteur de répertoire et les paramètres selon le tag tagSelect.addEventListener('change', function() { const selectedOption = tagSelect.options[tagSelect.selectedIndex]; const isUnique = selectedOption.dataset.isUnique === 'true' || selectedOption.dataset.isUnique === 'True'; const tagSlug = tagSelect.value; if (isUnique || tagSelect.value === '') { folderSelect.classList.add('d-none'); } else { folderSelect.classList.remove('d-none'); } // Afficher le champ de paramètre pour crossplan if (tagSlug === 'crossplan') { uniqueParamInput.classList.remove('d-none'); uniqueParamInput.placeholder = 'Câbles (ex: C1,C2)'; } else { uniqueParamInput.classList.add('d-none'); } }); } else { // Ancien système: select type de document const select = document.createElement('select'); select.name = `document_type_${index}`; select.classList.add('form-select', 'w-25'); for (const [value, label] of Object.entries(documentTypeChoices)) { const option = document.createElement('option'); option.value = value; option.textContent = label; if (value === 'other') { option.selected = true; } select.appendChild(option); } row.appendChild(select); } // Bouton de suppression const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.classList.add('btn', 'btn-outline-danger', 'btn-sm'); deleteBtn.innerHTML = ''; deleteBtn.onclick = () => { files.splice(index, 1); const dt = new DataTransfer(); files.forEach(f => dt.items.add(f)); fileInput.files = dt.files; renderFileFields(); // re-render }; row.appendChild(deleteBtn); fileContainer.appendChild(row); }); // Total const countInput = document.createElement('input'); countInput.type = 'hidden'; countInput.name = 'file_count'; countInput.value = files.length; fileContainer.appendChild(countInput); } if (fileInput) { fileInput.addEventListener('change', renderFileFields); } const deleteDocumentModal = document.getElementById('documentDeleteModal'); if (deleteDocumentModal) { deleteDocumentModal.addEventListener('show.bs.modal', function (event) { const button = event.relatedTarget; const fileName = button.getAttribute('data-filename'); const deleteUrl = button.getAttribute('data-delete-url'); const fileNameContainer = document.getElementById('deleteDocumentFileName'); const deleteForm = document.getElementById('deleteDocumentForm'); fileNameContainer.textContent = fileName; deleteForm.action = deleteUrl; // Ensure the page reloads back to the current URL after deletion let nextInput = deleteForm.querySelector('input[name="next"]'); if (!nextInput) { nextInput = document.createElement('input'); nextInput.type = 'hidden'; nextInput.name = 'next'; deleteForm.appendChild(nextInput); } nextInput.value = window.location.href; }); } historyModal = document.getElementById('documentHistoryModal'); if (historyModal) { document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { const button = event.relatedTarget; const type = button.getAttribute('data-document-type'); const tagSlug = button.getAttribute('data-tag-slug'); // Support both the generic data-location-id (document_viewer component) // and the legacy data-intersection-id (old-system buttons). const intersectionId = button.getAttribute('data-location-id') || button.getAttribute('data-intersection-id'); const useManaged = button.getAttribute('data-use-managed') === 'true'; const uniqueParams = button.getAttribute('data-unique-params'); let url; if (useManaged && tagSlug) { // Nouveau système de gestion documentaire url = `${document_history_url}?intersection_id=${intersectionId}&tag_slug=${tagSlug}`; if (uniqueParams && uniqueParams !== '{}' && uniqueParams !== 'None') { url += `&unique_params=${encodeURIComponent(uniqueParams)}`; } } else { // Ancien système url = `${document_history_url}?intersection_id=${intersectionId}&document_type=${type}`; } fetch(url) .then(response => response.json()) .then(data => { document.getElementById('documentHistoryContent').innerHTML = data.html; }); }); } // ── DXF Background Layer Loader ── function ensureDxfSourcesAndLayers() { if (!map.getSource('dxf-layout')) { map.addSource('dxf-layout', { type: 'geojson', data: { type: "FeatureCollection", features: [] } }); const beforeId = map.getLayer('traffic-light') ? 'traffic-light' : undefined; map.addLayer({ 'id': 'dxf-layout-fills', 'type': 'fill', 'source': 'dxf-layout', 'layout': { 'visibility': 'visible' }, 'paint': { 'fill-color': ['coalesce', ['get', 'color'], '#94a3b8'], 'fill-opacity': 0.1 } }, beforeId); map.addLayer({ 'id': 'dxf-layout-lines', 'type': 'line', 'source': 'dxf-layout', 'layout': { 'visibility': 'visible' }, 'paint': { 'line-color': ['coalesce', ['get', 'color'], '#475569'], 'line-width': 1.2, 'line-opacity': 0.7 } }, beforeId); map.addLayer({ 'id': 'dxf-layout-symbols', 'type': 'symbol', 'source': 'dxf-layout', 'filter': ['has', 'Text'], 'layout': { 'visibility': 'visible', 'text-field': ['get', 'Text'], 'text-font': ['Noto Sans Regular'], 'text-size': [ 'interpolate', ['linear'], ['zoom'], 17, ['coalesce', ['*', ['get', 'text_height'], 6], 10], 21, ['coalesce', ['*', ['get', 'text_height'], 24], 14] ], 'text-rotate': ['coalesce', ['*', ['get', 'text_angle'], -1], 0], 'text-rotation-alignment': 'map', 'text-anchor': ['coalesce', ['get', 'text_anchor'], 'center'], 'text-allow-overlap': true, 'text-ignore-placement': true }, 'paint': { 'text-color': ['coalesce', ['get', 'color'], '#000000'], 'text-halo-color': '#ffffff', 'text-halo-width': 2 } }, beforeId); } } // ── Georeferenced Plans & DXF Background Loader ── const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData'); let georeferencedDocuments = []; if (georeferencedDocumentsDataEl) { try { georeferencedDocuments = JSON.parse(georeferencedDocumentsDataEl.textContent); } catch (e) { console.error("Error parsing georeferenced documents data:", e); } } function showGeorefImage(doc, active, opacity) { const srcId = `georef-image-overlay-${doc.id}`; const lyrId = `georef-image-overlay-layer-${doc.id}`; if (!active) { if (map.getLayer(lyrId)) { map.setLayoutProperty(lyrId, 'visibility', 'none'); } return; } if (map.getSource(srcId)) { if (map.getLayer(lyrId)) { map.setLayoutProperty(lyrId, 'visibility', 'visible'); map.setPaintProperty(lyrId, 'raster-opacity', opacity); } } else { map.addSource(srcId, { type: 'image', url: doc.url, coordinates: doc.coords }); let beforeId = undefined; if (map.getLayer('dxf-layout-fills')) { beforeId = 'dxf-layout-fills'; } else if (map.getLayer('dxf-layout-lines')) { beforeId = 'dxf-layout-lines'; } else if (map.getLayer('traffic-light')) { beforeId = 'traffic-light'; } map.addLayer({ 'id': lyrId, 'type': 'raster', 'source': srcId, 'layout': { 'visibility': 'visible' }, 'paint': { 'raster-opacity': opacity } }, beforeId); } // Zoom/fitBounds to document const coords = doc.coords; const bounds = coords.reduce((acc, coord) => { return acc.extend(coord); }, new maplibregl.LngLatBounds(coords[0], coords[0])); map.fitBounds(bounds, { padding: 40, maxZoom: 19 }); } let fullDxfGeoJson = null; function fetchDxfGeoJson(file) { const spinner = document.getElementById('dxf-detail-layers-spinner'); if (spinner) spinner.style.display = 'block'; const url = `/assets/trafficlights/intersections/${intersectionId}/dxf-layout-geojson/?file=${encodeURIComponent(file)}&detail=true&layers=all`; fetch(url) .then(response => response.json()) .then(data => { fullDxfGeoJson = data; applyDxfVectorFilters(); if (spinner) spinner.style.display = 'none'; }) .catch(err => { console.error("Error fetching DXF GeoJSON:", err); if (spinner) spinner.style.display = 'none'; }); } function applyDxfVectorFilters() { if (!fullDxfGeoJson) return; const selectedLayers = []; document.querySelectorAll('.dxf-vector-layer-toggle:checked').forEach(cb => { selectedLayers.push(cb.dataset.layer); }); const filteredFeatures = fullDxfGeoJson.features.filter(f => { const layerName = f.properties && (f.properties.Layer || f.properties.layer); return selectedLayers.includes(layerName); }); const filteredData = { ...fullDxfGeoJson, features: filteredFeatures }; ensureDxfSourcesAndLayers(); if (map.getSource('dxf-layout')) { map.getSource('dxf-layout').setData(filteredData); ['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(lId => { if (map.getLayer(lId)) { map.setLayoutProperty(lId, 'visibility', 'visible'); } }); } } function updateButtonActiveState() { const btn = document.getElementById('btn-load-georef-plan'); if (!btn) return; let anyImageActive = false; document.querySelectorAll('.image-plan-toggle').forEach(cb => { if (cb.checked) anyImageActive = true; }); let anyVectorActive = false; document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => { if (cb.checked) anyVectorActive = true; }); if (anyImageActive || anyVectorActive) { btn.classList.remove('btn-light', 'bg-white', 'border'); btn.classList.add('btn-primary'); } else { btn.classList.remove('btn-primary'); btn.classList.add('btn-light', 'bg-white', 'border'); } } const noGeorefImagesAlert = document.getElementById('no-georef-images-alert'); const imagePlansList = document.getElementById('image-plans-list'); if (imagePlansList) { imagePlansList.innerHTML = ''; if (georeferencedDocuments.length === 0) { if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'block'; } else { if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'none'; georeferencedDocuments.forEach(doc => { const item = document.createElement('div'); item.className = 'image-plan-item p-3 bg-light border rounded-3 d-flex flex-column gap-2'; item.innerHTML = `
${doc.title}
`; imagePlansList.appendChild(item); const toggle = item.querySelector('.image-plan-toggle'); const opacityContainer = item.querySelector('.image-plan-opacity-container'); const slider = item.querySelector('.image-plan-opacity-slider'); const opacityVal = item.querySelector('.opacity-val'); toggle.addEventListener('change', () => { const active = toggle.checked; if (active) { opacityContainer.style.display = 'block'; } else { opacityContainer.style.display = 'none'; } const opacity = parseFloat(slider.value) / 100.0; showGeorefImage(doc, active, opacity); updateButtonActiveState(); }); slider.addEventListener('input', () => { const val = slider.value; opacityVal.textContent = val + '%'; const opacity = parseFloat(val) / 100.0; showGeorefImage(doc, toggle.checked, opacity); }); }); } } const dxfFileSelect = document.getElementById('select-dxf-file'); let detailLayersFetched = {}; let detailLayersResponseData = {}; function fetchDetailLayers() { if (!dxfFileSelect) return; const file = dxfFileSelect.value; if (!file) { const section = document.getElementById('dxf-detail-layers-section'); if (section) section.style.display = 'none'; return; } if (detailLayersFetched[file]) { renderDetailLayers(file); fetchDxfGeoJson(file); return; } const spinner = document.getElementById('dxf-detail-layers-spinner'); const list = document.getElementById('dxf-detail-layers-list'); const section = document.getElementById('dxf-detail-layers-section'); if (section) section.style.display = 'block'; if (spinner) spinner.style.display = 'block'; if (list) list.style.display = 'none'; fetch(`/assets/trafficlights/intersections/${intersectionId}/dxf-layers/?file=${encodeURIComponent(file)}`) .then(response => response.json()) .then(data => { detailLayersFetched[file] = data.layers || []; detailLayersResponseData[file] = data; renderDetailLayers(file); fetchDxfGeoJson(file); if (spinner) spinner.style.display = 'none'; if (list) list.style.display = 'block'; }) .catch(err => { console.error("Error fetching detail layers:", err); if (spinner) { spinner.innerHTML = `
Erreur de chargement.
`; } }); } function renderDetailLayers(file) { const container = document.getElementById('dxf-detail-layers-list'); if (!container) return; container.innerHTML = ''; const layers = detailLayersFetched[file] || []; const responseData = detailLayersResponseData[file] || {}; const selectedLayers = responseData.selected_layers || []; layers.forEach(layer => { const isChecked = false; const div = document.createElement('div'); div.className = 'form-check py-1 d-flex align-items-center justify-content-between'; div.innerHTML = `
`; container.appendChild(div); const toggle = div.querySelector('.dxf-vector-layer-toggle'); toggle.addEventListener('change', () => { applyDxfVectorFilters(); updateButtonActiveState(); }); }); } if (dxfFileSelect) { dxfFileSelect.addEventListener('change', () => { fetchDetailLayers(); }); // Auto-load if there is a default select value or single dxf if (dxfFileSelect.value) { fetchDetailLayers(); } } const btnToggleAllLayers = document.getElementById('dxf-detail-toggle-all-layers'); if (btnToggleAllLayers) { btnToggleAllLayers.addEventListener('click', () => { const checkboxes = document.querySelectorAll('.dxf-vector-layer-toggle'); const allChecked = Array.from(checkboxes).every(cb => cb.checked); checkboxes.forEach(cb => { cb.checked = !allChecked; }); btnToggleAllLayers.textContent = allChecked ? "Tout cocher" : "Décocher tout"; applyDxfVectorFilters(); updateButtonActiveState(); }); } const btnDisableAllPlans = document.getElementById('btn-disable-all-plans'); if (btnDisableAllPlans) { btnDisableAllPlans.addEventListener('click', () => { document.querySelectorAll('.image-plan-toggle').forEach(cb => { if (cb.checked) { cb.checked = false; cb.dispatchEvent(new Event('change')); } }); document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => { if (cb.checked) { cb.checked = false; } }); applyDxfVectorFilters(); const modalEl = document.getElementById('georefPlanModal'); if (modalEl) { const modal = bootstrap.Modal.getInstance(modalEl); if (modal) modal.hide(); } updateButtonActiveState(); }); }