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