使用百度地圖路書為騎行視訊新增同步軌跡

2023-07-25 15:00:43

問題背景

使用 gopro 記錄騎行過程 (參考《使用二手 gopro 做行車記錄儀 》),事後將視訊檔匯出來回顧整個旅程,會發現將它們與地圖對應起來是一件困難的事。想要視訊和地圖對應,首先需要上報每個時刻的位置,gopro 本身是支援的,然而要到版本 5 才可以,我的 3+ 太老了沒這能力。為此我配備了專門的 GPS 定位器來記錄騎行軌跡 (e.g. 途強定位),在官網上是可以看到整個騎行軌跡,像下面這樣:

這個介面也可以回放軌跡,回放速度還能調整:

不過即使調到最慢,速度相對視訊還是快,更最要命的是,這個回放看起來並不參考 GPS 時間,僅僅是按順序播放。舉例來說,間隔 10 秒的兩個點和間隔 10 分鐘的兩個點,播放時沒有差別,都是相同的速度播放。

所幸 GPS 軌跡資料是可以匯出的:

如果能用匯出的軌跡資料,根據 GPS 時間精準的製作騎行軌跡,是不是就能和視訊同步了?抱著這個想法,有了下面的探索過程。

可行性研究

DashWare

根據 GPS 資料製作騎行軌跡並不算一個新鮮需求,早期玩極限運動的各位先驅早已經探索的明明白白,也有專業的軟體支援這種需求,DashWare 就是其中的佼佼者。之前看到 B 站上一個旅遊區 UP,他就介紹過一種基於 DashWare 給國外徒步旅行的遊記視訊加軌跡路線的方法 (參考附錄 15),整個過程可以總結為三步:

  • 抽取視訊的 GPX 資訊
  • 編輯 GPX 然後快速跳轉地圖截圖
  • 用 Dashware 套用作者的模板生成軌跡路徑

這個過程嚴重依賴視訊記錄的 GPX 資訊,而我的硬體裝置不支援,放棄。如果你的裝置可以支援,其實用 DashWare 還是蠻方便的。

曬一下我自己配的 GPS 定位器:

這種硬體不太方便的地方是需要單獨供電並插流量卡,後者只保兩年,兩年以後需要自己續費或買流量卡應付。GPS 資料量不大,據客服說一直不停上報大約需要 22M/月,我騎的少 3M 就夠用,最後在某寶上配的 5M/月 的移動流量卡大約 8.7 元,給各位做個參考。流量查詢和續費可以通過公眾號進行:

百度路書

根據 GPS 資料繪製軌跡,其實國內各大地圖服務商都提供瞭解決方案,偶然的一個機會看到使用百度地圖的路書可以方便快捷的製作行駛軌跡 (參考附錄 1),最終效果和我的場景非常相似:

原始碼不過寥寥一百行,其中關鍵的就是下面這幾十行:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>百度地圖顯示車輛執行軌跡(靜態)</title>
        <script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=你在百度地圖開放平臺申請的ak"></script>
        <!--  你在百度地圖開放平臺申請的ak -->
        
        <!-- 路書功能 -->
        <script type="text/javascript" src="http://api.map.baidu.com/library/LuShu/1.2/src/LuShu_min.js"></script>
    </head>
    <body>
        <div id="allmap" style="position: absolute; width: 100%; top: 0px; bottom: 0px"></div>
        <script type="text/javascript">
...
        //顯示車輛軌跡線
        //車輛軌跡座標
        var latLngArray = [
            "113.408984,23.174023",
            "113.406639,23.174023",
            "113.403944,23.173566",
            "113.400827,23.17394",
            "113.397468,23.174496",
            "113.391494,23.174513",
            "113.389032,23.174588",
            "113.388736,23.173217",
            "113.388511,23.171888",
            "113.388592,23.170501",
            "113.38861,23.170219",
            "113.38861,23.168342",
            "113.388574,23.165218"
        ];
...
        var pois = [];
        for(var i = 0; i < latLngArray.length ; i++) {
            var latLng = latLngArray[i];
            var pointArray = latLng.split(",");
            pois.push(new BMap.Point(pointArray[0], pointArray[1]));
        }
...
        var polyline = new BMap.Polyline(pois, {
            enableEditing: false,//是否啟用線編輯,預設為false
            enableClicking: true,//是否響應點選事件,預設為true
            icons: [icons],
            strokeWeight: '8',//折線的寬度,以畫素為單位
            strokeOpacity: 0.8,//折線的透明度,取值範圍0 - 1
            strokeColor: "#18a45b" //折線顏色
        });
        map.addOverlay(polyline);
...
        var lushu = new BMapLib.LuShu(map,pois,{
            defaultContent: trackContent,
            autoView:false,//是否開啟自動視野調整,如果開啟那麼路書在運動過程中會根據視野自動調整
            icon: icon_gps_car_run,
            speed: 100,
            enableRotation:true,//是否設定marker隨著道路的走向進行旋轉
        });
        lushu.start(); 
        </script>
    </body>    
</html>

