JAL-3518 separation of ChimeraXManager, pull up of closeViewer etc
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 23 Jun 2020 15:55:42 +0000 (16:55 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 23 Jun 2020 15:55:42 +0000 (16:55 +0100)
16 files changed:
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/jalview/ext/pymol/PymolCommands.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
src/jalview/ext/rbvi/chimera/ChimeraXManager.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/JalviewChimeraXBindingModel.java
src/jalview/structure/AtomSpec.java
src/jalview/structure/StructureCommandsBase.java
src/jalview/structure/StructureCommandsI.java
src/jalview/structures/models/AAStructureBindingModel.java
test/jalview/ext/pymol/PymolCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java
test/jalview/structure/AtomSpecTest.java

index 31f5dc8..ea48cb6 100644 (file)
@@ -333,10 +333,7 @@ public class ChimeraManager
 
   public void stopListening()
   {
-    // TODO send this command when viewer connection is closed in Jalview
-    String command = isChimeraX
-            ? "info notify stop models jalview; info notify stop selection jalview"
-            : "listen stop models ; listen stop selection ";
+    String command = "listen stop models ; listen stop selection ";
     sendChimeraCommand(command, false);
   }
 
@@ -350,19 +347,13 @@ public class ChimeraManager
     /*
      * listen for model changes
      */
-    String command = isChimeraX
-            ? ("info notify start models prefix ModelChanged jalview url "
-                    + uri)
-            : ("listen start models url " + uri);
+    String command = "listen start models url " + uri;
     sendChimeraCommand(command, false);
 
     /*
      * listen for selection changes
      */
-    command = isChimeraX
-            ? ("info notify start selection jalview prefix SelectionChanged url "
-                    + uri)
-            : ("listen start select prefix SelectionChanged url " + uri);
+    command = "listen start select prefix SelectionChanged url " + uri;
     sendChimeraCommand(command, false);
   }
 
