基于vue+百度地圖的多車(chē)實(shí)時(shí)運(yùn)動(dòng)及軌跡追蹤實(shí)現(xiàn)(心路歷程篇)

開(kāi)局兩張圖,剩下全靠吹


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