leaflet 巡航器 播放历史轨迹

巡航器

功能与参考文档

暂时未找到leaflet巡航器插件,supermap也并未对此进行封装。于是打算自己着手写一个。

  1. 轨迹 L.polying 绘制
  2. 箭头方向 leaflet-rotatedmarker npm 下载一下
  3. 进度条 MovingMarker 引入js文件
    1. 加速、减速
    2. 播放、暂停
    3. 进度条

由于本巡航器要使用进度条功能,因此必须识别当前巡航器所在点,因此采用点与点之间添加动哈的方式完成巡航。暂停并非就地暂停,而是暂停到下一个轨迹点上。 如果你不需要进度条功能,可以更为简单的完成巡航器,movingMarker 支持多轨迹点动画,自带播放、停止功能。

参考: Leaflet MovingMarker 轨迹回放功能 | Ryan's Blog (ryancui.com)

环境准备

环境准备:

css 复制代码
npm install leaflet --save
npm install leaflet-rotatedmarker --save

引入 "MovingMarker.js"

我再这里使用了sass,elementui,大家可以按照自己实际需求引入。

json 复制代码
"dependencies": {
    "@supermap/iclient-leaflet": "^11.1.1",
    "axios": "^1.6.2",
    "core-js": "^3.8.3",
    "element-ui": "^2.15.14",
    "leaflet": "^1.9.4",
    "leaflet-rotatedmarker": "^0.2.0",
    "leaflet.markercluster": "^1.5.3",
    "vue": "^2.6.14",
    "vue-router": "^3.6.5"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@supermap/babel-plugin-import": "^0.0.1",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "node-sass": "^9.0.0",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.3",
    "vue-template-compiler": "^2.6.14"
  },

详解

1. 引入

js 复制代码
import { Message } from "element-ui";
import L from 'leaflet';
import MovingMarker from "./MovingMarker.js"
import 'leaflet-rotatedmarker'
import myData from "./myData" // 自己写的伪数据

2. 数据准备

js 复制代码
  data () {
    return {
      isShow: false,
      boxLoading: false, //盒子loading
      loading: false,
      // step 播放器
      renderBottom: {
        //表格筛选参数
        startTime: "",
        endTime: "",
        removeTime: "",
      },
      startIndex: 0,
      isPlaying: false,
      isPause: false,
      isPlayed: false,
      currSpeed: 1, //速度
      FILL_WIDTH: 235, // 进度条宽度
      fillWidth: 0, // 进度条宽度
      percentage: 0, // 进度条进度
      hasChangeIndex: false,
      // step map
      map: null,
      detailLayer: null,
      polyline: null,
      movingArrow: null,
      popup: null,
      mapData: [],
      latlngs: [],
      iconUrl: [
        //起终点icon
        require("./imgs/start.png"), // 起点
        require("./imgs/end.png"),  // 终点
        require("./imgs/leader.png") // 巡航器
      ],
      leader: null,
    };
  },
  computed: {
    per () {
      const per = this.fomatFloat(this.FILL_WIDTH / this.mapData.length, 2)
      return per
    } // 进度条进度
  },

3. 地图初始化

mounted的时候,调用此函数。这里采用高德地图

html 复制代码
    <div class="map-container"
         id="map-container">
    </div>
js 复制代码
  mounted () {
    this.initMap()
  },
js 复制代码
    initMap () {
      // 加载 open street map 图层服务
      this.amapLayer = L.tileLayer("http://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}", {
        attribution: '&copys; 高德地图', //用来进行属性控制的字符串,描述了图层数据
        maxZoom: 18, //map 对象中的 maxZoom 属性是必须的,否则在创建 **makerClusterGroup ** 会报错
        minZoom: 3,
        subdomains: "1234", // 子域名,替换{s}
      })
      this.map = L.map('map-container', {
        center: [34.1529, 108.5632], // 地图中心点
        zoomControl: false, //是否显示缩放比空间
        zoom: 10, //初始缩放比
      });
      this.amapLayer.addTo(this.map)
    },

引入成功后,页面上出现高德地图;初始定位点为西安。

4. 绘制轨迹线

需要提前准备一个数组 latlngs : [[lat,lng],[lat,lng],...] 所有轨迹点经纬度数组

