JAL-3518 more pull up / test coverage of structure command generation
[jalview.git] / src / jalview / structures / models / AAStructureBindingModel.java
index 92b00c7..0c1cd50 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.structures.models;
 
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
@@ -29,11 +30,15 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ResidueProperties;
 import jalview.structure.AtomSpec;
+import jalview.structure.AtomSpecModel;
 import jalview.structure.StructureCommandsI;
+import jalview.structure.StructureCommandsI.SuperposeData;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -45,6 +50,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -63,6 +69,8 @@ public abstract class AAStructureBindingModel
         extends SequenceStructureBindingModel
         implements StructureListener, StructureSelectionManagerProvider
 {
+  private static final int MIN_POS_TO_SUPERPOSE = 4;
+
   private static final String COLOURING_STRUCTURES = MessageManager
           .getString("status.colouring_structures");
 
@@ -124,35 +132,6 @@ public abstract class AAStructureBindingModel
   public String fileLoadingError;
 
   /**
-   * Data bean class to simplify parameterisation in superposeStructures
-   */
-  protected class SuperposeData
-  {
-    /**
-     * Constructor with alignment width argument
-     * 
-     * @param width
-     */
-    public SuperposeData(int width)
-    {
-      pdbResNo = new int[width];
-    }
-
-    public String filename;
-
-    public String pdbId;
-
-    public String chain = "";
-
-    public boolean isRna;
-
-    /*
-     * The pdb residue number (if any) mapped to each column of the alignment
-     */
-    public int[] pdbResNo;
-  }
-
-  /**
    * Constructor
    * 
    * @param ssm
@@ -692,7 +671,7 @@ public abstract class AAStructureBindingModel
              * for the same structure)
              */
             s = seqCountForPdbFile;
-            break;
+            break; // fixme break out of two loops here!
           }
         }
       }
@@ -813,21 +792,121 @@ public abstract class AAStructureBindingModel
   /**
    * Constructs and sends a command to align structures against a reference
    * structure, based on one or more sequence alignments. May optionally return
-   * an error or warning message for the alignment command.
+   * an error or warning message for the alignment command(s).
    * 
-   * @param alignments
-   *          an array of alignments to process
-   * @param structureIndices
-   *          an array of corresponding reference structures (index into pdb
-   *          file array); if a negative value is passed, the first PDB file
-   *          mapped to an alignment sequence is used as the reference for
-   *          superposition
-   * @param hiddenCols
-   *          an array of corresponding hidden columns for each alignment
+   * @param alignWith
+   *          an array of one or more alignment views to process
    * @return
    */
