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
This commit is contained in:
Tim Schaub
2008-09-12 08:58:23 +00:00
parent 3ad82c119a
commit 4ad8f2118c
10 changed files with 1697 additions and 5 deletions

670
examples/animator.js Normal file
View File

@@ -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<this.subjects.length; i++) {
if (this.subjects[i].setState) {
this.subjects[i].setState(value);
} else {
this.subjects[i](value);
}
}
},
// called once per frame to update the current state
onTimerEvent: function() {
var now = new Date().getTime();
var timePassed = now - this.lastTime;
this.lastTime = now;
var movement = (timePassed / this.options.duration) * (this.state < this.target ? 1 : -1);
if (Math.abs(movement) >= 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 = "#<Animator:\n";
for (var i=0; i<this.subjects.length; i++) {
str += this.subjects[i].inspect();
}
str += ">";
return str;
}
}
// merge the properties of two objects
Animator.applyDefaults = function(defaults, prefs) {
prefs = prefs || {};
var prop, result = {};
for (prop in defaults) result[prop] = prefs[prop] !== undefined ? prefs[prop] : defaults[prop];
return result;
}
// make an array from any object
Animator.makeArray = function(o) {
if (o == null) return [];
if (!o.length) return [o];
var result = [];
for (var i=0; i<o.length; i++) result[i] = o[i];
return result;
}
// convert a dash-delimited-property to a camelCaseProperty (c/o Prototype, thanks Sam!)
Animator.camelize = function(string) {
var oStringList = string.split('-');
if (oStringList.length == 1) return oStringList[0];
var camelizedString = string.indexOf('-') == 0
? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
: oStringList[0];
for (var i = 1, len = oStringList.length; i < len; i++) {
var s = oStringList[i];
camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
}
return camelizedString;
}
// syntactic sugar for creating CSSStyleSubjects
Animator.apply = function(el, style, options) {
if (style instanceof Array) {
return new Animator(options).addSubject(new CSSStyleSubject(el, style[0], style[1]));
}
return new Animator(options).addSubject(new CSSStyleSubject(el, style));
}
// make a transition function that gradually accelerates. pass a=1 for smooth
// gravitational acceleration, higher values for an exaggerated effect
Animator.makeEaseIn = function(a) {
return function(state) {
return Math.pow(state, a*2);
}
}
// as makeEaseIn but for deceleration
Animator.makeEaseOut = function(a) {
return function(state) {
return 1 - Math.pow(1 - state, a*2);
}
}
// make a transition function that, like an object with momentum being attracted to a point,
// goes past the target then returns
Animator.makeElastic = function(bounces) {
return function(state) {
state = Animator.tx.easeInOut(state);
return ((1-Math.cos(state * Math.PI * bounces)) * (1 - state)) + state;
}
}
// make an Attack Decay Sustain Release envelope that starts and finishes on the same level
//
Animator.makeADSR = function(attackEnd, decayEnd, sustainEnd, sustainLevel) {
if (sustainLevel == null) sustainLevel = 0.5;
return function(state) {
if (state < attackEnd) {
return state / attackEnd;
}
if (state < decayEnd) {
return 1 - ((state - attackEnd) / (decayEnd - attackEnd) * (1 - sustainLevel));
}
if (state < sustainEnd) {
return sustainLevel;
}
return sustainLevel * (1 - ((state - sustainEnd) / (1 - sustainEnd)));
}
}
// make a transition function that, like a ball falling to floor, reaches the target and/
// bounces back again
Animator.makeBounce = function(bounces) {
var fn = Animator.makeElastic(bounces);
return function(state) {
state = fn(state);
return state <= 1 ? state : 2-state;
}
}
// pre-made transition functions to use with the 'transition' option
Animator.tx = {
easeInOut: function(pos){
return ((-Math.cos(pos*Math.PI)/2) + 0.5);
},
linear: function(x) {
return x;
},
easeIn: Animator.makeEaseIn(1.5),
easeOut: Animator.makeEaseOut(1.5),
strongEaseIn: Animator.makeEaseIn(2.5),
strongEaseOut: Animator.makeEaseOut(2.5),
elastic: Animator.makeElastic(1),
veryElastic: Animator.makeElastic(3),
bouncy: Animator.makeBounce(1),
veryBouncy: Animator.makeBounce(3)
}
// animates a pixel-based style property between two integer values
function NumericalStyleSubject(els, property, from, to, units) {
this.els = Animator.makeArray(els);
if (property == 'opacity' && window.ActiveXObject) {
this.property = 'filter';
} else {
this.property = Animator.camelize(property);
}
this.from = parseFloat(from);
this.to = parseFloat(to);
this.units = units != null ? units : 'px';
}
NumericalStyleSubject.prototype = {
setState: function(state) {
var style = this.getStyle(state);
var visibility = (this.property == 'opacity' && state == 0) ? 'hidden' : '';
var j=0;
for (var i=0; i<this.els.length; i++) {
try {
this.els[i].style[this.property] = style;
} catch (e) {
// ignore fontWeight - intermediate numerical values cause exeptions in firefox
if (this.property != 'fontWeight') throw e;
}
if (j++ > 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<this.els.length; i++) {
this.els[i].style[this.property] = color;
}
},
inspect: function() {
return "\t" + this.property + "(" + this.origFrom + " to " + this.origTo + ")\n";
}
}
// return a properly formatted 6-digit hex colour spec, or false
ColorStyleSubject.parseColor = function(string) {
var color = '#', match;
if(match = ColorStyleSubject.parseColor.rgbRe.exec(string)) {
var part;
for (var i=1; i<=3; i++) {
part = Math.max(0, Math.min(255, parseInt(match[i])));
color += ColorStyleSubject.toColorPart(part);
}
return color;
}
if (match = ColorStyleSubject.parseColor.hexRe.exec(string)) {
if(match[1].length == 3) {
for (var i=0; i<3; i++) {
color += match[1].charAt(i) + match[1].charAt(i);
}
return color;
}
return '#' + match[1];
}
return false;
}
// convert a number to a 2 digit hex string
ColorStyleSubject.toColorPart = function(number) {
if (number > 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<this.els.length; i++) {
this.els[i].style[this.property] = state <= this.threshold ? this.from : this.to;
}
},
inspect: function() {
return "\t" + this.property + "(" + this.from + " to " + this.to + " @ " + this.threshold + ")\n";
}
}
// animates between two styles defined using CSS.
// if style1 and style2 are present, animate between them, if only style1
// is present, animate between the element's current style and style1
function CSSStyleSubject(els, style1, style2) {
els = Animator.makeArray(els);
this.subjects = [];
if (els.length == 0) return;
var prop, toStyle, fromStyle;
if (style2) {
fromStyle = this.parseStyle(style1, els[0]);
toStyle = this.parseStyle(style2, els[0]);
} else {
toStyle = this.parseStyle(style1, els[0]);
fromStyle = {};
for (prop in toStyle) {
fromStyle[prop] = CSSStyleSubject.getStyle(els[0], prop);
}
}
// remove unchanging properties
var prop;
for (prop in fromStyle) {
if (fromStyle[prop] == toStyle[prop]) {
delete fromStyle[prop];
delete toStyle[prop];
}
}
// discover the type (numerical or colour) of each style
var prop, units, match, type, from, to;
for (prop in fromStyle) {
var fromProp = String(fromStyle[prop]);
var toProp = String(toStyle[prop]);
if (toStyle[prop] == null) {
if (window.DEBUG) alert("No to style provided for '" + prop + '"');
continue;
}
if (from = ColorStyleSubject.parseColor(fromProp)) {
to = ColorStyleSubject.parseColor(toProp);
type = ColorStyleSubject;
} else if (fromProp.match(CSSStyleSubject.numericalRe)
&& toProp.match(CSSStyleSubject.numericalRe)) {
from = parseFloat(fromProp);
to = parseFloat(toProp);
type = NumericalStyleSubject;
match = CSSStyleSubject.numericalRe.exec(fromProp);
var reResult = CSSStyleSubject.numericalRe.exec(toProp);
if (match[1] != null) {
units = match[1];
} else if (reResult[1] != null) {
units = reResult[1];
} else {
units = reResult;
}
} else if (fromProp.match(CSSStyleSubject.discreteRe)
&& toProp.match(CSSStyleSubject.discreteRe)) {
from = fromProp;
to = toProp;
type = DiscreteStyleSubject;
units = 0; // hack - how to get an animator option down to here
} else {
if (window.DEBUG) {
alert("Unrecognised format for value of "
+ prop + ": '" + fromStyle[prop] + "'");
}
continue;
}
this.subjects[this.subjects.length] = new type(els, prop, from, to, units);
}
}
CSSStyleSubject.prototype = {
// parses "width: 400px; color: #FFBB2E" to {width: "400px", color: "#FFBB2E"}
parseStyle: function(style, el) {
var rtn = {};
// if style is a rule set
if (style.indexOf(":") != -1) {
var styles = style.split(";");
for (var i=0; i<styles.length; i++) {
var parts = CSSStyleSubject.ruleRe.exec(styles[i]);
if (parts) {
rtn[parts[1]] = parts[2];
}
}
}
// else assume style is a class name
else {
var prop, value, oldClass;
oldClass = el.className;
el.className = style;
for (var i=0; i<CSSStyleSubject.cssProperties.length; i++) {
prop = CSSStyleSubject.cssProperties[i];
value = CSSStyleSubject.getStyle(el, prop);
if (value != null) {
rtn[prop] = value;
}
}
el.className = oldClass;
}
return rtn;
},
setState: function(state) {
for (var i=0; i<this.subjects.length; i++) {
this.subjects[i].setState(state);
}
},
inspect: function() {
var str = "";
for (var i=0; i<this.subjects.length; i++) {
str += this.subjects[i].inspect();
}
return str;
}
}
// get the current value of a css property,
CSSStyleSubject.getStyle = function(el, property){
var style;
if(document.defaultView && document.defaultView.getComputedStyle){
style = document.defaultView.getComputedStyle(el, "").getPropertyValue(property);
if (style) {
return style;
}
}
property = Animator.camelize(property);
if(el.currentStyle){
style = el.currentStyle[property];
}
return style || el.style[property]
}
CSSStyleSubject.ruleRe = /^\s*([a-zA-Z\-]+)\s*:\s*(\S(.+\S)?)\s*$/;
CSSStyleSubject.numericalRe = /^-?\d+(?:\.\d+)?(%|[a-zA-Z]{2})?$/;
CSSStyleSubject.discreteRe = /^\w+$/;
// required because the style object of elements isn't enumerable in Safari
/*
CSSStyleSubject.cssProperties = ['background-color','border','border-color','border-spacing',
'border-style','border-top','border-right','border-bottom','border-left','border-top-color',
'border-right-color','border-bottom-color','border-left-color','border-top-width','border-right-width',
'border-bottom-width','border-left-width','border-width','bottom','color','font-size','font-size-adjust',
'font-stretch','font-style','height','left','letter-spacing','line-height','margin','margin-top',
'margin-right','margin-bottom','margin-left','marker-offset','max-height','max-width','min-height',
'min-width','orphans','outline','outline-color','outline-style','outline-width','overflow','padding',
'padding-top','padding-right','padding-bottom','padding-left','quotes','right','size','text-indent',
'top','width','word-spacing','z-index','opacity','outline-offset'];*/
CSSStyleSubject.cssProperties = ['azimuth','background','background-attachment','background-color','background-image','background-position','background-repeat','border-collapse','border-color','border-spacing','border-style','border-top','border-top-color','border-right-color','border-bottom-color','border-left-color','border-top-style','border-right-style','border-bottom-style','border-left-style','border-top-width','border-right-width','border-bottom-width','border-left-width','border-width','bottom','clear','clip','color','content','cursor','direction','display','elevation','empty-cells','css-float','font','font-family','font-size','font-size-adjust','font-stretch','font-style','font-variant','font-weight','height','left','letter-spacing','line-height','list-style','list-style-image','list-style-position','list-style-type','margin','margin-top','margin-right','margin-bottom','margin-left','max-height','max-width','min-height','min-width','orphans','outline','outline-color','outline-style','outline-width','overflow','padding','padding-top','padding-right','padding-bottom','padding-left','pause','position','right','size','table-layout','text-align','text-decoration','text-indent','text-shadow','text-transform','top','vertical-align','visibility','white-space','width','word-spacing','z-index','opacity','outline-offset','overflow-x','overflow-y'];
// chains several Animator objects together
function AnimatorChain(animators, options) {
this.animators = animators;
this.setOptions(options);
for (var i=0; i<this.animators.length; i++) {
this.listenTo(this.animators[i]);
}
this.forwards = false;
this.current = 0;
}
AnimatorChain.prototype = {
// apply defaults
setOptions: function(options) {
this.options = Animator.applyDefaults({
// by default, each call to AnimatorChain.play() calls jumpTo(0) of each animator
// before playing, which can cause flickering if you have multiple animators all
// targeting the same element. Set this to false to avoid this.
resetOnPlay: true
}, options);
},
// play each animator in turn
play: function() {
this.forwards = true;
this.current = -1;
if (this.options.resetOnPlay) {
for (var i=0; i<this.animators.length; i++) {
this.animators[i].jumpTo(0);
}
}
this.advance();
},
// play all animators backwards
reverse: function() {
this.forwards = false;
this.current = this.animators.length;
if (this.options.resetOnPlay) {
for (var i=0; i<this.animators.length; i++) {
this.animators[i].jumpTo(1);
}
}
this.advance();
},
// if we have just play()'d, then call reverse(), and vice versa
toggle: function() {
if (this.forwards) {
this.seekTo(0);
} else {
this.seekTo(1);
}
},
// internal: install an event listener on an animator's onComplete option
// to trigger the next animator
listenTo: function(animator) {
var oldOnComplete = animator.options.onComplete;
var _this = this;
animator.options.onComplete = function() {
if (oldOnComplete) oldOnComplete.call(animator);
_this.advance();
}
},
// play the next animator
advance: function() {
if (this.forwards) {
if (this.animators[this.current + 1] == null) return;
this.current++;
this.animators[this.current].play();
} else {
if (this.animators[this.current - 1] == null) return;
this.current--;
this.animators[this.current].reverse();
}
},
// this function is provided for drop-in compatibility with Animator objects,
// but only accepts 0 and 1 as target values
seekTo: function(target) {
if (target <= 0) {
this.forwards = false;
this.animators[this.current].seekTo(0);
} else {
this.forwards = true;
this.animators[this.current].seekTo(1);
}
}
}
// an Accordion is a class that creates and controls a number of Animators. An array of elements is passed in,
// and for each element an Animator and a activator button is created. When an Animator's activator button is
// clicked, the Animator and all before it seek to 0, and all Animators after it seek to 1. This can be used to
// create the classic Accordion effect, hence the name.
// see setOptions for arguments
function Accordion(options) {
this.setOptions(options);
var selected = this.options.initialSection, current;
if (this.options.rememberance) {
current = document.location.hash.substring(1);
}
this.rememberanceTexts = [];
this.ans = [];
var _this = this;
for (var i=0; i<this.options.sections.length; i++) {
var el = this.options.sections[i];
var an = new Animator(this.options.animatorOptions);
var from = this.options.from + (this.options.shift * i);
var to = this.options.to + (this.options.shift * i);
an.addSubject(new NumericalStyleSubject(el, this.options.property, from, to, this.options.units));
an.jumpTo(0);
var activator = this.options.getActivator(el);
activator.index = i;
activator.onclick = function(){_this.show(this.index)};
this.ans[this.ans.length] = an;
this.rememberanceTexts[i] = activator.innerHTML.replace(/\s/g, "");
if (this.rememberanceTexts[i] === current) {
selected = i;
}
}
this.show(selected);
}
Accordion.prototype = {
// apply defaults
setOptions: function(options) {
this.options = Object.extend({
// REQUIRED: an array of elements to use as the accordion sections
sections: null,
// a function that locates an activator button element given a section element.
// by default it takes a button id from the section's "activator" attibute
getActivator: function(el) {return document.getElementById(el.getAttribute("activator"))},
// shifts each animator's range, for example with options {from:0,to:100,shift:20}
// the animators' ranges will be 0-100, 20-120, 40-140 etc.
shift: 0,
// the first page to show
initialSection: 0,
// if set to true, document.location.hash will be used to preserve the open section across page reloads
rememberance: true,
// constructor arguments to the Animator objects
animatorOptions: {}
}, options || {});
},
show: function(section) {
for (var i=0; i<this.ans.length; i++) {
this.ans[i].seekTo(i > section ? 1 : 0);
}
if (this.options.rememberance) {
document.location.hash = this.rememberanceTexts[section];
}
}
}

