diff --git a/examples/animation.js b/examples/animation.js
index f287201039..136fd54b68 100644
--- a/examples/animation.js
+++ b/examples/animation.js
@@ -23,6 +23,7 @@ var bern = ol.projection.transform(
var map = new ol.Map({
layers: [
new ol.layer.TileLayer({
+ preload: 4,
source: new ol.source.OpenStreetMap()
})
],
diff --git a/examples/bing-maps.js b/examples/bing-maps.js
index e4be7be46a..4b75f11264 100644
--- a/examples/bing-maps.js
+++ b/examples/bing-maps.js
@@ -12,6 +12,7 @@ var layers = [];
for (var i = 0; i < styles.length; ++i) {
layers.push(new ol.layer.TileLayer({
visible: false,
+ preload: Infinity,
source: new ol.source.BingMaps({
key: 'AgtFlPYDnymLEe9zJ5PCkghbNiFZE9aAtTy3mPaEnEBXqLHtFuTcKoZ-miMC3w7R',
style: styles[i]
diff --git a/examples/preload.html b/examples/preload.html
new file mode 100644
index 0000000000..179642c3b2
--- /dev/null
+++ b/examples/preload.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+ Preload example
+
+
+
+
+
+
+
+
+
+
+
+
+
Preload example
+
Example of tile preloading. Low resolution tiles for the map are preloaded, the map on the right does not use any preloading. Try zooming out and panning to see the difference.
+
+
preload, bing
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/preload.js b/examples/preload.js
new file mode 100644
index 0000000000..278e1226b4
--- /dev/null
+++ b/examples/preload.js
@@ -0,0 +1,40 @@
+goog.require('ol.Coordinate');
+goog.require('ol.Map');
+goog.require('ol.RendererHints');
+goog.require('ol.View2D');
+goog.require('ol.layer.TileLayer');
+goog.require('ol.source.BingMaps');
+
+
+var map1 = new ol.Map({
+ layers: [
+ new ol.layer.TileLayer({
+ preload: Infinity,
+ source: new ol.source.BingMaps({
+ key: 'AgtFlPYDnymLEe9zJ5PCkghbNiFZE9aAtTy3mPaEnEBXqLHtFuTcKoZ-miMC3w7R',
+ style: 'Aerial'
+ })
+ })
+ ],
+ renderers: ol.RendererHints.createFromQueryData(),
+ target: 'map1',
+ view: new ol.View2D({
+ center: new ol.Coordinate(-4808600, -2620936),
+ zoom: 8
+ })
+});
+
+var map2 = new ol.Map({
+ layers: [
+ new ol.layer.TileLayer({
+ preload: 0, // default value
+ source: new ol.source.BingMaps({
+ key: 'AgtFlPYDnymLEe9zJ5PCkghbNiFZE9aAtTy3mPaEnEBXqLHtFuTcKoZ-miMC3w7R',
+ style: 'AerialWithLabels'
+ })
+ })
+ ],
+ renderers: ol.RendererHints.createFromQueryData(),
+ target: 'map2'
+});
+map2.bindTo('view', map1);
diff --git a/examples/rotation.js b/examples/rotation.js
index 9322443256..75a519f4f9 100644
--- a/examples/rotation.js
+++ b/examples/rotation.js
@@ -9,6 +9,7 @@ goog.require('ol.source.OpenStreetMap');
var map = new ol.Map({
layers: [
new ol.layer.TileLayer({
+ preload: 4,
source: new ol.source.OpenStreetMap()
})
],
diff --git a/examples/stamen.js b/examples/stamen.js
index 993cb6381f..d5dd0306cd 100644
--- a/examples/stamen.js
+++ b/examples/stamen.js
@@ -10,6 +10,7 @@ goog.require('ol.source.Stamen');
var map = new ol.Map({
layers: [
new ol.layer.TileLayer({
+ preload: 4,
source: new ol.source.Stamen({
layer: 'watercolor'
})
diff --git a/src/objectliterals.exports b/src/objectliterals.exports
index c60cc92e9a..593cae744f 100644
--- a/src/objectliterals.exports
+++ b/src/objectliterals.exports
@@ -100,6 +100,16 @@
@exportObjectLiteralProperty ol.layer.LayerOptions.source ol.source.Source
@exportObjectLiteralProperty ol.layer.LayerOptions.visible boolean|undefined
+@exportObjectLiteral ol.layer.TileLayerOptions
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.brightness number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.contrast number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.hue number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.opacity number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.preload number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.saturation number|undefined
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.source ol.source.Source
+@exportObjectLiteralProperty ol.layer.TileLayerOptions.visible boolean|undefined
+
@exportObjectLiteral ol.layer.VectorLayerOptions
@exportObjectLiteralProperty ol.layer.VectorLayerOptions.opacity number|undefined
@exportObjectLiteralProperty ol.layer.VectorLayerOptions.source ol.source.Source
diff --git a/src/ol/framestate.js b/src/ol/framestate.js
index a4c97039a0..cffecf36b4 100644
--- a/src/ol/framestate.js
+++ b/src/ol/framestate.js
@@ -23,6 +23,7 @@ goog.require('ol.layer.LayerState');
* backgroundColor: ol.Color,
* coordinateToPixelMatrix: goog.vec.Mat4.Number,
* extent: (null|ol.Extent),
+ * focus: ol.Coordinate,
* layersArray: Array.,
* layerStates: Object.,
* pixelToCoordinateMatrix: goog.vec.Mat4.Number,
diff --git a/src/ol/layer/tilelayer.exports b/src/ol/layer/tilelayer.exports
index a567e216ff..9e6ffaf01f 100644
--- a/src/ol/layer/tilelayer.exports
+++ b/src/ol/layer/tilelayer.exports
@@ -1 +1 @@
-@exportClass ol.layer.TileLayer ol.layer.LayerOptions
+@exportClass ol.layer.TileLayer ol.layer.TileLayerOptions
diff --git a/src/ol/layer/tilelayer.js b/src/ol/layer/tilelayer.js
index 878185e77f..bc27f66982 100644
--- a/src/ol/layer/tilelayer.js
+++ b/src/ol/layer/tilelayer.js
@@ -4,21 +4,58 @@ goog.require('ol.layer.Layer');
goog.require('ol.source.TileSource');
+/**
+ * @enum {string}
+ */
+ol.layer.TileLayerProperty = {
+ PRELOAD: 'preload'
+};
+
+
/**
* @constructor
* @extends {ol.layer.Layer}
- * @param {ol.layer.LayerOptions} layerOptions Layer options.
+ * @param {ol.layer.TileLayerOptions} options Options.
*/
-ol.layer.TileLayer = function(layerOptions) {
- goog.base(this, layerOptions);
+ol.layer.TileLayer = function(options) {
+
+ goog.base(this, options);
+
+ this.setPreload(
+ goog.isDef(options.preload) ? options.preload : 0);
+
};
goog.inherits(ol.layer.TileLayer, ol.layer.Layer);
+/**
+ * @return {number} Preload.
+ */
+ol.layer.TileLayer.prototype.getPreload = function() {
+ return /** @type {number} */ (this.get(ol.layer.TileLayerProperty.PRELOAD));
+};
+goog.exportProperty(
+ ol.layer.TileLayer.prototype,
+ 'getPreload',
+ ol.layer.TileLayer.prototype.getPreload);
+
+
/**
* @return {ol.source.TileSource} Source.
*/
ol.layer.TileLayer.prototype.getTileSource = function() {
return /** @type {ol.source.TileSource} */ (this.getSource());
};
+
+
+/**
+ * @param {number} preload Preload.
+ */
+ol.layer.TileLayer.prototype.setPreload = function(preload) {
+ this.set(ol.layer.TileLayerProperty.PRELOAD, preload);
+};
+goog.exportProperty(
+ ol.layer.TileLayer.prototype,
+ 'setPreload',
+ ol.layer.TileLayer.prototype.setPreload);
diff --git a/src/ol/map.js b/src/ol/map.js
index 45a6294b63..c594839c76 100644
--- a/src/ol/map.js
+++ b/src/ol/map.js
@@ -55,6 +55,7 @@ goog.require('ol.renderer.dom.Map');
goog.require('ol.renderer.dom.SUPPORTED');
goog.require('ol.renderer.webgl.Map');
goog.require('ol.renderer.webgl.SUPPORTED');
+goog.require('ol.structs.PriorityQueue');
/**
@@ -493,20 +494,22 @@ ol.Map.prototype.getTilePriority =
// are outside the visible extent.
var frameState = this.frameState_;
if (goog.isNull(frameState) || !(tileSourceKey in frameState.wantedTiles)) {
- return ol.TileQueue.DROP;
+ return ol.structs.PriorityQueue.DROP;
}
var coordKey = tile.tileCoord.toString();
if (!frameState.wantedTiles[tileSourceKey][coordKey]) {
- return ol.TileQueue.DROP;
+ return ol.structs.PriorityQueue.DROP;
}
- // Prioritize tiles closest to the focus or center. The + 1 helps to
- // prioritize tiles at higher zoom levels over tiles at lower zoom levels,
- // even if the tile's center is close to the focus.
- var focus = goog.isNull(this.focus_) ?
- frameState.view2DState.center : this.focus_;
- var deltaX = tileCenter.x - focus.x;
- var deltaY = tileCenter.y - focus.y;
- return tileResolution * (deltaX * deltaX + deltaY * deltaY + 1);
+ // Prioritize the highest zoom level tiles closest to the focus.
+ // Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
+ // Within a zoom level, tiles are prioritized by the distance in pixels
+ // between the center of the tile and the focus. The factor of 65536 means
+ // that the prioritization should behave as desired for tiles up to
+ // 65536 * Math.log(2) = 45426 pixels from the focus.
+ var deltaX = tileCenter.x - frameState.focus.x;
+ var deltaY = tileCenter.y - frameState.focus.y;
+ return 65536 * Math.log(tileResolution) +
+ Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
};
@@ -713,6 +716,7 @@ ol.Map.prototype.renderFrame_ = function(time) {
backgroundColor : new ol.Color(255, 255, 255, 1),
coordinateToPixelMatrix: this.coordinateToPixelMatrix_,
extent: null,
+ focus: goog.isNull(this.focus_) ? view2DState.center : this.focus_,
layersArray: layersArray,
layerStates: layerStates,
pixelToCoordinateMatrix: this.pixelToCoordinateMatrix_,
diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js
index 70e528542c..64a1398344 100644
--- a/src/ol/renderer/canvas/canvastilelayerrenderer.js
+++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js
@@ -285,8 +285,8 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame =
}
this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
- this.manageTilePyramid(
- frameState, tileSource, tileGrid, projection, extent, z);
+ this.manageTilePyramid(frameState, tileSource, tileGrid, projection, extent,
+ z, tileLayer.getPreload());
this.scheduleExpireCache(frameState, tileSource);
var transform = this.transform_;
diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js
index 8c056742bd..61cb6a3908 100644
--- a/src/ol/renderer/dom/domtilelayerrenderer.js
+++ b/src/ol/renderer/dom/domtilelayerrenderer.js
@@ -218,8 +218,8 @@ ol.renderer.dom.TileLayer.prototype.renderFrame =
}
this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
- this.manageTilePyramid(
- frameState, tileSource, tileGrid, projection, extent, z);
+ this.manageTilePyramid(frameState, tileSource, tileGrid, projection, extent,
+ z, tileLayer.getPreload());
this.scheduleExpireCache(frameState, tileSource);
};
diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js
index 6484c01a3e..ec05d79c8f 100644
--- a/src/ol/renderer/layerrenderer.js
+++ b/src/ol/renderer/layerrenderer.js
@@ -17,12 +17,6 @@ goog.require('ol.layer.LayerState');
goog.require('ol.source.TileSource');
-/**
- * @define {boolean} Preemptively load low resolution tiles.
- */
-ol.PREEMPTIVELY_LOAD_LOW_RESOLUTION_TILES = true;
-
-
/**
* @constructor
@@ -297,29 +291,39 @@ ol.renderer.Layer.prototype.snapCenterToPixel =
* @param {ol.Projection} projection Projection.
* @param {ol.Extent} extent Extent.
* @param {number} currentZ Current Z.
+ * @param {number} preload Load low resolution tiles up to 'preload' levels.
+ * @param {function(this: T, ol.Tile)=} opt_tileCallback Tile callback.
+ * @param {T=} opt_obj Object.
* @protected
+ * @template T
*/
-ol.renderer.Layer.prototype.manageTilePyramid =
- function(frameState, tileSource, tileGrid, projection, extent, currentZ) {
+ol.renderer.Layer.prototype.manageTilePyramid = function(
+ frameState, tileSource, tileGrid, projection, extent, currentZ, preload,
+ opt_tileCallback, opt_obj) {
var tileSourceKey = goog.getUid(tileSource).toString();
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
var wantedTiles = frameState.wantedTiles[tileSourceKey];
var tileQueue = frameState.tileQueue;
- var tile, tileCenter, tileRange, tileResolution, x, y, z;
+ var tile, tileRange, tileResolution, x, y, z;
// FIXME this should loop up to tileGrid's minZ when implemented
for (z = currentZ; z >= 0; --z) {
tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
tileResolution = tileGrid.getResolution(z);
for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
- if (ol.PREEMPTIVELY_LOAD_LOW_RESOLUTION_TILES || z == currentZ) {
+ if (currentZ - z <= preload) {
tile = tileSource.getTile(z, x, y, tileGrid, projection);
if (tile.getState() == ol.TileState.IDLE) {
- tileCenter = tileGrid.getTileCoordCenter(tile.tileCoord);
wantedTiles[tile.tileCoord.toString()] = true;
- tileQueue.enqueue(tile, tileSourceKey, tileCenter, tileResolution);
+ if (!tileQueue.isKeyQueued(tile.getKey())) {
+ tileQueue.enqueue([tile, tileSourceKey,
+ tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]);
+ }
+ }
+ if (goog.isDef(opt_tileCallback)) {
+ opt_tileCallback.call(opt_obj, tile);
}
} else {
tileSource.useTile(z, x, y);
diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js
index 655350d6f9..93b5073e61 100644
--- a/src/ol/renderer/webgl/webglmaprenderer.js
+++ b/src/ol/renderer/webgl/webglmaprenderer.js
@@ -23,6 +23,7 @@ goog.require('ol.renderer.webgl.map.shader');
goog.require('ol.structs.Buffer');
goog.require('ol.structs.IntegerSet');
goog.require('ol.structs.LRUCache');
+goog.require('ol.structs.PriorityQueue');
goog.require('ol.webgl');
goog.require('ol.webgl.WebGLContextEventType');
goog.require('ol.webgl.shader');
@@ -154,6 +155,52 @@ ol.renderer.webgl.Map = function(container, map) {
*/
this.textureCache_ = new ol.structs.LRUCache();
+ /**
+ * @private
+ * @type {ol.Coordinate}
+ */
+ this.focus_ = null;
+
+ /**
+ * @private
+ * @type {ol.structs.PriorityQueue}
+ */
+ this.tileTextureQueue_ = new ol.structs.PriorityQueue(
+ /**
+ * @param {Array} element Element.
+ * @return {number} Priority.
+ */
+ goog.bind(function(element) {
+ var tile = /** @type {ol.Tile} */ (element[0]);
+ var tileCenter = /** @type {ol.Coordinate} */ (element[1]);
+ var tileResolution = /** @type {number} */ (element[2]);
+ var deltaX = tileCenter.x - this.focus_.x;
+ var deltaY = tileCenter.y - this.focus_.y;
+ return 65536 * Math.log(tileResolution) +
+ Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
+ }, this),
+ /**
+ * @param {Array} element Element.
+ * @return {string} Key.
+ */
+ function(element) {
+ return /** @type {ol.Tile} */ (element[0]).getKey();
+ });
+
+ /**
+ * @private
+ * @type {ol.PostRenderFunction}
+ */
+ this.loadNextTileTexture_ = goog.bind(
+ function(map, frameState) {
+ if (!this.tileTextureQueue_.isEmpty()) {
+ this.tileTextureQueue_.reprioritize();
+ var tile =
+ /** @type {ol.Tile} */ (this.tileTextureQueue_.dequeue()[0]);
+ this.bindTileTexture(tile, goog.webgl.LINEAR, goog.webgl.LINEAR);
+ }
+ }, this);
+
/**
* @private
* @type {number}
@@ -424,6 +471,14 @@ ol.renderer.webgl.Map.prototype.getShader = function(shaderObject) {
};
+/**
+ * @return {ol.structs.PriorityQueue} Tile texture queue.
+ */
+ol.renderer.webgl.Map.prototype.getTileTextureQueue = function() {
+ return this.tileTextureQueue_;
+};
+
+
/**
* @param {goog.events.Event} event Event.
* @protected
@@ -494,6 +549,8 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) {
return false;
}
+ this.focus_ = frameState.focus;
+
this.textureCache_.set(frameState.time.toString(), null);
++this.textureCacheFrameMarkerCount_;
@@ -584,4 +641,9 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) {
frameState.postRenderFunctions.push(goog.bind(this.expireCache_, this));
}
+ if (!this.tileTextureQueue_.isEmpty()) {
+ frameState.postRenderFunctions.push(this.loadNextTileTexture_);
+ frameState.animate = true;
+ }
+
};
diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js
index a53ee77fce..690dd6ffaa 100644
--- a/src/ol/renderer/webgl/webgltilelayerrenderer.js
+++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js
@@ -5,7 +5,6 @@ goog.provide('ol.renderer.webgl.TileLayer');
goog.require('goog.array');
goog.require('goog.object');
-goog.require('goog.structs.PriorityQueue');
goog.require('goog.vec.Mat4');
goog.require('goog.vec.Vec4');
goog.require('goog.webgl');
@@ -210,10 +209,8 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame =
var findLoadedTiles = goog.bind(tileSource.findLoadedTiles, tileSource,
tilesToDrawByZ, getTileIfLoaded);
- var tilesToLoad = new goog.structs.PriorityQueue();
-
var allTilesLoaded = true;
- var deltaX, deltaY, priority, tile, tileCenter, tileState, x, y;
+ var tile, tileState, x, y;
for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
@@ -223,12 +220,6 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame =
if (mapRenderer.isTileTextureLoaded(tile)) {
tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
continue;
- } else {
- tileCenter = tileGrid.getTileCoordCenter(tile.tileCoord);
- deltaX = tileCenter.x - center.x;
- deltaY = tileCenter.y - center.y;
- priority = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- tilesToLoad.enqueue(priority, tile);
}
} else if (tileState == ol.TileState.ERROR ||
tileState == ol.TileState.EMPTY) {
@@ -263,19 +254,6 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame =
}, this);
}, this);
- if (!tilesToLoad.isEmpty()) {
- frameState.postRenderFunctions.push(
- goog.partial(function(mapRenderer, tilesToLoad) {
- var i, tile;
- // FIXME determine a suitable number of textures to upload per frame
- for (i = 0; !tilesToLoad.isEmpty() && i < 4; ++i) {
- tile = /** @type {ol.Tile} */ (tilesToLoad.remove());
- mapRenderer.bindTileTexture(
- tile, goog.webgl.LINEAR, goog.webgl.LINEAR);
- }
- }, mapRenderer, tilesToLoad));
- }
-
if (allTilesLoaded) {
this.renderedTileRange_ = tileRange;
this.renderedFramebufferExtent_ = framebufferExtent;
@@ -288,8 +266,21 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame =
}
this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
+ var tileTextureQueue = mapRenderer.getTileTextureQueue();
this.manageTilePyramid(
- frameState, tileSource, tileGrid, projection, extent, z);
+ frameState, tileSource, tileGrid, projection, extent, z,
+ tileLayer.getPreload(),
+ function(tile) {
+ if (tile.getState() == ol.TileState.LOADED &&
+ !mapRenderer.isTileTextureLoaded(tile) &&
+ !tileTextureQueue.isKeyQueued(tile.getKey())) {
+ tileTextureQueue.enqueue([
+ tile,
+ tileGrid.getTileCoordCenter(tile.tileCoord),
+ tileGrid.getResolution(tile.tileCoord.z)
+ ]);
+ }
+ }, this);
this.scheduleExpireCache(frameState, tileSource);
var texCoordMatrix = this.texCoordMatrix;
diff --git a/src/ol/structs/priorityqueue.js b/src/ol/structs/priorityqueue.js
new file mode 100644
index 0000000000..74bfb08e42
--- /dev/null
+++ b/src/ol/structs/priorityqueue.js
@@ -0,0 +1,288 @@
+goog.provide('ol.structs.PriorityQueue');
+
+goog.require('goog.asserts');
+goog.require('goog.object');
+
+
+
+/**
+ * Priority queue.
+ *
+ * The implementation is inspired from the Closure Library's Heap class and
+ * Python's heapq module.
+ *
+ * @see http://closure-library.googlecode.com/svn/docs/closure_goog_structs_heap.js.source.html
+ * @see http://hg.python.org/cpython/file/2.7/Lib/heapq.py
+ *
+ * @constructor
+ * @param {function(?): number} priorityFunction Priority function.
+ * @param {function(?): string} keyFunction Key function.
+ */
+ol.structs.PriorityQueue = function(priorityFunction, keyFunction) {
+
+ /**
+ * @type {function(?): number}
+ * @private
+ */
+ this.priorityFunction_ = priorityFunction;
+
+ /**
+ * @type {function(?): string}
+ * @private
+ */
+ this.keyFunction_ = keyFunction;
+
+ /**
+ * @type {Array}
+ * @private
+ */
+ this.elements_ = [];
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.priorities_ = [];
+
+ /**
+ * @type {Object.}
+ * @private
+ */
+ this.queuedElements_ = {};
+
+};
+
+
+/**
+ * @const {number}
+ */
+ol.structs.PriorityQueue.DROP = Infinity;
+
+
+/**
+ * FIXME empty desciption for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.assertValid = function() {
+ var elements = this.elements_;
+ var priorities = this.priorities_;
+ var n = elements.length;
+ goog.asserts.assert(priorities.length == n);
+ var i, priority;
+ for (i = 0; i < (n >> 1) - 1; ++i) {
+ priority = priorities[i];
+ goog.asserts.assert(priority <= priorities[this.getLeftChildIndex_(i)]);
+ goog.asserts.assert(priority <= priorities[this.getRightChildIndex_(i)]);
+ }
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.clear = function() {
+ this.elements_.length = 0;
+ this.priorities_.length = 0;
+ goog.object.clear(this.queuedElements_);
+};
+
+
+/**
+ * Remove and return the highest-priority element. O(log N).
+ * @return {*} Element.
+ */
+ol.structs.PriorityQueue.prototype.dequeue = function() {
+ var elements = this.elements_;
+ goog.asserts.assert(elements.length > 0);
+ var priorities = this.priorities_;
+ var element = elements[0];
+ if (elements.length == 1) {
+ elements.length = 0;
+ priorities.length = 0;
+ } else {
+ elements[0] = elements.pop();
+ priorities[0] = priorities.pop();
+ this.siftUp_(0);
+ }
+ var elementKey = this.keyFunction_(element);
+ goog.asserts.assert(elementKey in this.queuedElements_);
+ delete this.queuedElements_[elementKey];
+ return element;
+};
+
+
+/**
+ * Enqueue an element. O(log N).
+ * @param {*} element Element.
+ */
+ol.structs.PriorityQueue.prototype.enqueue = function(element) {
+ goog.asserts.assert(!(this.keyFunction_(element) in this.queuedElements_));
+ var priority = this.priorityFunction_(element);
+ if (priority != ol.structs.PriorityQueue.DROP) {
+ this.elements_.push(element);
+ this.priorities_.push(priority);
+ this.queuedElements_[this.keyFunction_(element)] = true;
+ this.siftDown_(0, this.elements_.length - 1);
+ }
+};
+
+
+/**
+ * @return {number} Count.
+ */
+ol.structs.PriorityQueue.prototype.getCount = function() {
+ return this.elements_.length;
+};
+
+
+/**
+ * Gets the index of the left child of the node at the given index.
+ * @param {number} index The index of the node to get the left child for.
+ * @return {number} The index of the left child.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getLeftChildIndex_ = function(index) {
+ return index * 2 + 1;
+};
+
+
+/**
+ * Gets the index of the right child of the node at the given index.
+ * @param {number} index The index of the node to get the right child for.
+ * @return {number} The index of the right child.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getRightChildIndex_ = function(index) {
+ return index * 2 + 2;
+};
+
+
+/**
+ * Gets the index of the parent of the node at the given index.
+ * @param {number} index The index of the node to get the parent for.
+ * @return {number} The index of the parent.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getParentIndex_ = function(index) {
+ return (index - 1) >> 1;
+};
+
+
+/**
+ * Make this a heap. O(N).
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.heapify_ = function() {
+ var i;
+ for (i = (this.elements_.length >> 1) - 1; i >= 0; i--) {
+ this.siftUp_(i);
+ }
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.structs.PriorityQueue.prototype.isEmpty = function() {
+ return this.elements_.length === 0;
+};
+
+
+/**
+ * @param {string} key Key.
+ * @return {boolean} Is key queued.
+ */
+ol.structs.PriorityQueue.prototype.isKeyQueued = function(key) {
+ return key in this.queuedElements_;
+};
+
+
+/**
+ * @param {*} element Element.
+ * @return {boolean} Is queued.
+ */
+ol.structs.PriorityQueue.prototype.isQueued = function(element) {
+ return this.isKeyQueued(this.keyFunction_(element));
+};
+
+
+/**
+ * @param {number} index The index of the node to move down.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.siftUp_ = function(index) {
+ var elements = this.elements_;
+ var priorities = this.priorities_;
+ var count = elements.length;
+ var element = elements[index];
+ var priority = priorities[index];
+ var startIndex = index;
+
+ while (index < (count >> 1)) {
+ var lIndex = this.getLeftChildIndex_(index);
+ var rIndex = this.getRightChildIndex_(index);
+
+ var smallerChildIndex = rIndex < count &&
+ priorities[rIndex] < priorities[lIndex] ?
+ rIndex : lIndex;
+
+ elements[index] = elements[smallerChildIndex];
+ priorities[index] = priorities[smallerChildIndex];
+ index = smallerChildIndex;
+ }
+
+ elements[index] = element;
+ priorities[index] = priority;
+ this.siftDown_(startIndex, index);
+};
+
+
+/**
+ * @param {number} startIndex The index of the root.
+ * @param {number} index The index of the node to move up.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.siftDown_ = function(startIndex, index) {
+ var elements = this.elements_;
+ var priorities = this.priorities_;
+ var element = elements[index];
+ var priority = priorities[index];
+
+ while (index > startIndex) {
+ var parentIndex = this.getParentIndex_(index);
+ if (priorities[parentIndex] > priority) {
+ elements[index] = elements[parentIndex];
+ priorities[index] = priorities[parentIndex];
+ index = parentIndex;
+ } else {
+ break;
+ }
+ }
+ elements[index] = element;
+ priorities[index] = priority;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.reprioritize = function() {
+ var priorityFunction = this.priorityFunction_;
+ var elements = this.elements_;
+ var priorities = this.priorities_;
+ var index = 0;
+ var n = elements.length;
+ var element, i, priority;
+ for (i = 0; i < n; ++i) {
+ element = elements[i];
+ priority = priorityFunction(element);
+ if (priority == ol.structs.PriorityQueue.DROP) {
+ delete this.queuedElements_[this.keyFunction_(element)];
+ } else {
+ priorities[index] = priority;
+ elements[index++] = element;
+ }
+ }
+ elements.length = index;
+ priorities.length = index;
+ this.heapify_();
+};
diff --git a/src/ol/tilequeue.js b/src/ol/tilequeue.js
index 43aaf920d3..4d21f2ef44 100644
--- a/src/ol/tilequeue.js
+++ b/src/ol/tilequeue.js
@@ -5,18 +5,7 @@ goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.Coordinate');
goog.require('ol.Tile');
-goog.require('ol.TileState');
-
-
-/**
- * Tile Queue.
- *
- * The implementation is inspired from the Closure Library's Heap
- * class and Python's heapq module.
- *
- * http://closure-library.googlecode.com/svn/docs/closure_goog_structs_heap.js.source.html
- * http://hg.python.org/cpython/file/2.7/Lib/heapq.py
- */
+goog.require('ol.structs.PriorityQueue');
/**
@@ -28,6 +17,7 @@ ol.TilePriorityFunction;
/**
* @constructor
+ * @extends {ol.structs.PriorityQueue}
* @param {ol.TilePriorityFunction} tilePriorityFunction
* Tile priority function.
* @param {Function} tileChangeCallback
@@ -35,11 +25,22 @@ ol.TilePriorityFunction;
*/
ol.TileQueue = function(tilePriorityFunction, tileChangeCallback) {
- /**
- * @private
- * @type {ol.TilePriorityFunction}
- */
- this.tilePriorityFunction_ = tilePriorityFunction;
+ goog.base(
+ this,
+ /**
+ * @param {Array} element Element.
+ * @return {number} Priority.
+ */
+ function(element) {
+ return tilePriorityFunction.apply(null, element);
+ },
+ /**
+ * @param {Array} element Element.
+ * @return {string} Key.
+ */
+ function(element) {
+ return /** @type {ol.Tile} */ (element[0]).getKey();
+ });
/**
* @private
@@ -59,72 +60,8 @@ ol.TileQueue = function(tilePriorityFunction, tileChangeCallback) {
*/
this.tilesLoading_ = 0;
- /**
- * @private
- * @type {Array.>}
- */
- this.heap_ = [];
-
- /**
- * @private
- * @type {Object.}
- */
- this.queuedTileKeys_ = {};
-
-};
-
-
-/**
- * @const {number}
- */
-ol.TileQueue.DROP = Infinity;
-
-
-/**
- * Remove and return the highest-priority tile. O(logn).
- * @private
- * @return {ol.Tile} Tile.
- */
-ol.TileQueue.prototype.dequeue_ = function() {
- var heap = this.heap_;
- goog.asserts.assert(heap.length > 0);
- var tile = /** @type {ol.Tile} */ (heap[0][1]);
- if (heap.length == 1) {
- heap.length = 0;
- } else {
- heap[0] = heap.pop();
- this.siftUp_(0);
- }
- var tileKey = tile.getKey();
- delete this.queuedTileKeys_[tileKey];
- return tile;
-};
-
-
-/**
- * Enqueue a tile. O(logn).
- * @param {ol.Tile} tile Tile.
- * @param {string} tileSourceKey Tile source key.
- * @param {ol.Coordinate} tileCenter Tile center.
- * @param {number} tileResolution Tile resolution.
- */
-ol.TileQueue.prototype.enqueue = function(
- tile, tileSourceKey, tileCenter, tileResolution) {
- if (tile.getState() != ol.TileState.IDLE) {
- return;
- }
- var tileKey = tile.getKey();
- if (!(tileKey in this.queuedTileKeys_)) {
- var priority = this.tilePriorityFunction_(
- tile, tileSourceKey, tileCenter, tileResolution);
- if (priority != ol.TileQueue.DROP) {
- this.heap_.push(
- [priority, tile, tileSourceKey, tileCenter, tileResolution]);
- this.queuedTileKeys_[tileKey] = true;
- this.siftDown_(0, this.heap_.length - 1);
- }
- }
};
+goog.inherits(ol.TileQueue, ol.structs.PriorityQueue);
/**
@@ -136,137 +73,16 @@ ol.TileQueue.prototype.handleTileChange = function() {
};
-/**
- * Gets the index of the left child of the node at the given index.
- * @param {number} index The index of the node to get the left child for.
- * @return {number} The index of the left child.
- * @private
- */
-ol.TileQueue.prototype.getLeftChildIndex_ = function(index) {
- return index * 2 + 1;
-};
-
-
-/**
- * Gets the index of the right child of the node at the given index.
- * @param {number} index The index of the node to get the right child for.
- * @return {number} The index of the right child.
- * @private
- */
-ol.TileQueue.prototype.getRightChildIndex_ = function(index) {
- return index * 2 + 2;
-};
-
-
-/**
- * Gets the index of the parent of the node at the given index.
- * @param {number} index The index of the node to get the parent for.
- * @return {number} The index of the parent.
- * @private
- */
-ol.TileQueue.prototype.getParentIndex_ = function(index) {
- return (index - 1) >> 1;
-};
-
-
-/**
- * Make _heap a heap. O(n).
- * @private
- */
-ol.TileQueue.prototype.heapify_ = function() {
- for (var i = (this.heap_.length >> 1) - 1; i >= 0; i--) {
- this.siftUp_(i);
- }
-};
-
-
/**
* FIXME empty description for jsdoc
*/
ol.TileQueue.prototype.loadMoreTiles = function() {
var tile;
- while (this.heap_.length > 0 && this.tilesLoading_ < this.maxTilesLoading_) {
- tile = /** @type {ol.Tile} */ (this.dequeue_());
+ while (!this.isEmpty() && this.tilesLoading_ < this.maxTilesLoading_) {
+ tile = /** @type {ol.Tile} */ (this.dequeue()[0]);
goog.events.listenOnce(tile, goog.events.EventType.CHANGE,
this.handleTileChange, false, this);
tile.load();
++this.tilesLoading_;
}
};
-
-
-/**
- * @param {number} index The index of the node to move down.
- * @private
- */
-ol.TileQueue.prototype.siftUp_ = function(index) {
- var heap = this.heap_;
- var count = heap.length;
- var node = heap[index];
- var startIndex = index;
-
- while (index < (count >> 1)) {
- var lIndex = this.getLeftChildIndex_(index);
- var rIndex = this.getRightChildIndex_(index);
-
- var smallerChildIndex = rIndex < count &&
- heap[rIndex][0] < heap[lIndex][0] ?
- rIndex : lIndex;
-
- heap[index] = heap[smallerChildIndex];
- index = smallerChildIndex;
- }
-
- heap[index] = node;
- this.siftDown_(startIndex, index);
-};
-
-
-/**
- * @param {number} startIndex The index of the root.
- * @param {number} index The index of the node to move up.
- * @private
- */
-ol.TileQueue.prototype.siftDown_ = function(startIndex, index) {
- var heap = this.heap_;
- var node = heap[index];
-
- while (index > startIndex) {
- var parentIndex = this.getParentIndex_(index);
- if (heap[parentIndex][0] > node[0]) {
- heap[index] = heap[parentIndex];
- index = parentIndex;
- } else {
- break;
- }
- }
- heap[index] = node;
-};
-
-
-/**
- * FIXME empty description for jsdoc
- */
-ol.TileQueue.prototype.reprioritize = function() {
- var heap = this.heap_;
- var i, n = 0, node, priority;
- var tile, tileCenter, tileKey, tileResolution, tileSourceKey;
- for (i = 0; i < heap.length; ++i) {
- node = heap[i];
- tile = /** @type {ol.Tile} */ (node[1]);
- tileSourceKey = /** @type {string} */ (node[2]);
- tileCenter = /** @type {ol.Coordinate} */ (node[3]);
- tileResolution = /** @type {number} */ (node[4]);
- priority = this.tilePriorityFunction_(
- tile, tileSourceKey, tileCenter, tileResolution);
- if (priority == ol.TileQueue.DROP) {
- tileKey = tile.getKey();
- delete this.queuedTileKeys_[tileKey];
- } else {
- node[0] = priority;
- heap[n++] = node;
- }
- }
- heap.length = n;
- this.heapify_();
-};
diff --git a/test/spec/ol/structs/priorityqueue.test.js b/test/spec/ol/structs/priorityqueue.test.js
new file mode 100644
index 0000000000..c9d7b27cd1
--- /dev/null
+++ b/test/spec/ol/structs/priorityqueue.test.js
@@ -0,0 +1,186 @@
+goog.provide('ol.test.structs.PriorityQueue');
+
+
+describe('ol.structs.PriorityQueue', function() {
+
+ describe('when empty', function() {
+
+ var pq;
+ beforeEach(function() {
+ pq = new ol.structs.PriorityQueue(
+ goog.identityFunction, goog.identityFunction);
+ });
+
+ it('is valid', function() {
+ expect(function() {
+ pq.assertValid();
+ }).not.to.throwException();
+ });
+
+ it('is empty', function() {
+ expect(pq.isEmpty()).to.be(true);
+ });
+
+ it('dequeue raises an exception', function() {
+ expect(function() {
+ pq.dequeue();
+ }).to.throwException();
+ });
+
+ it('enqueue adds an element', function() {
+ pq.enqueue(0);
+ expect(function() {
+ pq.assertValid();
+ }).not.to.throwException();
+ expect(pq.elements_).to.equalArray([0]);
+ expect(pq.priorities_).to.equalArray([0]);
+ });
+
+ it('maintains the pq property while elements are enqueued', function() {
+ var i;
+ for (i = 0; i < 32; ++i) {
+ pq.enqueue(Math.random());
+ expect(function() {
+ pq.assertValid();
+ }).not.to.throwException();
+ }
+ });
+
+ });
+
+ describe('when populated', function() {
+
+ var elements, pq;
+ beforeEach(function() {
+ elements = [];
+ pq = new ol.structs.PriorityQueue(
+ goog.identityFunction, goog.identityFunction);
+ var element, i;
+ for (i = 0; i < 32; ++i) {
+ element = Math.random();
+ pq.enqueue(element);
+ elements.push(element);
+ }
+ });
+
+ it('dequeues elements in the correct order', function() {
+ elements.sort();
+ var i;
+ for (i = 0; i < elements.length; ++i) {
+ expect(pq.dequeue()).to.be(elements[i]);
+ }
+ expect(pq.isEmpty()).to.be(true);
+ });
+
+ });
+
+ describe('with an impure priority function', function() {
+
+ var pq, target;
+ beforeEach(function() {
+ target = 0.5;
+ pq = new ol.structs.PriorityQueue(function(element) {
+ return Math.abs(element - target);
+ }, goog.identityFunction);
+ var element, i;
+ for (i = 0; i < 32; ++i) {
+ pq.enqueue(Math.random());
+ }
+ });
+
+ it('dequeue elements in the correct order', function() {
+ var lastDelta = 0;
+ var delta;
+ while (!pq.isEmpty()) {
+ delta = Math.abs(pq.dequeue() - target);
+ expect(lastDelta <= delta).to.be(true);
+ lastDelta = delta;
+ }
+ });
+
+ it('allows reprioritization', function() {
+ var target = 0.5;
+ pq.reprioritize();
+ var lastDelta = 0;
+ var delta;
+ while (!pq.isEmpty()) {
+ delta = Math.abs(pq.dequeue() - target);
+ expect(lastDelta <= delta).to.be(true);
+ lastDelta = delta;
+ }
+ });
+
+ it('allows dropping during reprioritization', function() {
+ var target = 0.5;
+ var i = 0;
+ pq.priorityFunction_ = function(element) {
+ if (i++ % 2 === 0) {
+ return Math.abs(element - target);
+ } else {
+ return ol.structs.PriorityQueue.DROP;
+ }
+ };
+ pq.reprioritize();
+ expect(pq.getCount()).to.be(16);
+ var lastDelta = 0;
+ var delta;
+ while (!pq.isEmpty()) {
+ delta = Math.abs(pq.dequeue() - target);
+ expect(lastDelta <= delta).to.be(true);
+ lastDelta = delta;
+ }
+ });
+
+ });
+
+ describe('tracks elements in the queue', function() {
+
+ var pq;
+ beforeEach(function() {
+ pq = new ol.structs.PriorityQueue(
+ goog.identityFunction, goog.identityFunction);
+ pq.enqueue('a');
+ pq.enqueue('b');
+ pq.enqueue('c');
+ });
+
+ it('tracks which elements have been queued', function() {
+ expect(pq.isQueued('a')).to.be(true);
+ expect(pq.isQueued('b')).to.be(true);
+ expect(pq.isQueued('c')).to.be(true);
+ });
+
+ it('tracks which elements have not been queued', function() {
+ expect(pq.isQueued('d')).to.be(false);
+ });
+
+ it('raises an error when an queued element is re-queued', function() {
+ expect(function() {
+ pq.enqueue('a');
+ }).to.throwException();
+ });
+
+ it('tracks which elements have be dequeued', function() {
+ expect(pq.isQueued('a')).to.be(true);
+ expect(pq.isQueued('b')).to.be(true);
+ expect(pq.isQueued('c')).to.be(true);
+ expect(pq.dequeue()).to.be('a');
+ expect(pq.isQueued('a')).to.be(false);
+ expect(pq.isQueued('b')).to.be(true);
+ expect(pq.isQueued('c')).to.be(true);
+ expect(pq.dequeue()).to.be('b');
+ expect(pq.isQueued('a')).to.be(false);
+ expect(pq.isQueued('b')).to.be(false);
+ expect(pq.isQueued('c')).to.be(true);
+ expect(pq.dequeue()).to.be('c');
+ expect(pq.isQueued('a')).to.be(false);
+ expect(pq.isQueued('b')).to.be(false);
+ expect(pq.isQueued('c')).to.be(false);
+ });
+
+ });
+
+});
+
+
+goog.require('ol.structs.PriorityQueue');
diff --git a/test/spec/ol/tilequeue.test.js b/test/spec/ol/tilequeue.test.js
index ab4b261e87..f46dbb8ac5 100644
--- a/test/spec/ol/tilequeue.test.js
+++ b/test/spec/ol/tilequeue.test.js
@@ -2,31 +2,14 @@ goog.provide('ol.test.TileQueue');
describe('ol.TileQueue', function() {
- // is the tile queue's array a heap?
- function isHeap(tq) {
- var heap = tq.heap_;
- var i;
- var key;
- var leftKey;
- var rightKey;
- for (i = 0; i < (heap.length >> 1) - 1; i++) {
- key = heap[i][0];
- leftKey = heap[tq.getLeftChildIndex_(i)][0];
- rightKey = heap[tq.getRightChildIndex_(i)][0];
- if (leftKey < key || rightKey < key) {
- return false;
- }
- }
- return true;
- }
-
function addRandomPriorityTiles(tq, num) {
var i, tile, priority;
for (i = 0; i < num; i++) {
tile = new ol.Tile();
priority = Math.floor(Math.random() * 100);
- tq.heap_.push([priority, tile, '', new ol.Coordinate(0, 0)]);
- tq.queuedTileKeys_[tile.getKey()] = true;
+ tq.elements_.push([tile, '', new ol.Coordinate(0, 0)]);
+ tq.priorities_.push(priority);
+ tq.queuedElements_[tile.getKey()] = true;
}
}
@@ -37,7 +20,9 @@ describe('ol.TileQueue', function() {
addRandomPriorityTiles(tq, 100);
tq.heapify_();
- expect(isHeap(tq)).to.be.ok();
+ expect(function() {
+ tq.assertValid();
+ }).not.to.throwException();
});
});
@@ -53,16 +38,19 @@ describe('ol.TileQueue', function() {
// rest
var i = 0;
- tq.tilePriorityFunction_ = function() {
+ tq.priorityFunction_ = function() {
if ((i++) % 2 === 0) {
- return ol.TileQueue.DROP;
+ return ol.structs.PriorityQueue.DROP;
}
return Math.floor(Math.random() * 100);
};
tq.reprioritize();
- expect(tq.heap_.length).to.eql(50);
- expect(isHeap(tq)).to.be.ok();
+ expect(tq.elements_.length).to.eql(50);
+ expect(tq.priorities_.length).to.eql(50);
+ expect(function() {
+ tq.assertValid();
+ }).not.to.throwException();
});
});
@@ -71,3 +59,4 @@ describe('ol.TileQueue', function() {
goog.require('ol.Coordinate');
goog.require('ol.Tile');
goog.require('ol.TileQueue');
+goog.require('ol.structs.PriorityQueue');