UNPKG

interval-tree

Version:

interval tree in javascript

289 lines (238 loc) 6.1 kB
var SortedList = require('sortedlist'); /** * IntervalTree * * @param (object) data: * @param (number) center: * @param (object) options: * center: * **/ function IntervalTree(center, options) { options || (options = {}); this.startKey = options.startKey || 0; // start key this.endKey = options.endKey || 1; // end key this.intervalHash = {}; // id => interval object this.pointTree = new SortedList({ // b-tree of start, end points compare: function(a, b) { if (a == null) return -1; if (b == null) return 1; var c = a[0]- b[0]; return (c > 0) ? 1 : (c == 0) ? 0 : -1; } }); this._autoIncrement = 0; // index of the root node if (!center || typeof center != 'number') { throw new Error('you must specify center index as the 2nd argument.'); } this.root = new Node(center, this); } /** * publid methods **/ /** * add new range **/ IntervalTree.prototype.add = function(data, id) { if (this.intervalHash[id]) { throw new Error('id ' + id + ' is already registered.'); } if (id == undefined) { while (this.intervalHash[this._autoIncrement]) { this._autoIncrement++; } id = this._autoIncrement; } var itvl = new Interval(data, id, this.startKey, this.endKey); this.pointTree.insert([itvl.start, id]); this.pointTree.insert([itvl.end, id]); this.intervalHash[id] = itvl; this._autoIncrement++; _insert.call(this, this.root, itvl); }; /** * search * * @param (integer) val: * @return (array) **/ IntervalTree.prototype.search = function(val1, val2) { var ret = []; if (typeof val1 != 'number') { throw new Error(val1 + ': invalid input'); } if (val2 == undefined) { _pointSearch.call(this, this.root, val1, ret); } else if (typeof val2 == 'number') { _rangeSearch.call(this, val1, val2, ret); } else { throw new Error(val1 + ',' + val2 + ': invalid input'); } return ret; }; /** * remove: **/ IntervalTree.prototype.remove = function(interval_id) { }; /** * private methods **/ /** * _insert **/ function _insert(node, itvl) { if (itvl.end < node.idx) { if (!node.left) { node.left = new Node((itvl.start + itvl.end / 2), this); } return _insert.call(this, node.left, itvl); } if (node.idx < itvl.start) { if (!node.right) { node.right = new Node((itvl.start + itvl.end) / 2, this); } return _insert.call(this, node.right, itvl); } return node.insert(itvl); } /** * _pointSearch * @param (Node) node * @param (integer) idx * @param (Array) arr **/ function _pointSearch(node, idx, arr) { if (!node) return; if (idx < node.idx) { node.starts.every(function(itvl) { var bool = (itvl.start <= idx); if (bool) arr.push(itvl.result()); return bool; }); return _pointSearch.call(this, node.left, idx, arr); } else if (idx > node.idx) { node.ends.every(function(itvl) { var bool = (itvl.end >= idx); if (bool) arr.push(itvl.result()); return bool; }); return _pointSearch.call(this, node.right, idx, arr); } // exact equal else { node.starts.map(function(itvl) { arr.push(itvl.result()) }); } } /** * _rangeSearch * @param (integer) start * @param (integer) end * @param (Array) arr **/ function _rangeSearch(start, end, arr) { if (end - start <= 0) { throw new Error('end must be greater than start. start: ' + start + ', end: ' + end); } var resultHash = {}; var wholeWraps = []; _pointSearch.call(this, this.root, (start + end) >> 1, wholeWraps, true); wholeWraps.forEach(function(result) { resultHash[result.id] = true; }); var idx1 = this.pointTree.bsearch([start, null]); var pointTreeArray = this.pointTree; while (idx1 >= 0 && pointTreeArray[idx1][0] == start) { idx1--; } var idx2 = this.pointTree.bsearch([end, null]); if (idx2 >= 0) { var len = pointTreeArray.length -1; while (idx2 <= len && pointTreeArray[idx2][0] <= end) { idx2++; } pointTreeArray.slice(idx1 + 1, idx2).forEach(function(point) { var id = point[1]; resultHash[id] = true; }, this); Object.keys(resultHash).forEach(function(id) { var itvl = this.intervalHash[id]; arr.push(itvl.result(start, end)); }, this); } } /** * subclasses * **/ /** * Node : prototype of each node in a interval tree * **/ function Node(idx) { this.idx = idx; this.starts = new SortedList({ compare: function(a, b) { if (a == null) return -1; if (b == null) return 1; var c = a.start - b.start; return (c > 0) ? 1 : (c == 0) ? 0 : -1; } }); this.ends = new SortedList({ compare: function(a, b) { if (a == null) return -1; if (b == null) return 1; var c = a.end - b.end; return (c < 0) ? 1 : (c == 0) ? 0 : -1; } }); }; /** * insert an Interval object to this node **/ Node.prototype.insert = function(interval) { this.starts.insert(interval); this.ends.insert(interval); }; /** * Interval : prototype of interval info **/ function Interval(data, id, s, e) { this.id = id; this.start = data[s]; this.end = data[e]; this.data = data; if (typeof this.start != 'number' || typeof this.end != 'number') { throw new Error('start, end must be number. start: ' + this.start + ', end: ' + this.end); } if ( this.start >= this.end) { throw new Error('start must be smaller than end. start: ' + this.start + ', end: ' + this.end); } } /** * get result object **/ Interval.prototype.result = function(start, end) { var ret = { id : this.id, data : this.data }; if (typeof start == 'number' && typeof end == 'number') { /** * calc overlapping rate **/ var left = Math.max(this.start, start); var right = Math.min(this.end, end); var lapLn = right - left; ret.rate1 = lapLn / (end - start); ret.rate2 = lapLn / (this.end - this.start); } return ret; }; module.exports = IntervalTree;