@@ -438,10 +429,7 @@ public class ChimeraManager
   {
     List<String> selectedResidues = new ArrayList<>();
 
-    // in fact 'listinfo' (undocumented) works in ChimeraX
-    String command = (isChimeraX
-            ? "info"
-            : "list") + " selection level residue";
+    String command = "list selection level residue";
     List<String> chimeraReply = sendChimeraCommand(command, true);
     if (chimeraReply != null)
     {
@@ -498,7 +486,7 @@ public class ChimeraManager
   {
     List<ChimeraModel> modelList = new ArrayList<>();
     String command = "list models type "
-            + (isChimeraX ? "AtomicStructure" : "molecule");
+            + (isChimeraX() ? "AtomicStructure" : "molecule");
     List<String> list = sendChimeraCommand(command, true);
     if (list != null)
     {
@@ -586,7 +574,6 @@ public class ChimeraManager
       {
         // ensure symbolic links are resolved
         chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
-        isChimeraX = chimeraPath.toLowerCase().contains("chimerax");
         File path = new File(chimeraPath);
         // uncomment the next line to simulate Chimera not installed
         // path = new File(chimeraPath + "x");
@@ -599,16 +586,7 @@ public class ChimeraManager
         args.add(chimeraPath);
         // shows Chimera output window but suppresses REST responses:
         // args.add("--debug");
-        if (isChimeraX())
-        {
-          args.add("--cmd");
-          args.add("remote rest start");
-        }
-        else
-        {
-          args.add("--start");
-          args.add("RESTServer");
-        }
+        addLaunchArguments(args);
         ProcessBuilder pb = new ProcessBuilder(args);
         chimera = pb.start();
         error = "";
@@ -640,6 +618,18 @@ public class ChimeraManager
   }
 
   /**
+   * Adds command-line arguments to start the REST server
+   * <p>
+   * Method extracted for Jalview to allow override in ChimeraXManager
+   * @param args
+   */
+  protected void addLaunchArguments(List<String> args)
+  {
+    args.add("--start");
+    args.add("RESTServer");
+  }
+
+  /**
    * Read and return the port number returned in the reply to --start RESTServer
    */
   private int getPortNumber()
@@ -751,7 +741,7 @@ public class ChimeraManager
   public List<String> getAttrList()
   {
     List<String> attributes = new ArrayList<>();
-    String command = (isChimeraX ? "info " : "list ") + "resattr";
+    String command = (isChimeraX() ? "info " : "list ") + "resattr";
     final List<String> reply = sendChimeraCommand(command, true);
     if (reply != null)
     {
@@ -811,8 +801,6 @@ public class ChimeraManager
 
   private volatile boolean busy = false;
 
-  private boolean isChimeraX;
-
   /**
    * Send a command to Chimera.
    * 
@@ -873,7 +861,7 @@ public class ChimeraManager
   {
     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
     List<NameValuePair> commands = new ArrayList<>(1);
-    String method = isChimeraX() ? "GET" : "POST";
+    String method = getHttpRequestMethod();
     if ("GET".equals(method))
     {
       command = command.replace(" ", "+").replace("#", "%23")
@@ -914,6 +902,15 @@ public class ChimeraManager
   }
 
   /**
+   * Returns "POST" as the HTTP request method to use for REST service calls to Chimera
+   * @return
+   */
+  protected String getHttpRequestMethod()
+  {
+    return "POST";
+  }
+
+  /**
    * Send a command to stdin of Chimera process, and optionally read any
    * responses.
    * 
@@ -964,11 +961,6 @@ public class ChimeraManager
 
   public boolean isChimeraX()
   {
-    return isChimeraX;
-  }
-
-  public void setChimeraX(boolean b)
-  {
-    isChimeraX = b;
+    return false;
   }
 }
index 7e5ba2d..be01533 100644 (file)
@@ -22,6 +22,8 @@ import jalview.structure.StructureCommandsBase;
  */
 public class PymolCommands extends StructureCommandsBase
 {
+  private static final StructureCommand CLOSE_PYMOL = new StructureCommand("quit");
+
   private static final StructureCommand COLOUR_BY_CHAIN = new StructureCommand("spectrum", "chain");
 
   private static final List<StructureCommandI> COLOR_BY_CHARGE = new ArrayList<>();
@@ -321,7 +323,7 @@ public class PymolCommands extends StructureCommandsBase
   public StructureCommandI closeViewer()
   {
     // https://pymolwiki.org/index.php/Quit
-    return new StructureCommand("quit");
+    return CLOSE_PYMOL;
   }
 
 }
index dd7b446..857dbcc 100644 (file)
@@ -40,6 +40,14 @@ import jalview.util.ColorUtils;
  */
 public class ChimeraCommands extends StructureCommandsBase
 {
+  private static final StructureCommand CLOSE_CHIMERA = new StructureCommand("stop really");
+
+  private static final StructureCommand STOP_NOTIFY_SELECTION = new StructureCommand("listen stop selection");
+
+  private static final StructureCommand STOP_NOTIFY_MODELS = new StructureCommand("listen stop models");
+
+  private static final StructureCommand GET_SELECTION = new StructureCommand("list selection level residue");
+
   private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
           "~display all;~ribbon;chain @CA|P");
 
@@ -410,7 +418,33 @@ public class ChimeraCommands extends StructureCommandsBase
   public StructureCommandI closeViewer()
   {
     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/stop.html
-    return new StructureCommand("stop really");
+    return CLOSE_CHIMERA;
+  }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(new StructureCommand("listen start models url " + uri));
+    cmds.add(new StructureCommand("listen start select prefix SelectionChanged url " + uri));
+    return cmds;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(STOP_NOTIFY_MODELS);
+    cmds.add(STOP_NOTIFY_SELECTION);
+    return cmds;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return GET_SELECTION;
   }
 
 }
index 889b1bc..f1a8b5f 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.ext.rbvi.chimera;
 
 import java.awt.Color;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -34,6 +35,14 @@ import jalview.util.ColorUtils;
  */
 public class ChimeraXCommands extends ChimeraCommands
 {
+  private static final StructureCommand CLOSE_CHIMERAX = new StructureCommand("exit");
+
+  private static final StructureCommand STOP_NOTIFY_SELECTION = new StructureCommand("info notify stop selection jalview");
+
+  private static final StructureCommand STOP_NOTIFY_MODELS = new StructureCommand("info notify stop models jalview");
+
+  private static final StructureCommand GET_SELECTION = new StructureCommand("info selection level residue");
+
   private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
           "~display all;~ribbon;show @CA|P atoms");
 
@@ -228,6 +237,32 @@ public class ChimeraXCommands extends ChimeraCommands
   public StructureCommandI closeViewer()
   {
     // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/exit.html
-    return new StructureCommand("exit");
+    return CLOSE_CHIMERAX;
+  }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/info.html#notify
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(new StructureCommand("info notify start models prefix ModelChanged jalview url " + uri));
+    cmds.add(new StructureCommand("info notify start selection jalview prefix SelectionChanged url " + uri));
+    return cmds;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/info.html#notify
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(STOP_NOTIFY_MODELS);
+    cmds.add(STOP_NOTIFY_SELECTION);
+    return cmds;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return GET_SELECTION;
   }
 }
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXManager.java b/src/jalview/ext/rbvi/chimera/ChimeraXManager.java
new file mode 100644 (file)
index 0000000..9d89ac7
--- /dev/null
@@ -0,0 +1,50 @@
+package jalview.ext.rbvi.chimera;
+
+import java.util.List;
+
+import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
+
+/**
+ * A class to help Jalview start, stop and send commands to ChimeraX.
+ * <p>
+ * Much of the functionality is common with Chimera, so for convenience we
+ * extend ChimeraManager, however note this class is <em>not</em> based on the
+ * Cytoscape class at
+ * {@code https://github.com/RBVI/structureVizX/blob/master/src/main/java/edu/ucsf/rbvi/structureVizX/internal/model/ChimeraManager.java}.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class ChimeraXManager extends ChimeraManager
+{
+
+  public ChimeraXManager(StructureManager structureManager)
+  {
+    super(structureManager);
+  }
+
+  public boolean isChimeraX()
+  {
+    return true;
+  }
+
+  /**
+   * Returns "POST" as the HTTP request method to use for REST service calls to ChimeraX
+   * @return
+   */
+  protected String getHttpRequestMethod()
+  {
+    return "GET";
+  }
+
+  /**
+   * Adds command-line arguments to start the REST server
+   */
+  protected void addLaunchArguments(List<String> args)
+  {
+    args.add("--cmd");
+    args.add("remote rest start");
+  }
+
+}
index bc4eef4..b20f135 100644 (file)
@@ -78,6 +78,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   String lastHighlightCommand;
 
   /**
+   * Returns a model of the structure positions described by the Chimera format atomspec
+   * @param atomSpec
+   * @return
+   */
+  protected  AtomSpec parseAtomSpec(String atomSpec)
+  {
+    return AtomSpec.fromChimeraAtomspec(atomSpec);
+  }
+
+  /**
    * Open a PDB structure file in Chimera and set up mappings from Jalview.
    * 
    * We check if the PDB model id is already loaded in Chimera, if so don't reopen
@@ -171,9 +181,9 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
           DataSourceType protocol)
   {
     super(ssm, pdbentry, sequenceIs, protocol);
-    chimeraManager = new ChimeraManager(new StructureManager(true));
-    chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType()));
-    setStructureCommands(new ChimeraCommands());
+    boolean chimeraX = ViewerType.CHIMERAX.equals(getViewerType());
+    chimeraManager = chimeraX ? new ChimeraXManager(new StructureManager(true)) : new ChimeraManager(new StructureManager(true));
+    setStructureCommands(chimeraX ? new ChimeraXCommands() : new ChimeraCommands());
   }
 
   @Override
@@ -191,7 +201,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     try
     {
       chimeraListener = new ChimeraListener(this);
-      chimeraManager.startListening(chimeraListener.getUri());
+      startListening(chimeraListener.getUri());
     } catch (BindException e)
     {
       System.err.println(
@@ -212,6 +222,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       chimeraListener.shutdown();
       chimeraListener = null;
     }
+    
+    /*
+     * the following call should not be needed but is temporarily included,
+     * to avoid a stack trace error in Chimera after "stop really" is sent
+     */
+    if (closeChimera)
+    {
+      chimeraManager.getChimeraProcess().destroy();
+    }
+
     chimeraManager.clearOnChimeraExit();
     chimeraManager = null;
   }
