JAL-3551 working proof of concept of Jalview driving PyMOL
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 13 Mar 2020 21:48:21 +0000 (21:48 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 13 Mar 2020 21:48:21 +0000 (21:48 +0000)
33 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/pymol/PymolCommands.java [new file with mode: 0644]
src/jalview/ext/pymol/PymolManager.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/AppJmol.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/JalviewChimeraXBindingModel.java
src/jalview/gui/Preferences.java
src/jalview/gui/PymolBindingModel.java [new file with mode: 0644]
src/jalview/gui/PymolViewer.java [new file with mode: 0644]
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/jbgui/GPreferences.java
src/jalview/structure/AtomSpecModel.java
src/jalview/structure/StructureCommand.java [new file with mode: 0644]
src/jalview/structure/StructureCommandI.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsBase.java
src/jalview/structure/StructureCommandsFactory.java [deleted file]
src/jalview/structure/StructureCommandsI.java
src/jalview/structures/models/AAStructureBindingModel.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/pymol/PymolCommandsTest.java [new file with mode: 0644]
test/jalview/ext/pymol/PymolManagerTest.java [new file with mode: 0644]
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/structure/AtomSpecModelTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java

index 9a72b2e..dd6dea3 100644 (file)
@@ -174,11 +174,4 @@ class AppletJmolBinding extends JalviewJmolBinding
   {
     return null;
   }
-
-  @Override
-  protected void sendAsynchronousCommand(String command, String progressMsg)
-  {
-    // TODO Auto-generated method stub
-    
-  }
 }
index 5be53a3..5c20136 100644 (file)
@@ -179,12 +179,4 @@ public class ExtJmol extends JalviewJmolBinding
   {
     return null;
   }
-
-  @Override
-  protected void sendAsynchronousCommand(String command, String progressMsg)
-  {
-    // TODO Auto-generated method stub
-    
-  }
-
 }
index fe5c980..844fb1b 100644 (file)
@@ -21,8 +21,6 @@
 package jalview.ext.jmol;
 
 import jalview.api.FeatureRenderer;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.IProgressIndicator;
@@ -30,10 +28,10 @@ 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.StructureCommand;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.MessageManager;
 
 import java.awt.Container;
 import java.awt.event.ComponentEvent;
@@ -41,7 +39,6 @@ import java.awt.event.ComponentListener;
 import java.io.File;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.BitSet;
 import java.util.List;
 import java.util.Map;
 import java.util.StringTokenizer;
@@ -125,19 +122,21 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   }
 
   @Override
-  public List<String> executeCommand(String command, boolean getReply)
+  public List<String> executeCommand(StructureCommandI command,
+          boolean getReply)
   {
     if (command == null)
     {
       return null;
     }
+    String cmd = command.getCommand();
     jmolHistory(false);
-    if (lastCommand == null || !lastCommand.equals(command))
+    if (lastCommand == null || !lastCommand.equals(cmd))
     {
-      jmolViewer.evalStringQuiet(command + "\n");
+      jmolViewer.evalStringQuiet(cmd + "\n");
     }
     jmolHistory(true);
-    lastCommand = command;
+    lastCommand = cmd;
     return null;
   }
 
@@ -425,7 +424,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ")
               .append(toks.nextToken());
       sb.append("|").append(label).append("\"");
-      executeCommand(sb.toString(), false);
+      executeCommand(new StructureCommand(sb.toString()), false);
     }
   }
 
@@ -712,14 +711,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         }
         if (matches)
         {
-          // add an entry for every chain in the model
-          for (int i = 0; i < pdb.getChains().size(); i++)
-          {
-            String chid = pdb.getId() + ":"
-                    + pdb.getChains().elementAt(i).id;
-            addChainFile(chid, fileName);
-            getChainNames().add(chid);
-          }
+          stashFoundChains(pdb, fileName);
           notifyLoaded = true;
         }
       }
@@ -955,20 +947,20 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   }
 
   @Override
-  protected int getModelNoForFile(String pdbFile)
+  protected String getModelIdForFile(String pdbFile)
   {
     if (modelFileNames == null)
     {
-      return -1;
+      return "";
     }
     for (int i = 0; i < modelFileNames.length; i++)
     {
       if (modelFileNames[i].equalsIgnoreCase(pdbFile))
       {
-        return i;
+        return String.valueOf(i);
       }
     }
-    return -1;
+    return "";
   }
 
   @Override
@@ -976,4 +968,10 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   {
     return ViewerType.JMOL;
   }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum + 1);
+  }
 }
index 6f682be..bd44921 100644 (file)
@@ -29,6 +29,8 @@ import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -36,6 +38,7 @@ import jalview.util.Comparison;
 
 import java.awt.Color;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
@@ -47,10 +50,20 @@ import java.util.Map;
  */
 public class JmolCommands extends StructureCommandsBase
 {
-  private static final String CMD_COLOUR_BY_CHARGE = "select *;color white;select ASP,GLU;color red;"
-          + "select LYS,ARG;color blue;select CYS;color yellow";
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "select *; cartoons off; backbone");
 
-  private static final String CMD_COLOUR_BY_CHAIN = "select *;color chain";
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand("zoom 0");
+
+  private static final StructureCommand COLOUR_ALL_WHITE = new StructureCommand(
+          "select *;color white;");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "select *;color white;select ASP,GLU;color red;"
+                  + "select LYS,ARG;color blue;select CYS;color yellow");
+
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "select *;color chain");
 
   private static final String PIPE = "|";
 
@@ -240,41 +253,39 @@ public class JmolCommands extends StructureCommandsBase
   }
 
   @Override
-  public String colourByChain()
+  public StructureCommandI colourByChain()
   {
-    return CMD_COLOUR_BY_CHAIN;
+    return COLOUR_BY_CHAIN;
   }
 
   @Override
-  public String colourByCharge()
+  public List<StructureCommandI> colourByCharge()
   {
-    return CMD_COLOUR_BY_CHARGE;
+    return Arrays.asList(COLOUR_BY_CHARGE);
   }
 
   @Override
-  public String colourByResidues(Map<String, Color> colours)
+  public List<StructureCommandI> colourByResidues(Map<String, Color> colours)
   {
-    StringBuilder cmd = new StringBuilder(128);
-    cmd.append("select *;color white;");
-    cmd.append(super.colourByResidues(colours));
-
-    return cmd.toString();
+    List<StructureCommandI> cmds = super.colourByResidues(colours);
+    cmds.add(0, COLOUR_ALL_WHITE);
+    return cmds;
   }
 
   @Override
-  public String setBackgroundColour(Color col)
+  public StructureCommandI setBackgroundColour(Color col)
   {
-    return "background " + getColourString(col);
+    return new StructureCommand("background " + getColourString(col));
   }
 
   @Override
-  public String focusView()
+  public StructureCommandI focusView()
   {
-    return "zoom 0";
+    return FOCUS_VIEW;
   }
 
   @Override
-  public String showChains(List<String> toShow)
+  public List<StructureCommandI> showChains(List<String> toShow)
   {
     StringBuilder atomSpec = new StringBuilder(128);
     boolean first = true;
@@ -295,7 +306,7 @@ public class JmolCommands extends StructureCommandsBase
     String spec = atomSpec.toString();
     String command = "select *;restrict " + spec + ";cartoon;center "
             + spec;
-    return command;
+    return Arrays.asList(new StructureCommand(command));
   }
 
   /**
@@ -319,13 +330,13 @@ public class JmolCommands extends StructureCommandsBase
    * @see https://chemapps.stolaf.edu/jmol/docs/#compare
    */
   @Override
-  public String superposeStructures(AtomSpecModel refAtoms,
+  public List<StructureCommandI> 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));
+    String refModel = refAtoms.getModels().iterator().next();
+    String model2 = atomSpec.getModels().iterator().next();
+    sb.append(String.format("compare {%s.1} {%s.1}", model2, refModel));
     sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {");
 
     /*
@@ -344,36 +355,36 @@ public class JmolCommands extends StructureCommandsBase
     sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator())
             .append("cartoons");
 
-    return sb.toString();
+    return Arrays.asList(new StructureCommand(sb.toString()));
   }
 
   @Override
-  public String openCommandFile(String path)
+  public StructureCommandI openCommandFile(String path)
   {
     /*
      * https://chemapps.stolaf.edu/jmol/docs/#script
      * not currently used in Jalview
      */
-    return "script " + path;
+    return new StructureCommand("script " + path);
   }
 
   @Override
-  public String saveSession(String filepath)
+  public StructureCommandI saveSession(String filepath)
   {
     /*
      * https://chemapps.stolaf.edu/jmol/docs/#write
      * not currently used in Jalview
      */
-    return "write \"" + filepath + "\"";
+    return new StructureCommand("write \"" + filepath + "\"");
   }
 
   @Override
-  protected String getColourCommand(String atomSpec, Color colour)
+  protected StructureCommandI 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();
+    return new StructureCommand(sb.toString());
   }
 
   @Override
@@ -398,7 +409,7 @@ public class JmolCommands extends StructureCommandsBase
     StringBuilder sb = new StringBuilder(128);
 
     boolean first = true;
-    for (int modelNo : model.getModels())
+    for (String modelNo : model.getModels())
     {
       for (String chain : model.getChains(modelNo))
       {
@@ -427,8 +438,14 @@ public class JmolCommands extends StructureCommandsBase
   }
 
   @Override
-  public String showBackbone()
+  public List<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
   {
-    return "select *; cartoons off; backbone";
+    return null;
   }
 }
