这是小的2025年暑假去洛阳老君山游玩的轨迹,因为看了好多户外徒步的视频感觉在3D地图上显示运动轨迹。感觉这个效果好酷所以也想自己搞一个^_^
准备工作
- Mapbox 账号,点击「Create a token」生成一个 Token(默认权限即可)。
- GPX 轨迹文件
HTML代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 提取出来的GPX配置项,只需要改这里的content值即可 -->
<meta name="gpx-url" content="轨迹文件路径">
<title>老君山夜爬GPX轨迹(Mapbox版)- 全屏自适应版</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* 重置默认样式,消除边距和滚动条 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden; /* 禁止页面滚动 */
font-family: sans-serif;
}
/* 地图容器:占满屏幕高度减去海拔表高度,自适应所有屏幕 */
#map {
width: 100%; /* 视口宽度100% */
height: calc(800px - 200px); /* 视口高度100% - 海拔表高度 */
}
/* 标记点弹窗自适应 */
.marker-popup {
font-size: 14px;
padding: 5px 0;
max-width: 200px; /* 小屏幕弹窗不超宽 */
}
.marker-popup h3 {
margin: 0 0 5px 0;
font-size: 16px;
color: #333;
}
.marker-popup p {
margin: 2px 0;
color: #666;
font-size: 13px;
}
/* 海拔剖面图表容器:自适应,小屏幕高度自动调整 */
#elevation-chart-container {
width: 100%;
height: 200px;
padding: 10px;
border-top: 1px solid #eee;
/* 小屏幕下海拔表高度自适应 */
@media (max-width: 768px) {
height: 180px;
}
@media (max-width: 480px) {
height: 150px;
}
}
</style>
</head>
<body>
<div id="map"></div>
<div id="elevation-chart-container">
<canvas id="elevation-chart"></canvas>
</div>
<!-- 引入外部的核心JS文件 -->
<script src="/js/map.js"></script>
</body>
</html>
JS部分
// map-gpx.js - GPX轨迹展示核心逻辑
// mapbox密钥 Token
mapboxgl.accessToken = "API";
// 从HTML的meta标签中读取GPX地址(核心修改点)
const GPX_META_TAG = document.querySelector('meta[name="gpx-url"]');
// 容错处理:如果没找到meta标签,给出默认值或提示
const GPX_URL = GPX_META_TAG ? GPX_META_TAG.content : "";
// 全局变量
const map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/outdoors-v12",
pitch: 45,
bearing: 0,
antialias: true
});
let markerList = [];
let elevationData = [];
let elevationChart = null;
// 地图加载完成后自动加载轨迹
map.on("load", async () => {
// 添加三维地形
map.addSource('mapbox-terrain', {
type: 'raster-dem',
url: 'mapbox://mapbox.terrain-rgb',
tileSize: 512,
maxzoom: 14
});
map.setTerrain({
source: 'mapbox-terrain',
exaggeration: 1.5
});
map.setLight({
anchor: 'map',
color: 'white',
intensity: 1.2,
position: [120, 60, 80]
});
// 容错:如果GPX地址为空,提示用户
if (!GPX_URL) {
alert("未配置GPX文件地址,请在HTML的meta[name='gpx-url']标签中设置!");
return;
}
// 加载GPX轨迹
await loadGPXFromURL(GPX_URL);
// 监听窗口大小变化,自动适配地图尺寸
window.addEventListener('resize', () => {
map.resize(); // Mapbox内置的自适应方法
// 海拔表也重新渲染,适配新尺寸
if (elevationChart) renderElevationChart();
});
});
// 地图加载失败提示
map.on("error", (err) => {
console.error("地图初始化失败:", err);
alert("地图加载失败,请检查网络或Token是否有效");
});
/**
* 加载并解析远程GPX文件
*/
async function loadGPXFromURL(url) {
try {
clearAllMarkers();
const response = await fetch(url);
if (!response.ok) {
throw new Error(`GPX文件加载失败,状态码:${response.status}`);
}
const gpxText = await response.text();
const parser = new DOMParser();
const gpxXml = parser.parseFromString(gpxText, "text/xml");
const trkpts = gpxXml.getElementsByTagName("trkpt");
if (trkpts.length === 0) throw new Error("GPX文件中未找到轨迹点(trkpt标签)");
let totalDistance = 0;
let prevLon = null;
let prevLat = null;
const coordinates = [];
elevationData = [];
// 解析每个轨迹点的经纬度、海拔、距离
Array.from(trkpts).forEach((pt, index) => {
const lon = parseFloat(pt.getAttribute("lon"));
const lat = parseFloat(pt.getAttribute("lat"));
if (isNaN(lon) || isNaN(lat)) return;
coordinates.push([lon, lat]);
// 提取海拔(兼容无ele标签的情况)
const eleNode = pt.getElementsByTagName("ele")[0];
const elevation = eleNode ? parseFloat(eleNode.textContent) : 0;
// 计算累计距离(米)
if (index > 0) {
totalDistance += calculateDistance(prevLon, prevLat, lon, lat);
}
prevLon = lon;
prevLat = lat;
// 存储距离(转km)和海拔(m)
elevationData.push({
distance: totalDistance / 1000,
elevation: elevation
});
});
// 移除旧轨迹,添加新轨迹
if (map.getSource("gpx-track")) map.removeSource("gpx-track");
if (map.getLayer("gpx-track-layer")) map.removeLayer("gpx-track-layer");
map.addSource("gpx-track", {
type: "geojson",
data: {
type: "Feature",
geometry: { type: "LineString", coordinates: coordinates }
}
});
// 轨迹线样式(带前进箭头)
map.addLayer({
id: "gpx-track-layer",
type: "line",
source: "gpx-track",
layout: {
"line-join": "round",
"line-cap": "round",
"line-arrows": "forward",
"line-arrow-scale": 1.2,
"line-arrow-spacing": 20
},
paint: {
"line-color": "#ff4500",
"line-width": 3,
"line-opacity": 0.8,
"line-arrow-color": "#000000",
"line-arrow-width": 2
}
});
// 自动定位到轨迹范围(自适应视野)
const bounds = coordinates.reduce((b, coord) => b.extend(coord), new mapboxgl.LngLatBounds());
map.fitBounds(bounds, {
padding: 40, // 小屏幕减少内边距,适配更好
maxZoom: 18,
pitch: 45,
duration: 800 // 平滑过渡动画
});
// 渲染标记点和起点终点
const markerCount = renderWaypoints(gpxXml);
if (markerCount === 0) addStartEndMarkers(coordinates);
// 渲染海拔剖面图表
renderElevationChart();
console.log(`轨迹加载成功:${coordinates.length}个轨迹点 | ${markerCount}个标记点`);
} catch (err) {
const errMsg = `轨迹加载失败:${err.message}`;
console.error(errMsg);
alert(errMsg);
// 即使出错也渲染海拔表,避免空白
renderElevationChart();
}
}
/**
* 渲染GPX中的标记点(wpt标签)
*/
function renderWaypoints(gpxXml) {
const wpts = gpxXml.getElementsByTagName("wpt");
if (wpts.length === 0) return 0;
Array.from(wpts).forEach(wpt => {
const lon = parseFloat(wpt.getAttribute("lon"));
const lat = parseFloat(wpt.getAttribute("lat"));
if (isNaN(lon) || isNaN(lat)) return;
const name = wpt.getElementsByTagName("name")[0]?.textContent || "未命名标记";
const desc = wpt.getElementsByTagName("desc")[0]?.textContent || "无描述";
const type = wpt.getElementsByTagName("type")[0]?.textContent || "普通标记";
// 创建标记点
const marker = new mapboxgl.Marker({
color: getMarkerColor(type),
scale: window.innerWidth < 480 ? 0.7 : 0.8 // 小屏幕缩小标记点
})
.setLngLat([lon, lat])
.setPopup(new mapboxgl.Popup({ offset: 20 })
.setHTML(`
<div class="marker-popup">
<h3>${name}</h3>
<p><strong>类型:</strong>${type}</p>
<p><strong>坐标:</strong>${lat.toFixed(6)}, ${lon.toFixed(6)}</p>
<p><strong>描述:</strong>${desc}</p>
</div>
`))
.addTo(map);
markerList.push(marker);
});
return wpts.length;
}
/**
* 添加起点和终点标记
*/
function addStartEndMarkers(coordinates) {
if (coordinates.length < 2) return;
// 起点标记(绿色,小屏幕缩小)
const startMarker = new mapboxgl.Marker({
color: "#28a745",
scale: window.innerWidth < 480 ? 0.8 : 1.0
})
.setLngLat(coordinates[0])
.setPopup(new mapboxgl.Popup({ offset: 22 })
.setHTML(`
<div class="marker-popup">
<h3>起点</h3>
<p>${coordinates[0][1].toFixed(6)}, ${coordinates[0][0].toFixed(6)}</p>
</div>
`))
.addTo(map);
markerList.push(startMarker);
// 终点标记(红色,小屏幕缩小)
const endMarker = new mapboxgl.Marker({
color: "#dc3545",
scale: window.innerWidth < 480 ? 0.8 : 1.0
})
.setLngLat(coordinates[coordinates.length-1])
.setPopup(new mapboxgl.Popup({ offset: 22 })
.setHTML(`
<div class="marker-popup">
<h3>终点</h3>
<p>${coordinates[coordinates.length-1][1].toFixed(6)}, ${coordinates[coordinates.length-1][0].toFixed(6)}</p>
</div>
`))
.addTo(map);
markerList.push(endMarker);
}
/**
* 根据标记类型返回对应颜色
*/
function getMarkerColor(type) {
const t = type.toLowerCase();
if (t.includes("起点") || t.includes("start")) return "#28a745";
if (t.includes("终点") || t.includes("end")) return "#dc3545";
if (t.includes("补给") || t.includes("supply")) return "#ffc107";
if (t.includes("休息") || t.includes("rest")) return "#17a2b8";
return "#007bff"; // 默认蓝色
}
/**
* 清除所有标记点和海拔表
*/
function clearAllMarkers() {
markerList.forEach(marker => marker.remove());
markerList = [];
if (elevationChart) {
elevationChart.destroy();
elevationChart = null;
}
elevationData = [];
}
/**
* 计算两个经纬度点之间的距离(米)
*/
function calculateDistance(lon1, lat1, lon2, lat2) {
const R = 6371000; // 地球半径(米)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* 渲染海拔剖面图表
*/
function renderElevationChart() {
const chartBox = document.getElementById("elevation-chart-container");
const ctx = document.getElementById("elevation-chart").getContext("2d");
chartBox.style.display = "block";
// 处理无数据的情况
const labels = elevationData.length > 0
? elevationData.map(i => i.distance.toFixed(1))
: ["0"];
const evls = elevationData.length > 0
? elevationData.map(i => i.elevation)
: [0];
// 销毁旧图表,避免重复渲染
if (elevationChart) {
elevationChart.destroy();
}
// 创建新图表
elevationChart = new Chart(ctx, {
type: "line",
data: {
labels: labels,
datasets: [{
label: "海拔 (m)",
data: evls,
fill: true,
backgroundColor: "rgba(100, 181, 246, 0.3)",
borderColor: "rgba(33, 150, 243, 1)",
borderWidth: 2,
tension: 0.1,
pointRadius: 0, // 隐藏小点,更简洁
pointHoverRadius: 4 // 鼠标悬浮显示小点
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // 禁用固定比例,适配容器尺寸
scales: {
x: {
title: {
display: true,
text: "距离 (km)",
font: { size: window.innerWidth < 480 ? 10 : 12 } // 小屏幕缩小字体
},
grid: { color: "rgba(0, 0, 0, 0.05)" },
ticks: { font: { size: window.innerWidth < 480 ? 9 : 11 } }
},
y: {
title: {
display: true,
text: "海拔 (m)",
font: { size: window.innerWidth < 480 ? 10 : 12 }
},
grid: { color: "rgba(0, 0, 0, 0.05)" },
ticks: { font: { size: window.innerWidth < 480 ? 9 : 11 } }
}
},
interaction: {
intersect: false,
mode: "index" // 鼠标悬浮显示对应点数据
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `海拔: ${context.raw} m,距离: ${context.label} km`;
}
},
bodyFont: { size: window.innerWidth < 480 ? 10 : 12 }
},
legend: { display: false } // 隐藏图例,更简洁
}
}
});
}

Comments NOTHING