IRanges.js

import * as generics from "./AllGenerics.js";
import * as utils from "./utils.js";
import * as cutils from "./clone-utils.js";
import * as df from "./DataFrame.js";
import * as vec from "./Vector.js";
import * as olap from "./overlap-utils.js";

/**
 * An IRanges object is a collection of integer ranges, inspired by the class of the same name from the Bioconductor ecosystem.
 * Each range consists of a start position and a width, and may be associated with arbitrary range-level metadata in a {@linkplain DataFrame}.
 * The IRanges defines methods for the following generics:
 *
 * - {@linkcode LENGTH}
 * - {@linkcode SLICE}
 * - {@linkcode COMBINE}
 * - {@linkcode CLONE}
 *
 * @extends Vector
 */
export class IRanges extends vec.Vector {
    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    /**
     * @param {Array|TypedArray} start - Array of start positions for each range.
     * This should be coercible into an Int32Array.
     * @param {Array|TypedArray} width - Array of widths for each range.
     * This should be coercible into an Int32Array.
     * @param {Object} [options={}] - Optional parameters.
     * @param {?Array} [options.names=null] - Array of strings of length equal to `start`, containing names for each range.
     * Alternatively `null`, in which case the ranges are assumed to be unnamed.
     * @param {?DataFrame} [options.elementMetadata=null] - A {@linkplain DataFrame} with number of rows equal to the length of `start`, containing arbitrary per-range annotations.
     * Alternatively `null`, in which case a zero-column DataFrame is automatically constructed.
     * @param {Object} [options.metadata={}] - Object containing arbitrary metadata as key-value pairs.
     */
    constructor(start, width, { names = null, elementMetadata = null, metadata = {} } = {}) {
        if (arguments.length == 0) {
            super();
            return;
        }

        super(start.length, { names, elementMetadata, metadata });

        this._start = utils.convertToInt32Array(start);
        utils.checkNonNegative(this._start, "start");

        this._width = utils.convertToInt32Array(width);
        utils.checkNonNegative(this._width, "width");

        let n = this._start.length;
        if (n !== this._width.length) {
            throw new Error("'start' and 'width' should have the same length");
        }
    }

    static className = "IRanges";

    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    /**
     * @return {Int32Array} Array of integers containing the start position for each range.
     */
    start() {
        return this._start;
    }

    /**
     * @return {Int32Array} Array of integers containing the end position (specifically, one-past-the-end) for each range.
     */
    end() {
        return this._start.map((x, i) => x + this._width[i]);
    }

    /**
     * @return {Int32Array} Array of integers containing the width of each range.
     */
    width() {
        return this._width;
    }

    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    /**
     * @param {Array|TypedArray} value - Array of start positions for each range.
     * This should have length equal to the number of ranges and be coercible into an Int32Array.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this IRanges instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {IRanges} The IRanges object after setting the start positions to `value`.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setStart(value, { inPlace = false } = {}) {
        let candidate = utils.convertToInt32Array(value);
        if (candidate.length !== generics.LENGTH(this)) {
            throw new Error("'start' should be replaced by array of the same length");
        }
        utils.checkNonNegative(candidate, "start");

        let target = cutils.setterTarget(this, inPlace);
        target._start = candidate;
        return target;
    }

    /**
     * @param {Array|TypedArray} value - Array of start positions for each range.
     * This should have length equal to the number of ranges and be coercible into an Int32Array.
     * @return {IRanges} A reference to this IRanges object after setting the start positions to `value`.
     */
    $setStart(value) {
        return this.setStart(value, { inPlace: true });
    }

    /**
     * @param {Array|TypedArray} value - Array of widths for each range.
     * This should have length equal to the number of ranges and be coercible into an Int32Array.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this IRanges instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {IRanges} The IRanges object after setting the widths to `value`.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setWidth(value, { inPlace = false } = {}) {
        let candidate = utils.convertToInt32Array(value);
        if (candidate.length !== generics.LENGTH(this)) {
            throw new Error("'width' should be replaced by array of the same length");
        }
        utils.checkNonNegative(candidate, "width");

        let target = cutils.setterTarget(this, inPlace);
        target._width = candidate;
        return target;
    }

    /**
     * @param {Array|TypedArray} value - Array of widths for each range.
     * This should have length equal to the number of ranges and be coercible into an Int32Array.
     * @return {IRanges} A reference to this IRanges object after setting the widths to `value`.
     */
    $setWidth(value) {
        return this.setWidth(value, { inPlace: true });
    }

    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    /**
     * @return {IRangesOverlapIndex} A pre-built index for computing overlaps with other {@linkplain IRanges} instances.
     */
    buildOverlapIndex() {
        let tree = olap.buildIntervalTree(this._start, this.end());
        return new IRangesOverlapIndex(tree);
    }

    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    _bioconductor_LENGTH() {
        return this._start.length;
    }

    _bioconductor_SLICE(output, i, { allowView = false }) {
        super._bioconductor_SLICE(output, i, { allowView });
        output._start = generics.SLICE(this._start, i, { allowView });
        output._width = generics.SLICE(this._width, i, { allowView });
        return;
    }

    _bioconductor_COMBINE(output, objects) {
        super._bioconductor_COMBINE(output, objects);

        let all_s = [];
        let all_w = [];
        for (const x of objects) {
            all_s.push(x._start);
            all_w.push(x._width);
        }

        output._start = generics.COMBINE(all_s);
        output._width = generics.COMBINE(all_w);
        return;
    }

    _bioconductor_CLONE(output, { deepCopy = true }) {
        super._bioconductor_CLONE(output, { deepCopy });
        output._start = cutils.cloneField(this._start, deepCopy);
        output._width = cutils.cloneField(this._width, deepCopy);
        return;
    }

    /**************************************************************************
     **************************************************************************
     **************************************************************************/

    /**
     * @return {IRanges} A zero-length IRanges object.
     */
    static empty() {
        return new IRanges(new Int32Array, new Int32Array);
    }
}

/**
 * Pre-built index for overlapping {@linkplain IRanges} objects.
 * This is typically constructed using the {@linkcode IRanges#buildOverlapIndex IRanges.buildOverlapIndex} method for a "reference" object,
 * and can be applied to different query IRanges to identify overlaps with the reference.
 *
 * @hideconstructor
 */
export class IRangesOverlapIndex {
    constructor(tree) {
        this._tree = tree;
    }

    /**
     * @param {IRanges} query - The query object, containing ranges to be overlapped with those in the reference IRanges (that was used to construct this IRangesOverlapIndex object).
     * @return {Array} An array of length equal to the number of ranges in `query`,
     * where each element is an array containing the indices of the overlapping ranges in the reference {@linkplain IRanges} object.
     */
    overlap(query) {
        let n = generics.LENGTH(query);
        let output = new Array(n);
        for (var i = 0; i < n; i++) {
            output[i] = olap.queryIntervalTree(query._start[i], query._start[i] + query._width[i], this._tree);
        }
        return output;
    }
}