diff options
-rw-r--r-- | chimere/static/chimere/js/clustering.js | 278 | ||||
-rw-r--r-- | chimere/templates/chimere/blocks/head_chimere.html | 1 | ||||
-rw-r--r-- | chimere/templates/chimere/blocks/map.html | 1 | ||||
-rw-r--r-- | chimere/templatetags/chimere_tags.py | 8 |
4 files changed, 285 insertions, 3 deletions
diff --git a/chimere/static/chimere/js/clustering.js b/chimere/static/chimere/js/clustering.js new file mode 100644 index 0000000..b3a142c --- /dev/null +++ b/chimere/static/chimere/js/clustering.js @@ -0,0 +1,278 @@ +/* + Add a recluster to Cluster class. + probably part of OpenLayers 2.13 +*/ + + +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. + */ + + /** + * 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 || (event && event.recluster)) && this.features) { + var resolution = this.layer.map.getResolution(); + if(resolution != this.resolution || !this.clustersExist() || (event && event.recluster)) { + 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: recluster + * User-callable function to recluster features + * Useful for instances where a clustering attribute (distance, threshold, ...) + * has changed + */ + recluster: function(){ + var event={"recluster":true}; + this.cluster(event); + }, + + /** + * 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" +}); diff --git a/chimere/templates/chimere/blocks/head_chimere.html b/chimere/templates/chimere/blocks/head_chimere.html index e0c2a0b..7ed2b54 100644 --- a/chimere/templates/chimere/blocks/head_chimere.html +++ b/chimere/templates/chimere/blocks/head_chimere.html @@ -3,6 +3,7 @@ {% for js_url in OSM_JS_URLS %} <script src="{{ js_url }}" type="text/javascript"></script>{% endfor %} {% if routing %}<script src="{{ STATIC_URL }}chimere/js/routing-widget.js" type="text/javascript"></script>{% endif %} +{% if enable_clustering %}<script src="{{ STATIC_URL }}chimere/js/clustering.js" type="text/javascript"></script>{% endif %} <script src="{{ STATIC_URL }}chimere/js/jquery.chimere.js" type="text/javascript"></script> <script type="text/javascript"> /* Global variables */ diff --git a/chimere/templates/chimere/blocks/map.html b/chimere/templates/chimere/blocks/map.html index c93dd10..75be866 100644 --- a/chimere/templates/chimere/blocks/map.html +++ b/chimere/templates/chimere/blocks/map.html @@ -20,6 +20,7 @@ $(function() {$('#panel').hide()});{% endif %} var chimere_init_options = {}; {% if MOBILE %}chimere_init_options["mobile"] = true;{% endif %} + {% if enable_clustering %}chimere_init_options["enable_clustering"] = true;{% endif %} chimere_init_options["default_icon"] = '{{STATIC_URL}}img/marker-green.png'; chimere_init_options["map_layers"] = [{{js_map_layers|safe|escape}}]; chimere_init_options['permalink_label'] = '{%trans "Permalink"%}'; diff --git a/chimere/templatetags/chimere_tags.py b/chimere/templatetags/chimere_tags.py index c4b4c95..4f3dff0 100644 --- a/chimere/templatetags/chimere_tags.py +++ b/chimere/templatetags/chimere_tags.py @@ -144,8 +144,9 @@ def head_chimere(context): "OSM_CSS_URLS": settings.OSM_CSS_URLS, 'MOBILE':context['MOBILE'], 'routing': settings.CHIMERE_ENABLE_ROUTING \ - if hasattr(settings, 'CHIMERE_ENABLE_ROUTING') else False - } + if hasattr(settings, 'CHIMERE_ENABLE_ROUTING') else False, + 'enable_clustering':settings.CHIMERE_ENABLE_CLUSTERING, + } context_data['OSM_JS_URLS'] = settings.OSM_JS_URLS if context['MOBILE']: context_data['OSM_JS_URLS'] = settings.OSM_MOBILE_JS_URLS @@ -184,7 +185,8 @@ def routing(context): @register.inclusion_tag('chimere/blocks/map.html', takes_context=True) def map(context, map_id='map'): context_data = {'map_id':map_id, - "STATIC_URL": settings.STATIC_URL} + "STATIC_URL": settings.STATIC_URL, + "enable_clustering":settings.CHIMERE_ENABLE_CLUSTERING} context_data['icon_offset_x'] = settings.CHIMERE_ICON_OFFSET_X context_data['icon_offset_y'] = settings.CHIMERE_ICON_OFFSET_Y context_data['icon_width'] = settings.CHIMERE_ICON_WIDTH |