UNPKG

@givengine/give-tree

Version:

Node implementation of interval-tree based cache data structures: base class

976 lines (931 loc) 33.6 kB
/** * @license * Copyright 2017-2019 The Regents of the University of California. * All Rights Reserved. * * Created by Xiaoyi Cao * Department of Bioengineering * * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @module GiveNonLeafNode */ const GiveTreeNode = require('./giveTreeNode') const ChromRegion = require('@givengine/chrom-region') /** * Specialized error used to signal cross-generation balancing requirements. * * @class * @extends {Error} */ class CannotBalanceError extends Error { constructor () { super(...arguments) if (Error.captureStackTrace) { Error.captureStackTrace(this, CannotBalanceError) } } } /** * Non-leaf nodes for GIVE Trees * This is an interface for all nodes that belongs to GIVE Trees, including * insertion, deletion, traversing, and other functionalities. * * When traversing, everything in `continuedList` of __the starting record entry * (see `DataNode`) only__ will be processed first, then everything in * `startList` in all overlapping records will be processed. * * Note that when creating `GiveNonLeafNode` instances, `props` should either * have both `start` and `end`, or both `keys` and `values` specified in its * properties. * * @interface GiveNonLeafNode * @alias module:GiveNonLeafNode * @implements {GiveTreeNode} * * @property {boolean} isRoot - Whether this node is a root node (needed to * handle changes in tree structure) * @property {GiveTree} tree - Link to the `GiveTree` object * to access tree-wise properties. * @property {Array<number>} keys - A list of keys of children. * See `this.values`. * @property {Array<GiveTreeNode|null|boolean>} values - A list of data * entries, can be `null` or `false` to represent data not loaded and * empty nodes respectively. * * `keys.length` will be `1` more than `childNum`; * * `keys[i]` will be the start coordinate of `values[i]` and end * coordinate of `values[i - 1]`; * * `keys[keys.length - 1]` will be the end coordinate of * `values[childNum - 1]`. * * Therefore, neighboring nodes will have exactly one overlapping key. * * `values` can be `false` or `null` (unless prohibited by * implementation) indicating empty regions or data not loaded, * respectively. * @property {number} reverseDepth - "Reversed depth" of the node. The one * holding leaf nodes (should be `DataNode` or similar * implementations) is at `0` and root is at maximum. * @property {GiveNonLeafNode|null|boolean} _next - The next node * (sibling). Can be `null` or `false`. * @property {GiveNonLeafNode|null|boolean} _prev - The previous node * (sibling). * @param {Object} props - properties that will be passed to the * individual implementations. For `GiveNonLeafNode`, these * properties will be used: * @param {boolean} props.isRoot - for `this.isRoot` * @param {GiveTree} props.tree - for `this.tree` * @param {number} [props.start] - The start coordinate this node will * cover. Equals to `this.keys[0]`. * @param {number} [props.end] - The end coordinate this node will cover. * Equals to `this.keys[this.keys.length - 1]`. * * Exceptions will be thrown if `props.start` or `props.end` is not an * positive integer number or `props.start >= props.end` (zero-length * regions not allowed). * @param {number} [props.reverseDepth] - for `this.reverseDepth` * @param {GiveNonLeafNode|boolean} [props.nextNode] - for `this._next` * @param {GiveNonLeafNode|boolean} [props.prevNode] - for `this._prev` * @param {Array<number>} [props.keys] - for `this.keys` * @param {Array<GiveTreeNode>} [props.values] - for `this.values`. * Note that if `keys` and `values` are provided, `start` and `end` * will be overridden as they are already provided in `keys`. */ class GiveNonLeafNode extends GiveTreeNode { constructor (props) { super(...arguments) // start and length is for the corresponding region props = props || {} this.isRoot = !!props.isRoot this.tree = props.tree if ( Array.isArray(props.keys) && Array.isArray(props.values) && props.values.length === props.keys.length - 1 ) { // TODO: Sanity check for `this.keys`? this.keys = props.keys this.values = props.values } else { if (!Number.isInteger(props.start) || !Number.isInteger(props.end)) { throw (new Error('start or end is not an integer number ' + 'in non-leaf node construction!')) } else if (props.start < 0 || props.end < 0 || props.start >= props.end ) { throw (new Error('Range error. start: ' + props.start + ', end: ' + props.end)) } this.keys = [props.start, props.end] this.values = [this.emptyChildValue] } this.reverseDepth = ( Number.isInteger(props.reverseDepth) && props.reverseDepth > 0) ? props.reverseDepth : 0 if (this.tree.neighboringLinks) { this.next = props.nextNode this.prev = props.prevNode } } /** * The value for an empty child node * @type {null|boolean} * * @readonly * @memberof GiveNonLeafNode */ get emptyChildValue () { return this.tree.localOnly ? false : null } /** * Trancate chromosomal range to the region covered by * `this`. * * @param {ChromRegion} chrRange - The chromosomal range to be * truncated * @param {boolean} [truncStart] - Whether to truncate the start coordinate * @param {boolean} [truncEnd] - Whether to truncate the end coordinate * @param {boolean} [doNotThrow] - Whether to throw an exception if * truncated region has a length not greater than 0 (because `chrRange` * does not overlap with this node at all). * @returns {ChromRegion} Returns a new chromosomal range with * trancated coordinates. */ truncateChrRange (chrRange, truncStart, truncEnd, doNotThrow) { var newRegion = chrRange.clone() try { if (truncStart && newRegion.start < this.start) { newRegion.start = this.start } if (truncEnd && newRegion.end > this.end) { newRegion.end = this.end } } catch (err) { if (!doNotThrow) { throw (new Error(chrRange + ' is not a valid chrRegion ' + 'or not overlapping with the current node. \nRange start: ' + newRegion.start + ', end: ' + newRegion.end + '\nCurrent node start: ' + this.start + ', end: ' + this.end)) } } return newRegion } get start () { return this.keys[0] } get end () { return this.keys[this.keys.length - 1] } /** * The length of the region covered by this node * * @type {number} */ get length () { return this.end - this.start } /** * The number of children under this node. * * @type {number} */ get childNum () { return this.values.length } set start (newStart) { this.keys[0] = newStart } set end (newEnd) { this.keys[this.keys.length - 1] = newEnd } /** * The next node * * @type {GiveNonLeafNode|null} */ get next () { if (!this.tree.neighboringLinks) { throw new Error( 'Cannot get the next sibling in an unlinked tree!') } return this._next } /** * The previous node * * @type {GiveNonLeafNode|null} */ get prev () { if (!this.tree.neighboringLinks) { throw new Error( 'Cannot get the previous sibling in an unlinked tree!') } return this._prev } set next (nextNode) { if (!this.tree.neighboringLinks) { throw new Error( 'Cannot set the next sibling in an unlinked tree!') } if (nextNode === undefined) { nextNode = null } this._next = nextNode if (nextNode) { nextNode._prev = this if (this.lastChild instanceof GiveNonLeafNode) { this.lastChild.next = nextNode.firstChild } else { // needs to handle child connections by themselves if (nextNode.firstChild instanceof GiveNonLeafNode) { nextNode.firstChild.prev = this.lastChild } } } else { // `nextNode === null` or `nextNode === false` try { this.lastChild.next = nextNode } catch (ignore) { } } } set prev (prevNode) { if (!this.tree.neighboringLinks) { throw new Error( 'Cannot set the previous sibling in an unlinked tree!') } if (prevNode === undefined) { prevNode = null } this._prev = prevNode if (prevNode) { prevNode._next = this if (this.firstChild instanceof GiveNonLeafNode) { this.firstChild.prev = prevNode.lastChild } else { // needs to handle child connections by themselves if (prevNode.lastChild instanceof GiveNonLeafNode) { prevNode.lastChild.next = this.firstChild } } } else { // `nextNode === null` or `nextNode === false` try { this.firstChild.prev = prevNode } catch (ignore) { } } } /** * Break links between siblings and `this` * * @param {boolean|null} [convertTo=null] convert the link into. Should be * `null` (default) or `false`. * @param {boolean} [noPrev] - do not severe links from previous siblings * @param {boolean} [noNext] - do not severe links from next siblings */ _severeSelfLinks (convertTo, noPrev, noNext) { if (!this.tree.neighboringLinks) { throw new Error( 'No sibling links to severe in an unlinked tree!') } if (!noPrev) { try { this.prev.next = convertTo } catch (ignore) { } } if (!noNext) { try { this.next.prev = convertTo } catch (ignore) { } } } /** * Break links between all children. * * @param {boolean|null} [convertTo=null] convert the link into. Should be * either `null` (default) or `false`. * @param {boolean} [noPrev] - do not severe links from previous siblings * @param {boolean} [noNext] - do not severe links from next siblings */ _severeChildLinks (convertTo, noPrev, noNext) { if (!this.tree.neighboringLinks) { throw new Error( 'No child links to severe in an unlinked tree!') } if (!noPrev) { try { this.firstChild._severeLinks(convertTo, false, true) } catch (ignore) { } } if (!noNext) { try { this.lastChild._severeLinks(convertTo, true, false) } catch (ignore) { } } } /** * Break links between siblings and `this`, and between all * children as well. * * @param {boolean|null} [convertTo=null] convert the link into. Should be * either `null` (default) or `false`. * @param {boolean} [noPrev] - do not severe links from previous siblings * @param {boolean} [noNext] - do not severe links from next siblings */ _severeLinks (convertTo, noPrev, noNext) { this._severeChildLinks(convertTo, noPrev, noNext) this._severeSelfLinks(convertTo, noPrev, noNext) } /** * Fix sibling links for a specific child. * * @param {number} index - the index of the child * @param {boolean} [doNotFixBack] - if `true`, the links after this child * will not be fixed. * @param {boolean} [doNotFixFront] - if `true`, the links before this * child will not be fixed. */ _fixChildLinks (index, doNotFixBack, doNotFixFront) { if (!this.tree.neighboringLinks) { throw new Error('No child links to fix in an unlinked tree!') } if (this.reverseDepth > 0) { if (!doNotFixBack) { try { let nextChild = this._getChildNext(index) if (this.values[index]) { this.values[index].next = nextChild } else { nextChild.prev = this.values[index] } } catch (ignore) { } } if (!doNotFixFront) { try { let prevChild = this._getChildPrev(index) if (this.values[index]) { this.values[index].prev = prevChild } else { prevChild.next = this.values[index] } } catch (ignore) { } } } } /** * The first child element of `this`. * * @type {GiveTreeNode|boolean|null} The first child element */ get firstChild () { return this.values[0] } /** * The first leaf element of `this`. * * @type {GiveTreeNode|boolean|null} The first child element */ get firstLeaf () { return this.reverseDepth > 0 ? this.firstChild.firstLeaf : this.firstChild } /** * The last child element of `this`. * * @type {GiveTreeNode|boolean|null} The last child element */ get lastChild () { return this.values[this.childNum - 1] } get lastLeaf () { return this.reverseDepth > 0 ? this.lastChild.lastLeaf : this.lastChild } /** * Get the previous sibling of child at `index`. * * @param {number} index - index of the child * @returns {GiveTreeNode|boolean|null} the previous sibling of the * child * @throws {Error} If no children available, throw an error */ _getChildPrev (index) { if (index > 0) { return this.values[index - 1] } if (this.prev) { return this.prev.lastChild } throw new Error('No previous children!') } /** * Get the next sibling of child at `index`. * * @param {number} index - index of the child * @returns {GiveTreeNode|boolean|null} the next sibling of the * child * @throws {Error} If no children available, throw an error */ _getChildNext (index) { if (index < (this.childNum - 1)) { return this.values[index + 1] } if (this.next) { return this.next.firstChild } throw new Error('No next children!') } /** * Insert data under this node. * * If auto-balancing is supported, after `this.insert()` is called, the * immediate children should all be balanced (`this` may still have * non-compliant number of children). If all children insertions * were done with `child.insert()` then the entire tree should be balanced. * If not, then child balancing needs to be done in * `this._addNonLeafRecords`. * * @param {Array<ChromRegion>} data - the sorted array of data * entries (each should be an extension of `ChromRegion`). * * `data === null` or `data === []` means there is no data in `chrRange` * and `false`s will be used in actual storage. * * __NOTICE:__ any data overlapping `chrRange` should appear either * here or in `continuedList`, otherwise `continuedList` in data * entries may not work properly. * * After insertion, any entry within `data` that overlaps `chrRange` * will be deleted from the array. * * @param {ChromRegion} chrRange - the chromosomal range that * `data` corresponds to. * * This is used to mark the empty regions correctly. No `null` will * present within these regions after this operation. * * This parameter should be an `Object` with at least two properties: * `{ start: <start coordinate>, end: <end coordinate>, ... }`, * preferably a `ChromRegion` object. * * @param {Object} [props] - additional properties being passed onto * nodes. * * @param {Array<ChromRegion>} [props.continuedList] - the list of data * entries that should not start in `chrRange` but are passed from the * earlier regions, this will be useful for later regions if date for * multiple regions are inserted at the same time * * @param {function} [props.callback] - the callback function to be * used (with the data entry as its sole parameter) when inserting * * @param {function} [props.LeafNodeCtor] - the constructor function of * leaf nodes if they are not the same as the non-leaf nodes. * * @returns {GiveNonLeafNode|false} * This shall reflect whether auto-balancing is supported for the tree. * See `_restructureImmediateChildren` for * details. */ insert (data, chrRange, props) { if (data && data.length === 1 && !chrRange) { chrRange = data[0] } if (data && !Array.isArray(data)) { throw (new Error('Data is not an array! ' + 'This will cause problems in continuedList.')) } if (chrRange) { // clip chrRegion first (should never happen) chrRange = this.truncateChrRange(chrRange, true, true) // there are two cases for insertion: // 1. leaf nodes: use `DataNode` to store raw data // 2. non-leaf nodes: // go deep to generate branch structure, or update summary // (for trees that support summary and resolutions) if (this.reverseDepth > 0) { // case 2 this._addNonLeafRecords(data, chrRange, props) // Note: keys may change after adding leaf records this.keys = this.values.map(node => node.start) this.keys.push(this.lastChild.end) } else { // case 1 this._addLeafRecords(data, chrRange, props) } } else { // chrRange throw (new Error(chrRange + ' is not a valid chrRegion.')) } // end if(chrRange) return this.constructor.restructuringRequired ? this._restructureImmediateChildren() : (this.isRoot || !this.isEmpty) && this } /** * The function to be called after adding/removing data to the node. * * This is used in implementations that involve post-insertion * processes of the tree (for example, rebalancing in B+ tree * derivatives). * * The function will only restructure the immediate children of `this` * or `this` if it is a root node. It will assume all grandchildren * (if any) has been already restructured correctly. * * For trees that do not implement post-insertion processes, return * `this`. * * @param {boolean} intermediate - Whether this restructuring is an * intermediate approach. * * If this is `true`, then the function is called to rearrange in parent * nodes because their children cannot get their grandchildren * conforming to B+ tree requirements. If this is the case, the * children in this call does not need to completely conform to B+ * tree requirements since the function flow will come back once the * grandchildren have been rearranged. * @returns {GiveNonLeafNode|false} * This shall reflect whether there are any changes in the tree * structure for root and non-root nodes: * * For root nodes, always return `this` (cannot delete root even * without any children). * * For inner nodes (or leaf), if the node should be removed (being * merged with its sibling(s) or becoming an empty node, for * example), return `false`. Return `this` in all other cases. */ _restructureImmediateChildren (intermediate) { // for non-auto-balancing trees, return false if this node has no data // any more if (this.values[0] && this.values[0].isEmpty) { this.values[0] = false } return (this.isRoot || !this.isEmpty) && this } /** * The function to be called after adding/removing data to the node. * * This is used in implementations that involve post-insertion * processes of the tree (for example, rebalancing in B+ tree * derivatives). * * The function will only restructure the immediate children of `this` * or `this` if it is a root node. It will assume all grandchildren * (if any) has been already restructured correctly. * * For trees that do not implement post-insertion processes, return * `this`. * * @returns {GiveNonLeafNode|false} * This shall reflect whether there are any changes in the tree * structure for root and non-root nodes: * * For root nodes, always return `this` (cannot delete root even * without any children). * * For inner nodes (or leaf), if the node should be removed (being * merged with its sibling(s) or becoming an empty node, for * example), return `false`. Return `this` in all other cases. */ restructure () { // for non-auto-balancing trees, return false if this node has no data // any more if (this.constructor.restructuringRequired) { try { if (this.reverseDepth > 0) { let grandChildrenCompliant = false do { try { this.values.forEach(node => node._restructure()) grandChildrenCompliant = true } catch (err) { if (err instanceof CannotBalanceError) { this._restructureImmediateChildren(true) } } } while (!grandChildrenCompliant) } } catch (err) { if (err instanceof CannotBalanceError && !this.isRoot) { throw err } } return this._restructureImmediateChildren() } return (this.isRoot || !this.isEmpty) && this } /** * Add records to a non-leaf node. * * @param {Array<ChromRegion>} data - the sorted array of data * entries. See `this.insert` for detailed description. * @param {ChromRegion} chrRange - see `this.insert` * @param {Object} props - additional properties being passed onto nodes. * @param {Array<ChromRegion>} props.continuedList - see `this.insert` * @param {function|null} props.callback - see `this.insert` */ _addNonLeafRecords (data, chrRange, props) { throw new Error('GiveNonLeafNode._addNonLeafRecords not ' + 'implemented in `' + this.constructor.name + '`!') } /** * Add records to a leaf node (with `revDepth === 0`). * * @param {Array<ChromRegion>} data - the sorted array of data * entries. See `this.insert` for detailed description. * @param {ChromRegion} chrRange - see `this.insert` * @param {Object} props - additional properties being passed onto nodes. * @param {Array<ChromRegion>} props.continuedList - see `this.insert` * @param {function|null} props.callback - see `this.insert` * @param {function|null} props.LeafNodeCtor - see `this.insert` */ _addLeafRecords (data, chrRange, props) { throw new Error('GiveNonLeafNode._addLeafRecords not ' + 'implemented in `' + this.constructor.name + '`!') } /** * Clear the tree into a given empty value, or `this.emptyChildValue` * * @param {boolean|null} [convertTo] converted value * @memberof GiveNonLeafNode */ clear (convertTo) { convertTo = convertTo === false ? false : this.emptyChildValue if (this.tree.neighboringLinks) { this._severeChildLinks(convertTo) } this.keys = [this.start, this.end] this.values = [convertTo] } /** * Split a child into two. * * If the old child at `index` is not `null` or `false`, both * `newLatterChild` and `newFormerChild` will be needed (otherwise the * tree structure may be corrupted). * * @param {number} index - index of the child to be split. * @param {number} newKey - the new key separating the two children * @param {GiveTreeNode|false|null} [newLatterChild] - the new * latter child. If `undefined`, use the old child. * @param {GiveTreeNode|false|null} [newFormerChild] - the new * former child. If `undefined`, use the old child. * @returns {number} Number of split children (2 in this case) */ _splitChild (index, newKey, newLatterChild, newFormerChild) { if (this.values[index] && (newLatterChild === undefined && newFormerChild === undefined) ) { throw new Error('Cannot split an existing child without ' + 'providing both resulting siblings!') } this.keys.splice(index + 1, 0, newKey) this.values.splice(index + 1, 0, newLatterChild === undefined ? this.values[index] : newLatterChild) if (newLatterChild !== undefined) { if (this.tree.neighboringLinks) { this._fixChildLinks(index + 1, false, true) } } if (newFormerChild !== undefined) { this.Value[index] = newFormerChild if (this.tree.neighboringLinks) { this._fixChildLinks(index, newLatterChild === undefined, false) } } return 2 } /** * Determine whether two children are mergable. * * @param {type} childFront - the child at front being considered to * merge. * @param {type} childBack - the child at back being considered to merge. * @returns {type} Return whether the children are mergable. * * If both are `null` or both are `false`, return `true`. * If `childFront` has `.mergeAfter(child)` function and returns true * when called with `childBack`, return `true`. * * Return false on all other cases. */ static _childMergable (childFront, childBack) { return (childFront === childBack && (childFront === this.emptyChildValue || childFront === false) ) || (childFront && childFront.mergeAfter(childBack) ) } /** * Merge neighboring children that are the same as * `this.values[index]`, if they are `false` or `null`. * This function will always merge with the child __before__ `index` * first, then, if `mergeNext === true`, merge with the child after * `index`. * * @param {number} index - index of the child * @param {boolean} mergeNext - whether merge the next child as well * @param {boolean} crossBorder - whether merging can happen across * parent borders. If so, the children nodes in siblings of this may be * expanded. (The number of children will not be affected in sibling * nodes, so that the structure of neighboring nodes are not messed * up.) * * __Note:__ `crossBorder` can only be used when * `this.tree.neighboringLinks === true`. * If `this.tree.neighboringLinks === false`, this argument will be * ignored, because `this` has no way of knowing its own siblings, thus * unable to merge children across sibling * borders. * @returns {boolean} whether merge happened to the previous child (this * is used for calling function to correct indices when merging during * traversing.) */ _mergeChild (index, mergeNext, crossBorder) { let mergedFront = false if (index > 0 || (this.tree.neighboringLinks && crossBorder && this.childNum > 1) ) { // merge previous child first try { if (this.constructor._childMergable( this._getChildPrev(index), this.values[index] )) { // remove child at `index` this.keys.splice(index, 1) this.values.splice(index, 1) if (this.tree.neighboringLinks) { if (index === 0) { this.prev.end = this.start this._fixChildLinks(index, true) } else { this._fixChildLinks(index - 1, false, true) } } mergedFront = true } } catch (ignore) {} } // if `mergeNext` is `true`, do the same to the next node if (mergeNext) { if (index < this.childNum - 1 && this.constructor._childMergable( this.values[index], this.values[index + 1] ) ) { // remove child at `index + 1` this.keys.splice(index + 1, 1) this.values.splice(index + 1, 1) if (this.tree.neighboringLinks) { this._fixChildLinks(index, false, true) } } else if ( crossBorder && index === this.childNum - 1 && this.next && this.childNum > 1 && this.constructor._childMergable( this.values[index], this._getChildNext(index) ) ) { this.next.keys[0] = this.keys[index] this.next.values[0] = this.values[index] this.keys.splice(-1) this.values.splice(-1) // needs to change the boundary of sibling node this.end = this.next.start if (this.tree.neighboringLinks) { this.next._fixChildLinks(0, false, true) } } } return mergedFront } traverse (chrRange, callback, filter, breakOnFalse, props, ...args) { // Implementation without resolution support // Because this is a non-leaf node, it always descends to its children // until some leaf node is reached. if (!chrRange) { throw (new Error(chrRange + ' is not a valid chrRegion.')) } let index = 0 while (index < this.childNum && this.keys[index + 1] <= chrRange.start ) { index++ } while (this.keys[index] < chrRange.end && index < this.childNum) { if (this.values[index] && !this.values[index].traverse(chrRange, callback, filter, breakOnFalse, props, ...args) ) { return false } props.notFirstCall = true index++ } return true } /** * Return an array of chrRegions that does not have * data loaded to allow buffered loading of data * * @param {ChromRegion} chrRange - The range of query. * @param {Object} [props] - additional properties being passed onto * nodes * @param {Array<ChromRegion>} [props._result] - previous unloaded * regions. This will be appended to the front of returned value. * This array will be updated if it gets appended to reduce memory * usage and GC. * @returns {Array<ChromRegion>} An ordered array of the regions that * does not have the data at the current resolution requirement. * If no non-data ranges are found, return [] */ getUncachedRange (chrRange, props) { props._result = props._result || [] if (chrRange) { var index = 0 while (index < this.childNum && this.keys[index + 1] <= chrRange.start ) { index++ } while (index < this.childNum && this.keys[index] < chrRange.end ) { if (this.values[index]) { // there is a child node here, descend this.values[index].getUncachedRange(chrRange, props) } else if (this.values[index] === null) { let newStart = Math.max(this.keys[index], chrRange.start) let newEnd = Math.min(this.keys[index + 1], chrRange.end) if (props._result[props._result.length - 1] && props._result[props._result.length - 1].end === newStart ) { props._result[props._result.length - 1].end = newEnd } else { props._result.push(new ChromRegion({ chr: chrRange.chr, start: newStart, end: newEnd })) } } index++ } return props._result } else { // chrRange throw (new Error(chrRange + ' is not a valid chrRegion.')) } } hasUncachedRange (chrRange, props) { if (chrRange) { var index = 0 while (index < this.childNum && this.keys[index + 1] <= chrRange.start ) { index++ } while (index < this.childNum && this.keys[index] < chrRange.end ) { if (this.values[index]) { // there is a child node here, descend if (this.values[index].hasUncachedRange(chrRange, props)) { return true } } else if (this.values[index] === null) { return true } index++ } return false } else { // chrRange throw (new Error(chrRange + ' is not a valid chrRegion.')) } } /** * Whether this node is empty. * If there is no child then the node is considered empty. * * @type {boolean} */ get isEmpty () { return this.childNum <= 0 || (this.childNum === 1 && (this.values[0] === false || !!(this.values[0] && this.values[0].isEmpty))) } } /** * @static * @property {boolean} restructuringRequired - Whether restructuring is needed * for this class of node * @memberof GiveNonLeafNode */ GiveNonLeafNode.restructuringRequired = false GiveNonLeafNode.CannotBalanceError = CannotBalanceError module.exports = GiveNonLeafNode