diff --git a/src/ol/tilequeue.js b/src/ol/tilequeue.js index d73f7c0901..9fea413f33 100644 --- a/src/ol/tilequeue.js +++ b/src/ol/tilequeue.js @@ -1,14 +1,25 @@ goog.provide('ol.TilePriorityFunction'); goog.provide('ol.TileQueue'); +goog.require('goog.array'); 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'); +/** + * 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, ol.Coordinate, number): (number|undefined)} */ @@ -43,9 +54,9 @@ ol.TileQueue = function(tilePriorityFunction) { /** * @private - * @type {goog.structs.PriorityQueue} + * @type {Array.>} */ - this.queue_ = new goog.structs.PriorityQueue(); + this.heap_ = []; /** * @private @@ -57,12 +68,44 @@ ol.TileQueue = function(tilePriorityFunction) { /** + * FIXME empty description for jsdoc + */ +ol.TileQueue.prototype.clear = function() { + goog.array.clear(this.heap_); +}; + + +/** + * Remove and return the highest-priority tile. O(logn). + * @private + * @return {ol.Tile|undefined} Tile. + */ +ol.TileQueue.prototype.dequeue_ = function() { + var heap = this.heap_; + var count = heap.length; + if (count <= 0) { + return undefined; + } + var tile = /** @type {ol.Tile} */ (heap[0][1]); + if (count == 1) { + goog.array.clear(heap); + } 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 {ol.Coordinate} tileCenter Tile center. * @param {number} tileResolution Tile resolution. */ -ol.TileQueue.prototype.enqueue = - function(tile, tileCenter, tileResolution) { +ol.TileQueue.prototype.enqueue = function(tile, tileCenter, tileResolution) { if (tile.getState() != ol.TileState.IDLE) { return; } @@ -70,8 +113,9 @@ ol.TileQueue.prototype.enqueue = if (!(tileKey in this.queuedTileKeys_)) { var priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution); if (goog.isDef(priority)) { - this.queue_.enqueue(priority, arguments); + this.heap_.push([priority, tile, tileCenter, tileResolution]); this.queuedTileKeys_[tileKey] = true; + this.siftDown_(0, this.heap_.length - 1); } else { // FIXME fire drop event? } @@ -87,15 +131,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 +190,73 @@ 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 count = heap.length; + var i, priority, node, tile, tileCenter, tileResolution; + for (i = count - 1; i >= 0; i--) { + node = heap[i]; + tile = /** @type {ol.Tile} */ (node[1]); + tileCenter = /** @type {ol.Coordinate} */ (node[2]); + tileResolution = /** @type {number} */ (node[3]); + priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution); + if (goog.isDef(priority)) { + node[0] = priority; + } else { + goog.array.removeAt(heap, i); } } + this.heapify_(); }; diff --git a/test/jasmine-extensions.js b/test/jasmine-extensions.js index 4649130908..46e24cb7ae 100644 --- a/test/jasmine-extensions.js +++ b/test/jasmine-extensions.js @@ -4,6 +4,12 @@ beforeEach(function() { toBeA: function(type) { return this.actual instanceof type; }, + toBeGreaterThanOrEqualTo: function(other) { + return this.actual >= other; + }, + toBeLessThanOrEqualTo: function(other) { + return this.actual <= other; + }, toRoughlyEqual: function(other, tol) { return Math.abs(this.actual - other) <= tol; } 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..3c5bc8a055 --- /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), 1]); + 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 undefined; + } + return Math.floor(Math.random() * 100); + }; + + tq.reprioritize(); + expect(tq.heap_.length).toEqual(50); + expect(isHeap(tq)).toBeTruthy(); + + }); + }); +}); +