前言
本篇文章主要介绍高德地图的轨迹回放或播放的实现过程,是基于vue2实现的功能,同时做一些改动也是能够适配vue3的。其中播放条是用的是element UI中的el-slider组件,包括使用到的图标也是element UI自带的。可以实现轨迹的播放、暂停、停止、播放倍数,以及播放拖拽,涉及到的高德地图的相关权限申请,这里就不再赘述,好了,废话不多说,效果图附上。
效果图
一、地图初始化
首先,需要在组件dom加载完毕后初始化地图,这里小谭直接用的new AMap.Map方法进行初始化,需要在index.html引入高德的服务。
<script src="https://webapi.amap.com/maps?v=2.0&key=你的key"></script>
其次,在引入高德服务之后,需要在单独引入高德AMapUI 组件库,因为轨迹播放是基于该组件库实现的,引入示例:
<!--引入UI组件库(1.1版本) -->
<script src="//webapi.amap.com/ui/1.1/main.js"></script>
最后,就可以进行初始化地图了,注意需要在组件dom加载完毕才能进行初始化!其中this.map是地图实例,附上代码:
this.map = new AMap.Map('myMap', {
zoom: 10, //级别
center:[120.209758, 30.246809], //中心点坐标 默认在杭州
});
二、轨迹插件初始化
在地图初始化完成之后,可以引入一些需要的插件,这里就不再过多赘述,我们直接引入AMapUI,我们这里用到的是PathSimplifier模块,故只需要引入该模块即可,附上代码:
//加载PathSimplifier,loadUI的路径参数为模块名中 'ui/' 之后的部分
new AMapUI.load(['ui/misc/PathSimplifier'], PathSimplifier => {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas!');
return;
}
if (this.pathList?.length) {
//启动页面
this.initPage(PathSimplifier);
}
});
其中,涉及到的this.pathList是我这边后端返回坐标点信息,this.pathList结构如下:
this.pathList = [
[
120.79580028, // 经度
30.03570354 // 纬度
],
...
];
this.initPage方法如下,需要注意的是,方法内声明的content是轨迹播放时展示的车辆图标,如果不需要可以删掉,PathSimplifier中的配置请参照高德地图轨迹展示的开发文档,还有方法最后调用的this.cruiseInit方法已经放到下一部分了。
initPage(PathSimplifier) {
let content = PathSimplifier.Render.Canvas.getImageContent(
'/img/car1.png',
() => {
//图片加载成功,重新绘制一次
this.pathSimplifierIns.renderLater();
},
function onerror(e) {
this.$message({ type: 'error', message: '图片加载失败!' });
}
);
this.pathSimplifierIns = new PathSimplifier({
zIndex: 100,
map: this.map,
getPath: function (pathData, pathIndex) {
return pathData.path;
},
renderOptions: {
//轨迹线的样式
getPathStyle: (pathItem, zoom) => {
return {
pathLineStyle: {
strokeStyle: "red",
lineWidth: 6,
dirArrowStyle: true,
},
};
},
pathNavigatorStyle: {
initRotateDegree: 180,
width: 20,
height: 35,
autoRotate: true,
content,
},
},
});
this.cruiseInit(); //巡航器初始化
}
三、巡航器初始化
巡航器初始化方法this.cruiseInit代码如下:
cruiseInit() {
let pathSimplifierIns = [{ path: this.pathList, color: '#28F' }];
this.pathSimplifierIns.setData(pathSimplifierIns);
this.pointSum = 0;
pathSimplifierIns.forEach((item, index) => {
this.pointSum += item.path.length;
});
this.marksIndex = marksIndex;
this.cruiseStop();//如果已经存在巡航器,则停止播放
}
其中this.pointSum是为了记录巡航器的最终点数,方便对应到播放条的最大值。
四、巡航器的播放暂停等功能
巡航器的播放、暂停以及倍数的方法如下:
// 创建一个巡航器
createdCruise(index) {
// 判断是否传入index
let cruiseIndex;
if (index != undefined) {
cruiseIndex = index;
this.cruiseIndex = index;
} else {
cruiseIndex = this.cruiseIndex;
}
let cruise = this.pathSimplifierIns.createPathNavigator(
cruiseIndex, //关联第index条轨迹
{
loop: false, //循环播放
speed: this.speedList[this.speedValue].speed, //速度
}
);
if (this.cruise) {
// 清空走过的路线
this.cruise.destroy();
this.cruise = null;
}
return cruise;
},
// 开始播放
cruiseStart() {
this.isPlay = true;
if (this.cruise && !this.cruise.isCursorAtPathEnd() && !this.cruise.isCursorAtPathStart() && !this.isComplete) {
// 路段未开始并且没有结束的时候 暂停恢复动画 并且动画没有完成的时候
this.cruise.resume();
return;
}
this.isComplete = false;
if (this.cruiseIndex == 0) {
this.cruiseStop();
return;
}
this.cruise = this.createdCruise();
// 判断是否传入初始坐标
if (this.startPoint) {
this.cruise.start(this.startPoint);
this.startPoint = 0;
} else {
this.cruise.start();
}
this.cruise.on('move', e => {
let idx = this.cruise.cursor.idx;
let { address, gpsTime, speed } = this.pathList[idx];
let trackAddress = {
address,
gpsTime,
speed,
};
this.$emit('changeData', 'trackAddress', trackAddress);
let [min, max] = this.marksIndex[this.cruiseIndex];
this.sliderValue = idx + min;
});
// 巡航完成事触发
this.cruise.on('pause', () => {
if (this.cruise && this.cruise.isCursorAtPathEnd()) {
this.cruiseStart();
}
});
},
// 暂停播放
cruisePause() {
this.cruise.pause();
this.isPlay = false;
},
// 停止播放
cruiseStop() {
if (this.cruise) {
// 清空走过的路线
this.cruise.destroy();
}
// 停止播放
this.isPlay = false;
this.isComplete = true;
this.cruiseIndex = -1;
// 为重新播放准备
this.cruise = this.createdCruise();
this.cruiseIndex = -1;
this.sliderValue = 0;
},
// 速度改变
speedChange() {
if (this.speedValue == this.speedList.length - 1) {
this.speedValue = 0;
} else {
this.speedValue++;
}
this.cruise.setSpeed(this.speedList[this.speedValue].speed);
},
到这里巡航器的基础功能已经实现,还有一部分关于播放器调整对应轨迹改变,这里我们要用的监听器,即vue的watch属性:
watch: {
sliderValue(val) {
// 正在播放禁止拖拽播放器
if (!this.cruise || this.isPlay) return;
this.cruise.moveToPoint(val);
this.startPoint = val;
this.pathSimplifierIns.render();
},
},
五、变量声明以及HTML结构
其中使用到的变量有这些:
data() {
return {
// 地图实例
map: null,
cruise: null, //巡航器实例
cruiseIndex: -1, // 当前播放轨迹下标
pathSimplifierIns: null, //轨迹实例
isPlay: false, //是否播放
isComplete: true, //是否完成
pointSum: 0, //播放器总数
sliderValue: 0, //播放器当前数
startPoint: 0, //下次播放轨迹从当前值开始
marksIndex: {}, //每段路的起止坐标
pathList: [],// 轨迹坐标
speedValue: 3,// 当前播放速度下标
// 速度列表,可自定义配置
speedList: [
{ value: 0.5, speed: 100 },
{ value: 1, speed: 200 },
{ value: 2, speed: 400 },
{ value: 4, speed: 1600 },
{ value: 8, speed: 12800 },
{ value: 16, speed: 25600 },
],
};
},
HTML结构:
<template>
<div class="workTrack">
<div id="myMap"></div>
<div class="sliderBar" v-show="pathList.length">
<span @click="cruiseStart()" v-if="!isPlay">
<i class="el-icon-video-play"></i>
</span>
<span @click="cruisePause" v-else>
<i class="el-icon-video-pause"></i>
</span>
<span @click="cruiseStop">
<i class="el-icon-error"></i>
</span>
<el-slider :disabled="isPlay" v-model="sliderValue" :max="pointSum" :show-tooltip="false"></el-slider>
<b @click="speedChange">
<i class="el-icon-d-arrow-right"></i>
<span>×{{ speedList[speedValue].value }}</span>
</b>
</div>
</div>
</template>
css:
.workTrack {
width: 100%;
position: relative;
height: 100%;
#myMap {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
bottom: 30px;
user-select: none;
width: 100%;
padding: 10px 2%;
background-color: #00000064;
border-radius: 400px;
backdrop-filter: blur(5px);
z-index: 99;
width: 80%;
right: 0;
left: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
.el-slider {
flex: 1;
transform: translateY(1px);
margin: 0 15px;
}
::v-deep .el-slider__runway {
pointer-events: none;
background-color: #00000021;
margin: 0;
.el-slider__bar {
background-color: #1682e6;
}
.el-slider__stop {
background-color: #1682e6;
border-radius: 0;
width: 2px;
}
.el-slider__button-wrapper {
pointer-events: auto;
}
.el-slider__marks-text {
white-space: nowrap;
color: #fff;
font-size: 0;
}
}
> span {
flex-shrink: 0;
transform: translateY(1px);
color: #eee;
cursor: pointer;
margin: 0 5px;
transition: 0.3s;
font-size: 20px;
&:hover {
opacity: 0.5;
}
}
> b {
flex-shrink: 0;
color: #eee;
font-weight: normal;
margin: 0 5px;
cursor: pointer;
border-radius: 3px;
border: 1px solid #eee;
padding: 0px 10px;
transition: 0.3s;
user-select: none;
> span {
vertical-align: middle;
font-size: 14px;
display: inline-block;
transform: translateY(-2px);
}
i {
vertical-align: middle;
font-size: 16px;
display: inline-block;
transform: translateY(-1px);
}
&:hover {
opacity: 0.5;
}
}
}
}
六:完整代码
完整代码如下:
<!--
* @description 轨迹回放
* @fileName: track.vue
* @author: tan
* @date: 2024-06-17 10:02:28
!-->
<template>
<div class="workTrack">
<div id="myMap"></div>
<div class="sliderBar" v-show="pathList.length">
<span @click="cruiseStart()" v-if="!isPlay">
<i class="el-icon-video-play"></i>
</span>
<span @click="cruisePause" v-else>
<i class="el-icon-video-pause"></i>
</span>
<span @click="cruiseStop">
<i class="el-icon-error"></i>
</span>
<el-slider :disabled="isPlay" v-model="sliderValue" :max="pointSum" :show-tooltip="false"></el-slider>
<b @click="speedChange">
<i class="el-icon-d-arrow-right"></i>
<span>×{{ speedList[speedValue].value }}</span>
</b>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// 地图实例
map: null,
cruise: null, //巡航器实例
cruiseIndex: -1, // 当前播放轨迹下标
pathSimplifierIns: null, //轨迹实例
isPlay: false, //是否播放
isComplete: true, //是否完成
pointSum: 0, //播放器总数
sliderValue: 0, //播放器当前数
startPoint: 0, //下次播放轨迹从当前值开始
marksIndex: {}, //每段路的起止坐标
// 轨迹坐标
pathList: [
// [经度,纬度] 可再次放置测试数据
[120.79573938, 30.03576463],
],
speedValue: 3, // 当前播放速度下标
// 速度列表,可自定义配置
speedList: [
{ value: 0.5, speed: 100 },
{ value: 1, speed: 200 },
{ value: 2, speed: 400 },
{ value: 4, speed: 1600 },
{ value: 8, speed: 12800 },
{ value: 16, speed: 25600 },
],
};
},
mounted() {
this.map = new AMap.Map('myMap', {
zoom: 10, //级别
center: [120.209758, 30.246809], //中心点坐标 默认在杭州
});
this.$nextTick(() => {
this.loadMap();
});
},
methods: {
// 加载地图
loadMap() {
return new Promise((reslove, reject) => {
//加载PathSimplifier,loadUI的路径参数为模块名中 'ui/' 之后的部分
new AMapUI.load(['ui/misc/PathSimplifier'], PathSimplifier => {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas!');
return;
}
if (this.pathList?.length) {
//启动页面
this.initPage(PathSimplifier);
}
});
reslove();
});
},
initPage(PathSimplifier) {
let content = PathSimplifier.Render.Canvas.getImageContent(
'/img/car1.png',
() => {
//图片加载成功,重新绘制一次
this.pathSimplifierIns.renderLater();
},
function onerror(e) {
this.$message({ type: 'error', message: '图片加载失败!' });
}
);
this.pathSimplifierIns = new PathSimplifier({
zIndex: 100,
map: this.map,
getPath: function (pathData, pathIndex) {
return pathData.path;
},
renderOptions: {
//轨迹线的样式
getPathStyle: (pathItem, zoom) => {
return {
pathLineStyle: {
strokeStyle: 'red',
lineWidth: 6,
dirArrowStyle: true,
},
};
},
pathNavigatorStyle: {
initRotateDegree: 180,
width: 20,
height: 35,
autoRotate: true,
content,
},
},
});
this.cruiseInit();
},
// 巡航器初始化
cruiseInit() {
let pathSimplifierIns = [{ path: this.pathList, color: '#28F' }];
this.pathSimplifierIns.setData(pathSimplifierIns);
this.pointSum = 0;
let marksIndex = {};
pathSimplifierIns.forEach((item, index) => {
this.pointSum += item.path.length;
marksIndex[index] = [0, this.pointSum];
});
this.marksIndex = marksIndex;
this.cruiseStop();
},
// 创建一个巡航器
createdCruise(index) {
this.cruiseIndex++;
// 判断是否传入index
let cruiseIndex;
if (index != undefined) {
cruiseIndex = index;
this.cruiseIndex = index;
} else {
cruiseIndex = this.cruiseIndex;
}
let cruise = this.pathSimplifierIns.createPathNavigator(
cruiseIndex, //关联第index条轨迹
{
loop: false, //循环播放
speed: this.speedList[this.speedValue].speed, //速度
}
);
if (this.cruise) {
// 清空走过的路线
this.cruise.destroy();
this.cruise = null;
}
return cruise;
},
// 开始播放
cruiseStart() {
this.isPlay = true;
if (this.cruise && !this.cruise.isCursorAtPathEnd() && !this.cruise.isCursorAtPathStart() && !this.isComplete) {
// 路段未开始并且没有结束的时候 暂停恢复动画 并且动画没有完成的时候
this.cruise.resume();
return;
}
this.isComplete = false;
if (this.cruiseIndex == 0) {
this.cruiseStop();
return;
}
this.cruise = this.createdCruise();
// 判断是否传入初始坐标
if (this.startPoint) {
this.cruise.start(this.startPoint);
this.startPoint = 0;
} else {
this.cruise.start();
}
this.cruise.on('move', e => {
let idx = this.cruise.cursor.idx;
let { address, gpsTime, speed } = this.pathList[idx];
let trackAddress = {
address,
gpsTime,
speed,
};
this.$emit('changeData', 'trackAddress', trackAddress);
let [min, max] = this.marksIndex[this.cruiseIndex];
this.sliderValue = idx + min;
});
// 巡航完成事触发
this.cruise.on('pause', () => {
if (this.cruise && this.cruise.isCursorAtPathEnd()) {
this.cruiseStart();
}
});
},
// 暂停播放
cruisePause() {
this.cruise.pause();
this.isPlay = false;
},
// 停止
cruiseStop() {
if (this.cruise) {
// 清空走过的路线
this.cruise.destroy();
}
// 停止播放
this.isPlay = false;
this.isComplete = true;
this.cruiseIndex = -1;
// 为重新播放准备
this.cruise = this.createdCruise();
this.cruiseIndex = -1;
this.sliderValue = 0;
},
speedChange() {
if (this.speedValue == this.speedList.length - 1) {
this.speedValue = 0;
} else {
this.speedValue++;
}
this.cruise.setSpeed(this.speedList[this.speedValue].speed);
},
},
watch: {
sliderValue(val) {
// 正在播放禁止拖拽播放器
if (!this.cruise || this.isPlay) return;
this.cruise.moveToPoint(val);
this.startPoint = val;
this.pathSimplifierIns.render();
},
},
beforeDestroy() {
if (this.pathSimplifierIns) this.pathSimplifierIns.clearPathNavigators();
if (this.pathSimplifierIns) this.pathSimplifierIns.setData([]);
if (this.cruise) this.cruise.destroy();
if (this.map) this.map.destroy();
},
};
</script>
<style lang="scss" scoped>
.workTrack {
width: 100%;
position: relative;
height: 100%;
#myMap {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
bottom: 30px;
user-select: none;
width: 100%;
padding: 10px 2%;
background-color: #00000064;
border-radius: 400px;
backdrop-filter: blur(5px);
z-index: 99;
width: 80%;
right: 0;
left: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
.el-slider {
flex: 1;
transform: translateY(1px);
margin: 0 15px;
}
::v-deep .el-slider__runway {
pointer-events: none;
background-color: #00000021;
margin: 0;
.el-slider__bar {
background-color: #1682e6;
}
.el-slider__stop {
background-color: #1682e6;
border-radius: 0;
width: 2px;
}
.el-slider__button-wrapper {
pointer-events: auto;
}
.el-slider__marks-text {
white-space: nowrap;
color: #fff;
font-size: 0;
}
}
> span {
flex-shrink: 0;
transform: translateY(1px);
color: #eee;
cursor: pointer;
margin: 0 5px;
transition: 0.3s;
font-size: 20px;
&:hover {
opacity: 0.5;
}
}
> b {
flex-shrink: 0;
color: #eee;
font-weight: normal;
margin: 0 5px;
cursor: pointer;
border-radius: 3px;
border: 1px solid #eee;
padding: 0px 10px;
transition: 0.3s;
user-select: none;
> span {
vertical-align: middle;
font-size: 14px;
display: inline-block;
transform: translateY(-2px);
}
i {
vertical-align: middle;
font-size: 16px;
display: inline-block;
transform: translateY(-1px);
}
&:hover {
opacity: 0.5;
}
}
}
}
</style>