diff --git a/src/jalview/ext/pymol/PymolCommands.java b/src/jalview/ext/pymol/PymolCommands.java
new file mode 100644 (file)
index 0000000..1638644
--- /dev/null
@@ -0,0 +1,214 @@
+package jalview.ext.pymol;
+
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsBase;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that generates commands to send to PyMol
+ * 
+ * @see https://pymolwiki.org/index.php/Category:Commands
+ */
+public class PymolCommands extends StructureCommandsBase
+{
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "util.cbc");
+
+  /*
+   * because the xml-rpc interface can only accept one command at a time, we can't
+   * concatenate commands and must instead form and send them individually
+   */
+  private static final List<StructureCommandI> COLOR_BY_CHARGE = new ArrayList<>();
+
+  private static final List<StructureCommandI> SHOW_BACKBONE = new ArrayList<>();
+
+  static {
+    COLOR_BY_CHARGE.add(new StructureCommand("color", "white", "*"));
+    COLOR_BY_CHARGE
+            .add(new StructureCommand("color", "red", "resn ASP resn GLU"));
+    COLOR_BY_CHARGE.add(
+            new StructureCommand("color", "blue", "resn LYS resn ARG"));
+    COLOR_BY_CHARGE.add(new StructureCommand("color"));
+    SHOW_BACKBONE.add(new StructureCommand("hide", "everything"));
+    SHOW_BACKBONE.add(new StructureCommand("show", "ribbon"));
+  }
+
+  @Override
+  public StructureCommandI colourByChain()
+  {
+    // https://pymolwiki.org/index.php/CBC
+    // TODO this doesn't execute as an xml-rpc command
+    return COLOUR_BY_CHAIN;
+  }
+
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return COLOR_BY_CHARGE;
+  }
+
+  @Override
+  public StructureCommandI setBackgroundColour(Color col)
+  {
+    // https://pymolwiki.org/index.php/Bg_Color
+    return new StructureCommand("bg_color", getColourString(col));
+  }
+
+  /**
+   * Returns a colour formatted suitable for use in viewer command syntax. For
+   * example, red is {@code "0xff0000"}.
+   * 
+   * @param c
+   * @return
+   */
+  protected String getColourString(Color c)
+  {
+    return String.format("0x%02x%02x%02x", c.getRed(), c.getGreen(),
+            c.getBlue());
+  }
+
+  @Override
+  public StructureCommandI focusView()
+  {
+    // TODO what?
+    return null;
+  }
+
+  @Override
+  public List<StructureCommandI> showChains(List<String> toShow)
+  {
+    // https://pymolwiki.org/index.php/Show
+    List<StructureCommandI> commands = new ArrayList<>();
+    commands.add(new StructureCommand("hide", "everything"));
+    commands.add(new StructureCommand("show", "lines"));
+    StringBuilder chains = new StringBuilder();
+    for (String chain : toShow)
+    {
+      chains.append(" chain ").append(chain);
+    }
+    commands.add(new StructureCommand("show", "cartoon", chains.toString()));
+    return commands;
+  }
+
+  @Override
+  public List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec)
+  {
+    // https://pymolwiki.org/index.php/Super
+    List<StructureCommandI> commands = new ArrayList<>();
+    String refAtomsAlphaOnly = getAtomSpec(refAtoms, true);
+    String atomSpec2AlphaOnly = getAtomSpec(atomSpec, true);
+    commands.add(new StructureCommand("super", refAtomsAlphaOnly,
+            atomSpec2AlphaOnly));
+
+    /*
+     * and show superposed residues as cartoon
+     */
+    String refAtomsAll = getAtomSpec(refAtoms, false);
+    String atomSpec2All = getAtomSpec(atomSpec, false);
+    commands.add(new StructureCommand("show", "cartoon",
+            refAtomsAll + " " + atomSpec2All));
+
+    return commands;
+  }
+
+  @Override
+  public StructureCommandI openCommandFile(String path)
+  {
+    // where is this documented by PyMol?
+    // todo : xml-rpc answers 'method "@" is not supported'
+    return new StructureCommand("@" + path); // should be .pml
+  }
+
+  @Override
+  public StructureCommandI saveSession(String filepath)
+  {
+    // https://pymolwiki.org/index.php/Save#EXAMPLES
+    return new StructureCommand("save", filepath); // should be .pse
+  }
+
+  /**
+   * Returns a selection string in PyMOL 'selection macro' format:
+   * 
+   * <pre>
+   * modelId// chain/residues/
+   * </pre>
+   * 
+   * If more than one chain, makes a selection expression for each, and they are
+   * separated by spaces.
+   * 
+   * @see https://pymolwiki.org/index.php/Selection_Macros
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel model, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    boolean first = true;
+    for (String modelId : model.getModels())
+    {
+      for (String chain : model.getChains(modelId))
+      {
+        if (!first)
+        {
+          sb.append(" ");
+        }
+        first = false;
+        List<int[]> rangeList = model.getRanges(modelId, chain);
+        chain = chain.trim();
+        sb.append(modelId).append("//").append(chain).append("/");
+        boolean firstRange = true;
+        for (int[] range : rangeList)
+        {
+          if (!firstRange)
+          {
+            sb.append("+");
+          }
+          firstRange = false;
+          sb.append(String.valueOf(range[0]));
+          if (range[0] != range[1])
+          {
+            sb.append("-").append(String.valueOf(range[1]));
+          }
+        }
+        sb.append("/");
+        if (alphaOnly)
+        {
+          sb.append("CA");
+        }
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public List<StructureCommandI> showBackbone()
+  {
+    return SHOW_BACKBONE;
+  }
+
+  @Override
+  protected StructureCommandI getColourCommand(String atomSpec, Color colour)
+  {
+    // https://pymolwiki.org/index.php/Color
+    return new StructureCommand("color", getColourString(colour), atomSpec);
+  }
+
+  @Override
+  protected String getResidueSpec(String residue)
+  {
+    // https://pymolwiki.org/index.php/Selection_Algebra
+    return "resn " + residue;
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
+  {
+    return new StructureCommand("load", file);
+  }
+
+}
diff --git a/src/jalview/ext/pymol/PymolManager.java b/src/jalview/ext/pymol/PymolManager.java
new file mode 100644 (file)
index 0000000..ad44677
--- /dev/null
@@ -0,0 +1,298 @@
+package jalview.ext.pymol;
+
+import jalview.bin.Cache;
+import jalview.gui.Preferences;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PymolManager
+{
+  private static final int RPC_REPLY_TIMEOUT_MS = 15000;
+
+  private static final int CONNECTION_TIMEOUT_MS = 100;
+
+  private static final String POST1 = "<methodCall><methodName>";
+
+  private static final String POST2 = "</methodName><params>";
+
+  private static final String POST3 = "</params></methodCall>";
+
+  private Process pymolProcess;
+
+  private int pymolXmlRpcPort;
+
+  /**
+   * Returns a list of paths to try for the PyMOL executable. Any user
+   * preference is placed first, otherwise 'standard' paths depending on the
+   * operating system.
+   * 
+   * @return
+   */
+  public static List<String> getPymolPaths()
+  {
+    List<String> pathList = new ArrayList<>();
+  
+    String userPath = Cache
+            .getDefault(Preferences.PYMOL_PATH, null);
+    if (userPath != null)
+    {
+      pathList.add(0, userPath);
+    }
+  
+    /*
+     * add default installation paths
+     */
+    String pymol = "PyMOL";
+    String os = System.getProperty("os.name");
+    if (os.startsWith("Linux"))
+    {
+      pathList.add("/usr/local/pymol/bin/" + pymol);
+      pathList.add("/usr/local/bin/" + pymol);
+      pathList.add("/usr/bin/" + pymol);
+      pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol);
+    }
+    else if (os.startsWith("Windows"))
+    {
+      // todo Windows installation path(s)
+    }
+    else if (os.startsWith("Mac"))
+    {
+      pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
+    }
+    return pathList;
+  }
+
+  public boolean isPymolLaunched()
+  {
+    // TODO pull up generic methods for external viewer processes
+    boolean launched = false;
+    if (pymolProcess != null)
+    {
+      try
+      {
+        pymolProcess.exitValue();
+        // if we get here, process has ended
+      } catch (IllegalThreadStateException e)
+      {
+        // ok - not yet terminated
+        launched = true;
+      }
+    }
+    return launched;
+  }
+
+  public void exitPymol()
+  {
+    if (isPymolLaunched() && pymolProcess != null)
+    {
+      sendCommand(new StructureCommand("quit"), false);
+    }
+    pymolProcess = null;
+    // currentModelsMap.clear();
+    this.pymolXmlRpcPort = 0;
+  }
+
+  /**
+   * Sends the command to Pymol; if requested, tries to get and return any
+   * replies, else returns null
+   * 
+   * @param command
+   * @param getReply
+   * @return
+   */
+  public List<String> sendCommand(StructureCommandI command,
+          boolean getReply)
+  {
+    String postBody = getPostRequest(command);
+    // System.out.println(postBody);// debug
+    String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort;
+    PrintWriter out = null;
+    BufferedReader in = null;
+    List<String> result = new ArrayList<>();
+    try
+    {
+      URL realUrl = new URL(rpcUrl);
+      URLConnection conn = realUrl.openConnection();
+      conn.setRequestProperty("accept", "*/*");
+      conn.setRequestProperty("content-type", "text/xml");
+      conn.setDoOutput(true);
+      conn.setDoInput(true);
+      out = new PrintWriter(conn.getOutputStream());
+      out.print(postBody);
+      out.flush();
+      in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+      String line;
+      while ((line = in.readLine()) != null)
+      {
+        result.add(line);
+      }
+    } catch (Exception e)
+    {
+      e.printStackTrace();
+    } finally
+    {
+      try
+      {
+        if (out != null)
+        {
+          out.close();
+        }
+        if (in != null)
+        {
+          in.close();
+        }
+      } catch (IOException ex)
+      {
+        ex.printStackTrace();
+        }
+    }
+    return result;
+  }
+
+  /**
+   * Builds the body of the XML-RPC format POST request to execute the command
+   * 
+   * @param command
+   * @return
+   */
+  static String getPostRequest(StructureCommandI command)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(POST1).append(command.getCommand()).append(POST2);
+    if (command.hasParameters())
+    {
+      for (String p : command.getParameters())
+      {
+        /*
+         * for now assuming all are string - <string> element is optional
+         * refactor in future if other data types needed
+         * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm
+         */
+        sb.append("<parameter><value>").append(p)
+                .append("</value></parameter>");
+      }
+    }
+    sb.append(POST3);
+    return sb.toString();
+  }
+
+  public boolean launchPymol()
+  {
+    // todo pull up much of this
+    // Do nothing if already launched
+    if (isPymolLaunched())
+    {
+      return true;
+    }
+
+    String error = "Error message: ";
+    for (String pymolPath : getPymolPaths())
+    {
+      try
+      {
+        // ensure symbolic links are resolved
+        pymolPath = Paths.get(pymolPath).toRealPath().toString();
+        File path = new File(pymolPath);
+        // uncomment the next line to simulate Pymol not installed
+        // path = new File(pymolPath + "x");
+        if (!path.canExecute())
+        {
+          error += "File '" + path + "' does not exist.\n";
+          continue;
+        }
+        List<String> args = new ArrayList<>();
+        args.add(pymolPath);
+        args.add("-R"); // https://pymolwiki.org/index.php/RPC
+        ProcessBuilder pb = new ProcessBuilder(args);
+        pymolProcess = pb.start();
+        error = "";
+        break;
+      } catch (Exception e)
+      {
+        // pPymol could not be started using this path
+        error += e.getMessage();
+      }
+    }
+    if (error.length() == 0)
+    {
+      this.pymolXmlRpcPort = getPortNumber();
+      System.out.println(
+              "PyMOL XMLRPC started on port " + pymolXmlRpcPort);
+      return (pymolXmlRpcPort > 0);
+    }
+
+    // logger.warn(error);
+    return false;
+  }
+
+  private int getPortNumber()
+  {
+    // TODO pull up most of this!
+    int port = 0;
+    InputStream readChan = pymolProcess.getInputStream();
+    BufferedReader lineReader = new BufferedReader(
+            new InputStreamReader(readChan));
+    StringBuilder responses = new StringBuilder();
+    try
+    {
+      String response = lineReader.readLine();
+      while (response != null)
+      {
+        responses.append("\n" + response);
+        // expect: xml-rpc server running on host localhost, port 9123
+        if (response.contains("xml-rpc"))
+        {
+          String[] tokens = response.split(" ");
+          for (int i = 0; i < tokens.length - 1; i++)
+          {
+            if ("port".equals(tokens[i]))
+            {
+              port = Integer.parseInt(tokens[i + 1]);
+              break;
+            }
+          }
+        }
+        if (port > 0)
+        {
+          break; // hack for hanging readLine()
+        }
+        response = lineReader.readLine();
+      }
+    } catch (Exception e)
+    {
+      System.err.println(
+              "Failed to get REST port number from " + responses + ": "
+              + e.getMessage());
+      // logger.error("Failed to get REST port number from " + responses + ": "
+      // + e.getMessage());
+    } finally
+    {
+      try
+      {
+        lineReader.close();
+      } catch (IOException e2)
+      {
+      }
+    }
+    if (port == 0)
+    {
+      System.err.println("Failed to start PyMOL with XMLRPC, response was: "
+              + responses);
+    }
+    System.err.println("PyMOL started with XMLRPC on port " + port);
+    return port;
+  }
+
+}
index 1b1dd35..7e04f39 100644 (file)
@@ -29,17 +29,17 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.Desktop;
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.ColorUtils;
-import jalview.util.IntRangeComparator;
 
 import java.awt.Color;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -52,21 +52,25 @@ import java.util.Map;
  */
 public class ChimeraCommands extends StructureCommandsBase
 {
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand("~display all;chain @CA|P");
+
   public static final String NAMESPACE_PREFIX = "jv_";
 
-  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 StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "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";
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "rainbow chain");
 
   // Chimera clause to exclude alternate locations in atom selection
   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
 
   @Override
-  public String getColourCommand(String atomSpec, Color colour)
+  public StructureCommandI getColourCommand(String atomSpec, Color colour)
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html
     String colourCode = getColourString(colour);
-    return "color " + colourCode + " " + atomSpec;
+    return new StructureCommand("color " + colourCode + " " + atomSpec);
   }
 
   /**
@@ -92,7 +96,7 @@ public class ChimeraCommands extends StructureCommandsBase
    * @return
    */
   @Override
-  public String[] setAttributesForFeatures(
+  public List<StructureCommandI> setAttributesForFeatures(
           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
           AlignmentViewPanel viewPanel)
   {
@@ -155,6 +159,7 @@ public class ChimeraCommands extends StructureCommandsBase
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
       final int modelNumber = pdbfnum + getModelStartNo();
+      String modelId = String.valueOf(modelNumber);
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
 
       if (mapping == null || mapping.length < 1)
@@ -178,12 +183,12 @@ public class ChimeraCommands extends StructureCommandsBase
             if (!visibleFeatures.isEmpty())
             {
               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
-                      theMap, modelNumber);
+                      theMap, modelId);
             }
             if (showLinkedFeatures)
             {
               scanComplementFeatures(complementRenderer, structureMapping,
-                      seq, theMap, modelNumber);
+                      seq, theMap, modelId);
             }
           }
         }
@@ -205,7 +210,8 @@ public class ChimeraCommands extends StructureCommandsBase
   protected static void scanComplementFeatures(
           FeatureRenderer complementRenderer,
           StructureMapping structureMapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+          Map<String, Map<Object, AtomSpecModel>> theMap,
+          String modelNumber)
   {
     /*
      * for each sequence residue mapped to a structure position...
@@ -269,19 +275,19 @@ public class ChimeraCommands extends StructureCommandsBase
   }
 
   /**
-   * 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.
+   * Inspect features on the sequence; for each feature that is visible,
+   * determine its mapped ranges in the structure (if any) according to the
+   * given mapping, and add them to the map.
    * 
    * @param visibleFeatures
    * @param mapping
    * @param seq
    * @param theMap
-   * @param modelNumber
+   * @param modelId
    */
   protected static void scanSequenceFeatures(List<String> visibleFeatures,
           StructureMapping mapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+          Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
   {
     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
             visibleFeatures.toArray(new String[visibleFeatures.size()]));
@@ -321,7 +327,7 @@ public class ChimeraCommands extends StructureCommandsBase
         }
         for (int[] range : mappedRanges)
         {
-          addAtomSpecRange(featureValues, value, modelNumber, range[0],
+          addAtomSpecRange(featureValues, value, modelId, range[0],
                   range[1], mapping.getChain());
         }
       }
@@ -343,10 +349,10 @@ public class ChimeraCommands extends StructureCommandsBase
    * @param featureMap
    * @return
    */
-  protected String[] setAttributes(
+  protected List<StructureCommandI> setAttributes(
           Map<String, Map<Object, AtomSpecModel>> featureMap)
   {
-    List<String> commands = new ArrayList<>();
+    List<StructureCommandI> commands = new ArrayList<>();
     for (String featureType : featureMap.keySet())
     {
       String attributeName = makeAttributeName(featureType);
@@ -368,13 +374,13 @@ public class ChimeraCommands extends StructureCommandsBase
         AtomSpecModel atomSpecModel = values.get(value);
         String featureValue = value.toString();
         featureValue = featureValue.replaceAll("\\'", "&#39;");
-        String cmd = setAttribute(attributeName, featureValue,
+        StructureCommandI cmd = setAttribute(attributeName, featureValue,
                 atomSpecModel);
         commands.add(cmd);
       }
     }
 
-    return commands.toArray(new String[commands.size()]);
+    return commands;
   }
 
   /**
@@ -390,7 +396,7 @@ public class ChimeraCommands extends StructureCommandsBase
    * @param atomSpecModel
    * @return
    */
-  protected String setAttribute(String attributeName,
+  protected StructureCommandI setAttribute(String attributeName,
           String attributeValue,
           AtomSpecModel atomSpecModel)
   {
@@ -398,7 +404,7 @@ public class ChimeraCommands extends StructureCommandsBase
     sb.append("setattr res ").append(attributeName).append(" '")
             .append(attributeValue).append("' ");
     sb.append(getAtomSpec(atomSpecModel, false));
-    return sb.toString();
+    return new StructureCommand(sb.toString());
   }
 
   /**
@@ -435,15 +441,15 @@ public class ChimeraCommands extends StructureCommandsBase
   }
 
   @Override
-  public String colourByChain()
+  public StructureCommandI colourByChain()
   {
-    return CMD_COLOUR_BY_CHAIN;
+    return COLOUR_BY_CHAIN;
   }
 
   @Override
-  public String colourByCharge()
+  public List<StructureCommandI> colourByCharge()
   {
-    return CMD_COLOUR_BY_CHARGE;
+    return Arrays.asList(COLOUR_BY_CHARGE);
   }
 
   @Override
@@ -453,21 +459,21 @@ public class ChimeraCommands extends StructureCommandsBase
   }
 
   @Override
-  public String setBackgroundColour(Color col)
+  public StructureCommandI setBackgroundColour(Color col)
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor
-    return "set bgColor " + ColorUtils.toTkCode(col);
+    return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col));
   }
 
   @Override
-  public String focusView()
+  public StructureCommandI focusView()
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html
-    return "focus";
+    return new StructureCommand("focus");
   }
 
   @Override
-  public String showChains(List<String> toShow)
+  public List<StructureCommandI> showChains(List<String> toShow)
   {
     /*
      * Construct a chimera command like
@@ -498,11 +504,12 @@ public class ChimeraCommands extends StructureCommandsBase
      */
     final String command = "~display #*; ~ribbon #*; ribbon :"
             + cmd.toString();
-    return command;
+    return Arrays.asList(new StructureCommand(command));
   }
 
   @Override
-  public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref)
+  public List<StructureCommandI> superposeStructures(AtomSpecModel spec,
+          AtomSpecModel ref)
   {
     /*
      * Form Chimera match command to match spec to ref
@@ -518,28 +525,26 @@ public class ChimeraCommands extends StructureCommandsBase
     cmd.append("match ").append(atomSpec).append(" ").append(refSpec);
 
     /*
-     * show superposed residues as ribbon, others as chain
+     * show superposed residues as ribbon
      */
-    // 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();
+    return Arrays.asList(new StructureCommand(cmd.toString()));
   }
 
   @Override
-  public String openCommandFile(String path)
+  public StructureCommandI openCommandFile(String path)
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
-    return "open cmd:" + path;
+    return new StructureCommand("open cmd:" + path);
   }
 
   @Override
-  public String saveSession(String filepath)
+  public StructureCommandI saveSession(String filepath)
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
-    return "save " + filepath;
+    return new StructureCommand("save " + filepath);
   }
 
   /**
@@ -571,7 +576,7 @@ public class ChimeraCommands extends StructureCommandsBase
   {
     StringBuilder sb = new StringBuilder(128);
     boolean firstModel = true;
-    for (Integer model : atomSpec.getModels())
+    for (String model : atomSpec.getModels())
     {
       if (!firstModel)
       {
@@ -591,7 +596,7 @@ public class ChimeraCommands extends StructureCommandsBase
    * @param atomSpec
    * @param alphaOnly
    */
-  protected void appendModel(StringBuilder sb, Integer model,
+  protected void appendModel(StringBuilder sb, String model,
           AtomSpecModel atomSpec, boolean alphaOnly)
   {
     sb.append("#").append(model).append(":");
@@ -603,46 +608,10 @@ public class ChimeraCommands extends StructureCommandsBase
       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())
+      for (int[] range : rangeList)
       {
-        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);
+        appendRange(sb, range[0], range[1], chain, firstPositionForModel,
+                false);
         firstPositionForModel = false;
       }
     }
