JAL-3518 basic refactoring / pull up of superposeStructures; to tidy!
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 28 Feb 2020 16:59:25 +0000 (16:59 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 28 Feb 2020 16:59:25 +0000 (16:59 +0000)
24 files changed:
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/appletgui/ExtJmol.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java [deleted file]
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/ChimeraXViewFrame.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/JalviewChimeraXBindingModel.java
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/structure/AtomSpecModel.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsBase.java
src/jalview/structure/StructureCommandsI.java
src/jalview/structures/models/AAStructureBindingModel.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java [deleted file]
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java
test/jalview/structure/AtomSpecModelTest.java [new file with mode: 0644]
test/jalview/structures/models/AAStructureBindingModelTest.java

index dd6dea3..9a72b2e 100644 (file)
@@ -174,4 +174,11 @@ class AppletJmolBinding extends JalviewJmolBinding
   {
     return null;
   }
+
+  @Override
+  protected void sendAsynchronousCommand(String command, String progressMsg)
+  {
+    // TODO Auto-generated method stub
+    
+  }
 }
index 28381bc..5be53a3 100644 (file)
@@ -180,4 +180,11 @@ public class ExtJmol extends JalviewJmolBinding
     return null;
   }
 
+  @Override
+  protected void sendAsynchronousCommand(String command, String progressMsg)
+  {
+    // TODO Auto-generated method stub
+    
+  }
+
 }
index 3b4a958..6b0a696 100644 (file)
@@ -26,9 +26,11 @@ import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.IProgressIndicator;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
 import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommandsI.SuperposeData;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
@@ -172,7 +174,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   /**
    * {@inheritDoc}
    */
-  @Override
   public String superposeStructures(AlignmentI[] _alignment,
           int[] _refStructure, HiddenColumns[] _hiddenCols)
   {
@@ -248,7 +249,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       SuperposeData[] structures = new SuperposeData[files.length];
       for (int f = 0; f < files.length; f++)
       {
-        structures[f] = new SuperposeData(alignment.getWidth());
+        structures[f] = new SuperposeData(alignment.getWidth(), f);
       }
 
       /*
@@ -1244,6 +1245,11 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       }
     }
     return -1;
+  }
 
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.JMOL;
   }
 }
index c8a54cd..7dd5c0b 100644 (file)
@@ -28,6 +28,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.structure.AtomSpecModel;
 import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -37,7 +38,6 @@ import java.awt.Color;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Routines for generating Jmol commands for Jalview/Jmol binding
@@ -52,15 +52,50 @@ public class JmolCommands extends StructureCommandsBase
 
   private static final String CMD_COLOUR_BY_CHAIN = "select *;color chain";
 
-  private static String formatRGB(Color c) {
+  private static final String PIPE = "|";
+
+  private static final String HYPHEN = "-";
+
+  private static final String COLON = ":";
+
+  private static final String SLASH = "/";
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
+  }
+
+  @Override
+  protected String getColourString(Color c)
+  {
     return c == null ? null
             : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(),
                     c.getBlue());
   }
 
+  /**
+   * Returns commands (one per colour key in the map) like
+   * 
+   * <pre>
+   *   select 2:A/1.1|3-27:B/1.1|9-12:A/2.1;color[173,0,82]
+   * </pre>
+   */
   @Override
-  public String[] colourBySequence(
-          StructureSelectionManager ssm, String[] files,
+  public String[] colourBySequence(Map<Object, AtomSpecModel> colourMap)
+  {
+    List<String> colourCommands = buildColourCommands(colourMap);
+
+    return colourCommands.toArray(new String[colourCommands.size()]);
+  }
+
+  public String[] colourBySequence(StructureSelectionManager ssm,
+          String[] files,
           SequenceI[][] sequence, SequenceRenderer sr,
           AlignmentViewPanel viewPanel)
   {
@@ -134,9 +169,8 @@ public class JmolCommands extends StructureCommandsBase
 
               String newSelcom = (mapping[m].getChain() != " "
                       ? ":" + mapping[m].getChain()
-                      : "") + "/" + (pdbfnum + 1) + ".1" + ";color["
-                      + col.getRed() + "," + col.getGreen() + ","
-                      + col.getBlue() + "]";
+                      : "") + "/" + (pdbfnum + 1) + ".1" + ";color"
+                      + getColourString(col);
               if (command.length() > newSelcom.length() && command
                       .substring(command.length() - newSelcom.length())
                       .equals(newSelcom))
@@ -230,20 +264,7 @@ public class JmolCommands extends StructureCommandsBase
   {
     StringBuilder cmd = new StringBuilder(128);
     cmd.append("select *;color white;");
-
-    /*
-     * concatenate commands like
-     * select VAL;color[100,215,55]
-     */
-    for (Entry<String, Color> entry : colours.entrySet())
-    {
-      Color col = entry.getValue();
-      String resCode = entry.getKey();
-      cmd.append("select ").append(resCode).append(";");
-      cmd.append("color[");
-      cmd.append(formatRGB(col));
-      cmd.append("];");
-    }
+    cmd.append(super.colourByResidues(colours));
 
     return cmd.toString();
   }
@@ -251,7 +272,7 @@ public class JmolCommands extends StructureCommandsBase
   @Override
   public String setBackgroundColour(Color col)
   {
-    return "background " + formatRGB(col);
+    return "background " + getColourString(col);
   }
 
   @Override
@@ -285,4 +306,137 @@ public class JmolCommands extends StructureCommandsBase
     return command;
   }
 
+  /**
+   * Returns a command to superpose atoms in {@code atomSpec} to those in
+   * {@code refAtoms}, restricted to alpha carbons only (Phosphorous for rna).
+   * For example
+   * 
+   * <pre>
+   * compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} 
+   *         ATOMS {1-87:A}{2-54:A|61-94:A} ROTATE TRANSLATE 1.0;
+   * </pre>
+   * 
+   * where {@code conformation=1} excludes ALTLOC atom locations, and 1.0 is the
+   * time in seconds to animate the action. For this example, atoms in model 2
+   * are moved towards atoms in model 1.
+   * <p>
+   * The two atomspecs should each be for one model only, but may have more than
+   * one chain. The number of atoms specified should be the same for both
+   * models, though if not, Jmol may make a 'best effort' at superposition.
+   * 
+   * @see https://chemapps.stolaf.edu/jmol/docs/#compare
+   */
+  @Override
+  public String superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    int refModel = refAtoms.getModels().iterator().next();
+    int model2 = atomSpec.getModels().iterator().next();
+    sb.append(String.format("compare {%d.1} {%d.1}", model2, refModel));
+    sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {");
+
+    /*
+     * command examples don't include modelspec with atoms, getAtomSpec does;
+     * it works, so leave it as it is for simplicity
+     */
+    sb.append(getAtomSpec(atomSpec, true)).append("}{");
+    sb.append(getAtomSpec(refAtoms, true)).append("}");
+    sb.append(" ROTATE TRANSLATE ");
+    sb.append(getCommandSeparator());
+
+    /*
+     * show residues used for superposition as ribbon
+     */
+    sb.append("select ").append(getAtomSpec(atomSpec, false)).append("|");
+    sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator())
+            .append("cartoons");
+
+    return sb.toString();
+  }
+
+  @Override
+  public String openCommandFile(String path)
+  {
+    /*
+     * https://chemapps.stolaf.edu/jmol/docs/#script
+     * not currently used in Jalview
+     */
+    return "script " + path;
+  }
+
+  @Override
+  public String saveSession(String filepath)
+  {
+    /*
+     * https://chemapps.stolaf.edu/jmol/docs/#write
+     * not currently used in Jalview
+     */
+    return "write \"" + filepath + "\"";
+  }
+
+  @Override
+  protected String getColourCommand(String atomSpec, Color colour)
+  {
+    StringBuilder sb = new StringBuilder(atomSpec.length()+20);
+    sb.append("select ").append(atomSpec).append(getCommandSeparator())
+            .append("color").append(getColourString(colour));
+    return sb.toString();
+  }
+
+  @Override
+  protected String getResidueSpec(String residue)
+  {
+    return residue;
+  }
+
+  /**
+   * Generates a Jmol atomspec string like
+   * 
+   * <pre>
+   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * </pre>
+   * 
+   * Parameter {@code alphaOnly} is not used here - this restriction is made by
+   * a separate clause in the {@code compare} (superposition) command.
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel model, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(128);
+
+    boolean first = true;
+    for (int modelNo : model.getModels())
+    {
+      for (String chain : model.getChains(modelNo))
+      {
+        for (int[] range : model.getRanges(modelNo, chain))
+        {
+          if (!first)
+          {
+            sb.append(PIPE);
+          }
+          first = false;
+          if (range[0] == range[1])
+          {
+            sb.append(range[0]);
+          }
+          else
+          {
+            sb.append(range[0]).append(HYPHEN).append(range[1]);
+          }
+          sb.append(COLON).append(chain.trim()).append(SLASH);
+          sb.append(String.valueOf(modelNo)).append(".1");
+        }
+      }
+    }
+
+    return sb.toString();
+  }
+
+  @Override
+  public String showBackbone()
+  {
+    return "select *; cartoons off; backbone";
+  }
 }
diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java
deleted file mode 100644 (file)
index a72844e..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.ext.rbvi.chimera;
-
-import jalview.util.IntRangeComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * A class to model a Chimera atomspec pattern, for example
- * 
- * <pre>
- * #0:15.A,28.A,54.A,63.A,70-72.A,83-84.A,97-98.A|#1:2.A,6.A,11.A,13-14.A,70.A,82.A,96-97.A
- * </pre>
- * 
- * where
- * <ul>
- * <li>#0 is a model number</li>
- * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
- * <li>.A is a chain identifier</li>
- * <li>residue ranges are separated by comma</li>
- * <li>atomspecs for distinct models are separated by | (or)</li>
- * </ul>
- * 
- * <pre>
- * &#64;see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
- * </pre>
- */
-public class AtomSpecModel
-{
-  private Map<Integer, Map<String, List<int[]>>> atomSpec;
-
-  /**
-   * Constructor
-   */
-  public AtomSpecModel()
-  {
-    atomSpec = new TreeMap<>();
-  }
-
-  /**
-   * Adds one contiguous range to this atom spec
-   * 
-   * @param model
-   * @param startPos
-   * @param endPos
-   * @param chain
-   */
-  public void addRange(int model, int startPos, int endPos, String chain)
-  {
-    /*
-     * Get/initialize map of data for the colour and model
-     */
-    Map<String, List<int[]>> modelData = atomSpec.get(model);
-    if (modelData == null)
-    {
-      atomSpec.put(model, modelData = new TreeMap<>());
-    }
-
-    /*
-     * Get/initialize map of data for colour, model and chain
-     */
-    List<int[]> chainData = modelData.get(chain);
-    if (chainData == null)
-    {
-      chainData = new ArrayList<>();
-      modelData.put(chain, chainData);
-    }
-
-    /*
-     * Add the start/end positions
-     */
-    chainData.add(new int[] { startPos, endPos });
-    // TODO add intelligently, using a RangeList class
-  }
-
-  /**
-   * Returns the range(s) formatted as a Chimera atomspec
-   * 
-   * @return
-   */
-  public String getAtomSpec()
-  {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModel = true;
-    for (Integer model : atomSpec.keySet())
-    {
-      if (!firstModel)
-      {
-        sb.append("|");
-      }
-      firstModel = false;
-      sb.append("#").append(model).append(":");
-
-      boolean firstPositionForModel = true;
-      final Map<String, List<int[]>> modelData = atomSpec.get(model);
-
-      for (String chain : modelData.keySet())
-      {
-        chain = " ".equals(chain) ? chain : chain.trim();
-
-        List<int[]> rangeList = modelData.get(chain);
-
-        /*
-         * sort ranges into ascending start position order
-         */
-        Collections.sort(rangeList, IntRangeComparator.ASCENDING);
-
-        int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
-        int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
-
-        Iterator<int[]> iterator = rangeList.iterator();
-        while (iterator.hasNext())
-        {
-          int[] range = iterator.next();
-          if (range[0] <= end + 1)
-          {
-            /*
-             * range overlaps or is contiguous with the last one
-             * - so just extend the end position, and carry on
-             * (unless this is the last in the list)
-             */
-            end = Math.max(end, range[1]);
-          }
-          else
-          {
-            /*
-             * we have a break so append the last range
-             */
-            appendRange(sb, start, end, chain, firstPositionForModel,
-                    false);
-            firstPositionForModel = false;
-            start = range[0];
-            end = range[1];
-          }
-        }
-
-        /*
-         * and append the last range
-         */
-        if (!rangeList.isEmpty())
-        {
-          appendRange(sb, start, end, chain, firstPositionForModel, false);
-          firstPositionForModel = false;
-        }
-      }
-    }
-    return sb.toString();
-  }
-
-  /**
-   * @param sb
-   * @param start
-   * @param end
-   * @param chain
-   * @param firstPositionForModel
-   */
-  protected void appendRange(StringBuilder sb, int start, int end,
-          String chain, boolean firstPositionForModel, boolean isChimeraX)
-  {
-    if (!firstPositionForModel)
-    {
-      sb.append(",");
-    }
-    if (end == start)
-    {
-      sb.append(start);
-    }
-    else
-    {
-      sb.append(start).append("-").append(end);
-    }
-
-    if (!isChimeraX)
-    {
-      sb.append(".");
-      if (!" ".equals(chain))
-      {
-        sb.append(chain);
-      }
-    }
-  }
-
-  /**
-   * Returns the range(s) formatted as a ChimeraX atomspec, for example
-   * <p>
-   * #1/A:2-20,30-40/B:10-20|#2/A:12-30
-   * 
-   * @return
-   */
-  public String getAtomSpecX()
-  {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModel = true;
-    for (Integer model : atomSpec.keySet())
-    {
-      if (!firstModel)
-      {
-        sb.append("|");
-      }
-      firstModel = false;
-      sb.append("#").append(model);
-  
-      final Map<String, List<int[]>> modelData = atomSpec.get(model);
-  
-      for (String chain : modelData.keySet())
-      {
-        boolean firstPositionForChain = true;
-        chain = " ".equals(chain) ? chain : chain.trim();
-        sb.append("/").append(chain).append(":");
-        List<int[]> rangeList = modelData.get(chain);
-  
-        /*
-         * sort ranges into ascending start position order
-         */
-        Collections.sort(rangeList, IntRangeComparator.ASCENDING);
-  
-        int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
-        int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
-  
-        Iterator<int[]> iterator = rangeList.iterator();
-        while (iterator.hasNext())
-        {
-          int[] range = iterator.next();
-          if (range[0] <= end + 1)
-          {
-            /*
-             * range overlaps or is contiguous with the last one
-             * - so just extend the end position, and carry on
-             * (unless this is the last in the list)
-             */
-            end = Math.max(end, range[1]);
-          }
-          else
-          {
-            /*
-             * we have a break so append the last range
-             */
-            appendRange(sb, start, end, chain, firstPositionForChain, true);
-            start = range[0];
-            end = range[1];
-            firstPositionForChain = false;
-          }
-        }
-  
-        /*
-         * and append the last range
-         */
-        if (!rangeList.isEmpty())
-        {
-          appendRange(sb, start, end, chain, firstPositionForChain, true);
-        }
-        firstPositionForChain = false;
-      }
-    }
-    return sb.toString();
-  }
-}
index 61adefe..14699ef 100644 (file)
@@ -23,27 +23,26 @@ package jalview.ext.rbvi.chimera;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 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.AtomSpecModel;
 import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.ColorUtils;
