blob: 9b91ab6a0acd4de7a3383b1c5db296963d836d21 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/base/base.html">
<script>
'use strict';
tr.exportTo('tr.b', function() {
function max(a, b) {
if (a === undefined)
return b;
if (b === undefined)
return a;
return Math.max(a, b);
}
/**
* This class implements an interval tree.
* See: http://wikipedia.org/wiki/Interval_tree
*
* Internally the tree is a Red-Black tree. The insertion/colour is done using
* the Left-leaning Red-Black Trees algorithm as described in:
* http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
*
* @param {function} beginPositionCb Callback to retrieve the begin position.
* @param {function} endPositionCb Callback to retrieve the end position.
*
* @constructor
*/
function IntervalTree(beginPositionCb, endPositionCb) {
this.beginPositionCb_ = beginPositionCb;
this.endPositionCb_ = endPositionCb;
this.root_ = undefined;
this.size_ = 0;
}
IntervalTree.prototype = {
/**
* Insert events into the interval tree.
*
* @param {Object} datum The object to insert.
*/
insert: function(datum) {
var startPosition = this.beginPositionCb_(datum);
var endPosition = this.endPositionCb_(datum);
var node = new IntervalTreeNode(datum,
startPosition, endPosition);
this.size_++;
this.root_ = this.insertNode_(this.root_, node);
this.root_.colour = Colour.BLACK;
return datum;
},
insertNode_: function(root, node) {
if (root === undefined)
return node;
if (root.leftNode && root.leftNode.isRed &&
root.rightNode && root.rightNode.isRed)
this.flipNodeColour_(root);
if (node.key < root.key)
root.leftNode = this.insertNode_(root.leftNode, node);
else if (node.key === root.key)
root.merge(node);
else
root.rightNode = this.insertNode_(root.rightNode, node);
if (root.rightNode && root.rightNode.isRed &&
(root.leftNode === undefined || !root.leftNode.isRed))
root = this.rotateLeft_(root);
if (root.leftNode && root.leftNode.isRed &&
root.leftNode.leftNode && root.leftNode.leftNode.isRed)
root = this.rotateRight_(root);
return root;
},
rotateRight_: function(node) {
var sibling = node.leftNode;
node.leftNode = sibling.rightNode;
sibling.rightNode = node;
sibling.colour = node.colour;
node.colour = Colour.RED;
return sibling;
},
rotateLeft_: function(node) {
var sibling = node.rightNode;
node.rightNode = sibling.leftNode;
sibling.leftNode = node;
sibling.colour = node.colour;
node.colour = Colour.RED;
return sibling;
},
flipNodeColour_: function(node) {
node.colour = this.flipColour_(node.colour);
node.leftNode.colour = this.flipColour_(node.leftNode.colour);
node.rightNode.colour = this.flipColour_(node.rightNode.colour);
},
flipColour_: function(colour) {
return colour === Colour.RED ? Colour.BLACK : Colour.RED;
},
/* The high values are used to find intersection. It should be called after
* all of the nodes are inserted. Doing it each insert is _slow_. */
updateHighValues: function() {
this.updateHighValues_(this.root_);
},
/* There is probably a smarter way to do this by starting from the inserted
* node, but need to handle the rotations correctly. Went the easy route
* for now. */
updateHighValues_: function(node) {
if (node === undefined)
return undefined;
node.maxHighLeft = this.updateHighValues_(node.leftNode);
node.maxHighRight = this.updateHighValues_(node.rightNode);
return max(max(node.maxHighLeft, node.highValue), node.maxHighRight);
},
validateFindArguments_: function(queryLow, queryHigh) {
if (queryLow === undefined || queryHigh === undefined)
throw new Error('queryLow and queryHigh must be defined');
if ((typeof queryLow !== 'number') || (typeof queryHigh !== 'number'))
throw new Error('queryLow and queryHigh must be numbers');
},
/**
* Retrieve all overlapping intervals.
*
* @param {number} queryLow The low value for the intersection interval.
* @param {number} queryHigh The high value for the intersection interval.
* @return {Array} All [begin, end] pairs inside intersecting intervals.
*/
findIntersection: function(queryLow, queryHigh) {
this.validateFindArguments_(queryLow, queryHigh);
if (this.root_ === undefined)
return [];
var ret = [];
this.root_.appendIntersectionsInto_(ret, queryLow, queryHigh);
return ret;
},
/**
* Returns the number of nodes in the tree.
*/
get size() {
return this.size_;
},
/**
* Returns the root node in the tree.
*/
get root() {
return this.root_;
},
/**
* Dumps out the [lowValue, highValue] pairs for each node in depth-first
* order.
*/
dump_: function() {
if (this.root_ === undefined)
return [];
return this.root_.dump();
}
};
var Colour = {
RED: 'red',
BLACK: 'black'
};
function IntervalTreeNode(datum, lowValue, highValue) {
this.lowValue_ = lowValue;
this.data_ = [{
datum: datum,
high: highValue,
low: lowValue
}];
this.colour_ = Colour.RED;
this.parentNode_ = undefined;
this.leftNode_ = undefined;
this.rightNode_ = undefined;
this.maxHighLeft_ = undefined;
this.maxHighRight_ = undefined;
}
IntervalTreeNode.prototype = {
appendIntersectionsInto_: function(ret, queryLow, queryHigh) {
/* This node starts has a start point at or further right then queryHigh
* so we know this node is out and all right children are out. Just need
* to check left */
if (this.lowValue_ >= queryHigh) {
if (!this.leftNode_)
return;
return this.leftNode_.appendIntersectionsInto_(
ret, queryLow, queryHigh);
}
/* If we have a maximum left high value that is bigger then queryLow we
* need to check left for matches */
if (this.maxHighLeft_ > queryLow) {
this.leftNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
}
/* We know that this node starts before queryHigh, if any of it's data
* ends after queryLow we need to add those nodes */
if (this.highValue > queryLow) {
for (var i = (this.data.length - 1); i >= 0; --i) {
/* data nodes are sorted by high value, so as soon as we see one
* before low value we're done. */
if (this.data[i].high < queryLow)
break;
ret.push(this.data[i].datum);
}
}
/* check for matches in the right tree */
if (this.rightNode_) {
this.rightNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
}
},
get colour() {
return this.colour_;
},
set colour(colour) {
this.colour_ = colour;
},
get key() {
return this.lowValue_;
},
get lowValue() {
return this.lowValue_;
},
get highValue() {
return this.data_[this.data_.length - 1].high;
},
set leftNode(left) {
this.leftNode_ = left;
},
get leftNode() {
return this.leftNode_;
},
get hasLeftNode() {
return this.leftNode_ !== undefined;
},
set rightNode(right) {
this.rightNode_ = right;
},
get rightNode() {
return this.rightNode_;
},
get hasRightNode() {
return this.rightNode_ !== undefined;
},
set parentNode(parent) {
this.parentNode_ = parent;
},
get parentNode() {
return this.parentNode_;
},
get isRootNode() {
return this.parentNode_ === undefined;
},
set maxHighLeft(high) {
this.maxHighLeft_ = high;
},
get maxHighLeft() {
return this.maxHighLeft_;
},
set maxHighRight(high) {
this.maxHighRight_ = high;
},
get maxHighRight() {
return this.maxHighRight_;
},
get data() {
return this.data_;
},
get isRed() {
return this.colour_ === Colour.RED;
},
merge: function(node) {
for (var i = 0; i < node.data.length; i++)
this.data_.push(node.data[i]);
this.data_.sort(function(a, b) {
return a.high - b.high;
});
},
dump: function() {
var ret = {};
if (this.leftNode_)
ret['left'] = this.leftNode_.dump();
ret['data'] = this.data_.map(function(d) { return [d.low, d.high]; });
if (this.rightNode_)
ret['right'] = this.rightNode_.dump();
return ret;
}
};
return {
IntervalTree: IntervalTree
};
});
</script>