@@ -420,14 +440,35 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     /*
      * Ask Chimera for its current selection
      */
-    List<String> selection = chimeraManager.getSelectedResidueSpecs();
+    StructureCommandI command = getCommandGenerator().getSelectedResidues();
+    List<String> chimeraReply = executeCommand(command, true);
+    List<String> selectedResidues = new ArrayList<>();
+    if (chimeraReply != null)
+    {
+      /*
+       * expect 0, 1 or more lines of the format either
+       * Chimera:
+       * residue id #0:43.A type GLY
+       * ChimeraX:
+       * residue id /A:89 name THR index 88
+       * We are only interested in the atomspec (third token of the reply)
+       */
+      for (String inputLine : chimeraReply)
+      {
+        String[] inputLineParts = inputLine.split("\\s+");
+        if (inputLineParts.length >= 5)
+        {
+          selectedResidues.add(inputLineParts[2]);
+        }
+      }
+    }
 
     /*
      * Parse model number, residue and chain for each selected position,
      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
      */
     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
-            selection);
+            selectedResidues);
 
     /*
      * Broadcast the selection (which may be empty, if the user just cleared all
@@ -437,7 +478,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
+   * Converts a list of Chimera(X) atomspecs to a list of AtomSpec representing the
    * corresponding residues (if any) in Jalview
    * 
    * @param structureSelection
@@ -446,13 +487,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   protected List<AtomSpec> convertStructureResiduesToAlignment(
           List<String> structureSelection)
   {
-    boolean chimeraX = chimeraManager.isChimeraX();
     List<AtomSpec> atomSpecs = new ArrayList<>();
     for (String atomSpec : structureSelection)
     {
       try
       {
-        AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
+        AtomSpec spec = parseAtomSpec(atomSpec);
         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
         spec.setPdbFile(pdbfilename);
         atomSpecs.add(spec);
@@ -645,7 +685,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   {
     boolean featureAdded = false;
     String featureGroup = getViewerFeatureGroup();
-    boolean chimeraX = chimeraManager.isChimeraX();
 
     for (String residue : residues)
     {
@@ -669,7 +708,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 
       try
       {
-        spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
+        spec = parseAtomSpec(atomSpec);
       } catch (IllegalArgumentException e)
       {
         System.err.println("Problem parsing atomspec " + atomSpec);
index 49655a4..cef92a8 100644 (file)
  */
 package jalview.gui;
 
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
 import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.io.DataSourceType;