-  public abstract String superposeStructures(AlignmentI[] alignments,
-          int[] structureIndices, HiddenColumns[] hiddenCols);
+  public String superposeStructures(List<AlignmentViewPanel> alignWith)
+  {
+    String error = "";
+    String[] files = getStructureFiles();
+
+    if (!waitForFileLoad(files))
+    {
+      return null;
+    }
+    refreshPdbEntries();
+
+    for (AlignmentViewPanel view : alignWith)
+    {
+      AlignmentI alignment = view.getAlignment();
+      HiddenColumns hiddenCols = alignment.getHiddenColumns();
+
+      /*
+       * 'matched' bit i will be set for visible alignment columns i where
+       * all sequences have a residue with a mapping to their PDB structure
+       */
+      BitSet matched = new BitSet();
+      final int width = alignment.getWidth();
+      for (int m = 0; m < width; m++)
+      {
+        if (hiddenCols == null || hiddenCols.isVisible(m))
+        {
+          matched.set(m);
+        }
+      }
+
+      SuperposeData[] structures = new SuperposeData[files.length];
+      for (int f = 0; f < files.length; f++)
+      {
+        structures[f] = new SuperposeData(width,
+                f + commandGenerator.getModelStartNo());
+      }
+
+      /*
+       * Calculate the superposable alignment columns ('matched'), and the
+       * corresponding structure residue positions (structures.pdbResNo)
+       */
+      int refStructure = findSuperposableResidues(alignment,
+              matched, structures);
+
+      /*
+       * require at least 4 positions to be able to execute superposition
+       */
+      int nmatched = matched.cardinality();
+      if (nmatched < MIN_POS_TO_SUPERPOSE)
+      {
+        String msg = MessageManager.formatMessage("label.insufficient_residues",
+                nmatched);
+        error += view.getViewName() + ": " + msg + "; ";
+        continue;
+      }
+
+      /*
+       * get a model of the superposable residues in the reference structure 
+       */
+      AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
+              matched);
+
+      /*
+       * Show all as backbone before doing superposition(s)
+       * (residues used for matching will be shown as ribbon)
+       */
+      executeCommand(commandGenerator.showBackbone(), false);
+
+      /*
+       * superpose each (other) structure to the reference in turn
+       */
+      for (int i = 0; i < structures.length; i++)
+      {
+        if (i != refStructure)
+        {
+          AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
+          String commands = commandGenerator.superposeStructures(refAtoms,
+                  atomSpec);
+          List<String> replies = executeCommands(true, commands);
+          for (String reply : replies)
+          {
+            // return this error (Chimera only) to the user
+            if (reply.toLowerCase().contains("unequal numbers of atoms"))
+            {
+              error += "; " + reply;
+            }
+          }
+        }
+      }
+    }
+
+    return error;
+  }
+
+  private AtomSpecModel getAtomSpec(SuperposeData superposeData,
+          BitSet matched)
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    int nextColumnMatch = matched.nextSetBit(0);
+    while (nextColumnMatch != -1)
+    {
+      int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
+      model.addRange(superposeData.modelNo, pdbResNum, pdbResNum,
+              superposeData.chain);
+      nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
+    }
+
+    return model;
+  }
 
   /**
    * returns the current sequenceRenderer that should be used to colour the
@@ -1001,10 +1080,15 @@ public abstract class AAStructureBindingModel
   protected List<String> executeCommands(boolean getReply,
           String... commands)
   {
-    List<String> response = null;
+    // todo: tidy this up
+    List<String> response = getReply ? new ArrayList<>() : null;
     for (String cmd : commands)
     {
-      response = executeCommand(cmd, getReply);
+      List<String> replies = executeCommand(cmd, getReply);
+      if (getReply && replies != null)
+      {
+        response.addAll(replies);
+      }
     }
     return response;
   }
@@ -1027,10 +1111,11 @@ public abstract class AAStructureBindingModel
     String[] files = getStructureFiles();
 
     SequenceRenderer sr = getSequenceRenderer(alignmentv);
+    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
+            sequence, sr, alignmentv);
 
     String[] colourBySequenceCommands = commandGenerator
-            .colourBySequence(getSsm(), files, getSequence(), sr,
-                    alignmentv);
+            .colourBySequence(colourMap);
     executeCommands(false, colourBySequenceCommands);
   }
 
@@ -1151,4 +1236,188 @@ public abstract class AAStructureBindingModel
   {
     return commandGenerator;
   }
+
+  protected abstract ViewerType getViewerType();
+
+  /**
+   * Send a structure viewer command asynchronously in a new thread. If the
+   * progress message is not null, display this message while the command is
+   * executing.
+   * 
+   * @param command
+   * @param progressMsg
+   */
+  protected void sendAsynchronousCommand(String command, String progressMsg)
+  {
+    final JalviewStructureDisplayI theViewer = getViewer();
+    final long handle = progressMsg == null ? 0
+            : theViewer.startProgressBar(progressMsg);
+    SwingUtilities.invokeLater(new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        try
+        {
+          executeCommand(command, false);
+        } finally
+        {
+          if (progressMsg != null)
+          {
+            theViewer.stopProgressBar(null, handle);
+          }
+        }
+      }
+    });
+
+  }
+
+  /**
+   * Builds a data structure which records mapped structure residues for each
+   * colour. From this we can easily generate the viewer commands for colour by
+   * sequence. Constructs and returns a map of {@code Color} to
+   * {@code AtomSpecModel}, where the atomspec model holds
+   * 
+   * <pre>
+   *   Model numbers
+   *     Chains
+   *       Residue positions
+   * </pre>
+   * 
+   * Ordering is by order of addition (for colours), natural ordering (for
+   * models and chains)
+   * 
+   * @param ssm
+   * @param files
+   * @param sequence
+   * @param sr
+   * @param viewPanel
+   * @return
+   */
+  protected Map<Object, AtomSpecModel> buildColoursMap(
+          StructureSelectionManager ssm, String[] files,
+          SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel)
+  {
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+    AlignmentI al = viewport.getAlignment();
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
+    Color lastColour = null;
+  
+    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+    {
+      final int modelNumber = pdbfnum + commandGenerator.getModelStartNo();
+      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+  
+      if (mapping == null || mapping.length < 1)
+      {
+        continue;
+      }
+  
+      int startPos = -1, lastPos = -1;
+      String lastChain = "";
+      for (int s = 0; s < sequence[pdbfnum].length; s++)
+      {
+        for (int sp, m = 0; m < mapping.length; m++)
+        {
+          final SequenceI seq = sequence[pdbfnum][s];
+          if (mapping[m].getSequence() == seq
+                  && (sp = al.findIndex(seq)) > -1)
+          {
+            SequenceI asp = al.getSequenceAt(sp);
+            for (int r = 0; r < asp.getLength(); r++)
+            {
+              // no mapping to gaps in sequence
+              if (Comparison.isGap(asp.getCharAt(r)))
+              {
+                continue;
+              }
+              int pos = mapping[m].getPDBResNum(asp.findPosition(r));
+  
+              if (pos < 1 || pos == lastPos)
+              {
+                continue;
+              }
+  
+              Color colour = sr.getResidueColour(seq, r, finder);
+  
+              /*
+               * darker colour for hidden regions
+               */
+              if (!cs.isVisible(r))
+              {
+                colour = Color.GRAY;
+              }
+  
+              final String chain = mapping[m].getChain();
+  
+              /*
+               * Just keep incrementing the end position for this colour range
+               * _unless_ colour, PDB model or chain has changed, or there is a
+               * gap in the mapped residue sequence
+               */
+              final boolean newColour = !colour.equals(lastColour);
+              final boolean nonContig = lastPos + 1 != pos;
+              final boolean newChain = !chain.equals(lastChain);
+              if (newColour || nonContig || newChain)
+              {
+                if (startPos != -1)
+                {
+                  addAtomSpecRange(colourMap, lastColour, modelNumber,
+                          startPos, lastPos, lastChain);
+                }
+                startPos = pos;
+              }
+              lastColour = colour;
+              lastPos = pos;
+              lastChain = chain;
+            }
+            // final colour range
+            if (lastColour != null)
+            {
+              addAtomSpecRange(colourMap, lastColour, modelNumber, startPos,
+                      lastPos, lastChain);
+            }
+            // break;
+          }
+        }
+      }
+    }
+    return colourMap;
+  }
+
+  /**
+   * 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 value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public static final 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(value);
+    if (atomSpec == null)
+    {
+      atomSpec = new AtomSpecModel();
+      map.put(value, atomSpec);
+    }
+  
+    atomSpec.addRange(model, startPos, endPos, chain);
+  }
 }