[mapbox-gl] 地图上展示运动轨迹、路线导航

3,803 阅读4分钟

我正在参加「掘金·启航计划」

记录一下 mapbox-gl 在地图上如何可视化轨迹。在实现路径导航、汽车运动轨迹以及人物运动轨迹回放的可视化时,可考虑这种方式。

基础路径显示

随便找一个运动网站,截取其某一份运动轨迹如下图:

打开 F12 找到这份轨迹数据保存本地。其轨迹数据如下图所示:

接下来就来实现一个基本的轨迹显示

function initMap() {
  const map = new mapboxgl.Map({
    container: mapContainer.value,
    style: "mapbox://styles/mapbox/light-v10",
    center: [116.390619, 39.924317],
    zoom: 13,

  });

  map.on("style.load", () => {
    // 定位视角
    sourceDatalist.length > 0 &&
      map.flyTo(
        {
          center: sourceDatalist[0],
          zoom: 11.5,
          duration: 1000,
        },
        { moveend: "FLY_END" }
      );
    
    map.addSource("pathSourceDone", {
      type: "geojson",
      lineMetrics: true,
      data: {
        type: "FeatureCollection",
        features: [
          {
            type: "Feature",
            geometry: {
              type: "LineString",
              coordinates: sourceDatalist,
            },
          },
        ],
      },
    });

    map.addLayer({
      id: "pathLayerDone",
      type: "line",
      source: "pathSourceDone",
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-width": 5,
        "line-color": "#009688",
      },
    });
  })
}

打开查看效果:

需要注意,源数据需要处理一下。

// jsonData 是保存的网站源数据
let sourceDatalist = jsonData.map((d) => {
  return [d[1], d[0]];
});

路径轨迹可以动起来

这里我们新建一个数据源,然后定义一个 geojson 用来保存轨迹数据。然后在 requestAnimationFrame 中逐渐添加轨迹点位数据,最后实现轨迹动画。

const geojson = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: {
        type: "LineString",
        coordinates: [],
      },
    },
  ],
};

let counterIndex = 0;

function initMap() {
  const map = new mapboxgl.Map({
    container: mapContainer.value,
    style: "mapbox://styles/mapbox/light-v10",
    center: [116.390619, 39.924317],
    zoom: 13,
  });

  state.map = map

  map.on("style.load", () => {
    sourceDatalist.length > 0 &&
      map.flyTo(
        {
          center: sourceDatalist[0],
          zoom: 11.5,
          duration: 1000,
        },
        { moveend: "FLY_END" }
      );

    map.addSource("pathSource", {
      type: "geojson",
      lineMetrics: true,
      data: geojson,
    });

        // 路径
    map.addLayer({
      id: "pathLayer",
      type: "line",
      source: "pathSource",
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-width": 4,
        "line-color": "#673ab7",
      },
    });

  })
}

function animate() {
  let map = state.map
  if (counterIndex > sourceDatalist.length) {
    return
  }

  geojson.features[0].geometry.coordinates.push(sourceDatalist[counterIndex]);
  map.getSource("pathSource").setData(geojson);
  counterIndex += 1;
}

效果如下:

完善轨迹路径,添加起点、终点、完整路径

取数据第一个点作为起点,最后一个点作为终点。

起点设置颜色为绿色,终点设置为红色。

添加完整路径。

代码如下:

