欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

轨迹回放与echarts图表响应变化(基于mapbox/mineMap)此方式实用于目前绝大部分地图服务

程序员文章站 2022-07-13 15:17:35
...

一:需求分析

1.场景:多数道路车辆信息都会设计到历史轨迹回放,包含当前车辆信息,道路信息,已经随之变化的响应数据变更,常采用图表形式展现
2:效果展示静态:轨迹回放与echarts图表响应变化(基于mapbox/mineMap)此方式实用于目前绝大部分地图服务

二 核心分析

  1. 轨迹 要素:点[经纬度],车头的方向(保证车的反向是当前的行进方向),速度(车的行驶速度)
    2.图表联动时数据的传输:当前点位与之对应的变化图表的数据应该时同步的

三 解决方案

1.了解可以实时播放的数据处理方式:
1-1:后端提供接口,按照约定的时间间隔实时获取
1-2:前端一次获取,使用定时器触发
利弊分析:后端实时获取可以省去前端数据处理的过程,但会长时间占用接口服务资源;前端一次获取请求之发起一次,初始静态数据和历史轨迹播放数据同源只需要一个接口;
2.解决echarts实时更新变化的方式:dispatchAction 这是核心
具体做法:为了和小车播放同步必须保证暂定,播放,重新播放时所在的位置,速度保持一致:

   /**
     * 获取当前单车底部图表实例,播放动画
     * @speed  速度
     * @index 当前播放所在的位置下标
     */
     startChart(speed,index){
       let _this=this;
       let chart=this.$refs.detailData.trendLine; //当前使用图表的实例
       let currentIndex =index; //播放所在下标
      _this.mTime = setInterval(function() {
       chart.dispatchAction({
          type: 'showTip',
          seriesIndex: 0,
            name: '车速',
          dataIndex: currentIndex
        });
         currentIndex++;
        _this.setIndex = currentIndex;  //存贮当前移动到的位置
        if(index > _this.chartDataX.length) {   //当播放到最后一位时表示已经结束 清除任务
          currentIndex = 0;
          _this.setIndex= 0;
          clearInterval(_this.mTime);
          _this.mTime=null;

        }
      }, speed);
     }

3.解决实时播放的历史轨迹数据

3-1:画出一条静态路线

 /**
     * 历史轨迹单车分析地图相关应用模块
     */
    drawCycleMap(data) {
      //画之前先清空之前图层
      this.resetHistoryRouterState()
      this.restMapInfoHistory()
      this.historyPointData=[];
      //弹窗信息集合
      let warnList = [];
      // 获取点坐标的集合
      let dataList=[];
      if(data.length>0){
        dataList=data;
      }
      // this.msgInfo = res.body.data.dataInfo;
      // 为了时间轴上数据的完整性,需要在第最后一个点之后补充一个和倒数二一样的点
      let supplementItem = JSON.parse(JSON.stringify(dataList[dataList.length - 1])) ;
      dataList.push(supplementItem);
      // 数据处理
      for (let i = 0; i < dataList.length; i++) {
        // 处理数据1: 为了历史轨迹画线和小车移动
        // 将一个路线上一个点之前的数据都放在一个数组里面,最后将这些数组放到 historyCurTimeAllData 中去,
        // 需要注意的是这里的【0】里面包含的是第一个点的信息,这里的数据处理主要是为了实时移动小车,速度弹窗和画线服务
        let tempArr = [];
        for (let j = 0; j <= i; j++) {
          let item = Object.assign({},dataList[j]);
          item.angle = dataList[j].deflectionAngle;
          item.point = dataList[j].point;
          item.speed = dataList[j].currentSpeed;
          tempArr.push(item)
        }
        this.historyCurTimeAllData[i] = tempArr;
        //处理数据2:e-charts数据
        this.historyYData.push(parseInt(dataList[i].currentSpeed));
        this.historyXData.push(dataList[i].createTime);
         
        // 处理数据3:点坐标集合,为了生成点坐标对应的marker pop,信息弹窗和marker的数据
        // 后端返回的point包含4个值,截取前两个的值, 经度,纬度,序号,角度
        this.historyPointData.push(dataList[i].point);
        let obj = {
          point: dataList[i].point
        }
        warnList.push(obj);
        if (i === dataList.length -1)  {
          this.mapController.addHistoryAllItems(this.historyPointData)
     
        }
        this.$emit("sendHistoryMapInfo",this.historyCurTimeAllData,2000,this.mapController.curIndex)
      }
      let obj={};
      obj.target=this.historyYData;
      obj.timeCell=this.historyXData;
      blur.$emit("senChartData",obj)
      this.$emit("getChart",this.historyXData);
    }