梳理一下其中的關鍵點:

  • html header 中宣告百度地圖的 ak (申請方式原作者有說明)
  • body 中建立 GPS 座標陣列 (latLngArray) 並轉化為路書能接受的格式 (pois)
  • 建立軌跡總覽線 (polyline)
  • 製作小車移動軌跡 (lushu.start)

可以看到核心功能其實都是通過百度地圖 js 類 BMapLib.LuShu 實現的,下面好好研究一下它的介面。

BMapLib.LuShu

這個類的官方檔案可參考附錄 16,提供的方法主要如下:

  • constructor
  • start
  • stop
  • pause

非常簡潔。其中 start / stop / pause 都不再提供引數,所以想進行更復雜的控制只能事先在建構函式中指定了:

BMapLib.LuShu(map, path, opts)
LuShu類別建構函式

參考範例:
var lushu = new BMapLib.LuShu(map,arrPois,{defaultContent:"從北京到天津",landmarkPois:[]});

引數:
{Map} map
    Baidu map的範例物件.
{Array} path
    構成路線的point的陣列.
{Json Object} opts
    可選的輸入引數,非必填項。可輸入選項包括:
    {
    "landmarkPois" : {Array} 要在覆蓋物移動過程中,顯示的特殊點。格式如下:landmarkPois:[
    {lng:116.314782,lat:39.913508,html:'加油站',pauseTime:2},
    {lng:116.315391,lat:39.964429,html:'高速公路收費站,pauseTime:3}]

    "icon" : {Icon} 覆蓋物的icon,
    "speed" : {Number} 覆蓋物移動速度,單位米/秒

    "defaultContent" : {String} 覆蓋物中的內容
    "autoView" : {Boolean} 是否自動調整路線視野,預設不調整
    "enableRotation" : {Boolean} 是否開啟marker隨路走向旋轉,預設為false,即不隨路走向旋轉
    }

除 map、path 是必需引數外,其它均為可選引數。下面對各個選項做個簡單說明:

  • speed:用於控制小車移動的速度,單位是 m/s,範例中給的值是 100,相當於 360 km/h,那是相當快了,如果按 72 km/h 算的話,才 20 m/s
  • autoView:隨著小車的移動,自動調整地圖位置,以保證小車位於視野之內,一般是在小車走出視野邊緣後進行調整。推薦開啟,除非是鷹眼檢視
  • icon:小車的圖形,可以指定本地檔案
  • defaultContent:對軌跡的文字說明,跟隨在小車左右
  • enableRotation:是否旋轉小車圖形以對準前進方向。推薦開啟,以獲取更好的演示效果
  • landmarkPois:設定的途經點陣列,及在途經點的經停時間,單位為秒,這個選項在範例中未使用

梳理了一遍 LuShu 的功能,發現即使能將小車移動速度調整到與實際平均速度一致,地圖與視訊仍然對不上。原因與之前一樣,LuShu 中根本沒有輸入 GPS 時間引數的地方,所以它完全沒有座標點與時間對照的概念,所有座標之間的時間間隔都是一致的,唯一可調節的地方就是 speed 引數,它用來控制這個間隔的大小。

回過頭來看途強線上的介面,基本可以確論,這也是基於 LuShu 改的,所以它們的問題是相通的。

定時器

現在問題的關鍵就變成如何等待真實的時間間隔。一開始想手動 pause 和 start,寫了個定時器來做這個事情:

...
        var lushu = new BMapLib.LuShu(map,pois,{
            defaultContent: trackContent,
            autoView:false,//是否開啟自動視野調整,如果開啟那麼路書在運動過程中會根據視野自動調整
            icon: icon_gps_car_run,
            speed: 100,
            enableRotation:true,//是否設定marker隨著道路的走向進行旋轉
        });
        lushu.start();

        let is_pause=false
        setInterval(function(){
            if (is_pause) {
                lushu.start(); 
            } else { 
                lushu.pause(); 
            }
            is_pause = !is_pause; 
        }, 1000);   
 ...

先簡單的設定成每秒一次,後面可以根據實際的時間差來控制等待時間。執行時發現,第一次定時器到期後小車暫停,然後就沒有然後了…小車再也沒有啟動過。在定時器函數中加了一些紀錄檔進一步觀察:

interval false, count 1
after true
interval true, count 2
interval true, count 3
interval true, count 4
interval true, count 5
interval true, count 6
interval true, count 7
interval true, count 8
interval true, count 9
interval true, count 10
interval true, count 11

發現函數結尾處只被呼叫了一次 (after true),之後就再也沒列印,且 is_pause 的值一直為 true,可以確認反轉 is_pause 值的程式碼沒有被執行。

為了確認是否是變數作用域的問題,增加了一個全域性變數 count,每次在函數入口處自增並列印它的值,可以看到能正常遞增,排除 js 變數作用域的問題。

經過這番折騰,基本可以確認問題是出在了 start 介面,看現象再次呼叫它貌似沒有返回,懷疑這個介面是不能重入的,或者就不能這樣用,定時器方案走不下去了,放棄。

landmarkPois

正所謂「踏破鐵鞋無覓處,柳暗花明又一村」