share-image
ESC

博客首屏集成高德地图与 GPX 运动轨迹可视化

作为一名热爱跑步的程序员,我一直想把自己的马拉松经历以一种独特的方式呈现在博客上。普通的文字和静态图片虽然能记录当下,但无法还原那 21.0975 公里的心路历程。

于是,我决定在博客的首屏实现一个动态的运动轨迹可视化效果:以高德地图为画布,实时绘制 GPX 轨迹,并同步展示心率、配速、步频等专业运动数据。

本文将详细拆解这一功能的实现过程。

1. 效果预览

最终实现的效果包含以下核心要素:

  • 全屏沉浸式体验:首屏加载高德地图深色模式,配合打字机效果的 Slogan。
  • 动态轨迹绘制:轨迹线随着时间推移像贪吃蛇一样在地图上延伸。
  • 关键里程碑:自动识别并标注 5km, 10km, 半马, 全马等关键节点。
  • 实时数据面板:右上角悬浮窗实时显示运动时间、距离、当前配速、实时心率和步频。
  • 心率趋势图:集成 Chart.js 动态绘制心率曲线。

2. 技术选型与准备

  • 地图服务:高德地图 JS API v2.0(支持 3D 视图和自定义样式)。
  • 数据源:运动手表(如 COROS, Garmin)导出的 .gpx 文件,我这里使用的是COROS。
  • 图表库:Chart.js(轻量级,适合绘制简单的趋势图)。
  • 框架:Hexo (EJS 模板引擎)。

申请高德地图 Key

首先需要在高德开放平台注册账号并创建 Web 端 (JS API) 应用,获取 Key安全密钥 (Security Code)

3. 核心实现步骤

3.1 页面布局 (Layout)

layout.ejs 中,我们需要一个全屏的容器来放置地图,以及覆盖在上面的数据面板。

<div class="hero-splash" id="hero-splash">
<!-- 地图背景层 -->
<div class="marathon-bg">
<div id="amap-container"></div>
</div>

<!-- 数据悬浮面板 -->
<div class="stats-overlay" id="stats-overlay">
<div class="stats-header">
<div class="stats-title">运动数据</div>
<div class="stats-time" id="stats-time">00:00:00</div>
</div>
<div class="stats-grid">
<!-- 距离、配速、心率、步频等数据项 -->
</div>
<!-- 心率图表容器 -->
<div class="chart-container">
<canvas id="elevation-chart"></canvas>
</div>
</div>

<!-- Slogan 内容 -->
<div class="hero-content">
<span id="typing-text"></span><span class="cursor">|</span>
</div>
</div>

CSS 方面,关键是将 .hero-splash 设置为 fixed 定位并拥有最高的 z-index,确保它覆盖在所有内容之上。

3.2 解析 GPX 数据

GPX 本质上是 XML 格式。我们使用浏览器原生的 DOMParser 来解析它。除了基础的经纬度 (lat, lon),我们还需要提取 <extensions> 中的心率和步频数据。

fetch('/marathon.gpx')
.then(response => response.text())
.then(str => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(str, "text/xml");
const trkpts = xmlDoc.getElementsByTagName("trkpt");

const points = [];
const stats = [];

for (let i = 0; i < trkpts.length; i++) {
const pt = trkpts[i];
// 解析扩展数据 (兼容不同厂商的命名空间)
const extensions = pt.getElementsByTagName("extensions")[0];
let hr = 0, cadence = 0;

if (extensions) {
const hrTag = extensions.getElementsByTagName("gpxdata:hr")[0] || extensions.getElementsByTagName("ns3:hr")[0];
const cadTag = extensions.getElementsByTagName("gpxdata:cadence")[0] || extensions.getElementsByTagName("ns3:cadence")[0];

if (hrTag) hr = parseInt(hrTag.textContent);
// 注意:部分设备记录的是单边步频,需要 * 2 转换为双边步频
if (cadTag) cadence = parseInt(cadTag.textContent) * 2;
}

points.push({
lat: parseFloat(pt.getAttribute("lat")),
lon: parseFloat(pt.getAttribute("lon"))
});

stats.push({
time: new Date(pt.getElementsByTagName("time")[0].textContent).getTime(),
hr: hr,
cadence: cadence
});
}

renderAMapPath(points, stats);
});

