Ticket #1606: strategy.patch
| File strategy.patch, 62.4 kB (added by tschaub, 4 months ago) |
|---|
-
tests/Strategy/Cluster.html
old new 1 <html> 2 <head> 3 <script src="../../lib/OpenLayers.js"></script> 4 <script type="text/javascript"> 5 6 function test_activate(t) { 7 t.plan(2); 8 9 var strategy = new OpenLayers.Strategy.Cluster(); 10 t.eq(strategy.active, false, "not active after construction"); 11 12 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 13 strategies: [strategy] 14 }); 15 var map = new OpenLayers.Map('map'); 16 map.addLayer(layer); 17 18 t.eq(strategy.active, true, "active after adding to map"); 19 } 20 21 function test_clusters(t) { 22 t.plan(10); 23 24 function featuresEq(got, exp) { 25 var eq = false; 26 if(got instanceof Array && exp instanceof Array) { 27 if(got.length === exp.length) { 28 for(var i=0; i<got.length; ++i) { 29 if(got[i] !== exp[i]) { 30 console.log(got[i], exp[i]); 31 break; 32 } 33 } 34 eq = (i == got.length); 35 } 36 } 37 return eq; 38 } 39 40 var strategy = new OpenLayers.Strategy.Cluster(); 41 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 42 strategies: [strategy], 43 isBaseLayer: true 44 }); 45 var map = new OpenLayers.Map('map', { 46 resolutions: [4, 2, 1], 47 maxExtent: new OpenLayers.Bounds(-40, -40, 40, 40) 48 }); 49 map.addLayer(layer); 50 51 // create features in a line, 1 unit apart 52 var features = new Array(80); 53 for(var i=0; i<80; ++i) { 54 features[i] = new OpenLayers.Feature.Vector( 55 new OpenLayers.Geometry.Point(-40 + i, 0) 56 ); 57 } 58 59 map.setCenter(new OpenLayers.LonLat(0, 0), 0); 60 layer.addFeatures(features); 61 62 // resolution 4 63 // threshold: 4 * 20 = 80 units 64 // one cluster 65 t.eq(layer.features.length, 1, "[4] layer has one cluster"); 66 t.ok(featuresEq(layer.features[0].cluster, features), "[4] cluster includes all features"); 67 68 // resolution 2 69 // threshold: 2 * 20 = 40 units 70 // two clusters (41 and 39) - first cluster includes all features within 40 units of the first (0-40 or 41 features) 71 map.zoomIn(); 72 t.eq(layer.features.length, 2, "[2] layer has two clusters"); 73 t.ok(featuresEq(layer.features[0].cluster, features.slice(0, 41)), "[2] first cluster includes first 41 features"); 74 t.ok(featuresEq(layer.features[1].cluster, features.slice(41, 80)), "[2] second cluster includes last 39 features"); 75 76 // resolution 1 77 // threshold: 1 * 20 = 20 units 78 // four clusters (21, 21, 21, and 17) 79 map.zoomIn(); 80 t.eq(layer.features.length, 4, "[1] layer has four clusters"); 81 t.ok(featuresEq(layer.features[0].cluster, features.slice(0, 21)), "[1] first cluster includes first 21 features"); 82 t.ok(featuresEq(layer.features[1].cluster, features.slice(21, 42)), "[2] second cluster includes second 21 features"); 83 t.ok(featuresEq(layer.features[2].cluster, features.slice(42, 63)), "[2] third cluster includes third 21 features"); 84 t.ok(featuresEq(layer.features[3].cluster, features.slice(63, 80)), "[2] fourth cluster includes last 17 features"); 85 } 86 87 function test_deactivate(t) { 88 t.plan(2); 89 90 var strategy = new OpenLayers.Strategy.Cluster(); 91 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 92 strategies: [strategy] 93 }); 94 var map = new OpenLayers.Map('map'); 95 map.addLayer(layer); 96 97 t.eq(strategy.active, true, "active after adding to map"); 98 99 map.removeLayer(layer); 100 t.eq(strategy.active, false, "not active after removing from map"); 101 } 102 103 </script> 104 </head> 105 <body> 106 <div id="map" style="width: 400px; height: 200px" /> 107 </body> 108 </html> -
tests/Strategy/Paging.html
old new 1 <html> 2 <head> 3 <script src="../../lib/OpenLayers.js"></script> 4 <script type="text/javascript"> 5 6 function test_activate(t) { 7 t.plan(2); 8 9 var strategy = new OpenLayers.Strategy.Paging(); 10 t.eq(strategy.active, false, "not active after construction"); 11 12 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 13 strategies: [strategy] 14 }); 15 var map = new OpenLayers.Map('map'); 16 map.addLayer(layer); 17 18 t.eq(strategy.active, true, "active after adding to map"); 19 } 20 21 function test_paging(t) { 22 t.plan(18); 23 24 var strategy = new OpenLayers.Strategy.Paging(); 25 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 26 strategies: [strategy], 27 drawFeature: function() {} 28 }); 29 var map = new OpenLayers.Map('map'); 30 map.addLayer(layer); 31 32 var features = new Array(25); 33 for(var i=0; i<features.length; ++i) { 34 features[i] = {destroy: function() {}}; 35 } 36 37 function featuresEq(got, exp) { 38 var eq = false; 39 if(got instanceof Array && exp instanceof Array) { 40 if(got.length === exp.length) { 41 for(var i=0; i<got.length; ++i) { 42 if(got[i] !== exp[i]) { 43 console.log(got[i], exp[i]); 44 break; 45 } 46 } 47 eq = (i == got.length); 48 } 49 } 50 return eq; 51 } 52 53 var len = strategy.pageLength(); 54 t.eq(len, 10, "page length defaults to 10"); 55 56 // add 25 features to the layer 57 layer.addFeatures(features); 58 t.eq(strategy.features.length, features.length, "strategy caches all features"); 59 t.eq(layer.features.length, len, "layer gets one page of features"); 60 t.ok(featuresEq(layer.features, features.slice(0, len)), "layer gets first page initially"); 61 t.eq(strategy.pageNum(), 0, "strategy reports 0 based page number"); 62 t.eq(strategy.pageCount(), Math.ceil(features.length / len), "strategy reports correct number of pages"); 63 64 // load next page of features 65 var changed = strategy.pageNext(); 66 t.eq(changed, true, "(1) strategy reports change"); 67 t.eq(strategy.pageNum(), 1, "second page"); 68 t.ok(featuresEq(layer.features, features.slice(len, 2*len)), "layer has second page of features"); 69 70 // load next page of features (half page) 71 changed = strategy.pageNext(); 72 t.eq(changed, true, "(2) strategy reports change"); 73 t.eq(strategy.pageNum(), 2, "third page"); 74 75 // try to change forward again 76 changed = strategy.pageNext(); 77 t.eq(changed, false, "strategy reports no change"); 78 t.eq(layer.features.length, features.length % len, "layer has partial page"); 79 t.ok(featuresEq(layer.features, features.slice(2*len, 3*len)), "layer has third page of features"); 80 t.eq(strategy.pageNum(), 2, "still on third page"); 81 82 // change back a page 83 changed = strategy.pagePrevious(); 84 t.eq(changed, true, "(3) strategy reports change"); 85 t.eq(strategy.pageNum(), 1, "back on second page"); 86 t.ok(featuresEq(layer.features, features.slice(len, 2*len)), "layer has second page of features again"); 87 88 layer.destroy(); 89 90 } 91 92 function test_deactivate(t) { 93 t.plan(2); 94 95 var strategy = new OpenLayers.Strategy.Paging(); 96 var layer = new OpenLayers.Layer.Vector("Vector Layer", { 97 strategies: [strategy] 98 }); 99 var map = new OpenLayers.Map('map'); 100 map.addLayer(layer); 101 102 t.eq(strategy.active, true, "active after adding to map"); 103 104 map.removeLayer(layer); 105 t.eq(strategy.active, false, "not active after removing from map"); 106 } 107 108 </script> 109 </head> 110 <body> 111 <div id="map" style="width: 400px; height: 200px" /> 112 </body> 113 </html> -
tests/list-tests.html
old new 124 124 <li>Request/XMLHttpRequest.html</li> 125 125 <li>Rule.html</li> 126 126 <li>Strategy.html</li> 127 <li>Strategy/Cluster.html</li> 127 128 <li>Strategy/Fixed.html</li> 129 <li>Strategy/Paging.html</li> 128 130 <li>Strategy/BBOX.html</li> 129 131 <li>Style.html</li> 130 132 <li>StyleMap.html</li> -
lib/OpenLayers/Strategy/Paging.js
old new 1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD 2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the 3 * full text of the license. */ 4 5 /** 6 * @requires OpenLayers/Strategy.js 7 */ 8 9 /** 10 * Class: OpenLayers.Strategy.Paging 11 * Strategy for vector feature paging 12 * 13 * Inherits from: 14 * - <OpenLayers.Strategy> 15 */ 16 OpenLayers.Strategy.Paging = OpenLayers.Class(OpenLayers.Strategy, { 17 18 /** 19 * Property: layer 20 * {<OpenLayers.Layer.Vector>} 21 */ 22 layer: null, 23 24 /** 25 * Property: features 26 * {Array(<OpenLayers.Feature.Vector>)} Cached features. 27 */ 28 features: null, 29 30 /** 31 * Property: length 32 * {Integer} Number of features per page. Default is 10. 33 */ 34 length: 10, 35 36 /** 37 * Property: num 38 * {Integer} The currently displayed page number. 39 */ 40 num: null, 41 42 /** 43 * Property: paging 44 * {Boolean} The strategy is currently changing pages. 45 */ 46 paging: false, 47 48 /** 49 * Constructor: OpenLayers.Strategy.Paging 50 * Create a new paging strategy. 51 * 52 * Parameters: 53 * options - {Object} Optional object whose properties will be set on the 54 * instance. 55 */ 56 initialize: function(options) { 57 OpenLayers.Strategy.prototype.initialize.apply(this, [options]); 58 }, 59 60 /** 61 * Method: activate 62 * Activate the strategy. Register any listeners, do appropriate setup. 63 * 64 * Returns: 65 * {Boolean} The strategy was successfully activated. 66 */ 67 activate: function() { 68 var activated = OpenLayers.Strategy.prototype.activate.call(this); 69 if(activated) { 70 this.layer.events.on({ 71 "beforefeaturesadded": this.cacheFeatures, 72 scope: this 73 }); 74 } 75 return activated; 76 }, 77 78 /** 79 * Method: deactivate 80 * Deactivate the strategy. Unregister any listeners, do appropriate 81 * tear-down. 82 * 83 * Returns: 84 * {Boolean} The strategy was successfully deactivated. 85 */ 86 deactivate: function() { 87 var deactivated = OpenLayers.Strategy.prototype.deactivate.call(this); 88 if(deactivated) { 89 this.clearCache(); 90 this.layer.events.un({ 91 "beforefeaturesadded": this.cacheFeatures, 92 scope: this 93 }); 94 } 95 return deactivated; 96 }, 97 98 /** 99 * Method: cacheFeatures 100 * Cache features before they are added to the layer. 101 */ 102 cacheFeatures: function(event) { 103 if(!this.paging) { 104 this.clearCache(); 105 this.features = event.features; 106 this.pageNext(event); 107 } 108 }, 109 110 /** 111 * Method: clearCache 112 * Clear out the cached features. This destroys features, assuming 113 * nothing else has a reference. 114 */ 115 clearCache: function() { 116 if(this.features) { 117 for(var i=0; i<this.features.length; ++i) { 118 this.features[i].destroy(); 119 } 120 } 121 this.features = null; 122 this.num = null; 123 }, 124 125 /** 126 * Method: pageCount 127 * Get the total count of pages given the current cache of features. 128 * 129 * Returns: 130 * {Integer} The page count. 131 */ 132 pageCount: function() { 133 var numFeatures = this.features ? this.features.length : 0; 134 return Math.ceil(numFeatures / this.length); 135 }, 136 137 /** 138 * Method: pageNum 139 * Get the zero based page number. 140 * 141 * Returns: 142 * {Integer} The current page number being displayed. 143 */ 144 pageNum: function() { 145 return this.num; 146 }, 147 148 /** 149 * Method: pageLength 150 * Gets or sets page length. 151 * 152 * Parameters: 153 * newLength: {Integer} Optional length to be set. 154 * 155 * Returns: 156 * {Integer} The length of a page (number of features per page). 157 */ 158 pageLength: function(newLength) { 159 if(newLength && newLength > 0) { 160 this.length = newLength; 161 } 162 return this.length; 163 }, 164 165 /** 166 * Method: pageNext 167 * Display the next page of features. 168 * 169 * Returns: 170 * {Boolean} A new page was displayed. 171 */ 172 pageNext: function(event) { 173 var changed = false; 174 if(this.features) { 175 if(this.num === null) { 176 this.num = -1; 177 } 178 var start = (this.num + 1) * this.length; 179 changed = this.page(start, event); 180 } 181 return changed; 182 }, 183 184 /** 185 * Method: pagePrevious 186 * Display the previous page of features. 187 * 188 * Returns: 189 * {Boolean} A new page was displayed. 190 */ 191 pagePrevious: function() { 192 var changed = false; 193 if(this.features) { 194 if(this.num === null) { 195 this.num = this.pageCount(); 196 } 197 var start = (this.num - 1) * this.length; 198 changed = this.page(start); 199 } 200 return changed; 201 }, 202 203 /** 204 * Method: page 205 * Display the page starting at the given index from the cache. 206 * 207 * Returns: 208 * {Boolean} A new page was displayed. 209 */ 210 page: function(start, event) { 211 var changed = false; 212 if(this.features) { 213 if(start >= 0 && start < this.features.length) { 214 var num = Math.floor(start / this.length); 215 if(num != this.num) { 216 this.paging = true; 217 var features = this.features.slice(start, start + this.length); 218 this.layer.removeFeatures(this.layer.features); 219 this.num = num; 220 // modify the event if any 221 if(event && event.features) { 222 // this.was called by an event listener 223 event.features = features; 224 } else { 225 // this was called directly on the strategy 226 this.layer.addFeatures(features); 227 } 228 this.paging = false; 229 changed = true; 230 } 231 } 232 } 233 return changed; 234 }, 235 236 CLASS_NAME: "OpenLayers.Strategy.Paging" 237 }); -
lib/OpenLayers/Strategy/Cluster.js
old new 1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD 2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the 3 * full text of the license. */ 4 5 /** 6 * @requires OpenLayers/Strategy.js 7 */ 8 9 /** 10 * Class: OpenLayers.Strategy.Cluster 11 * Strategy for vector feature clustering. 12 * 13 * Inherits from: 14 * - <OpenLayers.Strategy> 15 */ 16 OpenLayers.Strategy.Cluster = OpenLayers.Class(OpenLayers.Strategy, { 17 18 /** 19 * Property: layer 20 * {<OpenLayers.Layer.Vector>} 21 */ 22 layer: null, 23 24 /** 25 * Property: distance 26 * {Integer} Pixel distance between features that should be considered a 27 * single cluster. Default is 20 pixels. 28 */ 29 distance: 20, 30 31 /** 32 * Property: features 33 * {Array(<OpenLayers.Feature.Vector>)} Cached features. 34 */ 35 features: null, 36 37 /** 38 * Property: clusters 39 * {Array(<OpenLayers.Feature.Vector>)} Calculated clusters. 40 */ 41 clusters: null, 42 43 /** 44 * Property: clustering 45 * {Boolean} The strategy is currently clustering features. 46 */ 47 clustering: false, 48 49 /** 50 * Property: resolution 51 * {Float} The resolution (map units per pixel) of the current cluster set. 52 */ 53 resolution: null, 54 55 /** 56 * Constructor: OpenLayers.Strategy.Cluster 57 * Create a new clustering strategy. 58 * 59 * Parameters: 60 * options - {Object} Optional object whose properties will be set on the 61 * instance. 62 */ 63 initialize: function(options) { 64 OpenLayers.Strategy.prototype.initialize.apply(this, [options]); 65 }, 66 67 /** 68 * Method: activate 69 * Activate the strategy. Register any listeners, do appropriate setup. 70 * 71 * Returns: 72 * {Boolean} The strategy was successfully activated. 73 */ 74 activate: function() { 75 var activated = OpenLayers.Strategy.prototype.activate.call(this); 76 if(activated) { 77 this.layer.events.on({ 78 "beforefeaturesadded": this.cacheFeatures, 79 scope: this 80 }); 81 this.layer.map.events.on({"zoomend": this.cluster, scope: this}); 82 } 83 return activated; 84 }, 85 86 /** 87 * Method: deactivate 88 * Deactivate the strategy. Unregister any listeners, do appropriate 89 * tear-down. 90 * 91 * Returns: 92 * {Boolean} The strategy was successfully deactivated. 93 */ 94 deactivate: function() { 95 var deactivated = OpenLayers.Strategy.prototype.deactivate.call(this); 96 if(deactivated) { 97 this.clearCache(); 98 this.layer.events.un({ 99 "beforefeaturesadded": this.cacheFeatures, 100 scope: this 101 }); 102 this.layer.map.events.un({"zoomend": this.cluster, scope: this}); 103 } 104 return deactivated; 105 }, 106 107 /** 108 * Method: cacheFeatures 109 * Cache features before they are added to the layer. 110 * 111 * Returns: 112 * {Boolean} False to stop layer from being added to the layer. 113 */ 114 cacheFeatures: function(event) { 115 var propagate = true; 116 if(!this.clustering) { 117 this.clearCache(); 118 this.features = event.features; 119 this.cluster(); 120 propagate = false; 121 } 122 return propagate; 123 }, 124 125 /** 126 * Method: clearCache 127 * Clear out the cached features. This destroys features, assuming 128 * nothing else has a reference. 129 */ 130 clearCache: function() { 131 if(this.features) { 132 for(var i=0; i<this.features.length; ++i) { 133 this.features[i].destroy(); 134 } 135 } 136 this.features = null; 137 }, 138 139 /** 140 * Method: cluster 141 * Cluster features based on some threshold distance. 142 */ 143 cluster: function() { 144 if(this.features) { 145 var resolution = this.layer.getResolution(); 146 if(resolution != this.resolution || !this.clustersExist()) { 147 this.resolution = resolution; 148 var clusters = []; 149 var feature, clustered, cluster; 150 for(var i=0; i<this.features.length; ++i) { 151 feature = this.features[i]; 152 clustered = false; 153 for(var j=0; j<clusters.length; ++j) { 154 cluster = clusters[j]; 155 if(this.shouldCluster(cluster, feature)) { 156 this.addToCluster(cluster, feature); 157 clustered = true; 158 break; 159 } 160 } 161 if(!clustered) { 162 clusters.push(this.createCluster(this.features[i])); 163 } 164 } 165 this.layer.destroyFeatures(); 166 if(clusters.length > 0) { 167 this.clustering = true; 168 // A legitimate feature addition could occur during this 169 // addFeatures call. For clustering to behave well, features 170 // should be removed from a layer before requesting a new batch. 171 this.layer.addFeatures(clusters); 172 this.clustering = false; 173 } 174 this.clusters = clusters; 175 } 176 } 177 }, 178 179 /** 180 * Method: clustersExist 181 * Determine whether calculated clusters are already on the layer. 182 * 183 * Returns: 184 * {Boolean} The calculated clusters are already on the layer. 185 */ 186 clustersExist: function() { 187 var exist = false; 188 if(this.clusters && this.clusters.length > 0 && 189 this.clusters.length == this.layer.features.length) { 190 exist = true; 191 for(var i=0; i<this.clusters.length; ++i) { 192 if(this.clusters[i] != this.layer.features[i]) { 193 exist = false; 194 break; 195 } 196 } 197 } 198 return exist; 199 }, 200 201 /** 202 * Method: shouldCluster 203 * Determine whether to include a feature in a given cluster. 204 * 205 * Parameters: 206 * cluster - {<OpenLayers.Feature.Vector>} A cluster. 207 * feature - {<OpenLayers.Feature.Vector>} A feature. 208 * 209 * Returns: 210 * {Boolean} The feature should be included in the cluster. 211 */ 212 shouldCluster: function(cluster, feature) { 213 var cc = cluster.geometry.getBounds().getCenterLonLat(); 214 var fc = feature.geometry.getBounds().getCenterLonLat(); 215 var distance = ( 216 Math.sqrt( 217 Math.pow((cc.lon - fc.lon), 2) + Math.pow((cc.lat - fc.lat), 2) 218 ) / this.resolution 219 ); 220 return (distance <= this.distance); 221 }, 222 223 /** 224 * Method: addToCluster 225 * Add a feature to a cluster. 226 * 227 * Parameters: 228 * cluster - {<OpenLayers.Feature.Vector>} A cluster. 229 * feature - {<OpenLayers.Feature.Vector>} A feature. 230 */ 231 addToCluster: function(cluster, feature) { 232 cluster.cluster.push(feature); 233 cluster.attributes.count += 1; 234 }, 235 236 /** 237 * Method: createCluster 238 * Given a feature, create a cluster. 239 * 240 * Parameters: 241 * feature - {<OpenLayers.Feature.Vector>} 242 * 243 * Returns: 244 * {<OpenLayers.Feature.Vector>} A cluster. 245 */ 246 createCluster: function(feature) { 247 var center = feature.geometry.getBounds().getCenterLonLat(); 248 var cluster = new OpenLayers.Feature.Vector( 249 new OpenLayers.Geometry.Point(center.lon, center.lat), 250 {count: 1} 251 ); 252 cluster.cluster = [feature]; 253 return cluster; 254 }, 255 256 CLASS_NAME: "OpenLayers.Strategy.Cluster" 257 }); -
lib/OpenLayers/Layer/Vector.js
old new 38 38 * Supported map event types (in addition to those from <OpenLayers.Layer>): 39 39 * - *beforefeatureadded* Triggered before a feature is added. Listeners 40 40 * will receive an object with a *feature* property referencing the 41 * feature to be added. 41 * feature to be added. To stop the feature from being added, a 42 * listener should return false. 43 * - *beforefeaturesadded* Triggered before an array of features is added. 44 * Listeners will receive an object with a *features* property 45 * referencing the feature to be added. To stop the features from 46 * being added, a listener should return false. 42 47 * - *featureadded* Triggered after a feature is added. The event 43 48 * object passed to listeners will have a *feature* property with a 44 49 * reference to the added feature. … … 72 77 * - *refresh* Triggered when something wants a strategy to ask the protocol 73 78 * for a new set of features. 74 79 */ 75 EVENT_TYPES: ["beforefeatureadded", "featureadded", "featuresadded", 80 EVENT_TYPES: ["beforefeatureadded", "beforefeaturesadded", 81 "featureadded", "featuresadded", 76 82 "beforefeatureremoved", "featureremoved", "featuresremoved", 77 83 "beforefeatureselected", "featureselected", "featureunselected", 78 84 "beforefeaturemodified", "featuremodified", "afterfeaturemodified", … … 443 449 } 444 450 445 451 var notify = !options || !options.silent; 452 if(notify) { 453 var event = {features: features}; 454 var ret = this.events.triggerEvent("beforefeaturesadded", event); 455 if(ret === false) { 456 return; 457 } 458 features = event.features; 459 } 460 446 461 447 462 for (var i=0, len=features.length; i<len; i++) { 448 463 if (i != (features.length - 1)) { … … 469 484 } 470 485 471 486 if (notify) { 472 this.events.triggerEvent("beforefeatureadded", { 473 feature: feature 474 }); 487 if(this.events.triggerEvent("beforefeatureadded", 488 {feature: feature}) === false) { 489 continue; 490 }; 475 491 this.preFeatureInsert(feature); 476 492 } 477 493 -
lib/OpenLayers.js
old new 185 185 "OpenLayers/Layer/Vector.js", 186 186 "OpenLayers/Strategy.js", 187 187 "OpenLayers/Strategy/Fixed.js", 188 "OpenLayers/Strategy/Cluster.js", 189 "OpenLayers/Strategy/Paging.js", 188 190 "OpenLayers/Strategy/BBOX.js", 189 191 "OpenLayers/Protocol.js", 190 192 "OpenLayers/Protocol/HTTP.js", -
examples/animator.js
old new 1 /* 2 Animator.js 1.1.9 3 4 This library is released under the BSD license: 5 6 Copyright (c) 2006, Bernard Sumption. All rights reserved. 7 8 Redistribution and use in source and binary forms, with or without 9 modification, are permitted provided that the following conditions are met: 10 11 Redistributions of source code must retain the above copyright notice, this 12 list of conditions and the following disclaimer. Redistributions in binary 13 form must reproduce the above copyright notice, this list of conditions and 14 the following disclaimer in the documentation and/or other materials 15 provided with the distribution. Neither the name BernieCode nor 16 the names of its contributors may be used to endorse or promote products 17 derived from this software without specific prior written permission. 18 19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR 23 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 29 DAMAGE. 30 31 */ 32 33 34 // Applies a sequence of numbers between 0 and 1 to a number of subjects 35 // construct - see setOptions for parameters 36 function Animator(options) { 37 this.setOptions(options); 38 var _this = this; 39 this.timerDelegate = function(){_this.onTimerEvent()}; 40 this.subjects = []; 41 this.target = 0; 42 this.state = 0; 43 this.lastTime = null; 44 }; 45 Animator.prototype = { 46 // apply defaults 47 setOptions: function(options) { 48 this.options = Animator.applyDefaults({ 49 interval: 20, // time between animation frames 50 duration: 400, // length of animation 51 onComplete: function(){}, 52 onStep: function(){}, 53 transition: Animator.tx.easeInOut 54 }, options); 55 }, 56 // animate from the current state to provided value 57 seekTo: function(to) { 58 this.seekFromTo(this.state, to); 59 }, 60 // animate from the current state to provided value 61 seekFromTo: function(from, to) { 62 this.target = Math.max(0, Math.min(1, to)); 63 this.state = Math.max(0, Math.min(1, from)); 64 this.lastTime = new Date().getTime(); 65 if (!this.intervalId) { 66 this.intervalId = window.setInterval(this.timerDelegate, this.options.interval); 67 } 68 }, 69 // animate from the current state to provided value 70 jumpTo: function(to) { 71 this.target = this.state = Math.max(0, Math.min(1, to)); 72 this.propagate(); 73 }, 74 // seek to the opposite of the current target 75 toggle: function() { 76 this.seekTo(1 - this.target); 77 }, 78 // add a function or an object with a method setState(state) that will be called with a number 79 // between 0 and 1 on each frame of the animation 80 addSubject: function(subject) { 81 this.subjects[this.subjects.length] = subject; 82 return this; 83 }, 84 // remove all subjects 85 clearSubjects: function() { 86 this.subjects = []; 87 }, 88 // forward the current state to the animation subjects 89 propagate: function() { 90 var value = this.options.transition(this.state); 91 for (var i=0; i<this.subjects.length; i++) { 92 if (this.subjects[i].setState) { 93 this.subjects[i].setState(value); 94 } else { 95 this.subjects[i](value); 96 } 97 } 98 }, 99 // called once per frame to update the current state 100 onTimerEvent: function() { 101 var now = new Date().getTime(); 102 var timePassed = now - this.lastTime; 103 this.lastTime = now; 104 var movement = (timePassed / this.options.duration) * (this.state < this.target ? 1 : -1); 105 if (Math.abs(movement) >= Math.abs(this.state - this.target)) { 106 this.state = this.target; 107 } else { 108 this.state += movement; 109 } 110 111 try { 112 this.propagate(); 113 } finally { 114 this.options.onStep.call(this); 115 if (this.target == this.state) { 116 window.clearInterval(this.intervalId); 117 this.intervalId = null; 118 this.options.onComplete.call(this); 119 } 120 } 121 }, 122 // shortcuts 123 play: function() {this.seekFromTo(0, 1)}, 124 reverse: function() {this.seekFromTo(1, 0)}, 125 // return a string describing this Animator, for debugging 126 inspect: function() { 127 var str = "#<Animator:\n"; 128 for (var i=0; i<this.subjects.length; i++) { 129 str += this.subjects[i].inspect(); 130 } 131 str += ">"; 132 return str; 133 } 134 } 135 // merge the properties of two objects 136 Animator.applyDefaults = function(defaults, prefs) { 137 prefs = prefs || {}; 138 var prop, result = {}; 139 for (prop in defaults) result[prop] = prefs[prop] !== undefined ? prefs[prop] : defaults[prop]; 140 return result; 141 } 142 // make an array from any object 143 Animator.makeArray = function(o) { 144 if (o == null) return []; 145 if (!o.length) return [o]; 146 var result = []; 147 for (var i=0; i<o.length; i++) result[i] = o[i]; 148 return result; 149 } 150 // convert a dash-delimited-property to a camelCaseProperty (c/o Prototype, thanks Sam!) 151 Animator.camelize = function(string) { 152 var oStringList = string.split('-'); 153 if (oStringList.length == 1) return oStringList[0]; 154 155 var camelizedString = string.indexOf('-') == 0 156 ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) 157 : oStringList[0]; 158 159 for (var i = 1, len = oStringList.length; i < len; i++) { 160 var s = oStringList[i]; 161 camelizedString += s.charAt(0).toUpperCase() + s.substring(1); 162 } 163 return camelizedString; 164 } 165 // syntactic sugar for creating CSSStyleSubjects 166 Animator.apply = function(el, style, options) { 167 if (style instanceof Array) { 168 return new Animator(options).addSubject(new CSSStyleSubject(el, style[0], style[1])); 169 } 170 return new Animator(options).addSubject(new CSSStyleSubject(el, style)); 171 } 172 // make a transition function that gradually accelerates. pass a=1 for smooth 173 // gravitational acceleration, higher values for an exaggerated effect 174 Animator.makeEaseIn = function(a) { 175 return function(state) { 176 return Math.pow(state, a*2); 177 } 178 } 179 // as makeEaseIn but for deceleration 180 Animator.makeEaseOut = function(a) { 181 return function(state) { 182 return 1 - Math.pow(1 - state, a*2); 183 } 184 } 185 // make a transition function that, like an object with momentum being attracted to a point, 186 // goes past the target then returns 187 Animator.makeElastic = function(bounces) { 188 return function(state) { 189 state = Animator.tx.easeInOut(state); 190 return ((1-Math.cos(state * Math.PI * bounces)) * (1 - state)) + state; 191 } 192 } 193 // make an Attack Decay Sustain Release envelope that starts and finishes on the same level 194 // 195 Animator.makeADSR = function(attackEnd, decayEnd, sustainEnd, sustainLevel) { 196 if (sustainLevel == null) sustainLevel = 0.5; 197 return function(state) { 198 if (state < attackEnd) { 199 return state / attackEnd; 200 } 201 if (state < decayEnd) { 202 return 1 - ((state - attackEnd) / (decayEnd - attackEnd) * (1 - sustainLevel)); 203 } 204 if (state < sustainEnd) { 205 return sustainLevel; 206 } 207 return sustainLevel * (1 - ((state - sustainEnd) / (1 - sustainEnd))); 208 } 209 } 210 // make a transition function that, like a ball falling to floor, reaches the target and/ 211 // bounces back again 212 Animator.makeBounce = function(bounces) { 213 var fn = Animator.makeElastic(bounces); 214 return function(state) { 215 state = fn(state); 216 return state <= 1 ? state : 2-state; 217 } 218 } 219 220 // pre-made transition functions to use with the 'transition' option 221 Animator.tx = { 222 easeInOut: function(pos){ 223 return ((-Math.cos(pos*Math.PI)/2) + 0.5);<