@@ -657,9 +626,15 @@ public class ChimeraCommands extends StructureCommandsBase
   }
 
   @Override
-  public String showBackbone()
+  public List<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
   {
-    return "~display all;chain @CA|P";
+    return new StructureCommand("open " + file);
   }
 
 }
index 5eba203..9636a6a 100644 (file)
 package jalview.ext.rbvi.chimera;
 
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
 import jalview.util.ColorUtils;
-import jalview.util.IntRangeComparator;
 
 import java.awt.Color;
-import java.util.Collections;
-import java.util.Iterator;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -34,12 +34,19 @@ import java.util.List;
  */
 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";
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "~display all;show @CA|P pbonds");
+
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand(
+          "view");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow");
 
   @Override
-  public String colourByCharge()
+  public List<StructureCommandI> colourByCharge()
   {
-    return CMD_COLOUR_BY_CHARGE;
+    return Arrays.asList(COLOUR_BY_CHARGE);
   }
 
   @Override
@@ -49,26 +56,26 @@ public class ChimeraXCommands extends ChimeraCommands
   }
 
   @Override
-  public String setBackgroundColour(Color col)
+  public StructureCommandI setBackgroundColour(Color col)
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/set.html
-    return "set bgColor " + ColorUtils.toTkCode(col);
+    return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col));
   }
 
   @Override
-  public String getColourCommand(String atomSpec, Color colour)
+  public StructureCommandI getColourCommand(String atomSpec, Color colour)
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html
     String colourCode = getColourString(colour);
 
-    return "color " + atomSpec + " " + colourCode;
+    return new StructureCommand("color " + atomSpec + " " + colourCode);
   }
 
   @Override
-  public String focusView()
+  public StructureCommandI focusView()
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html
-    return "view";
+    return FOCUS_VIEW;
   }
 
   /**
@@ -96,7 +103,7 @@ public class ChimeraXCommands extends ChimeraCommands
    * @return
    */
   @Override
-  protected String setAttribute(String attributeName,
+  protected StructureCommandI setAttribute(String attributeName,
           String attributeValue, AtomSpecModel atomSpecModel)
   {
     StringBuilder sb = new StringBuilder(128);
@@ -104,21 +111,21 @@ public class ChimeraXCommands extends ChimeraCommands
     sb.append(" res ").append(attributeName).append(" '")
             .append(attributeValue).append("'");
     sb.append(" create true");
-    return sb.toString();
+    return new StructureCommand(sb.toString());
   }
 
   @Override
-  public String openCommandFile(String path)
+  public StructureCommandI openCommandFile(String path)
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
-    return "open " + path;
+    return new StructureCommand("open " + path);
   }
 
   @Override
-  public String saveSession(String filepath)
+  public StructureCommandI saveSession(String filepath)
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
-    return "save session " + filepath;
+    return new StructureCommand("save session " + filepath);
   }
 
   /**
@@ -133,7 +140,7 @@ public class ChimeraXCommands extends ChimeraCommands
   {
     StringBuilder sb = new StringBuilder(128);
     boolean firstModel = true;
-    for (Integer model : atomSpec.getModels())
+    for (String model : atomSpec.getModels())
     {
       if (!firstModel)
       {
@@ -157,7 +164,7 @@ public class ChimeraXCommands extends ChimeraCommands
    * @param model
    * @param atomSpec
    */
-  protected void appendModel(StringBuilder sb, Integer model,
+  protected void appendModel(StringBuilder sb, String model,
           AtomSpecModel atomSpec)
   {
     sb.append("#").append(model);
@@ -167,59 +174,29 @@ public class ChimeraXCommands extends ChimeraCommands
       boolean firstPositionForChain = true;
       sb.append("/").append(chain.trim()).append(":");
       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())
+      boolean first = true;
+      for (int[] range : rangeList)
       {
-        int[] range = iterator.next();
-        if (range[0] <= end + 1)
+        if (!first)
         {
-          /*
-           * 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]);
+          sb.append(",");
         }
-        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);
+        first = false;
+        appendRange(sb, range[0], range[1], chain, firstPositionForChain,
+                true);
       }
-      firstPositionForChain = false;
     }
   }
 
   @Override
-  public String showBackbone()
+  public List<StructureCommandI> showBackbone()
   {
-    return "~display all;show @CA|P pbonds";
+    return Arrays.asList(SHOW_BACKBONE);
   }
 
   @Override
-  public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref)
+  public List<StructureCommandI> superposeStructures(AtomSpecModel spec,
+          AtomSpecModel ref)
   {
     /*
      * Form ChimeraX match command to match spec to ref
@@ -242,7 +219,7 @@ public class ChimeraXCommands extends ChimeraCommands
     cmd.append(getAtomSpec(spec, false)).append("|");
     cmd.append(getAtomSpec(ref, false)).append("; view");
 
-    return cmd.toString();
+    return Arrays.asList(new StructureCommand(cmd.toString()));
   }
 
 }
index ae34bd0..3553d68 100644 (file)
@@ -24,7 +24,6 @@ import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
@@ -34,10 +33,10 @@ 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.StructureCommand;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.MessageManager;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -45,7 +44,6 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.BindException;
 import java.util.ArrayList;
-import java.util.BitSet;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -352,7 +350,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * @param getResponse
    */
   @Override
-  public List<String> executeCommand(final String command,
+  public List<String> executeCommand(final StructureCommandI command,
           boolean getResponse)
   {
     if (chimeraManager == null || command == null)
@@ -362,43 +360,21 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     }
     List<String> reply = null;
     // trim command or it may never find a match in the replyLog!!
+    String cmd = command.getCommand().trim();
     List<String> lastReply = chimeraManager
-            .sendChimeraCommand(command.trim(), getResponse);
+            .sendChimeraCommand(cmd, getResponse);
     if (getResponse)
     {
       reply = lastReply;
       if (debug)
       {
-        log("Response from command ('" + command + "') was:\n" + lastReply);
+        log("Response from command ('" + cmd + "') was:\n" + lastReply);
       }
     }
 
     return reply;
   }
 
-  /**
-   * @param command
-   */
-  protected void executeWhenReady(String command)
-  {
-    waitForChimera();
-    executeCommand(command, false);
-    waitForChimera();
-  }
-
-  private void waitForChimera()
-  {
-    while (chimeraManager != null && chimeraManager.isBusy())
-    {
-      try
-      {
-        Thread.sleep(15);
-      } catch (InterruptedException q)
-      {
-      }
-    }
-  }
-
   @Override
   public synchronized String[] getStructureFiles()
   {
@@ -582,7 +558,8 @@ 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 = getCommandGenerator().saveSession(filepath);
+      String command = getCommandGenerator().saveSession(filepath)
+              .getCommand();
       List<String> reply = chimeraManager.sendChimeraCommand(command, true);
       if (reply.contains("Session written"))
       {
@@ -611,7 +588,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
      * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
      */
-    executeCommand("open " + filepath, true);
+    executeCommand(getCommandGenerator().loadFile(filepath), true);
     // todo: test for failure - how?
     return true;
   }
@@ -660,20 +637,20 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       return 0;
     }
 
-    String[] commands = getCommandGenerator()
+    List<StructureCommandI> commands = getCommandGenerator()
             .setAttributesForFeatures(getSsm(), files, getSequence(), avp);
-    if (commands.length > 10)
+    if (commands.size() > 10)
     {
       sendCommandsByFile(commands);
     }
     else
     {
-      for (String command : commands)
+      for (StructureCommandI command : commands)
       {
         sendAsynchronousCommand(command, null);
       }
     }
-    return commands.length;
+    return commands.size();
   }
 
   /**
@@ -683,21 +660,22 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * 
    * @param commands
    */
-  protected void sendCommandsByFile(String[] commands)
+  protected void sendCommandsByFile(List<StructureCommandI> commands)
   {
     try
     {
       File tmp = File.createTempFile("chim", getCommandFileExtension());
       tmp.deleteOnExit();
       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
-      for (String command : commands)
+      for (StructureCommandI command : commands)
       {
-        out.println(command);
+        out.println(command.getCommand());
       }
       out.flush();
       out.close();
       String path = tmp.getAbsolutePath();
-      String command = getCommandGenerator().openCommandFile(path);
+      StructureCommandI command = getCommandGenerator()
+              .openCommandFile(path);
       sendAsynchronousCommand(command, null);
     } catch (IOException e)
     {
@@ -737,7 +715,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     // fails for 'average.bfactor' (which is bad):
 
     String cmd = "list residues attr '" + attName + "'";
-    List<String> residues = executeCommand(cmd, true);
+    List<String> residues = executeCommand(new StructureCommand(cmd), true);
 
     boolean featureAdded = createFeaturesForAttributes(attName, residues);
     if (featureAdded)
@@ -852,14 +830,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   @Override
-  public int getModelNoForFile(String pdbFile)
+  public String getModelIdForFile(String pdbFile)
   {
     List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
     if (foundModels != null && !foundModels.isEmpty())
     {
-      return foundModels.get(0).getModelNumber();
+      return String.valueOf(foundModels.get(0).getModelNumber());
     }
-    return -1;
+    return "";
   }
 
   /**
index 0768c00..4087e63 100644 (file)
@@ -26,6 +26,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
+import jalview.structure.StructureCommand;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.BrowserLauncher;
 import jalview.util.ImageMaker;
@@ -245,8 +246,8 @@ public class AppJmol extends StructureViewerBase
     {
       command = "";
     }
-    jmb.executeCommand(command, false);
-    jmb.executeCommand("set hoverDelay=0.1", false);
+    jmb.executeCommand(new StructureCommand(command), false);
+    jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
     jmb.setFinishedInit(true);
   }
 
@@ -323,7 +324,7 @@ public class AppJmol extends StructureViewerBase
       cmd.append("loadingJalviewdata=true\nload APPEND ");
       cmd.append(filesString);
       cmd.append("\nloadingJalviewdata=null");
-      final String command = cmd.toString();
+      final StructureCommand command = new StructureCommand(cmd.toString());
       lastnotify = jmb.getLoadNotifiesHandled();
 
       try
index 1a5e901..a6e479d 100644 (file)
@@ -23,7 +23,6 @@ package jalview.gui;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
@@ -34,7 +33,6 @@ import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.BrowserLauncher;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.ws.dbsources.Pdb;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -516,7 +514,7 @@ public class ChimeraViewFrame extends StructureViewerBase
             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
                     jmb.getChains()[pos], pe.getFile(), protocol,
                     getProgressIndicator());
-            stashFoundChains(pdb, pe.getFile());
+            jmb.stashFoundChains(pdb, pe.getFile());
 
           } catch (OutOfMemoryError oomerror)
           {
@@ -572,71 +570,6 @@ public class ChimeraViewFrame extends StructureViewerBase
     worker = null;
   }
 
-  /**
-   * Fetch PDB data and save to a local file. Returns the full path to the file,
-   * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
-   * 
-   * @param processingEntry
-   * @return
-   * @throws Exception
-   */
-
-  private void stashFoundChains(StructureFile pdb, String file)
-  {
-    for (int i = 0; i < pdb.getChains().size(); i++)
-    {
-      String chid = new String(
-              pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
-      jmb.getChainNames().add(chid);
-      jmb.addChainFile(chid, file);
-    }
-  }
-
-  private String fetchPdbFile(PDBEntry processingEntry) throws Exception
-  {
-    String filePath = null;
-    Pdb pdbclient = new Pdb();
-    AlignmentI pdbseq = null;
-    String pdbid = processingEntry.getId();
-    long handle = System.currentTimeMillis()
-            + Thread.currentThread().hashCode();
-
-    /*
-     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
-     */
-    String msg = MessageManager.formatMessage("status.fetching_pdb",
-            new Object[]
-            { pdbid });
-    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-    // long hdl = startProgressBar(MessageManager.formatMessage(
-    // "status.fetching_pdb", new Object[]
-    // { pdbid }));
-    try
-    {
-      pdbseq = pdbclient.getSequenceRecords(pdbid);
-    } catch (OutOfMemoryError oomerror)
-    {
-      new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
-    } finally
-    {
-      msg = pdbid + " " + MessageManager.getString("label.state_completed");
-      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-      // stopProgressBar(msg, hdl);
-    }
-    /*
-     * If PDB data were saved and are not invalid (empty alignment), return the
-     * file path.
-     */
-    if (pdbseq != null && pdbseq.getHeight() > 0)
-    {
-      // just use the file name from the first sequence's first PDBEntry
-      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
-              .elementAt(0).getFile()).getAbsolutePath();
-      processingEntry.setFile(filePath);
-    }
-    return filePath;
-  }
-
   @Override
   public void eps_actionPerformed()
   {
index 3a6c89c..3124fc1 100644 (file)
@@ -5,6 +5,7 @@ import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.ChimeraXCommands;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureSelectionManager;
 
 import java.util.List;
@@ -40,7 +41,7 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
     int modelNumber = chimeraMaps.size() + 1;
     String command = "setattr #" + modelNumber + " models name "
             + pe.getId();
-    executeCommand(command, false);
+    executeCommand(new StructureCommand(command), false);
     modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
             modelNumber, 0));
   }
@@ -79,4 +80,10 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
     return ViewerType.CHIMERAX;
   }
 
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum + 1);
+  }
+
 }
index 3070ca1..d0f6cdb 100755 (executable)
@@ -107,6 +107,8 @@ public class Preferences extends GPreferences
 
   public static final String CHIMERAX_PATH = "CHIMERAX_PATH";
 
+  public static final String PYMOL_PATH = "PYMOL_PATH";
+
   public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
 
   public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE";
@@ -347,14 +349,14 @@ public class Preferences extends GPreferences
     boolean isChimeraX = viewerType.equals(ViewerType.CHIMERAX.name());
     if (viewerType.equals(ViewerType.JMOL.name()))
     {
-      chimeraPath.setText("");
+      structureViewerPath.setText("");
     }
     else
     {
-      chimeraPath.setText(Cache
+      structureViewerPath.setText(Cache
               .getDefault(isChimeraX ? CHIMERAX_PATH : CHIMERA_PATH, ""));
     }
-    chimeraPath.addActionListener(new ActionListener()
+    structureViewerPath.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -364,7 +366,7 @@ public class Preferences extends GPreferences
           Cache.setProperty(structViewer.getSelectedItem()
                   .equals(ViewerType.CHIMERAX.name())
                   ? CHIMERAX_PATH
-                  : CHIMERA_PATH, chimeraPath.getText());
+                  : CHIMERA_PATH, structureViewerPath.getText());
         }
       }
     });
@@ -697,12 +699,22 @@ public class Preferences extends GPreferences
             Boolean.toString(useRnaView.isSelected()));
     Cache.applicationProperties.setProperty(STRUCT_FROM_PDB,
             Boolean.toString(structFromPdb.isSelected()));
