Merge pull request #423 from twpayne/preemptive-webgl-texture-uploads

White flash occurs when zooming out from initially zoomed in view with WebGL renderer
This commit is contained in:
Tom Payne
2013-03-27 16:00:56 -07:00
20 changed files with 774 additions and 284 deletions

View File

@@ -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()
})
],

View File

@@ -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]

58
examples/preload.html Normal file
View File

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="examples.css" type="text/css">
<link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.min.css" type="text/css">
<title>Preload example</title>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="./">OpenLayers 3 Examples</a>
<ul class="nav pull-right">
<li><iframe class="github-watch-button" src="http://ghbtns.com/github-btn.html?user=openlayers&repo=ol3&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" height="20" width="90"></iframe></li>
<li><a href="https://twitter.com/share" class="twitter-share-button" data-count="none" data-hashtags="openlayers">&nbsp;</a></li>
<li><div class="g-plusone-wrapper"><div class="g-plusone" data-size="medium" data-annotation="none"></div></div></li>
</ul>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div class="span6">
<div id="map1" class="map"></div>
</div>
<div class="span6">
<div id="map2" class="map"></div>
</div>
</div>
<div class="row-fluid">
<div class="span4">
<h4 id="title">Preload example</h4>
<p id="shortdesc">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.</p>
<div id="docs">
<p>See the <a href="simple.js" target="_blank">preload.js source</a> to see how this is done.</p>
</div>
<div id="tags">preload, bing</div>
</div>
</div>
</div>
<script src="loader.js?id=preload" type="text/javascript"></script>
<script src="social-links.js" type="text/javascript"></script>
</body>
</html>

40
examples/preload.js Normal file
View File

@@ -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);

View File

@@ -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()
})
],

View File

@@ -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'
})

View File

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

View File

@@ -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.<ol.layer.Layer>,
* layerStates: Object.<number, ol.layer.LayerState>,
* pixelToCoordinateMatrix: goog.vec.Mat4.Number,

View File

@@ -1 +1 @@
@exportClass ol.layer.TileLayer ol.layer.LayerOptions
@exportClass ol.layer.TileLayer ol.layer.TileLayerOptions

View File

@@ -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);

View File

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

View File

@@ -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_;

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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;
}
};

View File

@@ -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;

View File

@@ -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.<number>}
* @private
*/
this.priorities_ = [];
/**
* @type {Object.<string, boolean>}
* @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_();
};

View File

@@ -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.<Array.<*>>}
*/
this.heap_ = [];
/**
* @private
* @type {Object.<string, boolean>}
*/
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_();
};

View File

@@ -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');

View File

@@ -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');