1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 tvcm.exportTo('tvcm', function() {
13 return Math.max(a, b);
17 * This class implements an interval tree.
18 * See: http://wikipedia.org/wiki/Interval_tree
20 * Internally the tree is a Red-Black tree. The insertion/colour is done using
21 * the Left-leaning Red-Black Trees algorithm as described in:
22 * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
24 * @param {function} beginPositionCb Callback to retrieve the begin position.
25 * @param {function} endPositionCb Callback to retrieve the end position.
29 function IntervalTree(beginPositionCb, endPositionCb) {
30 this.beginPositionCb_ = beginPositionCb;
31 this.endPositionCb_ = endPositionCb;
33 this.root_ = undefined;
37 IntervalTree.prototype = {
39 * Insert events into the interval tree.
41 * @param {Object} begin The left object.
42 * @param {Object=} opt_end The end object, optional. If not provided the
43 * begin object is assumed to also be the end object.
45 insert: function(begin, opt_end) {
46 var startPosition = this.beginPositionCb_(begin);
47 var endPosition = this.endPositionCb_(opt_end || begin);
49 var node = new IntervalTreeNode(begin, opt_end || begin,
50 startPosition, endPosition);
53 this.root_ = this.insertNode_(this.root_, node);
54 this.root_.colour = Colour.BLACK;
57 insertNode_: function(root, node) {
58 if (root === undefined)
61 if (root.leftNode && root.leftNode.isRed &&
62 root.rightNode && root.rightNode.isRed)
63 this.flipNodeColour_(root);
65 if (node.key < root.key)
66 root.leftNode = this.insertNode_(root.leftNode, node);
67 else if (node.key === root.key)
70 root.rightNode = this.insertNode_(root.rightNode, node);
72 if (root.rightNode && root.rightNode.isRed &&
73 (root.leftNode === undefined || !root.leftNode.isRed))
74 root = this.rotateLeft_(root);
76 if (root.leftNode && root.leftNode.isRed &&
77 root.leftNode.leftNode && root.leftNode.leftNode.isRed)
78 root = this.rotateRight_(root);
83 rotateRight_: function(node) {
84 var sibling = node.leftNode;
85 node.leftNode = sibling.rightNode;
86 sibling.rightNode = node;
87 sibling.colour = node.colour;
88 node.colour = Colour.RED;
92 rotateLeft_: function(node) {
93 var sibling = node.rightNode;
94 node.rightNode = sibling.leftNode;
95 sibling.leftNode = node;
96 sibling.colour = node.colour;
97 node.colour = Colour.RED;
101 flipNodeColour_: function(node) {
102 node.colour = this.flipColour_(node.colour);
103 node.leftNode.colour = this.flipColour_(node.leftNode.colour);
104 node.rightNode.colour = this.flipColour_(node.rightNode.colour);
107 flipColour_: function(colour) {
108 return colour === Colour.RED ? Colour.BLACK : Colour.RED;
111 /* The high values are used to find intersection. It should be called after
112 * all of the nodes are inserted. Doing it each insert is _slow_. */
113 updateHighValues: function() {
114 this.updateHighValues_(this.root_);
117 /* There is probably a smarter way to do this by starting from the inserted
118 * node, but need to handle the rotations correctly. Went the easy route
120 updateHighValues_: function(node) {
121 if (node === undefined)
124 node.maxHighLeft = this.updateHighValues_(node.leftNode);
125 node.maxHighRight = this.updateHighValues_(node.rightNode);
127 return max(max(node.maxHighLeft, node.highValue), node.maxHighRight);
131 * Retrieve all overlapping intervals.
133 * @param {number} lowValue The low value for the intersection interval.
134 * @param {number} highValue The high value for the intersection interval.
135 * @return {Array} All [begin, end] pairs inside intersecting intervals.
137 findIntersection: function(lowValue, highValue) {
138 if (lowValue === undefined || highValue === undefined)
139 throw new Error('lowValue and highValue must be defined');
140 if ((typeof lowValue !== 'number') || (typeof highValue !== 'number'))
141 throw new Error('lowValue and highValue must be numbers');
143 if (this.root_ === undefined)
146 return this.findIntersection_(this.root_, lowValue, highValue);
149 findIntersection_: function(node, lowValue, highValue) {
152 /* This node starts has a start point at or further right then highValue
153 * so we know this node is out and all right children are out. Just need
155 if (node.lowValue >= highValue) {
156 if (!node.hasLeftNode)
158 return this.findIntersection_(node.leftNode, lowValue, highValue);
161 /* If we have a maximum left high value that is bigger then lowValue we
162 * need to check left for matches */
163 if (node.maxHighLeft > lowValue) {
165 this.findIntersection_(node.leftNode, lowValue, highValue));
168 /* We know that this node starts before highValue, if any of it's data
169 * ends after lowValue we need to add those nodes */
170 if (node.highValue > lowValue) {
171 for (var i = (node.data.length - 1); i >= 0; --i) {
172 /* data nodes are sorted by high value, so as soon as we see one
173 * before low value we're done. */
174 if (node.data[i].high < lowValue)
177 ret.unshift([node.data[i].start, node.data[i].end]);
181 /* check for matches in the right tree */
182 if (node.hasRightNode) {
184 this.findIntersection_(node.rightNode, lowValue, highValue));
191 * Returns the number of nodes in the tree.
198 * Returns the root node in the tree.
205 * Dumps out the [lowValue, highValue] pairs for each node in depth-first
209 if (this.root_ === undefined)
211 return this.dumpNode_(this.root_);
214 dumpNode_: function(node) {
216 if (node.hasLeftNode)
217 ret['left'] = this.dumpNode_(node.leftNode);
219 ret['node'] = node.dump();
221 if (node.hasRightNode)
222 ret['right'] = this.dumpNode_(node.rightNode);
233 function IntervalTreeNode(startObject, endObject, lowValue, highValue) {
234 this.lowValue_ = lowValue;
243 this.colour_ = Colour.RED;
245 this.parentNode_ = undefined;
246 this.leftNode_ = undefined;
247 this.rightNode_ = undefined;
249 this.maxHighLeft_ = undefined;
250 this.maxHighRight_ = undefined;
253 IntervalTreeNode.prototype = {
259 this.colour_ = colour;
263 return this.lowValue_;
267 return this.lowValue_;
271 return this.data_[this.data_.length - 1].high;
275 this.leftNode_ = left;
279 return this.leftNode_;
283 return this.leftNode_ !== undefined;
286 set rightNode(right) {
287 this.rightNode_ = right;
291 return this.rightNode_;
295 return this.rightNode_ !== undefined;
298 set parentNode(parent) {
299 this.parentNode_ = parent;
303 return this.parentNode_;
307 return this.parentNode_ === undefined;
310 set maxHighLeft(high) {
311 this.maxHighLeft_ = high;
315 return this.maxHighLeft_;
318 set maxHighRight(high) {
319 this.maxHighRight_ = high;
323 return this.maxHighRight_;
331 return this.colour_ === Colour.RED;
334 merge: function(node) {
335 this.data_ = this.data_.concat(node.data);
336 this.data_.sort(function(a, b) {
337 return a.high - b.high;
342 if (this.data_.length === 1)
343 return [this.data_[0].low, this.data[0].high];
345 return this.data_.map(function(d) { return [d.low, d.high]; });
350 IntervalTree: IntervalTree