+    String viewer = structViewer.getSelectedItem().toString();
+    String viewerPath = structureViewerPath.getText();
     Cache.applicationProperties.setProperty(STRUCTURE_DISPLAY,
-            structViewer.getSelectedItem().toString());
-    boolean isChimeraX = structViewer.getSelectedItem().toString()
-            .equals(ViewerType.CHIMERAX.name());
-    Cache.setOrRemove(isChimeraX ? CHIMERAX_PATH : CHIMERA_PATH,
-            chimeraPath.getText());
+            viewer);
+    if (viewer.equals(ViewerType.CHIMERA.name()))
+    {
+      Cache.setOrRemove(CHIMERA_PATH, viewerPath);
+    }
+    else if (viewer.equals(ViewerType.CHIMERAX.name()))
+    {
+      Cache.setOrRemove(CHIMERAX_PATH, viewerPath);
+    }
+    else if (viewer.equals(ViewerType.PYMOL.name()))
+    {
+      Cache.setOrRemove(PYMOL_PATH, viewerPath);
+    }
     Cache.applicationProperties.setProperty("MAP_WITH_SIFTS",
             Boolean.toString(siftsMapping.isSelected()));
     SiftsSettings.setMapWithSifts(siftsMapping.isSelected());
@@ -1212,9 +1224,9 @@ public class Preferences extends GPreferences
    */
   private boolean validateChimeraPath()
   {
-    if (chimeraPath.getText().trim().length() > 0)
+    if (structureViewerPath.getText().trim().length() > 0)
     {
-      File f = new File(chimeraPath.getText());
+      File f = new File(structureViewerPath.getText());
       if (!f.canExecute())
       {
         JvOptionPane.showInternalMessageDialog(Desktop.desktop,
@@ -1228,33 +1240,33 @@ public class Preferences extends GPreferences
   }
 
   /**
-   * If Chimera or ChimeraX is selected, check it can be found on default or
-   * user-specified path, if not show a warning/help dialog.
+   * If Chimera or ChimeraX or Pymol is selected, check it can be found on
+   * default or user-specified path, if not show a warning/help dialog
    */
   @Override
   protected void structureViewer_actionPerformed(String selectedItem)
   {
     if (selectedItem.equals(ViewerType.JMOL.name()))
     {
-      chimeraPath.setEnabled(false);
-      chimeraPathLabel.setEnabled(false);
+      structureViewerPath.setEnabled(false);
+      structureViewerPathLabel.setEnabled(false);
       return;
     }
     boolean found = false;
-    chimeraPath.setEnabled(true);
-    chimeraPathLabel.setEnabled(true);
-    chimeraPathLabel.setText(MessageManager
+    structureViewerPath.setEnabled(true);
+    structureViewerPathLabel.setEnabled(true);
+    structureViewerPathLabel.setText(MessageManager
             .formatMessage("label.chimera_path", selectedItem));
 
     /*
      * Try user-specified and standard paths for Chimera executable
      */
     boolean isChimeraX = selectedItem.equals(ViewerType.CHIMERAX.name());
-    chimeraPath.setText(Cache
+    structureViewerPath.setText(Cache
             .getDefault(isChimeraX ? CHIMERAX_PATH : CHIMERA_PATH, ""));
 
     List<String> paths = StructureManager.getChimeraPaths(isChimeraX);
-    paths.add(0, chimeraPath.getText());
+    paths.add(0, structureViewerPath.getText());
     for (String path : paths)
     {
       if (new File(path.trim()).canExecute())
diff --git a/src/jalview/gui/PymolBindingModel.java b/src/jalview/gui/PymolBindingModel.java
new file mode 100644 (file)
index 0000000..af4afb0
--- /dev/null
@@ -0,0 +1,176 @@
+package jalview.gui;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.ext.pymol.PymolCommands;
+import jalview.ext.pymol.PymolManager;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureSelectionManager;
+import jalview.structures.models.AAStructureBindingModel;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PymolBindingModel extends AAStructureBindingModel
+{
+  private PymolManager pymolManager;
+
+  private Thread pymolMonitor;
+
+  /*
+   * full paths to structure files opened in PyMOL
+   */
+  List<String> structureFiles = new ArrayList<>();
+
+  /*
+   * lookup from file path to PyMOL object name
+   */
+  Map<String, String> pymolObjects = new HashMap<>();
+
+  /**
+   * Constructor
+   * 
+   * @param viewer
+   * @param ssm
+   * @param pdbentry
+   * @param sequenceIs
+   */
+  public PymolBindingModel(StructureViewerBase viewer,
+          StructureSelectionManager ssm, PDBEntry[] pdbentry,
+          SequenceI[][] sequenceIs)
+  {
+    super(ssm, pdbentry, sequenceIs, null);
+    pymolManager = new PymolManager();
+    setStructureCommands(new PymolCommands());
+    setViewer(viewer);
+  }
+
+  @Override
+  public String[] getStructureFiles()
+  {
+    return structureFiles.toArray(new String[structureFiles.size()]);
+  }
+
+  @Override
+  public void highlightAtoms(List<AtomSpec> atoms)
+  {
+  }
+
+  @Override
+  public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
+  {
+    // pull up?
+    return new SequenceRenderer(alignment.getAlignViewport());
+  }
+
+  @Override
+  protected List<String> executeCommand(StructureCommandI command,
+          boolean getReply)
+  {
+    System.out.println(command.toString()); // debug
+    return pymolManager.sendCommand(command, getReply);
+  }
+
+  @Override
+  protected String getModelIdForFile(String file)
+  {
+    return pymolObjects.containsKey(file) ? pymolObjects.get(file) : "";
+  }
+
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.PYMOL;
+  }
+
+  public boolean isPymolRunning()
+  {
+    return pymolManager.isPymolLaunched();
+  }
+
+  public void closeViewer(boolean closePymol)
+  {
+    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
+    if (closePymol)
+    {
+      pymolManager.exitPymol();
+    }
+    // if (this.pymolListener != null)
+    // {
+    // pymolListener.shutdown();
+    // pymolListener = null;
+    // }
+    pymolManager = null;
+
+    if (pymolMonitor != null)
+    {
+      pymolMonitor.interrupt();
+    }
+    releaseUIResources();
+  }
+
+  public boolean openSession(String pymolSessionFile)
+  {
+    StructureCommandI cmd = getCommandGenerator()
+            .loadFile(pymolSessionFile);
+    executeCommand(cmd, false);
+    return true;
+  }
+
+  public boolean launchPymol()
+  {
+    if (pymolManager.isPymolLaunched())
+    {
+      return true;
+    }
+
+    boolean launched = pymolManager.launchPymol();
+    if (launched)
+    {
+      // start listening for PyMOL selections - how??
+    }
+    else
+    {
+      System.err.println("Failed to launch PyMOL!");
+    }
+    return launched;
+  }
+
+  public void openFile(PDBEntry pe)
+  {
+    // todo : check not already open, remap / rename, etc
+    String file = pe.getFile();
+    StructureCommandI cmd = getCommandGenerator().loadFile(file);
+
+    /*
+     * a second parameter sets the pdbid as the loaded PyMOL object name
+     */
+    String pdbId = pe.getId();
+    cmd.addParameter(pdbId);
+
+    executeCommand(cmd, false);
+
+    pymolObjects.put(file, pdbId);
+    if (!structureFiles.contains(file))
+    {
+      structureFiles.add(file);
+    }
+    if (getSsm() != null)
+    {
+      getSsm().addStructureViewerListener(this);
+    }
+
+  }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return file;
+  }
+
+}
diff --git a/src/jalview/gui/PymolViewer.java b/src/jalview/gui/PymolViewer.java
new file mode 100644 (file)
index 0000000..09451be
--- /dev/null
@@ -0,0 +1,338 @@
+package jalview.gui;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
+import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.MessageManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JInternalFrame;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+public class PymolViewer extends StructureViewerBase
+{
+  private static final int myWidth = 500;
+
+  private static final int myHeight = 150;
+
+  private PymolBindingModel binding;
+
+  private String pymolSessionFile;
+
+  public PymolViewer()
+  {
+    super();
+
+    /*
+     * closeViewer will decide whether or not to close this frame
+     * depending on whether user chooses to Cancel or not
+     */
+    setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
+  }
+
+  public PymolViewer(PDBEntry pdb, SequenceI[] seqs, Object object,
+          AlignmentPanel ap)
+  {
+    this();
+    openNewPymol(ap, new PDBEntry[] { pdb },
+            new SequenceI[][]
+            { seqs });
+  }
+
+  public PymolViewer(PDBEntry[] pe, boolean alignAdded, SequenceI[][] seqs,
+          AlignmentPanel ap)
+  {
+    this();
+    setAlignAddedStructures(alignAdded);
+    openNewPymol(ap, pe, seqs);
+  }
+
+  private void openNewPymol(AlignmentPanel ap, PDBEntry[] pe,
+          SequenceI[][] seqs)
+  {
+    createProgressBar();
+    binding = new PymolBindingModel(this, ap.getStructureSelectionManager(),
+            pe, seqs);
+    addAlignmentPanel(ap);
+    useAlignmentPanelForColourbyseq(ap);
+
+    if (pe.length > 1)
+    {
+      useAlignmentPanelForSuperposition(ap);
+    }
+    binding.setColourBySequence(true);
+    setSize(myWidth, myHeight);
+    initMenus();
+
+    addingStructures = false;
+    worker = new Thread(this);
+    worker.start();
+
+    this.addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosing(
+              InternalFrameEvent internalFrameEvent)
+      {
+        closeViewer(false);
+      }
+    });
+
+  }
+
+  /**
+   * Create a helper to manage progress bar display
+   */
+  protected void createProgressBar()
+  {
+    if (getProgressIndicator() == null)
+    {
+      setProgressIndicator(new ProgressBar(statusPanel, statusBar));
+    }
+  }
+
+  @Override
+  public void run()
+  {
+    // todo pull up much of this
+  
+    StringBuilder errormsgs = new StringBuilder(128);
+    List<PDBEntry> filePDB = new ArrayList<>();
+    List<Integer> filePDBpos = new ArrayList<>();
+    String[] curfiles = binding.getStructureFiles(); // files currently in viewer
+    for (int pi = 0; pi < binding.getPdbCount(); pi++)
+    {
+      String file = null;
+      PDBEntry thePdbEntry = binding.getPdbEntry(pi);
+      if (thePdbEntry.getFile() == null)
+      {
+        /*
+         * Retrieve PDB data, save to file, attach to PDBEntry
+         */
+        file = fetchPdbFile(thePdbEntry);
+        if (file == null)
+        {
+          errormsgs.append("'" + thePdbEntry.getId() + "' ");
+        }
+      }
+      else
+      {
+        /*
+         * got file already
+         */
+        file = new File(thePdbEntry.getFile()).getAbsoluteFile()
+                .getPath();
+        // todo - skip if already loaded in PyMOL
+      }
+      if (file != null)
+      {
+        filePDB.add(thePdbEntry);
+        filePDBpos.add(Integer.valueOf(pi));
+      }
+    }
+        
+    if (!filePDB.isEmpty())
+    {
+      /*
+       * at least one structure to add to viewer
+       */
+      binding.setFinishedInit(false);
+      if (!addingStructures)
+      {
+        try
+        {
+          initPymol();
+        } catch (Exception ex)
+        {
+          Cache.log.error("Couldn't open PyMOL viewer!", ex);
+        }
+      }
+      int num = -1;
+      for (PDBEntry pe : filePDB)
+      {
+        num++;
+        if (pe.getFile() != null)
+        {
+          try
+          {
+            int pos = filePDBpos.get(num).intValue();
+            long startTime = startProgressBar(getViewerName() + " "
+                    + MessageManager.getString("status.opening_file_for")
+                    + " " + pe.getId());
+            binding.openFile(pe);
+            binding.addSequence(pos, binding.getSequence()[pos]);
+            File fl = new File(pe.getFile());
+            DataSourceType protocol = DataSourceType.URL;
+            try
+            {
+              if (fl.exists())
+              {
+                protocol = DataSourceType.FILE;
+              }
+            } catch (Throwable e)
+            {
+            } finally
+            {
+              stopProgressBar("", startTime);
+            }
+
+            StructureFile pdb = binding.getSsm().setMapping(
+                    binding.getSequence()[pos], binding.getChains()[pos],
+                    pe.getFile(), protocol,
+                    getProgressIndicator());
+            binding.stashFoundChains(pdb, pe.getFile());
+          } catch (Exception ex)
+          {
+            Cache.log.error(
+                    "Couldn't open " + pe.getFile() + " in Chimera viewer!",
+                    ex);
+          } finally
+          {
+            // Cache.log.debug("File locations are " + files);
+          }
+        }
+      }
+
+      binding.refreshGUI();
+      binding.setFinishedInit(true);
+      binding.setLoadingFromArchive(false);
+
+      /*
+       * ensure that any newly discovered features (e.g. RESNUM)
+       * are added to any open feature settings dialog
+       */
+      FeatureRenderer fr = getBinding().getFeatureRenderer(null);
+      if (fr != null)
+      {
+        fr.featuresAdded();
+      }
+
+      // refresh the sequence colours for the new structure(s)
+      for (AlignmentViewPanel ap : _colourwith)
+      {
+        binding.updateColours(ap);
+      }
+      // do superposition if asked to
+      if (alignAddedStructures)
+      {
+        new Thread(new Runnable()
+        {
+          @Override
+          public void run()
+          {
+            alignStructsWithAllAlignPanels();
+          }
+        }).start();
+      }
+      addingStructures = false;
+    }
+    _started = false;
+    worker = null;
+
+  }
+
+  /**
+   * Launch PyMOL. If we have a session file name, send PyMOL the command to
+   * open its saved session file.
+   */
+  void initPymol()
+  {
+    Desktop.addInternalFrame(this,
+            binding.getViewerTitle(getViewerName(), true),
+            getBounds().width, getBounds().height);
+
+    if (!binding.launchPymol())
+    {
+      JvOptionPane.showMessageDialog(Desktop.desktop,
+              MessageManager.getString("label.pymol_failed"),
+              MessageManager.getString("label.error_loading_file"),
+              JvOptionPane.ERROR_MESSAGE);
+      this.dispose();
+      return;
+    }
+
+    if (this.pymolSessionFile != null)
+    {
+      boolean opened = binding.openSession(pymolSessionFile);
+      if (!opened)
+      {
+        System.err.println("An error occurred opening PyMOL session file "
+                + pymolSessionFile);
+      }
+    }
+    // binding.startPymolListener();
+  }
+
+  @Override
+  public AAStructureBindingModel getBinding()
+  {
+    return binding;
+  }
+
+  @Override
+  public void closeViewer(boolean closePymol)
+  {
+    if (binding != null && binding.isPymolRunning())
+    {
+      if (!closePymol)
+      {
+        // TODO i18n (and pull up)
+        String prompt = MessageManager
+                .formatMessage("label.confirm_close_pymol", new Object[]
+                { binding.getViewerTitle(getViewerName(), false) });
+        prompt = JvSwingUtils.wrapTooltip(true, prompt);
+        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
+                MessageManager.getString("label.close_viewer"),
+                JvOptionPane.YES_NO_CANCEL_OPTION);
+        /*
+         * abort closure if user hits escape or Cancel
+         */
+        if (confirm == JvOptionPane.CANCEL_OPTION
+                || confirm == JvOptionPane.CLOSED_OPTION)
+        {
+          return;
+        }
+        closePymol = confirm == JvOptionPane.YES_OPTION;
+      }
+      binding.closeViewer(closePymol);
+    }
+    setAlignmentPanel(null);
+    _aps.clear();
+    _alignwith.clear();
+    _colourwith.clear();
+    // TODO: check for memory leaks where instance isn't finalised because
+    // binding
+    // holds a reference to the window
+    binding = null;
+    dispose();
+  }
+
+  @Override
+  public String getStateInfo()
+  {
+    return null;
+  }
+
+  @Override
+  public ViewerType getViewerType()
+  {
+    return ViewerType.PYMOL;
+  }
+
+  @Override
+  protected String getViewerName()
+  {
+    return "PyMOL";
+  }
+
+}
index 79d3836..360ddf2 100644 (file)
@@ -56,7 +56,7 @@ public class StructureViewer
 
   public enum ViewerType
   {
-    JMOL, CHIMERA, CHIMERAX
+    JMOL, CHIMERA, CHIMERAX, PYMOL
   };
 
   /**
@@ -170,6 +170,10 @@ public class StructureViewer
       sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs,
               ap);
     }
+    else if (viewerType.equals(ViewerType.PYMOL))
+    {
+      sview = new PymolViewer(pdbsForFile, superposeAdded, theSeqs, ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
@@ -314,6 +318,10 @@ public class StructureViewer
     {
       sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap);
     }
+    else if (viewerType.equals(ViewerType.PYMOL))
+    {
+      sview = new PymolViewer(pdb, seqsForPdb, null, ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
index 6dd7d50..25d9998 100644 (file)
@@ -36,6 +36,7 @@ import jalview.schemes.ColourSchemes;
 import jalview.structure.StructureMapping;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
+import jalview.ws.dbsources.Pdb;
 
 import java.awt.Color;
 import java.awt.Component;
@@ -1087,4 +1088,57 @@ public abstract class StructureViewerBase extends GStructureViewer
     getBinding().showChains(toshow);
   }
 
+  /**
+   * Tries to fetch a PDB file and save to a temporary local file. Returns the
+   * saved file path if successful, or null if not.
+   * 
+   * @param processingEntry
+   * @return
+   */
+  protected String fetchPdbFile(PDBEntry processingEntry)
+  {
+    String filePath = null;
+    Pdb pdbclient = new Pdb();
+    AlignmentI pdbseq = null;
+    String pdbid = processingEntry.getId();
+    long handle = System.currentTimeMillis()
+            + Thread.currentThread().hashCode();
+  
+    /*
+     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
+     */
+    String msg = MessageManager.formatMessage("status.fetching_pdb",
+            new Object[]
+            { pdbid });
+    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
+    // long hdl = startProgressBar(MessageManager.formatMessage(
+    // "status.fetching_pdb", new Object[]
+    // { pdbid }));
+    try
+    {
+      pdbseq = pdbclient.getSequenceRecords(pdbid);
+    } catch (Exception e)
+    {
+      System.err.println(
+              "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
+    } finally
+    {
+      msg = pdbid + " " + MessageManager.getString("label.state_completed");
+      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
+      // stopProgressBar(msg, hdl);
+    }
+    /*
+     * If PDB data were saved and are not invalid (empty alignment), return the
+     * file path.
+     */
+    if (pdbseq != null && pdbseq.getHeight() > 0)
+    {
+      // just use the file name from the first sequence's first PDBEntry
+      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
+              .elementAt(0).getFile()).getAbsolutePath();
+      processingEntry.setFile(filePath);
+    }
+    return filePath;
+  }
+
 }
index 6c46f43..f2761dd 100755 (executable)
@@ -179,9 +179,9 @@ public class GPreferences extends JPanel
 
   protected JComboBox<String> structViewer = new JComboBox<>();
 
-  protected JLabel chimeraPathLabel;
+  protected JLabel structureViewerPathLabel;
 
-  protected JTextField chimeraPath = new JTextField();
+  protected JTextField structureViewerPath = new JTextField();
 
   protected ButtonGroup mappingMethod = new ButtonGroup();
 
@@ -1253,6 +1253,7 @@ public class GPreferences extends JPanel
     structViewer.addItem(ViewerType.JMOL.name());
     structViewer.addItem(ViewerType.CHIMERA.name());
     structViewer.addItem(ViewerType.CHIMERAX.name());
+    structViewer.addItem(ViewerType.PYMOL.name());
     structViewer.addActionListener(new ActionListener()
     {
       @Override
@@ -1265,38 +1266,38 @@ public class GPreferences extends JPanel
     structureTab.add(structViewer);
 
     ypos += lineSpacing;
-    chimeraPathLabel = new JLabel();
-    chimeraPathLabel.setFont(LABEL_FONT);// new Font("SansSerif", 0, 11));
-    chimeraPathLabel.setHorizontalAlignment(SwingConstants.LEFT);
-    chimeraPathLabel.setText(MessageManager
+    structureViewerPathLabel = new JLabel();
+    structureViewerPathLabel.setFont(LABEL_FONT);// new Font("SansSerif", 0, 11));
+    structureViewerPathLabel.setHorizontalAlignment(SwingConstants.LEFT);
+    structureViewerPathLabel.setText(MessageManager
             .formatMessage("label.chimera_path", "Chimera(X)"));
-    chimeraPathLabel.setBounds(new Rectangle(10, ypos, 170, height));
-    chimeraPathLabel.setEnabled(false);
-    structureTab.add(chimeraPathLabel);
+    structureViewerPathLabel.setBounds(new Rectangle(10, ypos, 170, height));
+    structureViewerPathLabel.setEnabled(false);
+    structureTab.add(structureViewerPathLabel);
 
-    chimeraPath.setFont(LABEL_FONT);
-    chimeraPath.setText("");
-    chimeraPath.setEnabled(false);
+    structureViewerPath.setFont(LABEL_FONT);
+    structureViewerPath.setText("");
+    structureViewerPath.setEnabled(false);
     final String tooltip = JvSwingUtils.wrapTooltip(true,
             MessageManager.getString("label.chimera_path_tip"));
-    chimeraPath.setToolTipText(tooltip);
-    chimeraPath.setBounds(new Rectangle(190, ypos, 290, height));
-    chimeraPath.addMouseListener(new MouseAdapter()
+    structureViewerPath.setToolTipText(tooltip);
+    structureViewerPath.setBounds(new Rectangle(190, ypos, 290, height));
+    structureViewerPath.addMouseListener(new MouseAdapter()
     {
       @Override
       public void mouseClicked(MouseEvent e)
       {
-        if (chimeraPath.isEnabled() && e.getClickCount() == 2)
+        if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
         {
           String chosen = openFileChooser();
           if (chosen != null)
           {
-            chimeraPath.setText(chosen);
+            structureViewerPath.setText(chosen);
           }
         }
       }
     });
-    structureTab.add(chimeraPath);
+    structureTab.add(structureViewerPath);
 
     ypos += lineSpacing;
     nwMapping.setFont(LABEL_FONT);
index 1b7d284..1ef653e 100644 (file)
@@ -33,9 +33,9 @@ import java.util.TreeMap;
 public class AtomSpecModel
 {
   /*
-   * { modelNo, {chainCode, List<from-to> ranges} }
+   * { modelId, {chainCode, List<from-to> ranges} }
    */
-  private Map<Integer, Map<String, BitSet>> atomSpec;
+  private Map<String, Map<String, BitSet>> atomSpec;
 
   /**
    * Constructor
@@ -53,7 +53,7 @@ public class AtomSpecModel
    * @param endPos
    * @param chain
    */
-  public void addRange(int model, int startPos, int endPos, String chain)
+  public void addRange(String model, int startPos, int endPos, String chain)
   {
     /*
      * Get/initialize map of data for the colour and model
@@ -80,12 +80,17 @@ public class AtomSpecModel
     chainData.set(startPos, endPos + 1);
   }
 
-  public Iterable<Integer> getModels()
+  public Iterable<String> getModels()
   {
     return atomSpec.keySet();
   }
 
-  public Iterable<String> getChains(Integer model)
+  public int getModelCount()
+  {
+    return atomSpec.size();
+  }
+
+  public Iterable<String> getChains(String model)
   {
     return atomSpec.containsKey(model) ? atomSpec.get(model).keySet()
             : null;
@@ -99,7 +104,7 @@ public class AtomSpecModel
    * @param chain
    * @return
    */
-  public List<int[]> getRanges(Integer model, String chain)
+  public List<int[]> getRanges(String model, String chain)
   {
     List<int[]> ranges = new ArrayList<>();
     if (atomSpec.containsKey(model))
diff --git a/src/jalview/structure/StructureCommand.java b/src/jalview/structure/StructureCommand.java
new file mode 100644 (file)
index 0000000..f7875ab
--- /dev/null
@@ -0,0 +1,75 @@
+package jalview.structure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StructureCommand implements StructureCommandI
+{
+  private String command;
+
+  private List<String> parameters;
+
+  public StructureCommand(String cmd, String... params)
+  {
+    command = cmd;
+    if (params != null)
+    {
+      for (String p : params)
+      {
+        addParameter(p);
+      }
+    }
+  }
+
+  @Override
+  public void addParameter(String param)
+  {
+    if (parameters == null)
+    {
+      parameters = new ArrayList<>();
+    }
+    parameters.add(param);
+  }
+
+  @Override
+  public String getCommand()
+  {
+    return command;
+  }
+
+  @Override
+  public List<String> getParameters()
+  {
+    return parameters;
+  }
+
+  @Override
+  public boolean hasParameters()
+  {
+    return parameters != null && !parameters.isEmpty();
+  }
+
+  @Override
+  public String toString()
+  {
+    if (!hasParameters()) 
+    {
+      return command;
+    }
+    StringBuilder sb = new StringBuilder(32);
+    sb.append(command).append("(");
+    boolean first = true;
+    for (String p : parameters)
+    {
+      if (!first)
+      {
+        sb.append(",");
+      }
+      first = false;
+      sb.append(p);
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/structure/StructureCommandI.java b/src/jalview/structure/StructureCommandI.java
new file mode 100644 (file)
index 0000000..e39bbba
--- /dev/null
@@ -0,0 +1,14 @@
+package jalview.structure;
+
+import java.util.List;
+
+public interface StructureCommandI
+{
+  String getCommand();
+
+  List<String> getParameters();
+
+  void addParameter(String param);
+
+  boolean hasParameters();
+}
index 44764db..8c6ea4e 100644 (file)
@@ -1,18 +1,10 @@
 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;
@@ -39,7 +31,8 @@ public abstract class StructureCommandsBase implements StructureCommandsI
   }
 
   @Override
-  public String[] setAttributesForFeatures(StructureSelectionManager ssm,
+  public List<StructureCommandI> setAttributesForFeatures(
+          StructureSelectionManager ssm,
           String[] files, SequenceI[][] sequence, AlignmentViewPanel avp)
   {
     // default does nothing, override where this is implemented
@@ -74,8 +67,8 @@ public abstract class StructureCommandsBase implements StructureCommandsI
    * @param chain
    */
   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
-          Object value,
-          int model, int startPos, int endPos, String chain)
+          Object value, String model, int startPos, int endPos,
+          String chain)
   {
     /*
      * Get/initialize map of data for the colour
@@ -99,30 +92,22 @@ public abstract class StructureCommandsBase implements StructureCommandsI
    * @return
    */
   @Override
-  public String[] colourBySequence(Map<Object, AtomSpecModel> colourMap)
+  public List<StructureCommandI> colourBySequence(
+          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.
+     * default implementation creates one command per colour;
+     * override to concatenate colour commands if wanted
      */
-    List<String> commands = new ArrayList<>();
-    StringBuilder sb = new StringBuilder(256);
-    boolean firstColour = true;
+    List<StructureCommandI> commands = new ArrayList<>();
     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(getColourCommand(colourData, colour));
     }
-    commands.add(sb.toString());
 
-    return commands.toArray(new String[commands.size()]);
+    return commands;
   }
 
   /**
@@ -133,7 +118,8 @@ public abstract class StructureCommandsBase implements StructureCommandsI
    * @param colour
    * @return
    */
-  protected String getColourCommand(AtomSpecModel atomSpecModel, Color colour)
+  protected StructureCommandI getColourCommand(AtomSpecModel atomSpecModel,
+          Color colour)
   {
     String atomSpec = getAtomSpec(atomSpecModel, false);
     return getColourCommand(atomSpec, colour);
@@ -147,21 +133,25 @@ public abstract class StructureCommandsBase implements StructureCommandsI
    * @param colour
    * @return
    */
-  protected abstract String getColourCommand(String atomSpec, Color colour);
+  protected abstract StructureCommandI getColourCommand(String atomSpec,
+          Color colour);
 
   @Override
-  public String colourByResidues(Map<String, Color> colours)
+  public List<StructureCommandI> colourByResidues(
+          Map<String, Color> colours)
   {
-    StringBuilder cmd = new StringBuilder(12 * colours.size());
-  
+    List<StructureCommandI> commands = new ArrayList<>();
     for (Entry<String, Color> entry : colours.entrySet())
     {
-      String residue = entry.getKey();
-      String atomSpec = getResidueSpec(residue);
-      cmd.append(getColourCommand(atomSpec, entry.getValue()));
-      cmd.append(getCommandSeparator());
+      commands.add(colourResidue(entry.getKey(), entry.getValue()));
     }
-    return cmd.toString();
+    return commands;
+  }
+
+  private StructureCommandI colourResidue(String resName, Color col)
+  {
+    String atomSpec = getResidueSpec(resName);
+    return getColourCommand(atomSpec, col);
   }
 
   /**
diff --git a/src/jalview/structure/StructureCommandsFactory.java b/src/jalview/structure/StructureCommandsFactory.java
deleted file mode 100644 (file)
index 9319427..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package jalview.structure;
-
-import jalview.ext.jmol.JmolCommands;
-import jalview.ext.rbvi.chimera.ChimeraCommands;
-import jalview.ext.rbvi.chimera.ChimeraXCommands;
-import jalview.gui.StructureViewer.ViewerType;
-
-/**
- * A factory that serves a class that can generate structure commands for a
- * specified structure viewer
- */
-public class StructureCommandsFactory
-{
-  public StructureCommandsI getStructureCommands(ViewerType viewer)
-  {
-    StructureCommandsI commands = null;
-    switch (viewer)
-    {
-    case JMOL:
-      commands = new JmolCommands();
-      break;
-    case CHIMERA:
-      commands = new ChimeraCommands();
-      break;
-    case CHIMERAX:
-      commands = new ChimeraXCommands();
-      break;
-    default:
-    }
-    return commands;
-  }
-}
index 359eac6..0934488 100644 (file)
@@ -17,47 +17,11 @@ 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
    * 
    * @return
    */
-  String colourByChain();
+  StructureCommandI colourByChain();
 
   /**
    * Returns the command to colour residues using a charge-based scheme:
@@ -70,7 +34,7 @@ public interface StructureCommandsI
    * 
    * @return
    */
-  String colourByCharge();
+  List<StructureCommandI> colourByCharge();
 
   /**
    * Returns the command to colour residues with the colours provided in the
@@ -79,7 +43,7 @@ public interface StructureCommandsI
    * @param colours
    * @return
    */
-  String colourByResidues(Map<String, Color> colours);
+  List<StructureCommandI> colourByResidues(Map<String, Color> colours);
 
   /**
    * Returns the command to set the background colour of the structure viewer
@@ -87,7 +51,7 @@ public interface StructureCommandsI
    * @param col
    * @return
    */
-  String setBackgroundColour(Color col);
+  StructureCommandI setBackgroundColour(Color col);
 
   /**
    * Returns commands to colour mapped residues of structures according to
@@ -98,23 +62,24 @@ public interface StructureCommandsI
    * @return
    */
 
-  String[] colourBySequence(Map<Object, AtomSpecModel> colourMap);
+  List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap);
 
   /**
    * Returns a command to centre the display in the structure viewer
    * 
    * @return
    */
-  String focusView();
+  StructureCommandI focusView();
 
   /**
    * Returns a command to show only the selected chains. The items in the input
-   * list should be formatted as "modelno:chainid".
+   * list should be formatted as "modelid:chainid".
    * 
    * @param toShow
    * @return
    */
-  String showChains(List<String> toShow);
+  List<StructureCommandI> showChains(List<String> toShow);
 
   /**
    * Returns zero, one or more commands to set attributes on mapped residues in
@@ -126,7 +91,8 @@ public interface StructureCommandsI
    * @param avp
    * @return
    */
-  String[] setAttributesForFeatures(StructureSelectionManager ssm,
+  List<StructureCommandI> setAttributesForFeatures(
+          StructureSelectionManager ssm,
           String[] files, SequenceI[][] sequence, AlignmentViewPanel avp);
 
   /**
@@ -139,7 +105,7 @@ public interface StructureCommandsI
    * @param atomSpec
    * @return
    */
-  String superposeStructures(AtomSpecModel refAtoms,
+  List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
           AtomSpecModel atomSpec);
 
   /**
@@ -148,7 +114,7 @@ public interface StructureCommandsI
    * @param path
    * @return
    */
-  String openCommandFile(String path);
+  StructureCommandI openCommandFile(String path);
 
   /**
    * Returns a command to save the current viewer session state to the given
@@ -157,7 +123,7 @@ public interface StructureCommandsI
    * @param filepath
    * @return
    */
-  String saveSession(String filepath);
+  StructureCommandI saveSession(String filepath);
 
   /**
    * Returns a representation of the atom set represented by the model, in
@@ -181,9 +147,19 @@ public interface StructureCommandsI
   int getModelStartNo();
 
   /**
-   * Show only the backbone of the peptide (cartoons in Jmol, chain in Chimera)
+   * Returns command(s) to show only the backbone of the peptide (cartoons in
+   * Jmol, chain in Chimera)
+   * 
+   * @return
+   */
+  List<StructureCommandI> showBackbone();
+
+  /**
+   * Returns a command to open a file at the given path
    * 
+   * @param file
    * @return
    */
-  String showBackbone();
+  // refactor if needed to distinguish loading data or session files
+  StructureCommandI loadFile(String file);
 }
index 0c1cd50..8d8957c 100644 (file)
@@ -32,13 +32,14 @@ import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ResidueProperties;
 import jalview.structure.AtomSpec;
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsI;
-import jalview.structure.StructureCommandsI.SuperposeData;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -69,6 +70,42 @@ public abstract class AAStructureBindingModel
         extends SequenceStructureBindingModel
         implements StructureListener, StructureSelectionManagerProvider
 {
+  /**
+   * Data bean class to simplify parameterisation in superposeStructures
+   */
+  public static 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 String modelId;
+  
+    /**
+     * 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, String model)
+    {
+      pdbResNo = new int[width];
+      modelId = model;
+    }
+  }
+
   private static final int MIN_POS_TO_SUPERPOSE = 4;
 
   private static final String COLOURING_STRUCTURES = MessageManager
@@ -125,7 +162,7 @@ public abstract class AAStructureBindingModel
   private boolean finishedInit = false;
 
   /**
-   * current set of model filenames loaded in the Jmol instance
+   * current set of model filenames loaded in the viewer
    */
   protected String[] modelFileNames = null;
 
@@ -613,7 +650,7 @@ public abstract class AAStructureBindingModel
    * @return
    */
   protected int findSuperposableResidues(AlignmentI alignment,
-          BitSet matched, SuperposeData[] structures)
+          BitSet matched, AAStructureBindingModel.SuperposeData[] structures)
   {
     int refStructure = -1;
     String[] files = getStructureFiles();
@@ -828,11 +865,11 @@ public abstract class AAStructureBindingModel
         }
       }
 
-      SuperposeData[] structures = new SuperposeData[files.length];
+      AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
       for (int f = 0; f < files.length; f++)
       {
-        structures[f] = new SuperposeData(width,
-                f + commandGenerator.getModelStartNo());
+        structures[f] = new AAStructureBindingModel.SuperposeData(width,
+                getModelIdForFile(files[f]));
       }
 
       /*
@@ -864,7 +901,8 @@ public abstract class AAStructureBindingModel
        * Show all as backbone before doing superposition(s)
        * (residues used for matching will be shown as ribbon)
        */
-      executeCommand(commandGenerator.showBackbone(), false);
+      // todo better way to ensure synchronous than setting getReply true!!
+      executeCommands(commandGenerator.showBackbone(), true, null);
 
       /*
        * superpose each (other) structure to the reference in turn
@@ -874,9 +912,10 @@ public abstract class AAStructureBindingModel
         if (i != refStructure)
         {
           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
-          String commands = commandGenerator.superposeStructures(refAtoms,
+          List<StructureCommandI> commands = commandGenerator
+                  .superposeStructures(refAtoms,
                   atomSpec);
-          List<String> replies = executeCommands(true, commands);
+          List<String> replies = executeCommands(commands, true, null);
           for (String reply : replies)
           {
             // return this error (Chimera only) to the user
@@ -892,7 +931,7 @@ public abstract class AAStructureBindingModel
     return error;
   }
 
-  private AtomSpecModel getAtomSpec(SuperposeData superposeData,
+  private AtomSpecModel getAtomSpec(AAStructureBindingModel.SuperposeData superposeData,
           BitSet matched)
   {
     AtomSpecModel model = new AtomSpecModel();
@@ -900,7 +939,7 @@ public abstract class AAStructureBindingModel
     while (nextColumnMatch != -1)
     {
       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
-      model.addRange(superposeData.modelNo, pdbResNum, pdbResNum,
+      model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
               superposeData.chain);
       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
     }
@@ -941,7 +980,7 @@ public abstract class AAStructureBindingModel
   {
     colourBySequence = false;
 
-    executeCommand(commandGenerator.colourByCharge(), false,
+    executeCommands(commandGenerator.colourByCharge(), false,
             COLOURING_STRUCTURES);
   }
 
@@ -980,13 +1019,14 @@ public abstract class AAStructureBindingModel
     /*
      * pass to the command constructor, and send the command
      */
-    String cmd = commandGenerator.colourByResidues(colours);
-    executeCommand(cmd, false, COLOURING_STRUCTURES);
+    List<StructureCommandI> cmd = commandGenerator
+            .colourByResidues(colours);
+    executeCommands(cmd, false, COLOURING_STRUCTURES);
   }
 
   public void setBackgroundColour(Color col)
   {
-    String cmd = commandGenerator.setBackgroundColour(col);
+    StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
     executeCommand(cmd, false, null);
   }
 
@@ -1002,95 +1042,119 @@ public abstract class AAStructureBindingModel
    * @param msg
    * @return
    */
-  private List<String> executeCommand(String cmd, boolean getReply,
-          String msg)
+  private List<String> executeCommand(StructureCommandI cmd,
+          boolean getReply, String msg)
   {
     if (getReply)
     {
-      return executeSynchronous(cmd, msg, getReply);
+      /*
+       * synchronous (same thread) execution so reply can be returned
+       */
+      final JalviewStructureDisplayI theViewer = getViewer();
+      final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
+      try
+      {
+        return executeCommand(cmd, getReply);
+      } finally
+      {
+        if (msg != null)
+        {
+          theViewer.stopProgressBar(null, handle);
+        }
+      }
     }
     else
     {
-      executeAsynchronous(cmd, msg);
+      /*
+       * asynchronous (new thread) execution if no reply needed
+       */
+      final JalviewStructureDisplayI theViewer = getViewer();
+      final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
+      
+      SwingUtilities.invokeLater(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          try
+          {
+            executeCommand(cmd, false);
+          } finally
+          {
+            if (msg != null)
+            {
+              theViewer.stopProgressBar(null, handle);
+            }
+          }
+        }
+      });
       return null;
     }
   }
 
   /**
-   * Sends the command in the current thread. If a message is supplied, this is
-   * shown before the thread is started, and removed when it completes. May
-   * return a reply to the command if requested.
+   * Execute one structure viewer command. If {@code getReply} is true, may
+   * optionally return one or more reply messages, else returns null.
    * 
    * @param cmd
-   * @param msg
    * @param getReply
-   * @return
    */
-  private List<String> executeSynchronous(String cmd, String msg, boolean getReply)
+  protected abstract List<String> executeCommand(StructureCommandI cmd,
+          boolean getReply);
+
+  /**
+   * A helper method that converts list of commands to a vararg array
+   * 
+   * @param commands
+   * @param getReply
+   * @param msg
+   */
+  private List<String> executeCommands(List<StructureCommandI> commands,
+          boolean getReply, String msg)
   {
-    final JalviewStructureDisplayI theViewer = getViewer();
-    final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
-    try
-    {
-      return executeCommand(cmd, getReply);
-    } finally
-    {
-      if (msg != null)
-      {
-        theViewer.stopProgressBar(null, handle);
-      }
-    }
+    return executeCommands(getReply, msg,
+            commands.toArray(new StructureCommandI[commands.size()]));
   }
 
   /**
-   * Sends the command in a separate thread. If a message is supplied, this is
-   * shown before the thread is started, and removed when it completes. No value
-   * is returned.
+   * Executes one or more structure viewer commands. If a progress message is
+   * provided, it is shown first, and removed after all commands have been run.
    * 
-   * @param cmd
+   * @param getReply
    * @param msg
+   * @param commands
+   * @return
    */
-  private void executeAsynchronous(String cmd, String msg)
+  protected List<String> executeCommands(boolean getReply, String msg,
+          StructureCommandI[] commands)
   {
+    // todo: tidy this up
+
+    /*
+     * show progress message if specified
+     */
     final JalviewStructureDisplayI theViewer = getViewer();
     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
 
-    SwingUtilities.invokeLater(new Runnable()
+    List<String> response = getReply ? new ArrayList<>() : null;
+    try
     {
-      @Override
-      public void run()
+      for (StructureCommandI cmd : commands)
       {
-        try
-        {
-          executeCommand(cmd, false);
-        } finally
+        List<String> replies = executeCommand(cmd, getReply, null);
+        if (getReply && replies != null)
         {
-          if (msg != null)
-          {
-            theViewer.stopProgressBar(null, handle);
-          }
+          response.addAll(replies);
         }
       }
-    });
-  }
-
-  protected abstract List<String> executeCommand(String command,
-          boolean getReply);
-
-  protected List<String> executeCommands(boolean getReply,
-          String... commands)
-  {
-    // todo: tidy this up
-    List<String> response = getReply ? new ArrayList<>() : null;
-    for (String cmd : commands)
+      return response;
+    } finally
     {
-      List<String> replies = executeCommand(cmd, getReply);
-      if (getReply && replies != null)
+      if (msg != null)
       {
-        response.addAll(replies);
+        theViewer.stopProgressBar(null, handle);
       }
     }
-    return response;
   }
 
   /**
@@ -1114,9 +1178,9 @@ public abstract class AAStructureBindingModel
     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
             sequence, sr, alignmentv);
 
-    String[] colourBySequenceCommands = commandGenerator
+    List<StructureCommandI> colourBySequenceCommands = commandGenerator
             .colourBySequence(colourMap);
-    executeCommands(false, colourBySequenceCommands);
+    executeCommands(colourBySequenceCommands, false, null);
   }
 
   /**
@@ -1124,7 +1188,7 @@ public abstract class AAStructureBindingModel
    */
   public void focusView()
   {
-    executeCommand(commandGenerator.focusView(), false);
+    executeCommand(commandGenerator.focusView(), false, null);
   }
 
   /**
@@ -1150,22 +1214,21 @@ public abstract class AAStructureBindingModel
       if (tokens.length == 2)
       {
         String pdbFile = getFileForChain(chainId);
-        int modelNo = getModelNoForFile(pdbFile);
-        String model = modelNo == -1 ? "" : String.valueOf(modelNo);
+        String model = getModelIdForFile(pdbFile);
         showThese.add(model + ":" + tokens[1]);
       }
     }
-    executeCommand(commandGenerator.showChains(showThese), false);
+    executeCommands(commandGenerator.showChains(showThese), false, null);
   }
 
   /**
-   * Answers the structure viewer's model number given a PDB file name. Returns
-   * -1 if model number is not found.
+   * Answers the structure viewer's model id given a PDB file name. Returns an
+   * empty string if model id is not found.
    * 
    * @param chainId
    * @return
    */
-  protected abstract int getModelNoForFile(String chainId);
+  protected abstract String getModelIdForFile(String chainId);
 
   public boolean hasFileLoadingError()
   {
@@ -1247,7 +1310,8 @@ public abstract class AAStructureBindingModel
    * @param command
    * @param progressMsg
    */
-  protected void sendAsynchronousCommand(String command, String progressMsg)
+  protected void sendAsynchronousCommand(StructureCommandI command,
+          String progressMsg)
   {
     final JalviewStructureDisplayI theViewer = getViewer();
     final long handle = progressMsg == null ? 0
@@ -1259,7 +1323,7 @@ public abstract class AAStructureBindingModel
       {
         try
         {
-          executeCommand(command, false);
+          executeCommand(command, false, null);
         } finally
         {
           if (progressMsg != null)
@@ -1279,7 +1343,7 @@ public abstract class AAStructureBindingModel
    * {@code AtomSpecModel}, where the atomspec model holds
    * 
    * <pre>
-   *   Model numbers
+   *   Model ids
    *     Chains
    *       Residue positions
    * </pre>
@@ -1308,7 +1372,8 @@ public abstract class AAStructureBindingModel
   
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
-      final int modelNumber = pdbfnum + commandGenerator.getModelStartNo();
+      // todo indirect this resolution / allow override
+      final String modelId = getModelIdForFile(files[pdbfnum]);
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
   
       if (mapping == null || mapping.length < 1)
@@ -1365,7 +1430,7 @@ public abstract class AAStructureBindingModel
               {
                 if (startPos != -1)
                 {
-                  addAtomSpecRange(colourMap, lastColour, modelNumber,
+                  addAtomSpecRange(colourMap, lastColour, modelId,
                           startPos, lastPos, lastChain);
                 }
                 startPos = pos;
@@ -1377,7 +1442,7 @@ public abstract class AAStructureBindingModel
             // final colour range
             if (lastColour != null)
             {
-              addAtomSpecRange(colourMap, lastColour, modelNumber, startPos,
+              addAtomSpecRange(colourMap, lastColour, modelId, startPos,
                       lastPos, lastChain);
             }
             // break;
@@ -1389,6 +1454,35 @@ public abstract class AAStructureBindingModel
   }
 
   /**
+   * todo better refactoring (map lookup or similar to get viewer structure id)
+   * 
+   * @param pdbfnum
+   * @param file
+   * @return
+   */
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum);
+  }
+
+  /**
+   * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
+   * full PDB file path
+   * 
+   * @param pdb
+   * @param file
+   */
+  public void stashFoundChains(StructureFile pdb, String file)
+  {
+    for (int i = 0; i < pdb.getChains().size(); i++)
+    {
+      String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
+      addChainFile(chid, file);
+      getChainNames().add(chid);
+    }
+  }
+
+  /**
    * 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>
@@ -1406,7 +1500,7 @@ public abstract class AAStructureBindingModel
    */
   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
           Object value,
-          int model, int startPos, int endPos, String chain)
+          String model, int startPos, int endPos, String chain)
   {
     /*
      * Get/initialize map of data for the colour
index d1a9df6..211e918 100644 (file)
@@ -33,6 +33,7 @@ import jalview.gui.JvOptionPane;
 import jalview.gui.SequenceRenderer;
 import jalview.schemes.JalviewColourScheme;
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -40,6 +41,7 @@ import jalview.structure.StructureSelectionManager;
 import java.awt.Color;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
@@ -131,32 +133,32 @@ public class JmolCommandsTest
     StructureCommandsI testee = new JmolCommands();
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, false), "");
-    model.addRange(1, 2, 4, "A");
+    model.addRange("1", 2, 4, "A");
     assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1");
-    model.addRange(1, 8, 8, "A");
+    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");
+    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");
+    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");
+    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");
+    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");
+    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");
+    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
+    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, " ");
+    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");
   
@@ -166,24 +168,27 @@ public class JmolCommandsTest
   public void testColourBySequence()
   {
     Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
-    JmolCommands.addAtomSpecRange(map, Color.blue, 1, 2, 5, "A");
-    JmolCommands.addAtomSpecRange(map, Color.blue, 1, 7, 7, "B");
-    JmolCommands.addAtomSpecRange(map, Color.blue, 1, 9, 23, "A");
-    JmolCommands.addAtomSpecRange(map, Color.blue, 2, 1, 1, "A");
-    JmolCommands.addAtomSpecRange(map, Color.blue, 2, 4, 7, "B");
-    JmolCommands.addAtomSpecRange(map, Color.yellow, 2, 8, 8, "A");
-    JmolCommands.addAtomSpecRange(map, Color.yellow, 2, 3, 5, "A");
-    JmolCommands.addAtomSpecRange(map, Color.red, 1, 3, 5, "A");
-    JmolCommands.addAtomSpecRange(map, Color.red, 1, 6, 9, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B");
+    JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A");
+    JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A");
 
     // Colours should appear in the Jmol command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
-    String[] commands = new JmolCommands().colourBySequence(map);
-    assertEquals(commands.length, 1);
-    String expected = "select 2-5:A/1.1|9-23:A/1.1|7:B/1.1|1:A/2.1|4-7:B/2.1;color[0,0,255]; "
-            + "select 3-5:A/2.1|8:A/2.1;color[255,255,0]; "
-            + "select 3-9:A/1.1;color[255,0,0]";
-    assertEquals(commands[0], expected);
+    List<StructureCommandI> commands = new JmolCommands()
+            .colourBySequence(map);
+    assertEquals(commands.size(), 3);
+    String expected1 = "select 2-5:A/1.1|9-23:A/1.1|7:B/1.1|1:A/2.1|4-7:B/2.1;color[0,0,255]";
+    String expected2 = "select 3-5:A/2.1|8:A/2.1;color[255,255,0]";
+    String expected3 = "select 3-9:A/1.1;color[255,0,0]";
+    assertEquals(commands.get(0).getCommand(), expected1);
+    assertEquals(commands.get(1).getCommand(), expected2);
+    assertEquals(commands.get(2).getCommand(), expected3);
   }
 
   @Test(groups = { "Functional" })
@@ -191,20 +196,22 @@ public class JmolCommandsTest
   {
     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");
+    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);
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> command = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(command.size(), 1);
     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);
+    assertEquals(command.get(0).getCommand(), expected);
   }
 
   @Test(groups = "Functional")
diff --git a/test/jalview/ext/pymol/PymolCommandsTest.java b/test/jalview/ext/pymol/PymolCommandsTest.java
new file mode 100644 (file)
index 0000000..a66f809
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * 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.pymol;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsI;
+
+import java.awt.Color;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class PymolCommandsTest
+{
+
+  @Test(groups = { "Functional" })
+  public void testColourBySequence()
+  {
+
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B");
+    PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A");
+    PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A");
+
+    // Colours should appear in the Pymol command in the order in which
+    // they were added; within colour, by model, by chain, ranges in start order
+    List<StructureCommandI> commands = new PymolCommands()
+            .colourBySequence(map);
+    assertEquals(commands.size(), 3);
+    assertEquals(commands.get(0).toString(),
+            "color(0x0000ff,0//A/2-5+9-23/ 0//B/7/ 1//A/1/ 1//B/4-7/)");
+    assertEquals(commands.get(1).toString(), "color(0xffff00,1//A/3-5+8/)");
+    assertEquals(
+            commands.get(2).toString(), "color(0xff0000,0//A/3-9/)");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    StructureCommandsI testee = new PymolCommands();
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4/");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/ 1//B/5-7/");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/ 5///25-35/");
+
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    StructureCommandsI testee = new PymolCommands();
+    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");
+    List<StructureCommandI> commands = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(commands.size(), 2);
+    String refSpecCA = "1//A/12-14/CA 1//B/18+22-23/CA";
+    String toAlignSpecCA = "2//B/15-17+20-21/CA 2//C/22/CA";
+    String refSpec = "1//A/12-14/ 1//B/18+22-23/";
+    String toAlignSpec = "2//B/15-17+20-21/ 2//C/22/";
+    String expected1 = String.format("super(%s,%s)", refSpecCA,
+            toAlignSpecCA);
+    String expected2 = String.format("show(cartoon,%s %s)", refSpec,
+            toAlignSpec);
+    assertEquals(commands.get(0).toString(), expected1);
+    assertEquals(commands.get(1).toString(), expected2);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec_alphaOnly()
+  {
+    StructureCommandsI testee = new PymolCommands();
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, true), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, true), "1//A/2-4/CA");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, true), "1//A/2-4+8/CA");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "1//A/2-4+8/CA 1//B/5-7/CA");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA 5///25-35/CA");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModelStartNo()
+  {
+    StructureCommandsI testee = new PymolCommands();
+    assertEquals(testee.getModelStartNo(), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResidueSpec()
+  {
+    PymolCommands testee = new PymolCommands();
+    assertEquals(testee.getResidueSpec("ALA"), "resn ALA");
+  }
+
+  @Test(groups = "Functional")
+  public void testShowBackbone()
+  {
+    PymolCommands testee = new PymolCommands();
+    List<StructureCommandI> cmds = testee.showBackbone();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0).toString(), "hide(everything)");
+    assertEquals(cmds.get(1).toString(), "show(ribbon)");
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenCommandFile()
+  {
+    PymolCommands testee = new PymolCommands();
+    assertEquals(testee.openCommandFile("commands.pml").toString(),
+            "@commands.pml");
+  }
+
+  @Test(groups = "Functional")
+  public void testSaveSession()
+  {
+    PymolCommands testee = new PymolCommands();
+    assertEquals(testee.saveSession("somewhere.pse").toString(),
+            "save(somewhere.pse)");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourCommand()
+  {
+    PymolCommands testee = new PymolCommands();
+    assertEquals(
+            testee.getColourCommand("something", Color.MAGENTA).toString(),
+            "color(0xff00ff,something)");
+  }
+}
diff --git a/test/jalview/ext/pymol/PymolManagerTest.java b/test/jalview/ext/pymol/PymolManagerTest.java
new file mode 100644 (file)
index 0000000..19b779d
--- /dev/null
@@ -0,0 +1,26 @@
+package jalview.ext.pymol;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.structure.StructureCommand;
+
+import org.testng.annotations.Test;
+
+public class PymolManagerTest
+{
+  @Test(groups = "Functional")
+  public void testGetPostRequest()
+  {
+    String req = PymolManager
+            .getPostRequest(new StructureCommand("foobar"));
+    assertEquals(req,
+            "<methodCall><methodName>foobar</methodName><params></params></methodCall>");
+
+    req = PymolManager
+            .getPostRequest(new StructureCommand("foobar", "blue", "all"));
+    assertEquals(req, "<methodCall><methodName>foobar</methodName><params>"
+            + "<parameter><value>blue</value></parameter>"
+            + "<parameter><value>all</value></parameter>"
+            + "</params></methodCall>");
+  }
+}
index 5f0e84b..5afbd7a 100644 (file)
@@ -24,11 +24,13 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsI;
 
 import java.awt.Color;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.Test;
@@ -41,23 +43,26 @@ public class ChimeraCommandsTest
   {
 
     Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
-    String[] commands = new ChimeraCommands().colourBySequence(map);
-    assertEquals(commands.length, 1);
-    assertEquals(
-            commands[0],
-            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:3-5.A,8.A; color #ff0000 #0:3-9.A");
+    List<StructureCommandI> commands = new ChimeraCommands()
+            .colourBySequence(map);
+    assertEquals(commands.size(), 3);
+    assertEquals(commands.get(0).getCommand(),
+            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B");
+    assertEquals(commands.get(1).getCommand(),
+            "color #ffff00 #1:3-5.A,8.A");
+    assertEquals(commands.get(2).getCommand(), "color #ff0000 #0:3-9.A");
   }
 
   @Test(groups = { "Functional" })
@@ -73,60 +78,62 @@ public class ChimeraCommandsTest
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A");
   
     ChimeraCommands commandGenerator = new ChimeraCommands();
-    String[] commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(1, commands.length);
+    List<StructureCommandI> commands = commandGenerator
+            .setAttributes(featuresMap);
+    assertEquals(1, commands.size());
 
     /*
      * feature name gets a jv_ namespace prefix
      * feature value is quoted in case it contains spaces
      */
-    assertEquals(commands[0], "setattr res jv_chain 'X' #0:8-20.A");
+    assertEquals(commands.get(0).getCommand(),
+            "setattr res jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(1, commands.length);
-    assertEquals(commands[0], "setattr res jv_chain 'X' #0:3-25.A");
+    assertEquals(1, commands.size());
+    assertEquals(commands.get(0).getCommand(),
+            "setattr res jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(1, commands.length);
+    assertEquals(1, commands.size());
     String expected1 = "setattr res jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A";
-    assertEquals(commands[0],
-            expected1);
+    assertEquals(commands.get(0).getCommand(), expected1);
 
     // same feature, different value
-    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(2, commands.length);
+    assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
     // so test for the expected command in either order
-    assertTrue(
-            commands[0].equals(expected1) || commands[1].equals(expected1));
+    String cmd1 = commands.get(0).getCommand();
+    String cmd2 = commands.get(1).getCommand();
+    assertTrue(cmd1.equals(expected1) || cmd2.equals(expected1));
     String expected2 = "setattr res jv_chain 'Y' #0:40-50.A";
-    assertTrue(
-            commands[0].equals(expected2) || commands[1].equals(expected2));
+    assertTrue(cmd1.equals(expected2) || cmd2.equals(expected2));
 
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
     ChimeraCommands.addAtomSpecRange(featureValues,
-            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", "0", 7, 15,
             "A");
     // feature names are sanitised to change non-alphanumeric to underscore
     // feature values are sanitised to encode single quote characters
     commands = commandGenerator.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
     String expected3 = "setattr res jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' #0:7-15.A";
-    assertTrue(
-            commands[0].equals(expected3) || commands[1].equals(expected3));
+    assertTrue(commands.get(0).getCommand().equals(expected3));
   }
 
   /**
@@ -155,31 +162,30 @@ public class ChimeraCommandsTest
     StructureCommandsI testee = new ChimeraCommands();
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, false), "");
-    model.addRange(1, 2, 4, "A");
+    model.addRange("1", 2, 4, "A");
     assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A");
-    model.addRange(1, 8, 8, "A");
+    model.addRange("1", 8, 8, "A");
     assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A");
-    model.addRange(1, 5, 7, "B");
+    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");
+    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");
+    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");
+    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");
+    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");
+    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
+    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
+    model.addRange("5", 25, 35, " ");
     assertEquals(testee.getAtomSpec(model, false),
             "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
 
@@ -190,20 +196,21 @@ public class ChimeraCommandsTest
   {
     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");
+    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);
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> 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",
+            "match %s %s; ribbon %s|%s; focus",
             refSpec, toAlignSpec, refSpec, toAlignSpec);
-    assertEquals(command, expected);
+    assertEquals(command.get(0).getCommand(), expected);
   }
 
   @Test(groups = "Functional")
@@ -212,34 +219,34 @@ public class ChimeraCommandsTest
     StructureCommandsI testee = new ChimeraCommands();
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, true), "");
-    model.addRange(1, 2, 4, "A");
+    model.addRange("1", 2, 4, "A");
     assertEquals(testee.getAtomSpec(model, true),
             "#1:2-4.A@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(1, 8, 8, "A");
+    model.addRange("1", 8, 8, "A");
     assertEquals(testee.getAtomSpec(model, true),
             "#1:2-4.A,8.A@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(1, 5, 7, "B");
+    model.addRange("1", 5, 7, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#1:2-4.A,8.A,5-7.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(1, 3, 5, "A");
+    model.addRange("1", 3, 5, "A");
     assertEquals(testee.getAtomSpec(model, true),
             "#1:2-5.A,8.A,5-7.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(0, 1, 4, "B");
+    model.addRange("0", 1, 4, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(0, 5, 9, "C");
+    model.addRange("0", 5, 9, "C");
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,5-9.C@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(1, 8, 10, "B");
+    model.addRange("1", 8, 10, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,5-9.C@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(1, 8, 9, "B");
+    model.addRange("1", 8, 9, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,5-9.C@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(0, 3, 10, "C"); // subsumes 5-9
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,3-10.C@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA|P&~@.B-Z&~@.2-9");
-    model.addRange(5, 25, 35, " "); // empty chain code
+    model.addRange("5", 25, 35, " "); // empty chain code
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,3-10.C@CA|P&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA|P&~@.B-Z&~@.2-9|#5:25-35.@CA|P&~@.B-Z&~@.2-9");
   
@@ -263,28 +270,33 @@ public class ChimeraCommandsTest
   public void testShowBackbone()
   {
     ChimeraCommands testee = new ChimeraCommands();
-    assertEquals(testee.showBackbone(), "~display all;chain @CA|P");
+    List<StructureCommandI> cmds = testee.showBackbone();
+    assertEquals(cmds.size(), 1);
+    assertEquals(cmds.get(0).getCommand(), "~display all;chain @CA|P");
   }
 
   @Test(groups = "Functional")
   public void testOpenCommandFile()
   {
     ChimeraCommands testee = new ChimeraCommands();
-    assertEquals(testee.openCommandFile("nowhere"), "open cmd:nowhere");
+    assertEquals(testee.openCommandFile("nowhere").getCommand(),
+            "open cmd:nowhere");
   }
 
   @Test(groups = "Functional")
   public void testSaveSession()
   {
     ChimeraCommands testee = new ChimeraCommands();
-    assertEquals(testee.saveSession("somewhere"), "save somewhere");
+    assertEquals(testee.saveSession("somewhere").getCommand(),
+            "save somewhere");
   }
 
   @Test(groups = "Functional")
   public void testGetColourCommand()
   {
     ChimeraCommands testee = new ChimeraCommands();
-    assertEquals(testee.getColourCommand("something", Color.MAGENTA),
+    assertEquals(testee.getColourCommand("something", Color.MAGENTA)
+            .getCommand(),
             "color #ff00ff something");
   }
 
@@ -293,10 +305,10 @@ public class ChimeraCommandsTest
   {
     ChimeraCommands testee = new ChimeraCommands();
     AtomSpecModel model = new AtomSpecModel();
-    model.addRange(1, 89, 92, "A");
-    model.addRange(2, 12, 20, "B");
-    model.addRange(2, 8, 9, "B");
-    assertEquals(testee.setAttribute("phi", "27.3", model),
+    model.addRange("1", 89, 92, "A");
+    model.addRange("2", 12, 20, "B");
+    model.addRange("2", 8, 9, "B");
+    assertEquals(testee.setAttribute("phi", "27.3", model).getCommand(),
             "setattr res phi '27.3' #1:89-92.A|#2:8-9.B,12-20.B");
   }
 }
index ddda2c0..3bd64d7 100644 (file)
@@ -24,11 +24,13 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureCommandsI;
 
 import java.awt.Color;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.Test;
@@ -38,46 +40,51 @@ public class ChimeraXCommandsTest
   @Test(groups = { "Functional" })
   public void testColourByCharge()
   {
-    String cmd = new ChimeraXCommands().colourByCharge();
-    assertEquals(cmd,
+    List<StructureCommandI> cmd = new ChimeraXCommands().colourByCharge();
+    assertEquals(cmd.size(), 1);
+    assertEquals(cmd.get(0).getCommand(),
             "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow");
   }
 
   @Test(groups = { "Functional" })
   public void testColourByChain()
   {
-    String cmd = new ChimeraXCommands().colourByChain();
-    assertEquals(cmd, "rainbow chain");
+    StructureCommandI cmd = new ChimeraXCommands().colourByChain();
+    assertEquals(cmd.getCommand(), "rainbow chain");
   }
 
   @Test(groups = { "Functional" })
   public void testFocusView()
   {
-    String cmd = new ChimeraXCommands().focusView();
-    assertEquals(cmd, "view");
+    StructureCommandI cmd = new ChimeraXCommands().focusView();
+    assertEquals(cmd.getCommand(), "view");
   }
 
   @Test(groups = { "Functional" })
   public void testColourBySequence()
   {
     Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 2, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 7, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 9, 23, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 2, 1, 1, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 2, 4, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 2, 8, 8, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 2, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 1, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 1, 6, 9, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A");
 
-    // Colours should appear in the Chimera command in the order in which
-    // they were added; within colour, by model, by chain, ranges in start order
-    String[] commands = new ChimeraXCommands().colourBySequence(map);
-    assertEquals(commands.length, 1);
-    assertEquals(
-            commands[0],
-            "color #1/A:2-5,9-23/B:7|#2/A:1/B:4-7 #0000ff; color #2/A:3-5,8 #ffff00; color #1/A:3-9 #ff0000");
+    /*
+     * Colours should appear in the Chimera command in the order in which
+     * they were added; within colour, by model, by chain, ranges in start order
+     */
+    List<StructureCommandI> commands = new ChimeraXCommands()
+            .colourBySequence(map);
+    assertEquals(commands.size(), 3);
+    assertEquals(commands.get(0).getCommand(),
+            "color #1/A:2-5,9-23/B:7|#2/A:1/B:4-7 #0000ff");
+    assertEquals(commands.get(1).getCommand(), "color #2/A:3-5,8 #ffff00");
+    assertEquals(commands.get(2).getCommand(), "color #1/A:3-9 #ff0000");
   }
 
   @Test(groups = { "Functional" })
@@ -93,61 +100,64 @@ public class ChimeraXCommandsTest
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A");
   
     ChimeraXCommands commandGenerator = new ChimeraXCommands();
-    String[] commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(commands.length, 1);
+    List<StructureCommandI> commands = commandGenerator
+            .setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
 
     /*
      * feature name gets a jv_ namespace prefix
      * feature value is quoted in case it contains spaces
      */
-    assertEquals(commands[0],
+    assertEquals(commands.get(0).getCommand(),
             "setattr #0/A:8-20 res jv_chain 'X' create true");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(commands.length, 1);
-    assertEquals(commands[0],
+    assertEquals(commands.size(), 1);
+    assertEquals(commands.get(0).getCommand(),
             "setattr #0/A:3-25 res jv_chain 'X' create true");
 
     // same feature value and model, different chain
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(commands.length, 1);
+    assertEquals(commands.size(), 1);
     String expected1 = "setattr #0/A:3-25/B:21-25|#1/A:26-30 res jv_chain 'X' create true";
-    assertEquals(commands[0], expected1);
+    assertEquals(commands.get(0).getCommand(), expected1);
 
     // same feature, different value
-    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A");
     commands = commandGenerator.setAttributes(featuresMap);
-    assertEquals(2, commands.length);
+    assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
     // so test for the expected command in either order
+    String cmd1 = commands.get(0).getCommand();
+    String cmd2 = commands.get(1).getCommand();
     assertTrue(
-            commands[0].equals(expected1) || commands[1].equals(expected1));
+            cmd1.equals(expected1) || cmd2.equals(expected1));
     String expected2 = "setattr #0/A:40-50 res jv_chain 'Y' create true";
     assertTrue(
-            commands[0].equals(expected2) || commands[1].equals(expected2));
+            cmd1.equals(expected2) || cmd2.equals(expected2));
 
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
     ChimeraCommands.addAtomSpecRange(featureValues,
-            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", "0", 7, 15,
             "A");
     // feature names are sanitised to change non-alphanumeric to underscore
     // feature values are sanitised to encode single quote characters
     commands = commandGenerator.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
     String expected3 = "setattr #0/A:7-15 res jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' create true";
-    assertTrue(
-            commands[0].equals(expected3) || commands[1].equals(expected3));
+    assertTrue(commands.get(0).getCommand().equals(expected3));
   }
 
   @Test(groups = { "Functional" })
@@ -155,14 +165,17 @@ public class ChimeraXCommandsTest
   {
     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");
+    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);
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> command = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(command.size(), 1);
+    String cmd = command.get(0).getCommand();
     String refSpec = "#1/A:12-14/B:18,22-23";
     String toAlignSpec = "#2/B:15-17,20-21/C:22";
 
@@ -173,7 +186,7 @@ public class ChimeraXCommandsTest
     String expected = String.format(
             "align %s@CA|P toAtoms %s@CA|P; ribbon %s|%s; view",
             refSpec, toAlignSpec, refSpec, toAlignSpec);
-    assertEquals(command, expected);
+    assertEquals(cmd, expected);
   }
 
   @Test(groups = "Functional")
@@ -182,30 +195,30 @@ public class ChimeraXCommandsTest
     StructureCommandsI testee = new ChimeraXCommands();
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, false), "");
-    model.addRange(1, 2, 4, "A");
+    model.addRange("1", 2, 4, "A");
     assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4");
-    model.addRange(1, 8, 8, "A");
+    model.addRange("1", 8, 8, "A");
     assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8");
-    model.addRange(1, 5, 7, "B");
+    model.addRange("1", 5, 7, "B");
     assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8/B:5-7");
-    model.addRange(1, 3, 5, "A");
+    model.addRange("1", 3, 5, "A");
     assertEquals(testee.getAtomSpec(model, false), "#1/A:2-5,8/B:5-7");
-    model.addRange(0, 1, 4, "B");
+    model.addRange("0", 1, 4, "B");
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4|#1/A:2-5,8/B:5-7");
-    model.addRange(0, 5, 9, "C");
+    model.addRange("0", 5, 9, "C");
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-7");
-    model.addRange(1, 8, 10, "B");
+    model.addRange("1", 8, 10, "B");
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10");
-    model.addRange(1, 8, 9, "B");
+    model.addRange("1", 8, 9, "B");
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10");
-    model.addRange(0, 3, 10, "C"); // subsumes 5-9
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10");
-    model.addRange(5, 25, 35, " ");
+    model.addRange("5", 25, 35, " ");
     assertEquals(testee.getAtomSpec(model, false),
             "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10|#5/:25-35");
   }
@@ -216,30 +229,30 @@ public class ChimeraXCommandsTest
     StructureCommandsI testee = new ChimeraXCommands();
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, true), "");
-    model.addRange(1, 2, 4, "A");
+    model.addRange("1", 2, 4, "A");
     assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4@CA|P");
-    model.addRange(1, 8, 8, "A");
+    model.addRange("1", 8, 8, "A");
     assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8@CA|P");
-    model.addRange(1, 5, 7, "B");
+    model.addRange("1", 5, 7, "B");
     assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8/B:5-7@CA|P");
-    model.addRange(1, 3, 5, "A");
+    model.addRange("1", 3, 5, "A");
     assertEquals(testee.getAtomSpec(model, true), "#1/A:2-5,8/B:5-7@CA|P");
-    model.addRange(0, 1, 4, "B");
+    model.addRange("0", 1, 4, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4@CA|P|#1/A:2-5,8/B:5-7@CA|P");
-    model.addRange(0, 5, 9, "C");
+    model.addRange("0", 5, 9, "C");
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4/C:5-9@CA|P|#1/A:2-5,8/B:5-7@CA|P");
-    model.addRange(1, 8, 10, "B");
+    model.addRange("1", 8, 10, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4/C:5-9@CA|P|#1/A:2-5,8/B:5-10@CA|P");
-    model.addRange(1, 8, 9, "B");
+    model.addRange("1", 8, 9, "B");
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4/C:5-9@CA|P|#1/A:2-5,8/B:5-10@CA|P");
-    model.addRange(0, 3, 10, "C"); // subsumes 5-9
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4/C:3-10@CA|P|#1/A:2-5,8/B:5-10@CA|P");
-    model.addRange(5, 25, 35, " "); // empty chain code
+    model.addRange("5", 25, 35, " "); // empty chain code
     assertEquals(testee.getAtomSpec(model, true),
             "#0/B:1-4/C:3-10@CA|P|#1/A:2-5,8/B:5-10@CA|P|#5/:25-35@CA|P");
   }
@@ -262,28 +275,34 @@ public class ChimeraXCommandsTest
   public void testShowBackbone()
   {
     ChimeraCommands testee = new ChimeraXCommands();
-    assertEquals(testee.showBackbone(), "~display all;show @CA|P pbonds");
+    List<StructureCommandI> showBackbone = testee.showBackbone();
+    assertEquals(showBackbone.size(), 1);
+    assertEquals(showBackbone.get(0).getCommand(),
+            "~display all;show @CA|P pbonds");
   }
 
   @Test(groups = "Functional")
   public void testOpenCommandFile()
   {
     ChimeraCommands testee = new ChimeraXCommands();
-    assertEquals(testee.openCommandFile("nowhere"), "open nowhere");
+    assertEquals(testee.openCommandFile("nowhere").getCommand(),
+            "open nowhere");
   }
 
   @Test(groups = "Functional")
   public void testSaveSession()
   {
     ChimeraCommands testee = new ChimeraXCommands();
-    assertEquals(testee.saveSession("somewhere"), "save session somewhere");
+    assertEquals(testee.saveSession("somewhere").getCommand(),
+            "save session somewhere");
   }
 
   @Test(groups = "Functional")
   public void testGetColourCommand()
   {
     ChimeraCommands testee = new ChimeraXCommands();
-    assertEquals(testee.getColourCommand("something", Color.MAGENTA),
+    assertEquals(testee.getColourCommand("something", Color.MAGENTA)
+            .getCommand(),
             "color something #ff00ff");
   }
 
@@ -292,10 +311,10 @@ public class ChimeraXCommandsTest
   {
     ChimeraCommands testee = new ChimeraXCommands();
     AtomSpecModel model = new AtomSpecModel();
-    model.addRange(1, 89, 92, "A");
-    model.addRange(2, 12, 20, "B");
-    model.addRange(2, 8, 9, "B");
-    assertEquals(testee.setAttribute("phi", "27.3", model),
+    model.addRange("1", 89, 92, "A");
+    model.addRange("2", 12, 20, "B");
+    model.addRange("2", 8, 9, "B");
+    assertEquals(testee.setAttribute("phi", "27.3", model).getCommand(),
             "setattr #1/A:89-92|#2/B:8-9,12-20 res phi '27.3' create true");
   }
 }
index 93ed555..30c23af 100644 (file)
@@ -41,6 +41,7 @@ import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 import jalview.ws.sifts.SiftsClient;
@@ -290,7 +291,8 @@ public class JalviewChimeraView
     /*
      * ask Chimera for its residue attribute names
      */
-    List<String> reply = binding.executeCommand("list resattr", true);
+    List<String> reply = binding
+            .executeCommand(new StructureCommand("list resattr"), true);
     // prefixed and sanitised attribute names for Jalview features:
     assertTrue(reply.contains("resattr jv_domain"));
     assertTrue(reply.contains("resattr jv_metal_ion_binding_site"));
@@ -307,7 +309,8 @@ public class JalviewChimeraView
      * 91 and 96 on sequence --> residues 40 and 45 on chains A and B
      */
     reply = binding.executeCommand(
-            "list resi att jv_metal_ion_binding_site", true);
+            new StructureCommand("list resi att jv_metal_ion_binding_site"),
+            true);
     assertEquals(reply.size(), 4);
     assertTrue(reply
             .contains("residue id #0:40.A jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 40"));
@@ -322,7 +325,8 @@ public class JalviewChimeraView
      * check attributes with score values
      * sequence positions 62 and 65 --> residues 11 and 14 on chains A and B
      */
-    reply = binding.executeCommand("list resi att jv_kd", true);
+    reply = binding.executeCommand(
+            new StructureCommand("list resi att jv_kd"), true);
     assertEquals(reply.size(), 4);
     assertTrue(reply.contains("residue id #0:11.A jv_kd -2.1 index 11"));
     assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14"));
@@ -333,7 +337,8 @@ public class JalviewChimeraView
      * list residues with positive kd score 
      */
     reply = binding.executeCommand(
-            "list resi spec :*/jv_kd>0 attr jv_kd", true);
+            new StructureCommand("list resi spec :*/jv_kd>0 attr jv_kd"),
+            true);
     assertEquals(reply.size(), 2);
     assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14"));
     assertTrue(reply.contains("residue id #0:14.B jv_kd 3.6 index 14"));
index 8c6ead7..394679f 100644 (file)
@@ -15,13 +15,13 @@ public class AtomSpecModelTest
   {
     AtomSpecModel model = new AtomSpecModel();
     assertFalse(model.getModels().iterator().hasNext());
-    List<int[]> ranges = model.getRanges(1, "A");
+    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");
+    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);
@@ -31,12 +31,12 @@ public class AtomSpecModelTest
      * 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");
+    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);
index f901cbd..5e0e52f 100644 (file)
@@ -43,7 +43,7 @@ import jalview.io.FileFormats;
 import jalview.schemes.JalviewColourScheme;
 import jalview.structure.AtomSpec;
 import jalview.structure.AtomSpecModel;
