JAL-3302 transfer visible 'linked' features to Chimera
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 14 Jun 2019 10:54:50 +0000 (11:54 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 14 Jun 2019 10:54:50 +0000 (11:54 +0100)
src/jalview/api/FeatureRenderer.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java

index a9bbe31..8aa2858 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.api;
 
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSetI;
@@ -283,4 +284,17 @@ public interface FeatureRenderer
    * @return
    */
   boolean isVisible(SequenceFeature feature);
+
+  /**
+   * Answers a bean containing a mapping, and a list of visible features in this
+   * alignment at a position (or range) which is mappable from the given sequence
+   * residue position in a mapped alignment. Features are returned in render order
+   * of feature type (on top last), with order within feature type undefined. If
+   * no features or mapping are found, answers null.
+   * 
+   * @param sequence
+   * @param pos
+   * @return
+   */
+  MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, int pos);
 }
index dad8511..3caaac3 100644 (file)
@@ -26,8 +26,10 @@ import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.gui.Desktop;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
@@ -104,7 +106,7 @@ public class ChimeraCommands
      * delimited). If length limit issues arise, refactor to return one color
      * command per colour.
      */
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     StringBuilder sb = new StringBuilder(256);
     boolean firstColour = true;
     for (Object key : colourMap.keySet())
@@ -196,7 +198,7 @@ public class ChimeraCommands
     AlignViewportI viewport = viewPanel.getAlignViewport();
     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
     AlignmentI al = viewport.getAlignment();
-    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
     Color lastColour = null;
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
@@ -257,7 +259,7 @@ public class ChimeraCommands
               {
                 if (startPos != -1)
                 {
-                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
+                  addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                           lastPos, lastChain);
                 }
                 startPos = pos;
@@ -269,7 +271,7 @@ public class ChimeraCommands
             // final colour range
             if (lastColour != null)
             {
-              addColourRange(colourMap, lastColour, pdbfnum, startPos,
+              addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                       lastPos, lastChain);
             }
             // break;
@@ -281,26 +283,32 @@ public class ChimeraCommands
   }
 
   /**
-   * Helper method to add one contiguous colour range to the colour map.
+   * Helper method to add one contiguous range to the AtomSpec model for the given
+   * value (creating the model if necessary). As used by Jalview, {@code value} is
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
    * 
    * @param map
-   * @param key
+   * @param value
    * @param model
    * @param startPos
    * @param endPos
    * @param chain
    */
-  protected static void addColourRange(Map<Object, AtomSpecModel> map,
-          Object key, int model, int startPos, int endPos, String chain)
+  protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
+          Object value, int model, int startPos, int endPos, String chain)
   {
     /*
      * Get/initialize map of data for the colour
      */
-    AtomSpecModel atomSpec = map.get(key);
+    AtomSpecModel atomSpec = map.get(value);
     if (atomSpec == null)
     {
       atomSpec = new AtomSpecModel();
-      map.put(key, atomSpec);
+      map.put(value, atomSpec);
     }
 
     atomSpec.addRange(model, startPos, endPos, chain);
@@ -349,7 +357,7 @@ public class ChimeraCommands
           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
           AlignmentViewPanel viewPanel)
   {
-    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
+    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
 
     FeatureRenderer fr = viewPanel.getFeatureRenderer();
     if (fr == null)
@@ -357,8 +365,27 @@ public class ChimeraCommands
       return theMap;
     }
 
+    AlignViewportI viewport = viewPanel.getAlignViewport();
     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
-    if (visibleFeatures.isEmpty())
+
+    /*
+     * if alignment is showing features from complement, we also transfer
+     * these features to the corresponding mapped structure residues
+     */
+    boolean showLinkedFeatures = viewport.isShowComplementFeatures();
+    List<String> complementFeatures = new ArrayList<>();
+    FeatureRenderer complementRenderer = null;
+    if (showLinkedFeatures)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      if (comp != null)
+      {
+        complementRenderer = Desktop.getAlignFrameFor(comp)
+                .getFeatureRenderer();
+        complementFeatures = complementRenderer.getDisplayedFeatureTypes();
+      }
+    }
+    if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
     {
       return theMap;
     }
@@ -379,16 +406,23 @@ public class ChimeraCommands
         {
           final SequenceI seq = seqs[pdbfnum][seqNo];
           int sp = alignment.findIndex(seq);
-          if (mapping[m].getSequence() == seq && sp > -1)
+          StructureMapping structureMapping = mapping[m];
+          if (structureMapping.getSequence() == seq && sp > -1)
           {
             /*
              * found a sequence with a mapping to a structure;
              * now scan its features
              */
-            SequenceI asp = alignment.getSequenceAt(sp);
-
-            scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
-                    pdbfnum);
+            if (!visibleFeatures.isEmpty())
+            {
+              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
+                      theMap, pdbfnum);
+            }
+            if (showLinkedFeatures)
+            {
+              scanComplementFeatures(complementRenderer, structureMapping,
+                      seq, theMap, pdbfnum);
+            }
           }
         }
       }
