百度地圖-大資料量點實時更新

劍繪驚鴻發表於2019-04-15

百度地圖-大資料量點實時更新

閒話

上一篇文章本來打算記錄一下自己做的東西,沒想到第一次得到了大哥們的點贊特別開心(●'◡'●),接下來我也會多寫寫東西的

一.需求和思路

百度地圖大多數前端開發者都使用過,或者是高德地圖之類的第三外掛。(要用好第三方外掛,過程總是特別的痛苦┭┮﹏┭┮)很多人一定和我一樣遇到過需要實時監控資料點的需求,通過WebSocket、SSe甚至http定時獲取和後臺建立連線,當資料點發生變化時,後臺將資料推送過來,然後前端將地圖上的點更新。

那麼地圖更新的時候會出現那些問題呢?

  1. 每次更新資料點,就算那個資料點經緯度和圖示都沒有變化,也會出現閃爍。
  2. 資料量過大會導致百度地圖渲染很慢,出現瀏覽器卡頓甚至卡死的問題。
  3. 資料量過多在層級拉小的時候會因為點過於密集導致看不清楚。

思考一下。。。

  1. 如果可以做到每次只更新改變經緯度或者其它資訊了的資料,這樣就能避免大面積閃爍問題。
  2. 如果每次更新不用重新畫點只改變經緯度和圖示,應該能夠提高一些效能。
  3. 百度地圖有一個點聚合的功能,應該可以解決這個問題。

開始動手!

有了想法,那就可以開始動手去實現了!(●'◡'●),其實之前對於百度地圖api不是特別的熟悉,只是會簡單的使用,這次為了這個優化特地去補習了一下,可能有些地方還是考慮好,歡迎您能評論和我交流交流*^____^*。

二.思路實現

如何監聽資料變化?

(為了方便 我這裡用定時器動態生成資料點模擬資料更新)

首先我對於之前思考的第一點,應該存粹是一個js的問題,只要把新獲取的資料與之前的資料進行比較篩選之後拿到我們需要的資料就行了。不過在這之前我們先解決另外一個問題,如果我想把它做成 一個元件我怎麼才能知道點資料更新了呢,第一反應是用watch,不過用過的人應該都知道watch對於陣列裡面儲存物件的這種資料無法監控到物件內部資料的改變,如果的確需要監控只能開啟深度監聽deep: true,考慮到點的資料很多,這種監控會對效能造成很大的影響我選擇父容器拿到新的資料的時候主動告知元件更新資料,我們看一下程式碼。

//父級
<mapBaidu :mapChange="isChange" :mapDateOld="mapDataTableOld" :mapDateNew="mapDataTable"></mapBaidu>
data () {
    return {
      isChange:false,//資料是否改變
    }
  },
//元件mapBaidu
watch: {
    mapChange: {
      handler() {
       console.log("資料發現改變");
      }
    },
  }複製程式碼

我們通過修改isChange的值來進入watch,然後做邏輯處理。

下一步處理資料!

(資料是模擬資料 我們現看一下資料格式,方便看懂後面的話)

       {
          id:index,//唯一標識
          lng:120+Math.random(),//經度
          name:`點${index}`,//名稱
          lat:30+Math.random(),//維度
          icon:Math.random()>0.5?"car-normal.png":"car-speeding.png"//圖示
        }複製程式碼

知道了資料變化之後我們就應該開始對資料做處理了,首先我們再來看一下我們需要什麼資料!

  1. 舊資料中應該刪除的點
  2. 新資料中應該新增的點
  3. 舊資料中應該修改經緯度或者圖示之類資訊的點
那麼大腦中的fitter、reduce、foreach、indexof、some、map、find都開始蠢蠢欲動了,首先第1點第2點其實比較簡單,我們只需要分別獲取新資料的id陣列newIDList,然後拿這個陣列newIDList和舊資料oldPintList比較找到舊資料中的id不在這個陣列中的點那麼這些點就是需要刪除的點delPointID,需要新增的點就是反過來找。那麼第3點呢其實也是一樣的我們在處理第一點的時候可以儲存一下舊資料oldPintList中需要刪除的點以為的點otherPointList,然後迴圈這個陣列otherPointList和新資料newPintList比較id相同的點的經緯度和圖示是否相同,如果出現變化則儲存新資料newPintList中對應的點為需要修改的陣列changePonitList我們看看程式碼,其實可能還要其他思路我暫時只想到這個如果你有更好的想法可以和我分享一下。

    //比較新舊陣列的不同
    filterMap(oldPintList, newPintList) {
      let delPointID = [], //相對於新獲取的點需要取消的點的id陣列
        otherPointList = [], //相對於新獲取的點不需要取消的點
        addPointList = [], //相對於舊的資料點需要新增的點
        newIDList = new Set(), //定義一個陣列用來存新資料的id的集合
        oldIDList = new Set(); //定義一個陣列用來存舊資料的id的集合   
      newPintList.forEach(item => {
        newIDList.add(item.id);     
      });
      oldPintList.forEach(item => {
        oldIDList.add(item.id);     
      });
      oldPintList.forEach(item =>
        newIDList.has(item.id)? otherPointList.push(item):delPointID.push(item.id)
      );
      newPintList.forEach(item =>{
          if(!oldIDList.has(item.id)){
            addPointList.push(item);
          }
        }
      );
      let changePonitList = this.filterChange(otherPointList, newPintList); //changePonitList:發生變化的點
      return {
        delPointID,
        addPointList,
        changePonitList
      };
    },
    //獲取新資料中發生變化的點
    filterChange(otherPointList, newPintList) {
      var changePonitList = [];//變化了的點
      otherPointList.forEach(point => {
        let pList = newPintList.find(item => {
          return item.id == point.id;
        });//新獲取的資料中對應的那個點
        if (pList.lng != point.lng || pList.lat != point.lat || pList.icon != point.icon ) {
          changePonitList.push(pList);
        }
      });
      return changePonitList;
    }複製程式碼

開始對百度地圖動手!!!

資料處理好了接下來就是怎麼處理這3個陣列delPointID、addPointList、changePonitList首先我們先獲取一下所有的覆蓋物overlaysList=this.map.getOverlays(),迴圈delPointID找到overlaysList對應的點通過百度地圖提供的removeOverlay方法,刪除對應的點。接下來迴圈changePonitList找到對應的點通過百度地圖提供的setIcon、setPosition方法重新設定經緯度和圖示。addPointList就不用多說了新增一個點到百度地圖,大部分人應該都會用,值得一提的是最好把每個點的id、icon的資訊儲存在marker上,這樣獲取覆蓋物的時候我們就能獲取到它,方便判斷。我們看看程式碼(●'◡'●)。

   
    let { delPointID, addPointList, changePonitList } = 
    this.filterMap(this.mapDateOld,this.mapDateNew);//獲取刪除點、新增點、修改點
    this.delEditMarker(changePonitList,delPointID);//修改刪除點
    addPointList.forEach(add=>{//新增點
        this.addMarker(add);
    })


    //刪除點
    delEditMarker(changePonitList,delPointID) {
        let overlaysList;
        if (this.pointAggregationType) {//開啟點聚合通過markerClusterer類獲取點
          overlaysList = this.markerClusterer.getMarkers().slice(0);
        } else {//未開啟點聚合獲取所有覆蓋物
          overlaysList = this.map.getOverlays();
        }
        if (changePonitList.length > 0 || delPointID.length > 0) {//如果存在需要修改和刪除的點
          overlaysList.forEach(item => {
            //刪除點
            if (delPointID.indexOf(item.id) > -1) {
              if (this.pointAggregationType) {
                this.markerClusterer.removeMarker(item);
              } else {
                this.map.removeOverlay(item);
              }
            }
            //修改點
            changePonitList.forEach(edit=>{
              if(item.id == changePonitList[i].id){
                let point = new BMap.Point(editPoint.lat, editPoint.lng);
                let icon = new BMap.Icon(editPoint.icon, new BMap.Size(29, 29));
                item.setIcon(icon);//重新設定圖示
                item.setPosition(point);//重新設定經緯度
                if (this.pointAggregationType) {
                  this.markerClusterer.setMarkers(item.id, item);
                }
              }
            });
          });
        }
    },

     //新增點
    addMarker(add) {
      let point = new BMap.Point(add.lng, add.lat);
      // console.log(point);
      
      var icon = new BMap.Icon("/img/"+add.icon, new BMap.Size(29, 29)); //設定圖示大小
      let marker = new BMap.Marker(point, {
        icon: icon
      });
      marker.id = add.id;
      marker.icon = add.icon;
      let opts = {
        position: point, // 指定文字標註所在的地理位置
        offset: new BMap.Size(-10, 26) //設定文字偏移量
      };
      let label = new BMap.Label(add.name, opts); // 建立文字標註物件
      label.setStyle({
        color: "000",
        fontSize: "12px",
        height: "20px",
        lineHeight: "20px",
        border: "1px solid #000",
        fontFamily: "微軟雅黑"
      });
      this.map.addOverlay(marker);//新增到地圖
      marker.disableMassClear();
      marker.setLabel(label);
      if (this.pointAggregationType) {
        this.markerClusterer.addMarker(marker);
      }
    },
複製程式碼

三.融合點聚合

什麼是點聚合?

首先我們來看看官網點聚合的效果圖

百度地圖-大資料量點實時更新

圖中圖示上有數字的就是聚合點,其實就是再層級拉到很小的時候,把點聚合顯示,然後拉大之後又再顯示出來,可以看的更加直觀。

加入點聚合後的影響?

首先一般來說加入點聚合之後,可以讓地圖看上去更整潔。我們還是按之前的思路走,看看有什麼問題,問題基本出現在刪除點新增點和修改點的時候,不能再像以前一樣處理了,因為聚合點下的點是沒有渲染的通過獲取圖層無法取到它們。那我改如何去更新刪除新增點呢,有點頭痛!!!┭┮﹏┭┮。假如我們實現了這個效果,那麼我們新增點或者刪除點是聚合點裡面的點,那麼聚合點上的數字就會發生變化。考慮到這一點我覺得必須去操作百度提供的點聚合的js才能做到了(MarkerClusterer_min.js)。內容我就不貼了,我直接放一個連結吧,東西比較多api.map.baidu.com/library/Mar….

我們來羅列一下對我們有用的東西

  1. 首先是獲取我們初始化的時候傳進去的markers(這就是所有的點)

      MarkerClusterer.prototype.getMarkers = function() {
            return this._markers
        };複製程式碼

  2. 刪除指定的marker

    MarkerClusterer.prototype._removeMarker = function(marker) {
            var index = indexOf(marker, this._markers);
            if (index === -1) {
                return false
            }
            tmplabel = marker.getLabel();
            this._map.removeOverlay(marker);
            marker.setLabel(tmplabel); 
            this._markers.splice(index, 1);
            return true
        };
        MarkerClusterer.prototype.removeMarker = function(marker) {
            var success = this._removeMarker(marker);
            if (success) {
                this._clearLastClusters();
                this._createClusters()
            }
            return success
        };複製程式碼

  3. 新增指定的marker

    MarkerClusterer.prototype._pushMarkerTo = function(marker) {
            var index = indexOf(marker, this._markers);
            if (index === -1) {
                marker.isInCluster = false;
                this._markers.push(marker)
            }
        };
        MarkerClusterer.prototype.addMarker = function(marker) {
            this._pushMarkerTo(marker);
            this._createClusters()
        };
        MarkerClusterer.prototype._createClusters = function() {
            var mapBounds = this._map.getBounds();
            var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize);
            for (var i = 0,
            marker; marker = this._markers[i]; i++) {
                if (!marker.isInCluster && extendedBounds.containsPoint(marker.getPosition())) {
                    this._addToClosestCluster(marker)
                }
            }
        };複製程式碼

  4. 並沒有修改指定maker的方法,js都拿到了自己動手寫一個

    MarkerClusterer.prototype.setMarkers = function(id,marker) {
            this._markers.forEach(
                (item)=>{
                    if(item.id==id){
                        item=marker;
                    }
                }
            )
        };複製程式碼

好了解決了這些,我們就可以融合點聚合了,(●'◡'●),為了滿足更多人,我們加一個布林值pointAggregationType來控制是否開啟點聚合我們來看看程式碼。

        if (this.map == null) {//地圖還未初始化
          return;
        }
        let { delPointID, addPointList, changePonitList } = this.filterMap(this.mapDateOld,this.mapDateNew);//獲取刪除點、新增點、修改點
        if (this.markerClusterer == null && this.pointAggregationType) {//如果點選後物件為null,且開啟點聚合,則重新建立點聚合
          this.markerClusterer = new BMapLib.MarkerClusterer(this.map, {markers: []});
        }
        this.delEditMarker(changePonitList,delPointID);//修改刪除點
        addPointList.forEach(add=>{//新增點
          this.addMarker(add);
        })
    //新增點

    addMarker(add) {
      let point = new BMap.Point(add.lng, add.lat);
      var icon = new BMap.Icon("/img/"+add.icon, new BMap.Size(29, 29)); //設定圖示大小
      let marker = new BMap.Marker(point, {
        icon: icon
      });
      marker.id = add.id;
      marker.icon = add.icon;
      let opts = {
        position: point, // 指定文字標註所在的地理位置
        offset: new BMap.Size(-10, 26) //設定文字偏移量
      };
      let label = new BMap.Label(add.name, opts); // 建立文字標註物件
      label.setStyle({
        color: "000",
        fontSize: "12px",
        height: "20px",
        lineHeight: "20px",
        border: "1px solid #000",
        fontFamily: "微軟雅黑"
      });
      this.map.addOverlay(marker);//新增到地圖
      marker.disableMassClear();
      marker.setLabel(label);
      if (this.pointAggregationType) {
        this.markerClusterer.addMarker(marker);
      }
    },
    //刪除點
    delEditMarker(changePonitList,delPointID) {
        let overlaysList;
        if (this.pointAggregationType) {//開啟點聚合通過markerClusterer類獲取點
          overlaysList = this.markerClusterer.getMarkers().slice(0);
        } else {//未開啟點聚合獲取所有覆蓋物
          overlaysList = this.map.getOverlays();
        }
        if (changePonitList.length > 0 || delPointID.length > 0) {//如果存在需要修改和刪除的點
          overlaysList.forEach(item => {
            //刪除點
            if (delPointID.indexOf(item.id) > -1) {
              if (this.pointAggregationType) {
                this.markerClusterer.removeMarker(item);
              } else {
                this.map.removeOverlay(item);
              }
            }
            //修改點
            changePonitList.forEach(edit=>{
              if(item.id == changePonitList[i].id){
                let point = new BMap.Point(editPoint.lat, editPoint.lng);
                let icon = new BMap.Icon(editPoint.icon, new BMap.Size(29, 29));
                item.setIcon(icon);//重新設定圖示
                item.setPosition(point);//重新設定經緯度
                if (this.pointAggregationType) {
                  this.markerClusterer.setMarkers(item.id, item);
                }
              }
            });
          });
        }
    }
複製程式碼

四.總結

編寫過程中幾個需要提一下的點

  1. 如何我們需要渲染一個label並且使用點聚合的時候,拖動地圖會存在label不顯示的問題,我在點聚合的js中重新賦值了一遍label解決這個問題。
  2. 當我們的資料達到一千個點以上,首次開啟存在卡頓問題,我們這裡有幾個可以優化的地方,非同步初始化地圖,讓使用者的體驗變好,打點的圖片儘量壓縮一下達到最小。
  3. 資料更新的時候拖動地圖容易導致卡頓我們可以直接在資料更新的時候禁止拖拽,更新完成後開啟拖拽(這個給使用者的體驗並不一定好,可以選擇顯示個過度動畫,只是個想法不一定要這樣做)。

看看效果圖

百度地圖-大資料量點實時更新


喜歡可以給我點個贊鼓勵我一下(●'◡'●)

附上github連結github.com/github30789…

其他文章傳送門

  1. 基於vue實現web端超大資料量表格:juejin.im/post/5ca1a9…
  2. js物件陣列Date的比較:juejin.im/post/5c739e…


相關文章