-import jalview.util.Comparison;
+import jalview.util.IntRangeComparator;
 
 import java.awt.Color;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Routines for generating Chimera commands for Jalview/Chimera binding
@@ -55,279 +54,39 @@ public class ChimeraCommands extends StructureCommandsBase
 {
   public static final String NAMESPACE_PREFIX = "jv_";
 
-  protected static final String CMD_SEPARATOR = ";";
-
   private static final String CMD_COLOUR_BY_CHARGE = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
 
   private static final String CMD_COLOUR_BY_CHAIN = "rainbow chain";
 
+  // Chimera clause to exclude alternate locations in atom selection
+  private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
+
   @Override
-  public String[] colourBySequence(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
-          AlignmentViewPanel viewPanel)
+  public String[] colourBySequence(Map<Object, AtomSpecModel> colourMap)
   {
-    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
-            sequence, sr, viewPanel);
-
     List<String> colourCommands = buildColourCommands(colourMap);
 
     return colourCommands.toArray(new String[colourCommands.size()]);
   }
 
-  /**
-   * Traverse the map of colours/models/chains/positions to construct a list of
-   * 'color' commands (one per distinct colour used). The format of each command
-   * is
-   * 
-   * <pre>
-   * <blockquote> 
-   * color colorname #modelnumber:range.chain 
-   * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
-   * </blockquote>
-   * </pre>
-   * 
-   * @param colourMap
-   * @return
-   */
-  protected List<String> buildColourCommands(
-          Map<Object, AtomSpecModel> colourMap)
-  {
-    /*
-     * This version concatenates all commands into a single String (semi-colon
-     * delimited). If length limit issues arise, refactor to return one color
-     * command per colour.
-     */
-    List<String> commands = new ArrayList<>();
-    StringBuilder sb = new StringBuilder(256);
-    boolean firstColour = true;
-    for (Object key : colourMap.keySet())
-    {
-      Color colour = (Color) key;
-      String colourCode = ColorUtils.toTkCode(colour);
-      if (!firstColour)
-      {
-        sb.append("; ");
-      }
-      firstColour = false;
-      final AtomSpecModel colourData = colourMap.get(colour);
-      sb.append(getColourCommand(colourData, colourCode));
-    }
-    commands.add(sb.toString());
-    return commands;
-  }
-
-  protected String getColourCommand(AtomSpecModel colourData,
-          String colourCode)
-  {
-    return "color " + colourCode + " " + colourData.getAtomSpec();
-  }
-
-  /**
-   * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
-   * builds a Chimera format atom spec
-   * 
-   * @param modelAndChainRanges
-   */
-  protected static String getAtomSpec(
-          Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
-  {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModelForColour = true;
-    for (Integer model : modelAndChainRanges.keySet())
-    {
-      boolean firstPositionForModel = true;
-      if (!firstModelForColour)
-      {
-        sb.append("|");
-      }
-      firstModelForColour = false;
-      sb.append("#").append(model).append(":");
-
-      final Map<String, List<int[]>> modelData = modelAndChainRanges
-              .get(model);
-      for (String chain : modelData.keySet())
-      {
-        boolean hasChain = !"".equals(chain.trim());
-        for (int[] range : modelData.get(chain))
-        {
-          if (!firstPositionForModel)
-          {
-            sb.append(",");
-          }
-          if (range[0] == range[1])
-          {
-            sb.append(range[0]);
-          }
-          else
-          {
-            sb.append(range[0]).append("-").append(range[1]);
-          }
-          if (hasChain)
-          {
-            sb.append(".").append(chain);
-          }
-          firstPositionForModel = false;
-        }
-      }
-    }
-    return sb.toString();
-  }
-
-  /**
-   * <pre>
-   * Build a data structure which records contiguous subsequences for each colour. 
-   * From this we can easily generate the Chimera command for colour by sequence.
-   * Color
-   *     Model number
-   *         Chain
-   *             list of start/end ranges
-   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
-   * </pre>
-   * 
-   * @param ssm
-   * @param files
-   * @param sequence
-   * @param sr
-   * @param viewPanel
-   * @return
-   */
-  protected static Map<Object, AtomSpecModel> buildColoursMap(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
-          AlignmentViewPanel viewPanel)
+  @Override
+  public String getColourCommand(String atomSpec, Color colour)
   {
-    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 + 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;
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html
+    String colourCode = getColourString(colour);
+    return "color " + colourCode + " " + atomSpec;
   }
 
   /**
-   * Returns the lowest model number used by the structure viewer
+   * Returns a colour formatted suitable for use in viewer command syntax
    * 
+   * @param colour
    * @return
    */
-  protected static int getModelStartNo()
+  @Override
+  protected String getColourString(Color colour)
   {
-    return 0;
-  }
-
-  /**
-   * 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
-   */
-  protected 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);
+    return ColorUtils.toTkCode(colour);
   }
 
   /**
@@ -366,7 +125,7 @@ public class ChimeraCommands extends StructureCommandsBase
    * @param viewPanel
    * @return
    */
