feat: add shared MapLibre GL utilities (journey line, markers)
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
/* Shared MapLibre GL utilities — loaded by map.html.twig, dailies.html.twig, home.html.twig */
|
||||||
|
(function (global) {
|
||||||
|
var ACCENT = '#2A8C73';
|
||||||
|
var ACCENT_DIM = '#155244';
|
||||||
|
var MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
|
||||||
|
|
||||||
|
/* Build a GeoJSON LineString feature */
|
||||||
|
function lineFeature(coords) {
|
||||||
|
return { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Progressively draw the journey line using a requestAnimationFrame loop.
|
||||||
|
* coords: [[lng, lat], ...] in chronological order.
|
||||||
|
* sourceId: the MapLibre source id to update each frame.
|
||||||
|
*/
|
||||||
|
function animateJourneyLine(map, coords, sourceId) {
|
||||||
|
if (coords.length < 2) return;
|
||||||
|
|
||||||
|
/* Cumulative Euclidean distance between waypoints */
|
||||||
|
var segDist = [0];
|
||||||
|
for (var i = 1; i < coords.length; i++) {
|
||||||
|
var dx = coords[i][0] - coords[i - 1][0];
|
||||||
|
var dy = coords[i][1] - coords[i - 1][1];
|
||||||
|
segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy));
|
||||||
|
}
|
||||||
|
var totalDist = segDist[segDist.length - 1];
|
||||||
|
var DURATION = 5000;
|
||||||
|
var startTime = performance.now();
|
||||||
|
|
||||||
|
function frame(now) {
|
||||||
|
if (!map.getSource(sourceId)) return; /* map was removed */
|
||||||
|
var t = Math.min((now - startTime) / DURATION, 1);
|
||||||
|
var eased = 1 - Math.pow(1 - t, 3); /* ease-out cubic */
|
||||||
|
var target = eased * totalDist;
|
||||||
|
|
||||||
|
var animCoords = [coords[0]];
|
||||||
|
for (var j = 1; j < coords.length; j++) {
|
||||||
|
if (segDist[j] <= target) {
|
||||||
|
animCoords.push(coords[j]);
|
||||||
|
} else {
|
||||||
|
var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]);
|
||||||
|
animCoords.push([
|
||||||
|
coords[j - 1][0] + (coords[j][0] - coords[j - 1][0]) * frac,
|
||||||
|
coords[j - 1][1] + (coords[j][1] - coords[j - 1][1]) * frac
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.getSource(sourceId).setData(lineFeature(animCoords));
|
||||||
|
if (t < 1) requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add a journey line source + two layers (glow + main) to a loaded map,
|
||||||
|
* then animate or draw instantly based on prefers-reduced-motion.
|
||||||
|
*/
|
||||||
|
function addJourneyLine(map, coords, sourceId) {
|
||||||
|
if (coords.length < 2) return;
|
||||||
|
|
||||||
|
map.addSource(sourceId, { type: 'geojson', data: lineFeature([coords[0]]) });
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: sourceId + '-glow', type: 'line', source: sourceId,
|
||||||
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||||
|
paint: { 'line-color': ACCENT, 'line-width': 6, 'line-opacity': 0.18 }
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: sourceId + '-line', type: 'line', source: sourceId,
|
||||||
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||||
|
paint: { 'line-color': ACCENT, 'line-width': 2.5, 'line-opacity': 0.85 }
|
||||||
|
});
|
||||||
|
|
||||||
|
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (reducedMotion) {
|
||||||
|
map.getSource(sourceId).setData(lineFeature(coords));
|
||||||
|
} else {
|
||||||
|
animateJourneyLine(map, coords, sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return a styled <div> element for a map marker dot.
|
||||||
|
* isLatest: make it larger with a teal ring.
|
||||||
|
*/
|
||||||
|
function createDotMarker(isLatest) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
var size = isLatest ? 18 : 12;
|
||||||
|
var bg = isLatest ? ACCENT_DIM : ACCENT;
|
||||||
|
var ring = isLatest ? ',0 0 0 4px rgba(42,140,115,0.25)' : '';
|
||||||
|
el.style.cssText = [
|
||||||
|
'width:' + size + 'px',
|
||||||
|
'height:' + size + 'px',
|
||||||
|
'background:' + bg,
|
||||||
|
'border:2px solid #fff',
|
||||||
|
'border-radius:50%',
|
||||||
|
'box-shadow:0 1px 4px rgba(0,0,0,0.4)' + ring,
|
||||||
|
'cursor:pointer'
|
||||||
|
].join(';');
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.MapUtils = { MAP_STYLE: MAP_STYLE, ACCENT: ACCENT, addJourneyLine: addJourneyLine, createDotMarker: createDotMarker };
|
||||||
|
})(window);
|
||||||
Reference in New Issue
Block a user