From 4ad8f2118cc47819b777bc9386c3e7be6213d7e5 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 12 Sep 2008 08:58:23 +0000 Subject: [PATCH] Adding paging and cluster strategies. The paging strategy intercepts a batch of features bound for the layer and caches them, giving the layer one page at a time. The cluster strategy intercepts a batch of features and groups proximate features as clusters - giving the clusters to the layer instead. Thanks for the careful review Erik. r=euzuro (see #1606). git-svn-id: http://svn.openlayers.org/trunk/openlayers@8003 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf --- examples/animator.js | 670 +++++++++++++++++++++++++++++ examples/strategy-cluster.html | 201 +++++++++ examples/strategy-paging.html | 78 ++++ lib/OpenLayers.js | 2 + lib/OpenLayers/Layer/Vector.js | 26 +- lib/OpenLayers/Strategy/Cluster.js | 261 +++++++++++ lib/OpenLayers/Strategy/Paging.js | 241 +++++++++++ tests/Strategy/Cluster.html | 108 +++++ tests/Strategy/Paging.html | 113 +++++ tests/list-tests.html | 2 + 10 files changed, 1697 insertions(+), 5 deletions(-) create mode 100644 examples/animator.js create mode 100644 examples/strategy-cluster.html create mode 100644 examples/strategy-paging.html create mode 100644 lib/OpenLayers/Strategy/Cluster.js create mode 100644 lib/OpenLayers/Strategy/Paging.js create mode 100644 tests/Strategy/Cluster.html create mode 100644 tests/Strategy/Paging.html diff --git a/examples/animator.js b/examples/animator.js new file mode 100644 index 0000000000..abe540377b --- /dev/null +++ b/examples/animator.js @@ -0,0 +1,670 @@ +/* + Animator.js 1.1.9 + + This library is released under the BSD license: + + Copyright (c) 2006, Bernard Sumption. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. Redistributions in binary + form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials + provided with the distribution. Neither the name BernieCode nor + the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + +*/ + + +// Applies a sequence of numbers between 0 and 1 to a number of subjects +// construct - see setOptions for parameters +function Animator(options) { + this.setOptions(options); + var _this = this; + this.timerDelegate = function(){_this.onTimerEvent()}; + this.subjects = []; + this.target = 0; + this.state = 0; + this.lastTime = null; +}; +Animator.prototype = { + // apply defaults + setOptions: function(options) { + this.options = Animator.applyDefaults({ + interval: 20, // time between animation frames + duration: 400, // length of animation + onComplete: function(){}, + onStep: function(){}, + transition: Animator.tx.easeInOut + }, options); + }, + // animate from the current state to provided value + seekTo: function(to) { + this.seekFromTo(this.state, to); + }, + // animate from the current state to provided value + seekFromTo: function(from, to) { + this.target = Math.max(0, Math.min(1, to)); + this.state = Math.max(0, Math.min(1, from)); + this.lastTime = new Date().getTime(); + if (!this.intervalId) { + this.intervalId = window.setInterval(this.timerDelegate, this.options.interval); + } + }, + // animate from the current state to provided value + jumpTo: function(to) { + this.target = this.state = Math.max(0, Math.min(1, to)); + this.propagate(); + }, + // seek to the opposite of the current target + toggle: function() { + this.seekTo(1 - this.target); + }, + // add a function or an object with a method setState(state) that will be called with a number + // between 0 and 1 on each frame of the animation + addSubject: function(subject) { + this.subjects[this.subjects.length] = subject; + return this; + }, + // remove all subjects + clearSubjects: function() { + this.subjects = []; + }, + // forward the current state to the animation subjects + propagate: function() { + var value = this.options.transition(this.state); + for (var i=0; i= Math.abs(this.state - this.target)) { + this.state = this.target; + } else { + this.state += movement; + } + + try { + this.propagate(); + } finally { + this.options.onStep.call(this); + if (this.target == this.state) { + window.clearInterval(this.intervalId); + this.intervalId = null; + this.options.onComplete.call(this); + } + } + }, + // shortcuts + play: function() {this.seekFromTo(0, 1)}, + reverse: function() {this.seekFromTo(1, 0)}, + // return a string describing this Animator, for debugging + inspect: function() { + var str = "# 20) return; + } + }, + getStyle: function(state) { + state = this.from + ((this.to - this.from) * state); + if (this.property == 'filter') return "alpha(opacity=" + Math.round(state*100) + ")"; + if (this.property == 'opacity') return state; + return Math.round(state) + this.units; + }, + inspect: function() { + return "\t" + this.property + "(" + this.from + this.units + " to " + this.to + this.units + ")\n"; + } +} + +// animates a colour based style property between two hex values +function ColorStyleSubject(els, property, from, to) { + this.els = Animator.makeArray(els); + this.property = Animator.camelize(property); + this.to = this.expandColor(to); + this.from = this.expandColor(from); + this.origFrom = from; + this.origTo = to; +} + +ColorStyleSubject.prototype = { + // parse "#FFFF00" to [256, 256, 0] + expandColor: function(color) { + var hexColor, red, green, blue; + hexColor = ColorStyleSubject.parseColor(color); + if (hexColor) { + red = parseInt(hexColor.slice(1, 3), 16); + green = parseInt(hexColor.slice(3, 5), 16); + blue = parseInt(hexColor.slice(5, 7), 16); + return [red,green,blue] + } + if (window.DEBUG) { + alert("Invalid colour: '" + color + "'"); + } + }, + getValueForState: function(color, state) { + return Math.round(this.from[color] + ((this.to[color] - this.from[color]) * state)); + }, + setState: function(state) { + var color = '#' + + ColorStyleSubject.toColorPart(this.getValueForState(0, state)) + + ColorStyleSubject.toColorPart(this.getValueForState(1, state)) + + ColorStyleSubject.toColorPart(this.getValueForState(2, state)); + for (var i=0; i 255) number = 255; + var digits = number.toString(16); + if (number < 16) return '0' + digits; + return digits; +} +ColorStyleSubject.parseColor.rgbRe = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i; +ColorStyleSubject.parseColor.hexRe = /^\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; + +// Animates discrete styles, i.e. ones that do not scale but have discrete values +// that can't be interpolated +function DiscreteStyleSubject(els, property, from, to, threshold) { + this.els = Animator.makeArray(els); + this.property = Animator.camelize(property); + this.from = from; + this.to = to; + this.threshold = threshold || 0.5; +} + +DiscreteStyleSubject.prototype = { + setState: function(state) { + var j=0; + for (var i=0; i section ? 1 : 0); + } + if (this.options.rememberance) { + document.location.hash = this.rememberanceTexts[section]; + } + } +} diff --git a/examples/strategy-cluster.html b/examples/strategy-cluster.html new file mode 100644 index 0000000000..91e1e428cd --- /dev/null +++ b/examples/strategy-cluster.html @@ -0,0 +1,201 @@ + + + OpenLayers Cluster Strategy Example + + + + + + + + + +

Cluster Strategy Example

+

+ Uses a cluster strategy to render points representing clusters of features. +

+
+
+

The Cluster strategy lets you display points representing clusters + of features within some pixel distance.

+
+
+

Hover over a cluster on the map to see the photos it includes.

+
+
+
<<
+
+ +
+
>>
+
+
+ + diff --git a/examples/strategy-paging.html b/examples/strategy-paging.html new file mode 100644 index 0000000000..3e0d54f62c --- /dev/null +++ b/examples/strategy-paging.html @@ -0,0 +1,78 @@ + + + OpenLayers Paging Strategy Example + + + + + + +

Paging Strategy Example

+

+ Uses a paging strategy to cache large batches of features and render a page at a time. +

+
+ Displaying page 0 of ... + + +

+
+

The Paging strategy lets you apply client side paging for protocols + that do not support paging on the server. In this case, the protocol requests a + batch of 100 features, the strategy caches those and supplies a single + page at a time to the layer.

+
+ + diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index cae000e7b7..6b3383e4d1 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -185,6 +185,8 @@ "OpenLayers/Layer/Vector.js", "OpenLayers/Strategy.js", "OpenLayers/Strategy/Fixed.js", + "OpenLayers/Strategy/Cluster.js", + "OpenLayers/Strategy/Paging.js", "OpenLayers/Strategy/BBOX.js", "OpenLayers/Protocol.js", "OpenLayers/Protocol/HTTP.js", diff --git a/lib/OpenLayers/Layer/Vector.js b/lib/OpenLayers/Layer/Vector.js index 23db07af29..d27c3f06df 100644 --- a/lib/OpenLayers/Layer/Vector.js +++ b/lib/OpenLayers/Layer/Vector.js @@ -38,7 +38,12 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { * Supported map event types (in addition to those from ): * - *beforefeatureadded* Triggered before a feature is added. Listeners * will receive an object with a *feature* property referencing the - * feature to be added. + * feature to be added. To stop the feature from being added, a + * listener should return false. + * - *beforefeaturesadded* Triggered before an array of features is added. + * Listeners will receive an object with a *features* property + * referencing the feature to be added. To stop the features from + * being added, a listener should return false. * - *featureadded* Triggered after a feature is added. The event * object passed to listeners will have a *feature* property with a * reference to the added feature. @@ -72,7 +77,8 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { * - *refresh* Triggered when something wants a strategy to ask the protocol * for a new set of features. */ - EVENT_TYPES: ["beforefeatureadded", "featureadded", "featuresadded", + EVENT_TYPES: ["beforefeatureadded", "beforefeaturesadded", + "featureadded", "featuresadded", "beforefeatureremoved", "featureremoved", "featuresremoved", "beforefeatureselected", "featureselected", "featureunselected", "beforefeaturemodified", "featuremodified", "afterfeaturemodified", @@ -443,6 +449,15 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { } var notify = !options || !options.silent; + if(notify) { + var event = {features: features}; + var ret = this.events.triggerEvent("beforefeaturesadded", event); + if(ret === false) { + return; + } + features = event.features; + } + for (var i=0, len=features.length; i + */ +OpenLayers.Strategy.Cluster = OpenLayers.Class(OpenLayers.Strategy, { + + /** + * Property: layer + * {} The layer that this strategy is assigned to. + */ + layer: null, + + /** + * APIProperty: distance + * {Integer} Pixel distance between features that should be considered a + * single cluster. Default is 20 pixels. + */ + distance: 20, + + /** + * Property: features + * {Array()} Cached features. + */ + features: null, + + /** + * Property: clusters + * {Array()} 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, + scope: this + }); + this.layer.map.events.on({"zoomend": 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, + scope: this + }); + this.layer.map.events.un({"zoomend": 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 layer 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. This destroys features, assuming + * nothing else has a reference. + */ + clearCache: function() { + if(this.features) { + for(var i=0; i 0) { + 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} A cluster. + * feature - {} 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 - {} A cluster. + * feature - {} A feature. + */ + addToCluster: function(cluster, feature) { + cluster.cluster.push(feature); + cluster.attributes.count += 1; + }, + + /** + * Method: createCluster + * Given a feature, create a cluster. + * + * Parameters: + * feature - {} + * + * Returns: + * {} 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/lib/OpenLayers/Strategy/Paging.js b/lib/OpenLayers/Strategy/Paging.js new file mode 100644 index 0000000000..c3ac684317 --- /dev/null +++ b/lib/OpenLayers/Strategy/Paging.js @@ -0,0 +1,241 @@ +/* Copyright (c) 2006-2008 MetaCarta, Inc., 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.Paging + * Strategy for vector feature paging + * + * Inherits from: + * - + */ +OpenLayers.Strategy.Paging = OpenLayers.Class(OpenLayers.Strategy, { + + /** + * Property: layer + * {} The layer that this strategy is assigned to. + */ + layer: null, + + /** + * Property: features + * {Array()} Cached features. + */ + features: null, + + /** + * Property: length + * {Integer} Number of features per page. Default is 10. + */ + length: 10, + + /** + * Property: num + * {Integer} The currently displayed page number. + */ + num: null, + + /** + * Property: paging + * {Boolean} The strategy is currently changing pages. + */ + paging: false, + + /** + * Constructor: OpenLayers.Strategy.Paging + * Create a new paging 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, + 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, + 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 paged. + */ + cacheFeatures: function(event) { + if(!this.paging) { + this.clearCache(); + this.features = event.features; + this.pageNext(event); + } + }, + + /** + * Method: clearCache + * Clear out the cached features. This destroys features, assuming + * nothing else has a reference. + */ + clearCache: function() { + if(this.features) { + for(var i=0; i 0) { + this.length = newLength; + } + return this.length; + }, + + /** + * APIMethod: pageNext + * Display the next page of features. + * + * Returns: + * {Boolean} A new page was displayed. + */ + pageNext: function(event) { + var changed = false; + if(this.features) { + if(this.num === null) { + this.num = -1; + } + var start = (this.num + 1) * this.length; + changed = this.page(start, event); + } + return changed; + }, + + /** + * APIMethod: pagePrevious + * Display the previous page of features. + * + * Returns: + * {Boolean} A new page was displayed. + */ + pagePrevious: function() { + var changed = false; + if(this.features) { + if(this.num === null) { + this.num = this.pageCount(); + } + var start = (this.num - 1) * this.length; + changed = this.page(start); + } + return changed; + }, + + /** + * Method: page + * Display the page starting at the given index from the cache. + * + * Returns: + * {Boolean} A new page was displayed. + */ + page: function(start, event) { + var changed = false; + if(this.features) { + if(start >= 0 && start < this.features.length) { + var num = Math.floor(start / this.length); + if(num != this.num) { + this.paging = true; + var features = this.features.slice(start, start + this.length); + this.layer.removeFeatures(this.layer.features); + this.num = num; + // modify the event if any + if(event && event.features) { + // this.was called by an event listener + event.features = features; + } else { + // this was called directly on the strategy + this.layer.addFeatures(features); + } + this.paging = false; + changed = true; + } + } + } + return changed; + }, + + CLASS_NAME: "OpenLayers.Strategy.Paging" +}); diff --git a/tests/Strategy/Cluster.html b/tests/Strategy/Cluster.html new file mode 100644 index 0000000000..7dd0fa6088 --- /dev/null +++ b/tests/Strategy/Cluster.html @@ -0,0 +1,108 @@ + + + + + + +
+ + diff --git a/tests/Strategy/Paging.html b/tests/Strategy/Paging.html new file mode 100644 index 0000000000..221cd5c86a --- /dev/null +++ b/tests/Strategy/Paging.html @@ -0,0 +1,113 @@ + + + + + + +
+ + diff --git a/tests/list-tests.html b/tests/list-tests.html index b762e8ddf0..86ab9a0438 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -124,7 +124,9 @@
  • Request/XMLHttpRequest.html
  • Rule.html
  • Strategy.html
  • +
  • Strategy/Cluster.html
  • Strategy/Fixed.html
  • +
  • Strategy/Paging.html
  • Strategy/BBOX.html
  • Style.html
  • StyleMap.html