-  protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
+  protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
           AlignmentViewPanel viewPanel)
   {
@@ -649,7 +408,7 @@ public class ChimeraCommands extends StructureCommandsBase
     StringBuilder sb = new StringBuilder(128);
     sb.append("setattr res ").append(attributeName).append(" '")
             .append(attributeValue).append("' ");
-    sb.append(atomSpecModel.getAtomSpec());
+    sb.append(getAtomSpec(atomSpecModel, false));
     return sb.toString();
   }
 
@@ -699,33 +458,22 @@ public class ChimeraCommands extends StructureCommandsBase
   }
 
   @Override
-  public String colourByResidues(Map<String, Color> colours)
+  public String getResidueSpec(String residue)
   {
-    StringBuilder cmd = new StringBuilder(12 * colours.size());
-
-    /*
-     * concatenate commands like
-     * color #4949b6 ::VAL
-     */
-    for (Entry<String, Color> entry : colours.entrySet())
-    {
-      String colorSpec = ColorUtils.toTkCode(entry.getValue());
-      String resCode = entry.getKey();
-      cmd.append("color ").append(colorSpec).append(" ::").append(resCode)
-              .append(CMD_SEPARATOR);
-    }
-    return cmd.toString();
+    return "::" + residue;
   }
 
   @Override
   public String setBackgroundColour(Color col)
   {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor
     return "set bgColor " + ColorUtils.toTkCode(col);
   }
 
   @Override
   public String focusView()
   {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html
     return "focus";
   }
 
@@ -764,4 +512,165 @@ public class ChimeraCommands extends StructureCommandsBase
     return command;
   }
 
+  @Override
+  public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref)
+  {
+    /*
+     * Form Chimera match command to match spec to ref
+     * 
+     * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
+     * 
+     * @see
+     * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
+     */
+    StringBuilder cmd = new StringBuilder();
+    String atomSpec = getAtomSpec(spec, true);
+    String refSpec = getAtomSpec(ref, true);
+    cmd.append("match ").append(atomSpec).append(" ").append(refSpec);
+
+    /*
+     * show superposed residues as ribbon, others as chain
+     */
+    // fixme this should precede the loop over all alignments/structures
+    cmd.append(";~display all; chain @CA|P");
+    cmd.append("; ribbon ");
+    cmd.append(atomSpec).append("|").append(refSpec).append("; focus");
+
+    return cmd.toString();
+  }
+
+  @Override
+  public String openCommandFile(String path)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
+    return "open cmd:" + path;
+  }
+
+  @Override
+  public String saveSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
+    return "save " + filepath;
+  }
+
+  /**
+   * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera
+   * atomspec string, e.g.
+   * 
+   * <pre>
+   * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
+   * </pre>
+   * 
+   * where
+   * <ul>
+   * <li>#0 is a model number</li>
+   * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
+   * <li>.A is a chain identifier</li>
+   * <li>residue ranges are separated by comma</li>
+   * <li>atomspecs for distinct models are separated by | (or)</li>
+   * </ul>
+   * 
+   * <pre>
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (Integer model : atomSpec.getModels())
+    {
+      if (!firstModel)
+      {
+        sb.append("|");
+      }
+      firstModel = false;
+      appendModel(sb, model, atomSpec, alphaOnly);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * A helper method to append an atomSpec string for atoms in the given model
+   * 
+   * @param sb
+   * @param model
+   * @param atomSpec
+   * @param alphaOnly
+   */
+  protected void appendModel(StringBuilder sb, Integer model,
+          AtomSpecModel atomSpec, boolean alphaOnly)
+  {
+    sb.append("#").append(model).append(":");
+
+    boolean firstPositionForModel = true;
+
+    for (String chain : atomSpec.getChains(model))
+    {
+      chain = " ".equals(chain) ? chain : chain.trim();
+
+      List<int[]> rangeList = atomSpec.getRanges(model, chain);
+
+      /*
+       * sort ranges into ascending start position order
+       */
+      Collections.sort(rangeList, IntRangeComparator.ASCENDING);
+
+      int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
+      int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
+
+      Iterator<int[]> iterator = rangeList.iterator();
+      while (iterator.hasNext())
+      {
+        int[] range = iterator.next();
+        if (range[0] <= end + 1)
+        {
+          /*
+           * range overlaps or is contiguous with the last one
+           * - so just extend the end position, and carry on
+           * (unless this is the last in the list)
+           */
+          end = Math.max(end, range[1]);
+        }
+        else
+        {
+          /*
+           * we have a break so append the last range
+           */
+          appendRange(sb, start, end, chain, firstPositionForModel, false);
+          firstPositionForModel = false;
+          start = range[0];
+          end = range[1];
+        }
+      }
+
+      /*
+       * and append the last range
+       */
+      if (!rangeList.isEmpty())
+      {
+        appendRange(sb, start, end, chain, firstPositionForModel, false);
+        firstPositionForModel = false;
+      }
+    }
+    if (alphaOnly)
+    {
+      /*
+       * restrict to alpha carbon, no alternative locations
+       * (needed to ensuring matching atom counts for superposition)
+       */
+      sb.append("@CA|P").append(NO_ALTLOCS);
+    }
+  }
+
+  @Override
+  public String showBackbone()
+  {
+    return "~display all;chain @CA|P";
+  }
+
 }
index 7693802..580fa4b 100644 (file)
  */
 package jalview.ext.rbvi.chimera;
 
-import jalview.api.AlignViewportI;
-import jalview.api.AlignmentViewPanel;
-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.StructureSelectionManager;
+import jalview.structure.AtomSpecModel;
 import jalview.util.ColorUtils;
-import jalview.util.Comparison;
+import jalview.util.IntRangeComparator;
 
 import java.awt.Color;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Routines for generating ChimeraX commands for Jalview/ChimeraX binding
