feat: enhance mobile asset map UI with floating alerts and updated limit handling
This commit is contained in:
parent
4b1bc59f1e
commit
2ada4f88b7
3 changed files with 204 additions and 134 deletions
|
|
@ -141,6 +141,30 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Alerte flottante sur la carte */
|
||||||
|
.map-floating-alert {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.65rem;
|
||||||
|
left: 0.65rem;
|
||||||
|
right: 0.65rem;
|
||||||
|
z-index: 25;
|
||||||
|
background-color: rgba(240, 249, 255, 0.96) !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid #bae6fd !important;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18) !important;
|
||||||
|
animation: slideDownFadeIn 0.25s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes slideDownFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bouton flottant GPS */
|
/* Bouton flottant GPS */
|
||||||
.map-gps-btn {
|
.map-gps-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -410,23 +434,10 @@
|
||||||
<div id="filterLimitAlert" class="alert alert-warning py-1.5 px-2.5 mb-1.5 small d-flex align-items-center gap-2 rounded-3 d-none flex-shrink-0" role="alert">
|
<div id="filterLimitAlert" class="alert alert-warning py-1.5 px-2.5 mb-1.5 small d-flex align-items-center gap-2 rounded-3 d-none flex-shrink-0" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle-fill fs-6 text-warning flex-shrink-0"></i>
|
<i class="bi bi-exclamation-triangle-fill fs-6 text-warning flex-shrink-0"></i>
|
||||||
<div class="flex-grow-1" style="line-height: 1.25;">
|
<div class="flex-grow-1" style="line-height: 1.25;">
|
||||||
<strong>{% translate "Limite atteinte" %}</strong> : {% translate "Plus de 500 assets correspondent. Veuillez zoomer sur la carte ou affiner vos critères de recherche." %}
|
<strong>{% translate "Limite atteinte" %}</strong> : <span id="filterLimitAlertText">{% translate "Plus de 500 assets correspondent. Veuillez zoomer sur la carte ou affiner vos critères de recherche pour les afficher." %}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alerte Aucun résultat dans la zone visible (invitation retour vue initiale) -->
|
|
||||||
<div id="noResultsInZoneAlert" class="alert alert-info py-2 px-2.5 mb-1.5 small d-flex align-items-center gap-2 rounded-3 shadow-sm d-none flex-shrink-0" role="alert">
|
|
||||||
<i class="bi bi-info-circle-fill fs-5 text-primary flex-shrink-0"></i>
|
|
||||||
<div class="flex-grow-1" style="line-height: 1.3;">
|
|
||||||
<div class="fw-semibold text-dark">{% translate "Aucun résultat dans cette zone" %}</div>
|
|
||||||
<div class="text-secondary small">{% translate "Appuyez sur le bouton pour revenir à la vue initiale." %}</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="btnResetToInitialView" class="btn btn-sm btn-primary rounded-3 text-nowrap py-1.5 px-2.5 d-flex align-items-center gap-1 shadow-sm flex-shrink-0">
|
|
||||||
<i class="bi bi-arrows-angle-expand"></i>
|
|
||||||
<span>{% translate "Vue initiale" %}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indicateur de résultats & compteur -->
|
<!-- Indicateur de résultats & compteur -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1.5 px-1 text-secondary small flex-shrink-0">
|
<div class="d-flex justify-content-between align-items-center mb-1.5 px-1 text-secondary small flex-shrink-0">
|
||||||
<div class="d-flex align-items-center gap-1 flex-wrap">
|
<div class="d-flex align-items-center gap-1 flex-wrap">
|
||||||
|
|
@ -451,6 +462,20 @@
|
||||||
<div id="assets-map-wrapper">
|
<div id="assets-map-wrapper">
|
||||||
<div id="assets-map"></div>
|
<div id="assets-map"></div>
|
||||||
|
|
||||||
|
<!-- Alerte flottante : Aucun résultat dans la zone visible (invitation retour vue initiale) -->
|
||||||
|
<div id="noResultsInZoneAlert" class="map-floating-alert alert alert-info py-2 px-2.5 mb-0 small d-flex align-items-center gap-2 rounded-3 shadow d-none" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill fs-5 text-primary flex-shrink-0"></i>
|
||||||
|
<div class="flex-grow-1" style="line-height: 1.25; min-width: 0;">
|
||||||
|
<div class="fw-semibold text-dark text-truncate">{% translate "Aucun résultat dans cette zone" %}</div>
|
||||||
|
<div class="text-secondary small">{% translate "Appuyez sur le bouton pour revenir à la vue initiale." %}</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btnResetToInitialView" class="btn btn-sm btn-primary rounded-3 text-nowrap py-1 px-2.5 d-flex align-items-center gap-1 shadow-sm flex-shrink-0">
|
||||||
|
<i class="bi bi-arrows-angle-expand"></i>
|
||||||
|
<span class="small">{% translate "Vue initiale" %}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="btnCloseNoResultsAlert" class="btn-close ms-0.5 flex-shrink-0" style="font-size: 0.7rem;" aria-label="{% translate 'Fermer' %}"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Invite sélection thématique sur la carte -->
|
<!-- Invite sélection thématique sur la carte -->
|
||||||
<div id="mapThematicPrompt" class="position-absolute top-50 start-50 translate-middle text-center p-3 bg-white bg-opacity-95 rounded-4 shadow-sm border {% if selected_thematic %}d-none{% endif %}" style="z-index: 8; max-width: 85%; pointer-events: none;">
|
<div id="mapThematicPrompt" class="position-absolute top-50 start-50 translate-middle text-center p-3 bg-white bg-opacity-95 rounded-4 shadow-sm border {% if selected_thematic %}d-none{% endif %}" style="z-index: 8; max-width: 85%; pointer-events: none;">
|
||||||
<div class="rounded-circle bg-primary-subtle text-primary d-inline-flex p-3 mb-2">
|
<div class="rounded-circle bg-primary-subtle text-primary d-inline-flex p-3 mb-2">
|
||||||
|
|
@ -488,9 +513,9 @@
|
||||||
|
|
||||||
<div id="emptyListMessage" class="text-center py-5 d-none">
|
<div id="emptyListMessage" class="text-center py-5 d-none">
|
||||||
<div class="rounded-circle bg-light d-inline-flex p-3 mb-3 text-muted">
|
<div class="rounded-circle bg-light d-inline-flex p-3 mb-3 text-muted">
|
||||||
<i class="bi bi-search fs-1"></i>
|
<i class="bi bi-search fs-1" id="emptyListIcon"></i>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fw-bold text-secondary">{% translate "Aucun asset trouvé" %}</h6>
|
<h6 class="fw-bold text-secondary" id="emptyListTitle">{% translate "Aucun asset trouvé" %}</h6>
|
||||||
<p class="small text-muted mb-3" id="emptyListSubtitle">{% translate "Modifiez votre recherche ou zoomez sur une autre zone." %}</p>
|
<p class="small text-muted mb-3" id="emptyListSubtitle">{% translate "Modifiez votre recherche ou zoomez sur une autre zone." %}</p>
|
||||||
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm rounded-3" id="btnResetFilters">
|
<button type="button" class="btn btn-outline-primary btn-sm rounded-3" id="btnResetFilters">
|
||||||
|
|
@ -594,6 +619,8 @@
|
||||||
let noResultsAlertTimeout = null;
|
let noResultsAlertTimeout = null;
|
||||||
let mapMoveTimeout = null;
|
let mapMoveTimeout = null;
|
||||||
let lastResults = [];
|
let lastResults = [];
|
||||||
|
let lastLimitReached = false;
|
||||||
|
let lastTotalCount = 0;
|
||||||
|
|
||||||
// Éléments DOM
|
// Éléments DOM
|
||||||
const searchInput = document.getElementById('assetSearchInput');
|
const searchInput = document.getElementById('assetSearchInput');
|
||||||
|
|
@ -608,11 +635,15 @@
|
||||||
const viewContainerList = document.getElementById('viewContainerList');
|
const viewContainerList = document.getElementById('viewContainerList');
|
||||||
const assetsListContainer = document.getElementById('assetsListContainer');
|
const assetsListContainer = document.getElementById('assetsListContainer');
|
||||||
const emptyListMessage = document.getElementById('emptyListMessage');
|
const emptyListMessage = document.getElementById('emptyListMessage');
|
||||||
|
const emptyListIcon = document.getElementById('emptyListIcon');
|
||||||
|
const emptyListTitle = document.getElementById('emptyListTitle');
|
||||||
const noThematicSelectedMessage = document.getElementById('noThematicSelectedMessage');
|
const noThematicSelectedMessage = document.getElementById('noThematicSelectedMessage');
|
||||||
const mapThematicPrompt = document.getElementById('mapThematicPrompt');
|
const mapThematicPrompt = document.getElementById('mapThematicPrompt');
|
||||||
const filterLimitAlert = document.getElementById('filterLimitAlert');
|
const filterLimitAlert = document.getElementById('filterLimitAlert');
|
||||||
|
const filterLimitAlertText = document.getElementById('filterLimitAlertText');
|
||||||
const noResultsInZoneAlert = document.getElementById('noResultsInZoneAlert');
|
const noResultsInZoneAlert = document.getElementById('noResultsInZoneAlert');
|
||||||
const btnResetToInitialView = document.getElementById('btnResetToInitialView');
|
const btnResetToInitialView = document.getElementById('btnResetToInitialView');
|
||||||
|
const btnCloseNoResultsAlert = document.getElementById('btnCloseNoResultsAlert');
|
||||||
const btnInitialView = document.getElementById('btnInitialView');
|
const btnInitialView = document.getElementById('btnInitialView');
|
||||||
const btnListInitialView = document.getElementById('btnListInitialView');
|
const btnListInitialView = document.getElementById('btnListInitialView');
|
||||||
const emptyListSubtitle = document.getElementById('emptyListSubtitle');
|
const emptyListSubtitle = document.getElementById('emptyListSubtitle');
|
||||||
|
|
@ -770,10 +801,6 @@
|
||||||
closeBottomSheet();
|
closeBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('movestart', () => {
|
|
||||||
hideNoResultsInZoneAlert();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Détection du déplacement ou zoom pour filtrage par zone visible (bbox) et synchronisation de la liste
|
// Détection du déplacement ou zoom pour filtrage par zone visible (bbox) et synchronisation de la liste
|
||||||
map.on('moveend', () => {
|
map.on('moveend', () => {
|
||||||
// Ignorer si la vue active n'est pas la carte ou si aucune thématique n'est choisie
|
// 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
|
// 4. Rendu des résultats sur Carte et dans la Liste
|
||||||
function renderResults(data, fromMapMove = false, options = {}) {
|
function renderResults(data, fromMapMove = false, options = {}) {
|
||||||
lastResults = data.results || [];
|
lastResults = data.results || [];
|
||||||
|
lastLimitReached = Boolean(data.limit_reached);
|
||||||
|
lastTotalCount = data.total_count || 0;
|
||||||
|
|
||||||
// Gestion de la limite de 500 assets atteinte
|
// Gestion de la limite de 500 assets atteinte
|
||||||
if (data.limit_reached) {
|
if (data.limit_reached) {
|
||||||
if (filterLimitAlert) filterLimitAlert.classList.remove('d-none');
|
if (filterLimitAlert) {
|
||||||
if (limitNotice) limitNotice.classList.remove('d-none');
|
filterLimitAlert.classList.remove('d-none');
|
||||||
resultsCountEl.textContent = `${data.count} / ${data.total_count}`;
|
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 {
|
} else {
|
||||||
if (filterLimitAlert) filterLimitAlert.classList.add('d-none');
|
if (filterLimitAlert) filterLimitAlert.classList.add('d-none');
|
||||||
if (limitNotice) limitNotice.classList.add('d-none');
|
if (limitNotice) limitNotice.classList.add('d-none');
|
||||||
|
|
@ -952,20 +986,31 @@
|
||||||
handleNoResultsInZoneAlert(data, options);
|
handleNoResultsInZoneAlert(data, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderListView(results) {
|
function renderListView(results = lastResults) {
|
||||||
assetsListContainer.innerHTML = '';
|
assetsListContainer.innerHTML = '';
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
emptyListMessage.classList.remove('d-none');
|
emptyListMessage.classList.remove('d-none');
|
||||||
if (btnListInitialView) {
|
if (lastLimitReached) {
|
||||||
if (currentQuery && !isAtMaxBbox()) {
|
if (emptyListIcon) emptyListIcon.className = 'bi bi-zoom-in fs-1 text-warning';
|
||||||
btnListInitialView.classList.remove('d-none');
|
if (emptyListTitle) emptyListTitle.textContent = "{% translate 'Limite de 500 assets atteinte' %}";
|
||||||
if (emptyListSubtitle) {
|
if (emptyListSubtitle) {
|
||||||
emptyListSubtitle.textContent = "{% translate 'Aucun asset trouvé dans cette zone pour votre recherche.' %}";
|
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." %}`;
|
||||||
}
|
}
|
||||||
} else {
|
if (btnListInitialView) btnListInitialView.classList.add('d-none');
|
||||||
btnListInitialView.classList.add('d-none');
|
} else {
|
||||||
if (emptyListSubtitle) {
|
if (emptyListIcon) emptyListIcon.className = 'bi bi-search fs-1 text-muted';
|
||||||
emptyListSubtitle.textContent = "{% translate 'Modifiez votre recherche ou zoomez sur une autre zone.' %}";
|
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)
|
// Vérifier si la vue courante correspond à la boîte englobante maximale (vue initiale)
|
||||||
function isAtMaxBbox() {
|
function isAtMaxBbox() {
|
||||||
if (!map) return true;
|
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 currentZoom = map.getZoom();
|
||||||
const currentCenter = map.getCenter();
|
const currentCenter = map.getCenter();
|
||||||
|
|
||||||
|
|
@ -1199,13 +1247,8 @@
|
||||||
const dLat = Math.abs(currentCenter.lat - defaultCenter[1]);
|
const dLat = Math.abs(currentCenter.lat - defaultCenter[1]);
|
||||||
const zoomDiff = currentZoom - defaultZoom;
|
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)
|
// 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.02 || dLat > 0.02) {
|
if (zoomDiff > 0.5 || dLng > 0.025 || dLat > 0.025) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si isBboxFiltered est actif et que la position/zoom a bougé
|
|
||||||
if (isBboxFiltered && (zoomDiff > 0.25 || dLng > 0.015 || dLat > 0.015)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1240,6 +1283,12 @@
|
||||||
const count = data.count || (data.results ? data.results.length : 0);
|
const count = data.count || (data.results ? data.results.length : 0);
|
||||||
const hasQuery = Boolean(currentQuery && currentQuery.trim().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
|
// Si la recherche a retourné des résultats ou si aucune recherche textuelle n'est active, masquer l'alerte
|
||||||
if (!hasQuery || count > 0) {
|
if (!hasQuery || count > 0) {
|
||||||
hideNoResultsInZoneAlert();
|
hideNoResultsInZoneAlert();
|
||||||
|
|
@ -1263,9 +1312,9 @@
|
||||||
if (currentQuery && currentQuery.trim().length >= 2 && lastResults.length === 0 && !isAtMaxBbox()) {
|
if (currentQuery && currentQuery.trim().length >= 2 && lastResults.length === 0 && !isAtMaxBbox()) {
|
||||||
showNoResultsInZoneAlert();
|
showNoResultsInZoneAlert();
|
||||||
}
|
}
|
||||||
}, 850);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
// Soumission explicite (touche Entrée ou clic loupe)
|
// Soumission explicite (touche Entrée ou clic loupe) ou déplacement de carte
|
||||||
showNoResultsInZoneAlert();
|
showNoResultsInZoneAlert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1300,6 +1349,9 @@
|
||||||
if (btnResetToInitialView) {
|
if (btnResetToInitialView) {
|
||||||
btnResetToInitialView.addEventListener('click', returnToInitialView);
|
btnResetToInitialView.addEventListener('click', returnToInitialView);
|
||||||
}
|
}
|
||||||
|
if (btnCloseNoResultsAlert) {
|
||||||
|
btnCloseNoResultsAlert.addEventListener('click', hideNoResultsInZoneAlert);
|
||||||
|
}
|
||||||
if (btnInitialView) {
|
if (btnInitialView) {
|
||||||
btnInitialView.addEventListener('click', returnToInitialView);
|
btnInitialView.addEventListener('click', returnToInitialView);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -317,11 +317,13 @@ class MobileAssetDetailTests(TestCase):
|
||||||
self.assertIn("ARB-TEST-001", codes)
|
self.assertIn("ARB-TEST-001", codes)
|
||||||
self.assertIn("ARB-SHRUB-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"})
|
response = self.client.get(search_url, {"thematic": "nature", "limit": "1"})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
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.assertEqual(data["limit"], 1)
|
||||||
self.assertTrue(data["limit_reached"])
|
self.assertTrue(data["limit_reached"])
|
||||||
self.assertGreater(data["total_count"], 1)
|
self.assertGreater(data["total_count"], 1)
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(max(0.0, 1.0 - a)))
|
||||||
return R * c
|
return R * c
|
||||||
|
|
||||||
results = []
|
model_querysets = []
|
||||||
features = []
|
|
||||||
total_count = 0
|
total_count = 0
|
||||||
|
|
||||||
t_code = thematic_filter
|
t_code = thematic_filter
|
||||||
|
|
@ -1386,116 +1385,133 @@ def mobile_assets_search(request):
|
||||||
# Nombre total d'assets correspondants (pour savoir si limite atteinte)
|
# Nombre total d'assets correspondants (pour savoir si limite atteinte)
|
||||||
count_for_model = qs.count()
|
count_for_model = qs.count()
|
||||||
total_count += count_for_model
|
total_count += count_for_model
|
||||||
|
model_querysets.append((model_key, model_class, qs))
|
||||||
|
|
||||||
remaining_slots = limit - len(results)
|
limit_reached = total_count > limit
|
||||||
if remaining_slots > 0:
|
|
||||||
items = list(qs[:remaining_slots])
|
# Si la limite est atteinte, ne renvoyer aucun asset ni GeoJSON pour ne pas induire en erreur et alléger la requête
|
||||||
for item in items:
|
if limit_reached:
|
||||||
lat = None
|
return JsonResponse({
|
||||||
lon = None
|
'count': 0,
|
||||||
if hasattr(item, 'lat') and item.lat is not None and hasattr(item, 'lon') and item.lon is not None:
|
'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:
|
try:
|
||||||
lat = float(str(item.lat).replace(',', '.'))
|
lat = float(str(loc.lat).replace(',', '.'))
|
||||||
lon = float(str(item.lon).replace(',', '.'))
|
lon = float(str(loc.lon).replace(',', '.'))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
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
|
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:
|
if user_lat is not None and user_lon is not None and lat is not None and lon is not None:
|
||||||
try:
|
try:
|
||||||
distance_m = round(_haversine(user_lat, user_lon, lat, lon))
|
distance_m = round(_haversine(user_lat, user_lon, lat, lon))
|
||||||
except Exception:
|
except Exception:
|
||||||
distance_m = None
|
distance_m = None
|
||||||
|
|
||||||
item_name = (
|
item_name = (
|
||||||
getattr(item, f'name_{lang}', None) or
|
getattr(item, f'name_{lang}', None) or
|
||||||
getattr(item, 'name_fr', 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
|
(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_val = getattr(item, 'status', 'active')
|
||||||
status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
|
status_display = item.get_status_display() if hasattr(item, 'get_status_display') else status_val
|
||||||
element_type = get_element_type_label(model_key)
|
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 = {
|
res_item = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'code': getattr(item, 'code', f'#{item.id}'),
|
'code': getattr(item, 'code', f'#{item.id}'),
|
||||||
'name': item_name,
|
'name': item_name,
|
||||||
'thematic': t_code,
|
'thematic': t_code,
|
||||||
'thematic_name': thematic_name,
|
'thematic_name': thematic_name,
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'status': status_val,
|
'status': status_val,
|
||||||
'status_display': status_display,
|
'status_display': status_display,
|
||||||
'lat': lat,
|
'lat': lat,
|
||||||
'lon': lon,
|
'lon': lon,
|
||||||
'distance_m': distance_m,
|
'distance_m': distance_m,
|
||||||
'detail_url': detail_url,
|
'detail_url': detail_url,
|
||||||
}
|
}
|
||||||
results.append(res_item)
|
results.append(res_item)
|
||||||
|
|
||||||
if lon is not None and lat is not None:
|
if lon is not None and lat is not None:
|
||||||
features.append({
|
features.append({
|
||||||
'type': 'Feature',
|
'type': 'Feature',
|
||||||
'id': f"{model_key}_{item.id}",
|
'id': f"{model_key}_{item.id}",
|
||||||
'geometry': {
|
'geometry': {
|
||||||
'type': 'Point',
|
'type': 'Point',
|
||||||
'coordinates': [lon, lat],
|
'coordinates': [lon, lat],
|
||||||
},
|
},
|
||||||
'properties': {
|
'properties': {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'code': getattr(item, 'code', f'#{item.id}'),
|
'code': getattr(item, 'code', f'#{item.id}'),
|
||||||
'name': item_name,
|
'name': item_name,
|
||||||
'thematic': t_code,
|
'thematic': t_code,
|
||||||
'thematic_name': thematic_name,
|
'thematic_name': thematic_name,
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'status': status_val,
|
'status': status_val,
|
||||||
'status_display': status_display,
|
'status_display': status_display,
|
||||||
'distance_m': distance_m,
|
'distance_m': distance_m,
|
||||||
'detail_url': detail_url,
|
'detail_url': detail_url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if user_lat is not None and user_lon is not None:
|
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']))
|
results.sort(key=lambda x: (x['distance_m'] if x['distance_m'] is not None else 999999999, x['code']))
|
||||||
else:
|
else:
|
||||||
results.sort(key=lambda x: x['code'])
|
results.sort(key=lambda x: x['code'])
|
||||||
|
|
||||||
results = results[:limit]
|
|
||||||
limit_reached = total_count > limit
|
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'count': len(results),
|
'count': len(results),
|
||||||
'total_count': total_count,
|
'total_count': total_count,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
'limit_reached': limit_reached,
|
'limit_reached': False,
|
||||||
'thematic': t_code,
|
'thematic': t_code,
|
||||||
'results': results,
|
'results': results,
|
||||||
'geojson': {
|
'geojson': {
|
||||||
'type': 'FeatureCollection',
|
'type': 'FeatureCollection',
|
||||||
'features': features[:limit],
|
'features': features,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue