1088 lines
No EOL
41 KiB
JavaScript
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();
|
|
});
|
|
} |