feat: add operation type icons to map activity points
This commit is contained in:
parent
ef40e48163
commit
f0fb0df8ec
1 changed files with 143 additions and 14 deletions
|
|
@ -50,7 +50,82 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
|
|
||||||
let currentMode = 'heatmap'; // 'heatmap' or 'points'
|
let currentMode = 'heatmap'; // 'heatmap' or 'points'
|
||||||
|
|
||||||
|
const OPERATION_ICONS = {
|
||||||
|
'intervention': {
|
||||||
|
svg: '<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3c0-.269-.035-.53-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814L1 0Zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708ZM3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242-.529-.026-.287-.445-.445-.287-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026L3 11Z"/>',
|
||||||
|
codepoint: 'f5db'
|
||||||
|
},
|
||||||
|
'observation': {
|
||||||
|
svg: '<path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>',
|
||||||
|
codepoint: 'f33e'
|
||||||
|
},
|
||||||
|
'inspection': {
|
||||||
|
svg: '<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>',
|
||||||
|
codepoint: 'f52a'
|
||||||
|
},
|
||||||
|
'control': {
|
||||||
|
svg: '<path fill-rule="evenodd" d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>',
|
||||||
|
codepoint: 'f28b'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function registerOperationIcon(mapInstance, opKey, iconInfo, size = 32) {
|
||||||
|
const imageName = `op-icon-${opKey}`;
|
||||||
|
if (mapInstance.hasImage(imageName)) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="-2.5 -2.5 21 21" fill="#ffffff">${iconInfo.svg}</svg>`;
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
|
||||||
|
// Ombre douce pour faire ressortir l'icône blanche sur n'importe quel fond thématique
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.45)';
|
||||||
|
ctx.shadowBlur = 2.5;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, size, size);
|
||||||
|
const imgData = ctx.getImageData(0, 0, size, size);
|
||||||
|
if (!mapInstance.hasImage(imageName)) {
|
||||||
|
mapInstance.addImage(imageName, imgData);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
// Fallback dessin via police bootstrap-icons
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
ctx.font = `${Math.round(size * 0.62)}px "bootstrap-icons"`;
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.45)';
|
||||||
|
ctx.shadowBlur = 2.5;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
ctx.fillText(String.fromCodePoint(parseInt(iconInfo.codepoint, 16)), size / 2, size / 2);
|
||||||
|
const imgData = ctx.getImageData(0, 0, size, size);
|
||||||
|
if (!mapInstance.hasImage(imageName)) {
|
||||||
|
mapInstance.addImage(imageName, imgData);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
map.on('load', async () => {
|
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))
|
||||||
|
);
|
||||||
|
|
||||||
// 1. Charger le masque de mise en lumière du territoire communal
|
// 1. Charger le masque de mise en lumière du territoire communal
|
||||||
try {
|
try {
|
||||||
const maskResp = await fetch('/common/territory-mask-geojson/');
|
const maskResp = await fetch('/common/territory-mask-geojson/');
|
||||||
|
|
@ -196,8 +271,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Layer B: Points individuels avec couleur selon la typologie
|
// Layer B: Cercles représentant les points avec couleur selon la thématique
|
||||||
// En mode Heatmap: émergent progressivement lorsque la heatmap s'estompe !
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: 'activity-points',
|
id: 'activity-points',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
|
|
@ -208,9 +282,10 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': [
|
'circle-radius': [
|
||||||
'interpolate', ['linear'], ['zoom'],
|
'interpolate', ['linear'], ['zoom'],
|
||||||
10, 5,
|
10, 6.5,
|
||||||
14, 8,
|
13, 9.5,
|
||||||
18, 14
|
15, 12.5,
|
||||||
|
18, 17
|
||||||
],
|
],
|
||||||
'circle-color': [
|
'circle-color': [
|
||||||
'match',
|
'match',
|
||||||
|
|
@ -224,7 +299,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
],
|
],
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': 2,
|
||||||
'circle-stroke-color': '#ffffff',
|
'circle-stroke-color': '#ffffff',
|
||||||
// Transition d'opacité fluide: invisibles au grand zoom, émergent dès zoom 13.5
|
// Transition d'opacité fluide: invisibles au grand zoom, émergent dès zoom 13.2
|
||||||
'circle-opacity': [
|
'circle-opacity': [
|
||||||
'interpolate', ['linear'], ['zoom'],
|
'interpolate', ['linear'], ['zoom'],
|
||||||
13.2, 0,
|
13.2, 0,
|
||||||
|
|
@ -238,7 +313,47 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
renderLegend();
|
// Layer C: Symboles de type d'opération positionnés à l'intérieur des cercles
|
||||||
|
map.addLayer({
|
||||||
|
id: 'activity-points-icons',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'activity-data',
|
||||||
|
layout: {
|
||||||
|
'visibility': 'visible',
|
||||||
|
'icon-image': [
|
||||||
|
'match',
|
||||||
|
['get', 'type'],
|
||||||
|
'intervention', 'op-icon-intervention',
|
||||||
|
'observation', 'op-icon-observation',
|
||||||
|
'inspection', 'op-icon-inspection',
|
||||||
|
'control', 'op-icon-control',
|
||||||
|
'op-icon-intervention'
|
||||||
|
],
|
||||||
|
'icon-size': [
|
||||||
|
'interpolate', ['linear'], ['zoom'],
|
||||||
|
10, 0.38,
|
||||||
|
13, 0.55,
|
||||||
|
15, 0.72,
|
||||||
|
18, 1.0
|
||||||
|
],
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
'icon-ignore-placement': true,
|
||||||
|
'icon-anchor': 'center'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'icon-opacity': [
|
||||||
|
'interpolate', ['linear'], ['zoom'],
|
||||||
|
13.2, 0,
|
||||||
|
14.8, 1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentMode === 'points') {
|
||||||
|
setMode('points');
|
||||||
|
} else {
|
||||||
|
renderLegend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not load activity geojson:", e);
|
console.error("Could not load activity geojson:", e);
|
||||||
|
|
@ -278,6 +393,14 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
14.8, 1
|
14.8, 1
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (map.getLayer('activity-points-icons')) {
|
||||||
|
map.setLayoutProperty('activity-points-icons', 'visibility', 'visible');
|
||||||
|
map.setPaintProperty('activity-points-icons', 'icon-opacity', [
|
||||||
|
'interpolate', ['linear'], ['zoom'],
|
||||||
|
13.2, 0,
|
||||||
|
14.8, 1
|
||||||
|
]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mode Points pur
|
// Mode Points pur
|
||||||
btnPoints.classList.add('btn-primary', 'active');
|
btnPoints.classList.add('btn-primary', 'active');
|
||||||
|
|
@ -293,6 +416,10 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
map.setPaintProperty('activity-points', 'circle-opacity', 0.95);
|
map.setPaintProperty('activity-points', 'circle-opacity', 0.95);
|
||||||
map.setPaintProperty('activity-points', 'circle-stroke-opacity', 1);
|
map.setPaintProperty('activity-points', 'circle-stroke-opacity', 1);
|
||||||
}
|
}
|
||||||
|
if (map.getLayer('activity-points-icons')) {
|
||||||
|
map.setLayoutProperty('activity-points-icons', 'visibility', 'visible');
|
||||||
|
map.setPaintProperty('activity-points-icons', 'icon-opacity', 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderLegend();
|
renderLegend();
|
||||||
}
|
}
|
||||||
|
|
@ -302,12 +429,12 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
|
|
||||||
// Clic pour afficher les détails du point
|
// Clic pour afficher les détails du point
|
||||||
map.on('click', function(e) {
|
map.on('click', function(e) {
|
||||||
const radiusInPixels = 12;
|
const radiusInPixels = 14;
|
||||||
const features = map.queryRenderedFeatures([
|
const features = map.queryRenderedFeatures([
|
||||||
[e.point.x - radiusInPixels, e.point.y - radiusInPixels],
|
[e.point.x - radiusInPixels, e.point.y - radiusInPixels],
|
||||||
[e.point.x + radiusInPixels, e.point.y + radiusInPixels]
|
[e.point.x + radiusInPixels, e.point.y + radiusInPixels]
|
||||||
], {
|
], {
|
||||||
layers: ['activity-points', 'activity-heatmap']
|
layers: ['activity-points', 'activity-points-icons', 'activity-heatmap']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!features.length) return;
|
if (!features.length) return;
|
||||||
|
|
@ -358,6 +485,8 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
|
|
||||||
map.on('mouseenter', 'activity-points', () => { map.getCanvas().style.cursor = 'pointer'; });
|
map.on('mouseenter', 'activity-points', () => { map.getCanvas().style.cursor = 'pointer'; });
|
||||||
map.on('mouseleave', 'activity-points', () => { map.getCanvas().style.cursor = ''; });
|
map.on('mouseleave', 'activity-points', () => { map.getCanvas().style.cursor = ''; });
|
||||||
|
map.on('mouseenter', 'activity-points-icons', () => { map.getCanvas().style.cursor = 'pointer'; });
|
||||||
|
map.on('mouseleave', 'activity-points-icons', () => { map.getCanvas().style.cursor = ''; });
|
||||||
|
|
||||||
function renderLegend() {
|
function renderLegend() {
|
||||||
const legendContainer = document.getElementById('activity-map-legend') || document.getElementById('map-legend');
|
const legendContainer = document.getElementById('activity-map-legend') || document.getElementById('map-legend');
|
||||||
|
|
@ -383,11 +512,11 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
<div class="d-flex align-items-center gap-1.5"><span style="width:10px;height:10px;border-radius:50%;background:#8b5cf6;display:inline-block;flex-shrink:0;"></span> Ouvrages & Bâtiments</div>
|
<div class="d-flex align-items-center gap-1.5"><span style="width:10px;height:10px;border-radius:50%;background:#8b5cf6;display:inline-block;flex-shrink:0;"></span> Ouvrages & Bâtiments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-semibold mb-1 text-dark border-top pt-1" style="font-size: 0.75rem;">Opérations (symboles)</div>
|
<div class="fw-semibold mb-1 text-dark border-top pt-1" style="font-size: 0.75rem;">Opérations (symboles)</div>
|
||||||
<div class="d-flex flex-wrap gap-2 text-muted" style="font-size: 0.70rem;">
|
<div class="d-flex flex-wrap gap-2 text-dark" style="font-size: 0.70rem;">
|
||||||
<span><i class="bi bi-tools text-dark me-0.5"></i> Interv.</span>
|
<span class="d-inline-flex align-items-center gap-1" title="Intervention"><span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-dark text-white" style="width: 15px; height: 15px; font-size: 8px;"><i class="bi bi-tools"></i></span> Interv.</span>
|
||||||
<span><i class="bi bi-eye-fill text-dark me-0.5"></i> Obs.</span>
|
<span class="d-inline-flex align-items-center gap-1" title="Observation"><span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-dark text-white" style="width: 15px; height: 15px; font-size: 8px;"><i class="bi bi-eye-fill"></i></span> Obs.</span>
|
||||||
<span><i class="bi bi-search text-dark me-0.5"></i> Insp.</span>
|
<span class="d-inline-flex align-items-center gap-1" title="Inspection"><span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-dark text-white" style="width: 15px; height: 15px; font-size: 8px;"><i class="bi bi-search"></i></span> Insp.</span>
|
||||||
<span><i class="bi bi-clipboard-check text-dark me-0.5"></i> Ctrl.</span>
|
<span class="d-inline-flex align-items-center gap-1" title="Contrôle"><span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-dark text-white" style="width: 15px; height: 15px; font-size: 8px;"><i class="bi bi-clipboard-check"></i></span> Ctrl.</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue