diff --git a/loko/mobile/templates/mobile/assets/assets_index_mobile.html b/loko/mobile/templates/mobile/assets/assets_index_mobile.html
index 2ed375e..36afb5a 100644
--- a/loko/mobile/templates/mobile/assets/assets_index_mobile.html
+++ b/loko/mobile/templates/mobile/assets/assets_index_mobile.html
@@ -239,7 +239,7 @@
box-shadow: 0 0 10px rgba(13, 110, 253, 0.7);
}
- /* Marqueur asset sur la carte */
+ /* Marqueur asset ponctuel standard */
.asset-map-marker {
width: 32px;
height: 32px;
@@ -255,6 +255,7 @@
transition: none !important;
padding: 3px;
box-sizing: border-box;
+ position: relative;
}
.maplibregl-marker {
transition: none !important;
@@ -282,6 +283,59 @@
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é */
.asset-map-marker.selected {
z-index: 50 !important;
@@ -304,6 +358,26 @@
pointer-events: none;
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 {
0%, 100% {
transform: scale(1);
@@ -1306,55 +1380,254 @@
activeMarkers = [];
selectedMarkerElement = null;
- (results || []).forEach(asset => {
- if (asset.lon === null || asset.lat === null) return;
+ // 1. Filtrer les assets ponctuels valides
+ 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
- const isPunctual = !asset.geom_type || asset.geom_type === 'Point' || asset.geom_type === 'MultiPoint';
- if (!isPunctual) return;
+ // 2. Regrouper les assets superposés aux mêmes coordonnées exactes ou sur le même poteau
+ const locationGroups = [];
+ const processedSet = new Set();
- 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)
- : ``;
+ punctualAssets.forEach(asset => {
+ const key = String(asset.feature_id || `${asset.model_key}_${asset.id}`);
+ if (processedSet.has(key)) return;
- const el = document.createElement('div');
- el.className = 'asset-map-marker';
- el.style.borderColor = color;
- el.style.color = color;
- el.innerHTML = iconHtml;
+ const group = [asset];
+ processedSet.add(key);
- el.addEventListener('click', (e) => {
- e.stopPropagation();
- if (e.originalEvent) {
- e.originalEvent._handledByAssetLayer = true;
- }
- const matchingAssets = findAssetsAtMarker(asset);
- if (matchingAssets.length > 1) {
- openMultiBottomSheet(matchingAssets);
- } else {
- openBottomSheet(asset);
+ punctualAssets.forEach(other => {
+ const otherKey = String(other.feature_id || `${other.model_key}_${other.id}`);
+ if (processedSet.has(otherKey)) return;
+
+ const samePole = (other.pole_id && asset.pole_id && String(other.pole_id) === String(asset.pole_id))
+ || (asset.model_key && asset.model_key.includes('pole') && String(other.pole_id) === String(asset.id))
+ || (other.model_key && other.model_key.includes('pole') && String(asset.pole_id) === String(other.id));
+
+ const dLon = Math.abs(other.lon - asset.lon);
+ 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 })
- .setLngLat([asset.lon, asset.lat])
- .addTo(map);
+ locationGroups.push(group);
+ });
- marker._asset = asset;
- marker._element = el;
+ // 3. Fonction pour déterminer l'asset prioritaire visuellement
+ 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);
- if (lastSelectedFeatureId && featId === lastSelectedFeatureId) {
- el.classList.add('selected');
- el.style.zIndex = '50';
- selectedMarkerElement = el;
+ // 4. Créer les marqueurs pour chaque emplacement avec offset vertical pour les panneaux
+ locationGroups.forEach(group => {
+ const signPanels = group.filter(a => a.model_key === 'signpanel');
+ const otherAssets = group.filter(a => a.model_key !== 'signpanel');
+
+ // 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)
+ : ``;
+
+ 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)
+ : ``;
+
+ 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)
+ : ``;
+
+ 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
@@ -1475,7 +1748,7 @@
}
// 5b. Volet d'aperçu individuel (Bottom Sheet)
- function openBottomSheet(asset, fromMulti = false) {
+ function openBottomSheet(asset, fromMulti = false, assetGroup = null) {
if (!asset) return;
// Fermer le volet multi-assets
@@ -1483,12 +1756,14 @@
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 (fromMulti && lastMultiAssets && lastMultiAssets.length > 1) {
+ if (siblingGroup && siblingGroup.length > 1) {
+ lastMultiAssets = siblingGroup;
btnBackToMultiSheet.classList.remove('d-none');
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 {
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);
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))
- ));
+ const matchingMarker = activeMarkers.find(m => {
+ if (!m._element) return false;
+ 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) {
selectedMarkerElement = matchingMarker._element;
selectedMarkerElement.classList.add('selected');
diff --git a/loko/mobile/tests_asset_mobile.py b/loko/mobile/tests_asset_mobile.py
index 1945312..4311528 100644
--- a/loko/mobile/tests_asset_mobile.py
+++ b/loko/mobile/tests_asset_mobile.py
@@ -533,6 +533,7 @@ class MobileAssetDetailTests(TestCase):
street=street,
pole=pole,
signpanel_type=panel_type,
+ vertical_order=1,
status="active",
)
@@ -549,6 +550,9 @@ class MobileAssetDetailTests(TestCase):
self.assertEqual(panel_item["panel_type"], "B1")
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["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["lon"], 4.3528, places=4)
self.assertIn("B1", panel_item["name"])
@@ -558,6 +562,9 @@ class MobileAssetDetailTests(TestCase):
self.assertEqual(feature["properties"]["panel_type"], "B1")
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"]["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"][0], 4.3528, places=4)
@@ -568,4 +575,63 @@ class MobileAssetDetailTests(TestCase):
self.assertEqual(data["count"], 1)
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)
+
+
diff --git a/loko/mobile/views.py b/loko/mobile/views.py
index 6631ef1..7f0f5ab 100644
--- a/loko/mobile/views.py
+++ b/loko/mobile/views.py
@@ -1370,7 +1370,23 @@ def mobile_assets_search(request):
# Filtrage par boîte englobante (bbox / zoom de la carte)
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)
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)
@@ -1421,7 +1437,7 @@ def mobile_assets_search(request):
for model_key, model_class, qs in model_querysets:
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'):
qs = qs.select_related('model', 'pole')
items = list(qs)
@@ -1572,6 +1588,21 @@ def mobile_assets_search(request):
structure_type_val = getattr(item, 'structure_type', None)
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 = {
'id': item.id,
'feature_id': feature_id,
@@ -1597,6 +1628,11 @@ def mobile_assets_search(request):
'lon': lon,
'distance_m': distance_m,
'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)
@@ -1631,6 +1667,11 @@ def mobile_assets_search(request):
'detail_url': detail_url,
'lon': lon,
'lat': lat,
+ 'pole_id': pole_id,
+ 'vertical_order': vertical_order,
+ 'panel_vertical_order': vertical_order,
+ 'width': panel_width,
+ 'height': panel_height,
}
})