From 12c5b2c4a18a6a3b82909b95e32d29f875db9e2d Mon Sep 17 00:00:00 2001 From: Mischa Date: Fri, 19 Jun 2026 21:43:45 +0200 Subject: [PATCH] feat: add shared MapLibre GL utilities (journey line, markers) --- themes/intotheeast/js/maplibre-utils.js | 109 ++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 themes/intotheeast/js/maplibre-utils.js diff --git a/themes/intotheeast/js/maplibre-utils.js b/themes/intotheeast/js/maplibre-utils.js new file mode 100644 index 0000000..2aa501f --- /dev/null +++ b/themes/intotheeast/js/maplibre-utils.js @@ -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
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);