js 复制代码
    // 绘制轨迹线
    createPolyline () {
      // 清除之前的轨迹
      if (this.detailLayer) {
        this.map.removeLayer(this.detailLayer)
        this.detailLayer.clearLayers()
        this.polyline = null
      }
      // 起点、终端marker
      const start = L.marker(this.latlngs[0], {
        icon: L.icon({
          iconSize: [30, 35],
          iconUrl: this.iconUrl[0],
          iconAnchor: [15, 35]
        })
      })
      const end = L.marker(this.latlngs[this.latlngs.length - 1], {
        icon: L.icon({
          iconSize: [30, 35],
          iconUrl: this.iconUrl[1],
          iconAnchor: [15, 35]
        })
      })
      this.detailLayer = L.layerGroup([start, end]).addTo(this.map)
      if (this.latlngs.length > 1) {
        //  轨迹点大于 1 ,画轨迹
        this.polyline = L.polyline(this.latlngs, { color: 'blue' })
        this.detailLayer.addLayer(this.polyline)
        // zoom the map to the polyline
        this.map.fitBounds(this.polyline.getBounds());
        // this.createLeader() // 添加巡航器, 写完createLeader函数后记得放开
      }
      this.loading = false;
      this.boxLoading = false;
    },

调用此函数效果如图所示。 暂未添加巡航器,写完createLeader函数后记得调用 this.createLeader()

4. 添加巡航器

先写一个进度条,html/css 代码如下。利用 fillWidth 控制进度条进度

html 复制代码
  <div id="wrapper"
	   @click="handleSlider">
	<div id="fill"
		 :style="{ width: fillWidth }"></div>
	<div class="btn"
		 :style="{ left: fillWidth }"></div>
	<div id="tool"></div>
  </div>
css 复制代码
// 巡航器
#wrapper {
  background-color: #fff;
  position: relative;
  border-radius: 15px;
  width: 230px; /* 自定义*/
}
#fill {
  display: inline-block;
  position: absolute;
  left: 0;
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(45deg, pink, transparent);
}
.video-btn {
  display: inline-block;
  position: absolute;
  left: 0;
  width: 10px;
  height: 10px;
  background: hotpink;
  border-radius: 50%;
}

大概能得到上图效果,当然你也可以根据自己的喜欢调节样式。

改变 fillWidth 的值,得到以下效果

下面来添加巡航器 createLeader

  1. 避免数据残余,清除之前巡航器
  2. 创建一个从 latlngs[0] --> latlngs[1]movingMarker ,添加到地图上。这里我添加到了轨迹线的图层 this.detailLayer 上,你也可以直接添加到 this.map 上,或者自己新建一个图层。
  3. 添加一个监控时间:当当前动画结束后,调用 this.addPoint() 函数。并且 this.startIndex = 1 , startIndexaddPoint() 的一个重要变量,下一步马上讲到。

这一步只是添加巡航器,并不播放动画,因此看不到动画效果,只能看到地图上多了一个巡航器图标

js 复制代码
    createLeader () {
      // 清除之前巡航器
      if (this.movingArrow) {
        this.detailLayer.removeLayer(this.movingArrow)
        this.movingArrow = null
        console.log('removeLayer(this.movingArrow))')
      }
      // 巡航器初始化
      this.movingArrow = L.Marker.movingMarker(this.latlngs.slice(0, 2), 2000).addTo(this.detailLayer);
      this.movingArrow.setIcon(L.icon({
        iconUrl: this.iconUrl[3], //  巡航器icon
        iconSize: [30, 40],
        iconAnchor: [15, 40]
      }))
      // 巡航器角度改变
      this.movingArrow.setRotationAngle(this.mapData[this.startIndex].dir)
      // 视野锁定巡航器
      this.map.setView(this.movingArrow.getLatLng())
      this.startIndex = 1
      //  下一步,从latlngs[1]-->latlngs[2]
      this.movingArrow.on('end', () => {
        this.addPoint()
      })
    },

然后再来写 addPoint ,看看当前动画结束后要干什么。当然时开启另一段动画啦 ~ addPoint , 添加 latlngs 轨迹数组点中从当前点到 startIndex + 1 处点的位移动画。startIndex 记录当前点的索引。

  1. 判断当前动画是否在播放状态 isPlaying,如果不在播放状态,还等什么,直接return就好
  2. 判断用户有无使用进度条 hasChangeIndex 。如果使用了,需要改变 startIndex 这个属性,改变到进度条对应的值 (在 moveTopoint 函数中改变),这里需要改变当前点的位置到用户期待的位置。****
