diff --git a/build/deps.js b/build/deps.js index 00e290dc3..6a466ea35 100644 --- a/build/deps.js +++ b/build/deps.js @@ -1,8 +1,9 @@ var deps = { - + Core: { src: ['MarkerClusterGroup.js', - 'MarkerCluster.js'], + 'MarkerCluster.js', + 'DistanceGrid.js'], desc: 'The core of the library.' }, diff --git a/src/DistanceGrid.js b/src/DistanceGrid.js new file mode 100644 index 000000000..d9b1f285a --- /dev/null +++ b/src/DistanceGrid.js @@ -0,0 +1,112 @@ + +L.DistanceGrid = function (cellSize) { + this._cellSize = cellSize; + this._sqCellSize = cellSize * cellSize; + this._grid = {}; +}; + +L.DistanceGrid.prototype = { + + addObject: function (obj, point) { + var x = Math.floor(point.x / this._cellSize), + y = Math.floor(point.y / this._cellSize), + row = this._grid[y] = this._grid[y] || {}, + cell = row[x] = row[x] || []; + + obj._dGridCell = cell; + obj._dGridPoint = point; + cell.push(obj); + }, + + updateObject: function (obj, point) { + this.removeObject(obj); + this.addObject(obj, point); + }, + + removeObject: function (obj) { + var oldCell = obj._dGridCell, + point = obj._dGridPoint, + x, y, i, len; + + for (i = 0, len = oldCell.length; i < len; i++) { + if (oldCell[i] === obj) { + oldCell.splice(i, 1); + + if (len === 1) { + x = Math.floor(point.x / this._cellSize), + y = Math.floor(point.y / this._cellSize), + delete this._grid[y][x]; + } + break; + } + } + }, + + replaceObject: function (newObj, oldObj) { + var cell = oldObj._dGridCell, + i, len; + + for (i = 0, len = cell.length; i < len; i++) { + if (cell[i] === oldObj) { + cell.splice(i, 1, newObj); + newObj._dGridCell = oldObj._dGridCell; + newObj._dGridPoint = oldObj._dGridPoint; + break; + } + } + }, + + eachObject: function (fn, context) { + var i, j, k, len, row, cell, removed, + grid = this._grid; + + for (i in grid) { + if (grid.hasOwnProperty(i)) { + row = grid[i]; + for (j in row) { + if (row.hasOwnProperty(j)) { + cell = row[j]; + for (k = 0, len = cell.length; k < len; k++) { + removed = fn.call(context, cell[k]); + if (removed) { + k--; + len--; + } + } + } + } + } + } + }, + + getNearObject: function (point) { + var x = Math.floor(point.x / this._cellSize), + y = Math.floor(point.y / this._cellSize), + i, j, k, row, cell, len, obj; + + for (i = y - 1; i <= y + 1; i++) { + row = this._grid[i]; + if (row) { + for (j = x - 1; j <= x + 1; j++) { + cell = row[j]; + if (cell) { + for (k = 0, len = cell.length; k < len; k++) { + obj = cell[k]; + if (this._sqDist(obj._dGridPoint, point) < this._sqCellSize) { + return obj; + } + } + } + } + } + } + + return null; + }, + + _sqDist: function (p, p2) { + var dx = p2.x - p.x, + dy = p2.y - p.y; + return dx * dx + dy * dy; + } +}; diff --git a/src/MarkerClusterGroup.js b/src/MarkerClusterGroup.js index ea4c24519..8fb0882d6 100644 --- a/src/MarkerClusterGroup.js +++ b/src/MarkerClusterGroup.js @@ -221,6 +221,7 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({ maxZoom = this._map.getMaxZoom(), currentZoom = this._map.getZoom(); + //console.time('cluster'); this._topClusterLevel = this._clusterToMarkerCluster(this._needsClustering, maxZoom); this._needsClustering = []; @@ -228,6 +229,7 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({ while (minZoom < this._topClusterLevel._zoom) { this._topClusterLevel = this._clusterToMarkerCluster(this._topClusterLevel._childClusters.concat(this._topClusterLevel._markers), this._topClusterLevel._zoom - 1); } + //console.timeEnd('cluster'); //Remember the current zoom level and bounds this._zoom = currentZoom; @@ -258,24 +260,13 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({ //Takes a list of markers and clusters the new marker in to them //Will return null or the new MarkerCluster. The clustered in marker is removed from the given array - _clusterOne: function (unclusteredMarkers, newMarker, zoom) { - var markerPos = newMarker._projCenter || this._map.project(newMarker.getLatLng(), zoom), - clusterDiameterSqrd = 2 * this.options.maxClusterRadius * 2 * this.options.maxClusterRadius, - i, m, mPos; + _clusterOne: function (unclustered, newMarker, markerPoint) { + var marker = unclustered.getNearObject(markerPoint); - for (i = unclusteredMarkers.length - 1; i >= 0; i--) { - m = unclusteredMarkers[i]; - mPos = m._projCenter || this._map.project(m.getLatLng(), zoom); - - if (this._sqDist(markerPos, mPos) <= clusterDiameterSqrd) { - //Create a new cluster with these 2 - var newCluster = new L.MarkerCluster(this, m, newMarker); - delete m._projCenter; - delete newMarker._projCenter; - - unclusteredMarkers.splice(i, 1); - return newCluster; - } + if (marker) { + // create a new cluster with these 2 + unclustered.removeObject(marker); + return new L.MarkerCluster(this, marker, newMarker); } return null; @@ -283,73 +274,69 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({ //Takes a list of objects that have a 'getLatLng()' function (Marker / MarkerCluster) //Performs clustering on them (using a greedy algorithm) and returns those clusters. - //toCluster: List of Markers/MarkerClusters to cluster + //markers: List of Markers/MarkerClusters to cluster //Returns { 'clusters': [new clusters], 'unclustered': [unclustered markers] } - _cluster: function (toCluster, zoom) { - var clusterRadiusSqrd = this.options.maxClusterRadius * this.options.maxClusterRadius, - clusters = [], - unclustered = [], - i, j, c; + _cluster: function (markers, zoom) { + var radius = this.options.maxClusterRadius, + clusters = new L.DistanceGrid(radius), + unclustered = new L.DistanceGrid(radius), + i, j, marker, markerPoint, cluster, newCluster; - //go through each point - for (i = toCluster.length - 1; i >= 0; i--) { - var point = toCluster[i], - used = false; + // go through each point + for (i = markers.length - 1; i >= 0; i--) { + marker = markers[i]; + markerPoint = this._map.project(marker.getLatLng(), zoom); // calculate pixel position - point._projCenter = this._map.project(point.getLatLng(), zoom); //Calculate pixel position + // try add it to an existing cluster + cluster = clusters.getNearObject(markerPoint); - //try add it to an existing cluster - for (j = clusters.length - 1; j >= 0; j--) { - c = clusters[j]; - if (this._sqDist(point._projCenter, c._projCenter) <= clusterRadiusSqrd) { - c._addChild(point); - delete point._projCenter; - c._projCenter = this._map.project(c.getLatLng(), zoom); - - used = true; - break; - } - } - - //otherwise, look through all of the markers we haven't managed to cluster and see if we should form a cluster with them - if (!used) { - var newCluster = this._clusterOne(unclustered, point); + if (cluster) { + cluster._addChild(marker); + clusters.updateObject(cluster, this._map.project(cluster.getLatLng(), zoom)); + } else { + // otherwise, look through all of the markers we haven't managed to cluster and see if we should form a cluster with them + newCluster = this._clusterOne(unclustered, marker, markerPoint); if (newCluster) { - newCluster._projCenter = this._map.project(newCluster.getLatLng(), zoom); - clusters.push(newCluster); + clusters.addObject(newCluster, this._map.project(newCluster.getLatLng(), zoom)); } else { - //Didn't manage to use it - unclustered.push(point); + // didn't manage to use it + unclustered.addObject(marker, markerPoint); } } } - //Any clusters that did not end up being a child of a new cluster, make them a child of a new cluster - for (i = unclustered.length - 1; i >= 0; i--) { - c = unclustered[i]; - delete c._projCenter; + var result = [], + group = this; - if (c instanceof L.MarkerCluster) { - var nc = new L.MarkerCluster(this, c); - nc._haveGeneratedChildClusters = true; - clusters.push(nc); - unclustered.splice(i, 1); + // any clusters that did not end up being a child of a new cluster, make them a child of a new cluster + unclustered.eachObject(function (cluster) { + if (cluster instanceof L.MarkerCluster) { + newCluster = new L.MarkerCluster(group, cluster); + newCluster._haveGeneratedChildClusters = true; + + clusters.addObject(newCluster, cluster._dGridPoint); + unclustered.removeObject(cluster); + + return true; } - } + }); - //Remove the _projCenter temp variable from clusters - for (i = clusters.length - 1; i >= 0; i--) { - delete clusters[i]._projCenter; - clusters[i]._baseInit(); - } + unclustered.eachObject(function (marker) { + result.push(marker); + }); - return { 'clusters': clusters, 'unclustered': unclustered }; + // initialize created clusters + clusters.eachObject(function (cluster) { + cluster._baseInit(); + result.push(cluster); + }); + + return result; }, - + //Clusters the given markers (with _cluster) and returns the result as a MarkerCluster - _clusterToMarkerCluster: function (toCluster, zoom) { - var res = this._cluster(toCluster, zoom), - toAdd = res.clusters.concat(res.unclustered), + _clusterToMarkerCluster: function (markers, zoom) { + var toAdd = this._cluster(markers, zoom), result = new L.MarkerCluster(this), i; @@ -369,7 +356,7 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({ height = L.Browser.mobile ? 0 : Math.abs(bounds.max.y - bounds.min.y), sw = map.unproject(new L.Point(bounds.min.x - width, bounds.min.y - height)), ne = map.unproject(new L.Point(bounds.max.x + width, bounds.max.y + height)); - + return new L.LatLngBounds(sw, ne); } }); @@ -462,7 +449,7 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? { }); //Remove the old clusters and close the zoom animation - + setTimeout(function () { //update the positions of the just added clusters/markers me._topClusterLevel._recursively(bounds, depthToStartAt, 0, function (c) { @@ -541,4 +528,4 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? { L.Util.falseFn(document.body.offsetWidth); } -}); \ No newline at end of file +});