@gmod/bbi
Version:
Parser for BigWig/BigBed files
276 lines • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BBI = void 0;
const generic_filehandle2_1 = require("generic-filehandle2");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const block_view_ts_1 = require("./block-view.js");
const BIG_WIG_MAGIC = -2003829722;
const BIG_BED_MAGIC = -2021002517;
function getDataView(buffer) {
return new DataView(buffer.buffer, buffer.byteOffset, buffer.length);
}
class BBI {
getHeader(opts) {
if (!this.headerP) {
this.headerP = this._getHeader(opts).catch((e) => {
this.headerP = undefined;
throw e;
});
}
return this.headerP;
}
/*
* @param filehandle - a filehandle from generic-filehandle2
*
* @param path - a Local file path as a string
*
* @param url - a URL string
*
* @param renameRefSeqs - an optional method to rename the internal reference
* sequences using a mapping function
*/
constructor(args) {
const { filehandle, renameRefSeqs = s => s, path, url } = args;
this.renameRefSeqs = renameRefSeqs;
if (filehandle) {
this.bbi = filehandle;
}
else if (url) {
this.bbi = new generic_filehandle2_1.RemoteFile(url);
}
else if (path) {
this.bbi = new generic_filehandle2_1.LocalFile(path);
}
else {
throw new Error('no file given');
}
}
async _getHeader(opts) {
const header = await this._getMainHeader(opts);
const chroms = await this._readChromTree(header, opts);
return {
...header,
...chroms,
};
}
async _getMainHeader(opts, requestSize = 2000) {
const b = await this.bbi.read(requestSize, 0, opts);
const dataView = getDataView(b);
const r1 = dataView.getInt32(0, true);
if (r1 !== BIG_WIG_MAGIC && r1 !== BIG_BED_MAGIC) {
throw new Error('not a BigWig/BigBed file');
}
let offset = 0;
const magic = dataView.getInt32(offset, true);
offset += 4;
const version = dataView.getUint16(offset, true);
offset += 2;
const numZoomLevels = dataView.getUint16(offset, true);
offset += 2;
const chromTreeOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const unzoomedDataOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const unzoomedIndexOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const fieldCount = dataView.getUint16(offset, true);
offset += 2;
const definedFieldCount = dataView.getUint16(offset, true);
offset += 2;
const asOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const totalSummaryOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const uncompressBufSize = dataView.getUint32(offset, true);
offset += 4;
const extHeaderOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const zoomLevels = [];
for (let i = 0; i < numZoomLevels; i++) {
const reductionLevel = dataView.getUint32(offset, true);
offset += 4;
const reserved = dataView.getUint32(offset, true);
offset += 4;
const dataOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
const indexOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
zoomLevels.push({
reductionLevel,
reserved,
dataOffset,
indexOffset,
});
}
const fileType = magic === BIG_BED_MAGIC ? 'bigbed' : 'bigwig';
// refetch header if it is too large on first pass,
// 8*5 is the sizeof the totalSummary struct
if (asOffset > requestSize || totalSummaryOffset > requestSize - 8 * 5) {
return this._getMainHeader(opts, requestSize * 2);
}
let totalSummary;
if (totalSummaryOffset) {
const b2 = b.subarray(Number(totalSummaryOffset));
let offset = 0;
const dataView = getDataView(b2);
const basesCovered = Number(dataView.getBigUint64(offset, true));
offset += 8;
const scoreMin = dataView.getFloat64(offset, true);
offset += 8;
const scoreMax = dataView.getFloat64(offset, true);
offset += 8;
const scoreSum = dataView.getFloat64(offset, true);
offset += 8;
const scoreSumSquares = dataView.getFloat64(offset, true);
offset += 8;
totalSummary = {
scoreMin,
scoreMax,
scoreSum,
scoreSumSquares,
basesCovered,
};
}
else {
throw new Error('no stats');
}
const decoder = new TextDecoder('utf8');
return {
zoomLevels,
magic,
extHeaderOffset,
numZoomLevels,
fieldCount,
totalSummary,
definedFieldCount,
uncompressBufSize,
asOffset,
chromTreeOffset,
totalSummaryOffset,
unzoomedDataOffset,
unzoomedIndexOffset,
fileType,
version,
autoSql: asOffset
? decoder.decode(b.subarray(asOffset, b.indexOf(0, asOffset)))
: '',
};
}
async _readChromTree(header, opts) {
const refsByNumber = [];
const refsByName = {};
const chromTreeOffset = Number(header.chromTreeOffset);
const dataView = getDataView(await this.bbi.read(32, chromTreeOffset, opts));
let offset = 0;
// const magic = dataView.getUint32(offset, true) // unused
offset += 4;
// const blockSize = dataView.getUint32(offset, true) // unused
offset += 4;
const keySize = dataView.getUint32(offset, true);
offset += 4;
const valSize = dataView.getUint32(offset, true);
offset += 4;
// const itemCount = dataView.getBigUint64(offset, true) // unused
offset += 8;
const decoder = new TextDecoder('utf8');
const bptReadNode = async (currentOffset) => {
const b = await this.bbi.read(4, currentOffset);
const dataView = getDataView(b);
let offset = 0;
const isLeafNode = dataView.getUint8(offset);
offset += 1;
// const reserved = dataView.getUint8(offset) // unused
offset += 1;
const count = dataView.getUint16(offset, true);
offset += 2;
if (isLeafNode) {
const b = await this.bbi.read(count * (keySize + valSize), currentOffset + offset);
const dataView = getDataView(b);
offset = 0;
for (let n = 0; n < count; n++) {
const key = decoder
.decode(b.subarray(offset, offset + keySize))
.replaceAll('\0', '');
offset += keySize;
const refId = dataView.getUint32(offset, true);
offset += 4;
const refSize = dataView.getUint32(offset, true);
offset += 4;
refsByName[this.renameRefSeqs(key)] = refId;
refsByNumber[refId] = {
name: key,
id: refId,
length: refSize,
};
}
}
else {
const nextNodes = [];
const dataView = getDataView(await this.bbi.read(count * (keySize + 8), currentOffset + offset));
offset = 0;
for (let n = 0; n < count; n++) {
offset += keySize;
const childOffset = Number(dataView.getBigUint64(offset, true));
offset += 8;
nextNodes.push(bptReadNode(childOffset));
}
await Promise.all(nextNodes);
}
};
await bptReadNode(chromTreeOffset + 32);
return {
refsByName,
refsByNumber,
};
}
/*
* fetches the "unzoomed" view of the bigwig data. this is the default for bigbed
* @param abortSignal - a signal to optionally abort this operation
*/
async getUnzoomedView(opts) {
const { unzoomedIndexOffset, refsByName, uncompressBufSize, fileType } = await this.getHeader(opts);
return new block_view_ts_1.BlockView(this.bbi, refsByName, unzoomedIndexOffset, uncompressBufSize > 0, fileType);
}
/**
* Gets features from a BigWig file
*
* @param refName - The chromosome name
*
* @param start - The start of a region
*
* @param end - The end of a region
*
* @param opts - An object containing basesPerSpan (e.g. pixels per basepair)
* or scale used to infer the zoomLevel to use
*/
async getFeatureStream(refName, start, end, opts) {
await this.getHeader(opts);
const chrName = this.renameRefSeqs(refName);
let view;
const { basesPerSpan, scale } = opts || {};
if (basesPerSpan) {
view = await this.getView(1 / basesPerSpan, opts);
}
else if (scale) {
view = await this.getView(scale, opts);
}
else {
view = await this.getView(1, opts);
}
return new rxjs_1.Observable(observer => {
view
.readWigData(chrName, start, end, observer, opts)
.catch((e) => {
observer.error(e);
});
});
}
async getFeatures(refName, start, end, opts) {
const ob = await this.getFeatureStream(refName, start, end, opts);
const ret = await (0, rxjs_1.firstValueFrom)(ob.pipe((0, operators_1.toArray)()));
return ret.flat();
}
}
exports.BBI = BBI;
//# sourceMappingURL=bbi.js.map