|
/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for |
|
* full list of contributors). Published under the Clear BSD license. |
|
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the |
|
* full text of the license. */ |
|
|
|
/** |
|
* @requires OpenLayers/Strategy.js |
|
*/ |
|
|
|
/** |
|
* Class: OpenLayers.Strategy.Cluster |
|
* Strategy for vector feature clustering. |
|
* |
|
* Inherits from: |
|
* - <OpenLayers.Strategy> |
|
*/ |
|
OpenLayers.Strategy.Cluster = OpenLayers.Class(OpenLayers.Strategy, { |
|
|
|
/** |
|
* APIProperty: distance |
|
* {Integer} Pixel distance between features that should be considered a |
|
* single cluster. Default is 20 pixels. |
|
*/ |
|
distance: 20, |
|
|
|
/** |
|
* APIProperty: threshold |
|
* {Integer} Optional threshold below which original features will be |
|
* added to the layer instead of clusters. For example, a threshold |
|
* of 3 would mean that any time there are 2 or fewer features in |
|
* a cluster, those features will be added directly to the layer instead |
|
* of a cluster representing those features. Default is null (which is |
|
* equivalent to 1 - meaning that clusters may contain just one feature). |
|
*/ |
|
threshold: null, |
|
|
|
/** |
|
* Property: features |
|
* {Array(<OpenLayers.Feature.Vector>)} Cached features. |
|
*/ |
|
features: null, |
|
|
|
/** |
|
* Property: clusters |
|
* {Array(<OpenLayers.Feature.Vector>)} Calculated clusters. |
|
*/ |
|
clusters: null, |
|
|
|
/** |
|
* Property: clustering |
|
* {Boolean} The strategy is currently clustering features. |
|
*/ |
|
clustering: false, |
|
|
|
/** |
|
* Property: resolution |
|
* {Float} The resolution (map units per pixel) of the current cluster set. |
|
*/ |
|
resolution: null, |
|
|
|
/** |
|
* Constructor: OpenLayers.Strategy.Cluster |
|
* Create a new clustering strategy. |
|
* |
|
* Parameters: |
|
* options - {Object} Optional object whose properties will be set on the |
|
* instance. |
|
*/ |
|
initialize: function(options) { |
|
OpenLayers.Strategy.prototype.initialize.apply(this, [options]); |
|
}, |
|
|
|
/** |
|
* APIMethod: activate |
|
* Activate the strategy. Register any listeners, do appropriate setup. |
|
* |
|
* Returns: |
|
* {Boolean} The strategy was successfully activated. |
|
*/ |
|
activate: function() { |
|
var activated = OpenLayers.Strategy.prototype.activate.call(this); |
|
if(activated) { |
|
this.layer.events.on({ |
|
"beforefeaturesadded": this.cacheFeatures, |
|
"moveend": this.cluster, |
|
scope: this |
|
}); |
|
} |
|
return activated; |
|
}, |
|
|
|
/** |
|
* APIMethod: deactivate |
|
* Deactivate the strategy. Unregister any listeners, do appropriate |
|
* tear-down. |
|
* |
|
* Returns: |
|
* {Boolean} The strategy was successfully deactivated. |
|
*/ |
|
deactivate: function() { |
|
var deactivated = OpenLayers.Strategy.prototype.deactivate.call(this); |
|
if(deactivated) { |
|
this.clearCache(); |
|
this.layer.events.un({ |
|
"beforefeaturesadded": this.cacheFeatures, |
|
"moveend": this.cluster, |
|
scope: this |
|
}); |
|
} |
|
return deactivated; |
|
}, |
|
|
|
/** |
|
* Method: cacheFeatures |
|
* Cache features before they are added to the layer. |
|
* |
|
* Parameters: |
|
* event - {Object} The event that this was listening for. This will come |
|
* with a batch of features to be clustered. |
|
* |
|
* Returns: |
|
* {Boolean} False to stop features from being added to the layer. |
|
*/ |
|
cacheFeatures: function(event) { |
|
var propagate = true; |
|
if(!this.clustering) { |
|
this.clearCache(); |
|
this.features = event.features; |
|
this.cluster(); |
|
propagate = false; |
|
} |
|
return propagate; |
|
}, |
|
|
|
/** |
|
* Method: clearCache |
|
* Clear out the cached features. |
|
*/ |
|
clearCache: function() { |
|
this.features = null; |
|
}, |
|
|
|
/** |
|
* Method: cluster |
|
* Cluster features based on some threshold distance. |
|
* |
|
* Parameters: |
|
* event - {Object} The event received when cluster is called as a |
|
* result of a moveend event. |
|
*/ |
|
cluster: function(event) { |
|
if((!event || event.zoomChanged) && this.features) { |
|
var resolution = this.layer.map.getResolution(); |
|
if(resolution != this.resolution || !this.clustersExist()) { |
|
this.resolution = resolution; |
|
var clusters = []; |
|
var feature, clustered, cluster; |
|
for(var i=0; i<this.features.length; ++i) { |
|
feature = this.features[i]; |
|
if(feature.geometry) { |
|
clustered = false; |
|
for(var j=clusters.length-1; j>=0; --j) { |
|
cluster = clusters[j]; |
|
if(this.shouldCluster(cluster, feature)) { |
|
this.addToCluster(cluster, feature); |
|
clustered = true; |
|
break; |
|
} |
|
} |
|
if(!clustered) { |
|
clusters.push(this.createCluster(this.features[i])); |
|
} |
|
} |
|
} |
|
this.layer.removeAllFeatures(); |
|
if(clusters.length > 0) { |
|
if(this.threshold > 1) { |
|
var clone = clusters.slice(); |
|
clusters = []; |
|
var candidate; |
|
for(var i=0, len=clone.length; i<len; ++i) { |
|
candidate = clone[i]; |
|
if(candidate.attributes.count < this.threshold) { |
|
Array.prototype.push.apply(clusters, candidate.cluster); |
|
} else { |
|
clusters.push(candidate); |
|
} |
|
} |
|
} |
|
this.clustering = true; |
|
// A legitimate feature addition could occur during this |
|
// addFeatures call. For clustering to behave well, features |
|
// should be removed from a layer before requesting a new batch. |
|
this.layer.addFeatures(clusters); |
|
this.clustering = false; |
|
} |
|
this.clusters = clusters; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Method: clustersExist |
|
* Determine whether calculated clusters are already on the layer. |
|
* |
|
* Returns: |
|
* {Boolean} The calculated clusters are already on the layer. |
|
*/ |
|
clustersExist: function() { |
|
var exist = false; |
|
if(this.clusters && this.clusters.length > 0 && |
|
this.clusters.length == this.layer.features.length) { |
|
exist = true; |
|
for(var i=0; i<this.clusters.length; ++i) { |
|
if(this.clusters[i] != this.layer.features[i]) { |
|
exist = false; |
|
break; |
|
} |
|
} |
|
} |
|
return exist; |
|
}, |
|
|
|
/** |
|
* Method: shouldCluster |
|
* Determine whether to include a feature in a given cluster. |
|
* |
|
* Parameters: |
|
* cluster - {<OpenLayers.Feature.Vector>} A cluster. |
|
* feature - {<OpenLayers.Feature.Vector>} A feature. |
|
* |
|
* Returns: |
|
* {Boolean} The feature should be included in the cluster. |
|
*/ |
|
shouldCluster: function(cluster, feature) { |
|
var cc = cluster.geometry.getBounds().getCenterLonLat(); |
|
var fc = feature.geometry.getBounds().getCenterLonLat(); |
|
var distance = ( |
|
Math.sqrt( |
|
Math.pow((cc.lon - fc.lon), 2) + Math.pow((cc.lat - fc.lat), 2) |
|
) / this.resolution |
|
); |
|
return (distance <= this.distance); |
|
}, |
|
|
|
/** |
|
* Method: addToCluster |
|
* Add a feature to a cluster. |
|
* |
|
* Parameters: |
|
* cluster - {<OpenLayers.Feature.Vector>} A cluster. |
|
* feature - {<OpenLayers.Feature.Vector>} A feature. |
|
*/ |
|
addToCluster: function(cluster, feature) { |
|
cluster.cluster.push(feature); |
|
cluster.attributes.count += 1; |
|
}, |
|
|
|
/** |
|
* Method: createCluster |
|
* Given a feature, create a cluster. |
|
* |
|
* Parameters: |
|
* feature - {<OpenLayers.Feature.Vector>} |
|
* |
|
* Returns: |
|
* {<OpenLayers.Feature.Vector>} A cluster. |
|
*/ |
|
createCluster: function(feature) { |
|
var center = feature.geometry.getBounds().getCenterLonLat(); |
|
var cluster = new OpenLayers.Feature.Vector( |
|
new OpenLayers.Geometry.Point(center.lon, center.lat), |
|
{count: 1} |
|
); |
|
cluster.cluster = [feature]; |
|
return cluster; |
|
}, |
|
|
|
CLASS_NAME: "OpenLayers.Strategy.Cluster" |
|
}); |
|
|