UNPKG

myseq-vcf

Version:

Query local or remote Tabix-indexed VCFs by genomic position

196 lines (146 loc) 6.54 kB
"use strict"; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } const findIndex = require('lodash/findIndex'); const last = require('lodash/last'); const isString = require('lodash/isString'); const defer = require('promise-defer'); const { ContigNotInIndexError } = require('../util/Errors'); const TabixIndexedFile = require('./TabixIndexedFile'); const Ref = require('../features/ReferenceGenome'); const VCFVariant = require('../features/VCFVariant'); class VCFSource { constructor(source, reference) { _defineProperty(this, "_source", void 0); _defineProperty(this, "_reference", void 0); _defineProperty(this, "_samples", void 0); this._source = source; const referenceResolver = defer(); if (reference !== undefined) { this._reference = Promise.resolve(reference); } else { // Will only be used if reference is not specified this._reference = referenceResolver.promise; } this._samples = this._source.header().then(headerLines => { if (!headerLines[0].startsWith('##fileformat=VCF')) { throw new Error('Source is not a valid VCF file'); } // 1. Look for a reference line const refIdx = findIndex(headerLines, line => line.startsWith('##reference=')); if (refIdx !== -1) { // Do we know this reference file or string? const referenceField = headerLines[refIdx].substring(12); const referenceFrom = Ref.referenceFromFile(referenceField) || Ref.referenceFromShortName(referenceField); if (referenceFrom !== undefined) { referenceResolver.resolve(referenceFrom); } } // 2. Parse contig lines to infer reference const contigs = headerLines.filter(line => line.startsWith('##contig=')).map(line => line.match(/ID=([^,>]+)/)).filter(match => match && match.length === 2).map(match => match[1]); if (contigs.length > 0) { const referenceFrom = Ref.referenceFromContigs(contigs); if (referenceFrom !== undefined) { referenceResolver.resolve(referenceFrom); } } // -OR- set hg19 as a default (will be a no-op if referenceResolver is already resolved) referenceResolver.resolve(Ref.hg19Reference); // Last line should be column labels const columns = last(headerLines).split('\t'); if (columns[0] !== '#CHROM' || columns.length < 8) { throw new Error('Invalid column header line (#CHROM...)'); } return columns.slice(9); }); } reference() { return this._reference; } samples() { return this._samples; } /** * Normalize (array of) string regions to objects describing the sorted non-overlapping * regions. * @param {[type]} regionOrRegions (Array of) contig:pos-end region strings * @return {Promise<Array<Region | Region>>} (Array of) region object with ctg, pos and end fields */ normalizeRegions(regionOrRegions) { if (Array.isArray(regionOrRegions)) { return Promise.all(regionOrRegions.map(region => this.normalizeRegions(region))).then(regions => Promise.all([regions, this._reference])).then(([regions, reference]) => { regions.sort((aRegion, bRegion) => { // eslint-disable-line arrow-body-style // Sort in reference order return reference.compareContig(aRegion.ctg, bRegion.ctg) || aRegion.pos - bRegion.pos || aRegion.end - bRegion.end; }); // Merge overlapping regions return regions.reduce((prev, curr) => { if (prev.length) { const tail = prev[prev.length - 1]; if (curr.ctg === tail.ctg && curr.pos >= tail.pos && curr.pos <= tail.end) { tail.end = Math.max(tail.end, curr.end); return prev; } } prev.push(curr); return prev; }, []); }); } if (isString(regionOrRegions)) { const [ctg, pos, end] = regionOrRegions.split(/[:-]/, 3); return this._reference.then(ref => ref.normalizeContig(ctg)).then(normCtg => ({ ctg: normCtg, pos: parseInt(pos, 10), end: parseInt(end || pos, 10) })); } throw new Error('Invalid region(s)'); } /** * Query for variants overlapping genomic region * @param {string} ctg Contig * @param {number} pos Inclusive start of genomic region * @param {number} end Inclusive end of genomic region * @return {Promise<Array<VCFVariant>>} Array of VCFVariants overlapping region */ variants(ctg, pos, end) { const queryResults = this._reference.then(ref => ref.normalizeContig(ctg)).then(normalizedCtg => this._source.records(normalizedCtg, pos, end)); return Promise.all([queryResults, this._samples]).then(([records, samples]) => records.map(record => new VCFVariant(record, samples)), err => { if (err instanceof ContigNotInIndexError) { return []; } throw err; }); } _synthVariant(ctg, pos, ref, alt) { return Promise.all([this._reference, this._samples]).then(([reference, samples]) => { let synthRecord = `${reference.normalizeContig(ctg)}\t${pos}\t.\t${ref}\t${alt}\t.\t.\t.`; if (samples.length > 0) { synthRecord += `\tGT${'\t0/0'.repeat(samples.length)}`; } return new VCFVariant(synthRecord, samples, true /* isSynth */ ); }); } /** * Query for single variant * @param {string} ctg Contig * @param {number} pos VCF position * @param {string} ref Reference allele * @param {string} alt Alternate allele * @param {boolean} assumeRefRef If variant not found, synthesize variant with REF/REF genotype * @return {Promise<VCFVariant>} Found (or synthetic) variant or undefined if * assumeRefRef is false and variant is not found */ variant(ctg, pos, ref, alt, assumeRefRef = false) { return this.variants(ctg, pos, pos).then(variants => { // Filter for exact position and allele match, if none found and assumeRefRef // is true, synthesize a variant with a Ref/Ref genotype const foundVariant = variants.filter(variant => variant.ref === ref && variant.alt.indexOf(alt) !== -1).shift(); if (!foundVariant && assumeRefRef) { return this._synthVariant(ctg, pos, ref, alt); } return foundVariant; }); } } module.exports = VCFSource;