diff --git a/loko/assets/models/core.py b/loko/assets/models/core.py
index 7714df2..99b887d 100644
--- a/loko/assets/models/core.py
+++ b/loko/assets/models/core.py
@@ -298,6 +298,18 @@ class AbstractAsset(models.Model):
related_query_name='%(app_label)s_%(class)s'
)
+ @property
+ def is_compliant(self):
+ return self.inspection_status == 'compliant'
+
+ @is_compliant.setter
+ def is_compliant(self, value):
+ if value is True:
+ self.inspection_status = 'compliant'
+ elif value is False:
+ self.inspection_status = 'non_compliant'
+
+
def get_replacement_chain(self):
diff --git a/loko/assets/static/assets/inspection_map.js b/loko/assets/static/assets/inspection_map.js
index 1e61c50..02c4a3d 100644
--- a/loko/assets/static/assets/inspection_map.js
+++ b/loko/assets/static/assets/inspection_map.js
@@ -96,7 +96,103 @@
map.addControl(searchCtrl, 'top-left');
}
+ // Chargeur dynamique d'images SVG / PNG de signalisation routière pour MapLibre
+ function initRoadSignImagesLoader(mapInstance) {
+ if (!mapInstance.hasImage('road-sign-default')) {
+ const canvas = document.createElement('canvas');
+ canvas.width = 48;
+ canvas.height = 48;
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = '#0d6efd';
+ ctx.beginPath();
+ ctx.arc(24, 24, 20, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.strokeStyle = '#ffffff';
+ ctx.lineWidth = 3;
+ ctx.stroke();
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ mapInstance.addImage('road-sign-default', {
+ width: canvas.width,
+ height: canvas.height,
+ data: imgData.data
+ });
+ }
+
+ const loadingRoadSignImages = new Set();
+ mapInstance.on('styleimagemissing', function(e) {
+ const missingId = e.id;
+ if (!missingId || !missingId.startsWith('road-sign-') || missingId === 'road-sign-default') return;
+ if (mapInstance.hasImage(missingId) || loadingRoadSignImages.has(missingId)) return;
+
+ const signCode = missingId.replace('road-sign-', '').replace(/-x12$/, '');
+ const baseUrl = "/static/assets/road_signs/2025/";
+ const targetSize = 64;
+
+ const greyCanvas = document.createElement('canvas');
+ greyCanvas.width = targetSize;
+ greyCanvas.height = targetSize;
+ const greyCtx = greyCanvas.getContext('2d');
+ greyCtx.fillStyle = '#ced4da';
+ greyCtx.fillRect(8, 8, targetSize - 16, targetSize - 16);
+ const greyData = greyCtx.getImageData(0, 0, targetSize, targetSize).data;
+ if (!mapInstance.hasImage(missingId)) {
+ mapInstance.addImage(missingId, { width: targetSize, height: targetSize, data: greyData });
+ }
+
+ loadingRoadSignImages.add(missingId);
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+
+ const variations = [
+ signCode.toUpperCase(),
+ signCode.charAt(0).toUpperCase() + signCode.slice(1),
+ signCode.toLowerCase()
+ ];
+ let attempt = 0;
+
+ function tryNext() {
+ if (attempt >= variations.length * 2) {
+ loadingRoadSignImages.delete(missingId);
+ return;
+ }
+ const v = variations[Math.floor(attempt / 2)];
+ const ext = (attempt % 2 === 0) ? 'svg' : 'png';
+ img.src = `${baseUrl}${v}.${ext}`;
+ attempt++;
+ }
+
+ img.onload = function() {
+ try {
+ const aspect = img.height / img.width;
+ const w = targetSize;
+ const h = Math.round(targetSize * aspect);
+ const c = document.createElement('canvas');
+ c.width = w;
+ c.height = h;
+ const ctx = c.getContext('2d');
+ ctx.drawImage(img, 0, 0, w, h);
+ const d = ctx.getImageData(0, 0, w, h);
+ if (mapInstance.hasImage(missingId)) {
+ mapInstance.removeImage(missingId);
+ }
+ mapInstance.addImage(missingId, { width: w, height: h, data: d.data });
+ mapInstance.triggerRepaint();
+ } catch(err) {
+ console.warn('Error loading sign sprite:', missingId, err);
+ } finally {
+ loadingRoadSignImages.delete(missingId);
+ }
+ };
+ img.onerror = function() {
+ tryNext();
+ };
+ tryNext();
+ });
+ }
+
map.on('load', function() {
+ initRoadSignImagesLoader(map);
+
// Source GeoJSON pour les assets et localisations
map.addSource('assets-source', {
type: 'geojson',
@@ -219,7 +315,7 @@
}
});
- // 6. Couche Points d'Assets généraux (Poteaux, Avaloirs, Regards, etc.)
+ // 6. Couche Points d'Assets généraux & Halo d'état de signalisation
map.addLayer({
id: 'assets-points',
type: 'circle',
@@ -231,21 +327,85 @@
['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false]
],
paint: {
- 'circle-radius': 7,
- 'circle-color': [
- 'match', ['coalesce', ['get', 'structure_type'], ''],
- ['massif drainant', 'tranchee drainante', 'oed'], '#8d6e63',
- ['jardin de pluie', 'noue', 'evc'], '#009688',
- ['bassin orage', 'citerne', 'ta'], '#0277bd',
- '#0288d1'
+ 'circle-radius': [
+ 'case',
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'], 13,
+ 7
],
- 'circle-stroke-width': 2,
- 'circle-stroke-color': '#ffffff'
+ 'circle-color': [
+ 'case',
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'],
+ [
+ 'case',
+ ['==', ['coalesce', ['get', 'is_compliant'], true], false], '#dc3545',
+ ['==', ['coalesce', ['get', 'status'], ''], 'inactive'], '#343a40',
+ '#198754'
+ ],
+ [
+ 'match', ['coalesce', ['get', 'structure_type'], ''],
+ ['massif drainant', 'tranchee drainante', 'oed'], '#8d6e63',
+ ['jardin de pluie', 'noue', 'evc'], '#009688',
+ ['bassin orage', 'citerne', 'ta'], '#0277bd',
+ '#0288d1'
+ ]
+ ],
+ 'circle-opacity': [
+ 'case',
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'], 0.25,
+ 1.0
+ ],
+ 'circle-stroke-width': [
+ 'case',
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'], 2,
+ 2
+ ],
+ 'circle-stroke-color': [
+ 'case',
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'],
+ [
+ 'case',
+ ['==', ['coalesce', ['get', 'is_compliant'], true], false], '#dc3545',
+ ['==', ['coalesce', ['get', 'status'], ''], 'inactive'], '#343a40',
+ '#198754'
+ ],
+ '#ffffff'
+ ]
+ }
+ });
+
+ // 7. Couche Symboles Signalisation Routière (Vrais pictogrammes SVGs)
+ map.addLayer({
+ id: 'assets-sign-symbols',
+ type: 'symbol',
+ source: 'assets-source',
+ filter: [
+ 'all',
+ ['!', ['coalesce', ['get', 'is_location'], false]],
+ ['==', ['coalesce', ['get', 'model_name'], ''], 'signpanel'],
+ ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false]
+ ],
+ layout: {
+ 'icon-image': [
+ 'coalesce',
+ ['get', 'panel_icon_image'],
+ 'road-sign-default'
+ ],
+ 'icon-size': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 13, 0.35,
+ 16, 0.65,
+ 19, 1.0,
+ 21, 1.35
+ ],
+ 'icon-allow-overlap': true,
+ 'icon-ignore-placement': true
}
});
const interactiveAssetLayers = [
- 'assets-points', 'parking-points', 'assets-lines', 'assets-polygons', 'parking-polygons', 'locations-polygons'
+ 'assets-sign-symbols', 'assets-points', 'parking-points', 'assets-lines', 'assets-polygons', 'parking-polygons', 'locations-polygons'
];
// Clic Priorisé sur un Asset / Localisation
@@ -362,6 +522,28 @@
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
+ let signHeaderHtml = '';
+ if (props.model_name === 'signpanel' || props.panel_type) {
+ const signCode = props.panel_type || props.code || '';
+ const signText = props.panel_text || '';
+ const upperCode = signCode.toUpperCase();
+ const svgSrc = props.svg_url || `/static/assets/road_signs/2025/${upperCode}.svg`;
+ const cleanBadge = (props.is_compliant === false) ? 'Non-conforme' : 'Conforme';
+
+ signHeaderHtml = `
+
+
+

+
+
+
${escapeHtml(signCode)}
+ ${signText ? `` : ''}
+
+ ${cleanBadge}
+
+ `;
+ }
+
let html = `
@@ -369,6 +551,8 @@
${props.inspections_count || inspectionsList.length}
+ ${signHeaderHtml}
+