766 lines
30 KiB
JavaScript
766 lines
30 KiB
JavaScript
// Asset Generic Detail Map - MapLibre with geometry display and editing support
|
|
// This module initializes a map for the generic asset detail view
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
const MAPTILER_STYLE = window.MAP_STYLE_LIGHT;
|
|
const ORTHO_WMTS_URL = '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}';
|
|
const URBIS_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}';
|
|
|
|
let map = null;
|
|
let assetGeometry = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeMap();
|
|
});
|
|
|
|
function initializeMap() {
|
|
const mapContainer = document.getElementById('asset-map');
|
|
if (!mapContainer) return;
|
|
|
|
const container = document.getElementById('asset-detail-container');
|
|
if (!container) return;
|
|
|
|
// Get geometry from data attribute
|
|
const geometryJson = container.dataset.assetGeometry;
|
|
if (geometryJson) {
|
|
try {
|
|
assetGeometry = JSON.parse(geometryJson);
|
|
} catch (e) {
|
|
console.warn('Failed to parse asset geometry:', e);
|
|
}
|
|
}
|
|
|
|
// Fallback to lon/lat if no geometry
|
|
if (!assetGeometry) {
|
|
const lonElement = document.querySelector('.asset-editable-field[data-field="lon"] .field-value');
|
|
const latElement = document.querySelector('.asset-editable-field[data-field="lat"] .field-value');
|
|
|
|
if (lonElement && latElement) {
|
|
const lon = parseFloat(lonElement.textContent.trim());
|
|
const lat = parseFloat(latElement.textContent.trim());
|
|
|
|
if (!isNaN(lon) && !isNaN(lat)) {
|
|
assetGeometry = {
|
|
type: 'Point',
|
|
coordinates: [lon, lat]
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate center and bounds (use Brussels center as default if no geometry)
|
|
const center = assetGeometry ? calculateCenter(assetGeometry) : [4.3517, 50.8503]; // Brussels center
|
|
const bounds = assetGeometry ? calculateBounds(assetGeometry) : null;
|
|
|
|
// Initialize MapLibre map
|
|
map = new maplibregl.Map({
|
|
container: 'asset-map',
|
|
style: MAPTILER_STYLE,
|
|
center: center,
|
|
zoom: assetGeometry ? 17 : 12, // Zoom out if no geometry
|
|
pitch: 0,
|
|
antialias: true
|
|
});
|
|
|
|
// Make map available globally for geometry editing
|
|
window.map = map;
|
|
|
|
// Add navigation controls
|
|
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
|
|
|
// Add layer switcher (Urbis default, Vlaanderen alternative)
|
|
addOrthoLayerSwitcher();
|
|
|
|
map.on('load', function() {
|
|
|
|
// Add asset geometry if available
|
|
if (assetGeometry) {
|
|
addAssetGeometry(assetGeometry);
|
|
|
|
// Fit to bounds if available
|
|
if (bounds) {
|
|
map.fitBounds(bounds, {
|
|
padding: 50,
|
|
maxZoom: 18
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add sibling assets from the same intersection (trafficlights only)
|
|
const thematicCode = container.dataset.thematicCode;
|
|
if (thematicCode === 'trafficlights') {
|
|
addIntersectionAssetsLayer(container);
|
|
addConnectionLinesLayer();
|
|
}
|
|
});
|
|
}
|
|
|
|
function calculateCenter(geometry) {
|
|
if (!geometry) return [4.3517, 50.8503]; // Default Brussels center
|
|
|
|
switch (geometry.type) {
|
|
case 'Point':
|
|
return geometry.coordinates;
|
|
|
|
case 'LineString':
|
|
// Return midpoint of the line
|
|
const lineCoords = geometry.coordinates;
|
|
const midIndex = Math.floor(lineCoords.length / 2);
|
|
return lineCoords[midIndex] || lineCoords[0];
|
|
|
|
case 'Polygon':
|
|
// Return centroid of the polygon
|
|
const polyCoords = geometry.coordinates[0];
|
|
if (!polyCoords || polyCoords.length === 0) return [4.3517, 50.8503];
|
|
|
|
let sumLon = 0, sumLat = 0;
|
|
polyCoords.forEach(coord => {
|
|
sumLon += coord[0];
|
|
sumLat += coord[1];
|
|
});
|
|
return [sumLon / polyCoords.length, sumLat / polyCoords.length];
|
|
|
|
case 'MultiPoint':
|
|
const firstPoint = geometry.coordinates[0];
|
|
return firstPoint || [4.3517, 50.8503];
|
|
|
|
case 'MultiLineString':
|
|
const firstLine = geometry.coordinates[0];
|
|
if (firstLine && firstLine.length > 0) {
|
|
return firstLine[Math.floor(firstLine.length / 2)];
|
|
}
|
|
return [4.3517, 50.8503];
|
|
|
|
case 'MultiPolygon':
|
|
const firstPoly = geometry.coordinates[0];
|
|
if (firstPoly && firstPoly[0] && firstPoly[0].length > 0) {
|
|
let sumLon = 0, sumLat = 0;
|
|
firstPoly[0].forEach(coord => {
|
|
sumLon += coord[0];
|
|
sumLat += coord[1];
|
|
});
|
|
return [sumLon / firstPoly[0].length, sumLat / firstPoly[0].length];
|
|
}
|
|
return [4.3517, 50.8503];
|
|
|
|
default:
|
|
return [4.3517, 50.8503];
|
|
}
|
|
}
|
|
|
|
function calculateBounds(geometry) {
|
|
if (!geometry) return null;
|
|
|
|
let allCoords = [];
|
|
|
|
function extractCoords(coords) {
|
|
if (Array.isArray(coords[0])) {
|
|
coords.forEach(extractCoords);
|
|
} else {
|
|
allCoords.push(coords);
|
|
}
|
|
}
|
|
|
|
extractCoords(geometry.coordinates);
|
|
|
|
if (allCoords.length === 0) return null;
|
|
|
|
let minLon = Infinity, maxLon = -Infinity;
|
|
let minLat = Infinity, maxLat = -Infinity;
|
|
|
|
allCoords.forEach(coord => {
|
|
minLon = Math.min(minLon, coord[0]);
|
|
maxLon = Math.max(maxLon, coord[0]);
|
|
minLat = Math.min(minLat, coord[1]);
|
|
maxLat = Math.max(maxLat, coord[1]);
|
|
});
|
|
|
|
// Add padding for single point
|
|
if (minLon === maxLon && minLat === maxLat) {
|
|
const offset = 0.001;
|
|
return [[minLon - offset, minLat - offset], [maxLon + offset, maxLat + offset]];
|
|
}
|
|
|
|
return [[minLon, minLat], [maxLon, maxLat]];
|
|
}
|
|
|
|
function addAssetGeometry(geometry) {
|
|
if (!map || !geometry) return;
|
|
|
|
const geojson = {
|
|
type: 'Feature',
|
|
geometry: geometry,
|
|
properties: {}
|
|
};
|
|
|
|
// Add source
|
|
map.addSource('asset-geometry', {
|
|
type: 'geojson',
|
|
data: geojson
|
|
});
|
|
|
|
// Add layers based on geometry type
|
|
switch (geometry.type) {
|
|
case 'Point':
|
|
case 'MultiPoint':
|
|
addPointLayer();
|
|
break;
|
|
|
|
case 'LineString':
|
|
case 'MultiLineString':
|
|
addLineLayer();
|
|
break;
|
|
|
|
case 'Polygon':
|
|
case 'MultiPolygon':
|
|
addPolygonLayer();
|
|
break;
|
|
|
|
default:
|
|
// Try to add all layer types
|
|
addPolygonLayer();
|
|
addLineLayer();
|
|
addPointLayer();
|
|
}
|
|
}
|
|
|
|
function addPointLayer() {
|
|
// Outer glow ring (yellow highlight for current asset)
|
|
map.addLayer({
|
|
id: 'asset-point-outer',
|
|
type: 'circle',
|
|
source: 'asset-geometry',
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
paint: {
|
|
'circle-radius': 14,
|
|
'circle-color': '#FFD700',
|
|
'circle-opacity': 0.5,
|
|
'circle-stroke-width': 0
|
|
}
|
|
});
|
|
|
|
// Middle ring (white border)
|
|
map.addLayer({
|
|
id: 'asset-point-middle',
|
|
type: 'circle',
|
|
source: 'asset-geometry',
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
paint: {
|
|
'circle-radius': 11,
|
|
'circle-color': '#ffffff',
|
|
'circle-stroke-width': 0
|
|
}
|
|
});
|
|
|
|
// Inner circle (orange fill, blue stroke)
|
|
map.addLayer({
|
|
id: 'asset-point-inner',
|
|
type: 'circle',
|
|
source: 'asset-geometry',
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
paint: {
|
|
'circle-radius': 8,
|
|
'circle-color': '#FF6B00',
|
|
'circle-stroke-width': 2,
|
|
'circle-stroke-color': '#0056b3'
|
|
}
|
|
});
|
|
}
|
|
|
|
function addLineLayer() {
|
|
// Line outline (white border)
|
|
map.addLayer({
|
|
id: 'asset-line-outline',
|
|
type: 'line',
|
|
source: 'asset-geometry',
|
|
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']],
|
|
layout: {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
paint: {
|
|
'line-color': '#ffffff',
|
|
'line-width': 3.5
|
|
}
|
|
});
|
|
|
|
// Line fill (blue)
|
|
map.addLayer({
|
|
id: 'asset-line-fill',
|
|
type: 'line',
|
|
source: 'asset-geometry',
|
|
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']],
|
|
layout: {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
paint: {
|
|
'line-color': '#007bff',
|
|
'line-width': 2
|
|
}
|
|
});
|
|
}
|
|
|
|
function addPolygonLayer() {
|
|
// Polygon fill
|
|
map.addLayer({
|
|
id: 'asset-polygon-fill',
|
|
type: 'fill',
|
|
source: 'asset-geometry',
|
|
filter: ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
|
|
paint: {
|
|
'fill-color': '#007bff',
|
|
'fill-opacity': 0.3
|
|
}
|
|
});
|
|
|
|
// Polygon outline
|
|
map.addLayer({
|
|
id: 'asset-polygon-outline',
|
|
type: 'line',
|
|
source: 'asset-geometry',
|
|
filter: ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
|
|
layout: {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
paint: {
|
|
'line-color': '#007bff',
|
|
'line-width': 2
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add sibling assets from the same intersection as semi-transparent markers
|
|
function addIntersectionAssetsLayer(container) {
|
|
const el = document.getElementById('intersection-assets-geojson');
|
|
if (!el) return;
|
|
|
|
let intersectionGeoJSON;
|
|
try {
|
|
intersectionGeoJSON = JSON.parse(el.textContent);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
const tlIconUrl = container.dataset.tlIconUrl;
|
|
const simplePoleIconUrl = container.dataset.simplePoleIconUrl;
|
|
const cabinetIconUrl = container.dataset.cabinetIconUrl;
|
|
const radarIconUrl = container.dataset.radarIconUrl;
|
|
|
|
// Load image at full opacity onto a fixed-size canvas.
|
|
// Opacity is controlled via MapLibre's icon-opacity paint property.
|
|
function loadImageToCanvas(url, size) {
|
|
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, h = img.height * scale;
|
|
ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
|
|
resolve(ctx.getImageData(0, 0, size, size));
|
|
};
|
|
img.onerror = reject;
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
const iconSize = ['interpolate', ['linear'], ['zoom'], 8, 0.1, 14, 0.5, 22, 1];
|
|
|
|
const promises = [
|
|
tlIconUrl ? loadImageToCanvas(tlIconUrl, 64) : Promise.resolve(null),
|
|
simplePoleIconUrl ? loadImageToCanvas(simplePoleIconUrl, 64) : Promise.resolve(null),
|
|
cabinetIconUrl ? loadImageToCanvas(cabinetIconUrl, 32) : Promise.resolve(null),
|
|
radarIconUrl ? loadImageToCanvas(radarIconUrl, 64) : Promise.resolve(null),
|
|
registerBootstrapGlyphSdfIcon(map, 'icon-trafficlightsequipment', 'f1d5', 32, '#331110')
|
|
];
|
|
|
|
Promise.all(promises).then(([tlImg, poleImg, cabinetImg, radarImg]) => {
|
|
if (tlImg && !map.hasImage('tl-icon')) map.addImage('tl-icon', tlImg);
|
|
if (poleImg && !map.hasImage('pole-icon')) map.addImage('pole-icon', poleImg);
|
|
if (cabinetImg && !map.hasImage('cabinet-icon')) map.addImage('cabinet-icon', cabinetImg);
|
|
if (radarImg && !map.hasImage('radar-icon')) map.addImage('radar-icon', radarImg);
|
|
|
|
// ── Sibling assets source ──────────────────────────────────────────
|
|
const hasSiblings = intersectionGeoJSON && intersectionGeoJSON.features && intersectionGeoJSON.features.length > 0;
|
|
if (hasSiblings && !map.getSource('intersection-assets')) {
|
|
map.addSource('intersection-assets', { type: 'geojson', data: intersectionGeoJSON });
|
|
}
|
|
|
|
if (hasSiblings) {
|
|
// Sibling poles (semi-transparent)
|
|
if ((tlImg || poleImg) && !map.getLayer('sibling-poles')) {
|
|
map.addLayer({
|
|
id: 'sibling-poles',
|
|
type: 'symbol',
|
|
source: 'intersection-assets',
|
|
filter: ['match', ['get', 'asset_type'], ['trafficlightpole', 'trafficlightradarpole'], true, false],
|
|
layout: {
|
|
'icon-image': [
|
|
'match',
|
|
['get', 'asset_type'],
|
|
'trafficlightradarpole', 'pole-icon',
|
|
'tl-icon'
|
|
],
|
|
'icon-size': iconSize,
|
|
'icon-allow-overlap': true,
|
|
'text-field': ['get', 'code'],
|
|
'text-font': ['Noto Sans Regular'],
|
|
'text-size': 10,
|
|
'text-anchor': 'top',
|
|
'text-offset': [0, 1.2],
|
|
'text-optional': true,
|
|
},
|
|
paint: {
|
|
'icon-opacity': 0.55,
|
|
'text-color': '#555555',
|
|
'text-halo-color': 'rgba(255,255,255,0.8)',
|
|
'text-halo-width': 1.5,
|
|
'text-opacity': 0.7,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Sibling electrical cabinets (semi-transparent)
|
|
if (cabinetImg && !map.getLayer('sibling-cabinets')) {
|
|
map.addLayer({
|
|
id: 'sibling-cabinets',
|
|
type: 'symbol',
|
|
source: 'intersection-assets',
|
|
filter: ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
|
layout: {
|
|
'icon-image': 'cabinet-icon',
|
|
'icon-size': iconSize,
|
|
'icon-allow-overlap': true,
|
|
'text-field': ['get', 'code'],
|
|
'text-font': ['Noto Sans Regular'],
|
|
'text-size': 10,
|
|
'text-anchor': 'top',
|
|
'text-offset': [0, 1.2],
|
|
'text-optional': true,
|
|
},
|
|
paint: {
|
|
'icon-opacity': 0.55,
|
|
'text-color': '#555555',
|
|
'text-halo-color': 'rgba(255,255,255,0.8)',
|
|
'text-halo-width': 1.5,
|
|
'text-opacity': 0.7,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Sibling radars (semi-transparent)
|
|
if (radarImg && !map.getLayer('sibling-radars')) {
|
|
map.addLayer({
|
|
id: 'sibling-radars',
|
|
type: 'symbol',
|
|
source: 'intersection-assets',
|
|
filter: ['==', ['get', 'asset_type'], 'trafficlightradar'],
|
|
layout: {
|
|
'icon-image': 'radar-icon',
|
|
'icon-size': iconSize,
|
|
'icon-allow-overlap': true,
|
|
'icon-offset': [
|
|
'case',
|
|
['coalesce', ['get', 'has_support'], false], ['literal', [0, -25]],
|
|
['literal', [0, 0]]
|
|
],
|
|
'text-field': ['get', 'code'],
|
|
'text-font': ['Noto Sans Regular'],
|
|
'text-size': 10,
|
|
'text-anchor': 'top',
|
|
'text-offset': [0, 1.2],
|
|
'text-optional': true,
|
|
},
|
|
paint: {
|
|
'icon-opacity': 0.55,
|
|
'text-color': '#555555',
|
|
'text-halo-color': 'rgba(255,255,255,0.8)',
|
|
'text-halo-width': 1.5,
|
|
'text-opacity': 0.7,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Sibling generic equipment (semi-transparent)
|
|
if (!map.getLayer('sibling-equipment')) {
|
|
map.addLayer({
|
|
id: 'sibling-equipment',
|
|
type: 'symbol',
|
|
source: 'intersection-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', 'code'],
|
|
'text-font': ['Noto Sans Regular'],
|
|
'text-size': 10,
|
|
'text-anchor': 'top',
|
|
'text-offset': [0, 1.2],
|
|
'text-optional': true,
|
|
},
|
|
paint: {
|
|
'icon-opacity': 0.55,
|
|
'text-color': '#555555',
|
|
'text-halo-color': 'rgba(255,255,255,0.8)',
|
|
'text-halo-width': 1.5,
|
|
'text-opacity': 0.7,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Sibling cables (line geometry)
|
|
if (!map.getLayer('sibling-cables')) {
|
|
map.addLayer({
|
|
id: 'sibling-cables',
|
|
type: 'line',
|
|
source: 'intersection-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.7,
|
|
'line-dasharray': [2.5, 2]
|
|
},
|
|
});
|
|
}
|
|
|
|
// Click to navigate to sibling assets
|
|
['sibling-poles', 'sibling-cabinets', 'sibling-radars', 'sibling-equipment', 'sibling-cables'].forEach(layerId => {
|
|
if (!map.getLayer(layerId)) return;
|
|
map.on('click', layerId, (e) => {
|
|
if (e.features.length > 0) {
|
|
const url = e.features[0].properties.detail_url;
|
|
if (url) window.location.href = url;
|
|
}
|
|
});
|
|
map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; });
|
|
map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; });
|
|
});
|
|
}
|
|
|
|
// ── Current asset icon (full opacity, on top of highlight rings) ──
|
|
const assetModel = container.dataset.assetModel;
|
|
const hasSupport = container.dataset.hasSupport === 'true';
|
|
const currentIconImage =
|
|
assetModel === 'trafficlightpole' && tlImg ? 'tl-icon' :
|
|
assetModel === 'trafficlightradarpole' && poleImg ? 'pole-icon' :
|
|
assetModel === 'trafficlightelectricalcabinet' && cabinetImg ? 'cabinet-icon' :
|
|
assetModel === 'trafficlightradar' && radarImg ? 'radar-icon' :
|
|
['trafficlightcontroller', 'trafficlightdetector', 'trafficlightcontrollerhardware', 'trafficlightnetworkhardware', 'trafficlightvoltagehardware'].includes(assetModel) ? 'icon-trafficlightsequipment' :
|
|
null;
|
|
|
|
if (currentIconImage && !map.getLayer('current-asset-icon')) {
|
|
map.addLayer({
|
|
id: 'current-asset-icon',
|
|
type: 'symbol',
|
|
source: 'asset-geometry',
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
layout: {
|
|
'icon-image': currentIconImage,
|
|
'icon-size': iconSize,
|
|
'icon-allow-overlap': true,
|
|
'icon-offset': (assetModel === 'trafficlightradar' && hasSupport) ? [0, -25] : [0, 0]
|
|
},
|
|
paint: {
|
|
'icon-opacity': 1.0,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Bring highlight rings and icon to the top so they render above all other layers
|
|
bringAssetLayersToTop();
|
|
|
|
}).catch(e => console.warn('Failed to load asset icons:', e));
|
|
}
|
|
|
|
function addConnectionLinesLayer() {
|
|
const el = document.getElementById('connection-lines-geojson');
|
|
if (!el) return;
|
|
|
|
let connectionGeoJSON;
|
|
try {
|
|
connectionGeoJSON = JSON.parse(el.textContent);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
if (connectionGeoJSON && connectionGeoJSON.features && connectionGeoJSON.features.length > 0) {
|
|
if (!map.getSource('connection-lines')) {
|
|
map.addSource('connection-lines', {
|
|
type: 'geojson',
|
|
data: connectionGeoJSON
|
|
});
|
|
}
|
|
|
|
if (!map.getLayer('connection-lines-layer')) {
|
|
map.addLayer({
|
|
id: 'connection-lines-layer',
|
|
type: 'line',
|
|
source: 'connection-lines',
|
|
layout: {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
paint: {
|
|
'line-color': '#007bff',
|
|
'line-width': 2,
|
|
'line-opacity': 0.8,
|
|
'line-dasharray': [2, 2]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add orthophoto WMTS source
|
|
// All layers that must always sit above the orthophoto
|
|
const ASSET_LAYER_ORDER = [
|
|
'connection-lines-layer',
|
|
'sibling-cables', 'sibling-poles', 'sibling-cabinets', 'sibling-radars', 'sibling-equipment',
|
|
'asset-polygon-fill', 'asset-polygon-outline',
|
|
'asset-line-outline', 'asset-line-fill',
|
|
'asset-point-outer', 'asset-point-middle', 'asset-point-inner',
|
|
'current-asset-icon',
|
|
];
|
|
|
|
function bringAssetLayersToTop() {
|
|
ASSET_LAYER_ORDER.forEach(id => {
|
|
if (map.getLayer(id)) map.moveLayer(id);
|
|
});
|
|
}
|
|
|
|
function addOrthoLayerSwitcher() {
|
|
if (typeof LayerSwitcherControl === 'undefined') return;
|
|
|
|
const layerSwitcher = new LayerSwitcherControl({
|
|
layers: [
|
|
{
|
|
id: 'ortho',
|
|
name: 'Orthophotos',
|
|
icon: 'satellite',
|
|
group: 'ortho',
|
|
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 || URBIS_WMS_URL],
|
|
tileSize: 256,
|
|
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS'
|
|
}
|
|
},
|
|
{
|
|
id: 'vlaanderen',
|
|
name: 'Vlaanderen 2023',
|
|
source: {
|
|
type: 'raster',
|
|
tiles: [ORTHO_WMTS_URL],
|
|
tileSize: 256,
|
|
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
|
|
map.addControl(layerSwitcher, 'top-left');
|
|
|
|
// The LayerSwitcherControl appends sublayer selectors to document.body with
|
|
// position:fixed and hardcoded coordinates designed for fullscreen maps.
|
|
// We move each selector into #asset-map (position:relative) and switch to
|
|
// position:absolute so it stays anchored to the map div.
|
|
const mapEl = document.getElementById('asset-map');
|
|
if (mapEl) {
|
|
// Wait one tick for the control to finish rendering
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.layer-switcher-sublayer-selector').forEach(sel => {
|
|
// Only move selectors that are still attached to body
|
|
if (sel.parentNode === document.body) {
|
|
mapEl.appendChild(sel);
|
|
sel.style.position = 'absolute';
|
|
sel.style.left = '46px';
|
|
sel.style.top = '10px';
|
|
sel.style.zIndex = '10';
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
// After any ortho interaction, put asset layers back on top
|
|
map.getContainer().addEventListener('click', (e) => {
|
|
if (e.target.closest('.layer-switcher-btn') || e.target.closest('.layer-switcher-sublayer-btn')) {
|
|
setTimeout(bringAssetLayersToTop, 150);
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
// Function to refresh the map with new geometry (called after editing)
|
|
window.refreshAssetMap = function(newGeometry) {
|
|
if (!map) return;
|
|
|
|
// Remove existing source and layers
|
|
const layerIds = ['asset-point-outer', 'asset-point-middle', 'asset-point-inner',
|
|
'asset-line-outline', 'asset-line-fill',
|
|
'asset-polygon-fill', 'asset-polygon-outline',
|
|
'current-asset-icon'];
|
|
|
|
layerIds.forEach(id => {
|
|
if (map.getLayer(id)) {
|
|
map.removeLayer(id);
|
|
}
|
|
});
|
|
|
|
if (map.getSource('asset-geometry')) {
|
|
map.removeSource('asset-geometry');
|
|
}
|
|
|
|
// Add new geometry
|
|
if (newGeometry) {
|
|
addAssetGeometry(newGeometry);
|
|
|
|
const bounds = calculateBounds(newGeometry);
|
|
if (bounds) {
|
|
map.fitBounds(bounds, {
|
|
padding: 50,
|
|
maxZoom: 18
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
})();
|