+
+
+
+
+
+
+
+
+
+
+
{% translate "Plusieurs assets à cet endroit" %}
+
2 {% translate "assets trouvés" %}
+
+
+
+
+
{% translate "Sélectionnez l'asset à consulter :" %}
+
+
+
+
{% endblock content %}
{% block extra_scripts %}
@@ -695,6 +776,18 @@
const sheetDetailLink = document.getElementById('sheetDetailLink');
const sheetRouteLink = document.getElementById('sheetRouteLink');
+ // Éléments du volet Multi-Assets
+ const multiAssetBottomSheet = document.getElementById('multiAssetBottomSheet');
+ const multiSheetCloseHandle = document.getElementById('multiSheetCloseHandle');
+ const btnMultiSheetClose = document.getElementById('btnMultiSheetClose');
+ const multiSheetCount = document.getElementById('multiSheetCount');
+ const multiAssetListContainer = document.getElementById('multiAssetListContainer');
+ const btnBackToMultiSheet = document.getElementById('btnBackToMultiSheet');
+ const btnBackToMultiSheetText = document.getElementById('btnBackToMultiSheetText');
+
+ let lastMultiAssets = null;
+ let selectedMarkerElement = null;
+
// Couleurs par thématique (source unique : AssetSymbology)
const THEMATIC_COLORS = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.THEMATIC_COLORS
@@ -851,15 +944,28 @@
layersConfigured = true;
['assets-fill', 'assets-line'].forEach(layerId => {
map.on('click', layerId, (e) => {
- if (e.features && e.features.length > 0) {
- const feature = e.features[0];
- const props = feature.properties || {};
- const featId = feature.id || props.feature_id;
+ if (e.originalEvent) {
+ e.originalEvent._handledByAssetLayer = true;
+ }
+ const bbox = [[e.point.x - 14, e.point.y - 14], [e.point.x + 14, e.point.y + 14]];
+ const rendered = map.queryRenderedFeatures(bbox, { layers: ['assets-fill', 'assets-line'] }) || [];
+ const foundMap = new Map();
+
+ rendered.forEach(feat => {
+ const props = feat.properties || {};
+ const featId = feat.id || props.feature_id;
const asset = lastResults.find(a => String(a.feature_id) === String(featId) || (a.model_key === props.model_key && String(a.id) === String(props.id))) || props;
- if (e.originalEvent) {
- e.originalEvent._handledByAssetLayer = true;
+ const key = String(asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : asset.code));
+ if (!foundMap.has(key)) {
+ foundMap.set(key, asset);
}
- openBottomSheet(asset);
+ });
+
+ const assetsList = Array.from(foundMap.values());
+ if (assetsList.length > 1) {
+ openMultiBottomSheet(assetsList);
+ } else if (assetsList.length === 1) {
+ openBottomSheet(assetsList[0]);
}
});
map.on('mouseenter', layerId, () => {
@@ -876,6 +982,7 @@
map.on('click', (e) => {
if (e.originalEvent && e.originalEvent._handledByAssetLayer) return;
closeBottomSheet();
+ closeMultiBottomSheet();
});
// Détection du déplacement ou zoom pour filtrage par zone visible (bbox) et synchronisation de la liste
@@ -982,6 +1089,10 @@
assetsListContainer.innerHTML = '';
activeMarkers.forEach(m => m.remove());
activeMarkers = [];
+ selectedMarkerElement = null;
+ lastMultiAssets = null;
+ closeBottomSheet();
+ closeMultiBottomSheet();
lastGeojson = { type: 'FeatureCollection', features: [] };
if (map && map.getSource('assets')) {
map.getSource('assets').setData(lastGeojson);
@@ -1168,6 +1279,7 @@
// Supprimer les anciens marqueurs
activeMarkers.forEach(m => m.remove());
activeMarkers = [];
+ selectedMarkerElement = null;
(results || []).forEach(asset => {
if (asset.lon === null || asset.lat === null) return;
@@ -1192,23 +1304,161 @@
el.addEventListener('click', (e) => {
e.stopPropagation();
- openBottomSheet(asset);
+ if (e.originalEvent) {
+ e.originalEvent._handledByAssetLayer = true;
+ }
+ const matchingAssets = findAssetsAtMarker(asset);
+ if (matchingAssets.length > 1) {
+ openMultiBottomSheet(matchingAssets);
+ } else {
+ openBottomSheet(asset);
+ }
});
const marker = new maplibregl.Marker({ element: el })
.setLngLat([asset.lon, asset.lat])
.addTo(map);
+ marker._asset = asset;
+ marker._element = el;
+
+ const featId = asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : null);
+ if (lastSelectedFeatureId && featId === lastSelectedFeatureId) {
+ el.classList.add('selected');
+ el.style.zIndex = '50';
+ selectedMarkerElement = el;
+ }
+
activeMarkers.push(marker);
});
// Ne jamais modifier la visualisation (zoom ou pan) de manière automatique lors d'une action de filtre ou de recherche
}
- // 5. Volet d'aperçu inférieur (Bottom Sheet)
- function openBottomSheet(asset) {
+ // Trouver tous les assets situés au même endroit ou à proximité immédiate (tap sur écran)
+ function findAssetsAtMarker(targetAsset) {
+ if (!map || !targetAsset) return [targetAsset];
+ const targetPoint = (targetAsset.lon !== null && targetAsset.lat !== null)
+ ? map.project([targetAsset.lon, targetAsset.lat])
+ : null;
+ const tolerancePx = 24; // Rayon de tolérance pour le tap tactile
+ const foundMap = new Map();
+ const targetKey = String(targetAsset.feature_id || (targetAsset.model_key && targetAsset.id ? `${targetAsset.model_key}_${targetAsset.id}` : targetAsset.code));
+ foundMap.set(targetKey, targetAsset);
+
+ (lastResults || []).forEach(a => {
+ if (a.lon === null || a.lat === null) return;
+ const key = String(a.feature_id || (a.model_key && a.id ? `${a.model_key}_${a.id}` : a.code));
+ if (foundMap.has(key)) return;
+
+ // 1. Coordonnées identiques ou quasi-identiques
+ const dLon = Math.abs(a.lon - targetAsset.lon);
+ const dLat = Math.abs(a.lat - targetAsset.lat);
+ if (dLon < 0.00005 && dLat < 0.00005) {
+ foundMap.set(key, a);
+ return;
+ }
+
+ // 2. Distance à l'écran (marqueurs superposés au zoom actuel)
+ if (targetPoint) {
+ const p = map.project([a.lon, a.lat]);
+ const dist = Math.hypot(p.x - targetPoint.x, p.y - targetPoint.y);
+ if (dist <= tolerancePx) {
+ foundMap.set(key, a);
+ }
+ }
+ });
+
+ return Array.from(foundMap.values());
+ }
+
+ // 5a. Volet Multi-Assets (quand plusieurs éléments se trouvent au même endroit)
+ function openMultiBottomSheet(assetsList) {
+ if (!assetsList || assetsList.length === 0) return;
+ lastMultiAssets = assetsList;
+
+ // Fermer le bottom sheet simple
+ bottomSheet.classList.remove('active');
+
+ if (multiSheetCount) {
+ multiSheetCount.textContent = `${assetsList.length} {% translate "assets trouvés à cet endroit" %}`;
+ }
+
+ multiAssetListContainer.innerHTML = '';
+ assetsList.forEach(asset => {
+ const symbology = (typeof AssetSymbology !== 'undefined')
+ ? AssetSymbology.getAssetSymbology(asset)
+ : { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
+ const color = symbology.color || THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
+ const iconHtml = (typeof AssetSymbology !== 'undefined')
+ ? AssetSymbology.renderIconHtml(symbology)
+ : `
`;
+
+ const statusClass = `badge-status badge-status-${asset.status || 'active'}`;
+ const distanceBadge = (asset.distance_m !== null && asset.distance_m !== undefined)
+ ? `
${formatDistance(asset.distance_m)}`
+ : '';
+
+ const card = document.createElement('div');
+ card.className = 'multi-asset-card';
+ card.innerHTML = `
+
+ ${iconHtml}
+
+
+
+ ${escapeHtml(asset.code)}
+ ${escapeHtml(asset.status_display || asset.status || 'Actif')}
+
+
${escapeHtml(asset.name || asset.element_type || asset.thematic_name || '')}
+
+ ${distanceBadge}
+
+ `;
+
+ card.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ closeMultiBottomSheet(true); // Conserve le contexte multi pour le bouton retour
+ openBottomSheet(asset, true); // fromMulti = true
+ });
+
+ multiAssetListContainer.appendChild(card);
+ });
+
+ multiAssetBottomSheet.classList.add('active');
+ }
+
+ function closeMultiBottomSheet(keepMultiContext = false) {
+ if (multiAssetBottomSheet) {
+ multiAssetBottomSheet.classList.remove('active');
+ }
+ if (!keepMultiContext) {
+ lastMultiAssets = null;
+ }
+ }
+
+ // 5b. Volet d'aperçu individuel (Bottom Sheet)
+ function openBottomSheet(asset, fromMulti = false) {
if (!asset) return;
+ // Fermer le volet multi-assets
+ if (multiAssetBottomSheet) {
+ multiAssetBottomSheet.classList.remove('active');
+ }
+
+ // Bouton retour vers la liste multi si ouvert depuis une sélection multiple
+ if (btnBackToMultiSheet) {
+ if (fromMulti && lastMultiAssets && lastMultiAssets.length > 1) {
+ btnBackToMultiSheet.classList.remove('d-none');
+ if (btnBackToMultiSheetText) {
+ btnBackToMultiSheetText.textContent = `{% translate "Retour aux" %} ${lastMultiAssets.length} {% translate "assets à cet endroit" %}`;
+ }
+ } else {
+ btnBackToMultiSheet.classList.add('d-none');
+ }
+ }
+
// Mettre en surbrillance sur la carte (pour surfaces polygonales et polylignes)
if (map && map.getSource('assets')) {
if (lastSelectedFeatureId) {
@@ -1225,6 +1475,25 @@
}
}
+ // Symbologie de sélection pour marqueur ponctuel
+ if (selectedMarkerElement) {
+ selectedMarkerElement.classList.remove('selected');
+ selectedMarkerElement.style.zIndex = '';
+ selectedMarkerElement = null;
+ }
+ const currentFeatId = asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : null);
+ if (currentFeatId) {
+ const matchingMarker = activeMarkers.find(m => m._asset && (
+ String(m._asset.feature_id) === String(currentFeatId) ||
+ (m._asset.model_key === asset.model_key && String(m._asset.id) === String(asset.id))
+ ));
+ if (matchingMarker && matchingMarker._element) {
+ selectedMarkerElement = matchingMarker._element;
+ selectedMarkerElement.classList.add('selected');
+ selectedMarkerElement.style.zIndex = '50';
+ }
+ }
+
sheetCode.textContent = asset.code || '-';
sheetName.textContent = asset.name || asset.element_type || '';
sheetThematicName.textContent = asset.thematic_name || '';
@@ -1263,41 +1532,61 @@
bottomSheet.classList.add('active');
- // Cadrage ou recentrage sur la carte
- if (map) {
- let bbox = asset.bbox;
- if (typeof bbox === 'string') {
- try { bbox = JSON.parse(bbox); } catch (e) { bbox = null; }
- }
-
- if (Array.isArray(bbox) && bbox.length === 4 &&
- isFinite(bbox[0]) && isFinite(bbox[1]) && isFinite(bbox[2]) && isFinite(bbox[3]) &&
- (bbox[2] > bbox[0] || bbox[3] > bbox[1])) {
- // Surface ou ligne : zoomer sur son emprise
- isProgrammaticMove = true;
- map.fitBounds(
- [[bbox[0], bbox[1]], [bbox[2], bbox[3]]],
- { padding: { top: 60, bottom: 220, left: 40, right: 40 }, maxZoom: 18 }
- );
- } else if (asset.lon !== null && asset.lat !== null && isFinite(asset.lon) && isFinite(asset.lat)) {
- isProgrammaticMove = true;
- map.easeTo({ center: [asset.lon, asset.lat], zoom: Math.max(map.getZoom(), 16), offset: [0, 80] });
- }
- }
+ // NOTE : Le centrage et le zoom ont été supprimés lors du tap pour préserver la vue choisie par l'utilisateur.
}
function closeBottomSheet() {
bottomSheet.classList.remove('active');
+ closeMultiBottomSheet();
if (map && map.getSource('assets') && lastSelectedFeatureId) {
try {
map.setFeatureState({ source: 'assets', id: lastSelectedFeatureId }, { selected: false });
} catch (e) {}
lastSelectedFeatureId = null;
}
+ if (selectedMarkerElement) {
+ selectedMarkerElement.classList.remove('selected');
+ selectedMarkerElement.style.zIndex = '';
+ selectedMarkerElement = null;
+ }
}
- btnSheetClose.addEventListener('click', closeBottomSheet);
- bottomSheetCloseHandle.addEventListener('click', closeBottomSheet);
+ btnSheetClose.addEventListener('click', () => {
+ closeBottomSheet();
+ lastMultiAssets = null;
+ });
+ bottomSheetCloseHandle.addEventListener('click', () => {
+ closeBottomSheet();
+ lastMultiAssets = null;
+ });
+
+ if (btnMultiSheetClose) {
+ btnMultiSheetClose.addEventListener('click', () => closeMultiBottomSheet());
+ }
+ if (multiSheetCloseHandle) {
+ multiSheetCloseHandle.addEventListener('click', () => closeMultiBottomSheet());
+ }
+
+ if (btnBackToMultiSheet) {
+ btnBackToMultiSheet.addEventListener('click', () => {
+ const multiContext = lastMultiAssets;
+ bottomSheet.classList.remove('active');
+ if (map && map.getSource('assets') && lastSelectedFeatureId) {
+ try {
+ map.setFeatureState({ source: 'assets', id: lastSelectedFeatureId }, { selected: false });
+ } catch (e) {}
+ lastSelectedFeatureId = null;
+ }
+ if (selectedMarkerElement) {
+ selectedMarkerElement.classList.remove('selected');
+ selectedMarkerElement.style.zIndex = '';
+ selectedMarkerElement = null;
+ }
+ if (multiContext && multiContext.length > 1) {
+ openMultiBottomSheet(multiContext);
+ }
+ });
+ }
// 6. Gestion du GPS et localisation
function locateUser() {