New tile queue implementation (heapify-powered)

This commit is contained in:
Éric Lemoine
2013-01-17 16:15:03 +01:00
parent 11938d2264
commit c0c9cdef15
4 changed files with 235 additions and 18 deletions

View File

@@ -1,14 +1,25 @@
goog.provide('ol.TilePriorityFunction'); goog.provide('ol.TilePriorityFunction');
goog.provide('ol.TileQueue'); goog.provide('ol.TileQueue');
goog.require('goog.array');
goog.require('goog.events'); goog.require('goog.events');
goog.require('goog.events.EventType'); goog.require('goog.events.EventType');
goog.require('goog.structs.PriorityQueue');
goog.require('ol.Coordinate'); goog.require('ol.Coordinate');
goog.require('ol.Tile'); goog.require('ol.Tile');
goog.require('ol.TileState'); 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)} * @typedef {function(ol.Tile, ol.Coordinate, number): (number|undefined)}
*/ */
@@ -43,9 +54,9 @@ ol.TileQueue = function(tilePriorityFunction) {
/** /**
* @private * @private
* @type {goog.structs.PriorityQueue} * @type {Array.<Array.<*>>}
*/ */
this.queue_ = new goog.structs.PriorityQueue(); this.heap_ = [];
/** /**
* @private * @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.Tile} tile Tile.
* @param {ol.Coordinate} tileCenter Tile center. * @param {ol.Coordinate} tileCenter Tile center.
* @param {number} tileResolution Tile resolution. * @param {number} tileResolution Tile resolution.
*/ */
ol.TileQueue.prototype.enqueue = ol.TileQueue.prototype.enqueue = function(tile, tileCenter, tileResolution) {
function(tile, tileCenter, tileResolution) {
if (tile.getState() != ol.TileState.IDLE) { if (tile.getState() != ol.TileState.IDLE) {
return; return;
} }
@@ -70,8 +113,9 @@ ol.TileQueue.prototype.enqueue =
if (!(tileKey in this.queuedTileKeys_)) { if (!(tileKey in this.queuedTileKeys_)) {
var priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution); var priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution);
if (goog.isDef(priority)) { if (goog.isDef(priority)) {
this.queue_.enqueue(priority, arguments); this.heap_.push([priority, tile, tileCenter, tileResolution]);
this.queuedTileKeys_[tileKey] = true; this.queuedTileKeys_[tileKey] = true;
this.siftDown_(0, this.heap_.length - 1);
} else { } else {
// FIXME fire drop event? // 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 * FIXME empty description for jsdoc
*/ */
ol.TileQueue.prototype.loadMoreTiles = function() { ol.TileQueue.prototype.loadMoreTiles = function() {
var tile, tileKey; var tile;
while (!this.queue_.isEmpty() && this.tilesLoading_ < this.maxTilesLoading_) { while (this.heap_.length > 0 && this.tilesLoading_ < this.maxTilesLoading_) {
tile = (/** @type {Array} */ (this.queue_.dequeue()))[0]; tile = /** @type {ol.Tile} */ (this.dequeue_());
tileKey = tile.getKey();
delete this.queuedTileKeys_[tileKey];
goog.events.listen(tile, goog.events.EventType.CHANGE, goog.events.listen(tile, goog.events.EventType.CHANGE,
this.handleTileChange, false, this); this.handleTileChange, false, this);
tile.load(); 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 * FIXME empty description for jsdoc
*/ */
ol.TileQueue.prototype.reprioritize = function() { ol.TileQueue.prototype.reprioritize = function() {
if (!this.queue_.isEmpty()) { var heap = this.heap_;
var values = /** @type {Array.<Array>} */ (this.queue_.getValues()); var count = heap.length;
this.queue_.clear(); var i, priority, node, tile, tileCenter, tileResolution;
this.queuedTileKeys_ = {}; for (i = count - 1; i >= 0; i--) {
var i; node = heap[i];
for (i = 0; i < values.length; ++i) { tile = /** @type {ol.Tile} */ (node[1]);
this.enqueue.apply(this, values[i]); 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_();
}; };

View File

@@ -4,6 +4,12 @@ beforeEach(function() {
toBeA: function(type) { toBeA: function(type) {
return this.actual instanceof type; return this.actual instanceof type;
}, },
toBeGreaterThanOrEqualTo: function(other) {
return this.actual >= other;
},
toBeLessThanOrEqualTo: function(other) {
return this.actual <= other;
},
toRoughlyEqual: function(other, tol) { toRoughlyEqual: function(other, tol) {
return Math.abs(this.actual - other) <= tol; return Math.abs(this.actual - other) <= tol;
} }

View File

@@ -84,6 +84,7 @@
<script type="text/javascript" src="spec/ol/source/xyz.test.js"></script> <script type="text/javascript" src="spec/ol/source/xyz.test.js"></script>
<script type="text/javascript" src="spec/ol/tilecoord.test.js"></script> <script type="text/javascript" src="spec/ol/tilecoord.test.js"></script>
<script type="text/javascript" src="spec/ol/tilegrid.test.js"></script> <script type="text/javascript" src="spec/ol/tilegrid.test.js"></script>
<script type="text/javascript" src="spec/ol/tilequeue.test.js"></script>
<script type="text/javascript" src="spec/ol/tilerange.test.js"></script> <script type="text/javascript" src="spec/ol/tilerange.test.js"></script>
<script type="text/javascript" src="spec/ol/tileurlfunction.test.js"></script> <script type="text/javascript" src="spec/ol/tileurlfunction.test.js"></script>
<script type="text/javascript" src="spec/ol/control/control.test.js"></script> <script type="text/javascript" src="spec/ol/control/control.test.js"></script>

View File

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