diff --git a/client/app/components/pages/GeneHome.vue b/client/app/components/pages/GeneHome.vue index 2c2a62e7..0a2a289d 100644 --- a/client/app/components/pages/GeneHome.vue +++ b/client/app/components/pages/GeneHome.vue @@ -934,6 +934,8 @@ export default { 'Not significant': 'not-sig', 'Not reviewed': 'not-reviewed' }, + + userFlaggedVariantsToApply: null, //allGenes: allGenesData, @@ -2192,13 +2194,19 @@ export default { window.cacheHelper = self.cacheHelper; self.cacheHelper.on("geneAnalyzed", function(theGene) { self.refreshCoverageCounts() - if (self.selectedGene && self.selectedGene.hasOwnProperty("gene_name") + self.promiseRefreshUserFlaggedState(theGene) + .then(function() { + if (self.selectedGene && self.selectedGene.hasOwnProperty("gene_name") && theGene.gene_name === self.selectedGene.gene_name) { - self.promiseLoadData() - .catch(function(error) { - self.addAlert("error", error, theGene) - }) - } + self.promiseLoadData() + .catch(function(error) { + self.addAlert("error", error, theGene) + }) + } + }) + .catch(function(error) { + self.addAlert("error", error, theGene) + }) }); self.cacheHelper.on("geneNotAnalyzed", function(geneName) { console.log("Gene not analyzed " + geneName) @@ -2248,9 +2256,23 @@ export default { this.resume = val; }, - promiseClearCache: function() { + promiseClearCache: function(options) { let self = this; - + + if (options && options.saveUserFlaggedState) { + // Keep track of user flagged variants. We will need to + // re-apply the variant interpretation and notes + // after all genes have been re-analyzed. + if (self.cohortModel && self.cohortModel.flaggedVariants) { + self.userFlaggedVariantsToApply = self.cohortModel.flaggedVariants.filter(function(variant) { + return (variant.notes && variant.notes.length > 0) || + (variant.interpretation && variant.interpretation.length > 0); + }) + } + } else { + self.userFlaggedVariantsToApply = null; + } + this.clearFilter(); self.cohortModel.clearFlaggedVariants(); @@ -2904,26 +2926,29 @@ export default { .then(function(data) { if (!data.isValidBuild) { self.addAlert('error', 'The genome build change to ' + buildName + ' resulted in a problem. ' + data.message) - } - }) - - self.addAlert('info', 'Genome build
' + buildName + '
selected.'); - self.onShowSnackbar({message: 'Genes will be reanalyzed based on genome build ' - + buildName, timeout: 3000}); + return; + } else { + self.addAlert('info', 'Genome build
' + buildName + '
selected.'); + self.onShowSnackbar({message: 'Genes will be reanalyzed based on genome build ' + + buildName, timeout: 3000}); - self.genomeBuildHelper.setCurrentBuild(buildName) - self.setUrlParameters() - self.promiseClearCache() - .then(function() { - return self.promiseResetAllGenes() - }) - .then(function() { - self.showLeftPanelForGenes() + self.genomeBuildHelper.setCurrentBuild(buildName) + self.setUrlParameters() + self.promiseClearCache({'saveUserFlaggedState': true}) + .then(function() { + return self.promiseResetAllGenes() + }) + .then(function() { + self.showLeftPanelForGenes() - if (self.geneModel.geneNames && self.geneModel.geneNames.length > 0) { - self.onAnalyzeAll() + if (self.geneModel.geneNames && self.geneModel.geneNames.length > 0) { + self.onAnalyzeAll() + } + }) + } }) + }, onGeneSourceSelected: function(theGeneSource) { var self = this; @@ -2933,7 +2958,7 @@ export default { self.onShowSnackbar({message: 'Genes will be re-analyzed based on ' + theGeneSource + ' transcripts', timeout: 3000}); - self.promiseClearCache() + self.promiseClearCache({'saveUserFlaggedState': true}) .then(function() { return self.promiseResetAllGenes() }) @@ -2962,7 +2987,7 @@ export default { self.onShowSnackbar({message: 'Genes will be re-analyzed to only annotate all variants in genes, including intronic regions ', timeout: 3000}); } - self.promiseClearCache() + self.promiseClearCache({'saveUserFlaggedState': true}) .then(function() { return self.promiseResetAllGenes() }) @@ -2992,9 +3017,11 @@ export default { let self = this; self.geneModel.geneRegionBuffer = theGeneRegionBuffer; // We have to clear the cache since the gene regions change - self.promiseClearCache() + self.promiseClearCache({'saveUserFlaggedState': true}) .then(function() { + self.promiseRefreshUserFlaggedState(self.selectedGene) self.onGeneSelected(self.selectedGene.gene_name); + }) setTimeout(function () { @@ -3417,6 +3444,8 @@ export default { self.geneModel.geneObjects = {}; self.geneModel.geneToLatestTranscript = {}; self.geneModel.clearAllGenes(); + + self.cohortModel.flaggedVariants = []; self.cacheHelper.geneToAltTranscript = {}; @@ -3428,6 +3457,8 @@ export default { self.applyGenesImpl(genesToReapply.join(","), options, function() { + console.log("user flagged variants to apply") + console.log(self.userFlaggedVariantsToApply) resolve(); }); }) @@ -3870,7 +3901,8 @@ export default { // If the variant isn't in the filtered variants list, // mark it as 'user flagged' - if (self.cohortModel.getFlaggedVariant(variant) == null) { + let theFlaggedVariant = self.cohortModel.getFlaggedVariant(variant) + if (theFlaggedVariant == null) { variant.gene = this.selectedGene; variant.transcript = this.selectedTranscript; self.cohortModel.addUserFlaggedVariant(self.selectedGene, self.selectedTranscript, variant); @@ -4410,6 +4442,149 @@ export default { self.onSendFiltersToClin(); } }) + }, + promiseRefreshUserFlaggedState: function(geneObject) { + let self = this; + + return new Promise(function(resolve, reject) { + + let geneName = geneObject.gene_name; + + if (self.userFlaggedVariantsToApply == null || self.userFlaggedVariantsToApply.length == 0 || + self.cohortModel.flaggedVariants == null || self.cohortModel.flaggedVariants.length == 0) { + resolve(); + } + + // Add any user flagged variants + let userFlaggedVariantsToAdd = self.userFlaggedVariantsToApply.filter(function(source) { + if (source.gene && source.gene.gene_name) { + let matchesGene = source.gene.gene_name == geneName; + let matchingFlaggedVariants = self.cohortModel.flaggedVariants.filter(function(target) { + let foundMatch = ( + self.globalApp.utility.stripRefName(source.chrom) == self.globalApp.utility.stripRefName(target.chrom) + && source.start == target.start + && source.ref == target.ref + && source.alt == target.alt); + return foundMatch; + }) + return (matchesGene && source.isUserFlagged && matchingFlaggedVariants.length == 0) + } else { + return false; + } + }) + + // Create the promises to add the user flagged variants + var promises = userFlaggedVariantsToAdd.map(function(source) { + return new Promise(function(resolve1, reject1) { + let theSource = source; + let theGeneObject = geneObject; + let theTranscript = self.geneModel.getCanonicalTranscript(theGeneObject) + self.cohortModel.getProbandModel().promiseGetMatchingVariant(theSource, theGeneObject, theTranscript) + .then(function(matchingVariant) { + if (matchingVariant) { + matchingVariant.isFlagged = true; + matchingVariant.isUserFlagged = true; + matchingVariant.filtersPassed = ['reviewed'] + matchingVariant.filtersPassedAll = ['reviewed'] + matchingVariant.notes = theSource.notes; + matchingVariant.interpretation = theSource.interpretation; + self.cohortModel.promiseAddUserFlaggedVariant(theGeneObject, theTranscript, matchingVariant) + .then(function() { + resolve1(); + }) + .catch(function(error) { + reject1('Failed to flag user flagged variant due to error: ' + error) + }) + } else { + let theNotes = theSource.notes && theSource.notes.length > 0 ? + theSource.notes.map(function(n){ + return n.note + }).join(", ") : ""; + self.addAlert('error', + 'Failed to flag ' + theGeneObject.gene_name + + ' variant ' + theSource.chrom + ":" + theSource.start + + '. Unable to capture interpretation and notes for variant.', + theGeneObject.gene_name, + ['interpretation: ' + theSource.interpretation, + 'notes: ' + theNotes], + {'showAlertPanel': true}) + resolve1(); + } + }) + }) + }) + + Promise.all(promises) + .then(function() { + // Get the tuples of source variant and target variant + let flaggedVariantsToRefresh = self.userFlaggedVariantsToApply.map(function(source) { + if (source.gene && source.gene.gene_name) { + let matchesGene = source.gene.gene_name == geneName; + let matchingFlaggedVariants = self.cohortModel.flaggedVariants.filter(function(target) { + let foundMatch = ( + self.globalApp.utility.stripRefName(source.chrom) == self.globalApp.utility.stripRefName(target.chrom) + && source.start == target.start + && source.ref == target.ref + && source.alt == target.alt); + return foundMatch; + }) + return {'source': source, + 'matchesGene': matchesGene, + 'matchingFlaggedVariants': matchingFlaggedVariants} + } else { + return {'source': source, + 'matchesGene': false, + 'matchingFlaggedVariants': []} + } + }).filter(function(matchInfo) { + return matchInfo.matchesGene; + }) + + // Iterate through the tuples of source and target variants and refresh the target + // variant notes and interpretation based on the source. Issue a warning + // if we have a source variant for a gene an no matching flagged variant. + flaggedVariantsToRefresh.forEach(function(matchInfo) { + if (matchInfo.matchingFlaggedVariants && matchInfo.matchingFlaggedVariants.length > 0) { + matchInfo.matchingFlaggedVariants.forEach(function(target) { + console.log("setting user flagged state for variant " + target.start + " for gene " + target.gene.gene_name) + target.interpretation = matchInfo.source.interpretation; + target.notes = matchInfo.source.notes; + var index = self.userFlaggedVariantsToApply.indexOf(matchInfo.source); + if (index !== -1) { + self.userFlaggedVariantsToApply.splice(index, 1); + } + }) + } else { + let theNotes = ""; + if (matchInfo.source.notes) { + theNotes = matchInfo.source.notes.map(function(n){ + return n.note + }).join(", ") + } + self.addAlert('error', + 'Cannot refresh the variant interpretation and notes for ' + geneName + ' variant ' + + matchInfo.source.chrom + ':' + matchInfo.source.start + ' ' + matchInfo.source.ref + '>' + matchInfo.source.alt + + ' because the variant no longer matches any filter criteria.', + geneName, + ['interpretation: ' + matchInfo.source.interpretation, + 'notes: ' + theNotes], + {'showAlertPanel': true}); + } + }) + + resolve(); + + }) + .catch(function(error) { + reject(error) + }) + + + + + }) + + }, refreshCoverageCounts: function() { let self = this; @@ -4548,7 +4723,7 @@ export default { this.featureMatrixModel.isBasicMode = false; this.featureMatrixModel.isSimpleMode = false; this.filterModel.isBasicMode = false; - this.promiseClearCache() + this.promiseClearCache({'saveUserFlaggedState': true}) .then(function() { self.calcFeatureMatrixWidthPercent(); self.onFilesLoaded(true, true, function() { @@ -4577,7 +4752,7 @@ export default { this.featureMatrixModel.isBasicMode = false; this.featureMatrixModel.isSimpleMode = true; this.filterModel.isBasicMode = false; - this.promiseClearCache() + this.promiseClearCache({'saveUserFlaggedState': true}) .then(function() { self.calcFeatureMatrixWidthPercent(); self.onFilesLoaded(true, true, function() { diff --git a/client/app/models/CohortModel.js b/client/app/models/CohortModel.js index b5f665dc..352824b9 100644 --- a/client/app/models/CohortModel.js +++ b/client/app/models/CohortModel.js @@ -1886,11 +1886,7 @@ class CohortModel { resolve(dangerObject); }). catch(function(error) { - if (error.indexOf(geneName) >= 0 ) { - self.dispatch.alertIssued('error', error, theGeneName); - } else { - self.dispatch.alertIssued('error', error + " for gene " + theGeneName, theGeneName); - } + self.dispatch.alertIssued('error', error, theGeneName); reject(error); }) }) @@ -2498,7 +2494,10 @@ class CohortModel { var self = this; this._recacheForFlaggedVariant(theGene, theTranscript, variant, options); } - + + // TODO: This method should be deprecated and replaced with promiseAddUserFlaggedVariant. + // However, there are complications due to blocking, so this should be + // undertaken when we migrate to Vue.js 3. addUserFlaggedVariant(theGene, theTranscript, variant) { var self = this; variant.isFlagged = true; @@ -2511,6 +2510,31 @@ class CohortModel { self._recacheForFlaggedVariant(theGene, theTranscript, variant, {summarizeDanger: true}); + } + + promiseAddUserFlaggedVariant(theGene, theTranscript, variant) { + var self = this; + return new Promise(function(resolve, reject) { + let theVariant = variant; + theVariant.isFlagged = true; + theVariant.isUserFlagged = true; + if (theVariant.filtersPassed == null) { + theVariant.filtersPassed = []; + } + theVariant.filtersPassed.push("userFlagged"); + theVariant.featureClass = "flagged"; + + self._promiseRecacheForFlaggedVariant(theGene, theTranscript, theVariant, {summarizeDanger: true}) + .then(function() { + resolve(); + }) + .catch(function(error) { + reject(error) + }) + + }) + + } removeFilterPassed(variant, filterName) { @@ -2580,7 +2604,53 @@ class CohortModel { this.flaggedVariants.splice(index, 1); } } + + _promiseRecacheForFlaggedVariant(theGene, theTranscript, variant, options) { + let self = this; + return new Promise(function(resolve, reject) { + let theVariant = variant; + self.getProbandModel().promiseGetVcfData(theGene, theTranscript) + .then(function(data) { + let cachedVcfData = data.vcfData; + cachedVcfData.features.forEach(function(v) { + var matches = ( + self.globalApp.utility.stripRefName(v.chrom) == self.globalApp.utility.stripRefName(theVariant.chrom) + && v.start == theVariant.start + && v.ref == theVariant.ref + && v.alt == theVariant.alt); + + if (matches) { + v.isFlagged = theVariant.isFlagged + v.isUserFlagged = theVariant.isUserFlagged; + v.filtersPassed = theVariant.filtersPassed; + v.interpretation = theVariant.interpretation; + v.notes = theVariant.notes; + } + }); + self.getProbandModel()._promiseCacheData(cachedVcfData, CacheHelper.VCF_DATA, data.gene.gene_name, data.transcript) + .then(function() { + if (options && options.summarizeDanger) { + // Now summarize the danger for the selected gene + self.promiseSummarizeDanger(theGene, theTranscript, cachedVcfData, null) + .then(function() { + resolve(); + }) + .catch(function(error) { + reject(error) + }) + } else { + resolve(); + } + }) + + }); + + }) + } + // TODO: This method should be deprecated and replaced with _promiseReachesForFlaggedVariant. + // However, there are complications due to blocking, so this should be + // undertaken when we migrate to Vue.js 3. _recacheForFlaggedVariant(theGene, theTranscript, variant, options) { let self = this; self.getProbandModel().promiseGetVcfData(theGene, theTranscript) diff --git a/client/app/models/GeneModel.js b/client/app/models/GeneModel.js index 9bf21d0f..7b9f0caa 100644 --- a/client/app/models/GeneModel.js +++ b/client/app/models/GeneModel.js @@ -818,6 +818,12 @@ class GeneModel { getCanonicalTranscript(theGeneObject) { let me = this; var geneObject = theGeneObject != null ? theGeneObject : window.gene; + + if (geneObject == null || geneObject.gene_name == null) { + console.log("Null gene object sent into method GeneModel.getCanonicalTranscript") + return null; + + } var canonical; if (geneObject.transcripts == null || geneObject.transcripts.length == 0) { diff --git a/client/app/models/SampleModel.js b/client/app/models/SampleModel.js index 903d8872..928668ac 100644 --- a/client/app/models/SampleModel.js +++ b/client/app/models/SampleModel.js @@ -202,6 +202,8 @@ class SampleModel { if (geneObject == null) { reject("Empty geneObject in SampleModel.promiseGetVcfData()"); } + let theGeneObject = geneObject; + let theTranscript = selectedTranscript; // If only alignments have specified, but not variant files, we will need to use the // getBamRefName function instead of the getVcfRefName function. @@ -224,24 +226,24 @@ class SampleModel { geneObject.end == me[dataKind].end && geneObject.strand == me[dataKind].strand) { theVcfData = me[dataKind]; - resolve({model: me, vcfData: theVcfData}); + resolve({model: me, vcfData: theVcfData, 'gene': theGeneObject, 'transcript': theTranscript}); } } if (theVcfData == null) { // Find vcf data in cache - me._promiseGetData(dataKind, geneObject.gene_name, selectedTranscript) + me._promiseGetData(dataKind, theGeneObject.gene_name, theTranscript) .then(function(data) { if (data != null && data != '') { me[dataKind] = data; theVcfData = data; - resolve({model: me, vcfData: theVcfData}); + resolve({model: me, vcfData: theVcfData, 'gene': theGeneObject, 'transcript': theTranscript}); } else { // If the vcf data is null, see if there are called variants in the cache. If so, // copy the called variants into the vcf data. if (whenEmptyUseFbData && me.isAlignmentsOnly()) { - me.promiseGetFbData(geneObject, selectedTranscript) + me.promiseGetFbData(theGeneObject, theTranscript) .then(function(theFbData) { // If no variants are loaded, create a dummy vcfData with 0 features if (theFbData && theFbData.features) { @@ -263,16 +265,16 @@ class SampleModel { } - resolve({model: me, vcfData: theVcfData}); + resolve({model: me, vcfData: theVcfData, 'gene': theGeneObject, 'transcript': theTranscript}); }) .catch(function (error) { - let msg = "Problem caching data in SampleModel.promiseGetVariantExtraAnnotations()." + let msg = "Problem getting fb data in SampleModel.promiseGetVcfData()." console.log(msg); reject(error); }); } else { - resolve({model: me, vcfData: theVcfData}); + resolve({model: me, vcfData: theVcfData, 'gene': theGeneObject, 'transcript': theTranscript}); } }