基于vue+百度地图的多车实时运动及轨迹追踪实现(心路历程篇)

2022-05-13 10:00:00
family2hu
转贴:
稀土掘金
4734
摘要:基于vue+百度地图的多车实时运动及轨迹追踪实现,共分为上下两篇,分别为上篇“心路历程篇”和下篇“上帝视角篇”,上篇是背景介绍和我实现过程中走的弯路,下篇是最终版的实现方案。其实并不存在上帝视角,只希望有一天,我们可以通过不断复盘,少走点弯路。
开局两张图,剩下全靠吹

auto-baidu

auto-baidu-2

基于vue+百度地图的多车实时运动及轨迹追踪实现,共分为上下两篇,分别为上篇“心路历程篇”和下篇“上帝视角篇”,上篇是背景介绍和我实现过程中走的弯路,下篇是最终版的实现方案。其实并不存在上帝视角,只希望有一天,我们可以通过不断复盘,少走点弯路。

本篇是上篇,心路历程篇。

项目背景

此处省略一万字,交给产品经理。到我这边接到的需求其实就是实现车辆实时移动,并进行轨迹追踪。 不,其实我接到的是一份队友跑路待整改的代码! 看我如何把图一变成图二的效果,嘻嘻【请忽略我图二录屏时右下角不小心保留的浮层图】

老代码思路

看了一眼这个素未谋面的队友的代码,其实他原来代码结构还是比较清晰的。

可以保留的设计

  • websocket方式接收数据。因为车辆数据会源源不断产生,前端定时拉取不可取,所以采用了websocket方式接收数据。
  • 通过id标记一个车的覆盖物(marker和label),方便后面删除
  • 嗯,我努力想想,一定会有3的

需要改进的点

  • 车辆是跳动前进的,很不真实
  • 背景过于朴素

需要增加的功能

  • 车辆需要显示实时的速度
  • 需要加一个列表,实时更新当前在跑的车辆信息,限制显示前10辆车
  • 增加轨迹线功能,实时追踪该车辆的前进轨迹

车辆跳动的原因

原来的实现思路是:在每次接收数据的时候,清空该车所画的覆盖物(车和label),并用新的经纬度数据重新画覆盖物,车辆是从第一点直接到了另一个点,所以看起来就像是跳过去的。

改造步骤

以下是项目过程中的一些最终被抛弃的思路,想直接看最终方案的,请移步上帝视角篇

1. 重画=》设置新位置

我的第一反应是移动位置总比重新画要快吧,所以首先从改造数据结构入手,把后端发过来的轨迹消息按照车辆归类记录下来,设置初始状态为'undraw',并在每次来一个新消息的时候,拿所有状态为'undraw'的点去移动,移动开始前把状态设为'drawing', 移动结束后把状态设为'drawed'(此处先埋一个坑,坑1)。

自认为看起来很完美,撸完后发现,设置车辆位置的时候,经常提示这个车的覆盖物还不存在。可是我明明是画完车再移动它的位置的呀,难道画车这个步骤的异步的?(此时的我还傻傻的忽视了那个大大的坐标转换函数,sigh)

异步就异步吧,再加两个状态!如果是这辆车第一个点,则画之前标记为'marking',画完后标记为'marked',只有当第一个点的状态为'marked'后,才进行后面的移动。终于不报错了,但我期待的效果是半点都没有,似乎还更槽糕了。(继续埋坑,坑2

2. 平滑效果

开始全网搜如何让车辆平滑移动:

a ) 百度地图自有的轨迹动画api参考4,更适用于已知整个轨迹,并在指定的时间内回放完成。


b) 前辈写的基于百度地图的多图标平滑移动方案参考2,主要思路是补点,根据两个点之间距离来计算要补多少个点,并用setInterval去定时移动到下一个点。但其中用到的计算距离的函数适用于百度地图jsapi v2版本,我们用的是百度地图js webgl v1版本,要注意一下不能直接使用。


c) 其他的基本上也是补点的思路,就是计算距离的函数不太一样,如参考3,是从其他文章里了解到的turfjs包,里面有很多跟地图和距离相关的工具函数,这里记录一下,以后可能用的到。

到这里基本确定使用补点的思路。最开始因为方案b)不能直接使用,采用了方案c)里的函数,发现车辆几乎都没动(其实是车已经飞走了,年少无知的我以为车没动),就把距离打印出来看了一下,看到两点之间的距离是0.00099km,想着莫非太近了,所以看起来不动?又或者是这个库的距离算起来不准,不死心地跑去百度那边试了一下,虽然大了那么一丢丢,但绝对值还是很小。

var from = turf.point([113.27720709322817, 23.351992192427748]);
var to = turf.point([113.2772194870973, 23.352001006312186]);
var options = {units: 'miles'};

turf.distance(from, to, options);
0.0009944611081986045