-import jalview.structure.StructureCommandsI.SuperposeData;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 
@@ -172,16 +172,16 @@ public class AAStructureBindingModelTest
       }
 
       @Override
-      protected List<String> executeCommand(String command,
+      protected List<String> executeCommand(StructureCommandI command,
               boolean getReply)
       {
         return null;
       }
 
       @Override
-      protected int getModelNoForFile(String chainId)
+      protected String getModelIdForFile(String chainId)
       {
-        return 0;
+        return "";
       }
 
       @Override
@@ -280,16 +280,16 @@ public class AAStructureBindingModelTest
       }
 
       @Override
-      protected List<String> executeCommand(String command,
+      protected List<String> executeCommand(StructureCommandI command,
               boolean getReply)
       {
         return null;
       }
 
       @Override
-      protected int getModelNoForFile(String chainId)
+      protected String getModelIdForFile(String chainId)
       {
-        return 0;
+        return "";
       }
 
       @Override
@@ -312,10 +312,10 @@ public class AAStructureBindingModelTest
     /*
      * create a data bean to hold data per structure file
      */
-    SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length];
+    AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[testee.getStructureFiles().length];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = new SuperposeData(al.getWidth(), 0);
+      structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0");
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for
@@ -360,10 +360,10 @@ public class AAStructureBindingModelTest
   @Test(groups = { "Functional" })
   public void testFindSuperposableResidues_hiddenColumn()
   {
-    SuperposeData[] structs = new SuperposeData[al.getHeight()];
+    AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[al.getHeight()];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = new SuperposeData(al.getWidth(), 0);
+      structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0");
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for