Merge branch 'develop' into features/JAL-2295setChimeraAttributes
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 29 Nov 2016 14:20:41 +0000 (14:20 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 29 Nov 2016 14:20:41 +0000 (14:20 +0000)
Conflicts:
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java

1  2 
resources/lang/Messages.properties
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/structure/StructureSelectionManager.java
test/jalview/ext/jmol/JmolViewerTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java

@@@ -126,7 -126,7 +126,7 @@@ action.colour = Colou
  action.calculate = Calculate
  action.select_all = Select all
  action.select_highlighted_columns = Select Highlighted Columns
- tooltip.select_highlighted_columns = Press B to mark highlighted columns, Ctrl-B to toggle, and Alt-B to mark all but highlighted columns 
+ tooltip.select_highlighted_columns = Press B to mark highlighted columns, Ctrl-(or Cmd)-B to toggle, and Alt-B to mark all but highlighted columns 
  action.deselect_all = Deselect all
  action.invert_selection = Invert selection
  action.using_jmol = Using Jmol
@@@ -716,8 -716,6 +716,8 @@@ label.colour_with_chimera = Colour wit
  label.align_structures = Align Structures
  label.jmol = Jmol
  label.chimera = Chimera
 +label.create_chimera_attributes = Write Jalview features
 +label.create_chimera_attributes_tip = Set Chimera residue attributes for visible features
  label.sort_alignment_by_tree = Sort Alignment By Tree
  label.mark_unlinked_leaves = Mark Unlinked Leaves
  label.associate_leaves_with = Associate Leaves With
@@@ -1,3 -1,35 +1,35 @@@
+ /* vim: set ts=2: */
+ /**
+  * Copyright (c) 2006 The Regents of the University of California.
+  * All rights reserved.
+  *
+  * Redistribution and use in source and binary forms, with or without
+  * modification, are permitted provided that the following conditions
+  * are met:
+  *   1. Redistributions of source code must retain the above copyright
+  *      notice, this list of conditions, and the following disclaimer.
+  *   2. Redistributions in binary form must reproduce the above
+  *      copyright notice, this list of conditions, and the following
+  *      disclaimer in the documentation and/or other materials provided
+  *      with the distribution.
+  *   3. Redistributions must acknowledge that this software was
+  *      originally developed by the UCSF Computer Graphics Laboratory
+  *      under support by the NIH National Center for Research Resources,
+  *      grant P41-RR01081.
+  *
+  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
+  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+  * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+  * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  *
+  */
  package ext.edu.ucsf.rbvi.strucviz2;
  
  import jalview.ws.HttpClientUtils;
@@@ -389,11 -421,6 +421,11 @@@ public class ChimeraManage
              "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+");
@@@ -27,11 -27,9 +27,12 @@@ 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;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ResidueProperties;
  import jalview.structure.AtomSpec;
