UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

310 lines 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildRFSchema = buildRFSchema; exports.default = decodeRecord; const errors_ts_1 = require("../../errors.js"); const record_ts_1 = require("../record.js"); const sectionParsers_ts_1 = require("../sectionParsers.js"); const util_ts_1 = require("../util.js"); // Uses DataView rather than typed arrays because the buffer is a subarray of a // larger ArrayBuffer. Int32Array(buffer.buffer) would start at byte 0 of the // parent, ignoring buffer.byteOffset, causing silent data corruption. function parseTagValueArray(buffer) { const arrayType = String.fromCharCode(buffer[0]); const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); const length = dv.getUint32(1, true); const array = new Array(length); const dataOffset = 5; if (arrayType === 'c') { for (let i = 0; i < length; i++) { array[i] = dv.getInt8(dataOffset + i); } } else if (arrayType === 'C') { for (let i = 0; i < length; i++) { array[i] = dv.getUint8(dataOffset + i); } } else if (arrayType === 's') { for (let i = 0; i < length; i++) { array[i] = dv.getInt16(dataOffset + i * 2, true); } } else if (arrayType === 'S') { for (let i = 0; i < length; i++) { array[i] = dv.getUint16(dataOffset + i * 2, true); } } else if (arrayType === 'i') { for (let i = 0; i < length; i++) { array[i] = dv.getInt32(dataOffset + i * 4, true); } } else if (arrayType === 'I') { for (let i = 0; i < length; i++) { array[i] = dv.getUint32(dataOffset + i * 4, true); } } else if (arrayType === 'f') { for (let i = 0; i < length; i++) { array[i] = dv.getFloat32(dataOffset + i * 4, true); } } else { throw new Error(`unknown type: ${arrayType}`); } return array; } function parseTagData(tagType, buffer) { if (tagType === 'Z') { return (0, util_ts_1.readNullTerminatedStringFromBuffer)(buffer); } if (tagType === 'A') { return String.fromCharCode(buffer[0]); } if (tagType === 'C') { return buffer[0]; } if (tagType === 'c') { return buffer[0] > 127 ? buffer[0] - 256 : buffer[0]; } if (tagType === 'B') { return parseTagValueArray(buffer); } if (tagType === 'H') { return Number.parseInt((0, util_ts_1.readNullTerminatedStringFromBuffer)(buffer).replace(/^0x/, ''), 16); } const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); if (tagType === 'I') { return dv.getUint32(0, true); } if (tagType === 'i') { return dv.getInt32(0, true); } if (tagType === 's') { return dv.getInt16(0, true); } if (tagType === 'S') { return dv.getUint16(0, true); } if (tagType === 'f') { return dv.getFloat32(0, true); } throw new errors_ts_1.CramMalformedError(`Unrecognized tag type ${tagType}`); } function buildRFSchema(bd, majorVersion) { const SC = majorVersion > 1 ? bd.SC : bd.IN; const arr = new Array(128); arr['B'.charCodeAt(0)] = [ 'B', () => [String.fromCharCode(bd.BA()), bd.QS()], ]; arr['X'.charCodeAt(0)] = ['X', () => bd.BS()]; arr['D'.charCodeAt(0)] = ['D', () => bd.DL()]; arr['I'.charCodeAt(0)] = ['I', () => (0, util_ts_1.decodeUtf8)(bd.IN())]; arr['i'.charCodeAt(0)] = ['i', () => String.fromCharCode(bd.BA())]; arr['b'.charCodeAt(0)] = ['b', () => (0, util_ts_1.decodeUtf8)(bd.BB())]; arr['q'.charCodeAt(0)] = ['q', () => Array.from(bd.QQ())]; arr['Q'.charCodeAt(0)] = ['Q', () => bd.QS()]; arr['H'.charCodeAt(0)] = ['H', () => bd.HC()]; arr['P'.charCodeAt(0)] = ['P', () => bd.PD()]; arr['N'.charCodeAt(0)] = ['N', () => bd.RS()]; arr['S'.charCodeAt(0)] = ['S', () => (0, util_ts_1.decodeUtf8)(SC())]; return arr; } function decodeReadFeatures(alignmentStart, readFeatureCount, bd, schema) { let readPos = 0; let refDelta = 0; const base = alignmentStart - 1; const readFeatures = new Array(readFeatureCount); const decodeFC = bd.FC; const decodeFP = bd.FP; for (let i = 0; i < readFeatureCount; i++) { const codeNum = decodeFC(); readPos += decodeFP(); const entry = schema[codeNum]; if (!entry) { throw new errors_ts_1.CramMalformedError(`invalid read feature code "${String.fromCharCode(codeNum)}"`); } const code = entry[0]; const data = entry[1](); readFeatures[i] = { code, pos: readPos, refPos: readPos + base + refDelta, data, }; if (code === 'D' || code === 'N') { refDelta += data; } else if (code === 'I' || code === 'S') { refDelta -= data.length; } else if (code === 'i') { refDelta -= 1; } } return [readFeatures, refDelta]; } function decodeQualityScores(readLength, decodeBulkBytesRaw, decodeQS) { const raw = decodeBulkBytesRaw?.('QS', readLength); if (raw) { return raw; } const out = new Uint8Array(readLength); for (let i = 0; i < readLength; i++) { out[i] = decodeQS(); } return out; } function decodeReadBases(readLength, decodeBulkBytesRaw, decodeBA) { const raw = decodeBulkBytesRaw?.('BA', readLength); if (raw) { return (0, util_ts_1.decodeUtf8)(raw); } const buf = new Uint8Array(readLength); for (let i = 0; i < readLength; i++) { buf[i] = decodeBA(); } return (0, util_ts_1.decodeUtf8)(buf); } function decodeRecord(slice, bd, rfSchema, boundTagDecoders, compressionScheme, sliceHeader, coreDataBlock, blocksByContentId, cursors, majorVersion, recordNumber, uniqueId, decodeOptions, decodeBulkBytesRaw) { let flags = bd.BF(); // note: the C data type of compressionFlags is byte in cram v1 and int32 in // cram v2+, but that does not matter for us here in javascript land. const cramFlags = bd.CF(); if (!(0, sectionParsers_ts_1.isMappedSliceHeader)(sliceHeader.parsedContent)) { throw new Error('slice header not mapped'); } const sequenceId = majorVersion > 1 && sliceHeader.parsedContent.refSeqId === -2 ? bd.RI() : sliceHeader.parsedContent.refSeqId; const readLength = bd.RL(); // if APDelta, AP is a delta from the previous record's alignmentStart let alignmentStart = bd.AP(); if (compressionScheme.APdelta) { alignmentStart = alignmentStart + cursors.lastAlignmentStart; } cursors.lastAlignmentStart = alignmentStart; const readGroupId = bd.RG(); let readNameRaw; if (compressionScheme.readNamesIncluded) { readNameRaw = bd.RN(); } let mate; let templateSize; let mateRecordNumber; // mate record if (record_ts_1.CramFlagsDecoder.isDetached(cramFlags)) { // note: the MF is a byte in 1.0, int32 in 2+, but once again this doesn't // matter for javascript const mateFlags = bd.MF(); let mateReadName; if (!compressionScheme.readNamesIncluded) { readNameRaw = bd.RN(); mateReadName = (0, util_ts_1.readNullTerminatedStringFromBuffer)(readNameRaw); } const mateSequenceId = bd.NS(); const mateAlignmentStart = bd.NP(); if (mateFlags || mateSequenceId > -1) { mate = { flags: mateFlags, sequenceId: mateSequenceId, alignmentStart: mateAlignmentStart, readName: mateReadName, }; } templateSize = bd.TS(); // set mate unmapped if needed if (record_ts_1.MateFlagsDecoder.isUnmapped(mateFlags)) { flags = record_ts_1.BamFlagsDecoder.setMateUnmapped(flags); } // set mate reversed if needed if (record_ts_1.MateFlagsDecoder.isOnNegativeStrand(mateFlags)) { flags = record_ts_1.BamFlagsDecoder.setMateReverseComplemented(flags); } } else if (record_ts_1.CramFlagsDecoder.isWithMateDownstream(cramFlags)) { mateRecordNumber = bd.NF() + recordNumber + 1; } // TODO: the aux tag parsing will have to be refactored if we want to support // cram v1 const TLindex = bd.TL(); if (TLindex < 0) { /* TODO: check nTL: TLindex >= compressionHeader.tagEncoding.size */ throw new errors_ts_1.CramMalformedError('invalid TL index'); } const tags = {}; // TN = tag names const TN = compressionScheme.getTagNames(TLindex); const ntags = TN.length; const shouldDecodeTags = decodeOptions?.decodeTags !== false; if (shouldDecodeTags) { for (let i = 0; i < ntags; i++) { const tagId = TN[i]; const tagData = boundTagDecoders[tagId](); const tagName = tagId[0] + tagId[1]; const tagType = tagId[2]; tags[tagName] = tagData === undefined ? undefined : typeof tagData === 'number' ? tagData : parseTagData(tagType, tagData); } } let readFeatures; let lengthOnRef; let mappingQuality; let qualityScores; let readBases = undefined; if (!record_ts_1.BamFlagsDecoder.isSegmentUnmapped(flags)) { // reading read features const readFeatureCount = bd.FN(); lengthOnRef = readLength; if (readFeatureCount) { const [features, refDelta] = decodeReadFeatures(alignmentStart, readFeatureCount, bd, rfSchema); readFeatures = features; lengthOnRef += refDelta; } if (Number.isNaN(lengthOnRef)) { console.warn(`${sequenceId}:${alignmentStart} record has invalid read features`); lengthOnRef = readLength; } // mapping quality mappingQuality = bd.MQ(); if (record_ts_1.CramFlagsDecoder.isPreservingQualityScores(cramFlags)) { qualityScores = decodeQualityScores(readLength, decodeBulkBytesRaw, bd.QS); } } else if (record_ts_1.CramFlagsDecoder.isDecodeSequenceAsStar(cramFlags)) { readBases = null; qualityScores = null; } else { readBases = decodeReadBases(readLength, decodeBulkBytesRaw, bd.BA); if (record_ts_1.CramFlagsDecoder.isPreservingQualityScores(cramFlags)) { qualityScores = decodeQualityScores(readLength, decodeBulkBytesRaw, bd.QS); } } return { readLength, sequenceId, cramFlags, flags, alignmentStart, readGroupId, readNameRaw, mate, templateSize, mateRecordNumber, readFeatures, lengthOnRef, mappingQuality, qualityScores, readBases, tags, uniqueId, }; } //# sourceMappingURL=decodeRecord.js.map