@@ -397,9 +431,85 @@ public class ChimeraCommands
   }
 
   /**
-   * Inspect features on the sequence; for each feature that is visible,
-   * determine its mapped ranges in the structure (if any) according to the
-   * given mapping, and add them to the map
+   * Scans visible features in mapped positions of the CDS/peptide complement, and
+   * adds any found to the map of attribute values/structure positions
+   * 
+   * @param complementRenderer
+   * @param structureMapping
+   * @param seq
+   * @param theMap
+   * @param modelNumber
+   */
+  protected static void scanComplementFeatures(
+          FeatureRenderer complementRenderer,
+          StructureMapping structureMapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  {
+    /*
+     * for each sequence residue mapped to a structure position...
+     */
+    for (int seqPos : structureMapping.getMapping().keySet())
+    {
+      /*
+       * find visible complementary features at mapped position(s)
+       */
+      MappedFeatures mf = complementRenderer
+              .findComplementFeaturesAtResidue(seq, seqPos);
+      if (mf != null)
+      {
+        for (SequenceFeature sf : mf.features)
+        {
+          String type = sf.getType();
+
+          /*
+           * Don't copy features which originated from Chimera
+           */
+          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+                  .equals(sf.getFeatureGroup()))
+          {
+            continue;
+          }
+
+          /*
+           * record feature 'value' (score/description/type) as at the
+           * corresponding structure position
+           */
+          List<int[]> mappedRanges = structureMapping
+                  .getPDBResNumRanges(seqPos, seqPos);
+
+          if (!mappedRanges.isEmpty())
+          {
+            String value = sf.getDescription();
+            if (value == null || value.length() == 0)
+            {
+              value = type;
+            }
+            float score = sf.getScore();
+            if (score != 0f && !Float.isNaN(score))
+            {
+              value = Float.toString(score);
+            }
+            Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+            if (featureValues == null)
+            {
+              featureValues = new HashMap<>();
+              theMap.put(type, featureValues);
+            }
+            for (int[] range : mappedRanges)
+            {
+              addAtomSpecRange(featureValues, value, modelNumber, range[0],
+                      range[1], structureMapping.getChain());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Inspect features on the sequence; for each feature that is visible, determine
+   * its mapped ranges in the structure (if any) according to the given mapping,
+   * and add them to the map.
    * 
    * @param visibleFeatures
    * @param mapping
@@ -418,15 +528,14 @@ public class ChimeraCommands
       String type = sf.getType();
 
       /*
-       * Only copy visible features, don't copy any which originated
-       * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
+       * Don't copy features which originated from Chimera
        */
-      boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup());
-      if (isFromViewer)
+      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+              .equals(sf.getFeatureGroup()))
       {
         continue;
       }
+
       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
               sf.getEnd());
 
@@ -445,12 +554,12 @@ public class ChimeraCommands
         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
         if (featureValues == null)
         {
-          featureValues = new HashMap<Object, AtomSpecModel>();
+          featureValues = new HashMap<>();
           theMap.put(type, featureValues);
         }
         for (int[] range : mappedRanges)
         {
-          addColourRange(featureValues, value, modelNumber, range[0],
+          addAtomSpecRange(featureValues, value, modelNumber, range[0],
                   range[1], mapping.getChain());
         }
       }
@@ -475,7 +584,7 @@ public class ChimeraCommands
   protected static List<String> buildSetAttributeCommands(
           Map<String, Map<Object, AtomSpecModel>> featureMap)
   {
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     for (String featureType : featureMap.keySet())
     {
       String attributeName = makeAttributeName(featureType);
index 838c41a..f9c9782 100644 (file)
@@ -1161,17 +1161,7 @@ public abstract class FeatureRendererModel
     return filter == null ? true : filter.matches(sf);
   }
 
-  /**
-   * Answers a bean containing a mapping, and a list of features in this
-   * alignment at a position (or range) which is mappable from the given
-   * sequence residue position in a mapped alignment. Features are returned in
-   * render order of feature type (on top last), with order within feature type
-   * undefined. If no features or mapping are found, answers null.
-   * 
-   * @param sequence
-   * @param pos
-   * @return
-   */
+  @Override
   public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence,
           int pos)
   {
index 2c973ca..06a09df 100644 (file)
@@ -60,15 +60,15 @@ public class ChimeraCommandsTest
   {
 
     Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
@@ -91,7 +91,7 @@ public class ChimeraCommandsTest
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
   
     List<String> commands = ChimeraCommands
             .buildSetAttributeCommands(featuresMap);
@@ -104,24 +104,24 @@ public class ChimeraCommandsTest
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0),
             "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
 
     // same feature, different value
-    ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
@@ -133,7 +133,7 @@ public class ChimeraCommandsTest
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
-    ChimeraCommands.addColourRange(featureValues,
+    ChimeraCommands.addAtomSpecRange(featureValues,
             "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
             "A");
     // feature names are sanitised to change non-alphanumeric to underscore