@gmod/cram
Version:
read CRAM files with pure Javascript
155 lines • 6.81 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_ts_1 = __importDefault(require("./cramFile/index.js"));
const errors_ts_1 = require("./errors.js");
class IndexedCramFile {
/**
*
* @param {object} args
* @param {CramFile} args.cram
*
* @param {Index-like} args.index object that supports
* getEntriesForRange(seqId,start,end) -> Promise[Array[index entries]]
*
* @param {number} [args.cacheSize] optional maximum number of CRAM records
* to cache. default 20,000
*
* @param {boolean} [args.checkSequenceMD5] - default true. if false,
* disables verifying the MD5 checksum of the reference sequence underlying a
* slice. In some applications, this check can cause an inconvenient amount
* (many megabases) of sequences to be fetched.
*/
constructor(args) {
this.cram =
args.cram ??
new index_ts_1.default({
url: args.cramUrl,
path: args.cramPath,
filehandle: args.cramFilehandle,
seqFetch: args.seqFetch,
checkSequenceMD5: args.checkSequenceMD5,
cacheSize: args.cacheSize,
});
if (!(this.cram instanceof index_ts_1.default)) {
throw new Error('invalid arguments: no cramfile');
}
this.index = args.index;
}
/**
*
* @param seq numeric ID of the reference sequence
* @param start start of the range of interest. 1-based closed coordinates.
* @param end end of the range of interest. 1-based closed coordinates.
*/
async getRecordsForRange(seq, start, end, opts = {}) {
opts.viewAsPairs = opts.viewAsPairs || false;
opts.pairAcrossChr = opts.pairAcrossChr || false;
opts.maxInsertSize = opts.maxInsertSize || 200000;
if (typeof seq === 'string') {
// TODO: support string reference sequence names somehow
throw new errors_ts_1.CramUnimplementedError('string sequence names not yet supported');
}
const seqId = seq;
const slices = await this.index.getEntriesForRange(seqId, start, end);
// fetch all the slices and parse the feature data
const sliceResults = await Promise.all(slices.map(slice => this.getRecordsInSlice(slice, feature => feature.sequenceId === seq &&
feature.alignmentStart <= end &&
feature.lengthOnRef !== undefined &&
feature.alignmentStart + feature.lengthOnRef - 1 >= start)));
let ret = Array.prototype.concat(...sliceResults);
if (opts.viewAsPairs) {
const readNames = {};
const readIds = {};
for (const read of ret) {
const name = read.readName;
if (name === undefined) {
throw new Error('readName undefined');
}
const id = read.uniqueId;
if (!readNames[name]) {
readNames[name] = 0;
}
readNames[name] += 1;
readIds[id] = 1;
}
const unmatedPairs = {};
Object.entries(readNames).forEach(([k, v]) => {
if (v === 1) {
unmatedPairs[k] = true;
}
});
const matePromises = [];
for (const cramRecord of ret) {
const name = cramRecord.readName;
if (name === undefined) {
throw new Error('readName undefined');
}
if (unmatedPairs[name] &&
cramRecord.mate &&
(cramRecord.mate.sequenceId === seqId || opts.pairAcrossChr) &&
Math.abs(cramRecord.alignmentStart - cramRecord.mate.alignmentStart) <
opts.maxInsertSize) {
const mateSlices = this.index.getEntriesForRange(cramRecord.mate.sequenceId, cramRecord.mate.alignmentStart, cramRecord.mate.alignmentStart + 1);
matePromises.push(mateSlices);
}
}
const mateBlocks = await Promise.all(matePromises);
let mateChunks = [];
for (const block of mateBlocks) {
mateChunks.push(...block);
}
// filter out duplicates
mateChunks = mateChunks
.sort((a, b) => a.toString().localeCompare(b.toString()))
.filter((item, pos, ary) => !pos || item.toString() !== ary[pos - 1].toString());
const mateRecordPromises = [];
const mateFeatPromises = [];
for (const c of mateChunks) {
let recordPromise = this.cram.featureCache.get(c.toString());
if (!recordPromise) {
recordPromise = this.getRecordsInSlice(c, () => true);
this.cram.featureCache.set(c.toString(), recordPromise);
}
mateRecordPromises.push(recordPromise);
const featPromise = recordPromise.then(feats => {
const mateRecs = [];
for (const feature of feats) {
if (feature.readName === undefined) {
throw new Error('readName undefined');
}
if (unmatedPairs[feature.readName] && !readIds[feature.uniqueId]) {
mateRecs.push(feature);
}
}
return mateRecs;
});
mateFeatPromises.push(featPromise);
}
const newMateFeats = await Promise.all(mateFeatPromises);
if (newMateFeats.length) {
const newMates = newMateFeats.reduce((result, current) => result.concat(current));
ret = ret.concat(newMates);
}
}
return ret;
}
getRecordsInSlice({ containerStart, sliceStart, sliceBytes, }, filterFunction) {
const container = this.cram.getContainerAtPosition(containerStart);
const slice = container.getSlice(sliceStart, sliceBytes);
return slice.getRecords(filterFunction);
}
/**
*
* @param {number} seqId
* @returns {Promise} true if the CRAM file contains data for the given
* reference sequence numerical ID
*/
hasDataForReferenceSequence(seqId) {
return this.index.hasDataForReferenceSequence(seqId);
}
}
exports.default = IndexedCramFile;
//# sourceMappingURL=indexedCramFile.js.map