高德地图轨迹回放/轨迹播放

前言

本篇文章主要介绍高德地图的轨迹回放或播放的实现过程,是基于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-el