perf: optimize activity_geojson query, add caching and parallelize map data fetching
This commit is contained in:
parent
f0fb0df8ec
commit
de1d1c3b43
3 changed files with 152 additions and 97 deletions
|
|
@ -121,16 +121,20 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
}
|
||||
|
||||
map.on('load', async () => {
|
||||
// 0. Enregistrer les icônes d'opérations (symboles à l'intérieur des points)
|
||||
await Promise.all(
|
||||
Object.entries(OPERATION_ICONS).map(([key, info]) => registerOperationIcon(map, key, info, 32))
|
||||
);
|
||||
// Enregistrer les icônes d'opérations et lancer les requêtes GeoJSON en parallèle (gain réseau immédiat)
|
||||
const [maskData, actData] = await Promise.all([
|
||||
fetch('/common/territory-mask-geojson/')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.catch(e => { console.warn("Could not load territory mask:", e); return null; }),
|
||||
fetch('/api/activity-geojson/')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.catch(e => { console.error("Could not load activity geojson:", e); return null; }),
|
||||
...Object.entries(OPERATION_ICONS).map(([key, info]) => registerOperationIcon(map, key, info, 32))
|
||||
]);
|
||||
|
||||
// 1. Charger le masque de mise en lumière du territoire communal
|
||||
// 1. Appliquer le masque de mise en lumière du territoire communal
|
||||
if (maskData) {
|
||||
try {
|
||||
const maskResp = await fetch('/common/territory-mask-geojson/');
|
||||
if (maskResp.ok) {
|
||||
const maskData = await maskResp.json();
|
||||
map.addSource('territory-mask', {
|
||||
type: 'geojson',
|
||||
data: maskData
|
||||
|
|
@ -207,27 +211,19 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
duration: 1000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error rendering territory mask:", e);
|
||||
}
|
||||
} else if (window.MAP_DEFAULT_EXTENT && Array.isArray(window.MAP_DEFAULT_EXTENT) && window.MAP_DEFAULT_EXTENT.length === 2) {
|
||||
map.fitBounds(window.MAP_DEFAULT_EXTENT, {
|
||||
padding: { top: 35, bottom: 35, left: 35, right: 35 },
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not load territory mask:", e);
|
||||
if (window.MAP_DEFAULT_EXTENT && Array.isArray(window.MAP_DEFAULT_EXTENT) && window.MAP_DEFAULT_EXTENT.length === 2) {
|
||||
map.fitBounds(window.MAP_DEFAULT_EXTENT, {
|
||||
padding: { top: 35, bottom: 35, left: 35, right: 35 },
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Charger les données d'activité isolées par contrat
|
||||
// 2. Appliquer les données d'activité isolées par contrat
|
||||
if (actData) {
|
||||
try {
|
||||
const actResp = await fetch('/api/activity-geojson/');
|
||||
if (actResp.ok) {
|
||||
const actData = await actResp.json();
|
||||
map.addSource('activity-data', {
|
||||
type: 'geojson',
|
||||
data: actData
|
||||
|
|
|
|||
|
|
@ -186,5 +186,5 @@
|
|||
|
||||
</script>
|
||||
<script src="{% static 'interventions/status_colors.js' %}"></script>
|
||||
<script src="{% static 'home/home_index.js' %}"></script>
|
||||
<script src="{% static 'home/home_index.js' %}?v=20260921_1"></script>
|
||||
{% endblock script %}
|
||||
|
|
|
|||
|
|
@ -1218,8 +1218,11 @@ def activity_geojson(request):
|
|||
"""
|
||||
Renvoie le GeoJSON des actions des 90 derniers jours (interventions, observations, inspections, contrôles)
|
||||
strictement restreintes aux contrats autorisés pour l'utilisateur connecté.
|
||||
Permet d'isoler parfaitement les démonstrations de chaque commune.
|
||||
Optimisé avec projection PostGIS directe, élimination des N+1 requêtes et mise en cache.
|
||||
"""
|
||||
import hashlib
|
||||
from django.core.cache import cache
|
||||
from django.contrib.gis.db.models.functions import Centroid, Transform
|
||||
from contracts.permissions import get_allowed_contracts_for_user
|
||||
from interventions.models import Intervention
|
||||
from observations.models import Observation
|
||||
|
|
@ -1230,24 +1233,52 @@ def activity_geojson(request):
|
|||
if not allowed_contracts.exists():
|
||||
return JsonResponse({"type": "FeatureCollection", "features": []})
|
||||
|
||||
contract_ids = tuple(sorted(allowed_contracts.values_list('id', flat=True)))
|
||||
cache_key = f"activity_geojson_{hashlib.md5(str(contract_ids).encode()).hexdigest()}"
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data is not None:
|
||||
return JsonResponse(cached_data)
|
||||
|
||||
cutoff = timezone.now() - timedelta(days=90)
|
||||
features = []
|
||||
|
||||
# 1. Interventions du contrat
|
||||
intervs = Intervention.objects.filter(
|
||||
# URLs de base précalculées pour éviter d'appeler reverse() des milliers de fois
|
||||
interv_base_url = reverse("interventions:intervention_detail", args=[0])[:-2]
|
||||
obs_base_url = reverse("observations:observation_detail", args=[0])[:-2]
|
||||
ctrl_base_url = reverse("controls:controls_detail", args=[0])[:-2]
|
||||
insp_list_url = reverse("inspections:inspections_list")
|
||||
|
||||
# Dictionnaires de libellés de statut pour accès O(1)
|
||||
interv_status_dict = dict(Intervention.STATUS_CHOICES)
|
||||
obs_status_dict = dict(Observation.STATUS_CHOICES)
|
||||
insp_status_dict = dict(getattr(Inspection, 'STATUS_CHOICES', []))
|
||||
ctrl_status_dict = dict(getattr(Control, 'STATUS_CHOICES', []))
|
||||
|
||||
# 1. Interventions du contrat (projection EPSG:4326 au niveau PostGIS)
|
||||
intervs = (
|
||||
Intervention.objects.filter(
|
||||
contract__in=allowed_contracts,
|
||||
creation_time__gte=cutoff
|
||||
).exclude(geom__isnull=True).select_related('thematic')
|
||||
)
|
||||
.exclude(geom__isnull=True)
|
||||
.select_related('thematic')
|
||||
.annotate(geom_4326=Transform(Centroid('geom'), 4326))
|
||||
.only(
|
||||
'id', 'code', 'title', 'description', 'status', 'address', 'creation_time',
|
||||
'thematic__id', 'thematic__name', 'thematic__code'
|
||||
)
|
||||
)
|
||||
|
||||
for item in intervs:
|
||||
geom_wgs = item.geom.centroid.clone()
|
||||
geom_wgs.transform(4326)
|
||||
pt = getattr(item, 'geom_4326', None)
|
||||
if not pt:
|
||||
continue
|
||||
t_key = _normalize_thematic_key(item.thematic, f"{item.title} {item.description or ''}")
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
||||
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||
},
|
||||
"properties": {
|
||||
"id": f"int-{item.id}",
|
||||
|
|
@ -1259,28 +1290,38 @@ def activity_geojson(request):
|
|||
"code": item.code or f"INT-{item.id}",
|
||||
"title": item.title or "-",
|
||||
"status": item.status,
|
||||
"status_label": item.get_status_display() if hasattr(item, 'get_status_display') else item.status,
|
||||
"status_label": interv_status_dict.get(item.status, item.status),
|
||||
"address": item.address or "",
|
||||
"date": item.creation_time.strftime("%d/%m/%Y") if item.creation_time else "",
|
||||
"url": reverse("interventions:intervention_detail", args=[item.id]),
|
||||
"url": f"{interv_base_url}/{item.id}/",
|
||||
}
|
||||
})
|
||||
|
||||
# 2. Observations liées à un contrat autorisé (via leur intervention)
|
||||
obs = Observation.objects.filter(
|
||||
# 2. Observations liées à un contrat autorisé (projection EPSG:4326 PostGIS)
|
||||
obs = (
|
||||
Observation.objects.filter(
|
||||
intervention__contract__in=allowed_contracts,
|
||||
created_at__gte=cutoff
|
||||
).exclude(geom__isnull=True).select_related('thematic')
|
||||
)
|
||||
.exclude(geom__isnull=True)
|
||||
.select_related('thematic')
|
||||
.annotate(geom_4326=Transform(Centroid('geom'), 4326))
|
||||
.only(
|
||||
'id', 'code', 'description', 'status', 'address', 'created_at',
|
||||
'thematic__id', 'thematic__name', 'thematic__code'
|
||||
)
|
||||
)
|
||||
|
||||
for item in obs:
|
||||
geom_wgs = item.geom.clone()
|
||||
geom_wgs.transform(4326)
|
||||
pt = getattr(item, 'geom_4326', None)
|
||||
if not pt:
|
||||
continue
|
||||
t_key = _normalize_thematic_key(item.thematic, f"{item.description or ''}")
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
||||
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||
},
|
||||
"properties": {
|
||||
"id": f"obs-{item.id}",
|
||||
|
|
@ -1292,30 +1333,40 @@ def activity_geojson(request):
|
|||
"code": item.code or f"OBS-{item.id}",
|
||||
"title": item.description[:60] if item.description else "-",
|
||||
"status": item.status,
|
||||
"status_label": item.get_status_display() if hasattr(item, 'get_status_display') else item.status,
|
||||
"status_label": obs_status_dict.get(item.status, item.status),
|
||||
"address": item.address or "",
|
||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at else "",
|
||||
"url": reverse("observations:observation_detail", args=[item.id]),
|
||||
"url": f"{obs_base_url}/{item.id}/",
|
||||
}
|
||||
})
|
||||
|
||||
# 3. Inspections liées à un contrat autorisé
|
||||
insps = Inspection.objects.filter(
|
||||
# 3. Inspections liées à un contrat autorisé (projection EPSG:4326 PostGIS)
|
||||
insps = (
|
||||
Inspection.objects.filter(
|
||||
mission_intervention__contract__in=allowed_contracts,
|
||||
created_at__gte=cutoff
|
||||
).select_related('mission_intervention', 'mission_intervention__thematic')
|
||||
)
|
||||
.exclude(mission_intervention__geom__isnull=True)
|
||||
.select_related('mission_intervention', 'mission_intervention__thematic')
|
||||
.annotate(geom_4326=Transform(Centroid('mission_intervention__geom'), 4326))
|
||||
.only(
|
||||
'id', 'nature', 'notes', 'status', 'created_at',
|
||||
'mission_intervention__id', 'mission_intervention__address',
|
||||
'mission_intervention__thematic__id', 'mission_intervention__thematic__name', 'mission_intervention__thematic__code'
|
||||
)
|
||||
)
|
||||
|
||||
for item in insps:
|
||||
if item.mission_intervention and item.mission_intervention.geom:
|
||||
geom_wgs = item.mission_intervention.geom.centroid.clone()
|
||||
geom_wgs.transform(4326)
|
||||
pt = getattr(item, 'geom_4326', None)
|
||||
if not pt:
|
||||
continue
|
||||
thematic_obj = item.mission_intervention.thematic if (item.mission_intervention and item.mission_intervention.thematic) else None
|
||||
t_key = _normalize_thematic_key(thematic_obj, f"{item.nature or ''} {item.notes or ''}")
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
||||
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||
},
|
||||
"properties": {
|
||||
"id": f"insp-{item.id}",
|
||||
|
|
@ -1327,32 +1378,38 @@ def activity_geojson(request):
|
|||
"code": f"INSP-{item.id:04d}",
|
||||
"title": item.nature or "Inspection",
|
||||
"status": item.status,
|
||||
"status_label": item.get_status_display() if hasattr(item, 'get_status_display') else item.status,
|
||||
"address": item.mission_intervention.address or "",
|
||||
"status_label": insp_status_dict.get(item.status, item.status),
|
||||
"address": item.mission_intervention.address if item.mission_intervention else "",
|
||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at else "",
|
||||
"url": reverse("inspections:inspections_list") + f"?tab=all&inspection_id={item.id}#inspectionDetailModal{item.id}",
|
||||
"url": f"{insp_list_url}?tab=all&inspection_id={item.id}#inspectionDetailModal{item.id}",
|
||||
}
|
||||
})
|
||||
|
||||
# 4. Contrôles liés à un contrat autorisé
|
||||
ctrls = Control.objects.filter(
|
||||
# 4. Contrôles liés à un contrat autorisé (avec prefetch_related pour supprimer les requêtes N+1)
|
||||
ctrls = (
|
||||
Control.objects.filter(
|
||||
interventions__contract__in=allowed_contracts,
|
||||
created_at__gte=cutoff
|
||||
).distinct().exclude(geom__isnull=True)
|
||||
)
|
||||
.distinct()
|
||||
.exclude(geom__isnull=True)
|
||||
.prefetch_related('interventions__thematic')
|
||||
.annotate(geom_4326=Transform(Centroid('geom'), 4326))
|
||||
.only('id', 'control_code', 'description', 'status', 'location', 'created_at')
|
||||
)
|
||||
|
||||
for item in ctrls:
|
||||
geom_wgs = item.geom.clone()
|
||||
geom_wgs.transform(4326)
|
||||
thematic_obj = None
|
||||
first_interv = item.interventions.select_related('thematic').first()
|
||||
if first_interv and first_interv.thematic:
|
||||
thematic_obj = first_interv.thematic
|
||||
pt = getattr(item, 'geom_4326', None)
|
||||
if not pt:
|
||||
continue
|
||||
first_interv = next(iter(item.interventions.all()), None)
|
||||
thematic_obj = first_interv.thematic if (first_interv and first_interv.thematic) else None
|
||||
t_key = _normalize_thematic_key(thematic_obj, f"{item.description or ''} {item.control_code or ''}")
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
||||
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||
},
|
||||
"properties": {
|
||||
"id": f"ctrl-{item.id}",
|
||||
|
|
@ -1364,14 +1421,16 @@ def activity_geojson(request):
|
|||
"code": item.control_code or f"CTRL-{item.id}",
|
||||
"title": item.description[:60] if item.description else "-",
|
||||
"status": item.status,
|
||||
"status_label": item.get_status_display() if hasattr(item, 'get_status_display') else item.status,
|
||||
"status_label": ctrl_status_dict.get(item.status, item.status),
|
||||
"address": item.location or "",
|
||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at else "",
|
||||
"url": reverse("controls:controls_detail", args=[item.id]),
|
||||
"url": f"{ctrl_base_url}/{item.id}/",
|
||||
}
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
result_data = {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
})
|
||||
}
|
||||
cache.set(cache_key, result_data, timeout=120)
|
||||
return JsonResponse(result_data)
|
||||
|
|
|
|||
Loading…
Reference in a new issue