@@ -51,500 +36,215 @@ public class ChimeraXCommands extends ChimeraCommands
 {
   private static final String CMD_COLOUR_BY_CHARGE = "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow";
 
-  /**
-   * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
-   * builds a ChimeraX format atom spec
-   * 
-   * @param modelAndChainRanges
-   */
-  protected static String getAtomSpec(
-          Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
+  @Override
+  public String colourByCharge()
   {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModelForColour = true;
-    for (Integer model : modelAndChainRanges.keySet())
-    {
-      boolean firstPositionForModel = true;
-      if (!firstModelForColour)
-      {
-        sb.append("|");
-      }
-      firstModelForColour = false;
-      sb.append("#").append(model).append(":");
-
-      final Map<String, List<int[]>> modelData = modelAndChainRanges
-              .get(model);
-      for (String chain : modelData.keySet())
-      {
-        boolean hasChain = !"".equals(chain.trim());
-        for (int[] range : modelData.get(chain))
-        {
-          if (!firstPositionForModel)
-          {
-            sb.append(",");
-          }
-          if (range[0] == range[1])
-          {
-            sb.append(range[0]);
-          }
-          else
-          {
-            sb.append(range[0]).append("-").append(range[1]);
-          }
-          if (hasChain)
-          {
-            sb.append(".").append(chain);
-          }
-          firstPositionForModel = false;
-        }
-      }
-    }
-    return sb.toString();
+    return CMD_COLOUR_BY_CHARGE;
   }
 
-  /**
-   * <pre>
-   * Build a data structure which records contiguous subsequences for each colour. 
-   * From this we can easily generate the Chimera command for colour by sequence.
-   * Color
-   *     Model number
-   *         Chain
-   *             list of start/end ranges
-   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
-   * </pre>
-   */
-  protected static Map<Object, AtomSpecModel> buildColoursMap(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
-          AlignmentViewPanel viewPanel)
+  @Override
+  public String getResidueSpec(String residue)
   {
-    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++)
-    {
-      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));
+    return ":" + residue;
+  }
 
-              if (pos < 1 || pos == lastPos)
-              {
-                continue;
-              }
+  @Override
+  public String setBackgroundColour(Color col)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/set.html
+    return "set bgColor " + ColorUtils.toTkCode(col);
+  }
 
-              Color colour = sr.getResidueColour(seq, r, finder);
+  @Override
+  protected String getColourCommand(AtomSpecModel colourData,
+          Color colour)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html
+    String colourCode = getColourString(colour);
 
-              /*
-               * darker colour for hidden regions
-               */
-              if (!cs.isVisible(r))
-              {
-                colour = Color.GRAY;
-              }
+    return "color " + getAtomSpec(colourData, false) + " " + colourCode;
+  }
 
-              final String chain = mapping[m].getChain();
+  @Override
+  public String focusView()
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html
+    return "view";
+  }
 
-              /*
-               * 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, pdbfnum, startPos,
-                          lastPos, lastChain);
-                }
-                startPos = pos;
-              }
-              lastColour = colour;
-              lastPos = pos;
-              lastChain = chain;
-            }
-            // final colour range
-            if (lastColour != null)
-            {
-              addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
-                      lastPos, lastChain);
-            }
-            // break;
-          }
-        }
-      }
-    }
-    return colourMap;
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
   }
 
   /**
+   * Returns a viewer command to set the given residue attribute value on
+   * residues specified by the AtomSpecModel, for example
+   * 
    * <pre>
-   * Helper method to build a map of 
-   *   { featureType, { feature value, AtomSpecModel } }
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
    * </pre>
    * 
-   * @param ssm
-   * @param files
-   * @param seqs
-   * @param viewPanel
+   * @param attributeName
+   * @param attributeValue
+   * @param atomSpecModel
    * @return
    */
-  protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
-          StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
-          AlignmentViewPanel viewPanel)
+  @Override
+  protected String getSetAttributeCommand(String attributeName,
+          String attributeValue, AtomSpecModel atomSpecModel)
   {
-    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
-
-    FeatureRenderer fr = viewPanel.getFeatureRenderer();
-    if (fr == null)
-    {
-      return theMap;
-    }
-
-    AlignViewportI viewport = viewPanel.getAlignViewport();
-    List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
-
-    /*
-     * 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;
-    }
-
-    AlignmentI alignment = viewPanel.getAlignment();
-    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-    {
-      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("setattr ").append(getAtomSpec(atomSpecModel, false));
+    sb.append(" res ").append(attributeName).append(" '")
+            .append(attributeValue).append("'");
+    sb.append(" create true");
+    return sb.toString();
+  }
 
-      if (mapping == null || mapping.length < 1)
-      {
-        continue;
-      }
+  @Override
+  public String openCommandFile(String path)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
+    return "open " + path;
+  }
 
-      for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
-      {
-        for (int m = 0; m < mapping.length; m++)
-        {
-          final SequenceI seq = seqs[pdbfnum][seqNo];
-          int sp = alignment.findIndex(seq);
-          StructureMapping structureMapping = mapping[m];
-          if (structureMapping.getSequence() == seq && sp > -1)
-          {
-            /*
-             * found a sequence with a mapping to a structure;
-             * now scan its features
-             */
-            if (!visibleFeatures.isEmpty())
-            {
-              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
-                      theMap, pdbfnum);
-            }
-            if (showLinkedFeatures)
-            {
-              scanComplementFeatures(complementRenderer, structureMapping,
-                      seq, theMap, pdbfnum);
-            }
-          }
-        }
-      }
-    }
-    return theMap;
+  @Override
+  public String saveSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
+    return "save session " + filepath;
   }
 
   /**
-   * Scans visible features in mapped positions of the CDS/peptide complement, and
-   * adds any found to the map of attribute values/structure positions
+   * Returns the range(s) formatted as a ChimeraX atomspec, for example
+   * <p>
+   * #1/A:2-20,30-40/B:10-20|#2/A:12-30
    * 
-   * @param complementRenderer
-   * @param structureMapping
-   * @param seq
-   * @param theMap
-   * @param modelNumber
+   * @return
    */
-  protected static void scanComplementFeatures(
-          FeatureRenderer complementRenderer,
-          StructureMapping structureMapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    /*
-     * for each sequence residue mapped to a structure position...
-     */
-    for (int seqPos : structureMapping.getMapping().keySet())
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (Integer model : atomSpec.getModels())
     {
-      /*
-       * find visible complementary features at mapped position(s)
-       */
-      MappedFeatures mf = complementRenderer
-              .findComplementFeaturesAtResidue(seq, seqPos);
-      if (mf != null)
+      if (!firstModel)
       {
-        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());
-            }
-          }
-        }
+        sb.append("|");
+      }
+      firstModel = false;
+      appendModel(sb, model, atomSpec);
+      if (alphaOnly)
+      {
+        sb.append("@CA|P");
       }
+      // todo: is there ChimeraX syntax to exclude altlocs?
     }
+    return sb.toString();
   }
 
   /**
-   * 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.
+   * A helper method to append an atomSpec string for atoms in the given model
    * 
-   * @param visibleFeatures
-   * @param mapping
-   * @param seq
-   * @param theMap
-   * @param modelNumber
+   * @param sb
+   * @param model
+   * @param atomSpec
    */
-  protected static void scanSequenceFeatures(List<String> visibleFeatures,
-          StructureMapping mapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  protected void appendModel(StringBuilder sb, Integer model,
+          AtomSpecModel atomSpec)
   {
-    List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
-            visibleFeatures.toArray(new String[visibleFeatures.size()]));
-    for (SequenceFeature sf : sfs)
+    sb.append("#").append(model);
+
+    for (String chain : atomSpec.getChains(model))
     {
-      String type = sf.getType();
+      boolean firstPositionForChain = true;
+      chain = " ".equals(chain) ? chain : chain.trim();
+      sb.append("/").append(chain).append(":");
+      List<int[]> rangeList = atomSpec.getRanges(model, chain);
 
       /*
-       * Don't copy features which originated from Chimera
+       * sort ranges into ascending start position order
        */
-      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup()))
-      {
-        continue;
-      }
+      Collections.sort(rangeList, IntRangeComparator.ASCENDING);
 
-      List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
-              sf.getEnd());
+      int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
+      int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
 
-      if (!mappedRanges.isEmpty())
+      Iterator<int[]> iterator = rangeList.iterator();
+      while (iterator.hasNext())
       {
-        String value = sf.getDescription();
-        if (value == null || value.length() == 0)
+        int[] range = iterator.next();
+        if (range[0] <= end + 1)
         {
-          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);
+          /*
+           * range overlaps or is contiguous with the last one
+           * - so just extend the end position, and carry on
+           * (unless this is the last in the list)
+           */
+          end = Math.max(end, range[1]);
         }
-        for (int[] range : mappedRanges)
+        else
         {
-          addAtomSpecRange(featureValues, value, modelNumber, range[0],
-                  range[1], mapping.getChain());
+          /*
+           * we have a break so append the last range
+           */
+          appendRange(sb, start, end, chain, firstPositionForChain, true);
+          start = range[0];
+          end = range[1];
+          firstPositionForChain = false;
         }
       }
