/* 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 } }; } /* * Catmull-Rom spline through waypoints → dense interpolated coords. * Produces a smooth curve that passes through every entry dot. * steps: interpolated points per segment (16 is plenty for daily entries). */ function catmullRomSpline(coords, steps) { if (coords.length < 2) return coords; steps = steps || 16; var out = []; for (var i = 0; i < coords.length - 1; i++) { var p0 = coords[Math.max(i - 1, 0)]; var p1 = coords[i]; var p2 = coords[i + 1]; var p3 = coords[Math.min(i + 2, coords.length - 1)]; for (var s = 0; s < steps; s++) { var t = s / steps; var t2 = t * t; var t3 = t2 * t; out.push([ 0.5 * ((2*p1[0]) + (-p0[0]+p2[0])*t + (2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*t2 + (-p0[0]+3*p1[0]-3*p2[0]+p3[0])*t3), 0.5 * ((2*p1[1]) + (-p0[1]+p2[1])*t + (2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*t2 + (-p0[1]+3*p1[1]-3*p2[1]+p3[1])*t3) ]); } } out.push(coords[coords.length - 1]); return out; } /* * Progressively draw the journey line using a requestAnimationFrame loop. * splineCoords: dense interpolated coords from catmullRomSpline(). */ function animateJourneyLine(map, splineCoords, sourceId) { if (splineCoords.length < 2) return; var segDist = [0]; for (var i = 1; i < splineCoords.length; i++) { var dx = splineCoords[i][0] - splineCoords[i - 1][0]; var dy = splineCoords[i][1] - splineCoords[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; var t = Math.min((now - startTime) / DURATION, 1); var eased = 1 - Math.pow(1 - t, 3); var target = eased * totalDist; var animCoords = [splineCoords[0]]; for (var j = 1; j < splineCoords.length; j++) { if (segDist[j] <= target) { animCoords.push(splineCoords[j]); } else { var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]); animCoords.push([ splineCoords[j - 1][0] + (splineCoords[j][0] - splineCoords[j - 1][0]) * frac, splineCoords[j - 1][1] + (splineCoords[j][1] - splineCoords[j - 1][1]) * frac ]); break; } } map.getSource(sourceId).setData(lineFeature(animCoords)); if (t < 1) requestAnimationFrame(frame); } requestAnimationFrame(frame); } /* * Add a journey line to a loaded map — dotted, subordinate style so GPX * tracks read as the primary route where they exist. * coords: [[lng, lat], ...] raw waypoints (daily entry positions). */ function addJourneyLine(map, coords, sourceId) { if (coords.length < 2) return; var splineCoords = catmullRomSpline(coords, 16); map.addSource(sourceId, { type: 'geojson', data: lineFeature([splineCoords[0]]) }); map.addLayer({ id: sourceId + '-line', type: 'line', source: sourceId, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': ACCENT, 'line-width': 2, 'line-opacity': 0.45, 'line-dasharray': [0, 2.5] } }); var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reducedMotion) { map.getSource(sourceId).setData(lineFeature(splineCoords)); } else { animateJourneyLine(map, splineCoords, sourceId); } } /* * Return a styled