@@@ -41,13 -39,8 +42,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;
@@@ -60,8 -53,6 +61,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;
  
    /*
     * @param protocol
     */
    public JalviewChimeraBinding(StructureSelectionManager ssm,
-           PDBEntry[] pdbentry, SequenceI[][] sequenceIs, String protocol)
+           PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol)
    {
      super(ssm, pdbentry, sequenceIs, protocol);
      viewer = new ChimeraManager(
        chimeraListener.shutdown();
        chimeraListener = null;
      }
 -    lastCommand = null;
      viewer = null;
  
      releaseUIResources();
    }
  
    /**
 -   * 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)
     * 
     * @return
     */
 +  @Override
 +  public List<String> getChainNames()
 +  {
 +    return chainNames;
 +  }
  
    /**
     * Send a 'focus' command to Chimera to recentre the visible display
      }
    }
  
 +  /**
 +   * 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;
@@@ -26,10 -26,9 +26,10 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
 +import jalview.ext.rbvi.chimera.ChimeraCommands;
  import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
  import jalview.gui.StructureViewer.ViewerType;
- import jalview.io.AppletFormatAdapter;
+ import jalview.io.DataSourceType;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
  import jalview.io.StructureFile;
@@@ -48,11 -47,8 +48,11 @@@ import jalview.util.Platform
  import jalview.ws.dbsources.Pdb;
  
  import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
  import java.awt.event.ItemEvent;
  import java.awt.event.ItemListener;
 +import java.awt.event.MouseAdapter;
 +import java.awt.event.MouseEvent;
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileInputStream;
@@@ -62,7 -58,6 +62,7 @@@ import java.io.IOException
  import java.io.InputStream;
  import java.io.PrintWriter;
  import java.util.ArrayList;
 +import java.util.Collections;
  import java.util.List;
  import java.util.Random;
  import java.util.Vector;
@@@ -71,7 -66,6 +71,7 @@@ import javax.swing.JCheckBoxMenuItem
  import javax.swing.JColorChooser;
  import javax.swing.JInternalFrame;
  import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
  import javax.swing.JOptionPane;
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
@@@ -186,97 -180,6 +186,97 @@@ public class ChimeraViewFrame extends S
          // TODO Auto-generated method stub
        }
      });
 +
 +    JMenuItem writeFeatures = new JMenuItem(
 +            MessageManager.getString("label.create_chimera_attributes"));
 +    writeFeatures.setToolTipText(MessageManager
 +            .getString("label.create_chimera_attributes_tip"));
 +    writeFeatures.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        sendFeaturesToChimera();
 +      }
 +    });
 +    viewerActionMenu.add(writeFeatures);
 +
 +    final JMenu fetchAttributes = new JMenu("Fetch Chimera attributes");
 +    fetchAttributes
 +            .setToolTipText("Copy Chimera attribute to Jalview feature");
 +    fetchAttributes.addMouseListener(new MouseAdapter()
 +    {
 +
 +      @Override
 +      public void mouseEntered(MouseEvent e)
 +      {
 +        buildAttributesMenu(fetchAttributes);
 +      }
 +    });
 +    viewerActionMenu.add(fetchAttributes);
 +
 +  }
 +
 +  /**
 +   * Query Chimera for its residue attribute names and add them as items off the
 +   * attributes menu
 +   * 
 +   * @param attributesMenu
 +   */
 +  protected void buildAttributesMenu(JMenu attributesMenu)
 +  {
 +    List<String> atts = jmb.sendChimeraCommand("list resattr", true);
 +    if (atts == null)
 +    {
 +      return;
 +    }
 +    attributesMenu.removeAll();
 +    Collections.sort(atts);
 +    for (String att : atts)
 +    {
 +      final String attName = att.split(" ")[1];
 +
 +      /*
 +       * ignore 'jv_*' attributes, as these are Jalview features that have
 +       * been transferred to residue attributes in Chimera!
 +       */
 +      if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
 +      {
 +        JMenuItem menuItem = new JMenuItem(attName);
 +        menuItem.addActionListener(new ActionListener()
 +        {
 +          @Override
 +          public void actionPerformed(ActionEvent e)
 +          {
 +            getChimeraAttributes(attName);
 +          }
 +        });
 +        attributesMenu.add(menuItem);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Read residues in Chimera with the given attribute name, and set as features
 +   * on the corresponding sequence positions (if any)
 +   * 
 +   * @param attName
 +   */
 +  protected void getChimeraAttributes(String attName)
 +  {
 +    jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
 +  }
 +
 +  /**
 +   * Send a command to Chimera to create residue attributes for Jalview features
 +   * <p>
 +   * The syntax is: setattr r <attName> <attValue> <atomSpec>
 +   * <p>
 +   * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A
 +   */
 +  protected void sendFeaturesToChimera()
 +  {
 +    jmb.sendFeaturesToViewer(getAlignmentPanel());
    }
  
    /**
            SequenceI[][] seqs)
    {
      createProgressBar();
 -    // FIXME extractChains needs pdbentries to match IDs to PDBEntry(s) on seqs
      jmb = new JalviewChimeraBindingModel(this,
              ap.getStructureSelectionManager(), pdbentrys, seqs, null);
      addAlignmentPanel(ap);
  
    }
  
 -
 -
    /**
     * Create a new viewer from saved session state data including Chimera session
     * file
     */
    void initChimera()
    {
 -    jmb.setFinishedInit(false);
 -    jalview.gui.Desktop.addInternalFrame(this,
 -            jmb.getViewerTitle("Chimera", true), getBounds().width,
 -            getBounds().height);
 +    Desktop.addInternalFrame(this, jmb.getViewerTitle("Chimera", true),
 +            getBounds().width, getBounds().height);
  
      if (!jmb.launchChimera())
      {
-       JOptionPane.showMessageDialog(Desktop.desktop,
+       JvOptionPane.showMessageDialog(Desktop.desktop,
                MessageManager.getString("label.chimera_failed"),
                MessageManager.getString("label.error_loading_file"),
-               JOptionPane.ERROR_MESSAGE);
+               JvOptionPane.ERROR_MESSAGE);
        this.dispose();
        return;
      }
                          + chimeraSessionFile);
        }
      }
 -    jmb.setFinishedInit(true);
  
      jmb.startChimeraListener();
    }
  
 +  /**
 +   * If the list is not empty, add menu items for 'All' and each individual
 +   * chain to the "View | Show Chain" sub-menu. Multiple selections are allowed.
 +   * 
 +   * @param chainNames
 +   */
 +  @Override
 +  void setChainMenuItems(List<String> chainNames)
 +  {
 +    chainMenu.removeAll();
 +    if (chainNames == null || chainNames.isEmpty())
 +    {
 +      return;
 +    }
 +    JMenuItem menuItem = new JMenuItem(
 +            MessageManager.getString("label.all"));
 +    menuItem.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent evt)
 +      {
 +        allChainsSelected = true;
 +        for (int i = 0; i < chainMenu.getItemCount(); i++)
 +        {
 +          if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
 +          {
 +            ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
 +          }
 +        }
 +        showSelectedChains();
 +        allChainsSelected = false;
 +      }
 +    });
 +
 +    chainMenu.add(menuItem);
 +
 +    for (String chainName : chainNames)
 +    {
 +      menuItem = new JCheckBoxMenuItem(chainName, true);
 +      menuItem.addItemListener(new ItemListener()
 +      {
 +        @Override
 +        public void itemStateChanged(ItemEvent evt)
 +        {
 +          if (!allChainsSelected)
 +          {
 +            showSelectedChains();
 +          }
 +        }
 +      });
 +
 +      chainMenu.add(menuItem);
 +    }
 +  }
  
    /**
     * Show only the selected chain(s) in the viewer
                  "label.confirm_close_chimera",
                  new Object[] { jmb.getViewerTitle("Chimera", false) });
          prompt = JvSwingUtils.wrapTooltip(true, prompt);
-         int confirm = JOptionPane.showConfirmDialog(this, prompt,
+         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
                  MessageManager.getString("label.close_viewer"),
-                 JOptionPane.YES_NO_CANCEL_OPTION);
+                 JvOptionPane.YES_NO_CANCEL_OPTION);
          /*
           * abort closure if user hits escape or Cancel
           */
-         if (confirm == JOptionPane.CANCEL_OPTION
-                 || confirm == JOptionPane.CLOSED_OPTION)
+         if (confirm == JvOptionPane.CANCEL_OPTION
+                 || confirm == JvOptionPane.CLOSED_OPTION)
          {
            return;
          }