-    }
-  }
 
-  /**
-   * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
-   * for a 'Jalview' namespace, and any non-alphanumeric character is converted
-   * to an underscore.
-   * 
-   * @param featureType
-   * @return
-   * 
-   *         <pre>
-   * &#64;see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
-   *         </pre>
-   */
-  protected static String makeAttributeName(String featureType)
-  {
-    StringBuilder sb = new StringBuilder();
-    if (featureType != null)
-    {
-      for (char c : featureType.toCharArray())
+      /*
+       * and append the last range
+       */
+      if (!rangeList.isEmpty())
       {
-        sb.append(Character.isLetterOrDigit(c) ? c : '_');
+        appendRange(sb, start, end, chain, firstPositionForChain, true);
       }
+      firstPositionForChain = false;
     }
-    String attName = NAMESPACE_PREFIX + sb.toString();
-
-    /*
-     * Chimera treats an attribute name ending in 'color' as colour-valued;
-     * Jalview doesn't, so prevent this by appending an underscore
-     */
-    if (attName.toUpperCase().endsWith("COLOR"))
-    {
-      attName += "_";
-    }
-
-    return attName;
   }
 
   @Override
-  public String colourByCharge()
+  public String showBackbone()
   {
-    return CMD_COLOUR_BY_CHARGE;
+    return "~display all;show @CA|P pbonds";
   }
 
   @Override
-  public String colourByResidues(Map<String, Color> colours)
+  public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref)
   {
-    StringBuilder cmd = new StringBuilder(12 * colours.size());
-
     /*
-     * concatenate commands like
-     * color :VAL #4949b6
+     * Form ChimeraX match command to match spec to ref
+     * 
+     * match #1/A:2-94 toAtoms #2/A:1-93
+     * 
+     * @see
+     * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
      */
-    for (Entry<String, Color> entry : colours.entrySet())
-    {
-      String colorSpec = ColorUtils.toTkCode(entry.getValue());
-      String resCode = entry.getKey();
-      cmd.append("color :").append(resCode).append(" ").append(colorSpec)
-              .append(CMD_SEPARATOR);
-    }
-    return cmd.toString();
-  }
-
-  @Override
-  public String setBackgroundColour(Color col)
-  {
-    return "set bgColor " + ColorUtils.toTkCode(col);
-  }
-
-  @Override
-  protected String getColourCommand(AtomSpecModel colourData,
-          String colourCode)
-  {
-    return "color " + colourData.getAtomSpecX() + " " + colourCode;
-  }
-
-  @Override
-  public String focusView()
-  {
-    return "view";
-  }
+    StringBuilder cmd = new StringBuilder();
+    String atomSpec = getAtomSpec(spec, true);
+    String refSpec = getAtomSpec(ref, true);
+    cmd.append("align ").append(atomSpec).append(" toAtoms ")
+            .append(refSpec);
 
-  /**
-   * {@inheritDoc}
-   * 
-   * @return
-   */
-  protected static int getModelStartNo()
-  {
-    return 1;
-  }
+    /*
+     * show superposed residues as ribbon, others as chain
+     */
+    cmd.append("; ribbon ");
+    cmd.append(getAtomSpec(spec, false)).append("|");
+    cmd.append(getAtomSpec(ref, false)).append("; view");
 
-  /**
-   * Returns a viewer command to set the given residue attribute value on
-   * residues specified by the AtomSpecModel, for example
-   * 
-   * <pre>
-   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
-   * </pre>
-   * 
-   * @param attributeName
-   * @param attributeValue
-   * @param atomSpecModel
-   * @return
-   */
-  @Override
-  protected String getSetAttributeCommand(String attributeName,
-          String attributeValue, AtomSpecModel atomSpecModel)
-  {
-    StringBuilder sb = new StringBuilder(128);
-    sb.append("setattr ").append(atomSpecModel.getAtomSpecX());
-    sb.append(" res ").append(attributeName).append(" '")
-            .append(attributeValue).append("'");
-    sb.append(" create true");
-    return sb.toString();
+    return cmd.toString();
   }
 
 }
index be1de5a..731ffea 100644 (file)
@@ -30,11 +30,11 @@ import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.gui.Preferences;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.httpserver.AbstractRequestHandler;
 import jalview.io.DataSourceType;
 import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommandsI.SuperposeData;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
@@ -184,11 +184,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   {
     super(ssm, pdbentry, sequenceIs, protocol);
     chimeraManager = new ChimeraManager(new StructureManager(true));
-    String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY);
-    chimeraManager.setChimeraX(ViewerType.CHIMERAX.name().equals(viewerType));
+    chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType()));
     setStructureCommands(new ChimeraCommands());
   }
 
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.CHIMERA;
+  }
+
   /**
    * Starts a thread that waits for the Chimera process to finish, so that we can
    * then close the associated resources. This avoids leaving orphaned Chimera
@@ -265,7 +270,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   /**
    * {@inheritDoc}
    */
-  @Override
   public String superposeStructures(AlignmentI[] _alignment,
           int[] _refStructure, HiddenColumns[] _hiddenCols)
   {
@@ -309,7 +313,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       SuperposeData[] structures = new SuperposeData[files.length];
       for (int f = 0; f < files.length; f++)
       {
-        structures[f] = new SuperposeData(alignment.getWidth());
+        structures[f] = new SuperposeData(alignment.getWidth(), f);
       }
 
       /*
@@ -519,8 +523,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       }
       else
       {
-        allComs.append("chain @CA|P; ribbon ; focus");
-        allComs.append(selectioncom.toString());
+        allComs.append("chain @CA|P; ribbon ");
+        allComs.append(selectioncom.toString()).append("; focus");
       }
       // allComs.append("; ~display all; chain @CA|P; ribbon ")
       // .append(selectioncom.toString()).append("; focus");
@@ -647,16 +651,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Send a Chimera 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 abstract void sendAsynchronousCommand(String command,
-          String progressMsg);
-
-  /**
    * @param command
    */
   protected void executeWhenReady(String command)
@@ -862,7 +856,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
        * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
        * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
        */
-      String command = getSaveSessionCommand(filepath);
+      String command = getCommandGenerator().saveSession(filepath);
       List<String> reply = chimeraManager.sendChimeraCommand(command, true);
       if (reply.contains("Session written"))
       {
@@ -878,17 +872,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Returns the command to save the viewer session to the given file path
-   * 
-   * @param filepath
-   * @return
-   */
-  protected String getSaveSessionCommand(String filepath)
-  {
-    return "save " + filepath;
-  }
-
-  /**
    * Ask Chimera to open a session file. Returns true if successful, else false.
    * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
    * this command to work.
@@ -988,7 +971,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       out.flush();
       out.close();
       String path = tmp.getAbsolutePath();
-      String command = getOpenCommandFileCommand(path);
+      String command = getCommandGenerator().openCommandFile(path);
       sendAsynchronousCommand(command, null);
     } catch (IOException e)
     {
@@ -998,18 +981,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Returns the command for the structure viewer to open a file of commands at
-   * the given file path
-   * 
-   * @param path
-   * @return
-   */
-  protected String getOpenCommandFileCommand(String path)
-  {
-    return "open cmd:" + path;
-  }
-
-  /**
    * Returns the file extension required for a file of commands to be read by
    * the structure viewer
    * @return
index ab5ee7a..1a5e901 100644 (file)
@@ -208,8 +208,7 @@ public class ChimeraViewFrame extends StructureViewerBase
           SequenceI[][] seqs)
   {
     createProgressBar();
-    jmb = new JalviewChimeraBindingModel(this,
-            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+    jmb = newBindingModel(ap, pdbentrys, seqs);
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
 
@@ -237,6 +236,13 @@ public class ChimeraViewFrame extends StructureViewerBase
 
   }
 
+  protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
+          PDBEntry[] pdbentrys, SequenceI[][] seqs)
+  {
+    return new JalviewChimeraBindingModel(this,
+            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+  }
+
   /**
    * Create a new viewer from saved session state data including Chimera session
    * file
@@ -753,20 +759,4 @@ public class ChimeraViewFrame extends StructureViewerBase
   {
     return "Chimera";
   }
-
-  /**
-   * Sends commands to align structures according to associated alignment(s).
-   * 
-   * @return
-   */
-  @Override
-  protected String alignStructsWithAllAlignPanels()
-  {
-    String reply = super.alignStructsWithAllAlignPanels();
-    if (reply != null)
-    {
-      statusBar.setText("Superposition failed: " + reply);
-    }
-    return reply;
-  }
 }
index de8820d..b33ccd6 100644 (file)
@@ -1,5 +1,7 @@
 package jalview.gui;
 
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
 
 /**
@@ -13,10 +15,22 @@ import jalview.gui.StructureViewer.ViewerType;
 public class ChimeraXViewFrame extends ChimeraViewFrame
 {
 
+  public ChimeraXViewFrame(PDBEntry pdb, SequenceI[] seqsForPdb,
+          String[] chains, AlignmentPanel ap)
+  {
+    super(pdb, seqsForPdb, chains, ap);
+  }
+
+  public ChimeraXViewFrame(PDBEntry[] pdbsForFile, boolean superposeAdded,
+          SequenceI[][] theSeqs, AlignmentPanel ap)
+  {
+    super(pdbsForFile, superposeAdded, theSeqs, ap);
+  }
+
   @Override
   public ViewerType getViewerType()
   {
-    return null;// ViewerType.CHIMERAX;
+    return ViewerType.CHIMERAX;
   }
 
   @Override
@@ -25,4 +39,12 @@ public class ChimeraXViewFrame extends ChimeraViewFrame
     return "ChimeraX";
   }
 
+  @Override
+  protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
+          PDBEntry[] pdbentrys, SequenceI[][] seqs)
+  {
+    return new JalviewChimeraXBindingModel(this,
+            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+  }
+
 }
index f6cfb59..49655a4 100644 (file)
@@ -62,34 +62,4 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
       }
     });
   }
-
-  /**
-   * Send an asynchronous command to Chimera, in a new thread, optionally with
-   * an 'in progress' message in a progress bar somewhere
-   */
-  @Override
-  protected void sendAsynchronousCommand(final String command,
-          final 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);
-          }
-        }
-      }
-    });
-  }
 }
