From c6d5255c09855fc1b0d03a085da9988a75cd3898 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 19 Feb 2019 11:15:34 +0000 Subject: [PATCH] JAL-3187 hacks to get peptide variant in to Jmol hover tooltip --- src/jalview/analysis/AlignmentUtils.java | 3 +- src/jalview/appletgui/SeqPanel.java | 4 +- src/jalview/datamodel/MappedFeatures.java | 154 ++++++++++++++++++++ src/jalview/ext/jmol/JalviewJmolBinding.java | 35 +++-- src/jalview/gui/SeqPanel.java | 79 +++++++++- .../renderer/seqfeatures/FeatureRenderer.java | 9 +- src/jalview/structure/SequenceListener.java | 10 +- .../structure/StructureSelectionManager.java | 19 ++- .../seqfeatures/FeatureRendererModel.java | 30 +++- 9 files changed, 312 insertions(+), 31 deletions(-) create mode 100644 src/jalview/datamodel/MappedFeatures.java diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 7a082be..0d2e002 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -2425,7 +2425,8 @@ public class AlignmentUtils static int computePeptideVariants(SequenceI peptide, int peptidePos, List[] codonVariants) { - String residue = String.valueOf(peptide.getCharAt(peptidePos - 1)); + String residue = String + .valueOf(peptide.getCharAt(peptidePos - peptide.getStart())); int count = 0; String base1 = codonVariants[0].get(0).base; String base2 = codonVariants[1].get(0).base; diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index e07dae6..7c85d12 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -744,7 +744,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (av.isFollowHighlight()) { @@ -761,7 +761,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } setStatusMessage(results); seqCanvas.highlightSearchResults(results); - + return null; } @Override diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java new file mode 100644 index 0000000..07d3857 --- /dev/null +++ b/src/jalview/datamodel/MappedFeatures.java @@ -0,0 +1,154 @@ +package jalview.datamodel; + +import jalview.io.gff.Gff3Helper; +import jalview.schemes.ResidueProperties; +import jalview.util.MappingUtils; +import jalview.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A data bean to hold a list of mapped sequence features (e.g. CDS features + * mapped from protein), and the mapping between the sequences + * + * @author gmcarstairs + */ +public class MappedFeatures +{ + /* + * the mapping from CDS to peptide + */ + public final Mapping mapping; + + /** + * the CDS sequence mapped to + */ + public final SequenceI fromSeq; + + /* + * the residue position in the peptide sequence + */ + public final int fromPosition; + + /* + * the peptide residue at the position + */ + public final char fromResidue; + + /* + * features on CDS that overlap the codon positions + */ + public final List features; + + /** + * Constructor + * + * @param theMapping + * @param pos + * @param res + * @param theFeatures + */ + public MappedFeatures(Mapping theMapping, SequenceI from, int pos, + char res, + List theFeatures) + { + mapping = theMapping; + fromSeq = from; + fromPosition = pos; + fromResidue = res; + features = theFeatures; + } + + /** + * Computes and returns a (possibly empty) list of HGVS notation peptide + * variants derived from codon allele variants + * + * @return + */ + public List findProteinVariants() + { + List vars = new ArrayList<>(); + + /* + * determine canonical codon + */ + int[] codonPos = MappingUtils.flattenRanges( + mapping.getMap().locateInFrom(fromPosition, fromPosition)); + if (codonPos.length != 3) + { + // error + return vars; + } + final char[] baseCodon = new char[3]; + int cdsStart = fromSeq.getStart(); + baseCodon[0] = fromSeq.getCharAt(codonPos[0] - cdsStart); + baseCodon[1] = fromSeq.getCharAt(codonPos[1] - cdsStart); + baseCodon[2] = fromSeq.getCharAt(codonPos[2] - cdsStart); + + // todo avoid duplication of code in AlignmentUtils.buildDnaVariantsMap + + for (SequenceFeature sf : features) + { + int cdsPos = sf.getBegin(); + if (cdsPos != sf.getEnd()) + { + // not handling multi-locus variant features + continue; + } + if (cdsPos != codonPos[0] && cdsPos != codonPos[1] + && cdsPos != codonPos[2]) + { + // e.g. feature on intron within spliced codon! + continue; + } + + String alls = (String) sf.getValue(Gff3Helper.ALLELES); + if (alls == null) + { + continue; + } + String from3 = StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet + .get(String.valueOf(fromResidue))); + + /* + * make a peptide variant for each SNP allele + * e.g. C,G,T gives variants G and T for base C + */ + String[] alleles = alls.toUpperCase().split(","); + for (String allele : alleles) + { + allele = allele.trim().toUpperCase(); + if (allele.length() > 1) + { + continue; // multi-locus variant + } + char[] variantCodon = new char[3]; + variantCodon[0] = baseCodon[0]; + variantCodon[1] = baseCodon[1]; + variantCodon[2] = baseCodon[2]; + + /* + * poke variant base into canonical codon + */ + int i = cdsPos == codonPos[0] ? 0 : (cdsPos == codonPos[1] ? 1 : 2); + variantCodon[i] = allele.toUpperCase().charAt(0); + String codon = new String(variantCodon); + String peptide = ResidueProperties.codonTranslate(codon); + if (fromResidue != peptide.charAt(0)) + { + String to3 = StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet.get(peptide)); + String var = "p." + from3 + fromPosition + to3; + if (!vars.contains(var)) + { + vars.add(var); + } + } + } + } + + return vars; + } +} diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index a5b1110..e85d387 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -50,6 +50,7 @@ import java.util.BitSet; import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import java.util.Vector; import org.jmol.adapter.smarter.SmarterJmolAdapter; @@ -65,6 +66,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel implements JmolStatusListener, JmolSelectionListener, ComponentListener { + private String lastMessage; + boolean allChainsSelected = false; /* @@ -89,8 +92,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel String lastCommand; - String lastMessage; - boolean loadedInline; StringBuffer resetLastRes = new StringBuffer(); @@ -822,7 +823,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel viewer.openStringInline(string); } - public void mouseOverStructure(int atomIndex, String strInfo) + protected void mouseOverStructure(int atomIndex, final String strInfo) { int pdbResNum; int alocsep = strInfo.indexOf("^"); @@ -876,7 +877,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel try { // recover PDB filename for the model hovered over. - int mnumber = new Integer(mdlId).intValue() - 1; + int mnumber = Integer.valueOf(mdlId).intValue() - 1; if (_modelFileNameMap != null) { int _mp = _modelFileNameMap.length - 1; @@ -903,18 +904,34 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } catch (Exception e) { } - ; } - if (lastMessage == null || !lastMessage.equals(strInfo)) + + /* + * highlight position on alignment(s); if some text is returned, + * show this as a second line on the structure hover tooltip + */ + String label = getSsm().mouseOverStructure(pdbResNum, chainId, + pdbfilename); + if (label != null) { - getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename); + StringTokenizer toks = new StringTokenizer(strInfo, " "); + StringBuilder sb = new StringBuilder(); + sb.append("select ").append(String.valueOf(pdbResNum)).append(":") + .append(chainId).append("/1"); + sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ") + .append(toks.nextToken()); + sb.append("|").append(label).append("\""); + evalStateCommand(sb.toString()); } - - lastMessage = strInfo; } public void notifyAtomHovered(int atomIndex, String strInfo, String data) { + if (strInfo.equals(lastMessage)) + { + return; + } + lastMessage = strInfo; if (data != null) { System.err.println("Ignoring additional hover info: " + data diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index cf0991e..c794e57 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -28,6 +28,7 @@ import jalview.commands.EditCommand.Edit; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; @@ -60,6 +61,7 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -715,11 +717,11 @@ public class SeqPanel extends JPanel * the start of the highlighted region. */ @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (results == null || results.equals(lastSearchResults)) { - return; + return null; } lastSearchResults = results; @@ -745,6 +747,74 @@ public class SeqPanel extends JPanel { setStatusMessage(results); } + return results.isEmpty() ? null : getHighlightInfo(results); + } + + /** + * temporary hack: answers a message suitable to show on structure hover + * label. This is normally null. It is a peptide variation description if + *
    + *
  • results are a single residue in a protein alignment
  • + *
  • there is a mapping to a coding sequence (codon)
  • + *
  • there are one or more SNP variant features on the codon
  • + *
+ * in which case the answer is of the format (e.g.) "p.Glu388Asp" + * + * @param results + * @return + */ + private String getHighlightInfo(SearchResultsI results) + { + /* + * ideally, just find mapped CDS (as we don't care about render style here); + * for now, go via split frame complement's FeatureRenderer + */ + AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); + if (complement == null) + { + return null; + } + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + + int j = results.getSize(); + List infos = new ArrayList<>(); + for (int i = 0; i < j; i++) + { + SearchResultMatchI match = results.getResults().get(i); + int pos = match.getStart(); + if (pos == match.getEnd()) + { + SequenceI seq = match.getSequence(); + SequenceI ds = seq.getDatasetSequence() == null ? seq + : seq.getDatasetSequence(); + MappedFeatures mf = fr2 + .findComplementFeaturesAtResidue(ds, pos); + List pv = mf.findProteinVariants(); + for (String s : pv) + { + if (!infos.contains(s)) + { + infos.addAll(pv); + } + } + } + } + + if (infos.isEmpty()) + { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String info : infos) + { + if (sb.length() > 0) + { + sb.append("|"); + } + sb.append(info); + } + return sb.toString(); } @Override @@ -861,8 +931,9 @@ public class SeqPanel extends JPanel .getCodingComplement(); AlignFrame af = Desktop.getAlignFrameFor(complement); FeatureRendererModel fr2 = af.getFeatureRenderer(); - features = fr2.findComplementFeaturesAtResidue(sequence, pos); - seqARep.appendFeatures(tooltipText, pos, features, fr2); + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence, + pos); + seqARep.appendFeatures(tooltipText, pos, mf.features, fr2); } } } diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 78f4989..39d705b 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -22,6 +22,7 @@ package jalview.renderer.seqfeatures; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.Range; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; @@ -454,9 +455,9 @@ public class FeatureRenderer extends FeatureRendererModel for (int pos = visiblePositions.start; pos <= visiblePositions.end; pos++) { int column = seq.findIndex(pos); - List features = fr2 + MappedFeatures mf = fr2 .findComplementFeaturesAtResidue(seq, pos); - for (SequenceFeature sf : features) + for (SequenceFeature sf : mf.features) { FeatureColourI fc = fr2.getFeatureStyle(sf.getType()); Color featureColour = fr2.getColor(sf, fc); @@ -560,11 +561,11 @@ public class FeatureRenderer extends FeatureRendererModel AlignViewportI complement = av.getCodingComplement(); AlignFrame af = Desktop.getAlignFrameFor(complement); FeatureRendererModel fr2 = af.getFeatureRenderer(); - List features = fr2.findComplementFeaturesAtResidue( + MappedFeatures mf = fr2.findComplementFeaturesAtResidue( seq, seq.findPosition(column - 1)); ReverseListIterator it = new ReverseListIterator<>( - features); + mf.features); while (it.hasNext()) { SequenceFeature sf = it.next(); diff --git a/src/jalview/structure/SequenceListener.java b/src/jalview/structure/SequenceListener.java index 81ff739..f8c5bea 100644 --- a/src/jalview/structure/SequenceListener.java +++ b/src/jalview/structure/SequenceListener.java @@ -28,7 +28,15 @@ public interface SequenceListener // TODO remove this? never called on SequenceListener type public void mouseOverSequence(SequenceI sequence, int index, int pos); - public void highlightSequence(SearchResultsI results); + /** + * Highlights any position(s) represented by the search results and + * (optionally) returns an informative message about the position(s) + * higlighted + * + * @param results + * @return + */ + public String highlightSequence(SearchResultsI results); // TODO remove this? never called public void updateColours(SequenceI sequence, int index); diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index cd986c0..65df679 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -858,13 +858,14 @@ public class StructureSelectionManager * @param pdbResNum * @param chain * @param pdbfile + * @return */ - public void mouseOverStructure(int pdbResNum, String chain, + public String mouseOverStructure(int pdbResNum, String chain, String pdbfile) { AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0); List atoms = Collections.singletonList(atomSpec); - mouseOverStructure(atoms); + return mouseOverStructure(atoms); } /** @@ -872,12 +873,12 @@ public class StructureSelectionManager * * @param atoms */ - public void mouseOverStructure(List atoms) + public String mouseOverStructure(List atoms) { if (listeners == null) { // old or prematurely sent event - return; + return null; } boolean hasSequenceListener = false; for (int i = 0; i < listeners.size(); i++) @@ -889,18 +890,24 @@ public class StructureSelectionManager } if (!hasSequenceListener) { - return; + return null; } SearchResultsI results = findAlignmentPositionsForStructurePositions( atoms); + String result = null; for (Object li : listeners) { if (li instanceof SequenceListener) { - ((SequenceListener) li).highlightSequence(results); + String s = ((SequenceListener) li).highlightSequence(results); + if (s != null) + { + result = s; + } } } + return result; } /** diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 2324a64..4fc143e 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -25,6 +25,7 @@ import jalview.api.FeatureColourI; import jalview.api.FeaturesDisplayedI; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.Mapping; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResults; @@ -1164,9 +1165,16 @@ public abstract class FeatureRendererModel * @param pos * @return */ - public List findComplementFeaturesAtResidue(SequenceI sequence, int pos) + public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, + int pos) { SequenceI ds = sequence.getDatasetSequence(); + if (ds == null) + { + ds = sequence; + } + final char residue = ds.getCharAt(pos - ds.getStart()); + List found = new ArrayList<>(); List mappings = this.av.getAlignment() .getCodonFrame(sequence); @@ -1175,9 +1183,12 @@ public abstract class FeatureRendererModel * todo: direct lookup of CDS for peptide and vice-versa; for now, * have to search through an unordered list of mappings for a candidate */ + Mapping mapping = null; + SequenceI mapFrom = null; + for (AlignedCodonFrame acf : mappings) { - Mapping mapping = acf.getMappingForSequence(sequence, true); + mapping = acf.getMappingForSequence(sequence, true); if (mapping == null || mapping.getMap().getFromRatio() == mapping .getMap().getToRatio()) { @@ -1189,6 +1200,7 @@ public abstract class FeatureRendererModel { int fromRes = match.getStart(); int toRes = match.getEnd(); + mapFrom = match.getSequence(); List fs = findFeaturesAtResidue( match.getSequence(), fromRes, toRes); for (SequenceFeature sf : fs) @@ -1199,7 +1211,16 @@ public abstract class FeatureRendererModel } } } + + /* + * just take the first mapped features we find + */ + if (!found.isEmpty()) + { + break; + } } + /* * sort by renderorder, inefficiently */ @@ -1213,13 +1234,14 @@ public abstract class FeatureRendererModel result.add(sf); if (result.size() == found.size()) { - return result; + return new MappedFeatures(mapping, mapFrom, pos, residue, + result); } } } } - return result; + return new MappedFeatures(mapping, mapFrom, pos, residue, result); } } -- 1.7.10.2