View File

@@ -0,0 +1,201 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>OpenLayers Cluster Strategy Example</title>
<link rel="stylesheet" href="../theme/default/style.css" type="text/css" />
<link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css">
#photos {
height: 100px;
width: 512px;
position: relative;
white-space: nowrap;
}
.shift {
height: 25px;
line-height: 25px;
background-color: #fefefe;
text-align: center;
position: absolute;
bottom: 10px;
font-size: 8px;
font-weight: bold;
color: #696969;
width: 25px;
}
#scroll-start {
left: 0px;
}
#scroll-end {
right: 0px;
}
#scroll {
left: 30px;
width: 452px;
height: 100px;
overflow: hidden;
position: absolute;
bottom: 0px;
}
#photos ul {
position: absolute;
bottom: 0px;
padding: 0;
margin: 0;
}
#photos ul.start {
left: 0px;
}
#photos ul.end {
right: 80px;
}
#photos ul li {
padding 10px;
margin: 0;
list-style: none;
display: inline;
}
img.thumb {
height: 30px;
}
img.big {
height: 90px;
}
</style>
<script src="../lib/OpenLayers.js"></script>
<script src="Jugl.js"></script>
<script src="animator.js"></script>
<script type="text/javascript">
var map, template;
var Jugl = window["http://jugl.tschaub.net/trunk/lib/Jugl.js"];
OpenLayers.ProxyHost = (window.location.host == "localhost") ?
"/cgi-bin/proxy.cgi?url=" : "proxy.cgi?url=";
function init() {
map = new OpenLayers.Map('map', {
restrictedExtent: new OpenLayers.Bounds(-180, -90, 180, 90)
});
var base = new OpenLayers.Layer.WMS("OpenLayers WMS",
["http://t3.labs.metacarta.com/wms-c/Basic.py",
"http://t2.labs.metacarta.com/wms-c/Basic.py",
"http://t1.labs.metacarta.com/wms-c/Basic.py"],
{layers: 'satellite'}
);
var style = new OpenLayers.Style({
pointRadius: "${radius}",
fillColor: "#ffcc66",
fillOpacity: 0.8,
strokeColor: "#cc6633",
strokeWidth: 2,
strokeOpacity: 0.8
}, {
context: {
radius: function(feature) {
return Math.min(feature.attributes.count, 7) + 3;
}
}
});
var photos = new OpenLayers.Layer.Vector("Photos", {
strategies: [
new OpenLayers.Strategy.Fixed(),
new OpenLayers.Strategy.Cluster()
],
protocol: new OpenLayers.Protocol.HTTP({
url: "http://labs.metacarta.com/flickrbrowse/flickr.py/flickr",
params: {
format: "WFS",
sort: "interestingness-desc",
service: "WFS",
request: "GetFeatures",
srs: "EPSG:4326",
maxfeatures: 150,
bbox: [-180, -90, 180, 90]
},
format: new OpenLayers.Format.GML()
}),
styleMap: new OpenLayers.StyleMap({
"default": style,
"select": {
fillColor: "#8aeeef",
strokeColor: "#32a8a9"
}
})
});
var select = new OpenLayers.Control.SelectFeature(
photos, {hover: true}
);
map.addControl(select);
select.activate();
photos.events.on({"featureselected": display});
map.addLayers([base, photos]);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
// template setup
template = new Jugl.Template("template");
}
function display(event) {
// clear previous photo list and create new one
$("photos").innerHTML = "";
var node = template.process({
context: {features: event.feature.cluster},
clone: true,
parent: $("photos")
});
// set up forward/rewind
var forward = Animator.apply($("list"), ["start", "end"], {duration: 1500});
$("scroll-end").onmouseover = function() {forward.seekTo(1)};
$("scroll-end").onmouseout = function() {forward.seekTo(forward.state)};
$("scroll-start").onmouseover = function() {forward.seekTo(0)};
$("scroll-start").onmouseout = function() {forward.seekTo(forward.state)};
// set up photo zoom
for(var i=0; i<event.feature.cluster.length; ++i) {
listen($("link-" + i), Animator.apply($("photo-" + i), ["thumb", "big"]));
}
}
function listen(el, anim) {
el.onmouseover = function() {anim.seekTo(1)};
el.onmouseout = function() {anim.seekTo(0)};
}
</script>
</head>
<body onload="init()">
<h1 id="title">Cluster Strategy Example</h1>
<p id="shortdesc">
Uses a cluster strategy to render points representing clusters of features.
</p>
<div id="map" class="smallmap"></div>
<div id="docs">
<p>The Cluster strategy lets you display points representing clusters
of features within some pixel distance.</p>
</div>
<div id="photos"></div>
<p>Hover over a cluster on the map to see the photos it includes.</p>
<div style="display: none;">
<div id="template">
<div class="shift" id="scroll-start">&lt;&lt;</div>
<div id="scroll">
<ul id="list" class="start">
<li jugl:repeat="feature features">
<a jugl:attributes="href feature.attributes.img_url;
id 'link-' + repeat.feature.index"
target="_blank">
<img jugl:attributes="src feature.attributes.img_url;
title feature.attributes.title;
id 'photo-' + repeat.feature.index"
class="thumb" />
</a>
</li>
</ul>
</div>
<div class="shift" id="scroll-end">&gt;&gt;</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>OpenLayers Paging Strategy Example</title>
<link rel="stylesheet" href="../theme/default/style.css" type="text/css" />
<link rel="stylesheet" href="style.css" type="text/css" />
<script src="../lib/OpenLayers.js"></script>
<script type="text/javascript">
var map, photos, paging;
OpenLayers.ProxyHost = (window.location.host == "localhost") ?
"/cgi-bin/proxy.cgi?url=" : "proxy.cgi?url=";
function init() {
map = new OpenLayers.Map('map', {
restrictedExtent: new OpenLayers.Bounds(-180, -90, 180, 90)
});
var base = new OpenLayers.Layer.WMS("OpenLayers WMS",
["http://t3.labs.metacarta.com/wms-c/Basic.py",
"http://t2.labs.metacarta.com/wms-c/Basic.py",
"http://t1.labs.metacarta.com/wms-c/Basic.py"],
{layers: 'satellite'}
);
var style = new OpenLayers.Style({
externalGraphic: "${img_url}",
pointRadius: 30
});
paging = new OpenLayers.Strategy.Paging();
photos = new OpenLayers.Layer.Vector("Photos", {
strategies: [new OpenLayers.Strategy.Fixed(), paging],
protocol: new OpenLayers.Protocol.HTTP({
url: "http://labs.metacarta.com/flickrbrowse/flickr.py/flickr",
params: {
format: "WFS",
sort: "interestingness-desc",
service: "WFS",
request: "GetFeatures",
srs: "EPSG:4326",
maxfeatures: 100,
bbox: [-180, -90, 180, 90]
},
format: new OpenLayers.Format.GML()
}),
styleMap: new OpenLayers.StyleMap(style)
});
map.addLayers([base, photos]);
photos.events.on({"featuresadded": updateButtons});
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
}
function updateButtons() {
document.getElementById("prev").disabled = (paging.pageNum() < 1);
document.getElementById("next").disabled = (paging.pageNum() >= paging.pageCount() - 1);
document.getElementById("num").innerHTML = paging.pageNum() + 1;
document.getElementById("count").innerHTML = paging.pageCount();
}
</script>
</head>
<body onload="init()">
<h1 id="title">Paging Strategy Example</h1>
<p id="shortdesc">
Uses a paging strategy to cache large batches of features and render a page at a time.
</p>
<div id="map" class="smallmap"></div>
Displaying page <span id="num">0</span> of <span id="count">...</span>
<button id="prev" disabled="disabled" onclick="paging.pagePrevious();">previous</button>
<button id="next" disabled="disabled" onclick="paging.pageNext();">next</button>
<br /><br />
<div id="docs">
<p>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.</p>
</div>
</body>
</html>

