mapbox-gl封装轨迹动画图层


前言

本文参考网上的例子,封装一个 mapbox 的轨迹回放图层,具体做的工作为:

  • 将轨迹回放图层封装为一个类,可以传入自己的图标图片
  • 将常用的方法暴露出来,并做了一些改进

效果

在这里插入图片描述

封装后的代码

import * as turf from '@turf/turf'
const svgXML =
  `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> 
      <path d="M529.6128 512L239.9232 222.4128 384.7168 77.5168 819.2 512 384.7168 946.4832 239.9232 801.5872z" p-id="9085" fill="#ffffff"></path> 
  </svg>
  `
//给图片对象写入base64编码的svg流
const svgBase64 = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgXML)));


export default class RouteReplay {
  /**
   * 
   * @param {*} map mapbox实例对象
   * @param {*} routejson 路径geojson type = lineString
   * @param {*} iconImg 图标img
   */
  constructor(map, routejson, iconImg) {
    this.map = map
    this._json = routejson
    this._img = iconImg
    this._animated = false
    this._counter = 0
    this._steps = 0
    this._newRouteGeoJson = null
    this._timer = null
    this._layerList = ['routeLayer', 'realRouteLayer', 'arrowLayer', 'animatePointLayer']

    // 车辆行进中的路线
    this._realRouteGeoJson = {
      'type': 'FeatureCollection',
      'features': [{
        'type': 'Feature',
        'geometry': {
          'type': 'LineString',
          'coordinates': []
        }
      }]
    }
    // 小车位置点的
    this._animatePointGeoJson = {
      'type': 'FeatureCollection',
      'features': [{
        'type': 'Feature',
        'properties': {},
        'geometry': {
          'type': 'Point',
          'coordinates': []
        }
      }]
    }

    this._init()
  }

  _init() {
    let arrowIcon = new Image(20, 20)
    arrowIcon.src = svgBase64

    arrowIcon.onload = () => {
      //     console.log(this.map)
      this.map.addImage('arrowIcon', arrowIcon)
      this.map.loadImage(this._img, (error, carIcon) => {
        if (error) throw error;
        this.map.addImage('carIcon', carIcon);
        this._animatePointGeoJson.features[0].geometry.coordinates = this._json.features[0].geometry.coordinates[0]
       
        // 小车轨迹点json
        this._newRouteGeoJson = this._resetRoute(this._json.features[0], 1000, 'kilometers')
        // 小车轨迹点json的点数量
        this._steps = this._newRouteGeoJson.geometry.coordinates.length

        this._addRoutelayer() // 添加轨迹线图层
        this._addRealRouteSource() // 添加实时轨迹线图层
        this._addArrowlayer() // 添加箭头图层
        this._addAnimatePointSource() // 添加动态点图层

      })
    }

  }

  _animate() {
    if (this._counter >= this._steps) {
      return
    }
    let startPnt, endPnt
    if (this._counter == 0) { // 开始
      this._realRouteGeoJson.features[0].geometry.coordinates = []
      startPnt = this._newRouteGeoJson.geometry.coordinates[this._counter]
      endPnt = this._newRouteGeoJson.geometry.coordinates[this._counter + 1]
    } else if (this._counter !== 0) {
      startPnt = this._newRouteGeoJson.geometry.coordinates[this._counter - 1]
      endPnt = this._newRouteGeoJson.geometry.coordinates[this._counter]
    }

    // 计算角度,用于小车的指向角度
    this._animatePointGeoJson.features[0].properties.bearing = turf.bearing(
      turf.point(startPnt),
      turf.point(endPnt)
    ) - 90;
    this._animatePointGeoJson.features[0].geometry.coordinates = this._newRouteGeoJson.geometry.coordinates[this._counter];
    this._realRouteGeoJson.features[0].geometry.coordinates.push(this._animatePointGeoJson.features[0].geometry.coordinates)

    // 小车的位置更新
    this.map.getSource('animatePointLayer').setData(this._animatePointGeoJson);
    // 已经走过的轨迹更新
    this.map.getSource('realRouteLayer').setData(this._realRouteGeoJson);
    if (this._animated) {
      this._timer = requestAnimationFrame(() => { this._animate() });
    }
    this._counter++;
  }

  _addRoutelayer() {
    console.log(222)
    this.map.addLayer({
      'id': 'routeLayer',
      'type': 'line',
      'source': {
        'type': 'geojson',
        'lineMetrics': true,
        'data': this._json
      },
      'paint': {
        'line-width': 10,
        'line-opacity': 1,
        'line-color': '#7ec1ff',
      }
    });
  }

  _addRealRouteSource() {
    this.map.addLayer({
      'id': 'realRouteLayer',
      'type': 'line',
      'source': {
        'type': 'geojson',
        'lineMetrics': true,
        'data': this._realRouteGeoJson
      },
      'paint': {
        'line-width': 10,
        'line-opacity': 1,
        'line-color': 'rgba(243,229,11,1)',
      }
    });
  }

  _addArrowlayer() {
    this.map.addLayer({
      'id': 'arrowLayer',
      'type': 'symbol',
      'source': {
        'type': 'geojson',
        'data': this._json //轨迹geojson格式数据
      },
      'layout': {
        'symbol-placement': 'line',
        'symbol-spacing': 50, // 图标间隔,默认为250
        'icon-image': 'arrowIcon', //箭头图标
        'icon-size': 0.5
      }
    });
  }

