Implémentation du suivi de mouvement et de trajectoire multi-véhicules en temps réel basé sur la carte vue+Baidu (voyage mental)

Il y a deux images au début, et le reste dépend du soufflage

origine.gif

202203231421Lecture.gif

La réalisation du suivi de mouvement et de trajectoire en temps réel multi-véhicules basé sur la carte vue+Baidu est divisée en deux parties : la première partie "Voyage mental" et la partie suivante "La perspective de Dieu". La dernière partie est l'introduction de fond et mon promenade dans le processus de réalisation Le détour, la partie suivante est la mise en place de la version finale. En fait, il n'y a pas de perspective de Dieu, j'espère juste qu'un jour, nous pourrons faire moins de détours grâce à une révision continue.

Cet article est le premier article, l'article du voyage mental.

Contexte du projet

10 000 mots sont omis ici et remis au chef de produit. La demande que j'ai reçue de ma part est en fait de réaliser le mouvement en temps réel du véhicule et de suivre la trajectoire. Non, en fait, ce que j'ai reçu était un code que mes coéquipiers ont dû rectifier ! Voyez comment je peux transformer l'image 1 en effet de l'image 2, hehe [veuillez ignorer l'image de la couche flottante que j'ai accidentellement conservée dans le coin inférieur droit de l'enregistrement d'écran de l'image 2]

vieilles idées de code

En regardant le code de ce coéquipier qui ne s'était jamais rencontré, en fait, sa structure de code d'origine était relativement claire.

Modèles pouvant être conservés :

  1. Recevez des données en mode websocket. Étant donné que les données du véhicule seront générées en continu et qu'il n'est pas conseillé d'extraire régulièrement le frontal, la méthode Websocket est donc utilisée pour recevoir les données.
  2. Marquez la couverture (marqueur et étiquette) d'une voiture par identifiant, ce qui est pratique pour une suppression ultérieure
  3. Eh bien, j'essaie d'y penser, il doit y avoir 3

points à améliorer

  1. Le véhicule bondit en avant, très irréel
  2. fond trop uni

Fonctionnalités à ajouter

  1. Le véhicule doit afficher la vitesse en temps réel
  2. Une liste doit être ajoutée pour mettre à jour en temps réel les informations sur les véhicules en cours d'exécution, et l'affichage des 10 premiers véhicules est limité
  3. Ajouter la fonction de ligne de trajectoire pour suivre la trajectoire avant du véhicule en temps réel

La raison du saut du véhicule

L'idée d'origine de la mise en œuvre est la suivante : chaque fois que les données sont reçues, effacez le revêtement (voiture et étiquette) dessiné par la voiture et redessinez le revêtement avec les nouvelles données de latitude et de longitude, le véhicule est directement du premier point à un autre point, Donc on dirait que ça saute.

Étapes de rénovation

Voici quelques idées qui ont finalement été abandonnées au cours du projet. Si vous voulez voir le plan final directement, veuillez passer à la perspective de Dieu.

1. Redessiner = "définir une nouvelle 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'
复制代码

Lorsque j'ai fini de baliser toutes les données à exécuter, j'ai de nouveau rencontré le problème précédent. Le principe du déplacement du véhicule est de créer le véhicule, et la création du véhicule est asynchrone. Je dois encore enregistrer l'état, afin que je puisse n'utilisez pas le backend pour renvoyer directement les données, le plan a échoué.

9. Simplifiez le problème du point de vue d'une voiture

Quand je pense à toutes les voitures ensemble, c'est vraiment difficile de localiser le problème. C'est là que j'ai compris pourquoi je n'avais pas commencé avec une voiture.

Pour une voiture, en effet, les exigences sont très claires :

  1. recevoir un message
  2. voiture de dessin
  3. voiture mobile
  4. retirer la voiture

À cette époque, l'idée est soudainement devenue claire et chaque étape pouvait être mise en œuvre indépendamment.

  1. Lorsque le backend envoie un message, il reçoit le message.
  2. Lors de la réception du message, il est déterminé s'il s'agit d'une nouvelle voiture, et s'il s'agit d'une nouvelle voiture, la voiture est dessinée.
  3. Après avoir dessiné la voiture, s'il y a encore des points non peints, déplacez la voiture. Retirez deux points à la fois comme point de départ et point d'arrivée, et parcourez l'algorithme du point complémentaire.
  4. S'il n'y a toujours pas de point non peint (préréglé 3 secondes), il est déterminé que le véhicule disparaît et le véhicule est retiré. Lorsqu'il est implémenté, le compte à rebours est redémarré à chaque fois qu'un nouveau point est tracé.

Du coup trouvé que cette idée n'a plus les problèmes d'origine, ni besoin de réfléchir au problème de la première force motrice (fosse 3), ni d'ajouter les états 'marquant' et 'marqué' au premier point afin de savoir quand le la voiture peut être déplacée (Pit 2), et l'algorithme de disparition du véhicule n'est plus le chat de Schrödinger, et peut être ajusté dynamiquement en fonction de la fréquence des données réellement collectées. De plus, il fallait auparavant un lot de points (pit 1) pour fonctionner, mais maintenant je prends deux points à chaque fois, et les performances sont plus stables.

Jusqu'à présent, la plupart des détours ont été effectués et la réalisation d'un suivi de mouvement et de trajectoire multi-véhicules en temps réel basé sur la carte vue + Baidu (God's Perspective) ouvrira un véritable partage de technologie.

les références

1. Utilisez chrome pour enregistrer et afficher les demandes de réseau

2. Les multi-icônes Baidu se déplacent en douceur

3. Mouvement fluide des polylignes

4. Animation de trajectoire

Je suppose que tu aimes

Origine juejin.im/post/7079366814752309278
conseillé
Classement