这时打算先取个巧,不计算距离了,自己先固定一个分割的点数看看效果。观察后端发过来的消息,大概每个车每秒有2条数据,按照60fps来算,设置了两点之间共分成30个点来画,又信心满满地试了一把。

这时发现了一个异常,移动的时候,从一跳一跳变成了一顿一顿,这时眼瞎的我终于发现了那个不起眼的百度坐标转换函数,它不仅要调用百度地图api来拿到转换后的结果,而且限制了每次最多只能转换10个点,而我家的破网为了让我按时下班,一到晚上就卡得不行,所以这个问题被无限放大了。敢情我瞎忙活了半天,瓶颈根本不在画图上,而在调用api上。这时坑2的问题得到了解释, 异步的原因不在于画图,而在于调用api!那就先用10个点凑合一下吧,总比没有好……

3. 换皮

随着时间一分一秒地过去,我内心还是比较焦虑的,想着我改的这破玩意儿没法交差啊,为了欺骗一下自己和产品,打算先做一下改进点2,换个背景。这时的我不知道这个专业术语叫做卫星图,又是一顿全网猛搜,找到了相关的设置,其实就一句话的事。

bMap.setMapType(BMAP_EARTH_MAP)

嗯,看起来像两天工作量的样子了。【想到了换皮不换内核的浏览器们,手动狗头】

4. 本地模拟websocket数据

这时已经周五下午了,据我前两天观察,后端服务一到晚上就会关掉,这如何满足我想周末加班的欲望?不就是发个数据嘛,我也会。

步骤1:利用参考1的方案,收集了一段实时数据,保存成har文件。

步骤2:搜了下如何打开har文件,发现它本质是个json,那就好办了,把后缀改成json,观察数据。

步骤3:用websocket关键词搜索,搜到了有且仅有一个_webSocketMessages这个字段,我关心的数据都在里面。

步骤4: 提取所有接收的数据,即类型为'receive'的数据,存成json文件。
const fs = require('fs')
const path = require('path')

const input = process.argv[2]
const fullname = input.split('/').slice(-1)[0]
const filename = fullname.substring(0, fullname.lastIndexOf('.')) 
let objArr = JSON.parse(fs.readFileSync(input, 'utf8')).log.entries

const websocketReqs = objArr.filter(o => { return o._webSocketMessages && o._webSocketMessages.length > 0})
const receivedMsgs = websocketReqs.length > 0 && websocketReqs[0]._webSocketMessages.filter(item => item.type === 'receive')

if (receivedMsgs && receivedMsgs.length > 0) {
    try {
        fs.writeFileSync(path.resolve(__dirname, `./${filename}.json`), JSON.stringify(receivedMsgs, null,"\t"))
    } catch(e) {
        console.log(e)
    }
}
json文件的格式如下:
[
 {
 "type": "receive",
 "time": 1648170807.1585,
 "opcode": 1,
 "data": "realdata1"
 },
 {
 "type": "receive",
 "time": 1648170807.329674,
 "opcode": 1,
 "data": "realdata2"
 }
]
步骤5: 用nodejs启一个最简单的websocket后端服务,读取json文件,按照数据中的time字段作为时间间隔进行数据回放。
const realtimeTraces = JSON.parse(fs.readFileSync('./已提取的jons文件.json', 'utf8'));
const server = ws.createServer((connect) => {
  console.log(`用户链接上来了`);
  // 用户传递过来的数据,text事件就会被触发
  connect.on("text", (data) => {
    console.log(`用户传来的数据${data}`);
  });
  // 当连接断开时,就会执行这个事件 注册close事件就要注册下面的error事件
  connect.on("close", () => {
    console.log(`链接断开了`);
  });

  // 注册一个error事件,处理用户的错误信息
  connect.on("error", () => {
    console.log(`用户链接异常`);
  });

// send realtime trace
let baseTime
for(let i=0; i<realtimeTraces.length; i++) {
    const item = realtimeTraces[i]
    if (i === 0) {
        baseTime = item.time
    }
    setTimeout(() => {
        connect.send(typeof item.data === 'string' ? item.data : JSON.stringify(item.data));
      }, (item.time - baseTime)*1000);
  }
  
});
const PORT = 7777;
server.listen(PORT, () => {
  console.log(`服务启动成功,端口号${PORT}`);
});

5. 修改绘图的触发时机

准备好后端数据后,又可以开心地研究前端实现了。(当我一遍一遍回放这段数据的时候,我想到了《开端》里的循环……)

这时我发现一个诡异的现象,车咋一瞬间铺满了屏幕,像极了当年windows中毒的感觉(不小心暴露了年龄)。作为一个密集恐惧症的我,立马关掉了页面,思考起了人生,哦不,思考起了原因。