function initMap() {
  const map = new mapboxgl.Map({
    container: mapContainer.value,
    style: "mapbox://styles/mapbox/light-v10",
    center: [116.390619, 39.924317],
    zoom: 13,
  });

  state.map = map

  map.on("style.load", () => {
    sourceDatalist.length > 0 &&
      map.flyTo(
        {
          center: sourceDatalist[0],
          zoom: 12,
          duration: 1000,
        },
        { moveend: "FLY_END" }
      );

    map.addSource("pathSourceDone", {
      type: "geojson",
      lineMetrics: true,
      data: {
        type: "FeatureCollection",
        features: [
          {
            type: "Feature",
            geometry: {
              type: "LineString",
              coordinates: sourceDatalist,
            },
          },
        ],
      },
    });

    // 完整路径
    map.addLayer({
      id: "pathLayerDone",
      type: "line",
      source: "pathSourceDone",
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-width": 5,
        "line-color": "#009688",
      },
    });

    map.addSource("pathSource", {
      type: "geojson",
      lineMetrics: true,
      data: geojson,
    });

    // 动态路径
    map.addLayer({
      id: "pathLayer",
      type: "line",
      source: "pathSource",
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-width": 5,
        "line-color": "blue",
      },
    });

    // 添加起点,终点
    let startEndGeojson = {
      type: "FeatureCollection",
      features: [
        {
          type: "Feature",
          properties: {
            typeColor: 'start'
          },
          geometry: {
            type: "Point",
            coordinates: sourceDatalist[0],
          },
        },
        {
          type: "Feature",
          properties: {
            typeColor: 'end'
          },
          geometry: {
            type: "Point",
            coordinates: sourceDatalist[sourceDatalist.length - 1],
          },
        },
      ],
    }

    map.addSource("startEndSource", {
      type: "geojson",
      data: startEndGeojson,
    });
    // 起点和终点
    map.addLayer({
      id: "startEnd-layer",
      type: "circle",
      source: "startEndSource",
      paint: {
        "circle-radius": 10,
        "circle-color": "circle-color": ["match", ["get", "typeColor"], "start", "green", "end", "red", "#cccccc"]
      },
    });

    animate()

  })
}

效果如下图:

添加小车,让小车沿轨迹运动

所谓小车,就是添加一个小车图标,在 mapbox-gl 中就是添加一个 symbol 图层。

小车的运动轨迹实现原理和动态路径相似,不同的是动态路径是将最新的点添加路径数据列表中,而小车只需要替换更新最新的坐标点位即可。

随便找一个小车图片。最好是能看到车顶的,因为咱们看地图的视角就是俯视图,所以小车图标视角最好也是俯视图。

另外需要注意的是,小车沿轨迹运动,咱们小车的车头角度是需要根据实际路径方向来实时改变的。

这个车头角度其实就是计算当前小车坐标点和下个一个坐标点之间的角度。

这个可以用 turf.js 实现:

turf.js文档可以看这里

代码如下:

const symbolIconRotate = -90	// 图片角度
const symbolIconSize = 0.2		// 图片大小

function initMap() {
  // ...
  map.addSource("iconSource", {
    type: "geojson",
    data: geojsonIcon,
  });

  map.loadImage(bicycleIcon, (err, image) => {
    if (err) throw err;
    map.addImage("bicycle-icon", image);

    // 图标
    map.addLayer({
      id: "bicycle-layer",
      type: "symbol",
      source: "iconSource",
      layout: {
        "icon-image": "bicycle-icon",
        "icon-size": symbolIconSize,
        "icon-rotate": ["get", "bearing"],
        "icon-rotation-alignment": "map",
        "icon-allow-overlap": true,
        "icon-ignore-placement": true,
      },
    });
  });

  animate()
}

function animate() {
  let map = state.map
  if (counterIndex > sourceDatalist.length) {
    return
  }

  let startPoint, endPoint
  if (counterIndex === 0) {
    startPoint = sourceDatalist[counterIndex]
    endPoint = sourceDatalist[counterIndex + 1]
  } else {
    startPoint = sourceDatalist[counterIndex - 1]
    endPoint = sourceDatalist[counterIndex]
  }

  // 计算icon角度
  geojsonIcon.features[0].properties.bearing = turf.bearing(
    turf.point(startPoint),
    turf.point(endPoint),
  ) + symbolIconRotate

  geojsonIcon.features[0].geometry.coordinates = sourceDatalist[counterIndex];
  geojson.features[0].geometry.coordinates.push(sourceDatalist[counterIndex]);
  map.getSource("iconSource").setData(geojsonIcon);
  map.getSource("pathSource").setData(geojson);

  counterIndex += 1;

  requestAnimationFrame(animate)
}

效果如下图:

需要注意的是我这里定义了一个修正角度: symbolIconRotate 。

这个变量值取决于你用的小车图片里小车车头的方向。如果小车车头是正上朝向则 symbolIconRotate = 0。

如果小车车头方向和我这个一样是旋转了90°,则 symbolIconRotate = -90

如果是用下图的小车,则大概角度就是 -45°, 则 symbolIconRotate = 45

参考阅读

juejin.cn/post/691234…

docs.mapbox.com/mapbox-gl-j…

docs.mapbox.com/mapbox-gl-j…