-
+
-
{% translate "Aucun asset trouvé" %}
+
{% translate "Aucun asset trouvé" %}
{% translate "Modifiez votre recherche ou zoomez sur une autre zone." %}
@@ -594,6 +619,8 @@
let noResultsAlertTimeout = null;
let mapMoveTimeout = null;
let lastResults = [];
+ let lastLimitReached = false;
+ let lastTotalCount = 0;
// Éléments DOM
const searchInput = document.getElementById('assetSearchInput');
@@ -608,11 +635,15 @@
const viewContainerList = document.getElementById('viewContainerList');
const assetsListContainer = document.getElementById('assetsListContainer');
const emptyListMessage = document.getElementById('emptyListMessage');
+ const emptyListIcon = document.getElementById('emptyListIcon');
+ const emptyListTitle = document.getElementById('emptyListTitle');
const noThematicSelectedMessage = document.getElementById('noThematicSelectedMessage');
const mapThematicPrompt = document.getElementById('mapThematicPrompt');
const filterLimitAlert = document.getElementById('filterLimitAlert');
+ const filterLimitAlertText = document.getElementById('filterLimitAlertText');
const noResultsInZoneAlert = document.getElementById('noResultsInZoneAlert');
const btnResetToInitialView = document.getElementById('btnResetToInitialView');
+ const btnCloseNoResultsAlert = document.getElementById('btnCloseNoResultsAlert');
const btnInitialView = document.getElementById('btnInitialView');
const btnListInitialView = document.getElementById('btnListInitialView');
const emptyListSubtitle = document.getElementById('emptyListSubtitle');
@@ -770,10 +801,6 @@
closeBottomSheet();
});
- map.on('movestart', () => {
- hideNoResultsInZoneAlert();
- });
-
// Détection du déplacement ou zoom pour filtrage par zone visible (bbox) et synchronisation de la liste
map.on('moveend', () => {
// Ignorer si la vue active n'est pas la carte ou si aucune thématique n'est choisie
@@ -930,12 +957,19 @@
// 4. Rendu des résultats sur Carte et dans la Liste
function renderResults(data, fromMapMove = false, options = {}) {
lastResults = data.results || [];
+ lastLimitReached = Boolean(data.limit_reached);
+ lastTotalCount = data.total_count || 0;
// Gestion de la limite de 500 assets atteinte
if (data.limit_reached) {
- if (filterLimitAlert) filterLimitAlert.classList.remove('d-none');
- if (limitNotice) limitNotice.classList.remove('d-none');
- resultsCountEl.textContent = `${data.count} / ${data.total_count}`;
+ if (filterLimitAlert) {
+ filterLimitAlert.classList.remove('d-none');
+ if (filterLimitAlertText && data.total_count) {
+ filterLimitAlertText.textContent = `{% translate "Plus de 500 assets correspondent" %} (${data.total_count} {% translate "au total" %}). {% translate "Veuillez zoomer sur la carte ou affiner vos critères de recherche pour les afficher." %}`;
+ }
+ }
+ if (limitNotice) limitNotice.classList.add('d-none');
+ resultsCountEl.textContent = `${data.total_count}`;
} else {
if (filterLimitAlert) filterLimitAlert.classList.add('d-none');
if (limitNotice) limitNotice.classList.add('d-none');
@@ -952,20 +986,31 @@
handleNoResultsInZoneAlert(data, options);
}
- function renderListView(results) {
+ function renderListView(results = lastResults) {
assetsListContainer.innerHTML = '';
if (!results || results.length === 0) {
emptyListMessage.classList.remove('d-none');
- if (btnListInitialView) {
- if (currentQuery && !isAtMaxBbox()) {
- btnListInitialView.classList.remove('d-none');
- if (emptyListSubtitle) {
- emptyListSubtitle.textContent = "{% translate 'Aucun asset trouvé dans cette zone pour votre recherche.' %}";
- }
- } else {
- btnListInitialView.classList.add('d-none');
- if (emptyListSubtitle) {
- emptyListSubtitle.textContent = "{% translate 'Modifiez votre recherche ou zoomez sur une autre zone.' %}";
+ if (lastLimitReached) {
+ if (emptyListIcon) emptyListIcon.className = 'bi bi-zoom-in fs-1 text-warning';
+ if (emptyListTitle) emptyListTitle.textContent = "{% translate 'Limite de 500 assets atteinte' %}";
+ if (emptyListSubtitle) {
+ emptyListSubtitle.textContent = `{% translate "Plus de 500 assets correspondent" %}${lastTotalCount ? ` (${lastTotalCount} {% translate "au total" %})` : ''}. {% translate "Veuillez zoomer sur la carte ou affiner vos critères de recherche pour afficher la liste." %}`;
+ }
+ if (btnListInitialView) btnListInitialView.classList.add('d-none');
+ } else {
+ if (emptyListIcon) emptyListIcon.className = 'bi bi-search fs-1 text-muted';
+ if (emptyListTitle) emptyListTitle.textContent = "{% translate 'Aucun asset trouvé' %}";
+ if (btnListInitialView) {
+ if (currentQuery && !isAtMaxBbox()) {
+ btnListInitialView.classList.remove('d-none');
+ if (emptyListSubtitle) {
+ emptyListSubtitle.textContent = "{% translate 'Aucun asset trouvé dans cette zone pour votre recherche.' %}";
+ }
+ } else {
+ btnListInitialView.classList.add('d-none');
+ if (emptyListSubtitle) {
+ emptyListSubtitle.textContent = "{% translate 'Modifiez votre recherche ou zoomez sur une autre zone.' %}";
+ }
}
}
}
@@ -1192,6 +1237,9 @@
// Vérifier si la vue courante correspond à la boîte englobante maximale (vue initiale)
function isAtMaxBbox() {
if (!map) return true;
+ // Si la recherche est restreinte par une boîte englobante (zone visible), elle n'est pas à la bbox maximale
+ if (isBboxFiltered) return false;
+
const currentZoom = map.getZoom();
const currentCenter = map.getCenter();
@@ -1199,13 +1247,8 @@
const dLat = Math.abs(currentCenter.lat - defaultCenter[1]);
const zoomDiff = currentZoom - defaultZoom;
- // Si le zoom est plus grand que le zoom initial (+0.5) ou si le centre s'est décalé (> 0.02 deg ~ 1.5 km)
- if (zoomDiff > 0.5 || dLng > 0.02 || dLat > 0.02) {
- return false;
- }
-
- // Si isBboxFiltered est actif et que la position/zoom a bougé
- if (isBboxFiltered && (zoomDiff > 0.25 || dLng > 0.015 || dLat > 0.015)) {
+ // Si le zoom est plus grand que le zoom initial (+0.5) ou si le centre s'est décalé (> 0.025 deg)
+ if (zoomDiff > 0.5 || dLng > 0.025 || dLat > 0.025) {
return false;
}
@@ -1240,6 +1283,12 @@
const count = data.count || (data.results ? data.results.length : 0);
const hasQuery = Boolean(currentQuery && currentQuery.trim().length > 0);
+ // Si la limite de 500 est atteinte, ce n'est pas une zone vide : masquer l'alerte de zone vide
+ if (data.limit_reached) {
+ hideNoResultsInZoneAlert();
+ return;
+ }
+
// Si la recherche a retourné des résultats ou si aucune recherche textuelle n'est active, masquer l'alerte
if (!hasQuery || count > 0) {
hideNoResultsInZoneAlert();
@@ -1263,9 +1312,9 @@
if (currentQuery && currentQuery.trim().length >= 2 && lastResults.length === 0 && !isAtMaxBbox()) {
showNoResultsInZoneAlert();
}
- }, 850);
+ }, 500);
} else {
- // Soumission explicite (touche Entrée ou clic loupe)
+ // Soumission explicite (touche Entrée ou clic loupe) ou déplacement de carte
showNoResultsInZoneAlert();
}
}
@@ -1300,6 +1349,9 @@
if (btnResetToInitialView) {
btnResetToInitialView.addEventListener('click', returnToInitialView);
}
+ if (btnCloseNoResultsAlert) {
+ btnCloseNoResultsAlert.addEventListener('click', hideNoResultsInZoneAlert);
+ }
if (btnInitialView) {
btnInitialView.addEventListener('click', returnToInitialView);
}
diff --git a/loko/mobile/tests_asset_mobile.py b/loko/mobile/tests_asset_mobile.py
index 9546cb0..e13202b 100644
--- a/loko/mobile/tests_asset_mobile.py
+++ b/loko/mobile/tests_asset_mobile.py
@@ -317,11 +317,13 @@ class MobileAssetDetailTests(TestCase):
self.assertIn("ARB-TEST-001", codes)
self.assertIn("ARB-SHRUB-001", codes)
- # 3. Test limit reached logic with limit=1
+ # 3. Test limit reached logic with limit=1: when limit is reached, return empty results and empty geojson
response = self.client.get(search_url, {"thematic": "nature", "limit": "1"})
self.assertEqual(response.status_code, 200)
data = response.json()
- self.assertEqual(data["count"], 1)
+ self.assertEqual(data["count"], 0)
+ self.assertEqual(data["results"], [])
+ self.assertEqual(data["geojson"]["features"], [])
self.assertEqual(data["limit"], 1)
self.assertTrue(data["limit_reached"])
self.assertGreater(data["total_count"], 1)
diff --git a/loko/mobile/views.py b/loko/mobile/views.py
index 7ef17dc..7b5d175 100644
--- a/loko/mobile/views.py
+++ b/loko/mobile/views.py
@@ -1348,8 +1348,7 @@ def mobile_assets_search(request):
c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(max(0.0, 1.0 - a)))
return R * c
- results = []
- features = []
+ model_querysets = []
total_count = 0
t_code = thematic_filter
@@ -1386,116 +1385,133 @@ def mobile_assets_search(request):
# Nombre total d'assets correspondants (pour savoir si limite atteinte)
count_for_model = qs.count()
total_count += count_for_model
+ model_querysets.append((model_key, model_class, qs))
- remaining_slots = limit - len(results)
- if remaining_slots > 0:
- items = list(qs[:remaining_slots])
- for item in items:
- lat = None
- lon = None
- if hasattr(item, 'lat') and item.lat is not None and hasattr(item, 'lon') and item.lon is not None:
+ limit_reached = total_count > limit
+
+ # Si la limite est atteinte, ne renvoyer aucun asset ni GeoJSON pour ne pas induire en erreur et alléger la requête
+ if limit_reached:
+ return JsonResponse({
+ 'count': 0,
+ 'total_count': total_count,
+ 'limit': limit,
+ 'limit_reached': True,
+ 'thematic': t_code,
+ 'results': [],
+ 'geojson': {
+ 'type': 'FeatureCollection',
+ 'features': [],
+ }
+ })
+
+ results = []
+ features = []
+
+ for model_key, model_class, qs in model_querysets:
+ items = list(qs)
+ for item in items:
+ lat = None
+ lon = None
+ if hasattr(item, 'lat') and item.lat is not None and hasattr(item, 'lon') and item.lon is not None:
+ try:
+ lat = float(str(item.lat).replace(',', '.'))
+ lon = float(str(item.lon).replace(',', '.'))
+ except (ValueError, TypeError):
+ pass
+ elif hasattr(item, 'geom') and item.geom:
+ try:
+ g = item.geom.clone()
+ g.transform(4326)
+ centroid = g.centroid
+ lon = float(centroid.x)
+ lat = float(centroid.y)
+ except Exception:
+ pass
+ elif hasattr(item, 'location') and item.location:
+ loc = item.location
+ if hasattr(loc, 'lat') and loc.lat and hasattr(loc, 'lon') and loc.lon:
try:
- lat = float(str(item.lat).replace(',', '.'))
- lon = float(str(item.lon).replace(',', '.'))
+ lat = float(str(loc.lat).replace(',', '.'))
+ lon = float(str(loc.lon).replace(',', '.'))
except (ValueError, TypeError):
pass
- elif hasattr(item, 'geom') and item.geom:
- try:
- g = item.geom.clone()
- g.transform(4326)
- centroid = g.centroid
- lon = float(centroid.x)
- lat = float(centroid.y)
- except Exception:
- pass
- elif hasattr(item, 'location') and item.location:
- loc = item.location
- if hasattr(loc, 'lat') and loc.lat and hasattr(loc, 'lon') and loc.lon:
- try:
- lat = float(str(loc.lat).replace(',', '.'))
- lon = float(str(loc.lon).replace(',', '.'))
- except (ValueError, TypeError):
- pass
- distance_m = None
- if user_lat is not None and user_lon is not None and lat is not None and lon is not None:
- try:
- distance_m = round(_haversine(user_lat, user_lon, lat, lon))
- except Exception:
- distance_m = None
+ distance_m = None
+ if user_lat is not None and user_lon is not None and lat is not None and lon is not None:
+ try:
+ distance_m = round(_haversine(user_lat, user_lon, lat, lon))
+ except Exception:
+ distance_m = None
- item_name = (
- getattr(item, f'name_{lang}', None) or
- getattr(item, 'name_fr', None) or
- (getattr(item.model, f'name_{lang}', None) or getattr(item.model, 'name_fr', None) if getattr(item, 'model', None) else '') or
- ''
- )
- status_val = getattr(item, 'status', 'active')
- status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
- element_type = get_element_type_label(model_key)
+ item_name = (
+ getattr(item, f'name_{lang}', None) or
+ getattr(item, 'name_fr', None) or
+ (getattr(item.model, f'name_{lang}', None) or getattr(item.model, 'name_fr', None) if getattr(item, 'model', None) else '') or
+ ''
+ )
+ status_val = getattr(item, 'status', 'active')
+ status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
+ element_type = get_element_type_label(model_key)
- detail_url = reverse('mobile:mobile_asset_detail_direct', kwargs={'asset_model': model_key, 'asset_id': item.id})
+ detail_url = reverse('mobile:mobile_asset_detail_direct', kwargs={'asset_model': model_key, 'asset_id': item.id})
- res_item = {
- 'id': item.id,
- 'code': getattr(item, 'code', f'#{item.id}'),
- 'name': item_name,
- 'thematic': t_code,
- 'thematic_name': thematic_name,
- 'thematic_icon': thematic_icon,
- 'model_key': model_key,
- 'element_type': element_type,
- 'status': status_val,
- 'status_display': status_display,
- 'lat': lat,
- 'lon': lon,
- 'distance_m': distance_m,
- 'detail_url': detail_url,
- }
- results.append(res_item)
+ res_item = {
+ 'id': item.id,
+ 'code': getattr(item, 'code', f'#{item.id}'),
+ 'name': item_name,
+ 'thematic': t_code,
+ 'thematic_name': thematic_name,
+ 'thematic_icon': thematic_icon,
+ 'model_key': model_key,
+ 'element_type': element_type,
+ 'status': status_val,
+ 'status_display': status_display,
+ 'lat': lat,
+ 'lon': lon,
+ 'distance_m': distance_m,
+ 'detail_url': detail_url,
+ }
+ results.append(res_item)
- if lon is not None and lat is not None:
- features.append({
- 'type': 'Feature',
- 'id': f"{model_key}_{item.id}",
- 'geometry': {
- 'type': 'Point',
- 'coordinates': [lon, lat],
- },
- 'properties': {
- 'id': item.id,
- 'code': getattr(item, 'code', f'#{item.id}'),
- 'name': item_name,
- 'thematic': t_code,
- 'thematic_name': thematic_name,
- 'thematic_icon': thematic_icon,
- 'model_key': model_key,
- 'element_type': element_type,
- 'status': status_val,
- 'status_display': status_display,
- 'distance_m': distance_m,
- 'detail_url': detail_url,
- }
- })
+ if lon is not None and lat is not None:
+ features.append({
+ 'type': 'Feature',
+ 'id': f"{model_key}_{item.id}",
+ 'geometry': {
+ 'type': 'Point',
+ 'coordinates': [lon, lat],
+ },
+ 'properties': {
+ 'id': item.id,
+ 'code': getattr(item, 'code', f'#{item.id}'),
+ 'name': item_name,
+ 'thematic': t_code,
+ 'thematic_name': thematic_name,
+ 'thematic_icon': thematic_icon,
+ 'model_key': model_key,
+ 'element_type': element_type,
+ 'status': status_val,
+ 'status_display': status_display,
+ 'distance_m': distance_m,
+ 'detail_url': detail_url,
+ }
+ })
if user_lat is not None and user_lon is not None:
results.sort(key=lambda x: (x['distance_m'] if x['distance_m'] is not None else 999999999, x['code']))
else:
results.sort(key=lambda x: x['code'])
- results = results[:limit]
- limit_reached = total_count > limit
-
return JsonResponse({
'count': len(results),
'total_count': total_count,
'limit': limit,
- 'limit_reached': limit_reached,
+ 'limit_reached': False,
'thematic': t_code,
'results': results,
'geojson': {
'type': 'FeatureCollection',
- 'features': features[:limit],
+ 'features': features,
}
})