基于 Vue + 百度地圖的多車實(shí)時(shí)運(yùn)動(dòng)及軌跡追蹤實(shí)現(xiàn)
點(diǎn)擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
開局兩張圖,剩下全靠吹


基于vue+百度地圖的多車實(shí)時(shí)運(yùn)動(dòng)及軌跡追蹤實(shí)現(xiàn),共分為上下兩篇,分別為上篇“心路歷程篇”和下篇“上帝視角篇”,上篇是背景介紹和我實(shí)現(xiàn)過程中走的彎路,下篇是最終版的實(shí)現(xiàn)方案。其實(shí)并不存在上帝視角,只希望有一天,我們可以通過不斷復(fù)盤,少走點(diǎn)彎路。
本篇是上篇,心路歷程篇。
項(xiàng)目背景
此處省略一萬字,交給產(chǎn)品經(jīng)理。到我這邊接到的需求其實(shí)就是實(shí)現(xiàn)車輛實(shí)時(shí)移動(dòng),并進(jìn)行軌跡追蹤。不,其實(shí)我接到的是一份隊(duì)友跑路待整改的代碼! 看我如何把圖一變成圖二的效果,嘻嘻【請忽略我圖二錄屏?xí)r右下角不小心保留的浮層圖】
老代碼思路
看了一眼這個(gè)素未謀面的隊(duì)友的代碼,其實(shí)他原來代碼結(jié)構(gòu)還是比較清晰的。
可以保留的設(shè)計(jì):
websocket方式接收數(shù)據(jù)。因?yàn)檐囕v數(shù)據(jù)會源源不斷產(chǎn)生,前端定時(shí)拉取不可取,所以采用了websocket方式接收數(shù)據(jù)。 通過id標(biāo)記一個(gè)車的覆蓋物(marker和label),方便后面刪除 嗯,我努力想想,一定會有3的
需要改進(jìn)的點(diǎn)
車輛是跳動(dòng)前進(jìn)的,很不真實(shí) 背景過于樸素
需要增加的功能
車輛需要顯示實(shí)時(shí)的速度 需要加一個(gè)列表,實(shí)時(shí)更新當(dāng)前在跑的車輛信息,限制顯示前10輛車 增加軌跡線功能,實(shí)時(shí)追蹤該車輛的前進(jìn)軌跡
車輛跳動(dòng)的原因
原來的實(shí)現(xiàn)思路是:在每次接收數(shù)據(jù)的時(shí)候,清空該車所畫的覆蓋物(車和label),并用新的經(jīng)緯度數(shù)據(jù)重新畫覆蓋物,車輛是從第一點(diǎn)直接到了另一個(gè)點(diǎn),所以看起來就像是跳過去的。
改造步驟
以下是項(xiàng)目過程中的一些最終被拋棄的思路,想直接看最終方案的,請移步上帝視角篇[1]
1. 重畫=》設(shè)置新位置
我的第一反應(yīng)是移動(dòng)位置總比重新畫要快吧,所以首先從改造數(shù)據(jù)結(jié)構(gòu)入手,把后端發(fā)過來的軌跡消息按照車輛歸類記錄下來,設(shè)置初始狀態(tài)為'undraw',并在每次來一個(gè)新消息的時(shí)候,拿所有狀態(tài)為'undraw'的點(diǎn)去移動(dòng),移動(dòng)開始前把狀態(tài)設(shè)為'drawing', 移動(dòng)結(jié)束后把狀態(tài)設(shè)為'drawed'(此處先埋一個(gè)坑,坑1)。
自認(rèn)為看起來很完美,擼完后發(fā)現(xiàn),設(shè)置車輛位置的時(shí)候,經(jīng)常提示這個(gè)車的覆蓋物還不存在。可是我明明是畫完車再移動(dòng)它的位置的呀,難道畫車這個(gè)步驟的異步的?(此時(shí)的我還傻傻的忽視了那個(gè)大大的坐標(biāo)轉(zhuǎn)換函數(shù),sigh)
異步就異步吧,再加兩個(gè)狀態(tài)!如果是這輛車第一個(gè)點(diǎn),則畫之前標(biāo)記為'marking',畫完后標(biāo)記為'marked',只有當(dāng)?shù)谝粋€(gè)點(diǎn)的狀態(tài)為'marked'后,才進(jìn)行后面的移動(dòng)。終于不報(bào)錯(cuò)了,但我期待的效果是半點(diǎn)都沒有,似乎還更槽糕了。(繼續(xù)埋坑,坑2)
2. 平滑效果
開始全網(wǎng)搜如何讓車輛平滑移動(dòng):
a ) 百度地圖自有的軌跡動(dòng)畫api參考4[2],更適用于已知整個(gè)軌跡,并在指定的時(shí)間內(nèi)回放完成。
b) 前輩寫的基于百度地圖的多圖標(biāo)平滑移動(dòng)方案參考2[3],主要思路是補(bǔ)點(diǎn),根據(jù)兩個(gè)點(diǎn)之間距離來計(jì)算要補(bǔ)多少個(gè)點(diǎn),并用setInterval去定時(shí)移動(dòng)到下一個(gè)點(diǎn)。但其中用到的計(jì)算距離的函數(shù)適用于百度地圖jsapi v2版本,我們用的是百度地圖js webgl v1版本,要注意一下不能直接使用。
c) 其他的基本上也是補(bǔ)點(diǎn)的思路,就是計(jì)算距離的函數(shù)不太一樣,如參考3[4],是從其他文章里了解到的turfjs包,里面有很多跟地圖和距離相關(guān)的工具函數(shù),這里記錄一下,以后可能用的到。
到這里基本確定使用補(bǔ)點(diǎn)的思路。最開始因?yàn)榉桨竍)不能直接使用,采用了方案c)里的函數(shù),發(fā)現(xiàn)車輛幾乎都沒動(dòng)(其實(shí)是車已經(jīng)飛走了,年少無知的我以為車沒動(dòng)),就把距離打印出來看了一下,看到兩點(diǎn)之間的距離是0.00099km,想著莫非太近了,所以看起來不動(dòng)?又或者是這個(gè)庫的距離算起來不準(zhǔn),不死心地跑去百度那邊試了一下,雖然大了那么一丟丟,但絕對值還是很小。
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
復(fù)制代碼
這時(shí)打算先取個(gè)巧,不計(jì)算距離了,自己先固定一個(gè)分割的點(diǎn)數(shù)看看效果。觀察后端發(fā)過來的消息,大概每個(gè)車每秒有2條數(shù)據(jù),按照60fps來算,設(shè)置了兩點(diǎn)之間共分成30個(gè)點(diǎn)來畫,又信心滿滿地試了一把。
這時(shí)發(fā)現(xiàn)了一個(gè)異常,移動(dòng)的時(shí)候,從一跳一跳變成了一頓一頓,這時(shí)眼瞎的我終于發(fā)現(xiàn)了那個(gè)不起眼的百度坐標(biāo)轉(zhuǎn)換函數(shù),它不僅要調(diào)用百度地圖api來拿到轉(zhuǎn)換后的結(jié)果,而且限制了每次最多只能轉(zhuǎn)換10個(gè)點(diǎn),而我家的破網(wǎng)為了讓我按時(shí)下班,一到晚上就卡得不行,所以這個(gè)問題被無限放大了。敢情我瞎忙活了半天,瓶頸根本不在畫圖上,而在調(diào)用api上。這時(shí)坑2的問題得到了解釋, 異步的原因不在于畫圖,而在于調(diào)用api!那就先用10個(gè)點(diǎn)湊合一下吧,總比沒有好……
3. 換皮
隨著時(shí)間一分一秒地過去,我內(nèi)心還是比較焦慮的,想著我改的這破玩意兒沒法交差啊,為了欺騙一下自己和產(chǎn)品,打算先做一下改進(jìn)點(diǎn)2,換個(gè)背景。這時(shí)的我不知道這個(gè)專業(yè)術(shù)語叫做衛(wèi)星圖,又是一頓全網(wǎng)猛搜,找到了相關(guān)的設(shè)置,其實(shí)就一句話的事。
bMap.setMapType(BMAP_EARTH_MAP)
嗯,看起來像兩天工作量的樣子了?!鞠氲搅藫Q皮不換內(nèi)核的瀏覽器們,手動(dòng)狗頭】
4. 本地模擬websocket數(shù)據(jù)
這時(shí)已經(jīng)周五下午了,據(jù)我前兩天觀察,后端服務(wù)一到晚上就會關(guān)掉,這如何滿足我想周末加班的欲望?不就是發(fā)個(gè)數(shù)據(jù)嘛,我也會。
步驟1:利用參考1[5]的方案,收集了一段實(shí)時(shí)數(shù)據(jù),保存成har文件。
步驟2:搜了下如何打開har文件,發(fā)現(xiàn)它本質(zhì)是個(gè)json,那就好辦了,把后綴改成json,觀察數(shù)據(jù)。
步驟3:用websocket關(guān)鍵詞搜索,搜到了有且僅有一個(gè)_webSocketMessages這個(gè)字段,我關(guān)心的數(shù)據(jù)都在里面。
步驟4:提取所有接收的數(shù)據(jù),即類型為'receive'的數(shù)據(jù),存成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)
????}
}
復(fù)制代碼
json文件的格式如下:
[
?{
??"type":?"receive",
??"time":?1648170807.1585,
??"opcode":?1,
??"data":?"realdata1"
?},
?{
??"type":?"receive",
??"time":?1648170807.329674,
??"opcode":?1,
??"data":?"realdata2"
?}
]
復(fù)制代碼
步驟5:用nodejs啟一個(gè)最簡單的websocket后端服務(wù),讀取json文件,按照數(shù)據(jù)中的time字段作為時(shí)間間隔進(jìn)行數(shù)據(jù)回放。
const?realtimeTraces?=?JSON.parse(fs.readFileSync('./已提取的jons文件.json',?'utf8'));
const?server?=?ws.createServer((connect)?=>?{
??console.log(`用戶鏈接上來了`);
??//?用戶傳遞過來的數(shù)據(jù),text事件就會被觸發(fā)
??connect.on("text",?(data)?=>?{
????console.log(`用戶傳來的數(shù)據(jù)${data}`);
??});
??//?當(dāng)連接斷開時(shí),就會執(zhí)行這個(gè)事件?注冊close事件就要注冊下面的error事件
??connect.on("close",?()?=>?{
????console.log(`鏈接斷開了`);
??});
??//?注冊一個(gè)error事件,處理用戶的錯(cuò)誤信息
??connect.on("error",?()?=>?{
????console.log(`用戶鏈接異常`);
??});
//?send?realtime?trace
let?baseTime
for(let?i=0;?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(`服務(wù)啟動(dòng)成功,端口號${PORT}`);
});
復(fù)制代碼
5. 修改繪圖的觸發(fā)時(shí)機(jī)
準(zhǔn)備好后端數(shù)據(jù)后,又可以開心地研究前端實(shí)現(xiàn)了。(當(dāng)我一遍一遍回放這段數(shù)據(jù)的時(shí)候,我想到了《開端》里的循環(huán)……)
這時(shí)我發(fā)現(xiàn)一個(gè)詭異的現(xiàn)象,車咋一瞬間鋪滿了屏幕,像極了當(dāng)年windows中毒的感覺(不小心暴露了年齡)。作為一個(gè)密集恐懼癥的我,立馬關(guān)掉了頁面,思考起了人生,哦不,思考起了原因。
之前的邏輯是每次收到消息的時(shí)候,去觸發(fā)繪圖【包括新增和移動(dòng)】動(dòng)作,但如果一下子涌來大量消息,就會在屏幕上堆滿車(后來發(fā)現(xiàn)其實(shí)這個(gè)問題被放大了1000倍,因?yàn)槲以诜职l(fā)數(shù)據(jù)的時(shí)候,setTimeout的時(shí)間忘記乘以1000了……)。
既然是接收消息的速度跟消費(fèi)消息的速度不匹配,此處應(yīng)該來一個(gè)消息隊(duì)列。哦,對,我是前端,此處應(yīng)該來一個(gè)數(shù)組,當(dāng)接收消息的時(shí)候,把車的信息先緩存起來,再找機(jī)會去消費(fèi)消息(埋坑3,坑3)。
哼哧哼哧改了一通,把接收消息和消費(fèi)消息的函數(shù)分別寫好了,消費(fèi)消息那里會遍歷所有車,并根據(jù)車的繪制狀態(tài)進(jìn)入自循環(huán),然后我痛苦地發(fā)現(xiàn)我找不到一個(gè)合適的第一推動(dòng)力(上帝應(yīng)該不會幫我),又hack了一把,在收消息時(shí)對車計(jì)個(gè)數(shù),當(dāng)車的數(shù)量為1時(shí),觸發(fā)消費(fèi)消息,作為函數(shù)調(diào)用的入口。
不管怎么樣,車好歹動(dòng)了起來。
6. 車輛消失后從地圖上移除
開心了不到2秒,車又堆滿了屏幕。
哦,該死,我只是不停地在增加車,卻沒有在車輛消失時(shí)把車移走。
那么問題來了,怎么判定車輛消失呢?
又觀察了一下后端數(shù)據(jù),發(fā)現(xiàn)很多消息是空的, 我就自己拍了個(gè)板,N條消息后如果還是沒有這輛車過來,就判定它消失了。但當(dāng)我不斷把N調(diào)大,發(fā)現(xiàn)效果還是很差后,我只好承認(rèn)這個(gè)算法不行。又想了一個(gè)很損的方法,反正車在高速上,所觀察的路段又很短,先假設(shè)先進(jìn)先出吧,當(dāng)車超過N輛后,把最先來的那輛車移除掉。
嗯,一頓操作后,我終于能正視屏幕了。
其實(shí)此時(shí)的我內(nèi)心慌得一逼,已經(jīng)處于代碼能不能跑起來完全聽天由命的狀態(tài)。
7. 向大佬求助
到了周五下班的點(diǎn),跟組長老實(shí)匯報(bào)了一下工作,感覺項(xiàng)目要失控,正常的前端聽我描述的第一反應(yīng)都是讓后端把數(shù)據(jù)處理好給我,我只負(fù)責(zé)展示就可以了。要展示10條數(shù)據(jù),就讓后端返回10條數(shù)據(jù),我也不用去判斷車輛是不是消失,也不用自己去緩存各種信息??傊褪遣挥霉芤郧暗南⒏袷绞窃趺礃拥模灰蚁牒梦乙裁磾?shù)據(jù),自己mock好數(shù)據(jù)做好demo,讓后端去適配就可以了。
我一邊想果然是大佬,思路就是不一樣;一邊心存疑慮,想著我不知道車啥時(shí)候消失,后端拿到的數(shù)據(jù)跟我一樣,他怎么會知道。
8. DIFF算法
不管怎么樣,按照大佬的思路搞一波吧。先模擬了第一條10車數(shù)據(jù)的消息,開心地接收好。等模擬第二條消息的時(shí)候,發(fā)現(xiàn)前端這邊無法一股腦替換,還是因?yàn)閿?shù)據(jù)的速度跟繪制的速度并不匹配,假設(shè)直接替換,那沒畫完的車,那些數(shù)據(jù)就再也沒有機(jī)會畫上去了。
能把所有簡單問題搞復(fù)雜的我此時(shí)還不死心,想到了大名鼎鼎的diff算法,根據(jù)前后兩次消息進(jìn)行diff,還煞有介事先寫好注釋。
//?新舊list對比,根據(jù)不同情況進(jìn)行不同的操作
//?若新的有,舊的沒有,則插入,tag記為'PLACEMENT'
//?若舊的有,新的沒有,則刪除,tag記為'DELETION'
//?若兩者都有,則更新,tag記為'UPDATE'
復(fù)制代碼
對所有數(shù)據(jù)打完tag要執(zhí)行的時(shí)候,又遇到了之前那個(gè)問題,移動(dòng)車輛的前提是創(chuàng)建好了車輛,而創(chuàng)建車輛是異步的,我還是需要記錄狀態(tài),那就沒法用后端直接返回的數(shù)據(jù)了,這個(gè)方案宣告失敗。
9. 從一個(gè)車的視角,把問題簡單化
當(dāng)我把所有車放一起思考的時(shí)候,其實(shí)問題很難定位。這時(shí)我才意識到,為什么我不從一個(gè)車開始。
對于一個(gè)車,其實(shí)需求很明確:
接收消息 畫車 移動(dòng)車 移除車
這時(shí)思路突然清晰了起來,每一個(gè)步驟都可以獨(dú)立實(shí)現(xiàn)。
后端發(fā)消息時(shí),進(jìn)行消息的接收。 接收消息時(shí)判定是否為新車,如果是新車,就畫車。 畫完車后,如果還存在未畫的點(diǎn),就去移動(dòng)車。每次拿出兩個(gè)點(diǎn),作為起始點(diǎn)和終止點(diǎn),通過補(bǔ)點(diǎn)算法去移動(dòng)。 如果一直沒有(預(yù)設(shè)3秒)未畫的點(diǎn)了,就判定車輛消失,移除車。實(shí)現(xiàn)的時(shí)候是每次畫新的點(diǎn)后重新倒計(jì)時(shí)。
突然發(fā)現(xiàn),這個(gè)思路再也沒有原來的煩惱,既不用思考第一推動(dòng)力的問題(坑3),又不用為了知道啥時(shí)候可以移動(dòng)車而給第一個(gè)點(diǎn)添加'marking'和'marked'狀態(tài)(坑2),并且車輛消失算法也不再是薛定諤的貓了,可以根據(jù)實(shí)際采集的數(shù)據(jù)頻率動(dòng)態(tài)調(diào)整。而且原來是拿一批點(diǎn)(坑1)去操作,現(xiàn)在每次拿兩個(gè)點(diǎn),表現(xiàn)也更穩(wěn)定了.
至此大部分彎路已走完,后面基于vue+百度地圖的多車實(shí)時(shí)運(yùn)動(dòng)及軌跡追蹤實(shí)現(xiàn)(上帝視角篇)[6]開啟真正的技術(shù)分享。
參考文獻(xiàn)
1\. 利用chrome保存和查看網(wǎng)絡(luò)請求[7]
2\. 百度多圖標(biāo)平滑移動(dòng)[8]
3\. 多線段平滑移動(dòng)[9]
4\. 軌跡動(dòng)畫[10]
關(guān)于本文
作者:family2hu
https://juejin.cn/post/7079366814752309278

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...


