百度地圖-大數據量點實時更新

閒話

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

一.需求和思路

百度地圖大多數前端開發者都使用過,或者是高德地圖之類的第三插件。(要用好第三方插件,過程老是特別的痛苦┭┮﹏┭┮)不少人必定和我同樣遇到過須要實時監控數據點的需求,經過WebSocket、SSe甚至http定時獲取和後臺創建鏈接,當數據點發生變化時,後臺將數據推送過來,而後前端將地圖上的點更新。前端

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

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

思考一下。。。

  1. 若是能夠作到每次只更新改變經緯度或者其它信息了的數據,這樣就能避免大面積閃爍問題。
  2. 若是每次更新不用從新畫點只改變經緯度和圖標,應該可以提升一些性能。
  3. 百度地圖有一個點聚合的功能,應該能夠解決這個問題。

開始動手!

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

二.思路實現

如何監聽數據變化?

(爲了方便 我這裏用定時器動態生成數據點模擬數據更新)vue

首先我對於以前思考的第一點,應該存粹是一個js的問題,只要把新獲取的數據與以前的數據進行比較篩選以後拿到咱們須要的數據就好了。不過在這以前咱們先解決另一個問題,若是我想把它作成 一個組件我怎麼才能知道點數據更新了呢,第一反應是用watch,不過用過的人應該都知道watch對於數組裏面存儲對象的這種數據沒法監控到對象內部數據的改變,若是的確須要監控只能開啓深度監聽deep: true,考慮到點的數據不少,這種監控會對性能形成很大的影響我選擇父容器拿到新的數據的時候主動告知組件更新數據,咱們看一下代碼。java

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

咱們經過修改isChange的值來進入watch,而後作邏輯處理。git

下一步處理數據!

(數據是模擬數據 咱們現看一下數據格式,方便看懂後面的話)github

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

知道了數據變化以後咱們就應該開始對數據作處理了,首先咱們再來看一下咱們須要什麼數據!web

  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上,這樣獲取覆蓋物的時候咱們就能獲取到它,方便判斷。咱們看看代碼(●'◡'●)。api

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//全部的點
        };
     _map//傳進來Map的對象複製代碼

  2. 刪除指定的marker

    var indexOf = function(item, source) {
            var index = -1;
            if (isArray(source)) {
                if (source.indexOf) {
                    index = source.indexOf(item)
                } else {
                    for (var i = 0,
                    m; m = source[i]; i++) {
                        if (m === item) {
                            index = i;
                            break
                        }
                    }
                }
            }
            return index
        };
    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都拿到了本身動手寫一個,或者直接獲取marker對象而後修改,由於淺拷貝的緣由直接能修改到。

    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: "微軟雅黑"
      });
      marker.disableMassClear();
      marker.setLabel(label);
      if (this.pointAggregationType) {
        this.markerClusterer.addMarker(marker);//添加到地圖覆蓋物體而且加入到點聚合的makers中
      }else{
        this.map.addOverlay(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);//從新設置經緯度 
              }
            });
          });
        }
    }
複製代碼

四.總結

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

  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…
相關文章
相關標籤/搜索