<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 Vue + 百度地圖的多車實(shí)時(shí)運(yùn)動(dòng)及軌跡追蹤實(shí)現(xiàn)

          共 6400字,需瀏覽 13分鐘

           ·

          2022-04-14 16:18

          點(diǎn)擊上方?前端Q,關(guān)注公眾號

          回復(fù)加群,加入前端Q技術(shù)交流群



          開局兩張圖,剩下全靠吹

          origin.gif

          202203231421回放.gif

          基于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ì):

          1. websocket方式接收數(shù)據(jù)。因?yàn)檐囕v數(shù)據(jù)會源源不斷產(chǎn)生,前端定時(shí)拉取不可取,所以采用了websocket方式接收數(shù)據(jù)。
          2. 通過id標(biāo)記一個(gè)車的覆蓋物(marker和label),方便后面刪除
          3. 嗯,我努力想想,一定會有3的

          需要改進(jìn)的點(diǎn)

          1. 車輛是跳動(dòng)前進(jìn)的,很不真實(shí)
          2. 背景過于樸素

          需要增加的功能

          1. 車輛需要顯示實(shí)時(shí)的速度
          2. 需要加一個(gè)列表,實(shí)時(shí)更新當(dāng)前在跑的車輛信息,限制顯示前10輛車
          3. 增加軌跡線功能,實(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í)需求很明確:

          1. 接收消息
          2. 畫車
          3. 移動(dòng)車
          4. 移除車

          這時(shí)思路突然清晰了起來,每一個(gè)步驟都可以獨(dú)立實(shí)現(xiàn)。

          1. 后端發(fā)消息時(shí),進(jìn)行消息的接收。
          2. 接收消息時(shí)判定是否為新車,如果是新車,就畫車。
          3. 畫完車后,如果還存在未畫的點(diǎn),就去移動(dòng)車。每次拿出兩個(gè)點(diǎn),作為起始點(diǎn)和終止點(diǎn),通過補(bǔ)點(diǎn)算法去移動(dòng)。
          4. 如果一直沒有(預(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


          往期推薦


          秒?。〈鸷眠@5個(gè)問題,就入門Docker了
          (字節(jié)/華為/美團(tuán))前端面經(jīng)記錄冷冷清清的金三銀四
          求求你們了,對自己代碼質(zhì)量有點(diǎn)要求!

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 138
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一级肏逼网 | 激情偷乱人成视频在线观看 | 色婷婷黄色无码视频 | 操逼视频高清 | 久久婷婷五月天 |