UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

149 lines 6.53 kB
import CramFile from "./cramFile/index.js"; import { CramUnimplementedError } from "./errors.js"; export default 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 CramFile({ url: args.cramUrl, path: args.cramPath, filehandle: args.cramFilehandle, seqFetch: args.seqFetch, checkSequenceMD5: args.checkSequenceMD5, cacheSize: args.cacheSize, }); if (!(this.cram instanceof CramFile)) { 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 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); } } //# sourceMappingURL=indexedCramFile.js.map