之前的逻辑是每次收到消息的时候,去触发绘图【包括新增和移动】动作,但如果一下子涌来大量消息,就会在屏幕上堆满车(后来发现其实这个问题被放大了1000倍,因为我在分发数据的时候,setTimeout的时间忘记乘以1000了……)。

既然是接收消息的速度跟消费消息的速度不匹配,此处应该来一个消息队列。哦,对,我是前端,此处应该来一个数组,当接收消息的时候,把车的信息先缓存起来,再找机会去消费消息(埋坑3,坑3)。

哼哧哼哧改了一通,把接收消息和消费消息的函数分别写好了,消费消息那里会遍历所有车,并根据车的绘制状态进入自循环,然后我痛苦地发现我找不到一个合适的第一推动力(上帝应该不会帮我),又hack了一把,在收消息时对车计个数,当车的数量为1时,触发消费消息,作为函数调用的入口。

不管怎么样,车好歹动了起来。

6. 车辆消失后从地图上移除

开心了不到2秒,车又堆满了屏幕。

哦,该死,我只是不停地在增加车,却没有在车辆消失时把车移走。

那么问题来了,怎么判定车辆消失呢?

又观察了一下后端数据,发现很多消息是空的, 我就自己拍了个板,N条消息后如果还是没有这辆车过来,就判定它消失了。但当我不断把N调大,发现效果还是很差后,我只好承认这个算法不行。 又想了一个很损的方法,反正车在高速上,所观察的路段又很短,先假设先进先出吧,当车超过N辆后,把最先来的那辆车移除掉。

嗯,一顿操作后,我终于能正视屏幕了。

其实此时的我内心慌得一逼,已经处于代码能不能跑起来完全听天由命的状态。

7. 向大佬求助

到了周五下班的点,跟组长老实汇报了一下工作,感觉项目要失控,正常的前端听我描述的第一反应都是让后端把数据处理好给我,我只负责展示就可以了。要展示10条数据,就让后端返回10条数据,我也不用去判断车辆是不是消失,也不用自己去缓存各种信息。总之就是不用管以前的消息格式是怎么样的,只要我想好我要什么数据,自己mock好数据做好demo,让后端去适配就可以了。

我一边想果然是大佬,思路就是不一样;一边心存疑虑,想着我不知道车啥时候消失,后端拿到的数据跟我一样,他怎么会知道。

8. DIFF算法

不管怎么样,按照大佬的思路搞一波吧。先模拟了第一条10车数据的消息,开心地接收好。等模拟第二条消息的时候,发现前端这边无法一股脑替换,还是因为数据的速度跟绘制的速度并不匹配,假设直接替换,那没画完的车,那些数据就再也没有机会画上去了。

能把所有简单问题搞复杂的我此时还不死心,想到了大名鼎鼎的diff算法,根据前后两次消息进行diff,还煞有介事先写好注释。
// 新旧list对比,根据不同情况进行不同的操作
// 若新的有,旧的没有,则插入,tag记为'PLACEMENT'
// 若旧的有,新的没有,则删除,tag记为'DELETION'
// 若两者都有,则更新,tag记为'UPDATE'
对所有数据打完tag要执行的时候,又遇到了之前那个问题,移动车辆的前提是创建好了车辆,而创建车辆是异步的,我还是需要记录状态,那就没法用后端直接返回的数据了,这个方案宣告失败。

9. 从一个车的视角,把问题简单化

当我把所有车放一起思考的时候,其实问题很难定位。这时我才意识到,为什么我不从一个车开始。

对于一个车,其实需求很明确:
  • 接收消息
  • 画车
  • 移动车
  • 移除车
这时思路突然清晰了起来,每一个步骤都可以独立实现。
  • 后端发消息时,进行消息的接收。
  • 接收消息时判定是否为新车,如果是新车,就画车。
  • 画完车后,如果还存在未画的点,就去移动车。每次拿出两个点,作为起始点和终止点,通过补点算法去移动。
  • 如果一直没有(预设3秒)未画的点了,就判定车辆消失,移除车。实现的时候是每次画新的点后重新倒计时。
突然发现,这个思路再也没有原来的烦恼,既不用思考第一推动力的问题(坑3),又不用为了知道啥时候可以移动车而给第一个点添加'marking'和'marked'状态(坑2),并且车辆消失算法也不再是薛定谔的猫了,可以根据实际采集的数据频率动态调整。而且原来是拿一批点(坑1)去操作,现在每次拿两个点,表现也更稳定了.

至此大部分弯路已走完,后面基于vue+百度地图的多车实时运动及轨迹追踪实现(上帝视角篇)开启真正的技术分享。

参考文献

  1. 利用chrome保存和查看网络请求
  2. 百度多图标平滑移动
  3. 多线段平滑移动
  4. 轨迹动画
发表评论
评论通过审核后显示。
联系我们
  • 联系人:阿道
  • 联系方式:17762006160
  • 地址:青岛市黄岛区长江西路118号青铁广场18楼