JAL-3187 hacks to get peptide variant in to Jmol hover tooltip
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 19 Feb 2019 11:15:34 +0000 (11:15 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 19 Feb 2019 11:15:34 +0000 (11:15 +0000)
src/jalview/analysis/AlignmentUtils.java
src/jalview/appletgui/SeqPanel.java
src/jalview/datamodel/MappedFeatures.java [new file with mode: 0644]
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/gui/SeqPanel.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/structure/SequenceListener.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java

index 7a082be..0d2e002 100644 (file)
@@ -2425,7 +2425,8 @@ public class AlignmentUtils
   static int computePeptideVariants(SequenceI peptide, int peptidePos,
           List<DnaVariant>[] 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;
index e07dae6..7c85d12 100644 (file)
@@ -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 (file)
index 0000000..07d3857
--- /dev/null
@@ -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<SequenceFeature> features;
+
+  /**
+   * Constructor
+   * 
+   * @param theMapping
+   * @param pos
+   * @param res
+   * @param theFeatures
+   */
+  public MappedFeatures(Mapping theMapping, SequenceI from, int pos,
+          char res,
+          List<SequenceFeature> 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<String> findProteinVariants()
+  {
+    List<String> 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;
+  }
+}
index a5b1110..e85d387 100644 (file)
@@ -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
index cf0991e..c794e57 100644 (file)
@@ -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
+   * <ul>
+   * <li>results are a single residue in a protein alignment</li>
+   * <li>there is a mapping to a coding sequence (codon)</li>
+   * <li>there are one or more SNP variant features on the codon</li>
+   * </ul>
+   * 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<String> 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<String> 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);
         }
       }
     }
index 78f4989..39d705b 100644 (file)
@@ -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<SequenceFeature> 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<SequenceFeature> features = fr2.findComplementFeaturesAtResidue(
+    MappedFeatures mf = fr2.findComplementFeaturesAtResidue(
             seq, seq.findPosition(column - 1));
 
     ReverseListIterator<SequenceFeature> it = new ReverseListIterator<>(
-            features);
+            mf.features);
     while (it.hasNext())
     {
       SequenceFeature sf = it.next();
index 81ff739..f8c5bea 100644 (file)
@@ -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);
index cd986c0..65df679 100644 (file)
@@ -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<AtomSpec> atoms = Collections.singletonList(atomSpec);
-    mouseOverStructure(atoms);
+    return mouseOverStructure(atoms);
   }
 
   /**
@@ -872,12 +873,12 @@ public class StructureSelectionManager
    * 
    * @param atoms
    */
-  public void mouseOverStructure(List<AtomSpec> atoms)
+  public String mouseOverStructure(List<AtomSpec> 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;
   }
 
   /**
index 2324a64..4fc143e 100644 (file)
@@ -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<SequenceFeature> 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<SequenceFeature> found = new ArrayList<>();
     List<AlignedCodonFrame> 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<SequenceFeature> 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);
   }
 
 }