3.3 高德地图初始化与轨迹绘制

初始化地图时,为了视觉效果,我们禁用了所有交互(拖拽、缩放),并使用了深色主题。

const map = new AMap.Map('amap-container', {
viewMode: '3D',
zoom: 10,
mapStyle: "amap://styles/dark", // 深色模式
zoomEnable: false,
dragEnable: false,
// ... 其他禁用项
});

绘制动态轨迹的核心思路是:创建一个空的 Polyline,然后在 requestAnimationFrame 循环中不断向其路径数组添加新的点

// 创建动态折线
const polyline = new AMap.Polyline({
path: [],
strokeColor: "#3366FF", // 动态获取主题色
strokeWeight: 6,
lineJoin: 'round',
lineCap: 'round',
zIndex: 50,
});
map.add(polyline);

function animate(timestamp) {
// 计算进度 (0.0 - 1.0)
const progress = Math.min((timestamp - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 4); // EaseOutQuart 缓动

// 根据进度截取路径点
const currentIdx = Math.floor(ease * pathArr.length);
const currentPath = pathArr.slice(0, currentIdx + 1);

// 更新折线
if (currentPath.length > 0) {
polyline.setPath(currentPath);
}

// 同步更新数据面板 (代码略)
updateStats(currentIdx);

if (progress < 1) requestAnimationFrame(animate);
}

3.4 动态心率图表

使用 Chart.js 创建一个折线图。为了性能,我们不需要渲染所有点,而是进行降采样(Sample)。

const ctx = document.getElementById('elevation-chart').getContext('2d');
// 降采样:每 100 个点取一个
const sampleRate = Math.ceil(stats.length / 100);
const chartData = stats.filter((_, i) => i % sampleRate === 0).map(s => s.hr);

new Chart(ctx, {
type: 'line',
data: {
labels: chartLabels,
datasets: [{
data: chartData,
borderColor: themeColor,
backgroundColor: gradient, // 渐变填充
fill: true,
pointRadius: 0, // 隐藏数据点,只显示线
tension: 0.4 // 平滑曲线
}]
},
options: {
animation: false, // 禁用初始动画,因为数据本身在动
scales: { x: { display: false }, y: { display: false } }
}
});

4. 遇到的坑与优化

  1. 高德地图 Polyline 颜色:高德 API 不支持 CSS 变量(如 var(--color))。解决方案是在 JS 中使用 getComputedStyle 动态读取 CSS 变量的十六进制值。
  2. 步频数据偏差:发现显示的步频只有 80-90,远低于正常跑步的 170-180。经排查,GPX 记录的是单边步频 (SPM),展示时乘以 2 即可修复。
  3. CDN 加载慢:Chart.js 默认 CDN 在国内访问较慢,替换为 cdnjs.cloudflare.com 后速度显著提升。
  4. 层级遮挡:地图容器层级较高,导致自定义的“向下滚动”箭头不可见。通过设置箭头 z-index: 20 并添加文字阴影解决了此问题。

5. 结语

通过这次折腾,不仅让博客首页变得“硬核”且充满个人特色,也通过技术手段回顾了自己的跑步历程。每当打开首页,看着那条蜿蜒的轨迹线被点亮,看着心率曲线的起伏,仿佛又回到了赛道上。

“人生是一场马拉松,安全完赛才是最重要的。”

希望这个教程能给同样喜欢运动和编码的你一些灵感。

文章作者:阿文
文章链接: https://www.awen.me/post/3e0ccab.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客