+import jalview.structure.AtomSpec;
 import jalview.structure.StructureSelectionManager;
 
-import javax.swing.JComponent;
-import javax.swing.SwingUtilities;
-
 public class JalviewChimeraBindingModel extends JalviewChimeraBinding
 {
   public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame,
@@ -51,7 +52,7 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
   @Override
   public void refreshGUI()
   {
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
+    SwingUtilities.invokeLater(new Runnable()
     {
       @Override
       public void run()
index c685f0f..5b7a928 100644 (file)
@@ -10,12 +10,12 @@ import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.ChimeraXCommands;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.structure.AtomSpec;
 import jalview.structure.StructureCommand;
 import jalview.structure.StructureSelectionManager;
 
 public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
 {
-
   public static final String CHIMERAX_SESSION_EXTENSION = ".cxs";
 
   public JalviewChimeraXBindingModel(ChimeraViewFrame chimeraViewFrame,
@@ -88,4 +88,14 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
     return String.valueOf(pdbfnum + 1);
   }
 
+  /**
+   * Returns a model of the structure positions described by the ChimeraX format atomspec
+   * @param atomSpec
+   * @return
+   */
+  protected AtomSpec parseAtomSpec(String atomSpec)
+  {
+    return AtomSpec.fromChimeraXAtomspec(atomSpec);
+  }
+
 }
index 8b8161f..f404d35 100644 (file)
@@ -46,21 +46,17 @@ public class AtomSpec
    * <pre>
    * Chimera format: 
    *    #1.2:12-20.A     model 1, submodel 2, chain A, atoms 12-20
-   * ChimeraX format:
-   *    #1.2/A:12-20
    * </pre>
    * 
    * @param spec
-   * @param chimeraX
    * @return
    * @throw IllegalArgumentException if the spec cannot be parsed, or represents
    *        more than one residue
    * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
-   * @see http://rbvi.ucsf.edu/chimerax/docs/user/commands/atomspec.html
    */
-  public static AtomSpec fromChimeraAtomspec(String spec, boolean chimeraX)
+  public static AtomSpec fromChimeraAtomspec(String spec)
   {
-    int modelSeparatorPos = spec.indexOf(chimeraX ? "/" : ":");
+    int modelSeparatorPos = spec.indexOf(":");
     if (modelSeparatorPos == -1)
     {
       throw new IllegalArgumentException(spec);
@@ -92,9 +88,8 @@ public class AtomSpec
      * ChimeraX: chain:atoms
      */
     String atomsAndChain = spec.substring(modelSeparatorPos + 1);
-    String[] tokens = atomsAndChain.split(chimeraX ? "\\:" : "\\.");
-    String atoms = tokens.length == 1 ? atomsAndChain
-            : (chimeraX ? tokens[1] : tokens[0]);
+    String[] tokens = atomsAndChain.split("\\.");
+    String atoms = tokens.length == 1 ? atomsAndChain : (tokens[0]);
     int resNum = 0;
     try
     {
@@ -105,8 +100,7 @@ public class AtomSpec
       throw new IllegalArgumentException(spec);
     }
 
-    String chainId = tokens.length == 1 ? ""
-            : (chimeraX ? tokens[0] : tokens[1]);
+    String chainId = tokens.length == 1 ? "" : (tokens[1]);
 
     return new AtomSpec(modelId, chainId, resNum, 0);
   }
@@ -179,4 +173,70 @@ public class AtomSpec
     return "pdbFile: " + pdbFile + ", chain: " + chain + ", res: "
             + pdbResNum + ", atom: " + atomIndex;
   }
+
+  /**
+   * Parses a ChimeraX atomspec to construct an AtomSpec model (with
+   * null pdb file name)
+   * 
+   * <pre>
+   * ChimeraX format:
+   *    #1.2/A:12-20     model 1, submodel 2, chain A, atoms 12-20
+   * </pre>
+   * 
+   * @param spec
+   * @return
+   * @throw IllegalArgumentException if the spec cannot be parsed, or represents
+   *        more than one residue
+   * @see http://rbvi.ucsf.edu/chimerax/docs/user/commands/atomspec.html
+   */
+  public static AtomSpec fromChimeraXAtomspec(String spec)
+  {
+    int modelSeparatorPos = spec.indexOf("/");
+    if (modelSeparatorPos == -1)
+    {
+      throw new IllegalArgumentException(spec);
+    }
+
+    int hashPos = spec.indexOf("#");
+    if (hashPos == -1 && modelSeparatorPos != 0)
+    {
+      // # is missing but something precedes : - reject
+      throw new IllegalArgumentException(spec);
+    }
+
+    String modelSubmodel = spec.substring(hashPos + 1, modelSeparatorPos);
+    int modelId = 0;
+    try
+    {
+      int subModelPos = modelSubmodel.indexOf(".");
+      modelId = Integer.valueOf(
+              subModelPos > 0 ? modelSubmodel.substring(0, subModelPos)
+                      : modelSubmodel);
+    } catch (NumberFormatException e)
+    {
+      // ignore, default to model 0
+    }
+
+    /*
+     * now process what follows the model, either
+     * Chimera:  atoms.chain
+     * ChimeraX: chain:atoms
+     */
+    String atomsAndChain = spec.substring(modelSeparatorPos + 1);
+    String[] tokens = atomsAndChain.split("\\:");
+    String atoms = tokens.length == 1 ? atomsAndChain : (tokens[1]);
+    int resNum = 0;
+    try
+    {
+      resNum = Integer.parseInt(atoms);
+    } catch (NumberFormatException e)
+    {
+      // could be a range e.g. #1:4-7.B
+      throw new IllegalArgumentException(spec);
+    }
+
+    String chainId = tokens.length == 1 ? "" : (tokens[0]);
+
+    return new AtomSpec(modelId, chainId, resNum, 0);
+  }
 }
index 3c29fd4..774327c 100644 (file)
@@ -223,4 +223,22 @@ public abstract class StructureCommandsBase implements StructureCommandsI
     // default does nothing, override where this is implemented
     return null;
   }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    return null;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    return null;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return null;
+  }
 }