-         closeChimera = confirm == JOptionPane.YES_OPTION;
+         closeChimera = confirm == JvOptionPane.YES_OPTION;
        }
        jmb.closeViewer(closeChimera);
      }
      if (errormsgs.length() > 0)
      {
  
-       JOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
+       JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
                .formatMessage("label.pdb_entries_couldnt_be_retrieved",
                        new Object[] { errormsgs.toString() }),
                MessageManager.getString("label.couldnt_load_file"),
-               JOptionPane.ERROR_MESSAGE);
+               JvOptionPane.ERROR_MESSAGE);
      }
  
      if (files.length() > 0)
      {
 +      jmb.setFinishedInit(false);
        if (!addingStructures)
        {
          try
              jmb.openFile(pe);
              jmb.addSequence(pos, jmb.getSequence()[pos]);
              File fl = new File(pe.getFile());
-             String protocol = AppletFormatAdapter.URL;
+             DataSourceType protocol = DataSourceType.URL;
              try
              {
                if (fl.exists())
                {
-                 protocol = AppletFormatAdapter.FILE;
+                 protocol = DataSourceType.FILE;
                }
              } catch (Throwable e)
              {
            }
          }
        }
 +
        jmb.refreshGUI();
        jmb.setFinishedInit(true);
        jmb.setLoadingFromArchive(false);
      setChainMenuItems(jmb.getChainNames());
  
      this.setTitle(jmb.getViewerTitle("Chimera", true));
 -    if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
 -    {
 +    // if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
 +    // {
        viewerActionMenu.setVisible(true);
 -    }
 +    // }
      if (!jmb.isLoadingFromArchive())
      {
        seqColour_actionPerformed(null);
@@@ -24,6 -24,7 +24,7 @@@ import jalview.api.AlignmentViewPanel
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
+ import jalview.io.DataSourceType;
  import jalview.structure.StructureSelectionManager;
  
  public class JalviewChimeraBindingModel extends JalviewChimeraBinding
@@@ -35,7 -36,7 +36,7 @@@
  
    public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame,
            StructureSelectionManager ssm, PDBEntry[] pdbentry,
-           SequenceI[][] sequenceIs, String protocol)
+           SequenceI[][] sequenceIs, DataSourceType protocol)
    {
      super(ssm, pdbentry, sequenceIs, protocol);
      cvf = chimeraViewFrame;
        @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);
 +          }
          }
        }
      });
@@@ -35,7 -35,7 +35,7 @@@ import jalview.datamodel.SearchResultsI
  import jalview.datamodel.SequenceI;
  import jalview.ext.jmol.JmolParser;
  import jalview.gui.IProgressIndicator;
- import jalview.io.AppletFormatAdapter;
+ import jalview.io.DataSourceType;
  import jalview.io.StructureFile;
  import jalview.util.MappingUtils;
  import jalview.util.MessageManager;
@@@ -323,7 -323,7 +323,7 @@@ public class StructureSelectionManage
     * @return null or the structure data parsed as a pdb file
     */
    synchronized public StructureFile setMapping(SequenceI[] sequence,
-           String[] targetChains, String pdbFile, String protocol)
+           String[] targetChains, String pdbFile, DataSourceType protocol)
    {
      return setMapping(true, sequence, targetChains, pdbFile, protocol);
    }
     *          (may be nill, individual elements may be nill)
     * @param pdbFile
     *          - structure data resource
-    * @param protocol
+    * @param sourceType
     *          - how to resolve data from resource
     * @return null or the structure data parsed as a pdb file
     */
    synchronized public StructureFile setMapping(boolean forStructureView,
            SequenceI[] sequenceArray, String[] targetChainIds,
-           String pdbFile, String protocol)
+           String pdbFile, DataSourceType sourceType)
    {
      /*
       * There will be better ways of doing this in the future, for now we'll use
      boolean isMapUsingSIFTs = SiftsSettings.isMapWithSifts();
      try
      {
-       pdb = new JmolParser(pdbFile, protocol);
+       pdb = new JmolParser(pdbFile, sourceType);
  
        if (pdb.getId() != null && pdb.getId().trim().length() > 0
-               && AppletFormatAdapter.FILE.equals(protocol))
+               && DataSourceType.FILE == sourceType)
        {
          registerPDBFile(pdb.getId().trim(), pdbFile);
        }
          continue;
        }
  
-       if (protocol.equals(jalview.io.AppletFormatAdapter.PASTE))
+       if (sourceType == DataSourceType.PASTE)
        {
          pdbFile = "INLINE" + pdb.getId();
        }
  
-       ArrayList<StructureMapping> seqToStrucMapping = new ArrayList<StructureMapping>();
+       List<StructureMapping> seqToStrucMapping = new ArrayList<StructureMapping>();
        if (isMapUsingSIFTs && seq.isProtein())
        {
          setProgressBar(null);
          }
          else
          {
-           ArrayList<StructureMapping> foundSiftsMappings = new ArrayList<StructureMapping>();
+           List<StructureMapping> foundSiftsMappings = new ArrayList<StructureMapping>();
            for (PDBChain chain : pdb.getChains())
            {
              try
        return;
      }
  
 -    SearchResultsI results = new SearchResults();
 +    SearchResultsI results = findAlignmentPositionsForStructurePositions(atoms);
 +    for (Object li : listeners)
 +    {
 +      if (li instanceof SequenceListener)
 +      {
 +        ((SequenceListener) li).highlightSequence(results);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Constructs a SearchResults object holding regions (if any) in the Jalview
 +   * alignment which have a mapping to the structure viewer positions in the
 +   * supplied list
 +   * 
 +   * @param atoms
 +   * @return
 +   */
 +  public SearchResults findAlignmentPositionsForStructurePositions(
 +          List<AtomSpec> atoms)
 +  {
 +    SearchResults results = new SearchResults();
      for (AtomSpec atom : atoms)
      {
        SequenceI lastseq = null;
          }
        }
      }
 -    for (Object li : listeners)
 -    {
 -      if (li instanceof SequenceListener)
 -      {
 -        ((SequenceListener) li).highlightSequence(results);
 -      }
 -    }
 +    return results;
    }
  
    /**
@@@ -24,13 -24,13 +24,14 @@@ import static org.testng.AssertJUnit.as
  
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
 +import jalview.bin.Jalview;
  import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
+ import jalview.gui.JvOptionPane;
  import jalview.gui.Preferences;
  import jalview.gui.StructureViewer;
  import jalview.gui.StructureViewer.ViewerType;
- import jalview.io.FormatAdapter;
+ import jalview.io.DataSourceType;
  
  import org.testng.annotations.AfterClass;
  import org.testng.annotations.BeforeClass;
@@@ -40,13 -40,21 +41,20 @@@ import org.testng.annotations.Test
  public class JmolViewerTest
  {
  
+   @BeforeClass(alwaysRun = true)
+   public void setUpJvOptionPane()
+   {
+     JvOptionPane.setInteractiveMode(false);
+     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+   }
    /**
     * @throws java.lang.Exception
     */
    @BeforeClass(alwaysRun = true)
    public static void setUpBeforeClass() throws Exception
    {
 -    jalview.bin.Jalview.main(new String[] {
 -        "-noquestionnaire -nonews -props",
 +    Jalview.main(new String[] { "-noquestionnaire", "-nonews", "-props",
          "test/jalview/ext/rbvi/chimera/testProps.jvprops" });
    }
  
@@@ -65,7 -73,7 +73,7 @@@
      Cache.setProperty(Preferences.STRUCTURE_DISPLAY, ViewerType.JMOL.name());
      String inFile = "examples/1gaq.txt";
      AlignFrame af = new jalview.io.FileLoader().LoadFileWaitTillLoaded(
-             inFile, FormatAdapter.FILE);
+             inFile, DataSourceType.FILE);
      assertTrue("Didn't read input file " + inFile, af != null);
      for (SequenceI sq : af.getViewport().getAlignment().getSequences())
      {
   */
  package jalview.ext.rbvi.chimera;
  
 -import static org.testng.AssertJUnit.assertEquals;
 -import static org.testng.AssertJUnit.assertTrue;
 +import static org.testng.Assert.assertEquals;
 +import static org.testng.Assert.assertTrue;
  
+ import jalview.gui.JvOptionPane;
  import java.awt.Color;
 -import java.util.Arrays;
 +import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  
+ import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
  public class ChimeraCommandsTest
  {
+   @BeforeClass(alwaysRun = true)
+   public void setUpJvOptionPane()
+   {
+     JvOptionPane.setInteractiveMode(false);
+     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+   }
    @Test(groups = { "Functional" })
    public void testBuildColourCommands()
    {
  
 -    Map<Color, Map<Integer, Map<String, List<int[]>>>> map = new LinkedHashMap<Color, Map<Integer, Map<String, List<int[]>>>>();
 -    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
 -    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
 -    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
 -    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
 -    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
 -    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
 -    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
 -    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
 +    Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
 +    ChimeraCommands.addRange(map, Color.blue, 0, 2, 5, "A");
 +    ChimeraCommands.addRange(map, Color.blue, 0, 7, 7, "B");
 +    ChimeraCommands.addRange(map, Color.blue, 0, 9, 23, "A");
 +    ChimeraCommands.addRange(map, Color.blue, 1, 1, 1, "A");
 +    ChimeraCommands.addRange(map, Color.blue, 1, 4, 7, "B");
 +    ChimeraCommands.addRange(map, Color.yellow, 1, 8, 8, "A");
 +    ChimeraCommands.addRange(map, Color.yellow, 1, 3, 5, "A");
 +    ChimeraCommands.addRange(map, Color.red, 0, 3, 5, "A");
 +    ChimeraCommands.addRange(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, and positions as
 -    // added
 +    // they were added; within colour, by model, by chain, ranges in start order
      String command = ChimeraCommands.buildColourCommands(map).get(0);
      assertEquals(
 -            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:8.A,3-5.A; color #ff0000 #0:3-5.A",
 -            command);
 +            command,
 +            "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");
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testBuildSetAttributeCommands()
 +  {
 +    /*
 +     * make a map of { featureType, {featureValue, {residue range specification } } }
 +     */
 +    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
 +    Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
 +    
 +    /*
 +     * start with just one feature/value...
 +     */
 +    featuresMap.put("chain", featureValues);
 +    ChimeraCommands.addRange(featureValues, "X", 0, 8, 20, "A");
 +  
 +    List<String> commands = ChimeraCommands
 +            .buildSetAttributeCommands(featuresMap);
 +    assertEquals(1, commands.size());
 +
 +    /*
 +     * feature name gets a jv_ namespace prefix
 +     * feature value is quoted in case it contains spaces
 +     */
 +    assertEquals(commands.get(0), "setattr r jv_chain \"X\" #0:8-20.A");
 +
 +    // add same feature value, overlapping range
 +    ChimeraCommands.addRange(featureValues, "X", 0, 3, 9, "A");
 +    // same feature value, contiguous range
 +    ChimeraCommands.addRange(featureValues, "X", 0, 21, 25, "A");
 +    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
 +    assertEquals(1, commands.size());
 +    assertEquals(commands.get(0), "setattr r jv_chain \"X\" #0:3-25.A");
 +
 +    // same feature value and model, different chain
 +    ChimeraCommands.addRange(featureValues, "X", 0, 21, 25, "B");
 +    // same feature value and chain, different model
 +    ChimeraCommands.addRange(featureValues, "X", 1, 26, 30, "A");
 +    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
 +    assertEquals(1, commands.size());
 +    assertEquals(commands.get(0),
 +            "setattr r jv_chain \"X\" #0:3-25.A,21-25.B|#1:26-30.A");
 +
 +    // same feature, different value
 +    ChimeraCommands.addRange(featureValues, "Y", 0, 40, 50, "A");
 +    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
 +    assertEquals(2, commands.size());
 +    // commands are ordered by feature type but not by value
 +    // so use contains to test for the expected command:
 +    assertTrue(commands
 +            .contains("setattr r jv_chain \"X\" #0:3-25.A,21-25.B|#1:26-30.A"));
 +    assertTrue(commands.contains("setattr r jv_chain \"Y\" #0:40-50.A"));
 +
 +    featuresMap.clear();
 +    featureValues.clear();
 +    featuresMap.put("side-chain binding!", featureValues);
 +    ChimeraCommands.addRange(featureValues, "metal ion!", 0, 7, 15, "A");
 +    // feature names are sanitised to change space or hyphen to underscore
 +    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
 +    assertTrue(commands
 +            .contains("setattr r jv_side_chain_binding_ \"metal ion!\" #0:7-15.A"));
 +  }
 +
 +  /**
 +   * Tests for the method that prefixes and sanitises a feature name so it can
 +   * be used as a valid, namespaced attribute name in Chimera
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testMakeAttributeName()
 +  {
 +    assertEquals(ChimeraCommands.makeAttributeName(null), "jv_");
 +    assertEquals(ChimeraCommands.makeAttributeName(""), "jv_");
 +    assertEquals(ChimeraCommands.makeAttributeName("helix"), "jv_helix");
 +    assertEquals(ChimeraCommands.makeAttributeName("Hello World 24"),
 +            "jv_Hello_World_24");
 +    assertEquals(
 +            ChimeraCommands.makeAttributeName("!this is-a_very*{odd(name"),
 +            "jv__this_is_a_very__odd_name");
 +    // name ending in color gets underscore appended
 +    assertEquals(ChimeraCommands.makeAttributeName("helixColor"),
 +            "jv_helixColor_");
    }
  }
   */
  package jalview.ext.rbvi.chimera;
  
 -import static org.testng.AssertJUnit.assertEquals;
 -import static org.testng.AssertJUnit.assertTrue;
 +import static org.testng.Assert.assertEquals;
 +import static org.testng.Assert.assertFalse;
 +import static org.testng.Assert.assertNotNull;
 +import static org.testng.Assert.assertTrue;
  
 +import jalview.api.FeatureRenderer;
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
 +import jalview.bin.Jalview;
 +import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
+ import jalview.gui.JvOptionPane;
  import jalview.gui.Preferences;
  import jalview.gui.StructureViewer;
  import jalview.gui.StructureViewer.ViewerType;
 +import jalview.io.FileLoader;
- import jalview.io.FormatAdapter;
 +import jalview.structure.StructureMapping;
 +import jalview.structure.StructureSelectionManager;
 +import jalview.ws.sifts.SiftsClient;
 +import jalview.ws.sifts.SiftsException;
 +import jalview.ws.sifts.SiftsSettings;
 +
 +import java.io.File;
 +import java.io.IOException;
 +import java.util.List;
 +import java.util.Vector;
+ import jalview.io.DataSourceType;
  
  import org.testng.annotations.AfterClass;
 +import org.testng.annotations.AfterMethod;
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
  public class JalviewChimeraView
  {
  
+   @BeforeClass(alwaysRun = true)
+   public void setUpJvOptionPane()
+   {
+     JvOptionPane.setInteractiveMode(false);
+     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+   }
 +  private JalviewStructureDisplayI chimeraViewer;
 +
    /**
     * @throws java.lang.Exception
     */
    @BeforeClass(alwaysRun = true)
    public static void setUpBeforeClass() throws Exception
    {
 -    jalview.bin.Jalview.main(new String[] {
 -        "-noquestionnaire -nonews -props",
 +    Jalview.main(new String[] { "-noquestionnaire", "-nonews", "-props",
          "test/jalview/ext/rbvi/chimera/testProps.jvprops" });
 +    Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
 +            ViewerType.CHIMERA.name());
 +    Cache.setProperty("SHOW_ANNOTATIONS", "false");
 +    Cache.setProperty(Preferences.STRUCT_FROM_PDB, "false");
 +    Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
 +            ViewerType.CHIMERA.name());
 +    Cache.setProperty("MAP_WITH_SIFTS", "true");
 +    // TODO this should not be necessary!
 +    SiftsSettings.setMapWithSifts(true);
    }
  
    /**
    @AfterClass(alwaysRun = true)
    public static void tearDownAfterClass() throws Exception
    {
 -    jalview.gui.Desktop.instance.closeAll_actionPerformed(null);
 +    Desktop.instance.closeAll_actionPerformed(null);
 +  }
 +
 +  @AfterMethod(alwaysRun = true)
 +  public void tearDownAfterTest() throws Exception
 +  {
 +    SiftsClient.setMockSiftsFile(null);
 +    if (chimeraViewer != null)
 +    {
 +      chimeraViewer.closeViewer(true);
 +    }
    }
  
 -  @Test(groups = { "Functional" })
 +  /**
 +   * Load 1GAQ and view the first structure for which a PDB id is found. Note no
 +   * network connection is needed - PDB file is read locally, SIFTS fetch fails
 +   * so mapping falls back to Needleman-Wunsch - ok for this test.
 +   */
 +  // External as local install of Chimera required
 +  @Test(groups = { "External" })
    public void testSingleSeqViewChimera()
    {
 -    Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
 -            ViewerType.CHIMERA.name());
      String inFile = "examples/1gaq.txt";
 -    AlignFrame af = new jalview.io.FileLoader().LoadFileWaitTillLoaded(
 -            inFile, DataSourceType.FILE);
 -    assertTrue("Didn't read input file " + inFile, af != null);
 -    for (SequenceI sq : af.getViewport().getAlignment().getSequences())
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
-             FormatAdapter.FILE);
++            DataSourceType.FILE);
 +    assertNotNull(af, "Failed to create AlignFrame");
 +    SequenceI sq = af.getViewport().getAlignment().getSequenceAt(0);
 +    assertEquals(sq.getName(), "1GAQ|A");
 +    SequenceI dsq = sq.getDatasetSequence();
 +    Vector<PDBEntry> pdbIds = dsq.getAllPDBEntries();
 +    assertEquals(pdbIds.size(), 1);
 +    PDBEntry pdbEntry = pdbIds.get(0);
 +    assertEquals(pdbEntry.getId(), "1GAQ");
 +    StructureViewer structureViewer = new StructureViewer(af.getViewport()
 +            .getStructureSelectionManager());
 +    chimeraViewer = structureViewer.viewStructures(pdbEntry,
 +            new SequenceI[] { sq }, af.getCurrentView().getAlignPanel());
 +    JalviewChimeraBinding binding = (JalviewChimeraBinding) chimeraViewer
 +            .getBinding();
 +
 +    /*
 +     * Wait for viewer load thread to complete
 +     */
 +    while (!binding.isFinishedInit())
      {
 -      System.out.println("** sq=" + sq.getName());
 -      SequenceI dsq = sq.getDatasetSequence();
 -      while (dsq.getDatasetSequence() != null)
 +      try
 +      {
 +        Thread.sleep(500);
 +      } catch (InterruptedException e)
        {
 -        dsq = dsq.getDatasetSequence();
        }
 -      if (dsq.getAllPDBEntries() != null
 -              && dsq.getAllPDBEntries().size() > 0)
 +    }
 +
 +    assertTrue(binding.isChimeraRunning(), "Failed to start Chimera");
 +
 +    assertEquals(chimeraViewer.getBinding().getPdbCount(), 1);
 +    chimeraViewer.closeViewer(true);
 +    chimeraViewer = null;
 +    return;
 +  }
 +
 +  /**
 +   * Test for writing Jalview features as attributes on mapped residues in
 +   * Chimera. Note this uses local copies of PDB and SIFTS file, no network
 +   * connection required.
 +   * 
 +   * @throws IOException
 +   * @throws SiftsException
 +   */
 +  // External as this requires a local install of Chimera
 +  @Test(groups = { "External" })
 +  public void testTransferFeatures() throws IOException, SiftsException
 +  {
 +    String inFile = "examples/uniref50.fa";
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
-             FormatAdapter.FILE);
++            DataSourceType.FILE);
 +    assertNotNull(af, "Failed to create AlignFrame");
 +    SequenceI sq = af.getViewport().getAlignment().findName("FER2_ARATH");
 +    assertNotNull(sq, "Didn't find FER2_ARATH");
 +
 +    /*
 +     * need a Uniprot dbref for SIFTS mapping to work!!
 +     */
 +    sq.addDBRef(new DBRefEntry("UNIPROT", "0", "P16972", null));
 +
 +    /*
 +     * use local test PDB and SIFTS files
 +     */
 +    String pdbFilePath = new File(
 +            "test/jalview/ext/rbvi/chimera/4zho.pdb").getPath();
 +    PDBEntry pdbEntry = new PDBEntry("4ZHO", null, null, pdbFilePath);
 +    String siftsFilePath = new File(
 +            "test/jalview/ext/rbvi/chimera/4zho.xml.gz")
 +            .getPath();
 +    SiftsClient.setMockSiftsFile(new File(siftsFilePath));
 +
 +    StructureViewer structureViewer = new StructureViewer(af.getViewport()
 +            .getStructureSelectionManager());
 +    chimeraViewer = structureViewer.viewStructures(pdbEntry,
 +            new SequenceI[] { sq }, af.getCurrentView().getAlignPanel());
 +
 +    JalviewChimeraBinding binding = (JalviewChimeraBinding) chimeraViewer
 +            .getBinding();
 +    do
 +    {
 +      try
 +      {
 +        Thread.sleep(500);
 +      } catch (InterruptedException e)
 +      {
 +      }
 +    } while (!binding.isFinishedInit());
 +
 +    assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera");
 +
 +    assertEquals(binding.getPdbCount(), 1);
 +
 +    /*
 +     * check mapping is (sequence) 53-145 to (structure) 2-94 A/B
 +     * (or possibly 52-145 to 1-94 - see JAL-2319)
 +     */
 +    StructureSelectionManager ssm = binding.getSsm();
 +    String pdbFile = binding.getPdbFile()[0];
 +    StructureMapping[] mappings = ssm.getMapping(pdbFile);
 +    assertTrue(mappings[0].getMappingDetailsOutput().contains("SIFTS"),
 +            "Failed to perform SIFTS mapping");
 +    assertEquals(mappings.length, 2);
 +    assertEquals(mappings[0].getChain(), "A");
 +    assertEquals(mappings[0].getPDBResNum(53), 2);
 +    assertEquals(mappings[0].getPDBResNum(145), 94);
 +    assertEquals(mappings[1].getChain(), "B");
 +    assertEquals(mappings[1].getPDBResNum(53), 2);
 +    assertEquals(mappings[1].getPDBResNum(145), 94);
 +
 +    /*
 +     * now add some features to FER2_ARATH 
 +     */
 +    // feature on a sequence region not mapped to structure:
 +    sq.addSequenceFeature(new SequenceFeature("transit peptide",
 +            "chloroplast", 1, 51, Float.NaN, null));
 +    // feature on a region mapped to structure:
 +    sq.addSequenceFeature(new SequenceFeature("domain",
 +            "2Fe-2S ferredoxin-type", 55, 145, Float.NaN, null));
 +    // on sparse positions of the sequence
 +    sq.addSequenceFeature(new SequenceFeature("metal ion-binding site",
 +            "Iron-Sulfur (2Fe-2S)", 91, 91, Float.NaN, null));
 +    sq.addSequenceFeature(new SequenceFeature("metal ion-binding site",
 +            "Iron-Sulfur (2Fe-2S)", 96, 96, Float.NaN, null));
 +    // on a sequence region that is partially mapped to structure:
 +    sq.addSequenceFeature(new SequenceFeature("helix", null, 50, 60,
 +            Float.NaN, null));
 +    // and again:
 +    sq.addSequenceFeature(new SequenceFeature("chain", null, 50, 70,
 +            Float.NaN, null));
 +    // add numeric valued features - score is set as attribute value
 +    sq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 62,
 +            62, -2.1f, null));
 +    sq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 65,
 +            65, 3.6f, null));
 +    sq.addSequenceFeature(new SequenceFeature("RESNUM", "ALA:   2  4zhoA",
 +            53, 53, Float.NaN, null));
 +
 +    /*
 +     * set all features visible except for chain
 +     */
 +    af.setShowSeqFeatures(true);
 +    FeatureRenderer fr = af.getFeatureRenderer();
 +    fr.setVisible("transit peptide");
 +    fr.setVisible("domain");
 +    fr.setVisible("metal ion-binding site");
 +    fr.setVisible("helix");
 +    fr.setVisible("kd");
 +    fr.setVisible("RESNUM");
 +
 +    /*
 +     * 'perform' menu action to copy visible features to
 +     * attributes in Chimera
 +     */
 +    // TODO rename and pull up method to binding interface
 +    // once functionality is added for Jmol as well
 +    binding.sendFeaturesToViewer(af.getViewport().getAlignPanel());
 +
 +    /*
 +     * give Chimera time to open the commands file and execute it
 +     */
 +    try
 +    {
 +      Thread.sleep(1000);
 +    } catch (InterruptedException e)
 +    {
 +    }
 +
 +    /*
 +     * ask Chimera for its residue attribute names
 +     */
 +    List<String> reply = binding.sendChimeraCommand("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"));
 +    assertTrue(reply.contains("resattr jv_helix"));
 +    assertTrue(reply.contains("resattr jv_kd"));
 +    assertTrue(reply.contains("resattr jv_RESNUM"));
 +    // feature is not on a mapped region - no attribute created
 +    assertFalse(reply.contains("resattr jv_transit_peptide"));
 +    // feature is not visible - no attribute created
 +    assertFalse(reply.contains("resattr jv_chain"));
 +
 +    /*
 +     * ask Chimera for residues with an attribute
 +     * 91 and 96 on sequence --> residues 40 and 45 on chains A and B
 +     */
 +    reply = binding.sendChimeraCommand(
 +            "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"));
 +    assertTrue(reply
 +            .contains("residue id #0:45.A jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 45"));
 +    assertTrue(reply
 +            .contains("residue id #0:40.B jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 40"));
 +    assertTrue(reply
 +            .contains("residue id #0:45.B jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 45"));
 +
 +    /*
 +     * check attributes with score values
 +     * sequence positions 62 and 65 --> residues 11 and 14 on chains A and B
 +     */
 +    reply = binding.sendChimeraCommand("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"));
 +    assertTrue(reply.contains("residue id #0:11.B jv_kd -2.1 index 11"));
 +    assertTrue(reply.contains("residue id #0:14.B jv_kd 3.6 index 14"));
 +
 +    /*
 +     * list residues with positive kd score 
 +     */
 +    reply = binding.sendChimeraCommand(
 +            "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"));
 +
 +    SiftsClient.setMockSiftsFile(null);
 +    chimeraViewer.closeViewer(true);
 +    chimeraViewer = null;
 +  }
 +
 +  /**
 +   * Test for creating Jalview features from attributes on mapped residues in
 +   * Chimera. Note this uses local copies of PDB and SIFTS file, no network
 +   * connection required.
 +   * 
 +   * @throws IOException
 +   * @throws SiftsException
 +   */
 +  // External as this requires a local install of Chimera
 +  @Test(groups = { "External" })
 +  public void testGetAttributes() throws IOException, SiftsException
 +  {
 +    String inFile = "examples/uniref50.fa";
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
-             FormatAdapter.FILE);
++            DataSourceType.FILE);
 +    assertNotNull(af, "Failed to create AlignFrame");
 +    SequenceI fer2Arath = af.getViewport().getAlignment()
 +            .findName("FER2_ARATH");
 +    assertNotNull(fer2Arath, "Didn't find FER2_ARATH");
 +  
 +    /*
 +     * need a Uniprot dbref for SIFTS mapping to work!!
 +     */
 +    fer2Arath.addDBRef(new DBRefEntry("UNIPROT", "0", "P16972", null));
 +  
 +    /*
 +     * use local test PDB and SIFTS files
 +     */
 +    String pdbFilePath = new File(
 +            "test/jalview/ext/rbvi/chimera/4zho.pdb").getPath();
 +    PDBEntry pdbEntry = new PDBEntry("4ZHO", null, null, pdbFilePath);
 +    String siftsFilePath = new File(
 +            "test/jalview/ext/rbvi/chimera/4zho.xml.gz")
 +            .getPath();
 +    SiftsClient.setMockSiftsFile(new File(siftsFilePath));
 +  
 +    StructureViewer structureViewer = new StructureViewer(af.getViewport()
 +            .getStructureSelectionManager());
 +    chimeraViewer = structureViewer.viewStructures(pdbEntry,
 +            new SequenceI[] { fer2Arath }, af.getCurrentView()
 +                    .getAlignPanel());
 +  
 +    JalviewChimeraBinding binding = (JalviewChimeraBinding) chimeraViewer
 +            .getBinding();
 +    do
 +    {
 +      try
 +      {
 +        Thread.sleep(500);
 +      } catch (InterruptedException e)
        {
 -        for (int q = 0; q < dsq.getAllPDBEntries().size(); q++)
 -        {
 -          final StructureViewer structureViewer = new StructureViewer(af
 -                  .getViewport().getStructureSelectionManager());
 -          structureViewer.setViewerType(ViewerType.CHIMERA);
 -          JalviewStructureDisplayI chimeraViewer = structureViewer
 -                  .viewStructures(dsq.getAllPDBEntries().elementAt(q),
 -                          new SequenceI[] { sq }, af.getCurrentView()
 -                                  .getAlignPanel());
 -          /*
 -           * Wait for viewer load thread to complete
 -           */
 -          while (!chimeraViewer.getBinding().isFinishedInit())
 -          {
 -            try
 -            {
 -              Thread.sleep(500);
 -            } catch (InterruptedException e)
 -            {
 -            }
 -          }
 -          assertEquals(1, chimeraViewer.getBinding().getPdbCount());
 -          chimeraViewer.closeViewer(true);
 -          // todo: break here means only once through this loop?
 -          break;
 -        }
 -        break;
        }
 +    } while (!binding.isFinishedInit());
 +  
 +    assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera");
 +  
 +    assertEquals(binding.getPdbCount(), 1);
 +  
 +    /*
 +     * 'perform' menu action to copy visible features to
 +     * attributes in Chimera
 +     */
 +    // TODO rename and pull up method to binding interface
 +    // once functionality is added for Jmol as well
 +    binding.copyStructureAttributesToFeatures("isHelix", af.getViewport()
 +            .getAlignPanel());
 +
 +    /*
 +     * verify 22 residues have isHelix feature
 +     * (may merge into ranges in future)
 +     */
 +    af.setShowSeqFeatures(true);
 +    FeatureRenderer fr = af.getFeatureRenderer();
 +    fr.setVisible("isHelix");
 +    for (int res = 75; res <= 83; res++)
 +    {
 +      checkFeaturesAtRes(fer2Arath, fr, res, "isHelix");
      }
 +    for (int res = 117; res <= 123; res++)
 +    {
 +      checkFeaturesAtRes(fer2Arath, fr, res, "isHelix");
 +    }
 +    for (int res = 129; res <= 131; res++)
 +    {
 +      checkFeaturesAtRes(fer2Arath, fr, res, "isHelix");
 +    }
 +    for (int res = 143; res <= 145; res++)
 +    {
 +      checkFeaturesAtRes(fer2Arath, fr, res, "isHelix");
 +    }
 +
 +    /*
 +     * fetch a numeric valued attribute
 +     */
 +    binding.copyStructureAttributesToFeatures("phi", af.getViewport()
 +            .getAlignPanel());
 +    fr.setVisible("phi");
 +    List<SequenceFeature> fs = fr.findFeaturesAtRes(fer2Arath, 54);
 +    assertEquals(fs.size(), 3);
 +    assertEquals(fs.get(0).getType(), "RESNUM");
 +    assertEquals(fs.get(1).getType(), "phi");
 +    assertEquals(fs.get(2).getType(), "phi");
 +    assertEquals(fs.get(1).getDescription(), "A"); // chain
 +    assertEquals(fs.get(2).getDescription(), "B");
 +    assertEquals(fs.get(1).getScore(), -131.0713f, 0.001f);
 +    assertEquals(fs.get(2).getScore(), -127.39512, 0.001f);
 +
 +    /*
 +     * tear down - also in AfterMethod
 +     */
 +    SiftsClient.setMockSiftsFile(null);
 +    chimeraViewer.closeViewer(true);
 +    chimeraViewer = null;
 +  }
 +
 +  /**
 +   * Helper method to verify new feature at a sequence position
 +   * 
 +   * @param seq
 +   * @param fr
 +   * @param res
 +   * @param featureType
 +   */
 +  protected void checkFeaturesAtRes(SequenceI seq, FeatureRenderer fr,
 +          int res, String featureType)
 +  {
 +    String where = "at position " + res;
 +    List<SequenceFeature> fs = fr.findFeaturesAtRes(seq, res);
 +    assertEquals(fs.size(), 2, where);
 +    assertEquals(fs.get(0).getType(), "RESNUM", where);
 +    SequenceFeature sf = fs.get(1);
 +    assertEquals(sf.getType(), featureType, where);
 +    assertEquals(sf.getFeatureGroup(), "Chimera", where);
 +    assertEquals(sf.getDescription(), "True", where);
 +    assertEquals(sf.getScore(), Float.NaN, where);
    }
  }