@gmod/cram
Version:
read CRAM files with pure Javascript
342 lines • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = decodeRecord;
const errors_ts_1 = require("../../errors.js");
const record_ts_1 = require("../record.js");
const sectionParsers_ts_1 = require("../sectionParsers.js");
/**
* given a Buffer, read a string up to the first null character
* @private
*/
function readNullTerminatedString(buffer) {
let r = '';
for (let i = 0; i < buffer.length && buffer[i] !== 0; i++) {
r += String.fromCharCode(buffer[i]);
}
return r;
}
/**
* parse a BAM tag's array value from a binary buffer
* @private
*/
function parseTagValueArray(buffer) {
const arrayType = String.fromCharCode(buffer[0]);
const dataView = new DataView(buffer.buffer);
const littleEndian = true;
const length = dataView.getUint32(1, littleEndian);
const array = new Array(length);
buffer = buffer.slice(5);
if (arrayType === 'c') {
const arr = new Int8Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 'C') {
const arr = new Uint8Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 's') {
const arr = new Int16Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 'S') {
const arr = new Uint16Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 'i') {
const arr = new Int32Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 'I') {
const arr = new Uint32Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else if (arrayType === 'f') {
const arr = new Float32Array(buffer.buffer);
for (let i = 0; i < length; i++) {
array[i] = arr[i];
}
}
else {
throw new Error(`unknown type: ${arrayType}`);
}
return array;
}
function parseTagData(tagType, buffer) {
if (tagType === 'Z') {
return readNullTerminatedString(buffer);
}
if (tagType === 'A') {
return String.fromCharCode(buffer[0]);
}
if (tagType === 'I') {
return new Uint32Array(buffer.buffer)[0];
}
if (tagType === 'i') {
return new Int32Array(buffer.buffer)[0];
}
if (tagType === 's') {
return new Int16Array(buffer.buffer)[0];
}
if (tagType === 'S') {
return new Uint16Array(buffer.buffer)[0];
}
if (tagType === 'c') {
return new Int8Array(buffer.buffer)[0];
}
if (tagType === 'C') {
return buffer[0];
}
if (tagType === 'f') {
return new Float32Array(buffer.buffer)[0];
}
if (tagType === 'H') {
return Number.parseInt(readNullTerminatedString(buffer).replace(/^0x/, ''), 16);
}
if (tagType === 'B') {
return parseTagValueArray(buffer);
}
throw new errors_ts_1.CramMalformedError(`Unrecognized tag type ${tagType}`);
}
function decodeReadFeatures(alignmentStart, readFeatureCount, decodeDataSeries, compressionScheme, majorVersion) {
let currentReadPos = 0;
let currentRefPos = alignmentStart - 1;
const readFeatures = new Array(readFeatureCount);
function decodeRFData([type, dataSeriesName]) {
const data = decodeDataSeries(dataSeriesName);
if (type === 'character') {
return String.fromCharCode(data);
}
else if (type === 'string') {
let r = '';
for (let i = 0; i < data.byteLength; i++) {
r += String.fromCharCode(data[i]);
}
return r;
}
else if (type === 'numArray') {
return Array.from(data);
}
// else if (type === 'number') {
// return data[0]
// }
return data;
}
for (let i = 0; i < readFeatureCount; i++) {
const code = String.fromCharCode(decodeDataSeries('FC'));
const readPosDelta = decodeDataSeries('FP');
// map of operator name -> data series name
const data1Schema = {
B: ['character', 'BA'],
S: ['string', majorVersion > 1 ? 'SC' : 'IN'], // IN if cram v1, SC otherwise
X: ['number', 'BS'],
D: ['number', 'DL'],
I: ['string', 'IN'],
i: ['character', 'BA'],
b: ['string', 'BB'],
q: ['numArray', 'QQ'],
Q: ['number', 'QS'],
H: ['number', 'HC'],
P: ['number', 'PD'],
N: ['number', 'RS'],
}[code];
if (!data1Schema) {
throw new errors_ts_1.CramMalformedError(`invalid read feature code "${code}"`);
}
let data = decodeRFData(data1Schema);
// if this is a tag with two data items, make the data an array and add the second item
const data2Schema = { B: ['number', 'QS'] }[code];
if (data2Schema) {
data = [data, decodeRFData(data2Schema)];
}
currentReadPos += readPosDelta;
const pos = currentReadPos;
currentRefPos += readPosDelta;
const refPos = currentRefPos;
// for gapping features, adjust the reference position for read features that follow
if (code === 'D' || code === 'N') {
currentRefPos += data;
}
else if (code === 'I' || code === 'S') {
currentRefPos -= data.length;
}
else if (code === 'i') {
currentRefPos -= 1;
}
readFeatures[i] = { code, pos, refPos, data };
}
return readFeatures;
}
function decodeRecord(slice, decodeDataSeries, compressionScheme, sliceHeader, coreDataBlock, blocksByContentId, cursors, majorVersion, recordNumber) {
let flags = decodeDataSeries('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 = decodeDataSeries('CF');
if (!(0, sectionParsers_ts_1.isMappedSliceHeader)(sliceHeader.parsedContent)) {
throw new Error('slice header not mapped');
}
const sequenceId = majorVersion > 1 && sliceHeader.parsedContent.refSeqId === -2
? decodeDataSeries('RI')
: sliceHeader.parsedContent.refSeqId;
const readLength = decodeDataSeries('RL');
// if APDelta, will calculate the true start in a second pass
let alignmentStart = decodeDataSeries('AP');
if (compressionScheme.APdelta) {
alignmentStart = alignmentStart + cursors.lastAlignmentStart;
}
cursors.lastAlignmentStart = alignmentStart;
const readGroupId = decodeDataSeries('RG');
let readName;
if (compressionScheme.readNamesIncluded) {
readName = readNullTerminatedString(decodeDataSeries('RN'));
}
let mateToUse;
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 = decodeDataSeries('MF');
let mateReadName;
if (!compressionScheme.readNamesIncluded) {
mateReadName = readNullTerminatedString(decodeDataSeries('RN'));
readName = mateReadName;
}
const mateSequenceId = decodeDataSeries('NS');
const mateAlignmentStart = decodeDataSeries('NP');
if (mateFlags || mateSequenceId > -1) {
mateToUse = {
mateFlags,
mateSequenceId,
mateAlignmentStart,
mateReadName,
};
}
templateSize = decodeDataSeries('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);
}
// detachedCount++
}
else if (record_ts_1.CramFlagsDecoder.isWithMateDownstream(cramFlags)) {
mateRecordNumber = decodeDataSeries('NF') + recordNumber + 1;
}
// TODO: the aux tag parsing will have to be refactored if we want to support
// cram v1
const TLindex = decodeDataSeries('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;
for (let i = 0; i < ntags; i++) {
const tagId = TN[i];
const tagName = tagId.slice(0, 2);
const tagType = tagId.slice(2, 3);
const tagData = compressionScheme
.getCodecForTag(tagId)
.decode(slice, coreDataBlock, blocksByContentId, cursors);
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 = decodeDataSeries('FN');
if (readFeatureCount) {
readFeatures = decodeReadFeatures(alignmentStart, readFeatureCount, decodeDataSeries, compressionScheme, majorVersion);
}
// compute the read's true span on the reference sequence, and the end
// coordinate of the alignment on the reference
lengthOnRef = readLength;
if (readFeatures) {
for (const { code, data } of readFeatures) {
if (code === 'D' || code === 'N') {
lengthOnRef += data;
}
else if (code === 'I' || code === 'S') {
lengthOnRef = lengthOnRef - data.length;
}
else if (code === 'i') {
lengthOnRef = lengthOnRef - 1;
}
}
}
if (Number.isNaN(lengthOnRef)) {
console.warn(`${readName || `${sequenceId}:${alignmentStart}`} record has invalid read features`);
lengthOnRef = readLength;
}
// mapping quality
mappingQuality = decodeDataSeries('MQ');
if (record_ts_1.CramFlagsDecoder.isPreservingQualityScores(cramFlags)) {
qualityScores = new Array(readLength);
for (let i = 0; i < qualityScores.length; i++) {
qualityScores[i] = decodeDataSeries('QS');
}
}
}
else if (record_ts_1.CramFlagsDecoder.isDecodeSequenceAsStar(cramFlags)) {
readBases = null;
qualityScores = null;
}
else {
const bases = new Array(readLength);
for (let i = 0; i < bases.length; i++) {
bases[i] = decodeDataSeries('BA');
}
readBases = String.fromCharCode(...bases);
if (record_ts_1.CramFlagsDecoder.isPreservingQualityScores(cramFlags)) {
qualityScores = new Array(readLength);
for (let i = 0; i < bases.length; i++) {
qualityScores[i] = decodeDataSeries('QS');
}
}
}
return {
readLength,
sequenceId,
cramFlags,
flags,
alignmentStart,
readGroupId,
readName,
mateToUse,
templateSize,
mateRecordNumber,
readFeatures,
lengthOnRef,
mappingQuality,
qualityScores,
readBases,
tags,
};
}
//# sourceMappingURL=decodeRecord.js.map