index 0779bef..3a6c89c 100644 (file)
@@ -2,6 +2,8 @@ package jalview.gui;
 
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
+import jalview.ext.rbvi.chimera.ChimeraXCommands;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
 
@@ -19,6 +21,7 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
           SequenceI[][] sequenceIs, DataSourceType protocol)
   {
     super(chimeraViewFrame, ssm, pdbentry, sequenceIs, protocol);
+    setStructureCommands(new ChimeraXCommands());
   }
 
   @Override
@@ -54,26 +57,6 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
   }
 
   /**
-   * {@inheritDoc}
-   * 
-   * @return
-   */
-  @Override
-  protected String getOpenCommandFileCommand(String path)
-  {
-    return "open " + path;
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  @Override
-  protected String getSaveSessionCommand(String filepath)
-  {
-    return "save session " + filepath;
-  }
-
-  /**
    * Returns the file extension to use for a saved viewer session file
    * 
    * @return
@@ -90,4 +73,10 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
     return "http://www.rbvi.ucsf.edu/chimerax/docs/user/index.html";
   }
 
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.CHIMERAX;
+  }
+
 }
index c8012a6..79d3836 100644 (file)
@@ -160,12 +160,16 @@ public class StructureViewer
     {
       sview = new AppJmol(ap, superposeAdded, pdbsForFile, theSeqs);
     }
-    else if (viewerType.equals(ViewerType.CHIMERA)
-            || viewerType.equals(ViewerType.CHIMERAX))
+    else if (viewerType.equals(ViewerType.CHIMERA))
     {
       sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs,
               ap);
     }
+    else if (viewerType.equals(ViewerType.CHIMERAX))
+    {
+      sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs,
+              ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
@@ -302,11 +306,14 @@ public class StructureViewer
     {
       sview = new AppJmol(pdb, seqsForPdb, null, ap);
     }
-    else if (viewerType.equals(ViewerType.CHIMERA)
-            || viewerType.equals(ViewerType.CHIMERAX))
+    else if (viewerType.equals(ViewerType.CHIMERA))
     {
       sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap);
     }
+    else if (viewerType.equals(ViewerType.CHIMERAX))
+    {
+      sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
index aa39ee7..6dd7d50 100644 (file)
@@ -22,9 +22,7 @@ package jalview.gui;
 
 import jalview.api.AlignmentViewPanel;
 import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
@@ -772,19 +770,8 @@ public abstract class StructureViewerBase extends GStructureViewer
     String reply = null;
     try
     {
-      AlignmentI[] als = new Alignment[_alignwith.size()];
-      HiddenColumns[] alc = new HiddenColumns[_alignwith.size()];
-      int[] alm = new int[_alignwith.size()];
-      int a = 0;
-
-      for (AlignmentViewPanel alignPanel : _alignwith)
-      {
-        als[a] = alignPanel.getAlignment();
-        alm[a] = -1;
-        alc[a++] = alignPanel.getAlignment().getHiddenColumns();
-      }
-      reply = getBinding().superposeStructures(als, alm, alc);
-      if (reply != null)
+      reply = getBinding().superposeStructures(_alignwith);
+      if (reply != null && !reply.isEmpty())
       {
         String text = MessageManager
                 .formatMessage("error.superposition_failed", reply);
diff --git a/src/jalview/structure/AtomSpecModel.java b/src/jalview/structure/AtomSpecModel.java
new file mode 100644 (file)
index 0000000..1b7d284
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.structure;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A class to model a set of models, chains and atom range positions
+ * 
+ */
+public class AtomSpecModel
+{
+  /*
+   * { modelNo, {chainCode, List<from-to> ranges} }
+   */
+  private Map<Integer, Map<String, BitSet>> atomSpec;
+
+  /**
+   * Constructor
+   */
+  public AtomSpecModel()
+  {
+    atomSpec = new TreeMap<>();
+  }
+
+  /**
+   * Adds one contiguous range to this atom spec
+   * 
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public void addRange(int model, int startPos, int endPos, String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour and model
+     */
+    Map<String, BitSet> modelData = atomSpec.get(model);
+    if (modelData == null)
+    {
+      atomSpec.put(model, modelData = new TreeMap<>());
+    }
+
+    /*
+     * Get/initialize map of data for colour, model and chain
+     */
+    BitSet chainData = modelData.get(chain);
+    if (chainData == null)
+    {
+      chainData = new BitSet();
+      modelData.put(chain, chainData);
+    }
+
+    /*
+     * Add the start/end positions
+     */
+    chainData.set(startPos, endPos + 1);
+  }
+
+  public Iterable<Integer> getModels()
+  {
+    return atomSpec.keySet();
+  }
+
+  public Iterable<String> getChains(Integer model)
+  {
+    return atomSpec.containsKey(model) ? atomSpec.get(model).keySet()
+            : null;
+  }
+
+  /**
+   * Returns a (possibly empty) ordered list of contiguous atom ranges for the
+   * given model and chain.
+   * 
+   * @param model
+   * @param chain
+   * @return
+   */
+  public List<int[]> getRanges(Integer model, String chain)
+  {
+    List<int[]> ranges = new ArrayList<>();
+    if (atomSpec.containsKey(model))
+    {
+      BitSet bs = atomSpec.get(model).get(chain);
+      int start = 0;
+      if (bs != null)
+      {
+        start = bs.nextSetBit(start);
+        int end = 0;
+        while (start != -1)
+        {
+          end = bs.nextClearBit(start);
+          ranges.add(new int[] { start, end - 1 });
+          start = bs.nextSetBit(end);
+        }
+      }
+    }
+    return ranges;
+  }
+}
index 119a27a..921c9cd 100644 (file)
@@ -1,7 +1,21 @@
 package jalview.structure;
 
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.api.SequenceRenderer;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.util.Comparison;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * A base class holding methods useful to all classes that implement commands
@@ -12,6 +26,17 @@ import jalview.datamodel.SequenceI;
  */
 public abstract class StructureCommandsBase implements StructureCommandsI
 {
+  private static final String CMD_SEPARATOR = ";";
+
+  /**
+   * Returns something that separates concatenated commands
+   * 
+   * @return
+   */
+  protected static String getCommandSeparator()
+  {
+    return CMD_SEPARATOR;
+  }
 
   @Override
   public String[] setAttributesForFeatures(StructureSelectionManager ssm,
@@ -20,4 +45,284 @@ public abstract class StructureCommandsBase implements StructureCommandsI
     // default does nothing, override where this is implemented
     return null;
   }
+
+  /**
+   * Returns the lowest model number used by the structure viewer
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 0;
+  }
+
+  /**
+   * <pre>
+   * Build a data structure which records contiguous subsequences for each colour. 
+   * From this we can easily generate the viewer command for colour by sequence.
+   * Color
+   *     Model number
+   *         Chain
+   *             list of start/end ranges
+   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * </pre>
+   * 
+   * @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 + 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);
+  }
+
+  /**
+   * Returns a colour formatted suitable for use in viewer command syntax
+   * 
+   * @param colour
+   * @return
+   */
+  protected abstract String getColourString(Color colour);
+
+  /**
+   * Traverse the map of colours/models/chains/positions to construct a list of
+   * 'color' commands (one per distinct colour used). The format of each command
+   * is specific to the structure viewer.
+   * 
+   * @param colourMap
+   * @return
+   */
+  public List<String> buildColourCommands(
+          Map<Object, AtomSpecModel> colourMap)
+  {
+    /*
+     * This version concatenates all commands into a single String (semi-colon
+     * delimited). If length limit issues arise, refactor to return one color
+     * command per colour.
+     */
+    List<String> commands = new ArrayList<>();
+    StringBuilder sb = new StringBuilder(256);
+    boolean firstColour = true;
+    for (Object key : colourMap.keySet())
+    {
+      Color colour = (Color) key;
+      if (!firstColour)
+      {
+        sb.append(getCommandSeparator()).append(" ");
+      }
+      firstColour = false;
+      final AtomSpecModel colourData = colourMap.get(colour);
+      sb.append(getColourCommand(colourData, colour));
+    }
+    commands.add(sb.toString());
+    return commands;
+  }
+
+  /**
+   * Returns a command to colour the atoms represented by {@code atomSpecModel}
+   * with the colour specified by {@code colourCode}.
+   * 
+   * @param atomSpecModel
+   * @param colour
+   * @return
+   */
+  protected String getColourCommand(AtomSpecModel atomSpecModel, Color colour)
+  {
+    String atomSpec = getAtomSpec(atomSpecModel, false);
+    return getColourCommand(atomSpec, colour);
+  }
+
+  /**
+   * Returns a command to colour the atoms described (in viewer command syntax)
+   * by {@code atomSpec} with the colour specified by {@code colourCode}
+   * 
+   * @param atomSpec
+   * @param colour
+   * @return
+   */
+  protected abstract String getColourCommand(String atomSpec, Color colour);
+
+  @Override
+  public String colourByResidues(Map<String, Color> colours)
+  {
+    StringBuilder cmd = new StringBuilder(12 * colours.size());
+  
+    for (Entry<String, Color> entry : colours.entrySet())
+    {
+      String residue = entry.getKey();
+      String atomSpec = getResidueSpec(residue);
+      cmd.append(getColourCommand(atomSpec, entry.getValue()));
+      cmd.append(getCommandSeparator());
+    }
+    return cmd.toString();
+  }
+
+  /**
+   * Helper method to append one start-end range to an atomspec string
+   * 
+   * @param sb
+   * @param start
+   * @param end
+   * @param chain
+   * @param firstPositionForModel
+   */
+  protected void appendRange(StringBuilder sb, int start, int end,
+          String chain, boolean firstPositionForModel, boolean isChimeraX)
+  {
+    if (!firstPositionForModel)
+    {
+      sb.append(",");
+    }
+    if (end == start)
+    {
+      sb.append(start);
+    }
+    else
+    {
+      sb.append(start).append("-").append(end);
+    }
+
+    if (!isChimeraX)
+    {
+      sb.append(".");
+      if (!" ".equals(chain))
+      {
+        sb.append(chain);
+      }
+    }
+  }
+
+  /**
+   * Returns the atom specifier meaning all occurrences of the given residue
+   * 
+   * @param residue
+   * @return
+   */
+  protected abstract String getResidueSpec(String residue);
 }
