3 Copyright (c) 2014 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
7 <link rel="import" href="/tvcm.html">
11 tvcm.exportTo('tvcm', function() {
17 return Math.max(a, b);
21 * This class implements an interval tree.
22 * See: http://wikipedia.org/wiki/Interval_tree
24 * Internally the tree is a Red-Black tree. The insertion/colour is done using
25 * the Left-leaning Red-Black Trees algorithm as described in:
26 * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
28 * @param {function} beginPositionCb Callback to retrieve the begin position.
29 * @param {function} endPositionCb Callback to retrieve the end position.
33 function IntervalTree(beginPositionCb, endPositionCb) {
34 this.beginPositionCb_ = beginPositionCb;
35 this.endPositionCb_ = endPositionCb;
37 this.root_ = undefined;
41 IntervalTree.prototype = {
43 * Insert events into the interval tree.
45 * @param {Object} begin The left object.
46 * @param {Object=} opt_end The end object, optional. If not provided the
47 * begin object is assumed to also be the end object.
49 insert: function(begin, opt_end) {
50 var startPosition = this.beginPositionCb_(begin);
51 var endPosition = this.endPositionCb_(opt_end || begin);
53 var node = new IntervalTreeNode(begin, opt_end || begin,
54 startPosition, endPosition);
57 this.root_ = this.insertNode_(this.root_, node);
58 this.root_.colour = Colour.BLACK;
61 insertNode_: function(root, node) {
62 if (root === undefined)
65 if (root.leftNode && root.leftNode.isRed &&
66 root.rightNode && root.rightNode.isRed)
67 this.flipNodeColour_(root);
69 if (node.key < root.key)
70 root.leftNode = this.insertNode_(root.leftNode, node);
71 else if (node.key === root.key)
74 root.rightNode = this.insertNode_(root.rightNode, node);
76 if (root.rightNode && root.rightNode.isRed &&
77 (root.leftNode === undefined || !root.leftNode.isRed))
78 root = this.rotateLeft_(root);
80 if (root.leftNode && root.leftNode.isRed &&
81 root.leftNode.leftNode && root.leftNode.leftNode.isRed)
82 root = this.rotateRight_(root);
87 rotateRight_: function(node) {
88 var sibling = node.leftNode;
89 node.leftNode = sibling.rightNode;
90 sibling.rightNode = node;
91 sibling.colour = node.colour;
92 node.colour = Colour.RED;
96 rotateLeft_: function(node) {
97 var sibling = node.rightNode;
98 node.rightNode = sibling.leftNode;
99 sibling.leftNode = node;
100 sibling.colour = node.colour;
101 node.colour = Colour.RED;
105 flipNodeColour_: function(node) {
106 node.colour = this.flipColour_(node.colour);
107 node.leftNode.colour = this.flipColour_(node.leftNode.colour);
108 node.rightNode.colour = this.flipColour_(node.rightNode.colour);
111 flipColour_: function(colour) {
112 return colour === Colour.RED ? Colour.BLACK : Colour.RED;
115 /* The high values are used to find intersection. It should be called after
116 * all of the nodes are inserted. Doing it each insert is _slow_. */
117 updateHighValues: function() {
118 this.updateHighValues_(this.root_);
121 /* There is probably a smarter way to do this by starting from the inserted
122 * node, but need to handle the rotations correctly. Went the easy route
124 updateHighValues_: function(node) {
125 if (node === undefined)
128 node.maxHighLeft = this.updateHighValues_(node.leftNode);
129 node.maxHighRight = this.updateHighValues_(node.rightNode);
131 return max(max(node.maxHighLeft, node.highValue), node.maxHighRight);
135 * Retrieve all overlapping intervals.
137 * @param {number} lowValue The low value for the intersection interval.
138 * @param {number} highValue The high value for the intersection interval.
139 * @return {Array} All [begin, end] pairs inside intersecting intervals.
141 findIntersection: function(lowValue, highValue) {
142 if (lowValue === undefined || highValue === undefined)
143 throw new Error('lowValue and highValue must be defined');
144 if ((typeof lowValue !== 'number') || (typeof highValue !== 'number'))
145 throw new Error('lowValue and highValue must be numbers');
147 if (this.root_ === undefined)
150 return this.findIntersection_(this.root_, lowValue, highValue);
153 findIntersection_: function(node, lowValue, highValue) {
156 /* This node starts has a start point at or further right then highValue
157 * so we know this node is out and all right children are out. Just need
159 if (node.lowValue >= highValue) {
160 if (!node.hasLeftNode)
162 return this.findIntersection_(node.leftNode, lowValue, highValue);
165 /* If we have a maximum left high value that is bigger then lowValue we
166 * need to check left for matches */
167 if (node.maxHighLeft > lowValue) {
169 this.findIntersection_(node.leftNode, lowValue, highValue));
172 /* We know that this node starts before highValue, if any of it's data
173 * ends after lowValue we need to add those nodes */
174 if (node.highValue > lowValue) {
175 for (var i = (node.data.length - 1); i >= 0; --i) {
176 /* data nodes are sorted by high value, so as soon as we see one
177 * before low value we're done. */
178 if (node.data[i].high < lowValue)
181 ret.unshift([node.data[i].start, node.data[i].end]);
185 /* check for matches in the right tree */
186 if (node.hasRightNode) {
188 this.findIntersection_(node.rightNode, lowValue, highValue));
195 * Returns the number of nodes in the tree.
202 * Returns the root node in the tree.
209 * Dumps out the [lowValue, highValue] pairs for each node in depth-first
213 if (this.root_ === undefined)
215 return this.dumpNode_(this.root_);
218 dumpNode_: function(node) {
220 if (node.hasLeftNode)
221 ret['left'] = this.dumpNode_(node.leftNode);
223 ret['node'] = node.dump();
225 if (node.hasRightNode)
226 ret['right'] = this.dumpNode_(node.rightNode);
237 function IntervalTreeNode(startObject, endObject, lowValue, highValue) {
238 this.lowValue_ = lowValue;
247 this.colour_ = Colour.RED;
249 this.parentNode_ = undefined;
250 this.leftNode_ = undefined;
251 this.rightNode_ = undefined;
253 this.maxHighLeft_ = undefined;
254 this.maxHighRight_ = undefined;
257 IntervalTreeNode.prototype = {
263 this.colour_ = colour;
267 return this.lowValue_;
271 return this.lowValue_;
275 return this.data_[this.data_.length - 1].high;
279 this.leftNode_ = left;
283 return this.leftNode_;
287 return this.leftNode_ !== undefined;
290 set rightNode(right) {
291 this.rightNode_ = right;
295 return this.rightNode_;
299 return this.rightNode_ !== undefined;
302 set parentNode(parent) {
303 this.parentNode_ = parent;
307 return this.parentNode_;
311 return this.parentNode_ === undefined;
314 set maxHighLeft(high) {
315 this.maxHighLeft_ = high;
319 return this.maxHighLeft_;
322 set maxHighRight(high) {
323 this.maxHighRight_ = high;
327 return this.maxHighRight_;
335 return this.colour_ === Colour.RED;
338 merge: function(node) {
339 this.data_ = this.data_.concat(node.data);
340 this.data_.sort(function(a, b) {
341 return a.high - b.high;
346 if (this.data_.length === 1)
347 return [this.data_[0].low, this.data[0].high];
349 return this.data_.map(function(d) { return [d.low, d.high]; });
354 IntervalTree: IntervalTree