3-2:点击播放按钮时开始播放或暂停

    /** 开启停止运行按钮操作 */
    toAnimate() {
      if (this.playState === 'PAUSE') {
        clearInterval(this.mapController.historyTimer);
        this.mapController.historyTimer=null;
        this.$emit("restChart")
        this.playState = 'START';

      } else {
        if(this.currentSend){
          this.mapController.addCarFollowRouterLineMove(this.historyMapData,this.currentSpeed,0,this.playState)
          this.$emit("startChart",2000,0)
        }else {
          this.mapController.addCarFollowRouterLineMove(this.historyMapData,this.currentSpeed,this.mapController.curIndex,this.playState)
          this.$emit("startChart",2000,this.setIndex)
        }
        this.currentSend=false;
        this.playState = 'PAUSE';
      }
    },

3-3:核心轨迹播放代码块:

  /**
     * 小车随着历史轨迹移动,轨迹不停的画线
     * 1. 定时器控制小车移动的速度
     * 2. 首先是数据的处理,historyCurTimeAllData 代表所有的数据 historyCurTimeAllData[200]:前200个点的数据
     *    historyData  处理后的历史数据,代表整个轨迹的数据
     * 3. 移动画线原理是 从一个点到下一个点,首先是小车的位置改变,然后是marker的位置跟着改变,最后是使用加上一个新点的数据来替换旧数据画线
     * 4. 点击暂停的时候需要计算并且记录当前的点
     * @param  包含每个点之前所有数据的集合的集合 小车移动的速度   当前小车的点
     * @return void
     */
    addCarFollowRouterLineMove(data, moveSpeed = 1000, pointIndex = 0,type) {
        data
        if (data.length === 0) return;

        const map = _map;
        let historyData = data[data.length - 1];  //
        let historyCurTimeData;  // 起点到小车目前所在位置点的所有的数据
        let curPoint = {};
        let curAngle = 0;
        let curSpeed = 0;

        // 处理已经通过的数据为适合线模式的数据
        let recodePathLish = []; // 已经通过的点的集合
        if (data[pointIndex].length > 0) {
            for (let i = 0; i < data[pointIndex].length; i++) {
                let item = Object.assign({},data[pointIndex][i].point);
                recodePathLish.push(item);
            }
        }
        if(data[0] != undefined){
            historyCurTimeData = data[pointIndex];//当前小时历史数据
            map.stop();
            // 设置底图跟随小车移动
            map.easeTo({
                center:historyCurTimeData[pointIndex].point,
            });

            // 如果当前的播放状态是停止,那么只需要将小车和marker移动到传入点的位置,画线当前位置之间的线数据
            if (type === 'PAUSE') {
                let curPoint = historyData[pointIndex].point;
                let curAngle = historyData[pointIndex].angle+270;
                let curSpeed = parseInt(historyData[pointIndex].speed);

                // 修改小车的位置
                let geo = turf.point(curPoint);
                map.getSource('history-play-single-car').setData(geo);

                // 修改marker的位置
                this.updateSpeedMarker(curPoint, curSpeed);
                curSpeed = parseInt(curSpeed);
                if (map.getLayer('history-play-single-car')) {
                    map.setLayoutProperty('history-play-single-car', 'icon-rotate', curAngle);
                }

                if (recodePathLish != null && recodePathLish.length > 1) {
                    let hisPath = turf.lineString(recodePathLish);
                    if (map.getSource('history-play-history-path')) {
                        map.getSource('history-play-history-path').setData(hisPath);
                    }
                    else {
                        console.log("无历史轨迹")
                    }
                } else {
                    // 形成一条线的必要条件是必须至少有2个点,当按钮拖动到第一个点的时候,自动补充2个点作为线,其实就是清除掉线
                    let hisPath = turf.lineString([[116.40717, 39.90469], [116.40717, 39.90469]]);
                    if (map.getSource('history-play-history-path')) {
                        map.getSource('history-play-history-path').setData(hisPath);
                    }
                }
                return;
            }
            // 如果当前的播放状态是播放,那么需要先做上面的操作,然后设置定时器让小车一个点一个点的往下走
            else {
                this.historyTimer = setInterval(() => {
                    let pathList = recodePathLish ? recodePathLish : [];
                    //时间轴跑完一程清零
                    if (pointIndex < data.length - 1) {
                        pointIndex++;     //第n+1个点
                        // 这里是为了记录当前停止时小车移动到哪个位置点上了
                        this.curIndex=pointIndex;
                        // this.context.$bus.emit('CarCurPointIndex',{pointIndex:pointIndex})
                    } else {
                        clearInterval(this.historyTimer);  //跑完
                    }
                    curPoint = historyData[pointIndex].point;
                    curAngle = historyData[pointIndex].angle+270;
                    curSpeed = parseInt(historyData[pointIndex].speed);

                    // 控制地图是否随着小车移动,配置地图中心点跟着小车移动
                    if (this.isUpdateCenter()) {
                        map.easeTo({
                            center: curPoint,
                            // zoom: 14,
                            duration: moveSpeed
                        });
                    }

                    // 修改小车的位置
                    let geo = turf.point(curPoint);
                    map.getSource('history-play-single-car').setData(geo);

                    // 修改marker的位置
                    curSpeed = parseInt(curSpeed);
                    this.updateSpeedMarker(curPoint,curSpeed);

                    if(map.getLayer('history-play-single-car')){
                        map.setLayoutProperty('history-play-single-car', 'icon-rotate', curAngle);
                    }
                    pathList.push(curPoint);

                    // 找到地图上已经存在的线图层,使用新的线数据替换老的线数据,让线看起来动,其实只是数据的替换
                    if (pathList.length > 1) {
                        let hisPath = turf.lineString(pathList);
                        if(map.getSource('history-play-history-path')){
                            map.getSource('history-play-history-path').setData(hisPath);
                        }else {
                            console.log("无历史轨迹")
                        }
                    }
                }, moveSpeed);
            }
        }
    }

