Merge branch 'develop' into features/JAL-2295setChimeraAttributes
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 2 Dec 2016 15:17:37 +0000 (15:17 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 2 Dec 2016 15:17:37 +0000 (15:17 +0000)
Conflicts:
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java

1  2 
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/JalviewChimeraBindingModel.java

@@@ -380,6 -380,8 +380,8 @@@ public class ChimeraManage
        sendChimeraCommand("stop really", false);
        try
        {
+         // TODO is this too violent? could it force close the process
+         // before it has done an orderly shutdown?
          chimera.destroy();
        } catch (Exception ex)
        {
              "list selection level residue", true);
      if (chimeraReply != null)
      {
 +      /*
 +       * expect 0, 1 or more lines of the format
 +       * residue id #0:43.A type GLY
 +       * where we are only interested in the atomspec #0.43.A
 +       */
        for (String inputLine : chimeraReply)
        {
          String[] inputLineParts = inputLine.split("\\s+");
    {
      return busy;
    }
+   public Process getChimeraProcess()
+   {
+     return chimera;
+   }
  }
@@@ -23,13 -23,11 +23,14 @@@ package jalview.ext.rbvi.chimera
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeatureRenderer;
  import jalview.api.SequenceRenderer;
+ import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SearchResultMatchI;
 +import jalview.datamodel.SearchResults;
 +import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.httpserver.AbstractRequestHandler;
  import jalview.io.DataSourceType;
@@@ -42,13 -40,8 +43,13 @@@ import jalview.structures.models.AAStru
  import jalview.util.MessageManager;
  
  import java.awt.Color;
 +import java.io.File;
 +import java.io.FileOutputStream;
 +import java.io.IOException;
 +import java.io.PrintWriter;
  import java.net.BindException;
  import java.util.ArrayList;
 +import java.util.Collections;
  import java.util.Hashtable;
  import java.util.LinkedHashMap;
  import java.util.List;
@@@ -61,8 -54,6 +62,8 @@@ import ext.edu.ucsf.rbvi.strucviz2.Stru
  
  public abstract class JalviewChimeraBinding extends AAStructureBindingModel
  {
 +  public static final String CHIMERA_FEATURE_GROUP = "Chimera";
 +
    // Chimera clause to exclude alternate locations in atom selection
    private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
  
     */
    private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
  
 -  /*
 -   * the default or current model displayed if the model cannot be identified
 -   * from the selection message
 -   */
 -  private int frameNo = 0;
 -
 -  private String lastCommand;
 -
    String lastHighlightCommand;
  
    /*
     */
    private long loadNotifiesHandled = 0;
  
+   private Thread chimeraMonitor;
    /**
     * Open a PDB structure file in Chimera and set up mappings from Jalview.
     * 
            PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol)
    {
      super(ssm, pdbentry, sequenceIs, protocol);
-     viewer = new ChimeraManager(
-             new ext.edu.ucsf.rbvi.strucviz2.StructureManager(true));
+     viewer = new ChimeraManager(new StructureManager(true));
+   }
+   /**
+    * Starts a thread that waits for the Chimera process to finish, so that we
+    * can then close the associated resources. This avoids leaving orphaned
+    * Chimera viewer panels in Jalview if the user closes Chimera.
+    */
+   protected void startChimeraProcessMonitor()
+   {
+     final Process p = viewer.getChimeraProcess();
+     chimeraMonitor = new Thread(new Runnable()
+     {
+       @Override
+       public void run()
+       {
+         try
+         {
+           p.waitFor();
+           JalviewStructureDisplayI display = getViewer();
+           if (display != null)
+           {
+             display.closeViewer(false);
+           }
+         } catch (InterruptedException e)
+         {
+           // exit thread if Chimera Viewer is closed in Jalview
+         }
+       }
+     });
+     chimeraMonitor.start();
    }
  
    /**
        chimeraListener.shutdown();
        chimeraListener = null;
      }
 -    lastCommand = null;
      viewer = null;
  
+     if (chimeraMonitor != null)
+     {
+       chimeraMonitor.interrupt();
+     }
      releaseUIResources();
    }
  
  
    /**
     * Launch Chimera, unless an instance linked to this object is already
-    * running. Returns true if chimera is successfully launched, or already
+    * running. Returns true if Chimera is successfully launched, or already
     * running, else false.
     * 
     * @return
     */
    public boolean launchChimera()
    {
-     if (!viewer.isChimeraLaunched())
-     {
-       return viewer.launchChimera(StructureManager.getChimeraPaths());
-     }
      if (viewer.isChimeraLaunched())
      {
        return true;
      }
-     log("Failed to launch Chimera!");
-     return false;
+     boolean launched = viewer.launchChimera(StructureManager
+             .getChimeraPaths());
+     if (launched)
+     {
+       startChimeraProcessMonitor();
+     }
+     else
+     {
+       log("Failed to launch Chimera!");
+     }
+     return launched;
    }
  
    /**
    }
  
    /**
 -   * Send a command to Chimera, and optionally log any responses.
 +   * Send a command to Chimera, and optionally log and return any responses.
 +   * <p>
 +   * Does nothing, and returns null, if the command is the same as the last one
 +   * sent [why?].
     * 
     * @param command
 -   * @param logResponse
 +   * @param getResponse
     */
 -  public void sendChimeraCommand(final String command, boolean logResponse)
 +  public List<String> sendChimeraCommand(final String command,
 +          boolean getResponse)
    {
      if (viewer == null)
      {
        // ? thread running after viewer shut down
 -      return;
 +      return null;
      }
 +    List<String> reply = null;
      viewerCommandHistory(false);
 -    if (lastCommand == null || !lastCommand.equals(command))
 +    if (true /*lastCommand == null || !lastCommand.equals(command)*/)
      {
        // trim command or it may never find a match in the replyLog!!
        List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
 -              logResponse);
 -      if (logResponse && debug)
 +              getResponse);
 +      if (getResponse)
        {
 -        log("Response from command ('" + command + "') was:\n" + lastReply);
 +        reply = lastReply;
 +        if (debug)
 +        {
 +          log("Response from command ('" + command + "') was:\n"
 +                  + lastReply);
 +        }
        }
      }
      viewerCommandHistory(true);
 -    lastCommand = command;
 +
 +    return reply;
    }
  
    /**
      }
      AlignmentI alignment = alignmentv.getAlignment();
  
 -    for (jalview.structure.StructureMappingcommandSet cpdbbyseq : getColourBySequenceCommands(
 -            files, sr, fr, alignment))
 +    StructureMappingcommandSet colourBySequenceCommands = ChimeraCommands
 +            .getColourBySequenceCommand(getSsm(), files, getSequence(), sr,
 +                    fr, alignment);
 +    for (String command : colourBySequenceCommands.commands)
      {
 -      for (String command : cpdbbyseq.commands)
 -      {
 -        sendAsynchronousCommand(command, COLOURING_CHIMERA);
 -      }
 +      sendAsynchronousCommand(command, COLOURING_CHIMERA);
      }
    }
  
    /**
 -   * @param files
 -   * @param sr
 -   * @param fr
 -   * @param alignment
 -   * @return
 -   */
 -  protected StructureMappingcommandSet[] getColourBySequenceCommands(
 -          String[] files, SequenceRenderer sr, FeatureRenderer fr,
 -          AlignmentI alignment)
 -  {
 -    return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
 -            getSequence(), sr, fr, alignment);
 -  }
 -
 -  /**
     * @param command
     */
    protected void executeWhenReady(String command)
     */
    public abstract void refreshPdbEntries();
  
 -  private int getModelNum(String modelFileName)
 -  {
 -    String[] mfn = getPdbFile();
 -    if (mfn == null)
 -    {
 -      return -1;
 -    }
 -    for (int i = 0; i < mfn.length; i++)
 -    {
 -      if (mfn[i].equalsIgnoreCase(modelFileName))
 -      {
 -        return i;
 -      }
 -    }
 -    return -1;
 -  }
 -
 -  /**
 -   * map between index of model filename returned from getPdbFile and the first
 -   * index of models from this file in the viewer. Note - this is not trimmed -
 -   * use getPdbFile to get number of unique models.
 -   */
 -  private int _modelFileNameMap[];
 -
    // ////////////////////////////////
    // /StructureListener
    @Override
       * 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);
 +
 +    /*
 +     * Broadcast the selection (which may be empty, if the user just cleared all
 +     * selections)
 +     */
 +    getSsm().mouseOverStructure(atomSpecs);
 +  }
 +
 +  /**
 +   * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
 +   * corresponding residues (if any) in Jalview
 +   * 
 +   * @param structureSelection
 +   * @return
 +   */
 +  protected List<AtomSpec> convertStructureResiduesToAlignment(
 +          List<String> structureSelection)
 +  {
      List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
 -    for (String atomSpec : selection)
 +    for (String atomSpec : structureSelection)
      {
 -      int colonPos = atomSpec.indexOf(":");
 -      if (colonPos == -1)
 -      {
 -        continue; // malformed
 -      }
 -
 -      int hashPos = atomSpec.indexOf("#");
 -      String modelSubmodel = atomSpec.substring(hashPos + 1, colonPos);
 -      int dotPos = modelSubmodel.indexOf(".");
 -      int modelId = 0;
        try
        {
 -        modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel
 -                : modelSubmodel.substring(0, dotPos));
 -      } catch (NumberFormatException e)
 +        AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec);
 +        String pdbfilename = getPdbFileForModel(spec.getModelNumber());
 +        spec.setPdbFile(pdbfilename);
 +        atomSpecs.add(spec);
 +      } catch (IllegalArgumentException e)
        {
 -        // ignore, default to model 0
 +        System.err.println("Failed to parse atomspec: " + atomSpec);
        }
 +    }
 +    return atomSpecs;
 +  }
  
 -      String residueChain = atomSpec.substring(colonPos + 1);
 -      dotPos = residueChain.indexOf(".");
 -      int pdbResNum = Integer.parseInt(dotPos == -1 ? residueChain
 -              : residueChain.substring(0, dotPos));
 -
 -      String chainId = dotPos == -1 ? "" : residueChain
 -              .substring(dotPos + 1);
 -
 -      /*
 -       * Work out the pdbfilename from the model number
 -       */
 -      String pdbfilename = modelFileNames[frameNo];
 -      findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
 +  /**
 +   * @param modelId
 +   * @return
 +   */
 +  protected String getPdbFileForModel(int modelId)
 +  {
 +    /*
 +     * Work out the pdbfilename from the model number
 +     */
 +    String pdbfilename = modelFileNames[0];
 +    findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
 +    {
 +      for (ChimeraModel cm : chimeraMaps.get(pdbfile))
        {
 -        for (ChimeraModel cm : chimeraMaps.get(pdbfile))
 +        if (cm.getModelNumber() == modelId)
          {
 -          if (cm.getModelNumber() == modelId)
 -          {
 -            pdbfilename = pdbfile;
 -            break findfileloop;
 -          }
 +          pdbfilename = pdbfile;
 +          break findfileloop;
          }
        }
 -      atomSpecs.add(new AtomSpec(pdbfilename, chainId, pdbResNum, 0));
      }
 -
 -    /*
 -     * Broadcast the selection (which may be empty, if the user just cleared all
 -     * selections)
 -     */
 -    getSsm().mouseOverStructure(atomSpecs);
 +    return pdbfilename;
    }
  
    private void log(String message)
    }
  
    /**
 +   * Returns a list of chains mapped in this viewer. Note this list is not
 +   * currently scoped per structure.
 +   * 
 +   * @return
 +   */
 +  @Override
 +  public List<String> getChainNames()
 +  {
 +    return chainNames;
 +  }
 +
 +  /**
     * Send a 'focus' command to Chimera to recentre the visible display
     */
    public void focusView()
      }
    }
  
 +  /**
 +   * Constructs and send commands to Chimera to set attributes on residues for
 +   * features visible in Jalview
 +   * 
 +   * @param avp
 +   */
 +  public void sendFeaturesToViewer(AlignmentViewPanel avp)
 +  {
 +    // TODO refactor as required to pull up to an interface
 +    AlignmentI alignment = avp.getAlignment();
 +    FeatureRenderer fr = getFeatureRenderer(avp);
  
 -  @Override
 -  public List<String> getChainNames()
 +    /*
 +     * fr is null if feature display is turned off
 +     */
 +    if (fr == null)
 +    {
 +      return;
 +    }
 +
 +    String[] files = getPdbFile();
 +    if (files == null)
 +    {
 +      return;
 +    }
 +
 +    StructureMappingcommandSet commandSet = ChimeraCommands
 +            .getSetAttributeCommandsForFeatures(getSsm(), files,
 +                    getSequence(), fr, alignment);
 +    String[] commands = commandSet.commands;
 +    if (commands.length > 10)
 +    {
 +      sendCommandsByFile(commands);
 +    }
 +    else
 +    {
 +      for (String command : commands)
 +      {
 +        sendAsynchronousCommand(command, null);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Write commands to a temporary file, and send a command to Chimera to open
 +   * the file as a commands script. For use when sending a large number of
 +   * separate commands would overload the REST interface mechanism.
 +   * 
 +   * @param commands
 +   */
 +  protected void sendCommandsByFile(String[] commands)
    {
 -    return chainNames;
 +    try
 +    {
 +      File tmp = File.createTempFile("chim", ".com");
 +      tmp.deleteOnExit();
 +      PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
 +      for (String command : commands)
 +      {
 +        out.println(command);
 +      }
 +      out.flush();
 +      out.close();
 +      String path = tmp.getAbsolutePath();
 +      sendAsynchronousCommand("open cmd:" + path, null);
 +    } catch (IOException e)
 +    {
 +      System.err
 +              .println("Sending commands to Chimera via file failed with "
 +                      + e.getMessage());
 +    }
 +  }
 +
 +  /**
 +   * Get Chimera residues which have the named attribute, find the mapped
 +   * positions in the Jalview sequence(s), and set as sequence features
 +   * 
 +   * @param attName
 +   * @param alignmentPanel
 +   */
 +  public void copyStructureAttributesToFeatures(String attName,
 +          AlignmentViewPanel alignmentPanel)
 +  {
 +    // todo pull up to AAStructureBindingModel (and interface?)
 +
 +    /*
 +     * ask Chimera to list residues with the attribute, reporting its value
 +     */
 +    // this alternative command
 +    // list residues spec ':*/attName' attr attName
 +    // doesn't report 'None' values (which is good), but
 +    // fails for 'average.bfactor' (which is bad):
 +
 +    String cmd = "list residues attr '" + attName + "'";
 +    List<String> residues = sendChimeraCommand(cmd, true);
 +
 +    boolean featureAdded = createFeaturesForAttributes(attName, residues);
 +    if (featureAdded)
 +    {
 +      alignmentPanel.getFeatureRenderer().featuresAdded();
 +    }
 +  }
 +
 +  /**
 +   * Create features in Jalview for the given attribute name and structure
 +   * residues.
 +   * 
 +   * <pre>
 +   * The residue list should be 0, 1 or more reply lines of the format: 
 +   *     residue id #0:5.A isHelix -155.000836316 index 5 
 +   * or 
 +   *     residue id #0:6.A isHelix None
 +   * </pre>
 +   * 
 +   * @param attName
 +   * @param residues
 +   * @return
 +   */
 +  protected boolean createFeaturesForAttributes(String attName,
 +          List<String> residues)
 +  {
 +    boolean featureAdded = false;
 +    String featureGroup = getViewerFeatureGroup();
 +
 +    for (String residue : residues)
 +    {
 +      AtomSpec spec = null;
 +      String[] tokens = residue.split(" ");
 +      if (tokens.length < 5)
 +      {
 +        continue;
 +      }
 +      String atomSpec = tokens[2];
 +      String attValue = tokens[4];
 +
 +      /*
 +       * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
 +       */
 +      if ("None".equalsIgnoreCase(attValue)
 +              || "False".equalsIgnoreCase(attValue))
 +      {
 +        continue;
 +      }
 +
 +      try
 +      {
 +        spec = AtomSpec.fromChimeraAtomspec(atomSpec);
 +      } catch (IllegalArgumentException e)
 +      {
 +        System.err.println("Problem parsing atomspec " + atomSpec);
 +        continue;
 +      }
 +
 +      String chainId = spec.getChain();
 +      String description = attValue;
 +      float score = Float.NaN;
 +      try
 +      {
 +        score = Float.valueOf(attValue);
 +        description = chainId;
 +      } catch (NumberFormatException e)
 +      {
 +        // was not a float value
 +      }
 +
 +      String pdbFile = getPdbFileForModel(spec.getModelNumber());
 +      spec.setPdbFile(pdbFile);
 +
 +      List<AtomSpec> atoms = Collections.singletonList(spec);
 +
 +      /*
 +       * locate the mapped position in the alignment (if any)
 +       */
 +      SearchResults sr = getSsm()
 +              .findAlignmentPositionsForStructurePositions(atoms);
 +
 +      /*
 +       * expect one matched alignment position, or none 
 +       * (if the structure position is not mapped)
 +       */
 +      for (SearchResultMatchI m : sr.getResults())
 +      {
 +        SequenceI seq = m.getSequence();
 +        int start = m.getStart();
 +        int end = m.getEnd();
 +        SequenceFeature sf = new SequenceFeature(attName, description,
 +                start, end, score, featureGroup);
 +        // todo: should SequenceFeature have an explicit property for chain?
 +        // note: repeating the action shouldn't duplicate features
 +        featureAdded |= seq.addSequenceFeature(sf);
 +      }
 +    }
 +    return featureAdded;
    }
  
 +  /**
 +   * Answers the feature group name to apply to features created in Jalview from
 +   * Chimera attributes
 +   * 
 +   * @return
 +   */
 +  protected String getViewerFeatureGroup()
 +  {
 +    // todo pull up to interface
 +    return CHIMERA_FEATURE_GROUP;
 +  }
 +
 +
    public Hashtable<String, String> getChainFile()
    {
      return chainFile;
@@@ -21,6 -21,7 +21,7 @@@
  package jalview.gui;
  
  import jalview.api.AlignmentViewPanel;
+ import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
@@@ -127,20 -128,13 +128,20 @@@ public class JalviewChimeraBindingMode
        @Override
        public void run()
        {
 -        long stm = cvf.startProgressBar(progressMsg);
 +        long handle = 0;
 +        if (progressMsg != null)
 +        {
 +          handle = cvf.startProgressBar(progressMsg);
 +        }
          try
          {
            sendChimeraCommand(command, false);
          } finally
          {
 -          cvf.stopProgressBar(null, stm);
 +          if (progressMsg != null)
 +          {
 +            cvf.stopProgressBar(null, handle);
 +          }
          }
        }
      });
  
    }
  
+   @Override
+   public JalviewStructureDisplayI getViewer()
+   {
+     return cvf;
+   }
  }