feat: add sign panel map markers, count badges, and asset grouping logic
This commit is contained in:
parent
57976c71eb
commit
e2be198802
3 changed files with 438 additions and 50 deletions
|
|
@ -239,7 +239,7 @@
|
||||||
box-shadow: 0 0 10px rgba(13, 110, 253, 0.7);
|
box-shadow: 0 0 10px rgba(13, 110, 253, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Marqueur asset sur la carte */
|
/* Marqueur asset ponctuel standard */
|
||||||
.asset-map-marker {
|
.asset-map-marker {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
@ -255,6 +255,7 @@
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.maplibregl-marker {
|
.maplibregl-marker {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
|
|
@ -282,6 +283,59 @@
|
||||||
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
|
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Marqueur spécifique panneau de signalisation (sans cercle bleu/blanc) */
|
||||||
|
.asset-map-marker-sign {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
.asset-map-marker-sign img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.45));
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.asset-map-marker-sign:hover img, .asset-map-marker-sign:active img {
|
||||||
|
transform: scale(1.2);
|
||||||
|
filter: drop-shadow(0 3px 8px rgba(0,0,0,0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge compteur pour assets multiples au même endroit */
|
||||||
|
.marker-count-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 3px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.45);
|
||||||
|
border: 1.5px solid #ffffff;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
/* Marqueur ponctuel sélectionné */
|
/* Marqueur ponctuel sélectionné */
|
||||||
.asset-map-marker.selected {
|
.asset-map-marker.selected {
|
||||||
z-index: 50 !important;
|
z-index: 50 !important;
|
||||||
|
|
@ -304,6 +358,26 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: marker-selected-pulse 1.8s infinite ease-in-out;
|
animation: marker-selected-pulse 1.8s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
.asset-map-marker-sign.selected {
|
||||||
|
z-index: 50 !important;
|
||||||
|
}
|
||||||
|
.asset-map-marker-sign.selected img {
|
||||||
|
transform: scale(1.25);
|
||||||
|
filter: drop-shadow(0 0 5px #eab308) drop-shadow(0 0 10px #fde047) drop-shadow(0 3px 6px rgba(0,0,0,0.5)) !important;
|
||||||
|
}
|
||||||
|
.asset-map-marker-sign.selected::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: -6px;
|
||||||
|
right: -6px;
|
||||||
|
bottom: -6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2.5px solid #eab308;
|
||||||
|
box-shadow: 0 0 10px rgba(234, 179, 8, 0.85);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: marker-selected-pulse 1.8s infinite ease-in-out;
|
||||||
|
}
|
||||||
@keyframes marker-selected-pulse {
|
@keyframes marker-selected-pulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
|
@ -1306,55 +1380,254 @@
|
||||||
activeMarkers = [];
|
activeMarkers = [];
|
||||||
selectedMarkerElement = null;
|
selectedMarkerElement = null;
|
||||||
|
|
||||||
(results || []).forEach(asset => {
|
// 1. Filtrer les assets ponctuels valides
|
||||||
if (asset.lon === null || asset.lat === null) return;
|
const punctualAssets = (results || []).filter(asset => {
|
||||||
|
if (asset.lon === null || asset.lat === null) return false;
|
||||||
|
return !asset.geom_type || asset.geom_type === 'Point' || asset.geom_type === 'MultiPoint';
|
||||||
|
});
|
||||||
|
|
||||||
// N'afficher un marqueur DOM HTML que pour les géométries ponctuelles
|
// 2. Regrouper les assets superposés aux mêmes coordonnées exactes ou sur le même poteau
|
||||||
const isPunctual = !asset.geom_type || asset.geom_type === 'Point' || asset.geom_type === 'MultiPoint';
|
const locationGroups = [];
|
||||||
if (!isPunctual) return;
|
const processedSet = new Set();
|
||||||
|
|
||||||
const symbology = (typeof AssetSymbology !== 'undefined')
|
punctualAssets.forEach(asset => {
|
||||||
? AssetSymbology.getAssetSymbology(asset)
|
const key = String(asset.feature_id || `${asset.model_key}_${asset.id}`);
|
||||||
: { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
|
if (processedSet.has(key)) return;
|
||||||
const color = symbology.color || THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
|
|
||||||
const iconHtml = (typeof AssetSymbology !== 'undefined')
|
|
||||||
? AssetSymbology.renderIconHtml(symbology)
|
|
||||||
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
|
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const group = [asset];
|
||||||
el.className = 'asset-map-marker';
|
processedSet.add(key);
|
||||||
el.style.borderColor = color;
|
|
||||||
el.style.color = color;
|
|
||||||
el.innerHTML = iconHtml;
|
|
||||||
|
|
||||||
el.addEventListener('click', (e) => {
|
punctualAssets.forEach(other => {
|
||||||
e.stopPropagation();
|
const otherKey = String(other.feature_id || `${other.model_key}_${other.id}`);
|
||||||
if (e.originalEvent) {
|
if (processedSet.has(otherKey)) return;
|
||||||
e.originalEvent._handledByAssetLayer = true;
|
|
||||||
}
|
const samePole = (other.pole_id && asset.pole_id && String(other.pole_id) === String(asset.pole_id))
|
||||||
const matchingAssets = findAssetsAtMarker(asset);
|
|| (asset.model_key && asset.model_key.includes('pole') && String(other.pole_id) === String(asset.id))
|
||||||
if (matchingAssets.length > 1) {
|
|| (other.model_key && other.model_key.includes('pole') && String(asset.pole_id) === String(other.id));
|
||||||
openMultiBottomSheet(matchingAssets);
|
|
||||||
} else {
|
const dLon = Math.abs(other.lon - asset.lon);
|
||||||
openBottomSheet(asset);
|
const dLat = Math.abs(other.lat - asset.lat);
|
||||||
|
const isSameLocation = samePole || (dLon < 0.00003 && dLat < 0.00003);
|
||||||
|
|
||||||
|
if (isSameLocation) {
|
||||||
|
group.push(other);
|
||||||
|
processedSet.add(otherKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: el })
|
locationGroups.push(group);
|
||||||
.setLngLat([asset.lon, asset.lat])
|
});
|
||||||
.addTo(map);
|
|
||||||
|
|
||||||
marker._asset = asset;
|
// 3. Fonction pour déterminer l'asset prioritaire visuellement
|
||||||
marker._element = el;
|
function getDisplayPriority(a) {
|
||||||
|
// 1. Panneau avec visuel SVG/PNG : priorité maximale (100)
|
||||||
|
if (a.model_key === 'signpanel' && (a.svg_url || a.icon_url || a.panel_type)) return 100;
|
||||||
|
// 2. Autre panneau de signalisation : 90
|
||||||
|
if (a.model_key === 'signpanel') return 90;
|
||||||
|
// 3. Lanterne tricolore / luminaire : 80
|
||||||
|
if (a.model_key === 'trafficlightlantern' || a.model_key === 'publiclightinglightfixture') return 80;
|
||||||
|
// 4. Autre équipement non-poteau : 70
|
||||||
|
if (!a.model_key.includes('pole')) return 70;
|
||||||
|
// 5. Poteau / mât structural : priorité minimale (10)
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
const featId = asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : null);
|
// 4. Créer les marqueurs pour chaque emplacement avec offset vertical pour les panneaux
|
||||||
if (lastSelectedFeatureId && featId === lastSelectedFeatureId) {
|
locationGroups.forEach(group => {
|
||||||
el.classList.add('selected');
|
const signPanels = group.filter(a => a.model_key === 'signpanel');
|
||||||
el.style.zIndex = '50';
|
const otherAssets = group.filter(a => a.model_key !== 'signpanel');
|
||||||
selectedMarkerElement = el;
|
|
||||||
|
// CAS A : Emplacement avec un ou plusieurs panneaux de signalisation
|
||||||
|
if (signPanels.length > 0) {
|
||||||
|
// 1. S'il y a un poteau ou support à la base, le positionner à l'offset [0, 0]
|
||||||
|
if (otherAssets.length > 0) {
|
||||||
|
otherAssets.sort((a, b) => getDisplayPriority(b) - getDisplayPriority(a));
|
||||||
|
const baseAsset = otherAssets[0];
|
||||||
|
|
||||||
|
const symbology = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.getAssetSymbology(baseAsset)
|
||||||
|
: { color: THEMATIC_COLORS[baseAsset.thematic] || '#888888', iconClass: baseAsset.thematic_icon || 'bi-signpost-split' };
|
||||||
|
const color = symbology.color || THEMATIC_COLORS[baseAsset.thematic] || '#888888';
|
||||||
|
const iconHtml = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.renderIconHtml(symbology)
|
||||||
|
: `<i class="bi ${symbology.iconClass || 'bi-signpost-split'}"></i>`;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'asset-map-marker';
|
||||||
|
el.style.borderColor = color;
|
||||||
|
el.style.color = color;
|
||||||
|
el.style.zIndex = '5';
|
||||||
|
el.innerHTML = iconHtml;
|
||||||
|
|
||||||
|
if (otherAssets.length > 1) {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'marker-count-badge';
|
||||||
|
badge.textContent = otherAssets.length > 99 ? '99+' : otherAssets.length;
|
||||||
|
badge.title = `${otherAssets.length} équipements à la base`;
|
||||||
|
el.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.originalEvent) {
|
||||||
|
e.originalEvent._handledByAssetLayer = true;
|
||||||
|
}
|
||||||
|
if (otherAssets.length > 1) {
|
||||||
|
openMultiBottomSheet(otherAssets);
|
||||||
|
} else {
|
||||||
|
openBottomSheet(baseAsset, false, group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: el, offset: [0, 0] })
|
||||||
|
.setLngLat([baseAsset.lon, baseAsset.lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
marker._asset = baseAsset;
|
||||||
|
marker._group = group;
|
||||||
|
marker._element = el;
|
||||||
|
|
||||||
|
const isBaseSelected = otherAssets.some(a => {
|
||||||
|
const featId = a.feature_id || (a.model_key && a.id ? `${a.model_key}_${a.id}` : null);
|
||||||
|
return lastSelectedFeatureId && featId === lastSelectedFeatureId;
|
||||||
|
});
|
||||||
|
if (isBaseSelected) {
|
||||||
|
el.classList.add('selected');
|
||||||
|
el.style.zIndex = '50';
|
||||||
|
selectedMarkerElement = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMarkers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Calcul de l'offset vertical pour chaque panneau sur le poteau (identique à la version desktop)
|
||||||
|
signPanels.sort((a, b) => {
|
||||||
|
const orderA = a.vertical_order != null ? Number(a.vertical_order) : (a.panel_vertical_order != null ? Number(a.panel_vertical_order) : 1);
|
||||||
|
const orderB = b.vertical_order != null ? Number(b.vertical_order) : (b.panel_vertical_order != null ? Number(b.panel_vertical_order) : 1);
|
||||||
|
return (orderA - orderB) || (Number(a.id) - Number(b.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasBaseSupport = otherAssets.length > 0;
|
||||||
|
const MIN_PANEL_BASE_UP_OFFSET_PX = 36;
|
||||||
|
const DEFAULT_PANEL_HEIGHT_PX = 32;
|
||||||
|
const PANEL_STACK_GAP_PX = 2;
|
||||||
|
|
||||||
|
let cumulativeOffset = -MIN_PANEL_BASE_UP_OFFSET_PX;
|
||||||
|
let previousHeight = DEFAULT_PANEL_HEIGHT_PX;
|
||||||
|
|
||||||
|
signPanels.forEach((panel, index) => {
|
||||||
|
const renderHeight = DEFAULT_PANEL_HEIGHT_PX;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
if (hasBaseSupport) {
|
||||||
|
cumulativeOffset = -MIN_PANEL_BASE_UP_OFFSET_PX - Math.max(0, (renderHeight - DEFAULT_PANEL_HEIGHT_PX) / 2);
|
||||||
|
} else if (signPanels.length > 1) {
|
||||||
|
cumulativeOffset = 0 - Math.max(0, (renderHeight - DEFAULT_PANEL_HEIGHT_PX) / 2);
|
||||||
|
} else {
|
||||||
|
cumulativeOffset = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cumulativeOffset -= (previousHeight + renderHeight) / 2 + PANEL_STACK_GAP_PX;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousHeight = renderHeight;
|
||||||
|
|
||||||
|
const symbology = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.getAssetSymbology(panel)
|
||||||
|
: { color: '#0022ff', iconClass: 'bi-sign-stop-fill' };
|
||||||
|
const iconHtml = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.renderIconHtml(symbology)
|
||||||
|
: `<i class="bi ${symbology.iconClass || 'bi-sign-stop-fill'}"></i>`;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'asset-map-marker-sign';
|
||||||
|
el.innerHTML = iconHtml;
|
||||||
|
el.style.zIndex = String(10 + index);
|
||||||
|
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.originalEvent) {
|
||||||
|
e.originalEvent._handledByAssetLayer = true;
|
||||||
|
}
|
||||||
|
openBottomSheet(panel, false, group);
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: el, offset: [0, cumulativeOffset] })
|
||||||
|
.setLngLat([panel.lon, panel.lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
marker._asset = panel;
|
||||||
|
marker._group = group;
|
||||||
|
marker._element = el;
|
||||||
|
|
||||||
|
const featId = panel.feature_id || (panel.model_key && panel.id ? `${panel.model_key}_${panel.id}` : null);
|
||||||
|
if (lastSelectedFeatureId && featId === lastSelectedFeatureId) {
|
||||||
|
el.classList.add('selected');
|
||||||
|
el.style.zIndex = '50';
|
||||||
|
selectedMarkerElement = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMarkers.push(marker);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// CAS B : Emplacement sans panneau (arbres, bancs, corbeilles, etc.)
|
||||||
|
group.sort((a, b) => getDisplayPriority(b) - getDisplayPriority(a));
|
||||||
|
const primaryAsset = group[0];
|
||||||
|
|
||||||
|
const symbology = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.getAssetSymbology(primaryAsset)
|
||||||
|
: { color: THEMATIC_COLORS[primaryAsset.thematic] || '#2f3e8b', iconClass: primaryAsset.thematic_icon || 'bi-pin-map' };
|
||||||
|
const color = symbology.color || THEMATIC_COLORS[primaryAsset.thematic] || '#2f3e8b';
|
||||||
|
const iconHtml = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.renderIconHtml(symbology)
|
||||||
|
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'asset-map-marker';
|
||||||
|
el.style.borderColor = color;
|
||||||
|
el.style.color = color;
|
||||||
|
el.innerHTML = iconHtml;
|
||||||
|
|
||||||
|
if (group.length > 1) {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'marker-count-badge';
|
||||||
|
badge.textContent = group.length > 99 ? '99+' : group.length;
|
||||||
|
badge.title = `${group.length} assets à cet endroit`;
|
||||||
|
el.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.originalEvent) {
|
||||||
|
e.originalEvent._handledByAssetLayer = true;
|
||||||
|
}
|
||||||
|
if (group.length > 1) {
|
||||||
|
openMultiBottomSheet(group);
|
||||||
|
} else {
|
||||||
|
openBottomSheet(primaryAsset, false, group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: el, offset: [0, 0] })
|
||||||
|
.setLngLat([primaryAsset.lon, primaryAsset.lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
marker._asset = primaryAsset;
|
||||||
|
marker._group = group;
|
||||||
|
marker._element = el;
|
||||||
|
|
||||||
|
const isGroupSelected = group.some(a => {
|
||||||
|
const featId = a.feature_id || (a.model_key && a.id ? `${a.model_key}_${a.id}` : null);
|
||||||
|
return lastSelectedFeatureId && featId === lastSelectedFeatureId;
|
||||||
|
});
|
||||||
|
if (isGroupSelected) {
|
||||||
|
el.classList.add('selected');
|
||||||
|
el.style.zIndex = '50';
|
||||||
|
selectedMarkerElement = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMarkers.push(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeMarkers.push(marker);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ne jamais modifier la visualisation (zoom ou pan) de manière automatique lors d'une action de filtre ou de recherche
|
// Ne jamais modifier la visualisation (zoom ou pan) de manière automatique lors d'une action de filtre ou de recherche
|
||||||
|
|
@ -1475,7 +1748,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5b. Volet d'aperçu individuel (Bottom Sheet)
|
// 5b. Volet d'aperçu individuel (Bottom Sheet)
|
||||||
function openBottomSheet(asset, fromMulti = false) {
|
function openBottomSheet(asset, fromMulti = false, assetGroup = null) {
|
||||||
if (!asset) return;
|
if (!asset) return;
|
||||||
|
|
||||||
// Fermer le volet multi-assets
|
// Fermer le volet multi-assets
|
||||||
|
|
@ -1483,12 +1756,14 @@
|
||||||
multiAssetBottomSheet.classList.remove('active');
|
multiAssetBottomSheet.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bouton retour vers la liste multi si ouvert depuis une sélection multiple
|
// Bouton retour vers la liste multi si ouvert depuis une sélection multiple ou s'il y a d'autres assets sur le même support
|
||||||
|
const siblingGroup = (assetGroup && assetGroup.length > 1) ? assetGroup : ((lastMultiAssets && lastMultiAssets.length > 1) ? lastMultiAssets : null);
|
||||||
if (btnBackToMultiSheet) {
|
if (btnBackToMultiSheet) {
|
||||||
if (fromMulti && lastMultiAssets && lastMultiAssets.length > 1) {
|
if (siblingGroup && siblingGroup.length > 1) {
|
||||||
|
lastMultiAssets = siblingGroup;
|
||||||
btnBackToMultiSheet.classList.remove('d-none');
|
btnBackToMultiSheet.classList.remove('d-none');
|
||||||
if (btnBackToMultiSheetText) {
|
if (btnBackToMultiSheetText) {
|
||||||
btnBackToMultiSheetText.textContent = `{% translate "Retour aux" %} ${lastMultiAssets.length} {% translate "assets à cet endroit" %}`;
|
btnBackToMultiSheetText.textContent = `{% translate "Voir les" %} ${siblingGroup.length} {% translate "assets à cet endroit" %}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
btnBackToMultiSheet.classList.add('d-none');
|
btnBackToMultiSheet.classList.add('d-none');
|
||||||
|
|
@ -1519,10 +1794,16 @@
|
||||||
}
|
}
|
||||||
const currentFeatId = asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : null);
|
const currentFeatId = asset.feature_id || (asset.model_key && asset.id ? `${asset.model_key}_${asset.id}` : null);
|
||||||
if (currentFeatId) {
|
if (currentFeatId) {
|
||||||
const matchingMarker = activeMarkers.find(m => m._asset && (
|
const matchingMarker = activeMarkers.find(m => {
|
||||||
String(m._asset.feature_id) === String(currentFeatId) ||
|
if (!m._element) return false;
|
||||||
(m._asset.model_key === asset.model_key && String(m._asset.id) === String(asset.id))
|
return m._asset && (
|
||||||
));
|
String(m._asset.feature_id) === String(currentFeatId) ||
|
||||||
|
(m._asset.model_key === asset.model_key && String(m._asset.id) === String(asset.id))
|
||||||
|
);
|
||||||
|
}) || activeMarkers.find(m => {
|
||||||
|
if (!m._element) return false;
|
||||||
|
return m._group && m._group.some(g => String(g.feature_id) === String(currentFeatId) || (g.model_key === asset.model_key && String(g.id) === String(asset.id)));
|
||||||
|
});
|
||||||
if (matchingMarker && matchingMarker._element) {
|
if (matchingMarker && matchingMarker._element) {
|
||||||
selectedMarkerElement = matchingMarker._element;
|
selectedMarkerElement = matchingMarker._element;
|
||||||
selectedMarkerElement.classList.add('selected');
|
selectedMarkerElement.classList.add('selected');
|
||||||
|
|
|
||||||
|
|
@ -533,6 +533,7 @@ class MobileAssetDetailTests(TestCase):
|
||||||
street=street,
|
street=street,
|
||||||
pole=pole,
|
pole=pole,
|
||||||
signpanel_type=panel_type,
|
signpanel_type=panel_type,
|
||||||
|
vertical_order=1,
|
||||||
status="active",
|
status="active",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -549,6 +550,9 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(panel_item["panel_type"], "B1")
|
self.assertEqual(panel_item["panel_type"], "B1")
|
||||||
self.assertEqual(panel_item["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
self.assertEqual(panel_item["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
||||||
self.assertEqual(panel_item["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
self.assertEqual(panel_item["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
||||||
|
self.assertEqual(panel_item["pole_id"], pole.id)
|
||||||
|
self.assertEqual(panel_item["vertical_order"], 1)
|
||||||
|
self.assertEqual(panel_item["panel_vertical_order"], 1)
|
||||||
self.assertAlmostEqual(panel_item["lat"], 50.8466, places=4)
|
self.assertAlmostEqual(panel_item["lat"], 50.8466, places=4)
|
||||||
self.assertAlmostEqual(panel_item["lon"], 4.3528, places=4)
|
self.assertAlmostEqual(panel_item["lon"], 4.3528, places=4)
|
||||||
self.assertIn("B1", panel_item["name"])
|
self.assertIn("B1", panel_item["name"])
|
||||||
|
|
@ -558,6 +562,9 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(feature["properties"]["panel_type"], "B1")
|
self.assertEqual(feature["properties"]["panel_type"], "B1")
|
||||||
self.assertEqual(feature["properties"]["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
self.assertEqual(feature["properties"]["svg_url"], "/static/assets/road_signs/2025/B1.svg")
|
||||||
self.assertEqual(feature["properties"]["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
self.assertEqual(feature["properties"]["icon_url"], "/static/assets/road_signs/2025/B1.svg")
|
||||||
|
self.assertEqual(feature["properties"]["pole_id"], pole.id)
|
||||||
|
self.assertEqual(feature["properties"]["vertical_order"], 1)
|
||||||
|
self.assertEqual(feature["properties"]["panel_vertical_order"], 1)
|
||||||
self.assertAlmostEqual(feature["geometry"]["coordinates"][1], 50.8466, places=4)
|
self.assertAlmostEqual(feature["geometry"]["coordinates"][1], 50.8466, places=4)
|
||||||
self.assertAlmostEqual(feature["geometry"]["coordinates"][0], 4.3528, places=4)
|
self.assertAlmostEqual(feature["geometry"]["coordinates"][0], 4.3528, places=4)
|
||||||
|
|
||||||
|
|
@ -568,4 +575,63 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertEqual(data["count"], 1)
|
self.assertEqual(data["count"], 1)
|
||||||
self.assertEqual(data["results"][0]["id"], panel.id)
|
self.assertEqual(data["results"][0]["id"], panel.id)
|
||||||
|
|
||||||
|
def test_search_multiple_signpanels_on_same_pole_vertical_order(self):
|
||||||
|
"""Multiple panels on the same pole return distinct vertical_order and shared pole_id."""
|
||||||
|
user = User.objects.create_user(username="multi_panel_user", password="password")
|
||||||
|
user_config = UserConfig.objects.create(user=user, is_intern=True)
|
||||||
|
user_config.roles.add(self.admin_role)
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=user_config,
|
||||||
|
thematic=self.sign_thematic,
|
||||||
|
can_view_assets=True
|
||||||
|
)
|
||||||
|
|
||||||
|
from assets.models import SignPanel, SignPole, SignStreet, SignPanelType
|
||||||
|
from common.models import Municipality
|
||||||
|
mun, _ = Municipality.objects.get_or_create(name_fr="Bruxelles", defaults={"name_nl": "Brussel", "niscode": "21004"})
|
||||||
|
street = SignStreet.objects.create(name_fr="Rue Royale", municipality=mun)
|
||||||
|
pole = SignPole.objects.create(
|
||||||
|
code="POT-MULTI-01",
|
||||||
|
street=street,
|
||||||
|
lat=50.8500,
|
||||||
|
lon=4.3600,
|
||||||
|
status="active",
|
||||||
|
)
|
||||||
|
type_a, _ = SignPanelType.objects.get_or_create(code="A1a", defaults={"name_fr": "Virage dangereux à droite"})
|
||||||
|
type_c, _ = SignPanelType.objects.get_or_create(code="C1", defaults={"name_fr": "Sens interdit"})
|
||||||
|
|
||||||
|
panel1 = SignPanel.objects.create(
|
||||||
|
code="PAN-A1a-001",
|
||||||
|
street=street,
|
||||||
|
pole=pole,
|
||||||
|
signpanel_type=type_a,
|
||||||
|
vertical_order=1,
|
||||||
|
status="active",
|
||||||
|
)
|
||||||
|
panel2 = SignPanel.objects.create(
|
||||||
|
code="PAN-C1-002",
|
||||||
|
street=street,
|
||||||
|
pole=pole,
|
||||||
|
signpanel_type=type_c,
|
||||||
|
vertical_order=2,
|
||||||
|
status="active",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(username="multi_panel_user", password="password")
|
||||||
|
search_url = reverse("mobile:mobile_assets_search")
|
||||||
|
|
||||||
|
response = self.client.get(search_url, {"thematic": "sign"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
results_by_id = {r["id"]: r for r in data["results"] if r["model_key"] == "signpanel"}
|
||||||
|
self.assertIn(panel1.id, results_by_id)
|
||||||
|
self.assertIn(panel2.id, results_by_id)
|
||||||
|
|
||||||
|
self.assertEqual(results_by_id[panel1.id]["pole_id"], pole.id)
|
||||||
|
self.assertEqual(results_by_id[panel1.id]["vertical_order"], 1)
|
||||||
|
self.assertEqual(results_by_id[panel2.id]["pole_id"], pole.id)
|
||||||
|
self.assertEqual(results_by_id[panel2.id]["vertical_order"], 2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1370,7 +1370,23 @@ def mobile_assets_search(request):
|
||||||
|
|
||||||
# Filtrage par boîte englobante (bbox / zoom de la carte)
|
# Filtrage par boîte englobante (bbox / zoom de la carte)
|
||||||
if bbox_poly_3812:
|
if bbox_poly_3812:
|
||||||
if hasattr(model_class, 'geom'):
|
if model_key == 'signpanel':
|
||||||
|
panel_geo_q = Q(geom__bboverlaps=bbox_poly_3812)
|
||||||
|
panel_geo_q |= Q(geom__isnull=True, pole__geom__bboverlaps=bbox_poly_3812)
|
||||||
|
panel_geo_q |= Q(geom__isnull=True, trafficlight_pole__geom__bboverlaps=bbox_poly_3812)
|
||||||
|
panel_geo_q |= Q(geom__isnull=True, publiclighting_pole__geom__bboverlaps=bbox_poly_3812)
|
||||||
|
if min_lon is not None:
|
||||||
|
panel_geo_q |= Q(lon__gte=min_lon, lon__lte=max_lon, lat__gte=min_lat, lat__lte=max_lat)
|
||||||
|
panel_geo_q |= Q(lon__isnull=True, pole__lon__gte=min_lon, pole__lon__lte=max_lon, pole__lat__gte=min_lat, pole__lat__lte=max_lat)
|
||||||
|
qs = qs.filter(panel_geo_q)
|
||||||
|
elif model_key == 'trafficlightlantern':
|
||||||
|
tl_geo_q = Q(geom__bboverlaps=bbox_poly_3812)
|
||||||
|
tl_geo_q |= Q(geom__isnull=True, pole__geom__bboverlaps=bbox_poly_3812)
|
||||||
|
if min_lon is not None:
|
||||||
|
tl_geo_q |= Q(lon__gte=min_lon, lon__lte=max_lon, lat__gte=min_lat, lat__lte=max_lat)
|
||||||
|
tl_geo_q |= Q(lon__isnull=True, pole__lon__gte=min_lon, pole__lon__lte=max_lon, pole__lat__gte=min_lat, pole__lat__lte=max_lat)
|
||||||
|
qs = qs.filter(tl_geo_q)
|
||||||
|
elif hasattr(model_class, 'geom'):
|
||||||
qs = qs.filter(geom__bboverlaps=bbox_poly_3812)
|
qs = qs.filter(geom__bboverlaps=bbox_poly_3812)
|
||||||
elif min_lon is not None and hasattr(model_class, 'lon') and hasattr(model_class, 'lat'):
|
elif min_lon is not None and hasattr(model_class, 'lon') and hasattr(model_class, 'lat'):
|
||||||
qs = qs.filter(lon__gte=min_lon, lon__lte=max_lon, lat__gte=min_lat, lat__lte=max_lat)
|
qs = qs.filter(lon__gte=min_lon, lon__lte=max_lon, lat__gte=min_lat, lat__lte=max_lat)
|
||||||
|
|
@ -1421,7 +1437,7 @@ def mobile_assets_search(request):
|
||||||
|
|
||||||
for model_key, model_class, qs in model_querysets:
|
for model_key, model_class, qs in model_querysets:
|
||||||
if model_key == 'signpanel' and hasattr(model_class, 'signpanel_type'):
|
if model_key == 'signpanel' and hasattr(model_class, 'signpanel_type'):
|
||||||
qs = qs.select_related('signpanel_type', 'model', 'pole')
|
qs = qs.select_related('signpanel_type', 'model', 'pole', 'trafficlight_pole', 'publiclighting_pole')
|
||||||
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
|
elif model_key == 'trafficlightlantern' and hasattr(model_class, 'model'):
|
||||||
qs = qs.select_related('model', 'pole')
|
qs = qs.select_related('model', 'pole')
|
||||||
items = list(qs)
|
items = list(qs)
|
||||||
|
|
@ -1572,6 +1588,21 @@ def mobile_assets_search(request):
|
||||||
structure_type_val = getattr(item, 'structure_type', None)
|
structure_type_val = getattr(item, 'structure_type', None)
|
||||||
feature_id = f"{model_key}_{item.id}"
|
feature_id = f"{model_key}_{item.id}"
|
||||||
|
|
||||||
|
pole_id = None
|
||||||
|
vertical_order = None
|
||||||
|
panel_width = None
|
||||||
|
panel_height = None
|
||||||
|
if model_key == 'signpanel':
|
||||||
|
pole_id = getattr(item, 'pole_id', None) or getattr(item, 'trafficlight_pole_id', None) or getattr(item, 'publiclighting_pole_id', None)
|
||||||
|
vertical_order = getattr(item, 'vertical_order', None)
|
||||||
|
try:
|
||||||
|
panel_width = item.get_width()
|
||||||
|
panel_height = item.get_height()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif hasattr(item, 'pole_id'):
|
||||||
|
pole_id = getattr(item, 'pole_id', None)
|
||||||
|
|
||||||
res_item = {
|
res_item = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'feature_id': feature_id,
|
'feature_id': feature_id,
|
||||||
|
|
@ -1597,6 +1628,11 @@ def mobile_assets_search(request):
|
||||||
'lon': lon,
|
'lon': lon,
|
||||||
'distance_m': distance_m,
|
'distance_m': distance_m,
|
||||||
'detail_url': detail_url,
|
'detail_url': detail_url,
|
||||||
|
'pole_id': pole_id,
|
||||||
|
'vertical_order': vertical_order,
|
||||||
|
'panel_vertical_order': vertical_order,
|
||||||
|
'width': panel_width,
|
||||||
|
'height': panel_height,
|
||||||
}
|
}
|
||||||
results.append(res_item)
|
results.append(res_item)
|
||||||
|
|
||||||
|
|
@ -1631,6 +1667,11 @@ def mobile_assets_search(request):
|
||||||
'detail_url': detail_url,
|
'detail_url': detail_url,
|
||||||
'lon': lon,
|
'lon': lon,
|
||||||
'lat': lat,
|
'lat': lat,
|
||||||
|
'pole_id': pole_id,
|
||||||
|
'vertical_order': vertical_order,
|
||||||
|
'panel_vertical_order': vertical_order,
|
||||||
|
'width': panel_width,
|
||||||
|
'height': panel_height,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue