百度地图-大数据量点实时更新

5,446 阅读10分钟

闲话

上一篇文章本来打算记录一下自己做的东西,没想到第一次得到了大哥们的点赞特别开心(●'◡'●),接下来我也会多写写东西的

一.需求和思路

``百度地图``大多数前端开发者都使用过,或者是高德地图之类的第三插件。(要用好第三方插件,过程总是特别的痛苦┭┮﹏┭┮)很多人一定和我一样遇到过需要实时监控数据点的需求,通过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//所有的点
        };
     _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.cn/post/684490…
  2. js对象数组Date的比较:juejin.cn/post/684490…