index 3a47f83..c224187 100644 (file)
@@ -169,7 +169,35 @@ public interface StructureCommandsI
 
   /**
    * Returns a command to ask the viewer to close down
+   * 
    * @return
    */
   StructureCommandI closeViewer();
+
+  /**
+   * Returns one or more commands to ask the viewer to notify model or selection
+   * changes to the given uri. Returns null if this is not supported by the
+   * structure viewer.
+   * 
+   * @param uri
+   * @return
+   */
+  List<StructureCommandI> startNotifications(String uri);
+
+  /**
+   * Returns one or more commands to ask the viewer to stop notifying model or
+   * selection changes. Returns null if this is not supported by the structure
+   * viewer.
+   * 
+   * @return
+   */
+  List<StructureCommandI> stopNotifications();
+
+  /**
+   * Returns a command to ask the viewer for its current residue selection, or
+   * null if no such command is supported
+   * 
+   * @return
+   */
+  StructureCommandI getSelectedResidues();
 }
index 05cfd5a..f69a423 100644 (file)
@@ -1585,6 +1585,8 @@ public abstract class AAStructureBindingModel
       externalViewerMonitor = null;
     }
 
+    stopListening();
+    
     if (forceClose)
     {
       StructureCommandI cmd = getCommandGenerator().closeViewer();
@@ -1900,4 +1902,35 @@ public abstract class AAStructureBindingModel
     });
     externalViewerMonitor.start();
   }
