loko/streetup/assets/static/assets/trafficlights_intersection_detail.js
2026-07-22 14:48:40 +02:00

1088 lines
No EOL
41 KiB
JavaScript

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 = '<i class="bi bi-geo-alt-fill text-primary"></i> 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 = '<i class="bi bi-x-lg"></i>';
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 = `
<div class="d-flex justify-content-between align-items-center">
<span class="text-dark font-weight-bold" style="font-size: 0.9rem;">
<i class="bi ${doc.is_dxf ? 'bi-file-earmark-code text-success' : 'bi-file-earmark-image text-primary'} me-2"></i>${doc.title}
</span>
<div class="form-check form-switch">
<input class="form-check-input image-plan-toggle" type="checkbox" role="switch" id="switch-georef-img-${doc.id}" data-id="${doc.id}">
</div>
</div>
<div class="image-plan-opacity-container" id="opacity-container-${doc.id}" style="display: none;">
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between mb-1">
<span>Opacité</span>
<span class="opacity-val text-primary font-weight-bold">95%</span>
</label>
<input type="range" class="form-range image-plan-opacity-slider" min="0" max="100" value="95" id="range-georef-img-opacity-${doc.id}" data-id="${doc.id}">
</div>
`;
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 = `<div class="text-danger small"><i class="bi bi-exclamation-triangle-fill"></i> Erreur de chargement.</div>`;
}
});
}
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 = `
<div class="d-flex align-items-center">
<span class="color-indicator me-2" style="display: inline-block; width: 14px; height: 14px; border-radius: 3px; background-color: ${layer.color}; border: 1px solid rgba(0,0,0,0.1);"></span>
<label class="form-check-label text-dark font-weight-medium small" for="dxf-detail-layer-${layer.name}">
${layer.name}
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input dxf-vector-layer-toggle" type="checkbox" role="switch"
id="dxf-detail-layer-${layer.name}" data-layer="${layer.name}" ${isChecked ? 'checked' : ''}>
</div>
`;
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();
});
}