  // 添加动态点图层--小车
  _addAnimatePointSource() {
    this.map.addLayer({
      'id': 'animatePointLayer',
      'type': 'symbol',
      'source': {
        'type': 'geojson',
        'data': this._animatePointGeoJson
      },
      'layout': {
        'icon-image': 'carIcon',
        'icon-size': 0.5,
        'icon-rotate': ['get', 'bearing'],
        'icon-rotation-alignment': 'map',
        'icon-allow-overlap': true,
        'icon-ignore-placement': true
      }
    });
  }

  _resetRoute(route, nstep, units) {
    const newroute = {
      'type': 'Feature',
      'geometry': {
        'type': 'LineString',
        'coordinates': []
      }
    }
    // 指定点集合的总路长
    const lineDistance = turf.lineDistance(route);

    // 每一段的平均距离
    const nDistance = lineDistance / nstep;
    const length =  this._json.features[0].geometry.coordinates.length;
    for (let i = 0; i < length - 1; i++) {
      let from = turf.point(route.geometry.coordinates[i]); // type 为 point的feature
      let to = turf.point(route.geometry.coordinates[i + 1]);
      let lDistance = turf.distance(from, to, {  // 两个点之间的距离
        units: units
      });
      if (i == 0) { // 起始点直接推入
        newroute.geometry.coordinates.push(route.geometry.coordinates[0])
      }
      if (lDistance > nDistance) { // 两点距离大于每段值,将这条线继续分隔
        let rings = this._splitLine(from, to, lDistance, nDistance, units)
        newroute.geometry.coordinates = newroute.geometry.coordinates.concat(rings)
      } else { // 两点距离小于每次移动的距离,直接推入
        newroute.geometry.coordinates.push(route.geometry.coordinates[i + 1])
      }
    }
    return newroute
  }

  // 过长的两点轨迹点分段
  _splitLine(from, to, distance, splitLength, units) {
    var step = parseInt(distance / splitLength)

    const leftLength = distance - step * splitLength
    const rings = []
    const route = turf.lineString([from.geometry.coordinates, to.geometry.coordinates])
    for (let i = 1; i <= step; i++) {
      let nlength = i * splitLength
      // turf.alone返回沿着route<LineString>距离为nlength<number>的点
      let pnt = turf.along(route, nlength, {
        units: units
      });
      rings.push(pnt.geometry.coordinates)
    }
    if (leftLength > 0) {
      rings.push(to.geometry.coordinates)
    }
    return rings
  }

  start() {
    if (!this._animated) {
      this._animated = true
      this._animate()
    }
  }

  pause() {
    this._animated = false
    this._animate()
  }

  end() {
    this._animated = false
    this._counter = 0
    this._animate()
  }

  remove() {
    window.cancelAnimationFrame(this._timer)
    this._layerList.map(layer => {
      // console.log(layer)
      if (this.map.getSource(layer)) {
        this.map.removeLayer(layer)
        this.map.removeSource(layer)
      }
    });
  }
}

在mapbox中使用

import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import * as turf from '@turf/turf'
import MapboxLanguage from '@mapbox/mapbox-gl-language';
import carImg from '../../img/car2.jpg'
import RouteReplay from '../libs/routeReplay'
import routeGeoJson from '../../testData/json/routeGeoJson.json';  // 基础图层
import 'antd/dist/antd.css';

function App() {
  const mapContainerRef = useRef();
  const mapRef = useRef();
  const routeReplayRef = useRef();

  // 初始化基础图层
  useEffect(() => {
    mapboxgl.accessToken = 'token'
    mapRef.current = new mapboxgl.Map({
      center: [116.761, 39.452], // starting position [lng, lat]
      zoom: 10,// starting zoom
      pitch: 60,
      style: 'mapbox://styles/mapbox/streets-v11',
      container: mapContainerRef.current,
      antialias: true,
    },
    );
    mapRef.current.addControl(new MapboxLanguage({ defaultLanguage: "zh-Hans" }))
    mapRef.current.on('load', (e) => {
      routeReplayRef.current = new RouteReplay(mapRef.current, routeGeoJson, carImg)
    });
  }, []);


  function startClick() {
    routeReplayRef.current.start()
    console.log(routeGeoJson)
  }

  function pauseClick() {
    routeReplayRef.current.pause()
  }

  function endClick() {
    routeReplayRef.current.end()
  }

  function removeClick() {
    routeReplayRef.current.remove()
  }


  return (
    <div style={{ display: 'flex' }}>
      <div
        id="map-container"
        ref={mapContainerRef}
        style={{ height: '100vh', width: '100vw' }}
      />
      <div style={{ position: 'fixed', top: '0', right: '0' }}>
        <button onClick={() => { startClick() }} style={{ marginRight: '10px' }}>开始</button>
        <button onClick={() => { pauseClick() }} style={{ marginRight: '10px' }}>暂停</button>
        <button onClick={() => { endClick() }} style={{ marginRight: '10px' }}>停止</button>
        <button onClick={() => { removeClick() }} style={{ marginRight: '10px' }}>移除</button>
      </div>
    </div>
  );
}

export default App;

总结

该功核心的思路在参考中都已经实现,笔者只不过在它的基础上进行了封装,代码结构还存在着优化点。
参考:mapboxgl实现带箭头轨迹线

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值