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'
|
||||
|
||||
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 () => {
|
||||
// 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
|
||||
try {
|
||||
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
|
||||
// En mode Heatmap: émergent progressivement lorsque la heatmap s'estompe !
|
||||
// Layer B: Cercles représentant les points avec couleur selon la thématique
|
||||
map.addLayer({
|
||||
id: 'activity-points',
|
||||
type: 'circle',
|
||||
|
|
@ -208,9 +282,10 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
paint: {
|
||||
'circle-radius': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
10, 5,
|
||||
14, 8,
|
||||
18, 14
|
||||
10, 6.5,
|
||||
13, 9.5,
|
||||
15, 12.5,
|
||||
18, 17
|
||||
],
|
||||
'circle-color': [
|
||||
'match',
|
||||
|
|
@ -224,7 +299,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
],
|
||||
'circle-stroke-width': 2,
|
||||
'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': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
13.2, 0,
|
||||
|
|
@ -238,8 +313,48 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
console.error("Could not load activity geojson:", e);
|
||||
}
|
||||
|
|
@ -278,6 +393,14 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
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 {
|
||||
// Mode Points pur
|
||||
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-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();
|
||||
}
|
||||
|
|
@ -302,12 +429,12 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
|
||||
// Clic pour afficher les détails du point
|
||||
map.on('click', function(e) {
|
||||
const radiusInPixels = 12;
|
||||
const radiusInPixels = 14;
|
||||
const features = map.queryRenderedFeatures([
|
||||
[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;
|
||||
|
|
@ -358,6 +485,8 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
|||
|
||||
map.on('mouseenter', 'activity-points', () => { map.getCanvas().style.cursor = 'pointer'; });
|
||||
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() {
|
||||
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>
|
||||
<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;">
|
||||
<span><i class="bi bi-tools text-dark me-0.5"></i> Interv.</span>
|
||||
<span><i class="bi bi-eye-fill text-dark me-0.5"></i> Obs.</span>
|
||||
<span><i class="bi bi-search text-dark me-0.5"></i> Insp.</span>
|
||||
<span><i class="bi bi-clipboard-check text-dark me-0.5"></i> Ctrl.</span>
|
||||
<div class="d-flex flex-wrap gap-2 text-dark" style="font-size: 0.70rem;">
|
||||
<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 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 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 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>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue