交通行业,轨迹回放是过不去的坎,算是基本功能。本文讲述通过查询车辆某一时间段的Gps数据,将不同状态的路段绘制在高德地图上,并通过slider滑块拖动轨迹,并且marker能沿轨迹行走,本文可算是个人开发过程的记录笔记,并非基础教程,不负责解答。

由于本人也刚接触Vue没多久,本来有同事在其他项目实现类似的功能,但封装太多功能复杂,看起来费劲,于是自己花了两三天时间研究了下,好在vue-amap兼容高德地图原生sdk,所以我感觉基本上是用JavaScript的方式实现轨迹回放。

先放效果图吧

1、用echarts画的时间速度图,不同颜色表示不同车辆状态,空车重车离线等,这个不是本篇重点,不讲了。

2、slider的轴为时间轴,长度为gps数量,拖到哪就画到哪的轨迹,为了更好的拖动效果,采用预先画好polyline,实时加载。

3、车辆会同时间轴一起动,有位移时,车辆按一定速度行驶,gps经纬度一样时,车不动,slider动,达到滑块和轨迹同步的效果。

总体思路大致是这样:

1、通过接口获取Gps数组,长度为N;

2、遍历Gps数据,根据点位的状态,两点为一段,预先绘制各个线段,位移不变则以0代替,总共有N-1段;

3、

直接上代码,

1、定义一个地图和marker,因为要区分线路颜色,polyline在获取到gps后动态生成。

<div class="amap-page-container">
      <el-amap
        vid="amapDemo"
        :center="mapData.center"
        :zoom="mapData.zoom"
        :plugin="plugin"
        class="amap-demo"
        :events="mapData.events"
        pitch-enable="false">
        <el-amap-marker vid="carmarker"
                        auto-rotation="true"
                        angle="0"
                        :position="carMarker.position"
                        :icon="carMarker.icon"
                        :offset="carMarker.offset"
                        :events="carMarker.events" />
      </el-amap>
    </div>

2、一个echarts图表,element的Slide组件,还有各个button及绑定的相应事件。

<div>
        <el-row>
          <pie id="line1"
               width="100%"
               height="100px"
               :chart-options="speedChartOptions" />
        </el-row>
        <el-row style="margin-top:-15px;margin-left:30px;margin-right:5px; ">
          <el-slider ref="timeSlider"
                     v-model="currentIndex"
                     :format-tooltip="formatTooltip"
                     :max="maxIndex"
                     @input="moveSlider"
                     @change="afterMoveSlider" />
        </el-row>
        <el-row style="margin-left:30px;margin-top:-5px;">
          <el-button v-if="!isMoving"
                     type="primary"
                     size="mini"
                     :disabled="!list||list.length===0||isMoving"
                     @click="carStartMove()">开始</el-button>
          <el-button v-else
                     type="primary"
                     size="mini"
                     :disabled="!isMoving"
                     @click="carPauseMove()">暂停</el-button>
          <el-button type="primary"
                     size="mini"
                     :disabled="currentGpsIndex===0"
                     @click="carResetMove()">重置</el-button>
          <el-button type="primary"
                     size="mini"
                     :disabled="currentGpsIndex===0"
                     @click="carBackMove()">向后</el-button>
          <el-button type="primary"
                     size="mini"
                     :disabled="currentGpsIndex===maxIndex"
                     @click="carforeMove()">向前</el-button>
          <el-button type="primary"
                     size="mini"
                     :disabled="!isMoving"
                     @click="carSlowMove()">减速</el-button>
          <el-button type="primary"
                     size="mini"
                     :disabled="!isMoving"
                     @click="carFastMove()">加速</el-button>
        </el-row>
      </div>

3、绑定的数据

data: function () {
    const self = this;
    return {
      filterQuery: {
        plateNo: '',
        beginTime: this.$moment().add(-1, 'h').format('YYYY-MM-DD HH:mm:ss'),
        endTime: this.$moment().format('YYYY-MM-DD HH:mm:ss')
      },
      listLoading: false,
      searchVisible: false,
      speedChartOptions: {},
      list: null,
      gpsCount: 0,
      maxIndex: 0,
      currentIndex: 0,
      currentGpsIndex: 0,

      isMoving: false,
      moveSpeed: 2000,

      traceLine: [],
      movingTraceLine: [],
      passedPathLine: [],
      markers: [],
      carMarker: {
        position: [116.478935, 39.997761],
        icon: carIcon,
        offset: [-26, -13],
        events: {
          init(o) {
            self.moveMarker = o;
          },
          moving(e) {
            //为了保持行驶过程中的线路样式与silder拖动的样式一致,或许有其他更好的方式
            const passingPolyline = self.movingTraceLine[self.currentGpsIndex - 1];
            passingPolyline.setPath(e.passedPath);
            self.mapObj.add(passingPolyline);
            self.passedPathLine.push(passingPolyline);
          },
          moveend(e) {
            self.moveCar();
          }
        }
      },
      moveMarker: null,
      mapObj: null,
      mapData: {
        zoom: 16,
        center: [113.32845, 23.134527],
        events: {
          init(o) {
            self.mapObj = o;
          },
          click(e) {
            // const { lng, lat } = e.lnglat;
            // self.mapData.center = [lng, lat];
          }
        }
      },
      /* 一些工具插件*/
      plugin: [
        {
          // 地图类型
          pName: 'MapType',
          defaultType: 0,
          events: {
            init(instance) {
              // console.log(instance);
            }
          }
        }
      ]
    };
  },

4、具体方法,onSubmit,查询坐标点,提前画好不同样式的线段,位移为0则线段为0,即可根据点位序号快速加载轨迹,通过地图原生map.add([])或map.remove([])方法来显示滑块拖动的实时效果,这里有一个问题就是当线段多的时候,或许会卡,可以试验下,但目前我还找不出更好的办法。

onSubmit() {
      this.listLoading = true;

      if (this.isMoving) {
        this.moveMarker.stopMove();
        this.isMoving = false;
      }
      //清除前一次查询轨迹
      this.currentIndex = 0;
      this.currentGpsIndex = 0;
      this.maxIndex = 0;
      if (this.mapObj && this.passedPathLine.length > 0) {
        this.mapObj.remove(this.passedPathLine);
      }
      //定义两个数组存储轨迹,在滑块拖动时可以快速响应
      this.passedPathLine = [];
      this.movingTraceLine = [];
      
      const arrDate = [];
      const arrLoadSpeed = [];
      const arrEmptySpeed = [];
      const arrOffSpeed = [];
      fetchTracePlayBack(this.filterQuery).then(response => {
        this.listLoading = false;
        var resultList = JSON.parse(response);
        if (resultList.length === 0) {
          this.$message.error(`【${this.filterQuery.plateNo}】gps为空`);
          if (this.markers.length > 0) {
            this.mapData.center = this.markers[0].position;
          }
          return;
        }

        // const gpsList = [];
        const traceLineArr = [];

        resultList.forEach((item, index) => {
          if (index < resultList.length - 1) {
            if (resultList[index].Longitude !== resultList[index + 1].Longitude || resultList[index].Latitude !== resultList[index + 1].Latitude) {
              //根据两点距离,提前画好轨迹,N个点则有N-1个线段,主要是不同线段样式
              const computeline = this.getPolyLine(resultList[index], resultList[index + 1]);
              this.movingTraceLine.push(computeline);
              traceLineArr.push(computeline);
            } else {
              //两点间无位移,则线段为0
              this.movingTraceLine.push(0);
              traceLineArr.push(0);
            }
          }
          item.index = index;
          item.position = this.$wgs84togcj02(item.Longitude, item.Latitude);
          item.GpsTime = this.$moment(item.GpsTime).format('YYYY-MM-DD HH:mm:ss');
          if (parseInt(item.RunStatus) === 0) {
            item.RunState = '熄火';
          } else if (parseInt(item.RunStatus) === 2) {
            item.RunState = '离线';
          } else if (parseInt(item.RunStatus) === 1) {
            if (parseInt(item.CarryStatus) === 1) {
              item.RunState = '空车';
            } else if (parseInt(item.CarryStatus) === 2) {
              item.RunState = '重车';
            } else {
              item.RunState = '运行';
            }
          } else {
            item.RunState = '未知';
          }
      
        
        this.traceLine = traceLineArr;
        this.passedPathLine = traceLineArr;
        this.list = resultList;
        this.gpsCount = resultList.length;
        this.maxIndex = resultList.length - 1;
        this.moveMarker.setPosition(resultList[0].position);
        this.moveMarker.setIcon(this.getCarIcon(resultList[0].RunStatus, resultList[0].CarryStatus));
        this.mapData.center = resultList[0].position;
        this.mapObj.add(this.passedPathLine);
        this.$nextTick(() => {
          this.mapObj.setFitView();
        });
      }).catch(err => {
        this.$message.error(err);
        this.listLoading = false;
      });
    },

 getpolyline方法,根据两gps点的属性返回不同样式的线路

getPolyLine(dataCur, dataNext) {
      if (dataCur == null || dataNext == null) { return null; }
      const lineArr = [];// 创建线覆盖物节点坐标数组
      lineArr.push(this.$wgs84togcj02(dataCur.Longitude, dataCur.Latitude));
      lineArr.push(this.$wgs84togcj02(dataNext.Longitude, dataNext.Latitude));
      let polyline = null;
      const curCarryStatus = parseInt(dataCur.CarryStatus);
      const nextCarryStatus = parseInt(dataNext.CarryStatus);
      if ((curCarryStatus === 1 && nextCarryStatus === 1) || (curCarryStatus === 2 && nextCarryStatus === 1)) {
        // 空车
        polyline = new AMap.Polyline({
          path: lineArr, // 设置线覆盖物路径
          strokeColor: '#0000ff', // 线颜色
          strokeOpacity: 0.6, // 线透明度
          strokeWeight: 3, // 线宽
          strokeStyle: 'solid', // 线样式
          strokeDasharray: [10, 5] // 补充线样式
        });
      } else if ((curCarryStatus === 1 && nextCarryStatus === 2) || (curCarryStatus === 2 && nextCarryStatus === 2)) {
        // 重车
        polyline = new AMap.Polyline({
          path: lineArr, // 设置线覆盖物路径
          strokeColor: 'red', // 线颜色
          strokeOpacity: 0.8, // 线透明度
          strokeWeight: 3, // 线宽
          strokeStyle: 'solid', // 线样式
          strokeDasharray: [10, 5] // 补充线样式
        });
      } else {
        polyline = new AMap.Polyline({
          path: lineArr, // 设置线覆盖物路径
          strokeColor: 'black', // 线颜色
          strokeOpacity: 0.8, // 线透明度
          strokeWeight: 3, // 线宽
          strokeStyle: 'solid', // 线样式
          strokeDasharray: [10, 5] // 补充线样式
        });
      }
      return polyline;
    },

elementui滑块拖动时显示当前点的时间

formatTooltip(val) {
      if (this.list !== null) {
        return this.list[val].GpsTime;
      }
    },

button绑定的方法

carStartMove() {
      if (this.currentGpsIndex === this.maxIndex) {
        this.currentGpsIndex = 0;
        this.currentIndex = 0;
        this.moveMarker.setPosition(this.list[0].position);
        this.mapObj.remove(this.passedPathLine);
      }
      if (this.currentGpsIndex === 0 && this.passedPathLine.length > 0) {
        this.mapObj.remove(this.passedPathLine);
        this.passedPathLine = [];
      }
      this.isMoving = true;
      this.moveCar();
    },
    carPauseMove() {
      if (this.isMoving) {
        this.moveMarker.stopMove();
        this.isMoving = false;
      }
    },
    carResetMove() {
      if (this.isMoving) {
        this.moveMarker.stopMove();
        this.isMoving = false;
      }
      this.currentGpsIndex = 0;
      this.currentIndex = 0;
      this.moveMarker.setPosition(this.list[0].position);
      this.moveMarker.setIcon(this.getCarIcon(this.list[0].RunStatus, this.list[0].CarryStatus));
    },
    carBackMove() {
      if (this.currentGpsIndex > 0) {
        this.currentGpsIndex--;
        this.currentIndex--;
      }
    },
    carforeMove() {
      if (this.currentIndex < this.maxIndex) {
        this.currentGpsIndex++;
        this.currentIndex++;
      }
    },
    carSlowMove() {
      if (this.moveSpeed > 200) {
        this.moveSpeed = this.moveSpeed - 200;
      }
    },
    carFastMove() {
      this.moveSpeed += 200;
    },

图标移动的主要方法movecar,通过marker.moveto来移动图标,moveend之后再调用movecar来完成全部轨迹的行走,直至最后,movealong我试过并不满足我的思路,没深入。moveSlider和afterMoveSlider则是slider绑定的事件,用户配合图标的移动,

moveCar() {
      const self = this;
      if (this.currentIndex === this.maxIndex) {
        this.moveMarker.stopMove();
        this.isMoving = false;
        return;
      }
      const bound = this.mapObj.getBounds();
      if (!bound.contains(this.moveMarker.getPosition())) {
        this.mapObj.panTo(this.moveMarker.getPosition());
      }
      this.currentGpsIndex++;
      this.currentIndex++;
      this.moveMarker.setIcon(this.getCarIcon(this.list[this.currentGpsIndex].RunStatus, this.list[this.currentGpsIndex].CarryStatus));
      if (this.traceLine[this.currentGpsIndex - 1] === 0 || this.traceLine[this.currentGpsIndex - 1].getLength() < 0.5) {
        if (this.isMoving) {
          setTimeout((e) => {
            if (self.isMoving) {
              self.moveCar();
            }
          }, 100);
        }
      } else {
        this.moveMarker.moveTo(this.list[this.currentGpsIndex].position, self.moveSpeed);
      }
    },

    moveSlider(v) {
      if (this.isMoving) {
        if (v > this.currentGpsIndex || v < this.currentGpsIndex) {
          this.moveMarker.stopMove();
          this.isMoving = false;

          this.mapObj.remove(this.passedPathLine);
          this.passedPathLine = this.traceLine.slice(0, v);
          this.mapObj.add(this.passedPathLine);
          this.moveMarker.setPosition(this.list[v].position);
          this.moveMarker.setIcon(this.getCarIcon(this.list[v].RunStatus, this.list[v].CarryStatus));

          const bound = this.mapObj.getBounds();
          if (!bound.contains(this.moveMarker.getPosition())) {
            this.mapObj.panTo(this.moveMarker.getPosition());
          }
        }
      } else {
        if (this.mapObj != null) {
          if (this.passedPathLine.length > 0) {
            this.mapObj.remove(this.passedPathLine);
          }
          this.passedPathLine = this.traceLine.slice(0, v);
          this.mapObj.add(this.passedPathLine);
          this.moveMarker.setPosition(this.list[v].position);
          this.moveMarker.setIcon(this.getCarIcon(this.list[v].RunStatus, this.list[v].CarryStatus));
          const bound = this.mapObj.getBounds();
          if (!bound.contains(this.moveMarker.getPosition())) {
            this.mapObj.panTo(this.moveMarker.getPosition());
          }
        }
      }
      // 当重置为0时触发,可重新绘制轨迹
      if (v === 0 && this.mapObj !== null) {
        this.passedPathLine = this.traceLine.slice(0);
        this.mapObj.add(this.passedPathLine);
      }
    },
    afterMoveSlider(v) {
      this.currentGpsIndex = v;
    },

这大概就是所有的思路了,时间有限,浅尝辄止,或许并不完善,但这样的结合和代码,我想肯定可以帮到你,写文章比代码要纠结些,希望能够给需求的人提供一种思路启发。

要是有用的话,点个赞转发下啥的,技术在于分享,代码在于复制,拿去拿去都拿去

Logo

前往低代码交流专区

更多推荐