View File

@@ -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",

View File

@@ -38,7 +38,12 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, {
* Supported map event types (in addition to those from <OpenLayers.Layer>):
* - *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<len; i++) {
if (i != (features.length - 1)) {
@@ -469,9 +484,10 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, {
}
if (notify) {
this.events.triggerEvent("beforefeatureadded", {
feature: feature
});
if(this.events.triggerEvent("beforefeatureadded",
{feature: feature}) === false) {
continue;
};
this.preFeatureInsert(feature);
}

View File

@@ -0,0 +1,261 @@
/* 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.Cluster
* Strategy for vector feature clustering.
*
* Inherits from:
* - <OpenLayers.Strategy>
*/
OpenLayers.Strategy.Cluster = OpenLayers.Class(OpenLayers.Strategy, {
/**
* Property: layer
* {<OpenLayers.Layer.Vector>} 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(<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,
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<this.features.length; ++i) {
this.features[i].destroy();
}
}
this.features = null;
},
/**
* Method: cluster
* Cluster features based on some threshold distance.
*/
cluster: function() {
if(this.features) {
var resolution = this.layer.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];
clustered = false;
for(var j=0; j<clusters.length; ++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.destroyFeatures();
if(clusters.length > 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<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"
});

View File

@@ -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>
*/
OpenLayers.Strategy.Paging = OpenLayers.Class(OpenLayers.Strategy, {
/**
* Property: layer
* {<OpenLayers.Layer.Vector>} The layer that this strategy is assigned to.
*/
layer: null,
/**
* Property: features
* {Array(<OpenLayers.Feature.Vector>)} 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<this.features.length; ++i) {
this.features[i].destroy();
}
}
this.features = null;
this.num = null;
},
/**
* APIMethod: pageCount
* Get the total count of pages given the current cache of features.
*
* Returns:
* {Integer} The page count.
*/
pageCount: function() {
var numFeatures = this.features ? this.features.length : 0;
return Math.ceil(numFeatures / this.length);
},
/**
* APIMethod: pageNum
* Get the zero based page number.
*
* Returns:
* {Integer} The current page number being displayed.
*/
pageNum: function() {
return this.num;
},
/**
* APIMethod: pageLength
* Gets or sets page length.
*
* Parameters:
* newLength: {Integer} Optional length to be set.
*
* Returns:
* {Integer} The length of a page (number of features per page).
*/
pageLength: function(newLength) {
if(newLength && newLength > 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"
});

108
tests/Strategy/Cluster.html Normal file
View File

@@ -0,0 +1,108 @@
<html>
<head>
<script src="../../lib/OpenLayers.js"></script>
<script type="text/javascript">
function test_activate(t) {
t.plan(2);
var strategy = new OpenLayers.Strategy.Cluster();
t.eq(strategy.active, false, "not active after construction");
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy]
});
var map = new OpenLayers.Map('map');
map.addLayer(layer);
t.eq(strategy.active, true, "active after adding to map");
}
function test_clusters(t) {
t.plan(10);
function featuresEq(got, exp) {
var eq = false;
if(got instanceof Array && exp instanceof Array) {
if(got.length === exp.length) {
for(var i=0; i<got.length; ++i) {
if(got[i] !== exp[i]) {
console.log(got[i], exp[i]);
break;
}
}
eq = (i == got.length);
}
}
return eq;
}
var strategy = new OpenLayers.Strategy.Cluster();
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy],
isBaseLayer: true
});
var map = new OpenLayers.Map('map', {
resolutions: [4, 2, 1],
maxExtent: new OpenLayers.Bounds(-40, -40, 40, 40)
});
map.addLayer(layer);
// create features in a line, 1 unit apart
var features = new Array(80);
for(var i=0; i<80; ++i) {
features[i] = new OpenLayers.Feature.Vector(
new OpenLayers.Geometry.Point(-40 + i, 0)
);
}
map.setCenter(new OpenLayers.LonLat(0, 0), 0);
layer.addFeatures(features);
// resolution 4
// threshold: 4 * 20 = 80 units
// one cluster
t.eq(layer.features.length, 1, "[4] layer has one cluster");
t.ok(featuresEq(layer.features[0].cluster, features), "[4] cluster includes all features");
// resolution 2
// threshold: 2 * 20 = 40 units
// two clusters (41 and 39) - first cluster includes all features within 40 units of the first (0-40 or 41 features)
map.zoomIn();
t.eq(layer.features.length, 2, "[2] layer has two clusters");
t.ok(featuresEq(layer.features[0].cluster, features.slice(0, 41)), "[2] first cluster includes first 41 features");
t.ok(featuresEq(layer.features[1].cluster, features.slice(41, 80)), "[2] second cluster includes last 39 features");
// resolution 1
// threshold: 1 * 20 = 20 units
// four clusters (21, 21, 21, and 17)
map.zoomIn();
t.eq(layer.features.length, 4, "[1] layer has four clusters");
t.ok(featuresEq(layer.features[0].cluster, features.slice(0, 21)), "[1] first cluster includes first 21 features");
t.ok(featuresEq(layer.features[1].cluster, features.slice(21, 42)), "[2] second cluster includes second 21 features");
t.ok(featuresEq(layer.features[2].cluster, features.slice(42, 63)), "[2] third cluster includes third 21 features");
t.ok(featuresEq(layer.features[3].cluster, features.slice(63, 80)), "[2] fourth cluster includes last 17 features");
}
function test_deactivate(t) {
t.plan(2);
var strategy = new OpenLayers.Strategy.Cluster();
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy]
});
var map = new OpenLayers.Map('map');
map.addLayer(layer);
t.eq(strategy.active, true, "active after adding to map");
map.removeLayer(layer);
t.eq(strategy.active, false, "not active after removing from map");
}
</script>
</head>
<body>
<div id="map" style="width: 400px; height: 200px" />
</body>
</html>

