HTML运动轨迹分享 mapbox等高线3D地图

程 涛 发布于 13 天前 86 次阅读


这是小的2025年暑假去洛阳老君山游玩的轨迹,因为看了好多户外徒步的视频感觉在3D地图上显示运动轨迹。感觉这个效果好酷所以也想自己搞一个^_^

老君山夜爬GPX轨迹(Mapbox版)- 可滚动版

准备工作

  • 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 } // 隐藏图例,更简洁
            }
        }
    });
}
此作者没有提供个人介绍。
最后更新于 2026-01-21