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 () => {
|
map.on('load', async () => {
|
||||||
// 0. Enregistrer les icônes d'opérations (symboles à l'intérieur des points)
|
// Enregistrer les icônes d'opérations et lancer les requêtes GeoJSON en parallèle (gain réseau immédiat)
|
||||||
await Promise.all(
|
const [maskData, actData] = await Promise.all([
|
||||||
Object.entries(OPERATION_ICONS).map(([key, info]) => registerOperationIcon(map, key, info, 32))
|
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 {
|
try {
|
||||||
const maskResp = await fetch('/common/territory-mask-geojson/');
|
|
||||||
if (maskResp.ok) {
|
|
||||||
const maskData = await maskResp.json();
|
|
||||||
map.addSource('territory-mask', {
|
map.addSource('territory-mask', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: maskData
|
data: maskData
|
||||||
|
|
@ -207,27 +211,19 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
duration: 1000
|
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) {
|
} else if (window.MAP_DEFAULT_EXTENT && Array.isArray(window.MAP_DEFAULT_EXTENT) && window.MAP_DEFAULT_EXTENT.length === 2) {
|
||||||
map.fitBounds(window.MAP_DEFAULT_EXTENT, {
|
map.fitBounds(window.MAP_DEFAULT_EXTENT, {
|
||||||
padding: { top: 35, bottom: 35, left: 35, right: 35 },
|
padding: { top: 35, bottom: 35, left: 35, right: 35 },
|
||||||
duration: 1000
|
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 {
|
try {
|
||||||
const actResp = await fetch('/api/activity-geojson/');
|
|
||||||
if (actResp.ok) {
|
|
||||||
const actData = await actResp.json();
|
|
||||||
map.addSource('activity-data', {
|
map.addSource('activity-data', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: actData
|
data: actData
|
||||||
|
|
|
||||||
|
|
@ -186,5 +186,5 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'interventions/status_colors.js' %}"></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 %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -1218,8 +1218,11 @@ def activity_geojson(request):
|
||||||
"""
|
"""
|
||||||
Renvoie le GeoJSON des actions des 90 derniers jours (interventions, observations, inspections, contrôles)
|
Renvoie le GeoJSON des actions des 90 derniers jours (interventions, observations, inspections, contrôles)
|
||||||
strictement restreintes aux contrats autorisés pour l'utilisateur connecté.
|
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 contracts.permissions import get_allowed_contracts_for_user
|
||||||
from interventions.models import Intervention
|
from interventions.models import Intervention
|
||||||
from observations.models import Observation
|
from observations.models import Observation
|
||||||
|
|
@ -1230,24 +1233,52 @@ def activity_geojson(request):
|
||||||
if not allowed_contracts.exists():
|
if not allowed_contracts.exists():
|
||||||
return JsonResponse({"type": "FeatureCollection", "features": []})
|
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)
|
cutoff = timezone.now() - timedelta(days=90)
|
||||||
features = []
|
features = []
|
||||||
|
|
||||||
# 1. Interventions du contrat
|
# URLs de base précalculées pour éviter d'appeler reverse() des milliers de fois
|
||||||
intervs = Intervention.objects.filter(
|
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,
|
contract__in=allowed_contracts,
|
||||||
creation_time__gte=cutoff
|
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:
|
for item in intervs:
|
||||||
geom_wgs = item.geom.centroid.clone()
|
pt = getattr(item, 'geom_4326', None)
|
||||||
geom_wgs.transform(4326)
|
if not pt:
|
||||||
|
continue
|
||||||
t_key = _normalize_thematic_key(item.thematic, f"{item.title} {item.description or ''}")
|
t_key = _normalize_thematic_key(item.thematic, f"{item.title} {item.description or ''}")
|
||||||
features.append({
|
features.append({
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": f"int-{item.id}",
|
"id": f"int-{item.id}",
|
||||||
|
|
@ -1259,28 +1290,38 @@ def activity_geojson(request):
|
||||||
"code": item.code or f"INT-{item.id}",
|
"code": item.code or f"INT-{item.id}",
|
||||||
"title": item.title or "-",
|
"title": item.title or "-",
|
||||||
"status": item.status,
|
"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 "",
|
"address": item.address or "",
|
||||||
"date": item.creation_time.strftime("%d/%m/%Y") if item.creation_time else "",
|
"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)
|
# 2. Observations liées à un contrat autorisé (projection EPSG:4326 PostGIS)
|
||||||
obs = Observation.objects.filter(
|
obs = (
|
||||||
|
Observation.objects.filter(
|
||||||
intervention__contract__in=allowed_contracts,
|
intervention__contract__in=allowed_contracts,
|
||||||
created_at__gte=cutoff
|
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:
|
for item in obs:
|
||||||
geom_wgs = item.geom.clone()
|
pt = getattr(item, 'geom_4326', None)
|
||||||
geom_wgs.transform(4326)
|
if not pt:
|
||||||
|
continue
|
||||||
t_key = _normalize_thematic_key(item.thematic, f"{item.description or ''}")
|
t_key = _normalize_thematic_key(item.thematic, f"{item.description or ''}")
|
||||||
features.append({
|
features.append({
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": f"obs-{item.id}",
|
"id": f"obs-{item.id}",
|
||||||
|
|
@ -1292,30 +1333,40 @@ def activity_geojson(request):
|
||||||
"code": item.code or f"OBS-{item.id}",
|
"code": item.code or f"OBS-{item.id}",
|
||||||
"title": item.description[:60] if item.description else "-",
|
"title": item.description[:60] if item.description else "-",
|
||||||
"status": item.status,
|
"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 "",
|
"address": item.address or "",
|
||||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at else "",
|
"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é
|
# 3. Inspections liées à un contrat autorisé (projection EPSG:4326 PostGIS)
|
||||||
insps = Inspection.objects.filter(
|
insps = (
|
||||||
|
Inspection.objects.filter(
|
||||||
mission_intervention__contract__in=allowed_contracts,
|
mission_intervention__contract__in=allowed_contracts,
|
||||||
created_at__gte=cutoff
|
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:
|
for item in insps:
|
||||||
if item.mission_intervention and item.mission_intervention.geom:
|
pt = getattr(item, 'geom_4326', None)
|
||||||
geom_wgs = item.mission_intervention.geom.centroid.clone()
|
if not pt:
|
||||||
geom_wgs.transform(4326)
|
continue
|
||||||
thematic_obj = item.mission_intervention.thematic if (item.mission_intervention and item.mission_intervention.thematic) else None
|
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 ''}")
|
t_key = _normalize_thematic_key(thematic_obj, f"{item.nature or ''} {item.notes or ''}")
|
||||||
features.append({
|
features.append({
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": f"insp-{item.id}",
|
"id": f"insp-{item.id}",
|
||||||
|
|
@ -1327,32 +1378,38 @@ def activity_geojson(request):
|
||||||
"code": f"INSP-{item.id:04d}",
|
"code": f"INSP-{item.id:04d}",
|
||||||
"title": item.nature or "Inspection",
|
"title": item.nature or "Inspection",
|
||||||
"status": item.status,
|
"status": item.status,
|
||||||
"status_label": item.get_status_display() if hasattr(item, 'get_status_display') else item.status,
|
"status_label": insp_status_dict.get(item.status, item.status),
|
||||||
"address": item.mission_intervention.address or "",
|
"address": item.mission_intervention.address if item.mission_intervention else "",
|
||||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at 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é
|
# 4. Contrôles liés à un contrat autorisé (avec prefetch_related pour supprimer les requêtes N+1)
|
||||||
ctrls = Control.objects.filter(
|
ctrls = (
|
||||||
|
Control.objects.filter(
|
||||||
interventions__contract__in=allowed_contracts,
|
interventions__contract__in=allowed_contracts,
|
||||||
created_at__gte=cutoff
|
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:
|
for item in ctrls:
|
||||||
geom_wgs = item.geom.clone()
|
pt = getattr(item, 'geom_4326', None)
|
||||||
geom_wgs.transform(4326)
|
if not pt:
|
||||||
thematic_obj = None
|
continue
|
||||||
first_interv = item.interventions.select_related('thematic').first()
|
first_interv = next(iter(item.interventions.all()), None)
|
||||||
if first_interv and first_interv.thematic:
|
thematic_obj = first_interv.thematic if (first_interv and first_interv.thematic) else None
|
||||||
thematic_obj = first_interv.thematic
|
|
||||||
t_key = _normalize_thematic_key(thematic_obj, f"{item.description or ''} {item.control_code or ''}")
|
t_key = _normalize_thematic_key(thematic_obj, f"{item.description or ''} {item.control_code or ''}")
|
||||||
features.append({
|
features.append({
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
"coordinates": [round(geom_wgs.x, 6), round(geom_wgs.y, 6)]
|
"coordinates": [round(pt.x, 6), round(pt.y, 6)]
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": f"ctrl-{item.id}",
|
"id": f"ctrl-{item.id}",
|
||||||
|
|
@ -1364,14 +1421,16 @@ def activity_geojson(request):
|
||||||
"code": item.control_code or f"CTRL-{item.id}",
|
"code": item.control_code or f"CTRL-{item.id}",
|
||||||
"title": item.description[:60] if item.description else "-",
|
"title": item.description[:60] if item.description else "-",
|
||||||
"status": item.status,
|
"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 "",
|
"address": item.location or "",
|
||||||
"date": item.created_at.strftime("%d/%m/%Y") if item.created_at else "",
|
"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",
|
"type": "FeatureCollection",
|
||||||
"features": features
|
"features": features
|
||||||
})
|
}
|
||||||
|
cache.set(cache_key, result_data, timeout=120)
|
||||||
|
return JsonResponse(result_data)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue