diff --git a/leafclusterer.js b/leafclusterer.js index b4c8f66..e3659bd 100644 --- a/leafclusterer.js +++ b/leafclusterer.js @@ -1,8 +1,9 @@ /** - * @name LeafClusterer + * @name L.Marker.Clusterer * @version 1.0 * @author Xiaoxi Wu * @author Imre Deak, ported for Leaflet + * @author Bruno Bergot / b_b, ported to use Leaflet classes * @copyright (c) 2009 Xiaoxi Wu * @fileoverview * This javascript library creates and manages per-zoom-level @@ -11,13 +12,13 @@ * Maptimize hosted clustering solution. *

* How it works:
- * The LeafClusterer will group markers into clusters according to + * The L.Marker.Clusterer will group markers into clusters according to * their distance from a cluster's center. When a marker is added, * the marker cluster will find a position in all the clusters, and * if it fails to find one, it will create a new cluster with the marker. * The number of markers in a cluster will be displayed * on the cluster marker. When the map viewport changes, - * LeafClusterer will destroy the clusters in the viewport + * L.Marker.Clusterer will destroy the clusters in the viewport * and regroup them into new clusters. * */ @@ -38,8 +39,8 @@ /** - * @name LeafClustererOptions - * @class This class represents optional arguments to the {@link LeafClusterer} + * @name L.Marker.Clusterer.Options + * @class This class represents optional arguments to the {@link L.Marker.Clusterer} * constructor. * @property {Number} [maxZoom] The max zoom level monitored by a * marker cluster. If not given, the marker cluster assumes the maximum map @@ -57,7 +58,7 @@ /** * @name MarkerStyleOptions - * @class An array of these is passed into the {@link LeafClustererOptions} + * @class An array of these is passed into the {@link L.Marker.Clusterer.Options} * styles option. * @property {String} [url] Image url. * @property {Number} [height] Image height. @@ -68,671 +69,675 @@ */ /** - * Creates a new LeafClusterer to cluster markers on the map. + * Creates a new Clusterer to cluster markers on the map. * * @constructor - * @param {GMap2} map The map that the markers should be added to. - * @param {Array of GMarker} opt_markers Initial set of markers to be clustered. - * @param {LeafClustererOptions} opt_opts A container for optional arguments. + * @param {L.Map} map The map that the markers should be added to. + * @param {Array of L.Marker} opt_markers Initial set of markers to be clustered. + * @param {L.Marker.Clusterer.Options} opt_opts A container for optional arguments. */ -function LeafClusterer(map, opt_markers, opt_opts) { - // private members - var clusters_ = []; - var map_ = map; - var maxZoom_ = null; - var me_ = this; - var gridSize_ = 40; - var sizes = [53, 56, 66, 78, 90]; - var styles_ = []; - var leftMarkers_ = []; - var mcfn_ = null; - - var i = 0; - for (i = 1; i <= 5; ++i) { - styles_.push({ - 'url': "http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/images/m" + i + ".png", - 'height': sizes[i - 1], - 'width': sizes[i - 1] - }); - } - - if (typeof opt_opts === "object" && opt_opts !== null) { - if (typeof opt_opts.gridSize === "number" && opt_opts.gridSize > 0) { - gridSize_ = opt_opts.gridSize; - } - if (typeof opt_opts.maxZoom === "number") { - maxZoom_ = opt_opts.maxZoom; - } - if (typeof opt_opts.styles === "object" && opt_opts.styles !== null && opt_opts.styles.length !== 0) { - styles_ = opt_opts.styles; - } - } - - /** - * When we add a marker, the marker may not in the viewport of map, then we don't deal with it, instead - * we add the marker into a array called leftMarkers_. When we reset LeafClusterer we should add the - * leftMarkers_ into LeafClusterer. - */ - function addLeftMarkers_() { - if (leftMarkers_.length === 0) { - return; - } - var leftMarkers = []; - for (i = 0; i < leftMarkers_.length; ++i) { - me_.addMarker(leftMarkers_[i], true, null, null, true); - } - leftMarkers_ = leftMarkers; - } - - /** - * Get cluster marker images of this marker cluster. Mostly used by {@link Cluster} - * @private - * @return {Array of String} - */ - this.getStyles_ = function () { - return styles_; - }; - - /** - * Remove all markers from LeafClusterer. - */ - this.clearMarkers = function () { - for (var i = 0; i < clusters_.length; ++i) { - if (typeof clusters_[i] !== "undefined" && clusters_[i] !== null) { - clusters_[i].clearMarkers(); - } - } - clusters_ = []; - leftMarkers_ = []; - map.off(mcfn_); - }; - - /** - * Check a marker, whether it is in current map viewport. - * @private - * @return {Boolean} if it is in current map viewport - */ - function isMarkerInViewport_(marker) { - return map_.getBounds().contains(marker.getLatLng()); - } - - /** - * When reset LeafClusterer, there will be some markers get out of its cluster. - * These markers should be add to new clusters. - * @param {Array of GMarker} markers Markers to add. - */ - function reAddMarkers_(markers) { - var len = markers.length; - var clusters = []; - for (var i = len - 1; i >= 0; --i) { - me_.addMarker(markers[i].marker, true, markers[i].isAdded, clusters, true); - } - addLeftMarkers_(); - } - - /** - * Add a marker. - * @private - * @param {GMarker} marker Marker you want to add - * @param {Boolean} opt_isNodraw Whether redraw the cluster contained the marker - * @param {Boolean} opt_isAdded Whether the marker is added to map. Never use it. - * @param {Array of Cluster} opt_clusters Provide a list of clusters, the marker - * cluster will only check these cluster where the marker should join. - */ - this.addMarker = function (marker, opt_isNodraw, opt_isAdded, opt_clusters, opt_isNoCheck) { - if (opt_isNoCheck !== true) { - if (!isMarkerInViewport_(marker)) { - leftMarkers_.push(marker); - return; - } - } - - var isAdded = opt_isAdded; - var clusters = opt_clusters; - var pos = map_.latLngToLayerPoint(marker.getLatLng()); - - if (typeof isAdded !== "boolean") { - isAdded = false; - } - if (typeof clusters !== "object" || clusters === null) { - clusters = clusters_; - } - - var length = clusters.length; - var cluster = null; - for (var i = length - 1; i >= 0; i--) { - cluster = clusters[i]; - var center = cluster.getCenter(); - if (center === null) { - continue; - } - center = map_.latLngToLayerPoint(center); - - // Found a cluster which contains the marker. - if (pos.x >= center.x - gridSize_ && pos.x <= center.x + gridSize_ && - pos.y >= center.y - gridSize_ && pos.y <= center.y + gridSize_) { - cluster.addMarker({ - 'isAdded': isAdded, - 'marker': marker - }); - if (!opt_isNodraw) { - cluster.redraw_(); - } - return; - } - } - - // No cluster contain the marker, create a new cluster. - cluster = new Cluster(this, map); - cluster.addMarker({ - 'isAdded': isAdded, - 'marker': marker - }); - if (!opt_isNodraw) { - cluster.redraw_(); - } - - // Add this cluster both in clusters provided and clusters_ - clusters.push(cluster); - if (clusters !== clusters_) { - clusters_.push(cluster); - } - }; - - /** - * Remove a marker. - * - * @param {GMarker} marker The marker you want to remove. - */ - - this.removeMarker = function (marker) { - for (var i = 0; i < clusters_.length; ++i) { - if (clusters_[i].remove(marker)) { - clusters_[i].redraw_(); - return; - } - } - }; - - /** - * Redraw all clusters in viewport. - */ - this.redraw_ = function () { - var clusters = this.getClustersInViewport_(); - for (var i = 0; i < clusters.length; ++i) { - clusters[i].redraw_(true); - } - }; - - /** - * Get all clusters in viewport. - * @return {Array of Cluster} - */ - this.getClustersInViewport_ = function () { - var clusters = []; - var curBounds = map_.getBounds(); - for (var i = 0; i < clusters_.length; i ++) { - if (clusters_[i].isInBounds(curBounds)) { - clusters.push(clusters_[i]); - } - } - return clusters; - }; - - /** - * Get max zoom level. - * @private - * @return {Number} - */ - this.getMaxZoom_ = function () { - return maxZoom_; - }; - - /** - * Get map object. - * @private - * @return {GMap2} - */ - this.getMap_ = function () { - return map_; - }; - - /** - * Get grid size - * @private - * @return {Number} - */ - this.getGridSize_ = function () { - return gridSize_; - }; - - /** - * Get total number of markers. - * @return {Number} - */ - this.getTotalMarkers = function () { - var result = 0; - for (var i = 0; i < clusters_.length; ++i) { - result += clusters_[i].getTotalMarkers(); - } - return result; - }; - - /** - * Get total number of clusters. - * @return {int} - */ - this.getTotalClusters = function () { - return clusters_.length; - }; - - /** - * Collect all markers of clusters in viewport and regroup them. - */ - this.resetViewport = function () { - var clusters = this.getClustersInViewport_(); - var tmpMarkers = []; - var removed = 0; - - for (var i = 0; i < clusters.length; ++i) { - var cluster = clusters[i]; - var oldZoom = cluster.getCurrentZoom(); - if (oldZoom === null) { - continue; - } - var curZoom = map_.getZoom(); - if (curZoom !== oldZoom) { - - // If the cluster zoom level changed then destroy the cluster - // and collect its markers. - var mks = cluster.getMarkers(); - for (var j = 0; j < mks.length; ++j) { - var newMarker = { - 'isAdded': false, - 'marker': mks[j].marker - }; - tmpMarkers.push(newMarker); - } - cluster.clearMarkers(); - removed++; - for (j = 0; j < clusters_.length; ++j) { - if (cluster === clusters_[j]) { - clusters_.splice(j, 1); - } - } - } - } - - // Add the markers collected into marker cluster to reset - reAddMarkers_(tmpMarkers); - this.redraw_(); - }; - - - /** - * Add a set of markers. - * - * @param {Array of GMarker} markers The markers you want to add. - */ - this.addMarkers = function (markers) { - for (var i = 0; i < markers.length; ++i) { - this.addMarker(markers[i], true); - } - this.redraw_(); - }; - - // initialize - if (typeof opt_markers === "object" && opt_markers !== null) { - this.addMarkers(opt_markers); - } - - // when map move end, regroup. - mcfn_ = map_.on("moveend", function () { - me_.resetViewport(); - }); -} +L.Marker.Clusterer = L.Class.extend({ + + includes: [L.Mixin.Events], + + initialize: function (map, opt_markers, opt_opts) { + // private members + this._clusters = []; + this._map = map; + this._maxZoom = null; + this._gridSize = 40; + this._sizes = [53, 56, 66, 78, 90]; + this._styles = []; + this._leftMarkers = []; + this._mcfn = null; + + var i = 0; + for (i = 1; i <= 5; ++i) { + this._styles.push({ + 'url': "http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/images/m" + i + ".png", + 'height': this._sizes[i - 1], + 'width': this._sizes[i - 1] + }); + } + + if (typeof opt_opts === "object" && opt_opts !== null) { + if (typeof opt_opts.gridSize === "number" && opt_opts.gridSize > 0) { + this._gridSize = opt_opts.gridSize; + } + if (typeof opt_opts.maxZoom === "number") { + this._maxZoom = opt_opts.maxZoom; + } + if (typeof opt_opts.styles === "object" && opt_opts.styles !== null && opt_opts.styles.length !== 0) { + this._styles = opt_opts.styles; + } + } + + // initialize + if (typeof opt_markers === "object" && opt_markers !== null) { + this.addMarkers(opt_markers); + } + + // when map move end, regroup. + this._mcfn = this._map.on("moveend", function() { this.resetViewport(); }, this); + + }, + + /** + * When we add a marker, the marker may not in the viewport of map, then we don't deal with it, instead + * we add the marker into a array called this._leftMarkers. When we reset L.Marker.Clusterer we should add the + * this._leftMarkers into L.Marker.Clusterer. + */ + _addLeftMarkers: function () { + if (this._leftMarkers.length === 0) { + return; + } + var leftMarkers = []; + for (i = 0; i < this._leftMarkers.length; ++i) { + this.addMarker(this._leftMarkers[i], true, null, null, true); + } + this._leftMarkers = leftMarkers; + }, + + /** + * Get cluster marker images of this marker cluster. Mostly used by {@link L.Marker.Cluster} + * @private + * @return {Array of String} + */ + getStyles: function () { + return this._styles; + }, + + /** + * Remove all markers from L.Marker.Clusterer. + */ + clearMarkers: function () { + for (var i = 0; i < this._clusters.length; ++i) { + if (typeof this._clusters[i] !== "undefined" && this._clusters[i] !== null) { + this._clusters[i].clearMarkers(); + } + } + this._clusters = []; + this._leftMarkers = []; + this._map.off(this._mcfn); + }, + + /** + * Check a marker, whether it is in current map viewport. + * @private + * @return {Boolean} if it is in current map viewport + */ + _isMarkerInViewport: function (marker) { + return this._map.getBounds().contains(marker.getLatLng()); + }, + + /** + * When reset L.Marker.Clusterer, there will be some markers get out of its cluster. + * These markers should be add to new clusters. + * @param {Array of L.Marker} markers Markers to add. + */ + _reAddMarkers: function (markers) { + var len = markers.length; + var clusters = []; + for (var i = len - 1; i >= 0; --i) { + this.addMarker(markers[i].marker, true, markers[i].isAdded, clusters, true); + } + this._addLeftMarkers(); + }, + + /** + * Add a marker. + * @private + * @param {L.Marker} marker Marker you want to add + * @param {Boolean} opt_isNodraw Whether redraw the cluster contained the marker + * @param {Boolean} opt_isAdded Whether the marker is added to map. Never use it. + * @param {Array of L.Marker.Cluster} opt_clusters Provide a list of clusters, the marker + * cluster will only check these cluster where the marker should join. + */ + addMarker: function (marker, opt_isNodraw, opt_isAdded, opt_clusters, opt_isNoCheck) { + if (opt_isNoCheck !== true) { + if (!this._isMarkerInViewport(marker)) { + this._leftMarkers.push(marker); + return; + } + } + + var isAdded = opt_isAdded; + var clusters = opt_clusters; + var pos = this._map.latLngToLayerPoint(marker.getLatLng()); + + if (typeof isAdded !== "boolean") { + isAdded = false; + } + if (typeof clusters !== "object" || clusters === null) { + clusters = this._clusters; + } + + var length = clusters.length; + var cluster = null; + for (var i = length - 1; i >= 0; i--) { + cluster = clusters[i]; + var center = cluster.getCenter(); + if (center === null) { + continue; + } + center = this._map.latLngToLayerPoint(center); + + // Found a cluster which contains the marker. + if (pos.x >= center.x - this._gridSize && pos.x <= center.x + this._gridSize && + pos.y >= center.y - this._gridSize && pos.y <= center.y + this._gridSize) + { + cluster.addMarker({ + 'isAdded': isAdded, + 'marker': marker + }); + if (!opt_isNodraw) { + cluster._redraw(); + } + return; + } + } + + // No cluster contain the marker, create a new cluster. + cluster = new L.Marker.Cluster(this, this._map); + cluster.addMarker({ + 'isAdded': isAdded, + 'marker': marker + }); + if (!opt_isNodraw) { + cluster._redraw(); + } + + // Add this cluster both in clusters provided and this._clusters + clusters.push(cluster); + if (clusters !== this._clusters) { + this._clusters.push(cluster); + } + }, + + /** + * Remove a marker. + * + * @param {L.Marker} marker The marker you want to remove. + */ + removeMarker: function (marker) { + for (var i = 0; i < this._clusters.length; ++i) { + if (this._clusters[i].remove(marker)) { + this._clusters[i]._redraw(); + return; + } + } + }, + + /** + * Redraw all clusters in viewport. + */ + redraw: function () { + var clusters = this.getClustersInViewport(); + for (var i = 0; i < clusters.length; ++i) { + clusters[i]._redraw(true); + } + }, + + /** + * Get all clusters in viewport. + * @return {Array of L.Marker.Cluster} + */ + getClustersInViewport: function () { + var clusters = []; + var curBounds = this._map.getBounds(); + for (var i = 0; i < this._clusters.length; i ++) { + if (this._clusters[i].isInBounds(curBounds)) { + clusters.push(this._clusters[i]); + } + } + return clusters; + }, + + /** + * Get max zoom level. + * @private + * @return {Number} + */ + getMaxZoom: function () { + return this._maxZoom; + }, + + /** + * Get map object. + * @private + * @return {L.Map} + */ + getMap: function () { + return this._map; + }, + + /** + * Get grid size + * @private + * @return {Number} + */ + getGridSize: function () { + return this._gridSize; + }, + + /** + * Get total number of markers. + * @return {Number} + */ + getTotalMarkers: function () { + var result = 0; + for (var i = 0; i < this._clusters.length; ++i) { + result += this._clusters[i].getTotalMarkers(); + } + return result; + }, + + /** + * Get total number of clusters. + * @return {int} + */ + getTotalClusters: function () { + return this._clusters.length; + }, + + /** + * Collect all markers of clusters in viewport and regroup them. + */ + resetViewport: function () { + var clusters = this.getClustersInViewport(); + var tmpMarkers = []; + var removed = 0; + + for (var i = 0; i < clusters.length; ++i) { + var cluster = clusters[i]; + var oldZoom = cluster.getCurrentZoom(); + if (oldZoom === null) { + continue; + } + var curZoom = this._map.getZoom(); + if (curZoom !== oldZoom) { + // If the cluster zoom level changed then destroy the cluster + // and collect its markers. + var mks = cluster.getMarkers(); + for (var j = 0; j < mks.length; ++j) { + var newMarker = { + 'isAdded': false, + 'marker': mks[j].marker + }; + tmpMarkers.push(newMarker); + } + cluster.clearMarkers(); + removed++; + for (j = 0; j < this._clusters.length; ++j) { + if (cluster === this._clusters[j]) { + this._clusters.splice(j, 1); + } + } + } + } + + // Add the markers collected into marker cluster to reset + this._reAddMarkers(tmpMarkers); + this.redraw(); + }, + + /** + * Add a set of markers. + * + * @param {Array of L.Marker} markers The markers you want to add. + */ + addMarkers: function (markers) { + for (var i = 0; i < markers.length; ++i) { + this.addMarker(markers[i], true); + } + this.redraw(); + } +}); /** * Create a cluster to collect markers. * A cluster includes some markers which are in a block of area. * If there are more than one markers in cluster, the cluster - * will create a {@link ClusterMarker_} and show the total number + * will create a {@link L.Marker.ClusterMarker} and show the total number * of markers in cluster. * * @constructor * @private - * @param {LeafClusterer} leafClusterer The marker cluster object + * @param {clusterer} clusterer The marker clusterer object */ -function Cluster(leafClusterer) { - var center_ = null; - var markers_ = []; - var leafClusterer_ = leafClusterer; - var map_ = leafClusterer.getMap_(); - var clusterMarker_ = null; - var zoom_ = map_.getZoom(); - - /** - * Get markers of this cluster. - * - * @return {Array of GMarker} - */ - this.getMarkers = function () { - return markers_; - }; - - /** - * If this cluster intersects certain bounds. - * - * @param {GLatLngBounds} bounds A bounds to test - * @return {Boolean} Is this cluster intersects the bounds - */ - this.isInBounds = function (bounds) { - if (center_ === null) { - return false; - } - - if (!bounds) { - bounds = map_.getBounds(); - } - var sw = map_.latLngToLayerPoint(bounds.getSouthWest()); - var ne = map_.latLngToLayerPoint(bounds.getNorthEast()); - - var centerxy = map_.latLngToLayerPoint(center_); - var inViewport = true; - var gridSize = leafClusterer.getGridSize_(); - if (zoom_ !== map_.getZoom()) { - var dl = map_.getZoom() - zoom_; - gridSize = Math.pow(2, dl) * gridSize; - } - if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x)) { - inViewport = false; - } - if (inViewport && (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)) { - inViewport = false; - } - return inViewport; - }; - - /** - * Get cluster center. - * - * @return {GLatLng} - */ - this.getCenter = function () { - return center_; - }; - - /** - * Add a marker. - * - * @param {Object} marker An object of marker you want to add: - * {Boolean} isAdded If the marker is added on map. - * {GMarker} marker The marker you want to add. - */ - this.addMarker = function (marker) { - if (center_ === null) { - center_ = marker.marker.getLatLng(); - } - markers_.push(marker); - }; - - /** - * Remove a marker from cluster. - * - * @param {GMarker} marker The marker you want to remove. - * @return {Boolean} Whether find the marker to be removed. - */ - this.removeMarker = function (marker) { - for (var i = 0; i < markers_.length; ++i) { - if (marker === markers_[i].marker) { - if (markers_[i].isAdded) { - map_.removeLayer(markers_[i].marker); - } - markers_.splice(i, 1); - return true; - } - } - return false; - }; - - /** - * Get current zoom level of this cluster. - * Note: the cluster zoom level and map zoom level not always the same. - * - * @return {Number} - */ - this.getCurrentZoom = function () { - return zoom_; - }; - - /** - * Redraw a cluster. - * @private - * @param {Boolean} isForce If redraw by force, no matter if the cluster is - * in viewport. - */ - this.redraw_ = function (isForce) { - if (!isForce && !this.isInBounds()) { - return; - } - - // Set cluster zoom level. - zoom_ = map_.getZoom(); - var i = 0; - var mz = leafClusterer.getMaxZoom_(); - if (mz === null) { - mz = map_.getMaxZoom(); - } - if (zoom_ >= mz || this.getTotalMarkers() === 1) { - // If current zoom level is beyond the max zoom level or the cluster - // have only one marker, the marker(s) in cluster will be showed on map. - for (i = 0; i < markers_.length; ++i) { - map_.addLayer(markers_[i].marker); - markers_[i].isAdded = true; - } - if (clusterMarker_ !== null) - clusterMarker_.hide(); - } else { - // Else add a cluster marker on map to show the number of markers in - // this cluster. - for (i = 0; i < markers_.length; ++i) { - if (markers_[i].isAdded) { - map_.removeLayer(markers_[i].marker); - } - } - if (clusterMarker_ === null) { - clusterMarker_ = new ClusterMarker_(center_, this.getTotalMarkers(), leafClusterer_.getStyles_(), leafClusterer_.getGridSize_() / 2); - map_.addLayer(clusterMarker_); - } else { - clusterMarker_.reset({count: this.getTotalMarkers()}); - clusterMarker_.redraw(); - if (clusterMarker_.isHidden()) { - clusterMarker_.show(); - } - } - } - }; - - /** - * Remove all the markers from this cluster. - */ - this.clearMarkers = function () { - if (clusterMarker_ !== null) { - map_.removeLayer(clusterMarker_); - } - for (var i = 0; i < markers_.length; ++i) { - if (markers_[i].isAdded) { - map_.removeLayer(markers_[i].marker); - } - } - markers_ = []; - }; - - /** - * Get number of markers. - * @return {Number} - */ - this.getTotalMarkers = function () { - return markers_.length; - }; -} - -ClusterMarker_ = L.Class.extend({ - initialize: function(latLng_, count_, styles_, padding_) { - this.reset({latLng:latLng_, count: count_, styles: styles_, padding: padding_}); - }, - - reset: function(opts) { - if (!opts || typeof opts !== "object") - return; - - var updated = 0; - if (typeof opts.latLng === "object" && opts.latLng != this.latlng_) { - this.latlng_ = opts.latLng; - updated = 1; - } - - var styles_updated = 0; - if (typeof opts.styles === "object" && opts.styles != this.styles_) { - this.styles_ = opts.styles; - updated = 1; - styles_updated = 1; - } - - if (typeof opts.count === "number" && opts.count != this.count_ || styles_updated) { - this.count_ = opts.count; - - var index = 0; - var dv = this.count_; - while (dv !== 0) { - dv = parseInt(dv / 10, 10); - index ++; - } - - var styles = this.styles_; - - if (styles.length < index) { - index = styles.length; - } - this.url_ = styles[index - 1].url; - this.height_ = styles[index - 1].height; - this.width_ = styles[index - 1].width; - this.textColor_ = styles[index - 1].opt_textColor; - this.anchor_ = styles[index - 1].opt_anchor; - this.index_ = index; - } - - if (typeof opts.padding === "number" && this.padding_ != opts.padding) { - this.padding_ = opts.padding; - updated = 1; - } - - this.updated |= updated; - }, - - onAdd: function(map) { - this.map_ = map; - this.container_ = L.DomUtil.create('div', 'cluster-marker-container'); - map.getPanes().overlayPane.appendChild(this.container_); - var cluster = this; - - if (this.container_.addEventListener) { - this.container_.addEventListener("click", - function() { - cluster.onClick_(cluster); - }, false); - } else if (this.container_.attachEvent) { - this.container_.attachEvent("onclick", - function() { - cluster.onClick_(cluster); - }); - } - map.on('viewreset', this.redraw, this); - this.redraw(); - }, - - onClick_: function(cluster) { - var padding = cluster.padding_; - var map = cluster.map_; - - var pos = cluster.map_.latLngToLayerPoint(cluster.latlng_); - var sw = new L.Point(pos.x - padding, pos.y + padding); - sw = map.layerPointToLatLng(sw); - var ne = new L.Point(pos.x + padding, pos.y - padding); - ne = map.layerPointToLatLng(ne); - var zoom = map.getBoundsZoom(new L.LatLngBounds(sw, ne)); - map.setView(cluster.latlng_, zoom); - }, - - onRemove: function(map) { - map.getPanes().overlayPane.removeChild(this.container_); - map.off('viewreset', this.redraw, this); - }, - - redraw: function() { - if (this.div_ && this.updated) { - this.container_.removeChild(this.div_); - this.div_ = null; - } - if (!this.div_) { - this.div_ = this.initLayout_(); - this.container_.appendChild(this.div_); - } - - var pos = this.map_.latLngToLayerPoint(this.latlng_); - pos.x -= parseInt(this.width_ / 2, 10); - pos.y -= parseInt(this.height_ / 2, 10); - this.container_.style.top = pos.y + "px"; - this.container_.style.left = pos.x + "px"; - }, - - hide: function() { - this.div_.style.display = "none"; - }, - - show: function() { - this.div_.style.display = ""; - }, - - isHidden: function () { - return this.div_.style.display === "none"; - }, - - initLayout_: function() { - var div = L.DomUtil.create('div', 'cluster-marker'); - var latlng = this.latlng_; - var pos = this.map_.latLngToLayerPoint(latlng); - pos.x -= parseInt(this.width_ / 2, 10); - pos.y -= parseInt(this.height_ / 2, 10); - var mstyle = ""; - - if (document.all) { - mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");'; - } else { - mstyle = "background:url(" + this.url_ + ");"; - } - if (typeof this.anchor_ === "object") { - if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) { - mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;'; - } else { - mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;'; - } - if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) { - mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;'; - } else { - mstyle += 'width:' + this.width_ + 'px;text-align:center;'; - } - } else { - mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;'; - mstyle += 'width:' + this.width_ + 'px;text-align:center;'; - } - var txtColor = this.textColor_ ? this.textColor_ : 'black'; - - div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" + - pos.x + "px;color:" + txtColor + ";position:absolute;font-size:11px;" + - 'font-family:Arial,sans-serif;font-weight:bold'; - div.innerHTML = this.count_; +L.Marker.Cluster = L.Class.extend({ + initialize: function (clusterer) { + this._center = null; + this._markers = []; + this._clusterer = clusterer; + this._map = clusterer.getMap(); + this._clusterMarker = null; + this._zoom = this._map.getZoom(); + }, + + /** + * Get markers of this cluster. + * + * @return {Array of L.Marker} + */ + getMarkers: function () { + return this._markers; + }, + + /** + * If this cluster intersects certain bounds. + * + * @param {GLatLngBounds} bounds A bounds to test + * @return {Boolean} Is this cluster intersects the bounds + */ + isInBounds: function (bounds) { + if (this._center === null) { + return false; + } + if (!bounds) { + bounds = this._map.getBounds(); + } + var sw = this._map.latLngToLayerPoint(bounds.getSouthWest()); + var ne = this._map.latLngToLayerPoint(bounds.getNorthEast()); + + var centerxy = this._map.latLngToLayerPoint(this._center); + var inViewport = true; + var gridSize = this._clusterer.getGridSize(); + if (this._zoom !== this._map.getZoom()) { + var dl = this._map.getZoom() - this._zoom; + gridSize = Math.pow(2, dl) * gridSize; + } + if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x)) { + inViewport = false; + } + if (inViewport && (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)) { + inViewport = false; + } + return inViewport; + }, + + /** + * Get cluster center. + * + * @return {GLatLng} + */ + getCenter: function () { + return this._center; + }, + + /** + * Add a marker. + * + * @param {Object} marker An object of marker you want to add: + * {Boolean} isAdded If the marker is added on map. + * {L.Marker} marker The marker you want to add. + */ + addMarker: function (marker) { + if (this._center === null) { + this._center = marker.marker.getLatLng(); + } + this._markers.push(marker); + }, + + /** + * Remove a marker from cluster. + * + * @param {L.Marker} marker The marker you want to remove. + * @return {Boolean} Whether find the marker to be removed. + */ + removeMarker: function (marker) { + for (var i = 0; i < this._markers.length; ++i) { + if (marker === this._markers[i].marker) { + if (this._markers[i].isAdded) { + this._map.removeLayer(this._markers[i].marker); + } + this._markers.splice(i, 1); + return true; + } + } + return false; + }, + + /** + * Get current zoom level of this cluster. + * Note: the cluster zoom level and map zoom level not always the same. + * + * @return {Number} + */ + getCurrentZoom: function () { + return this._zoom; + }, + + /** + * Redraw a cluster. + * @private + * @param {Boolean} isForce If redraw by force, no matter if the cluster is + * in viewport. + */ + _redraw: function (isForce) { + if (!isForce && !this.isInBounds()) { + return; + } + + // Set cluster zoom level. + this._zoom = this._map.getZoom(); + var i = 0; + var mz = this._clusterer.getMaxZoom(); + if (mz === null) { + mz = this._map.getMaxZoom(); + } + if (this._zoom >= mz || this.getTotalMarkers() === 1) { + // If current zoom level is beyond the max zoom level or the cluster + // have only one marker, the marker(s) in cluster will be showed on map. + for (i = 0; i < this._markers.length; ++i) { + this._map.addLayer(this._markers[i].marker); + this._markers[i].isAdded = true; + } + if (this._clusterMarker !== null) + this._clusterMarker.hide(); + } else { + // Else add a cluster marker on map to show the number of markers in + // this cluster. + for (i = 0; i < this._markers.length; ++i) { + if (this._markers[i].isAdded) { + this._map.removeLayer(this._markers[i].marker); + } + } + if (this._clusterMarker === null) { + this._clusterMarker = new L.Marker.ClusterMarker(this, this._clusterer.getStyles()); + this._map.addLayer(this._clusterMarker); + } else { + this._clusterMarker.reset({count: this.getTotalMarkers()}); + this._clusterMarker.redraw(); + if (this._clusterMarker.isHidden()) { + this._clusterMarker.show(); + } + } + } + }, + + /** + * Remove all the markers from this cluster. + */ + clearMarkers: function () { + if (this._clusterMarker !== null) { + this._map.removeLayer(this._clusterMarker); + } + for (var i = 0; i < this._markers.length; ++i) { + if (this._markers[i].isAdded) { + this._map.removeLayer(this._markers[i].marker); + } + } + this._markers = []; + }, + + /** + * Get number of markers. + * @return {Number} + */ + getTotalMarkers: function () { + return this._markers.length; + } +}); - return div; - } +L.Marker.ClusterMarker = L.Class.extend({ + initialize: function(cluster, styles) { + this._cluster = cluster; + this.reset({ + latLng:cluster.getCenter(), + count: cluster.getTotalMarkers(), + styles: styles, + padding: cluster._clusterer.getGridSize() / 2 + }); + }, + + reset: function(opts) { + if (!opts || typeof opts !== "object") + return; + + var updated = 0; + if (typeof opts.latLng === "object" && opts.latLng != this._latlng) { + this._latlng = opts.latLng; + updated = 1; + } + + var styles_updated = 0; + if (typeof opts.styles === "object" && opts.styles != this._styles) { + this._styles = opts.styles; + updated = 1; + styles_updated = 1; + } + + if (typeof opts.count === "number" && opts.count != this._count || styles_updated) { + this._count = opts.count; + + var index = 0; + var dv = this._count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index ++; + } + + var styles = this._styles; + + if (styles.length < index) { + index = styles.length; + } + this.url_ = styles[index - 1].url; + this.height_ = styles[index - 1].height; + this.width_ = styles[index - 1].width; + this.textColor_ = styles[index - 1].opt_textColor; + this.anchor_ = styles[index - 1].opt_anchor; + this.index_ = index; + } + + if (typeof opts.padding === "number" && this._padding != opts.padding) { + this._padding = opts.padding; + updated = 1; + } + + this.updated |= updated; + }, + + onAdd: function(map) { + this._map = map; + this._container = L.DomUtil.create('div', 'cluster-marker-container'); + map.getPanes().overlayPane.appendChild(this._container); + var me = this; + + if (this._container.addEventListener) { + this._container.addEventListener("click", function() { me._onClick(); }, me); + } else if (this._container.attachEvent) { + this._container.attachEvent("onclick", function() { me._onClick(); }, me); + } + map.on('viewreset', this.redraw, this); + this.redraw(); + }, + + _onClick: function() { + var padding = this._padding; + var map = this._map; + + var pos = map.latLngToLayerPoint(this._latlng); + var sw = new L.Point(pos.x - padding, pos.y + padding); + sw = map.layerPointToLatLng(sw); + var ne = new L.Point(pos.x + padding, pos.y - padding); + ne = map.layerPointToLatLng(ne); + var zoom = map.getBoundsZoom(new L.LatLngBounds(sw, ne)); + map.setView(this._latlng, zoom); + // fire click event for clusterer + this._cluster._clusterer.fire("click", {cluster: this._cluster}); + }, + + onRemove: function(map) { + map.getPanes().overlayPane.removeChild(this._container); + map.off('viewreset', this.redraw, this); + }, + + redraw: function() { + if (this._div && this.updated) { + this._container.removeChild(this._div); + this._div = null; + } + if (!this._div) { + this._div = this.initLayout_(); + this._container.appendChild(this._div); + } + + var pos = this._map.latLngToLayerPoint(this._latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + this._container.style.top = pos.y + "px"; + this._container.style.left = pos.x + "px"; + }, + + hide: function() { + this._div.style.display = "none"; + }, + + show: function() { + this._div.style.display = ""; + }, + + isHidden: function () { + return this._div.style.display === "none"; + }, + + initLayout_: function() { + var div = L.DomUtil.create('div', 'cluster-marker'); + var latlng = this._latlng; + var pos = this._map.latLngToLayerPoint(latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + var mstyle = ""; + + if (document.all) { + mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");'; + } else { + mstyle = "background:url(" + this.url_ + ");"; + } + if (typeof this.anchor_ === "object") { + if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) { + mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;'; + } else { + mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;'; + } + if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) { + mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;'; + } else { + mstyle += 'width:' + this.width_ + 'px;text-align:center;'; + } + } else { + mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;'; + mstyle += 'width:' + this.width_ + 'px;text-align:center;'; + } + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + + div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" + + pos.x + "px;color:" + txtColor + ";position:absolute;font-size:11px;" + + 'font-family:Arial,sans-serif;font-weight:bold'; + div.innerHTML = this._count; + + return div; + } });