js 复制代码
    addPoint () {
      if (!this.isPlaying) {
        return   // 完成已绑定的动画后直接退出,后续无法监听到end时间,动画结束
      }
      if (this.hasChangeIndex) {
        //  使用进度条,让巡航器快进/ 快退到某点,视野
        this.hasChangeIndex = false
        this.movingArrow.setLatLng(this.latlngs[this.startIndex])
        this.movingArrow.setRotationAngle(this.mapData[this.startIndex].dir)
        this.map.setView(this.latlngs[this.startIndex])
      }
      // 进度条进度
      this.fillWidth = parseFloat(this.per * this.startIndex) + "px";
      setTimeout(() => {
        if (this.startIndex < this.mapData.length - 1) {
          this.renderBottom.removeTime = this.mapData[this.startIndex].dataTime
          let duration // 点与点之间移动的时间
          if (this.latlngs[this.startIndex][0] == this.latlngs[this.startIndex + 1][0] && this.latlngs[this.startIndex][1] == this.latlngs[this.startIndex + 1][1]) {
            duration = 1
          } else {
            const temp = (new Date(this.mapData[this.startIndex + 1].dataTime).getTime() - new Date(this.mapData[this.startIndex].dataTime).getTime()) / 10 / this.currSpeed
            duration = temp > 2000 ? 2000 : temp
          }
          this.map.setView(this.movingArrow.getLatLng())
          this.movingArrow.moveTo(this.latlngs[++this.startIndex], duration)
          this.movingArrow.setRotationAngle(this.mapData[this.startIndex].dir)
          if (this.startIndex == this.latlngs.length - 1) {
            Message.warning("历史轨迹播放完毕!");
            this.isPlaying = false
          }
        }
      }, 0);
    },
    fomatFloat (src, pos) {
      return Math.round(src * Math.pow(10, pos)) / Math.pow(10, pos);
    },
  1. 进度条点击事件
JS 复制代码
    // 进度条点击
    handleSlider (e) {
      if (this.movingArrow) {
        let offsetX = e.offsetX;
        let width = "";
        if (offsetX > 230) {
          width = 230;
        } else if (offsetX < 20) {
          width = 0;
        } else {
          width = offsetX - 10;
        }
        let num = this.mapData.length;
        let per = width / 230;
        let index = Math.floor(per * num);
        this.moveToPoint(index); // 动画位置改变
        this.startIndex = index;
        this.fillWidth = width + "px"; // 进度条长度改变
      } else {
        Message.warning("请先查询车辆历史轨迹!");
      }
    },
    // 移动到某点
    moveToPoint (index) {
      this.startIndex = index // startIndex 控制动画开始于latlngs数组的哪一个index
      if (this.isPlaying) {
        this.hasChangeIndex = true
      } else {
        this.movingArrow.setLatLng(this.latlngs[this.startIndex])
	    this.movingArrow.setRotationAngle(this.mapData[this.startIndex].dir)
        this.map.setView(this.latlngs[this.startIndex])
      }
    },
  1. 加减速、播放、暂停
js 复制代码
    // 开始
    startAnimation () {
      if (this.movingArrow && !this.isPlaying) {
        this.isPlaying = true
        this.map.setZoom(18)
        this.addPoint()
      }
    },
    // 暂停
    pauseAnimation () {
      if (this.movingArrow && this.isPlaying) {
        this.isPlaying = false
      }
    },
    // 加减速按钮
    handleLowSpeed (type) {
      if (!this.movingArrow) return
      if (type == 1) {
        if (this.currSpeed == 1) {
          this.currSpeed = 0.5;
        } else if (this.currSpeed > 1) {
          this.currSpeed--;
        }
      } else {
        if (this.currSpeed == 0.5) {
          this.currSpeed = 1;
        } else if (this.currSpeed < 4) {
          this.currSpeed++;
        }
      }
    },

最终效果展示

相关推荐
GIS阵地3 小时前
QgsProviderMetadata 详解(基于 QGIS 3.40.13 API)
数据库·qt·arcgis·oracle·gis·开源软件·qgis
GISBox3 天前
PostGIS数据通过GISBox发布WFS/WMS全攻略
数据库·postgresql·wms·gis·postgis·矢量·gisbox
NULIWEIMENXIANG3 天前
ArcPy 程序调用 QGIS 进程实现几何拓扑检查
python·arcgis·gis
我才是银古4 天前
为什么要做 GeoPipeAgent
gis·ai平台
夜郎king5 天前
耒阳童车产业园POI实证分析——基于高德地图,还原“百亿园区”真实面貌
大数据·人工智能·gis·空间分析
ct9786 天前
Cesium的时间与时钟系统
gis·webgl·cesium
奔跑的呱呱牛9 天前
GeoJSON 在大数据场景下为什么不够用?替代方案分析
java·大数据·servlet·gis·geojson
奔跑的呱呱牛9 天前
GeoJSON vs TopoJSON:不仅是体积差异,而是数据模型的差异
gis·geojson·topojson
GISBox10 天前
技术干货:3DTiles转OSGB的适用场景及标准操作流程
gis·数据修复·3dtiles·osgb·gisbox·切片转换·反切
qq_2837200512 天前
Cesium实战(三):加载天地图(影像图,注记图)避坑指南
json·gis·cesium