Real-time multi-vehicle motion and trajectory tracking implementation based on vue+Baidu map (mental journey)

There are two pictures at the beginning, and the rest depends on blowing

origin.gif

202203231421Playback.gif

The realization of multi-vehicle real-time motion and trajectory tracking based on vue+Baidu map is divided into two parts: the first part "Mental Journey" and the next part "God's Perspective". The last part is the background introduction and my walk in the process of realization The detour, the next part is the implementation of the final version. In fact, there is no God's perspective. I just hope that one day, we can make less detours through continuous review.

This article is the first article, the mental journey article.

Background of the project

10,000 words are omitted here and handed over to the product manager. The demand I received from my side is actually to realize the real-time movement of the vehicle and track the trajectory. No, in fact, what I received was a code that my teammates had to rectify! See how I can turn picture 1 into the effect of picture 2, hehe [please ignore the floating layer picture that I accidentally retained in the lower right corner of the screen recording of picture 2]

old code ideas

Taking a look at the code of this teammate who had never met, in fact, his original code structure was relatively clear.

Designs that can be kept:

  1. Receive data in websocket mode. Because the vehicle data will be continuously generated, and it is not advisable to pull the front-end regularly, so the websocket method is used to receive the data.
  2. Mark the covering (marker and label) of a car by id, which is convenient for later deletion
  3. Well, I try to think about it, there must be 3

points for improvement

  1. The vehicle is jumping forward, very unreal
  2. too plain background

Features that need to be added

  1. The vehicle needs to display real-time speed
  2. A list needs to be added to update the current running vehicle information in real time, and the display of the first 10 vehicles is limited
  3. Add the function of trajectory line to track the forward trajectory of the vehicle in real time

The reason for the jumping of the vehicle

The original implementation idea is: every time the data is received, clear the covering (car and label) drawn by the car, and redraw the covering with the new latitude and longitude data, the vehicle is directly from the first point to another point, So it looks like it's jumping over.

Retrofit steps

The following are some ideas that were finally abandoned in the process of the project. If you want to see the final plan directly, please move to God's Perspective.

1. Redraw = "set new position"

我的第一反应是移动位置总比重新画要快吧,所以首先从改造数据结构入手,把后端发过来的轨迹消息按照车辆归类记录下来,设置初始状态为'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'
复制代码

When I finished tagging all the data to be executed, I encountered the previous problem again. The premise of moving the vehicle is to create the vehicle, and the creation of the vehicle is asynchronous. I still need to record the status, so I can't use the backend to return directly. data, the plan failed.

9. Simplify the problem from the perspective of a car

When I think about all the cars together, it's really hard to locate the problem. That's when I realized why I didn't start with a car.

For a car, in fact, the requirements are very clear:

  1. receive message
  2. drawing car
  3. mobile car
  4. remove car

At this time, the idea suddenly became clear, and each step could be implemented independently.

  1. When the backend sends a message, it receives the message.
  2. When receiving the message, it is determined whether it is a new car, and if it is a new car, the car is drawn.
  3. After drawing the car, if there are still unpainted points, move the car. Take out two points at a time as the starting point and the ending point, and move through the complement point algorithm.
  4. If there is still no (preset 3 seconds) unpainted point, it is determined that the vehicle disappears and the vehicle is removed. When implemented, the countdown is restarted every time a new point is drawn.

Suddenly found that this idea no longer has the original troubles, neither need to think about the problem of the first driving force (pit 3), nor add the 'marking' and 'marked' states to the first point in order to know when the car can be moved (Pit 2), and the vehicle disappearance algorithm is no longer Schrödinger's cat, and can be dynamically adjusted according to the frequency of the actual collected data. Moreover, it used to take a batch of points (pit 1) to operate, but now I take two points each time, and the performance is more stable.

So far, most of the detours have been completed, and the realization of multi-vehicle real-time motion and trajectory tracking based on vue+Baidu map (God's Perspective) will open up real technology sharing.

references

1. Use chrome to save and view network requests

2. Baidu multi-icons move smoothly

3. Smooth movement of polylines

4. Trajectory animation

Guess you like

Origin juejin.im/post/7079366814752309278