index eda5aa9..359eac6 100644 (file)
@@ -1,7 +1,6 @@
 package jalview.structure;
 
 import jalview.api.AlignmentViewPanel;
-import jalview.api.SequenceRenderer;
 import jalview.datamodel.SequenceI;
 
 import java.awt.Color;
@@ -17,6 +16,41 @@ import java.util.Map;
  */
 public interface StructureCommandsI
 {
+  /**
+   * Data bean class to simplify parameterisation in superposeStructures
+   */
+  public class SuperposeData
+  {
+    public String filename;
+
+    public String pdbId;
+
+    public String chain = "";
+
+    public boolean isRna;
+
+    /*
+     * The pdb residue number (if any) mapped to columns of the alignment
+     */
+    public int[] pdbResNo; // or use SparseIntArray?
+
+    public int modelNo;
+
+    /**
+     * Constructor
+     * 
+     * @param width
+     *          width of alignment (number of columns that may potentially
+     *          participate in superposition)
+     * @param model
+     *          structure viewer model number
+     */
+    public SuperposeData(int width, int model)
+    {
+      pdbResNo = new int[width];
+      modelNo = model;
+    }
+  }
 
   /**
    * Returns the command to colour by chain
@@ -57,19 +91,14 @@ public interface StructureCommandsI
 
   /**
    * Returns commands to colour mapped residues of structures according to
-   * Jalview's colouring (including feature colouring if applied)
+   * Jalview's colouring (including feature colouring if applied). Parameter is
+   * a map from Color to a model of all residues assigned that colour.
    * 
-   * @param structureSelectionManager
-   * @param files
-   * @param seqs
-   * @param sr
-   * @param alignmentv
+   * @param colourMap
    * @return
    */
-  String[] colourBySequence(
-          StructureSelectionManager structureSelectionManager,
-          String[] files, SequenceI[][] seqs, SequenceRenderer sr,
-          AlignmentViewPanel alignmentv);
+
+  String[] colourBySequence(Map<Object, AtomSpecModel> colourMap);
 
   /**
    * Returns a command to centre the display in the structure viewer
@@ -100,4 +129,61 @@ public interface StructureCommandsI
   String[] setAttributesForFeatures(StructureSelectionManager ssm,
           String[] files, SequenceI[][] sequence, AlignmentViewPanel avp);
 
+  /**
+   * Returns a command to superpose structures by closest positioning of
+   * residues in {@code atomSpec} to the corresponding residues in {@ refAtoms}.
+   * If wanted, this may include commands to visually highlight the residues
+   * that were used for the superposition.
+   * 
+   * @param refAtoms
+   * @param atomSpec
+   * @return
+   */
+  String superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec);
+
+  /**
+   * Returns a command to open a file of commands at the given path
+   * 
+   * @param path
+   * @return
+   */
+  String openCommandFile(String path);
+
+  /**
+   * Returns a command to save the current viewer session state to the given
+   * file
+   * 
+   * @param filepath
+   * @return
+   */
+  String saveSession(String filepath);
+
+  /**
+   * Returns a representation of the atom set represented by the model, in
+   * viewer syntax format. If {@code alphaOnly} is true, this is restricted to
+   * Alpha Carbon (peptide) or Phosphorous (rna) only
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   */
+  String getAtomSpec(AtomSpecModel model, boolean alphaOnly);
+
+  /**
+   * Returns the lowest model number used by the structure viewer (likely 0 or
+   * 1)
+   * 
+   * @return
+   */
+  // TODO remove by refactoring so command generation is purely driven by
+  // AtomSpecModel objects derived in the binding classes?
+  int getModelStartNo();
+
+  /**
+   * Show only the backbone of the peptide (cartoons in Jmol, chain in Chimera)
+   * 
+   * @return
+   */
+  String showBackbone();
 }