113
tests/Strategy/Paging.html Normal file
View File

@@ -0,0 +1,113 @@
<html>
<head>
<script src="../../lib/OpenLayers.js"></script>
<script type="text/javascript">
function test_activate(t) {
t.plan(2);
var strategy = new OpenLayers.Strategy.Paging();
t.eq(strategy.active, false, "not active after construction");
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy]
});
var map = new OpenLayers.Map('map');
map.addLayer(layer);
t.eq(strategy.active, true, "active after adding to map");
}
function test_paging(t) {
t.plan(18);
var strategy = new OpenLayers.Strategy.Paging();
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy],
drawFeature: function() {}
});
var map = new OpenLayers.Map('map');
map.addLayer(layer);
var features = new Array(25);
for(var i=0; i<features.length; ++i) {
features[i] = {destroy: function() {}};
}
function featuresEq(got, exp) {
var eq = false;
if(got instanceof Array && exp instanceof Array) {
if(got.length === exp.length) {
for(var i=0; i<got.length; ++i) {
if(got[i] !== exp[i]) {
console.log(got[i], exp[i]);
break;
}
}
eq = (i == got.length);
}
}
return eq;
}
var len = strategy.pageLength();
t.eq(len, 10, "page length defaults to 10");
// add 25 features to the layer
layer.addFeatures(features);
t.eq(strategy.features.length, features.length, "strategy caches all features");
t.eq(layer.features.length, len, "layer gets one page of features");
t.ok(featuresEq(layer.features, features.slice(0, len)), "layer gets first page initially");
t.eq(strategy.pageNum(), 0, "strategy reports 0 based page number");
t.eq(strategy.pageCount(), Math.ceil(features.length / len), "strategy reports correct number of pages");
// load next page of features
var changed = strategy.pageNext();
t.eq(changed, true, "(1) strategy reports change");
t.eq(strategy.pageNum(), 1, "second page");
t.ok(featuresEq(layer.features, features.slice(len, 2*len)), "layer has second page of features");
// load next page of features (half page)
changed = strategy.pageNext();
t.eq(changed, true, "(2) strategy reports change");
t.eq(strategy.pageNum(), 2, "third page");
// try to change forward again
changed = strategy.pageNext();
t.eq(changed, false, "strategy reports no change");
t.eq(layer.features.length, features.length % len, "layer has partial page");
t.ok(featuresEq(layer.features, features.slice(2*len, 3*len)), "layer has third page of features");
t.eq(strategy.pageNum(), 2, "still on third page");
// change back a page
changed = strategy.pagePrevious();
t.eq(changed, true, "(3) strategy reports change");
t.eq(strategy.pageNum(), 1, "back on second page");
t.ok(featuresEq(layer.features, features.slice(len, 2*len)), "layer has second page of features again");
layer.destroy();
}
function test_deactivate(t) {
t.plan(2);
var strategy = new OpenLayers.Strategy.Paging();
var layer = new OpenLayers.Layer.Vector("Vector Layer", {
strategies: [strategy]
});
var map = new OpenLayers.Map('map');
map.addLayer(layer);
t.eq(strategy.active, true, "active after adding to map");
map.removeLayer(layer);
t.eq(strategy.active, false, "not active after removing from map");
}
</script>
</head>
<body>
<div id="map" style="width: 400px; height: 200px" />
</body>
</html>

View File

@@ -124,7 +124,9 @@
<li>Request/XMLHttpRequest.html</li>
<li>Rule.html</li>
<li>Strategy.html</li>
<li>Strategy/Cluster.html</li>
<li>Strategy/Fixed.html</li>
<li>Strategy/Paging.html</li>
<li>Strategy/BBOX.html</li>
<li>Style.html</li>
<li>StyleMap.html</li>