diff --git a/src/ol/control/attributioncontrol.js b/src/ol/control/attributioncontrol.js index f7148722e8..52e33effba 100644 --- a/src/ol/control/attributioncontrol.js +++ b/src/ol/control/attributioncontrol.js @@ -69,7 +69,7 @@ ol.control.Attribution.prototype.handleMapPostrender = function(mapEvent) { if (goog.isNull(frameState)) { this.updateElement_(null); } else { - this.updateElement_(frameState.tileUsage); + this.updateElement_(frameState.usedTiles); } }; @@ -94,12 +94,12 @@ ol.control.Attribution.prototype.setMap = function(map) { /** * @private - * @param {?Object.>} tileUsage Tile - * usage. + * @param {?Object.>} usedTiles Used + * tiles. */ -ol.control.Attribution.prototype.updateElement_ = function(tileUsage) { +ol.control.Attribution.prototype.updateElement_ = function(usedTiles) { - if (goog.isNull(tileUsage)) { + if (goog.isNull(usedTiles)) { if (this.renderedVisible_) { goog.style.showElement(this.element, false); this.renderedVisible_ = false; @@ -136,14 +136,14 @@ ol.control.Attribution.prototype.updateElement_ = function(tileUsage) { var attributions = {}; var i, tileRanges, tileSource, tileSourceAttribution, tileSourceAttributionKey, tileSourceAttributions, tileSourceKey, z; - for (tileSourceKey in tileUsage) { + for (tileSourceKey in usedTiles) { goog.asserts.assert(tileSourceKey in tileSources); tileSource = tileSources[tileSourceKey]; tileSourceAttributions = tileSource.getAttributions(); if (goog.isNull(tileSourceAttributions)) { continue; } - tileRanges = tileUsage[tileSourceKey]; + tileRanges = usedTiles[tileSourceKey]; for (i = 0; i < tileSourceAttributions.length; ++i) { tileSourceAttribution = tileSourceAttributions[i]; tileSourceAttributionKey = goog.getUid(tileSourceAttribution).toString(); diff --git a/src/ol/framestate.js b/src/ol/framestate.js index 574fefe20f..6f06b5fff6 100644 --- a/src/ol/framestate.js +++ b/src/ol/framestate.js @@ -1,4 +1,5 @@ // FIXME add view3DState +// FIXME factor out common code between usedTiles and wantedTiles goog.provide('ol.FrameState'); goog.provide('ol.PostRenderFunction'); @@ -26,10 +27,11 @@ goog.require('ol.layer.LayerState'); * postRenderFunctions: Array., * size: ol.Size, * tileQueue: ol.TileQueue, - * tileUsage: Object.>, * time: number, + * usedTiles: Object.>, * view2DState: ol.View2DState, - * viewHints: Array.}} + * viewHints: Array., + * wantedTiles: Object.>}} */ ol.FrameState; diff --git a/src/ol/map.js b/src/ol/map.js index 85635f5615..6ab92eb8b8 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -453,19 +453,24 @@ ol.Map.prototype.getOverlayContainer = function() { /** * @param {ol.Tile} tile Tile. + * @param {string} tileSourceKey Tile source key. * @param {ol.Coordinate} tileCenter Tile center. - * @param {number} tileResolution Tile resolution. - * @return {number|undefined} Tile priority. + * @return {number} Tile priority. */ -ol.Map.prototype.getTilePriority = function(tile, tileCenter, tileResolution) { - if (goog.isNull(this.frameState_)) { - return undefined; - } else { - var center = this.frameState_.view2DState.center; - var deltaX = tileCenter.x - center.x; - var deltaY = tileCenter.y - center.y; - return Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; +ol.Map.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter) { + var frameState = this.frameState_; + if (goog.isNull(frameState) || !(tileSourceKey in frameState.wantedTiles)) { + return ol.TileQueue.DROP; } + var zKey = tile.tileCoord.z.toString(); + if (!(zKey in frameState.wantedTiles[tileSourceKey]) || + !frameState.wantedTiles[tileSourceKey][zKey].contains(tile.tileCoord)) { + return ol.TileQueue.DROP; + } + var center = frameState.view2DState.center; + var deltaX = tileCenter.x - center.x; + var deltaY = tileCenter.y - center.y; + return deltaX * deltaX + deltaY * deltaY; }; @@ -627,10 +632,11 @@ ol.Map.prototype.renderFrame_ = function(time) { postRenderFunctions: [], size: size, tileQueue: this.tileQueue_, - tileUsage: {}, + time: time, + usedTiles: {}, view2DState: view2DState, viewHints: viewHints, - time: time + wantedTiles: {} }; } @@ -664,6 +670,8 @@ ol.Map.prototype.renderFrame_ = function(time) { } this.renderer_.renderFrame(frameState); + this.frameState_ = frameState; + this.dirty_ = false; if (!goog.isNull(frameState)) { if (frameState.animate) { @@ -672,8 +680,6 @@ ol.Map.prototype.renderFrame_ = function(time) { Array.prototype.push.apply( this.postRenderFunctions_, frameState.postRenderFunctions); } - this.frameState_ = frameState; - this.dirty_ = false; this.dispatchEvent( new ol.MapEvent(ol.MapEventType.POSTRENDER, this, frameState)); diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 4c17795648..c79251e6f9 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -87,6 +87,7 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var tileSize = tileGrid.getTileSize(); var z = tileGrid.getZForResolution(view2DState.resolution); @@ -165,7 +166,7 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { tilesToDrawByZ[z][tileCoord.toString()] = tile; continue; @@ -213,9 +214,10 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = if (!allTilesLoaded) { frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); var transform = this.transform_; goog.vec.Mat4.makeIdentity(transform); diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index 58a55295f7..9cf215a98e 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -83,6 +83,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); @@ -132,7 +133,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { tilesToDrawByZ[z][tileCoord.toString()] = tile; continue; @@ -234,9 +235,10 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = if (!allTilesLoaded) { frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); }; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index b2003467ab..c460fafb3b 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -130,24 +130,49 @@ ol.renderer.Layer.prototype.handleLayerVisibleChange = goog.nullFunction; /** * @protected - * @param {Object.>} tileUsage Tile usage. + * @param {Object.>} usedTiles Used tiles. * @param {ol.source.Source} source Source. * @param {number} z Z. * @param {ol.TileRange} tileRange Tile range. */ -ol.renderer.Layer.prototype.updateTileUsage = - function(tileUsage, source, z, tileRange) { +ol.renderer.Layer.prototype.updateUsedTiles = + function(usedTiles, source, z, tileRange) { // FIXME should we use tilesToDrawByZ instead? var sourceKey = goog.getUid(source).toString(); var zKey = z.toString(); - if (sourceKey in tileUsage) { - if (z in tileUsage[sourceKey]) { - tileUsage[sourceKey][zKey].extend(tileRange); + if (sourceKey in usedTiles) { + if (zKey in usedTiles[sourceKey]) { + usedTiles[sourceKey][zKey].extend(tileRange); } else { - tileUsage[sourceKey][zKey] = tileRange; + usedTiles[sourceKey][zKey] = tileRange; } } else { - tileUsage[sourceKey] = {}; - tileUsage[sourceKey][zKey] = tileRange; + usedTiles[sourceKey] = {}; + usedTiles[sourceKey][zKey] = tileRange; + } +}; + + +/** + * @protected + * @param {Object.>} wantedTiles Wanted + * tile ranges. + * @param {ol.source.Source} source Source. + * @param {number} z Z. + * @param {ol.TileRange} tileRange Tile range. + */ +ol.renderer.Layer.prototype.updateWantedTiles = + function(wantedTiles, source, z, tileRange) { + var sourceKey = goog.getUid(source).toString(); + var zKey = z.toString(); + if (sourceKey in wantedTiles) { + if (zKey in wantedTiles[sourceKey]) { + wantedTiles[sourceKey][zKey].extend(tileRange); + } else { + wantedTiles[sourceKey][zKey] = tileRange; + } + } else { + wantedTiles[sourceKey] = {}; + wantedTiles[sourceKey][zKey] = tileRange; } }; diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index de1f18e1e2..621883f635 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -270,6 +270,7 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); @@ -392,7 +393,7 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { if (mapRenderer.isTileTextureLoaded(tile)) { tilesToDrawByZ[z][tileCoord.toString()] = tile; @@ -455,11 +456,12 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = this.renderedTileRange_ = null; this.renderedFramebufferExtent_ = null; frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); goog.vec.Mat4.makeIdentity(this.matrix_); goog.vec.Mat4.translate(this.matrix_, diff --git a/src/ol/tilequeue.js b/src/ol/tilequeue.js index d73f7c0901..64984ba1a8 100644 --- a/src/ol/tilequeue.js +++ b/src/ol/tilequeue.js @@ -3,14 +3,24 @@ goog.provide('ol.TileQueue'); goog.require('goog.events'); goog.require('goog.events.EventType'); -goog.require('goog.structs.PriorityQueue'); goog.require('ol.Coordinate'); goog.require('ol.Tile'); goog.require('ol.TileState'); /** - * @typedef {function(ol.Tile, ol.Coordinate, number): (number|undefined)} + * 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 + */ + + +/** + * @typedef {function(ol.Tile, string, ol.Coordinate): number} */ ol.TilePriorityFunction; @@ -43,9 +53,9 @@ ol.TileQueue = function(tilePriorityFunction) { /** * @private - * @type {goog.structs.PriorityQueue} + * @type {Array.>} */ - this.queue_ = new goog.structs.PriorityQueue(); + this.heap_ = []; /** * @private @@ -57,23 +67,49 @@ ol.TileQueue = function(tilePriorityFunction) { /** - * @param {ol.Tile} tile Tile. - * @param {ol.Coordinate} tileCenter Tile center. - * @param {number} tileResolution Tile resolution. + * @const {number} */ -ol.TileQueue.prototype.enqueue = - function(tile, tileCenter, tileResolution) { +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. + */ +ol.TileQueue.prototype.enqueue = function(tile, tileSourceKey, tileCenter) { if (tile.getState() != ol.TileState.IDLE) { return; } var tileKey = tile.getKey(); if (!(tileKey in this.queuedTileKeys_)) { - var priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution); - if (goog.isDef(priority)) { - this.queue_.enqueue(priority, arguments); + var priority = this.tilePriorityFunction_(tile, tileSourceKey, tileCenter); + if (priority != ol.TileQueue.DROP) { + this.heap_.push([priority, tile, tileSourceKey, tileCenter]); this.queuedTileKeys_[tileKey] = true; - } else { - // FIXME fire drop event? + this.siftDown_(0, this.heap_.length - 1); } } }; @@ -87,15 +123,57 @@ 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, tileKey; - while (!this.queue_.isEmpty() && this.tilesLoading_ < this.maxTilesLoading_) { - tile = (/** @type {Array} */ (this.queue_.dequeue()))[0]; - tileKey = tile.getKey(); - delete this.queuedTileKeys_[tileKey]; + var tile; + while (this.heap_.length > 0 && this.tilesLoading_ < this.maxTilesLoading_) { + tile = /** @type {ol.Tile} */ (this.dequeue_()); goog.events.listen(tile, goog.events.EventType.CHANGE, this.handleTileChange, false, this); tile.load(); @@ -104,17 +182,75 @@ ol.TileQueue.prototype.loadMoreTiles = function() { }; +/** + * @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() { - if (!this.queue_.isEmpty()) { - var values = /** @type {Array.} */ (this.queue_.getValues()); - this.queue_.clear(); - this.queuedTileKeys_ = {}; - var i; - for (i = 0; i < values.length; ++i) { - this.enqueue.apply(this, values[i]); + var heap = this.heap_; + var i, n = 0, node, priority, tile, tileCenter, tileKey, 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]); + priority = this.tilePriorityFunction_(tile, tileSourceKey, tileCenter); + 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/ol.html b/test/ol.html index b0e05e2f6d..c490d01fd1 100644 --- a/test/ol.html +++ b/test/ol.html @@ -84,6 +84,7 @@ + diff --git a/test/spec/ol/tilequeue.test.js b/test/spec/ol/tilequeue.test.js new file mode 100644 index 0000000000..37b8d609e0 --- /dev/null +++ b/test/spec/ol/tilequeue.test.js @@ -0,0 +1,68 @@ +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; + } + } + + describe('heapify', function() { + it('does convert an arbitrary array into a heap', function() { + + var tq = new ol.TileQueue(function() {}); + addRandomPriorityTiles(tq, 100); + + tq.heapify_(); + expect(isHeap(tq)).toBeTruthy(); + }); + }); + + describe('reprioritize', function() { + it('does reprioritize the array', function() { + + var tq = new ol.TileQueue(function() {}); + addRandomPriorityTiles(tq, 100); + + tq.heapify_(); + + // now reprioritize, changing the priority of 50 tiles and removing the + // rest + + var i = 0; + tq.tilePriorityFunction_ = function() { + if ((i++) % 2 === 0) { + return ol.TileQueue.DROP; + } + return Math.floor(Math.random() * 100); + }; + + tq.reprioritize(); + expect(tq.heap_.length).toEqual(50); + expect(isHeap(tq)).toBeTruthy(); + + }); + }); +}); +