+
+  /**
+   * If supported by the external structure viewer, sends it commands to notify
+   * model or selection changes to the specified URL (where Jalview has started
+   * a listener)
+   * 
+   * @param uri
+   */
+  protected void startListening(String uri)
+  {
+    List<StructureCommandI> commands = getCommandGenerator()
+            .startNotifications(uri);
+    if (commands != null)
+    {
+      executeCommands(commands, false, null);
+    }
+  }
+
+  /**
+   * If supported by the external structure viewer, sends it commands to stop
+   * notifying model or selection changes
+   */
+  protected void stopListening()
+  {
+    List<StructureCommandI> commands = getCommandGenerator()
+            .stopNotifications();
+    if (commands != null)
+    {
+      executeCommands(commands, false, null);
+    }
+  }
 }
index 9759ff3..f6bad92 100644 (file)
@@ -341,4 +341,10 @@ public class PymolCommandsTest
             "p.jv_side_chain_binding_='<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!'");
     assertEquals(commands.get(0), expected3);
   }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("quit"));
+  }
 }
index b040687..15d744b 100644 (file)
@@ -33,6 +33,7 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureCommandI;
 
 public class ChimeraCommandsTest
@@ -347,4 +348,35 @@ public class ChimeraCommandsTest
     assertEquals(testee.setAttribute("jv_kd", "27.3", model).getCommand(),
             "setattr res jv_kd '27.3' #1:89-92.A|#2:8-9.B,12-20.B");
   }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("stop really"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSelectedResidues()
+  {
+    assertEquals(testee.getSelectedResidues(),
+            new StructureCommand("list selection level residue"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStartNotifications()
+  {
+    List<StructureCommandI> cmds = testee.startNotifications("to here");
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("listen start models url to here"));
+    assertEquals(cmds.get(1), new StructureCommand("listen start select prefix SelectionChanged url to here"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStopNotifications()
+  {
+    List<StructureCommandI> cmds = testee.stopNotifications();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("listen stop models"));
+    assertEquals(cmds.get(1), new StructureCommand("listen stop selection"));
+  }
 }
index cb951c6..c1fa528 100644 (file)
@@ -33,6 +33,7 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureCommandI;
 
 public class ChimeraXCommandsTest
@@ -322,4 +323,35 @@ public class ChimeraXCommandsTest
             .getCommand(),
             "setattr #1/A:89-92|#2/B:8-9,12-20 res jv_kd '27.3' create true");
   }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("exit"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSelectedResidues()
+  {
+    assertEquals(testee.getSelectedResidues(),
+            new StructureCommand("info selection level residue"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStartNotifications()
+  {
+    List<StructureCommandI> cmds = testee.startNotifications("to here");
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("info notify start models prefix ModelChanged jalview url to here"));
+    assertEquals(cmds.get(1), new StructureCommand("info notify start selection jalview prefix SelectionChanged url to here"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStopNotifications()
+  {
+    List<StructureCommandI> cmds = testee.stopNotifications();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("info notify stop models jalview"));
+    assertEquals(cmds.get(1), new StructureCommand("info notify stop selection jalview"));
+  }
 }
index ff6e6cb..9881baf 100644 (file)
@@ -9,23 +9,23 @@ import org.testng.annotations.Test;
 public class AtomSpecTest
 {
   @Test
-  public void testFromChimeraAtomSpec_chimera()
+  public void testFromChimeraAtomSpec()
   {
-    AtomSpec as = AtomSpec.fromChimeraAtomspec("#1:12.B", false);
+    AtomSpec as = AtomSpec.fromChimeraAtomspec("#1:12.B");
     assertEquals(as.getModelNumber(), 1);
     assertEquals(as.getPdbResNum(), 12);
     assertEquals(as.getChain(), "B");
     assertNull(as.getPdbFile());
 
     // no model - default to zero
-    as = AtomSpec.fromChimeraAtomspec(":13.C", false);
+    as = AtomSpec.fromChimeraAtomspec(":13.C");
     assertEquals(as.getModelNumber(), 0);
     assertEquals(as.getPdbResNum(), 13);
     assertEquals(as.getChain(), "C");
     assertNull(as.getPdbFile());
 
     // model.submodel
-    as = AtomSpec.fromChimeraAtomspec("#3.2:15", false);
+    as = AtomSpec.fromChimeraAtomspec("#3.2:15");
     assertEquals(as.getModelNumber(), 3);
     assertEquals(as.getPdbResNum(), 15);
     assertEquals(as.getChain(), "");
@@ -34,7 +34,7 @@ public class AtomSpecTest
     String spec = "3:12.B";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, false);
+      as = AtomSpec.fromChimeraAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -44,7 +44,7 @@ public class AtomSpecTest
     spec = "#3:12-14.B";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, false);
+      as = AtomSpec.fromChimeraAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -54,7 +54,7 @@ public class AtomSpecTest
     spec = "";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, false);
+      as = AtomSpec.fromChimeraAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -64,7 +64,7 @@ public class AtomSpecTest
     spec = null;
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, false);
+      as = AtomSpec.fromChimeraAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (NullPointerException e)
     {
@@ -73,23 +73,23 @@ public class AtomSpecTest
   }
 
   @Test
-  public void testFromChimeraAtomSpec_chimeraX()
+  public void testFromChimeraXAtomSpec()
   {
-    AtomSpec as = AtomSpec.fromChimeraAtomspec("#1/B:12", true);
+    AtomSpec as = AtomSpec.fromChimeraXAtomspec("#1/B:12");
     assertEquals(as.getModelNumber(), 1);
     assertEquals(as.getPdbResNum(), 12);
     assertEquals(as.getChain(), "B");
     assertNull(as.getPdbFile());
   
     // no model - default to zero
-    as = AtomSpec.fromChimeraAtomspec("/C:13", true);
+    as = AtomSpec.fromChimeraXAtomspec("/C:13");
     assertEquals(as.getModelNumber(), 0);
     assertEquals(as.getPdbResNum(), 13);
     assertEquals(as.getChain(), "C");
     assertNull(as.getPdbFile());
   
     // model.submodel
-    as = AtomSpec.fromChimeraAtomspec("#3.2/:15", true);
+    as = AtomSpec.fromChimeraXAtomspec("#3.2/:15");
     assertEquals(as.getModelNumber(), 3);
     assertEquals(as.getPdbResNum(), 15);
     assertEquals(as.getChain(), "");
@@ -98,7 +98,7 @@ public class AtomSpecTest
     String spec = "3:12.B";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, true);
+      as = AtomSpec.fromChimeraXAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -108,7 +108,7 @@ public class AtomSpecTest
     spec = "#3:12-14.B";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, true);
+      as = AtomSpec.fromChimeraXAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -118,7 +118,7 @@ public class AtomSpecTest
     spec = "";
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, true);
+      as = AtomSpec.fromChimeraXAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (IllegalArgumentException e)
     {
@@ -128,7 +128,7 @@ public class AtomSpecTest
     spec = null;
     try
     {
-      as = AtomSpec.fromChimeraAtomspec(spec, true);
+      as = AtomSpec.fromChimeraXAtomspec(spec);
       fail("Expected exception for " + spec);
     } catch (NullPointerException e)
     {