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;
}