index 92b00c7..4dfdc2a 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) sequence to it 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,184 @@ 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);
+          }
+        }
+      }
+    });
+
+  }
+
+  /**
+   * <pre>
+   * Build a data structure which records residues for each colour. 
+   * From this we can easily generate the viewer command for colour by sequence.
+   * Color
+   *     Model number
+   *         Chain
+   *             Residue positions
+   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * </pre>
+   * 
+   * @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);
+  }
 }
index 5846b33..c3ece9d 100644 (file)
@@ -32,6 +32,8 @@ import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.gui.SequenceRenderer;
 import jalview.schemes.JalviewColourScheme;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandsI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 
@@ -138,4 +140,62 @@ public class JmolCommandsTest
     assertTrue(
             chainBCommand.contains(";select 26-30:B/2.1;color[73,73,182]"));
   }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    StructureCommandsI testee = new JmolCommands();
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange(1, 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1");
+    model.addRange(1, 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1|8:A/1.1");
+    model.addRange(1, 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-4:A/1.1|8:A/1.1|5-7:B/1.1");
+    model.addRange(1, 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1");
+    model.addRange(2, 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1");
+    model.addRange(2, 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange(1, 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange(1, 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange(2, 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1");
+    model.addRange(5, 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1|25-35:/5.1");
+  
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    StructureCommandsI testee = new JmolCommands();
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange(1, 12, 14, "A");
+    ref.addRange(1, 18, 18, "B");
+    ref.addRange(1, 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange(2, 15, 17, "B");
+    toAlign.addRange(2, 20, 21, "B");
+    toAlign.addRange(2, 22, 22, "C");
+    String command = testee.superposeStructures(ref, toAlign);
+    String refSpec = "12-14:A/1.1|18:B/1.1|22-23:B/1.1";
+    String toAlignSpec = "15-17:B/2.1|20-21:B/2.1|22:C/2.1";
+    String expected = String.format(
+            "compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS {%s}{%s} ROTATE TRANSLATE ;select %s|%s;cartoons",
+            toAlignSpec, refSpec, toAlignSpec, refSpec);
+    assertEquals(command, expected);
+  }
 }
diff --git a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java b/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java
deleted file mode 100644 (file)
index 63d5e4e..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package jalview.ext.rbvi.chimera;
-
-import static org.testng.Assert.assertEquals;
-
-import org.testng.annotations.Test;
-
-public class AtomSpecModelTest
-{
-  @Test(groups = "Functional")
-  public void testGetAtomSpec()
-  {
-    AtomSpecModel model = new AtomSpecModel();
-    assertEquals(model.getAtomSpec(), "");
-    model.addRange(1, 2, 4, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A");
-    model.addRange(1, 8, 8, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A");
-    model.addRange(1, 5, 7, "B");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A,5-7.B");
-    model.addRange(1, 3, 5, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-5.A,8.A,5-7.B");
-    model.addRange(0, 1, 4, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B|#1:2-5.A,8.A,5-7.B");
-    model.addRange(0, 5, 9, "C");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B");
-    model.addRange(1, 8, 10, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(1, 8, 9, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(0, 3, 10, "C"); // subsumes 5-9
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology
-                                    // modelling
-    assertEquals(model.getAtomSpec(),
-            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
-
-  }
-
-}
index 0679098..d0e6155 100644 (file)
@@ -31,6 +31,8 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.SequenceRenderer;
 import jalview.schemes.JalviewColourScheme;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandsI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 
@@ -209,4 +211,98 @@ public class ChimeraCommandsTest
     // S and G are both coloured #4949b6
     assertTrue(theCommand.contains("color #4949b6 #0:26-30.A|#1:26-30.B"));
   }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    StructureCommandsI testee = new ChimeraCommands();
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange(1, 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A");
+    model.addRange(1, 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A");
+    model.addRange(1, 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A,5-7.B");
+    model.addRange(1, 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-5.A,8.A,5-7.B");
+    model.addRange(0, 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B|#1:2-5.A,8.A,5-7.B");
+    model.addRange(0, 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B");
+    model.addRange(1, 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange(1, 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange(0, 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology
+                                    // modelling
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
+
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec_alphaOnly()
+  {
+    StructureCommandsI testee = new ChimeraCommands();
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, true), "");
+    model.addRange(1, 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, true), "#1:2-4.A@CA|P");
+    model.addRange(1, 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, true), "#1:2-4.A,8.A@CA|P");
+    model.addRange(1, 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-4.A,8.A,5-7.B@CA|P");
+    model.addRange(1, 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-5.A,8.A,5-7.B@CA|P");
+    model.addRange(0, 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B@CA|P|#1:2-5.A,8.A,5-7.B@CA|P");
+    model.addRange(0, 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-7.B@CA|P");
+    model.addRange(1, 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P");
+    model.addRange(1, 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P");
+    model.addRange(0, 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,3-10.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P");
+    model.addRange(5, 25, 35, " "); // empty chain code
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,3-10.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P|#5:25-35.@CA|P");
+  
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    StructureCommandsI testee = new ChimeraCommands();
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange(1, 12, 14, "A");
+    ref.addRange(1, 18, 18, "B");
+    ref.addRange(1, 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange(2, 15, 17, "B");
+    toAlign.addRange(2, 20, 21, "B");
+    toAlign.addRange(2, 22, 22, "C");
+    String command = testee.superposeStructures(ref, toAlign);
+    String refSpec = "#1:12-14.A,18.B,22-23.B@CA|P&~@.B-Z&~@.2-9";
+    String toAlignSpec = "#2:15-17.B,20-21.B,22.C@CA|P&~@.B-Z&~@.2-9";
+    String expected = String.format(
+            "match %s %s;~display all; chain @CA|P; ribbon %s|%s; focus",
+            refSpec, toAlignSpec, refSpec, toAlignSpec);
+    assertEquals(command, expected);
+  }
 }
index d8b60c2..a9eaca3 100644 (file)
@@ -31,6 +31,8 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.SequenceRenderer;
 import jalview.schemes.JalviewColourScheme;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandsI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 
@@ -191,4 +193,25 @@ public class ChimeraXCommandsTest
     // S and G are both coloured #4949b6
     assertTrue(theCommand.contains("color #0/A:26-30|#1/B:26-30"));
   }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    StructureCommandsI testee = new ChimeraXCommands();
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange(1, 12, 14, "A");
+    ref.addRange(1, 18, 18, "B");
+    ref.addRange(1, 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange(2, 15, 17, "B");
+    toAlign.addRange(2, 20, 21, "B");
+    toAlign.addRange(2, 22, 22, "C");
+    String command = testee.superposeStructures(ref, toAlign);
+    String refSpec = "#1/A:12-14/B:18,22-23";
+    String toAlignSpec = "#2/B:15-17,20-21/C:22";
+    String expected = String.format(
+            "align %s %s;~display all; chain @CA|P; ribbon %s|%s; focus",
+            refSpec, toAlignSpec, refSpec, toAlignSpec);
+    assertEquals(command, expected);
+  }
 }
diff --git a/test/jalview/structure/AtomSpecModelTest.java b/test/jalview/structure/AtomSpecModelTest.java
new file mode 100644 (file)
index 0000000..8c6ead7
--- /dev/null
@@ -0,0 +1,51 @@
+package jalview.structure;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class AtomSpecModelTest
+{
+  @Test(groups="Functional")
+  public void testGetRanges()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertFalse(model.getModels().iterator().hasNext());
+    List<int[]> ranges = model.getRanges(1, "A");
+    assertTrue(ranges.isEmpty());
+
+    model.addRange(1, 12, 14, "A");
+    assertTrue(model.getRanges(1, "B").isEmpty());
+    assertTrue(model.getRanges(2, "A").isEmpty());
+    ranges = model.getRanges(1, "A");
+    assertEquals(ranges.size(), 1);
+    int[] range = ranges.get(0);
+    assertEquals(range[0], 12);
+    assertEquals(range[1], 14);
+
+    /*
+     * add some ranges; they should be coalesced and
+     * ordered when retrieved
+     */
+    model.addRange(1, 25, 25, "A");
+    model.addRange(1, 20, 24, "A");
+    model.addRange(1, 6, 8, "A");
+    model.addRange(1, 13, 18, "A");
+    model.addRange(1, 5, 6, "A");
+    ranges = model.getRanges(1, "A");
+    assertEquals(ranges.size(), 3);
+    range = ranges.get(0);
+    assertEquals(range[0], 5);
+    assertEquals(range[1], 8);
+    range = ranges.get(1);
+    assertEquals(range[0], 12);
+    assertEquals(range[1], 18);
+    range = ranges.get(2);
+    assertEquals(range[0], 20);
+    assertEquals(range[1], 25);
+  }
+}
index c890536..c201926 100644 (file)
@@ -28,17 +28,17 @@ import jalview.api.AlignmentViewPanel;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormats;
 import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommandsI.SuperposeData;
 import jalview.structure.StructureSelectionManager;
-import jalview.structures.models.AAStructureBindingModel.SuperposeData;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -149,13 +149,6 @@ public class AAStructureBindingModelTest
       }
       
       @Override
-      public String superposeStructures(AlignmentI[] alignments,
-              int[] structureIndices, HiddenColumns[] hiddenCols)
-      {
-        return null;
-      }
-      
-      @Override
       public void highlightAtoms(List<AtomSpec> atoms)
       {
       }
@@ -178,6 +171,12 @@ public class AAStructureBindingModelTest
       {
         return 0;
       }
+
+      @Override
+      protected ViewerType getViewerType()
+      {
+        return null;
+      }
     };
     String[][] chains = binder.getChains();
     assertFalse(chains == null || chains[0] == null,
@@ -247,13 +246,6 @@ public class AAStructureBindingModelTest
       }
 
       @Override
-      public String superposeStructures(AlignmentI[] als, int[] alm,
-              HiddenColumns[] alc)
-      {
-        return null;
-      }
-
-      @Override
       public SequenceRenderer getSequenceRenderer(
               AlignmentViewPanel alignment)
       {
@@ -272,6 +264,12 @@ public class AAStructureBindingModelTest
       {
         return 0;
       }
+
+      @Override
+      protected ViewerType getViewerType()
+      {
+        return null;
+      }
     };
   }
 
@@ -288,7 +286,7 @@ public class AAStructureBindingModelTest
     SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = testee.new SuperposeData(al.getWidth());
+      structs[i] = new SuperposeData(al.getWidth(), 0);
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for
@@ -335,7 +333,7 @@ public class AAStructureBindingModelTest
     SuperposeData[] structs = new SuperposeData[al.getHeight()];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = testee.new SuperposeData(al.getWidth());
+      structs[i] = new SuperposeData(al.getWidth(), 0);
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for
@@ -363,4 +361,4 @@ public class AAStructureBindingModelTest
     assertFalse(matched.get(4)); // superposable, but hidden, column
     assertTrue(matched.get(5));
   }
-}
+}
\ No newline at end of file