百度地图点聚合点击事件卡顿问题解决

更新时间: 2023-09-14 09:49:23

# 遇到问题

公司的新项目需要用到百度地图的点聚合功能,按照官网上的例子一顿抄自我感觉很良好,但是测试却报了bug,然后我打开页面一看,是这个样子的: (图片录得有点大,耐心等一下哈) 点击点聚合图标的时候,竟然把我的程序卡死了,可以看到旁边原本运行流畅的动画也被卡得动不了。

我以为是自己的问题,但打开官方例子将点改多了之后,也出现了同样的问题。

好家伙,点聚合原本是解决点太多地图卡顿而存在的,没想到它自己先卡顿了,瞅了一眼源码也就600行,撸起袖子自己干吧,省时间可以翻到最后

# 梳理点聚合源码逻辑

一行一行读完源码后,我整理了一份思维导图:(按住ctrl加滚轮缩放哈)

  • 红色是我改动了删除的部分
  • 绿色是我新增的部分
  • 黄色是我觉得效率低下可以优化的部分(不过我没改,不是我懒,突然又来活了没空哈)

# 点聚合源码结构

源码一共分为三个大部分:

  • 工具方法
  • MarkerClusterer
  • Cluster

其中工具方法没什么好说的,自己对着思维导图看一下就行了,是一些使用频率较高的方法

因为这个库依赖于百度地图,所以一下所说的点或marker指的是 BMap.Marker

# MarkerClusterer

MarkerClusterer类管理着地图上所有需要被聚合的点以及聚合点,这里我将源码的一部分贴出来,上面加上了我的注释:

var MarkerClusterer = BMapLib.MarkerClusterer = function(map, options) {
        // map实例必传
        if(!map) {
            return false
        }
        // 地图实例
        this._map = map
        // 普通点集合
        this._markers = []
        // 聚合点集合
        this._clusters = []

        // 处理一下传过来的参数
        var opts = options || {}
        this._gridSize = opts["gridSize"] || 60
        this._maxZoom = opts["maxZoom"] || 18
        this._minClusterSize = opts["minClusterSize"] || 2
        this._isAverageCenter = false
        if (opts["isAverageCenter"] != undefined) {
            this._isAverageCenter = opts["isAverageCenter"]
        }
        this._styles = opts["styles"] || []

        var that = this

        // 监听地图 zoomend 事件和 moveend 事件
        this._map.addEventListener("zoomend",function() {
            console.log(123)
            that._redraw()
        })

        this._map.addEventListener("moveend",function() {
            that._redraw()
        })

        var mkrs = opts["markers"]
        // 学习:先做判断是不是数组,传入的参数很有必要做判断
        isArray(mkrs) && this.addMarkers(mkrs)
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

可以看到MarkerClusterer其实就是将一些关键的参数储存作为属性,然后给地图绑定了zoomendmoveend事件,最后调用了this.addMarkers方法。

MarkerClusterer中的方法大概分为几大类:

  • 改查各种属性的方法

    • getMap(获取聚合的Map实例,即_map属性)
    • getMarkers(获取this._markers
    • getMinClusterSize (获取单个聚合最小数量,即_minClusterSize
    • setMinClusterSize(设置单个聚合最小数量)
    • isAverageCenter (获取单个聚合的落脚点是否是聚合内所有标记的平均中心,即_isAverageCenter)
    • getStyles(获取聚合的样式风格集合,即_styles)
    • setStyles(设置聚合的样式风格集合)
    • getMaxZoom(获取聚合的最大缩放级别,即 _maxZoom)
    • setMaxZoom(设置聚合最大缩放级别)
    • getGridSize(获取网格大小)
    • setGridSize(设置网格大小)
    • getClustersCount(获取真聚合的数量)
  • 地图中需要被聚合的点的增删改查方法(这里比较清晰的点在于,并不在MarkerClusterer类中处理具体应该如何聚合的逻辑以及显示,具体的应该在Cluster类中实现)

    • addMarker(将单个marker添加到聚合中)
    • addMarkers(将多个marker添加到聚合中)
    • _pushMarkerTo(处理并保存markers数组)
    • _addToClosestCluster(将点保存到最近的聚合点中)
    • _removeMarker(删除单个marker,该方法只是从数据层删除,并没有重绘聚合)
    • removeMarker(删除单个marker,并重绘聚合)
    • removeMarkers(删除一组markers,并重绘聚合)
    • clearMarkers(从地图上彻底清除所有标记)
  • 渲染相关的方法

    • _createClusters(创建所有聚合点)
    • _redraw(重新生成)
    • _clearLastClusters(清除上一次的聚合的结果)
    • _removeMarkersFromCluster(将所有markerisInCluster属性改为false
    • _removeMarkersFromMap(从地图上彻底清除所有散点)

# Cluster

Cluster是单个聚合点的类,它的代码我也加上了注释, 可以看到它的构造函数就只是初始化了一些属性。

function Cluster(markerClusterer) {
    // 总聚合markerClusterer实例
    this._markerClusterer = markerClusterer
    this._map = markerClusterer.getMap()
    this._minClusterSize = markerClusterer.getMinClusterSize()
    this._isAverageCenter = markerClusterer.isAverageCenter()
    // 落脚位置
    this._center = null
    // 这个Cluster中所包含的markers
    this._markers = []
    // 以中心点为准,向四边扩大gridSize个像素范围,即网格范围
    this._gridBounds = null
    // 是否为真聚合
    this._isReal = false
    // 带文字的聚合图标
    this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles": this._markerClusterer.getStyles()})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Cluster的方法就要精简得多,毕竟这个类并没有直接暴露出去:

  • addMarker(向该聚合添加一个marker
  • isMarkerInCluster(判断一个marker是否在该聚合中)
  • updateGridBounds(更新该聚合的网格范围)
  • updateClusterMarker(更新聚合点图标显示样式)
  • getBounds(获取该聚合所包含的所有marker的最小外接矩形范围)
  • getCenter(获取聚合的落脚点,即_center
  • isMarkerInClusterBounds(判断marker是否在聚合网格范围中)
  • remove(删除该聚合)
  • isReal(获取this._isReal

# 排查bug

那么bug到底出在哪呢,这一次我们从点聚合的流程入手:

  1. 首先MarkerClusterer类的构造函数调用了this.addMarkers(mkrs)







 


var MarkerClusterer = BMapLib.MarkerClusterer = function(map, options) {
        //中间部分省略
        //.....
        //中间部分省略

        var mkrs = opts["markers"]
        // 学习:先做判断是不是数组,传入的参数很有必要做判断
        isArray(mkrs) && this.addMarkers(mkrs)
    }
1
2
3
4
5
6
7
8
9
  1. 将目光聚焦到MarkerClusterer.prototype.addMarkers


 

 


MarkerClusterer.prototype.addMarkers = function(markers){
    for(var i = 0,len = markers.length; i < len; i++) {
        this._pushMarkerTo(markers[i])
    }
    this._createClusters()
}
1
2
3
4
5
6
  1. 然后看看MarkerClusterer.protoType._pushMarkersTo
MarkerClusterer.prototype._pushMarkerTo = function(marker) {
    var index = indexOf(marker, this._markers)
    if(index === -1) {
        marker.isInCluster = false
        this._markers.push(marker)
    }
}
1
2
3
4
5
6
7

看起来这就是将marker添加到this._markers数组中的方法

  1. 那么继续看MarkerClusterer.prototype._createClusters










 




MarkerClusterer.prototype._createClusters = function() {
    // 获取地图可视区域,以地理坐标表示
    var mapBounds = this._map.getBounds()
    // 扩展mapBounds
    var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize)
    for(var i = 0,marker; marker = this._markers[i]; i++) {
        // 如果marker的isInCluster为false,证明该点没有被添加进去
        // Bounds.containsPoint 如果点的地理坐标位于此矩形内,则返回true
        if(!marker.isInCluster && extendedBounds.containsPoint(marker.getPosition())) {
            // 将点添加到最近的聚合中
            this._addToClosestCluster(marker)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

前面的代码都是各种判断,然后关键的步骤是for循环中的this._addToClosestCluster(marker)

  1. 继续找到 MarkerClusterer.prototype._addToClosestCluster


























 


 
 




MarkerClusterer.prototype._addToClosestCluster = function(marker) {
        // 指定一个distance
        var distance = 4000000
        // 将要添加进的聚合实例
        var clusterToAddTo = null
        // 点的位置 地理坐标
        var position = marker.getPosition()
        // 遍历所有的聚合数组
        for(var i = 0, cluster;cluster = this._clusters[i],i<this._clusters.length;i++) {
            // 获取聚合的中心
            var center = cluster.getCenter()
            if(center) {
                // this._map.getDistance 返回两点之间的距离,单位是米
                // 这里直接用position不就好了吗
                var d = this._map.getDistance(center, position)
                // 400万米相当于山东青岛到新疆乌鲁木齐这么远,先给定一个较远的范围
                if(d < distance) {
                    // 找出距离最近的那个聚合
                    distance = d
                    clusterToAddTo = cluster
                }
            }
        }

        // 如果点在该聚合的边界中就将该点添加进去
        if(clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
            clusterToAddTo.addMarker(marker)
        } else {
            // 如果不在就创建一个新聚合
            var cluster = new Cluster(this)
            cluster.addMarker(marker)
            this._clusters.push(cluster)
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

我的注释写的比较详细,总之能看到比较关键的下一步,应该是 cluster.addMarker(marker)

  1. 继续看Cluster.prototype.addMarker











































 



Cluster.prototype.addMarker = function(marker) {
        // 如果这个点原本就在该聚合中,就不再进行下一步
        if(this.isMarkerInCluster(marker)) {
            return false
        }
        if(!this._center) {
            // 如果没有_center属性则将center设置为第一个marker的坐标
            this._center = marker.getPosition()
            // 更新聚合的网格边界
            this.updateGridBounds()
        } else {
            if(this._isAverageCenter) {
                // 因为加了一个点,所以需要加1
                var l = this.markers.length + 1
                // 计算平均纬度
                var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l
                // 计算平均经度
                var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l
                // 更新聚合网格边界
                this._center = new BMap.Point(lng, lat)
                this.updateGridBounds()
            }
        }

        marker.isInCluster = true
        this._markers.push(marker)

        var len = this._markers.length
        if(len < this._minClusterSize) {
            // 如果markers的长度小于minClusterSize,就将marker直接添加到map中
            this._map.addOverlay(marker)
            return true
        } else if(len === this._minClusterSize) {
            // 如果markers的长度等于minClusterSize就遍历将之前添加到地图的marker全部移除,此处有效率低下卡顿的嫌疑
            for (var i = 0;i < len;i++) {
                this._markers[i].getMap() && this._map.removeOverlay(this._markers[i])
            }
        }

        // 将_clusterMarker添加到地图上,此时上面的数量还是上次的,位置也是上次的
        this._map.addOverlay(this._clusterMarker)
        this._isReal = true
        // 更新_clusterMarker显示
        this.updateClusterMarker()
        return true
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

这个方法是在一个大循环中调用到的,因此会频繁的往地图上添加移除标注点,这里有效率低下导致卡顿的嫌疑,但是不至于会那么卡,我们接着看一下this.updateClusterMarker()里写了什么。

  1. Cluster.prototype.updateClusterMarker

























 
 
 
 
 
 


Cluster.prototype.updateClusterMarker = function(){
        // 当前地图的缩放级别是否大于最大聚合缩放级别
        if(this._map.getZoom() > this._markerClusterer.getMaxZoom()) {
            // 删除地图上的聚合图标
            this._clusterMarker && this._map.removeOverlay(this._clusterMarker)
            // 将该聚合中的点添加到地图上
            for(var i = 0,marker; marker = this._markers[i],i<this._markers.length;i++) {
                this._map.addOverlay(marker)
            }
            return
        }

        if(this._markers.length < this._minClusterSize) {
            // 如果markers的长度小于最小聚合数量,就将聚合图标隐藏起来
            // 问题:隐藏起来了那这个聚合中的点怎么办
            this._clusterMarker.hide()
            return
        }

        // 更新图标的位置和文字
        this._clusterMarker.setPosition(this._center)

        this._clusterMarker.setText(this._markers.length)

        var thatMap = this._map
        var thatBounds = this.getBounds()
        //给点聚合标记绑定click事件,此处因多次绑定事件会造成卡顿
        this._clusterMarker.addEventListener("click",function(event) {
            // 设置map的Viewport到合适的位置和尺寸
            thatMap.setViewport(thatBounds)
        })
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

家人们!终于找到问题所在了啊,updateClusterMarker方法会被调用多次,那么this._clusterMarker上就会被绑定多次click事件,不卡就有鬼了啊!

  1. 解决问题 终于定位到问题位置了,怎么解决呢,非常简单,我们只需要在Cluster的构造函数中去绑定这个click事件就好了,这样就只会绑定一次

















 
 
 
 
 
 
 
 


function Cluster(markerClusterer) {
    // 总聚合markerClusterer实例
    this._markerClusterer = markerClusterer
    this._map = markerClusterer.getMap()
    this._minClusterSize = markerClusterer.getMinClusterSize()
    this._isAverageCenter = markerClusterer.isAverageCenter()
    // 落脚位置
    this._center = null
    // 这个Cluster中所包含的markers
    this._markers = []
    // 以中心点为准,向四边扩大gridSize个像素范围,即网格范围
    this._gridBounds = null
    // 是否为真聚合
    this._isReal = false
    // 带文字的聚合图标
    this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles": this._markerClusterer.getStyles()})

    // 改为在此处绑定click事件
    var that = this
    this._clusterMarker.addEventListener("click",function(event) {
        var thatMap = that._map
        var thatBounds = that.getBounds()
        // 设置map的Viewport到合适的位置和尺寸
        thatMap.setViewport(thatBounds)
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

再来看一下效果: 不卡了耶!!!!

# 完整代码

解决方式很简单,将原本引用的

<script type="text/javascript" src="https://api.map.baidu.com/library/MarkerClusterer/1.2/src/MarkerClusterer_min.js"></script>
1

改成下面的文件就好了:

/**
 * write by 张舟 zhangzhou 2023/09/11
 * 为了解决百度地图点聚合点击聚合图标卡顿问题,重新写一遍并梳理流程
 */

/**
 * @namespace BMap的所有library类均放在BMapLib命名空间下
 */
var BMapLib = window.BMapLib = BMapLib || {};
(function(){
    /**
     * 获取一个扩展的视图范围,把上下左右都扩大一样的像素值
     * @param {Map} map BMap.Map的实例化对象
     * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象
     * @param {Number} gridSize 要扩大的像素值
     *
     * @return {BMap.Bounds} 返回扩大后的视图范围
     */
     var getExtendedBounds = function(map,bounds,gridSize){
         // 1.处理边界,让其边界符合百度地图规范
         bounds = cutBoundsInRange(bounds)
         // bounds东北角像素坐标
         var pixelNE = map.pointToPixel(bounds.getNorthEast())
         // bounds西南角像素坐标
         var pixelSW = map.pointToPixel(bounds.getSouthWest())
         // 扩展像素坐标
         pixelNE.x += gridSize
         pixelNE.y -= gridSize
         pixelSW.x -= gridSize
         pixelSW.y += gridSize
         // 计算新的东北角和西南角,并重新生成Bounds返回
         var newNE = map.pixelToPoint(pixelNE)
         var newSW = map.pixelToPoint(pixelSW)
         return new BMap.Bounds(newSW, newNE)
     }

    /**
     * 按照百度地图支持的世界范围对bounds进行边界处理
     * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象
     *
     * @return {BMap.Bounds} 返回不越界的视图范围
     */
     var cutBoundsInRange = function(bounds) {
         // getNorthEast() 返回矩形区域的东北角
         var maxX = getRange(bounds.getNorthEast().lng, -180, 180)
         var minX = getRange(bounds.getSouthWest().lng, -180, 180)
         var maxY = getRange(bounds.getNorthEast().lat, -74, 74)
         var minY = getRange(bounds.getSouthWest().lat, -74,74)
        /**
         * Bounds(sw: Point, ne: Point)
         * 创建一个包含所有给定点坐标的矩形区域。其中sw表示矩形区域的西南角,参数ne表示矩形区域的东北角
         */
         return new BMap.Bounds(new BMap.Point(minX,minY), new BMap.Point(maxX,maxY))
    }

    /**
     * 对单个值进行边界处理
     * @param {Number} i 要处理的数值
     * @param {Number} min 下边界值
     * @param {Number} max 上边界值
     *
     * @return {Number} 返回不越界的数值
     */
    var getRange = function(i, min,max) {
        // i值取i和min中较大的那一个,即i不可以超出min这个最小的边界
        min && (i = Math.max(i, min))
        // i值取i和max中较小的那一个,即i不可以超出max这个最大的边界
        max && (i = Math.min(i, max))
        return i
    }

    /**
     * 判断给定的对象是否为数组
     * @param {Object} source 要测试的对象
     * @return {Boolean} 如果是数组返回true,否则返回false
     */
    var isArray = function (source) {
        return '[object Array]' === Object.prototype.toString.call(source)
    }

    /**
     * 返回item在source中的所以位置
     * @param {Object} item 要测试的对象
     * @param {Array} source 数组
     *
     * @return {Number} 如果在数组内,返回索引,否则返回 -1
     */
    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类
     * @class 用来解决加载大量点的问题,并提高性能
     * @param map 地图实例
     * @param options
     * options:{
     *  markers: Marker[] 要聚合的标记数组
     *  gridSize: Number 聚合计算时网格的像素大小,默认60
     *  maxZoom: Number 最大聚合级别,大于该级别就不进行相应的聚合
     *  minClusterSize: Number 最小的聚合数量,小于改数量的不能成为一个聚合,默认为2
     *  isAverangeCenter: Boolean 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
     *  styles: IconStyle[] 自定义聚合后的图标风格,请参考TextIconOverlay类
     * }
     */
    console.log("看看有没有BMapLib",BMapLib)
    var MarkerClusterer = BMapLib.MarkerClusterer = function(map, options) {
        // map实例必传
        if(!map) {
            return false
        }
        // 地图实例
        this._map = map
        // 普通点集合
        this._markers = []
        // 点聚合集合
        this._clusters = []

        // 处理一下传过来的参数
        var opts = options || {}
        this._gridSize = opts["gridSize"] || 60
        this._maxZoom = opts["maxZoom"] || 18
        this._minClusterSize = opts["minClusterSize"] || 2
        this._isAverageCenter = false
        if (opts["isAverageCenter"] != undefined) {
            this._isAverageCenter = opts["isAverageCenter"]
        }
        this._styles = opts["styles"] || []

        var that = this

        // 监听地图 zoomend 事件和 moveend 事件
        this._map.addEventListener("zoomend",function() {
            console.log(123)
            that._redraw()
        })

        this._map.addEventListener("moveend",function() {
            that._redraw()
        })

        var mkrs = opts["markers"]
        // 学习:先做判断是不是数组,传入的参数很有必要做判断
        isArray(mkrs) && this.addMarkers(mkrs)
    }

    /**
     * 添加要聚合的标记数组
     * @param {Array<Marker>} markers 要聚合的标记数组
     * @return 无返回值
     */
    MarkerClusterer.prototype.addMarkers = function(markers){
        for(var i = 0,len = markers.length; i < len; i++) {
            this._pushMarkerTo(markers[i])
        }
        this._createClusters()
    }

    /**
     * 把一个标记添加到要聚合的标记数组中
     * @param {BMap.Marker} marker 要添加的标记
     * @return 无返回值
     */
    MarkerClusterer.prototype._pushMarkerTo = function(marker) {
        var index = indexOf(marker, this._markers)
        if(index === -1) {
            marker.isInCluster = false
            this._markers.push(marker)
        }
    }

    /**
     * 添加一个聚合的标记
     * @param {BMap.Marker} marker 要聚合的单个标记
     */
    MarkerClusterer.prototype.addMarker = function(marker) {
        this._pushMarkerTo(marker)
        this._createClusters()
    }

    /**
     * 根据所给定的标记,创建聚合点
     * @return 无返回值
     */
    MarkerClusterer.prototype._createClusters = function() {
        // 获取地图可视区域,以地理坐标表示
        var mapBounds = this._map.getBounds()
        // 扩展mapBounds
        var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize)
        for(var i = 0,marker; marker = this._markers[i]; i++) {
            // 如果marker的isInCluster为false,证明该点没有被添加进去
            // Bounds.containsPoint 如果点的地理坐标位于此矩形内,则返回true
            if(!marker.isInCluster && extendedBounds.containsPoint(marker.getPosition())) {
                // 将点添加到最近的聚合中
                this._addToClosestCluster(marker)
            }
        }
    }

    /**
     * 根据marker的位置,把它添加到最近的聚合中
     * @param {BMap.Marker} marker 要进行聚合的单个marker
     *
     * @return 无返回值
     */
    MarkerClusterer.prototype._addToClosestCluster = function(marker) {
        // 指定一个distance
        var distance = 4000000
        // 将要添加进的聚合实例
        var clusterToAddTo = null
        // 点的位置 地理坐标
        var position = marker.getPosition()
        // 遍历所有的聚合数组
        for(var i = 0, cluster;cluster = this._clusters[i],i<this._clusters.length;i++) {
            // 获取聚合的中心
            var center = cluster.getCenter()
            if(center) {
                // this._map.getDistance 返回两点之间的距离,单位是米
                // 这里直接用position不就好了吗
                var d = this._map.getDistance(center, position)
                // 400万米相当于山东青岛到新疆乌鲁木齐这么远,先给定一个较远的范围
                if(d < distance) {
                    // 找出距离最近的那个聚合
                    distance = d
                    clusterToAddTo = cluster
                }
            }
        }

        // 如果点在该聚合的边界中就将该点添加进去
        if(clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
            clusterToAddTo.addMarker(marker)
        } else {
            // 如果不在就创建一个新聚合
            var cluster = new Cluster(this)
            cluster.addMarker(marker)
            this._clusters.push(cluster)
        }
    }

    /**
     * 获取聚合的Map实例
     * @return {Map} Map的实例
     */
    MarkerClusterer.prototype.getMap = function() {
        return this._map
    }

    /**
     * 获取单个聚合的最小数量
     * @return {Number} 单个聚合的最小数量
     */
    MarkerClusterer.prototype.getMinClusterSize = function() {
        return this._minClusterSize
    }

    /**
     * 设置单个聚合的最小数量
     * @param {Number} size 单个聚合的最小数量
     *
     * @return 无返回值
     */
    MarkerClusterer.prototype.setMinClusterSize = function(size) {
        this._minClusterSize = size
        this._redraw()
    }

    /**
     * 获取单个聚合的落脚点是否是聚合内所有标记的平均中心
     * @return {Boolean} true 或 false
     */
    MarkerClusterer.prototype.isAverageCenter = function() {
        return this._isAverageCenter
    }

    /**
     * 获取聚合的样式风格集合
     */
    MarkerClusterer.prototype.getStyles = function() {
        return this._styles
    }

    /**
     * 设置聚合的样式风格集合
     * @param {Array<IconStyle>} styles 样式风格数组
     * @return 无返回值
     */
    MarkerClusterer.prototype.setStyles = function(styles) {
        this._styles = styles
        this._redraw()
    }

    /**
     * 获取聚合的最大缩放级别
     *
     * @return {Number} 聚合的最大缩放级别
     */
    MarkerClusterer.prototype.getMaxZoom = function() {
        return this._maxZoom
    }

    /**
     * 设置聚合的最大缩放级别
     * @param {Number} maxZoom 聚合的最大缩放级别
     *
     * @return false
     */
    MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
        this._maxZoom = maxZoom
        this._redraw()
    }

    /**
     * 获取网格大小
     * @return {Number} 网格大小
     */
    MarkerClusterer.prototype.getGridSize = function() {
        return this._gridSize
    }

    /**
     * 设置网格大小
     * @param {Number} Size 网格大小
     *
     * @return 无返回值
     */
    MarkerClusterer.prototype.setGridSize = function(size) {
        this._gridSize = size
        this._redraw()
    }

    /**
     * 重新生成, 比如改变了属性等
     * @return 无返回值
     */
    MarkerClusterer.prototype._redraw = function() {
        this._clearLastClusters()
        this._createClusters()
    }

    /**
     * 清除上一次的聚合的结果
     * @return 无返回值
     */
    MarkerClusterer.prototype._clearLastClusters = function() {
        // 遍历_cluster数组,调用cluster的remove方法
        for(var i = 0,cluster;cluster = this._clusters[i],i<this._clusters.length;i++) {
            cluster.remove()
        }
        this._clusters = []
        this._removeMarkersFromCluster()
    }

    /**
     * 清除聚合中的所有标志位
     *
     * @return 无返回值
     */
    MarkerClusterer.prototype._removeMarkersFromCluster = function() {
        for(var i = 0,marker;marker = this._markers[i],i<this._markers.length;i++) {
            marker.isInCluster = false
        }
    }

    /**
     * 从地图上彻底清除所有标记
     *
     * @return 无返回值
     */
    MarkerClusterer.prototype.clearMarkers = function() {
        // 清除上一次的聚合结果
        this._clearLastClusters()
        // 将地图上的散点清除掉
        this._removeMarkersFromMap()
        this._markers = []
    }

    /**
     * 把所有的marker从地图上清除
     * @return 无返回值
     */
    MarkerClusterer.prototype._removeMarkersFromMap = function() {
        for(var i = 0,marker;marker = this._markers[i],i<this._markers.length;i++) {
            marker.isInCluster = false
            this._map.removeOverlay(marker)
        }
    }

    /**
     * 从地图和数据层面删除单个marker
     * @param {BMap.Marker} marker 需要被删除的marker
     *
     * @return {Boolean} 删除成功返回true,否则返回false
     */
    MarkerClusterer.prototype._removeMarker = function(marker) {
        var index = indexOf(marker, this._markers)
        if(index === -1) {
            return false
        }
        this._map.removeOlerlay(marker)
        this._markers.splice(index, 1)
        return true
    }

    /**
     * 删除单个marker并重绘聚合
     * * @param {BMap.Marker} marker 需要被删除的marker
     *
     * @return {Boolean} 删除成功返回true,否则返回false
     */
    MarkerClusterer.prototype.removeMarker = function(marker) {
        var success = this._removeMarker(marker)
        if(success) {
            this._redraw()
        }
        return success
    }

    /**
     * 删除一组marker
     * @param {Array<BMap.Marker>} markers 需要被删除的marker数组
     *
     * @return {Boolean} 删除成功返回true,否则返回false
     */
    MarkerClusterer.prototype.removeMarkers = function(markers) {
        var success = false
        for(var i = 0;i<markers.length;i++) {
            var r = this._removeMarker(markers[i])
            success = success || r
        }
        if(success) {
            this._redraw()
        }
        return success
    }

    /**
     * 获取所以的标记数组
     * @return {Array<Marker>} 标记数组
     */
    MarkerClusterer.prototype.getMarkers = function() {
        return this._markers
    }

    /**
     * 获取聚合的总数量
     * @return {Number} 聚合的总数量
     */
    MarkerClusterer.prototype.getClustersCount = function() {
        var count = 0;
        for(var i = 0,cluster;cluster = this._clusters[i];i++) {
            cluster.isReal() && count++
        }
        return count
    }

    /**
     * Cluster
     * @class 表示一个聚合对象,该聚合,包含有N个marker,这N个marker组成的范围,并有予以显示在Map上的TextIconOverlay
     * @constructor
     * @param {MarkerClusterer} markerClusterer 聚合实例
     */
    function Cluster(markerClusterer) {
        // 总聚合markerClusterer实例
        this._markerClusterer = markerClusterer
        this._map = markerClusterer.getMap()
        this._minClusterSize = markerClusterer.getMinClusterSize()
        this._isAverageCenter = markerClusterer.isAverageCenter()
        // 落脚位置
        this._center = null
        // 这个Cluster中所包含的markers
        this._markers = []
        // 以中心点为准,向四边扩大gridSize个像素范围,即网格范围
        this._gridBounds = null
        // 是否为真聚合
        this._isReal = false
        // 带文字的聚合图标
        this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles": this._markerClusterer.getStyles()})

        // 改为在此处绑定click事件
        var that = this
        this._clusterMarker.addEventListener("click",function(event) {
            var thatMap = that._map
            var thatBounds = that.getBounds()
            // 设置map的Viewport到合适的位置和尺寸
            thatMap.setViewport(thatBounds)
        })
    }

    /**
     * 向该聚合添加一个标记
     * @param {Marker} marker 要添加的标记
     * @return {Boolean}
     */
    Cluster.prototype.addMarker = function(marker) {
        // 如果这个点原本就在该聚合中,就不再进行下一步
        if(this.isMarkerInCluster(marker)) {
            return false
        }
        if(!this._center) {
            // 如果没有_center属性则将center设置为第一个marker的坐标
            this._center = marker.getPosition()
            // 更新聚合的网格边界
            this.updateGridBounds()
        } else {
            if(this._isAverageCenter) {
                // 因为加了一个点,所以需要加1
                var l = this.markers.length + 1
                // 计算平均纬度
                var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l
                // 计算平均经度
                var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l
                // 更新聚合网格边界
                this._center = new BMap.Point(lng, lat)
                this.updateGridBounds()
            }
        }

        marker.isInCluster = true
        this._markers.push(marker)

        var len = this._markers.length
        if(len < this._minClusterSize) {
            // 如果markers的长度小于minClusterSize,就将marker直接添加到map中
            this._map.addOverlay(marker)
            return true
        } else if(len === this._minClusterSize) {
            // 如果markers的长度等于minClusterSize就遍历将之前添加到地图的marker全部移除,此处有效率低下卡顿的嫌疑
            for (var i = 0;i < len;i++) {
                this._markers[i].getMap() && this._map.removeOverlay(this._markers[i])
            }
        }

        // 将_clusterMarker添加到地图上,此时上面的数量还是上次的,位置也是上次的
        this._map.addOverlay(this._clusterMarker)
        this._isReal = true
        this.updateClusterMarker()
        return true
    }

    /**
     * 判断一个标记是否在该聚合中
     * @param {Marker} marker 要判断的标记
     *
     * @return {Boolean} true或false
     */
    Cluster.prototype.isMarkerInCluster = function(marker) {
        return indexOf(marker, this._markers) !== -1
    }

    /**
     * 更新该聚合的网格范围
     *
     * @return 无返回值
     */
    Cluster.prototype.updateGridBounds = function() {
        // 设置初始的边界,位置仅仅包含this._center这个点
        var bounds = new BMap.Bounds(this._center, this._center)
        // 将网格上下所有扩大gridSize像素
        this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize())
    }

    /**
     * 更新该聚合的显示样式,也即TextIconOverlay
     * @return 无返回值
     */
    Cluster.prototype.updateClusterMarker = function(){
        // 当前地图的缩放级别是否大于最大聚合缩放级别
        if(this._map.getZoom() > this._markerClusterer.getMaxZoom()) {
            // 删除地图上的聚合图标
            this._clusterMarker && this._map.removeOverlay(this._clusterMarker)
            // 将该聚合中的点添加到地图上
            for(var i = 0,marker; marker = this._markers[i],i<this._markers.length;i++) {
                this._map.addOverlay(marker)
            }
            return
        }

        if(this._markers.length < this._minClusterSize) {
            // 如果markers的长度小于最小聚合数量,就将聚合图标隐藏起来
            // 问题:隐藏起来了那这个聚合中的点怎么办
            this._clusterMarker.hide()
            return
        }

        // 更新图标的位置和文字
        this._clusterMarker.setPosition(this._center)

        this._clusterMarker.setText(this._markers.length)

        // var thatMap = this._map
        // var thatBounds = this.getBounds()
        // 给点聚合标记绑定click事件,此处因多次绑定事件会造成卡顿
        // this._clusterMarker.addEventListener("click",function(event) {
        //     // 设置map的Viewport到合适的位置和尺寸
        //     thatMap.setViewport(thatBounds)
        // })
    }

    /**
     * 获取该聚合所包含的所有marker的最小外接矩形范围
     *
     * @return {BMap.Bounds} 计算出的范围
     */
    Cluster.prototype.getBounds = function() {
        var bounds = new BMap.Bounds(this._center, this._center)
        for(var i = 0,marker;marker = this._markers[i],i<this._markers.length;i++) {
            // 放大此矩形,使其包含给定的点
            bounds.extend(marker.getPosition())
        }
        return bounds
    }

    /**
     * 获取该聚合的落脚点
     * @return {BMap.Point} 该聚合的落脚点
     */
    Cluster.prototype.getCenter = function() {
        return this._center
    }

    /**
     * 判断一个标记是否在聚合网格范围中
     * @param {Marker} marker 要判断的标记
     *
     * @return {Boolean} true或false
     */
    Cluster.prototype.isMarkerInClusterBounds = function(marker) {
        return this._gridBounds.containsPoint(marker.getPosition())
    }

    /**
     * 删除该聚合
     * @return 无返回值
     */
    Cluster.prototype.remove = function() {
        // 清除散点
        for(var i = 0,m;m = this._markers[i],i<this._markers.length;i++) {
            // 将所有点的isInCluster改为false
            this._markers[i].isInCluster = false
            this._markers[i].getMap() && this._map.removeOverlay(this._markers[i])
        }
        // 清除掉聚合标记
        this._map.removeOverlay(this._clusterMarker)
        this._markers.length = 0
        delete this._markers
    }

    /**
     * 获取该聚合是否为真聚合
     *
     * @return {Boolean}
     */
    Cluster.prototype.isReal = function(marker) {
        return this._isReal
    }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670