3-4:小车移动时小车信息框体跟随移动

  /**
     * 实时更新小车marker上面的速度,和marker的位置,让marker跟着小车跑
     * @param  经纬度  marker上面显示的速度文字
     * @return void
     */
    updateSpeedMarker(lngLat, text) {
        SingleSpeedMarker.speed_el.innerText = text + 'km/h';
        SingleSpeedMarker.speedMarker.setLngLat(lngLat).addTo(_map);
    }

注意:
1.地图线,点应对应使用当前地图依赖所使用的方法,此出只是作为示例(mineMap)
2.车头方向可能后端无法提供或者未提供偏角时前端需要手动计算,且计算的数据应在3-1步骤内完成数据的重组,减少后续数据操作
3:由于点位信息密集,后端提供的echarts图表数据list[],并不适用当前密集场景所以建议再3-1内完成echarts数据组装,x轴可以使用线性分割或者使用后端提供的时间范围,保证显示效果良好即可

角度计算

// 在后端没有返回值的情况下前端计算两个点坐标之间的角度
export function calRotate(point,point1) {
  let rotate = null;
  const m_point = lonlatTomercator(point);
  const m_point1 = lonlatTomercator(point1);

  const x = m_point1[0] - m_point[0];
  const y = m_point1[1] - m_point[1];

  const len = Math.sqrt(Math.pow(x,2)+Math.pow(y,2));
  const cos = x/len;
  const deg = Math.acos(cos)*(180/Math.PI);
  if(y>0){
    rotate = -deg;
  }else if(y<0){
    rotate = deg;
  }else if(y===0){
    if(x>0) rotate = 0;
    else if(x<0) rotate = 180;
  }
  // console.log(rotate);
  return rotate
}

// 投影转换
function lonlatTomercator(lonlat) {
  var mercator= [];
  var x = lonlat[0] *20037508.34/180;
  var y = Math.log(Math.tan((90+lonlat[1])*Math.PI/360))/(Math.PI/180);
  y = y *20037508.34/180;
  mercator[0] = x;
  mercator[1] = y;
  return mercator;
}