Merge branch 'features/JAL-2295setChimeraAttributes' into
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 1 Mar 2017 14:38:50 +0000 (14:38 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 1 Mar 2017 14:38:50 +0000 (14:38 +0000)
merges/develop_JAL2295setChimeraAttributes

Conflicts:
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/ChimeraViewFrame.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java

13 files changed:
1  2 
resources/lang/Messages.properties
src/MCview/PDBChain.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/api/AlignViewportI.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/AnnotationRowFilter.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/ConsensusThread.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/gui/AnnotationChooserTest.java

@@@ -620,6 -618,6 +620,8 @@@ label.web_services = Web Service
  label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter.
  label.let_jmol_manage_structure_colours = Let Jmol manage structure colours
  label.let_chimera_manage_structure_colours = Let Chimera manage structure colours
++label.fetch_chimera_attributes = Fetch Chimera attributes
++label.fetch_chimera_attributes_tip = Copy Chimera attribute to Jalview feature
  label.marks_leaves_tree_not_associated_with_sequence = Marks leaves of tree not associated with a sequence
  label.index_web_services_menu_by_host_site = Index web services in menu by the host site
  label.option_want_informed_web_service_URL_cannot_be_accessed_jalview_when_starts_up = Check this option if you want to be informed<br>when a web service URL cannot be accessed by Jalview<br>when it starts up
@@@ -1271,21 -1276,4 +1275,21 @@@ label.SEQUENCE_ID_no_longer_used = $SEQ
  label.SEQUENCE_ID_for_DB_ACCESSION1 = Please review your URL links in the 'Connections' tab of the Preferences window:
  label.SEQUENCE_ID_for_DB_ACCESSION2 = URL links using '$SEQUENCE_ID$' for DB accessions now use '$DB_ACCESSION$'.
  label.do_not_display_again = Do not display this message again
 +exception.url_cannot_have_miriam_id = {0} is a MIRIAM id and cannot be used as a custom url name
 +exception.url_cannot_have_duplicate_id = {0} cannot be used as a label for more than one line
 +label.filter = Filter text:
 +action.customfilter = Custom only
 +action.showall = Show All
 +label.insert = Insert:
 +action.seq_id = $SEQUENCE_ID$
 +action.db_acc = $DB_ACCESSION$
 +label.primary = Double Click
 +label.inmenu = In Menu
 +label.id = ID
 +label.database = Database
 +label.urltooltip = Only one url, which must use a sequence id, can be selected for the 'On Click' option
 +label.edit_sequence_url_link = Edit sequence URL link
 +warn.name_cannot_be_duplicate = User-defined URL names must be unique and cannot be MIRIAM ids
 +label.invalid_name = Invalid Name !
  label.output_seq_details = Output Sequence Details to list all database references
- label.urllinks = Links
++label.urllinks = Links
Simple merge
Simple merge
@@@ -47,28 -47,34 +47,34 @@@ import java.util.Map
  public class ChimeraCommands
  {
  
+   public static final String NAMESPACE_PREFIX = "jv_";
    /**
-    * utility to construct the commands to colour chains by the given alignment
-    * for passing to Chimera
-    * 
-    * @returns Object[] { Object[] { <model being coloured>,
+    * Constructs Chimera commands to colour residues as per the Jalview alignment
     * 
+    * @param ssm
+    * @param files
+    * @param sequence
+    * @param sr
+    * @param fr
+    * @param alignment
+    * @return
     */
 -  public static StructureMappingcommandSet getColourBySequenceCommand(
 +  public static StructureMappingcommandSet[] getColourBySequenceCommand(
            StructureSelectionManager ssm, String[] files,
            SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
            AlignmentI alignment)
    {
-     Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> colourMap = buildColoursMap(
-             ssm, files, sequence, sr, fr, alignment);
 -    Map<Object, AtomSpecModel> colourMap = buildColoursMap(
 -            ssm, files, sequence, sr, fr, alignment);
++    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
++            sequence, sr, fr, alignment);
  
      List<String> colourCommands = buildColourCommands(colourMap);
  
      StructureMappingcommandSet cs = new StructureMappingcommandSet(
              ChimeraCommands.class, null,
-             colourCommands.toArray(new String[0]));
+             colourCommands.toArray(new String[colourCommands.size()]));
  
 -    return cs;
 +    return new StructureMappingcommandSet[] { cs };
    }
  
    /**
        }
        sb.append("color ").append(colourCode).append(" ");
        firstColour = false;
-       boolean firstModelForColour = true;
-       final Map<Integer, Map<String, List<int[]>>> colourData = colourMap
-               .get(colour);
-       for (Integer model : colourData.keySet())
 -      final AtomSpecModel colourData = colourMap
 -              .get(colour);
++      final AtomSpecModel colourData = colourMap.get(colour);
+       sb.append(colourData.getAtomSpec());
+     }
+     commands.add(sb.toString());
+     return commands;
+   }
+   /**
+    * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
+    * builds a Chimera format atom spec
+    * 
+    * @param modelAndChainRanges
+    */
+   protected static String getAtomSpec(
+           Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
+   {
+     StringBuilder sb = new StringBuilder(128);
+     boolean firstModelForColour = true;
+     for (Integer model : modelAndChainRanges.keySet())
+     {
+       boolean firstPositionForModel = true;
+       if (!firstModelForColour)
        {
-         boolean firstPositionForModel = true;
-         if (!firstModelForColour)
-         {
-           sb.append("|");
-         }
-         firstModelForColour = false;
-         sb.append("#").append(model).append(":");
+         sb.append("|");
+       }
+       firstModelForColour = false;
+       sb.append("#").append(model).append(":");
  
-         final Map<String, List<int[]>> modelData = colourData.get(model);
-         for (String chain : modelData.keySet())
+       final Map<String, List<int[]>> modelData = modelAndChainRanges
+               .get(model);
+       for (String chain : modelData.keySet())
+       {
+         boolean hasChain = !"".equals(chain.trim());
+         for (int[] range : modelData.get(chain))
          {
-           boolean hasChain = !"".equals(chain.trim());
-           for (int[] range : modelData.get(chain))
+           if (!firstPositionForModel)
            {
-             if (!firstPositionForModel)
-             {
-               sb.append(",");
-             }
-             if (range[0] == range[1])
-             {
-               sb.append(range[0]);
-             }
-             else
-             {
-               sb.append(range[0]).append("-").append(range[1]);
-             }
-             if (hasChain)
-             {
-               sb.append(".").append(chain);
-             }
-             firstPositionForModel = false;
+             sb.append(",");
+           }
+           if (range[0] == range[1])
+           {
+             sb.append(range[0]);
+           }
+           else
+           {
+             sb.append(range[0]).append("-").append(range[1]);
+           }
+           if (hasChain)
+           {
+             sb.append(".").append(chain);
            }
+           firstPositionForModel = false;
          }
        }
      }
              // final colour range
              if (lastColour != null)
              {
-               addColourRange(colourMap, lastColour, pdbfnum, startPos,
-                       lastPos, lastChain);
 -              addRange(colourMap, lastColour, pdbfnum, startPos,
 -                      lastPos, lastChain);
++              addColourRange(colourMap, lastColour, pdbfnum, startPos, lastPos,
++                      lastChain);
              }
              // break;
            }
     * @param endPos
     * @param chain
     */
-   protected static void addColourRange(
-           Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> colourMap,
-           Color colour, int model, int startPos, int endPos, String chain)
 -  protected static void addRange(Map<Object, AtomSpecModel> map,
++  protected static void addColourRange(Map<Object, AtomSpecModel> map,
+           Object key, int model, int startPos, int endPos, String chain)
    {
      /*
       * Get/initialize map of data for the colour
       */
-     SortedMap<Integer, Map<String, List<int[]>>> colourData = colourMap
-             .get(colour);
-     if (colourData == null)
+     AtomSpecModel atomSpec = map.get(key);
+     if (atomSpec == null)
      {
-       colourMap
-               .put(colour,
-                       colourData = new TreeMap<Integer, Map<String, List<int[]>>>());
+       atomSpec = new AtomSpecModel();
+       map.put(key, atomSpec);
      }
  
-     /*
-      * Get/initialize map of data for the colour and model
-      */
-     Map<String, List<int[]>> modelData = colourData.get(model);
-     if (modelData == null)
+     atomSpec.addRange(model, startPos, endPos, chain);
+   }
+   /**
+    * Constructs and returns Chimera commands to set attributes on residues
+    * corresponding to features in Jalview. Attribute names are the Jalview
+    * feature type, with a "jv_" prefix.
+    * 
+    * @param ssm
+    * @param files
+    * @param seqs
+    * @param fr
+    * @param alignment
+    * @return
+    */
+   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
+           StructureSelectionManager ssm, String[] files,
+           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
+   {
+     Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
+             ssm, files, seqs, fr, alignment);
+     List<String> commands = buildSetAttributeCommands(featureMap);
+     StructureMappingcommandSet cs = new StructureMappingcommandSet(
+             ChimeraCommands.class, null,
+             commands.toArray(new String[commands.size()]));
+     return cs;
+   }
+   /**
+    * <pre>
+    * Helper method to build a map of 
+    *   { featureType, { feature value, AtomSpecModel } }
+    * </pre>
+    * 
+    * @param ssm
+    * @param files
+    * @param seqs
+    * @param fr
+    * @param alignment
+    * @return
+    */
+   protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
+           StructureSelectionManager ssm, String[] files,
+           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
+   {
+     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
+     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
+     if (visibleFeatures.isEmpty())
      {
-       colourData.put(model, modelData = new TreeMap<String, List<int[]>>());
+       return theMap;
      }
 -    
 +
-     /*
-      * Get/initialize map of data for colour, model and chain
-      */
-     List<int[]> chainData = modelData.get(chain);
-     if (chainData == null)
+     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+     {
+       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+       if (mapping == null || mapping.length < 1)
+       {
+         continue;
+       }
+       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
+       {
+         for (int m = 0; m < mapping.length; m++)
+         {
+           final SequenceI seq = seqs[pdbfnum][seqNo];
+           int sp = alignment.findIndex(seq);
+           if (mapping[m].getSequence() == seq && sp > -1)
+           {
+             /*
+              * found a sequence with a mapping to a structure;
+              * now scan its features
+              */
+             SequenceI asp = alignment.getSequenceAt(sp);
+             scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
+                     pdbfnum);
+           }
+         }
+       }
+     }
+     return theMap;
+   }
+   /**
+    * Inspect features on the sequence; for each feature that is visible,
+    * determine its mapped ranges in the structure (if any) according to the
+    * given mapping, and add them to the map
+    * 
+    * @param visibleFeatures
+    * @param mapping
+    * @param seq
+    * @param theMap
+    * @param modelNumber
+    */
+   protected static void scanSequenceFeatures(List<String> visibleFeatures,
+           StructureMapping mapping, SequenceI seq,
+           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+   {
+     SequenceFeature[] sfs = seq.getSequenceFeatures();
+     if (sfs == null)
+     {
+       return;
+     }
+     for (SequenceFeature sf : sfs)
+     {
+       String type = sf.getType();
+       /*
+        * Only copy visible features, don't copy any which originated
+        * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
+        */
+       boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+               .equals(sf.getFeatureGroup());
+       if (isFromViewer || !visibleFeatures.contains(type))
+       {
+         continue;
+       }
+       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
+               sf.getEnd());
+       if (!mappedRanges.isEmpty())
+       {
+         String value = sf.getDescription();
+         if (value == null || value.length() == 0)
+         {
+           value = type;
+         }
+         float score = sf.getScore();
+         if (score != 0f && !Float.isNaN(score))
+         {
+           value = Float.toString(score);
+         }
+         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+         if (featureValues == null)
+         {
+           featureValues = new HashMap<Object, AtomSpecModel>();
+           theMap.put(type, featureValues);
+         }
+         for (int[] range : mappedRanges)
+         {
 -          addRange(featureValues, value, modelNumber, range[0], range[1],
++          addColourRange(featureValues, value, modelNumber, range[0], range[1],
+                   mapping.getChain());
+         }
+       }
+     }
+   }
+   /**
+    * Traverse the map of features/values/models/chains/positions to construct a
+    * list of 'setattr' commands (one per distinct feature type and value).
+    * <p>
+    * The format of each command is
+    * 
+    * <pre>
+    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
+    * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
+    * </blockquote>
+    * </pre>
+    * 
+    * @param featureMap
+    * @return
+    */
+   protected static List<String> buildSetAttributeCommands(
+           Map<String, Map<Object, AtomSpecModel>> featureMap)
+   {
+     List<String> commands = new ArrayList<String>();
+     for (String featureType : featureMap.keySet())
      {
-       modelData.put(chain, chainData = new ArrayList<int[]>());
+       String attributeName = makeAttributeName(featureType);
+       /*
+        * clear down existing attributes for this feature
+        */
+       // 'problem' - sets attribute to None on all residues - overkill?
+       // commands.add("~setattr r " + attributeName + " :*");
+       Map<Object, AtomSpecModel> values = featureMap.get(featureType);
+       for (Object value : values.keySet())
+       {
+         /*
+          * for each distinct value recorded for this feature type,
+          * add a command to set the attribute on the mapped residues
+          */
+         StringBuilder sb = new StringBuilder(128);
+         sb.append("setattr r ").append(attributeName).append(" \"")
+                 .append(value.toString()).append("\" ");
+         sb.append(values.get(value).getAtomSpec());
+         commands.add(sb.toString());
+       }
      }
  
+     return commands;
+   }
+   /**
+    * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
+    * for a 'Jalview' namespace, and any non-alphanumeric character is converted
+    * to an underscore.
+    * 
+    * @param featureType
+    * @return <pre>
+    * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
+    * </pre>
+    */
+   protected static String makeAttributeName(String featureType)
+   {
+     StringBuilder sb = new StringBuilder();
+     if (featureType != null)
+     {
+       for (char c : featureType.toCharArray())
+       {
+         sb.append(Character.isLetterOrDigit(c) ? c : '_');
+       }
+     }
+     String attName = NAMESPACE_PREFIX + sb.toString();
      /*
-      * Add the start/end positions
+      * Chimera treats an attribute name ending in 'color' as colour-valued;
+      * Jalview doesn't, so prevent this by appending an underscore
       */
-     chainData.add(new int[] { startPos, endPos });
+     if (attName.toUpperCase().endsWith("COLOR"))
+     {
+       attName += "_";
+     }
+     return attName;
    }
  
  }
@@@ -735,13 -771,6 +747,14 @@@ public abstract class JalviewChimeraBin
     */
    public abstract void refreshPdbEntries();
  
 +  /**
 +   * 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
  package jalview.gui;
  
  import jalview.bin.Cache;
 -import jalview.datamodel.Alignment;
  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.DataSourceType;
@@@ -35,18 -48,35 +36,24 @@@ 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;
 -import java.io.FileOutputStream;
 -import java.io.FileReader;
  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;
  
  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;
 -import javax.swing.event.MenuEvent;
 -import javax.swing.event.MenuListener;
  
  /**
   * GUI elements for handling an external chimera display
@@@ -58,8 -88,8 +65,6 @@@ public class ChimeraViewFrame extends S
  {
    private JalviewChimeraBinding jmb;
  
--  private boolean allChainsSelected = false;
--
    private IProgressIndicator progressBar = null;
  
    /*
              .getString("label.colour_with_chimera"));
      viewerColour.setToolTipText(MessageManager
              .getString("label.let_chimera_manage_structure_colours"));
 -    helpItem.setText(MessageManager.getString("label.chimera_help"));
 -    seqColour.setSelected(jmb.isColourBySequence());
 -    viewerColour.setSelected(!jmb.isColourBySequence());
 -    if (_colourwith == null)
 -    {
 -      _colourwith = new Vector<AlignmentPanel>();
 -    }
 -    if (_alignwith == null)
 -    {
 -      _alignwith = new Vector<AlignmentPanel>();
 -    }
 -
 -    // save As not yet implemented
 -    savemenu.setVisible(false);
  
 -    ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
 -            MessageManager.getString("label.colour_by"), this, _colourwith,
 -            new ItemListener()
 -            {
 -              @Override
 -              public void itemStateChanged(ItemEvent e)
 -              {
 -                if (!seqColour.isSelected())
 -                {
 -                  seqColour.doClick();
 -                }
 -                else
 -                {
 -                  // update the Chimera display now.
 -                  seqColour_actionPerformed(null);
 -                }
 -              }
 -            });
 -    viewMenu.add(seqColourBy);
 +    helpItem.setText(MessageManager.getString("label.chimera_help"));
 +    savemenu.setVisible(false); // not yet implemented
      viewMenu.add(fitToWindow);
 -    final ItemListener handler;
 -    JMenu alpanels = new ViewSelectionMenu(
 -            MessageManager.getString("label.superpose_with"), this,
 -            _alignwith, handler = new ItemListener()
 -            {
 -              @Override
 -              public void itemStateChanged(ItemEvent e)
 -              {
 -                alignStructs.setEnabled(_alignwith.size() > 0);
 -                alignStructs.setToolTipText(MessageManager
 -                        .formatMessage(
 -                                "label.align_structures_using_linked_alignment_views",
 -                                new Object[] { new Integer(_alignwith
 -                                        .size()).toString() }));
 -              }
 -            });
 -    handler.itemStateChanged(null);
 -    viewerActionMenu.add(alpanels);
 -    viewerActionMenu.addMenuListener(new MenuListener()
 -    {
 -
 -      @Override
 -      public void menuSelected(MenuEvent e)
 -      {
 -        handler.itemStateChanged(null);
 -      }
 -
 -      @Override
 -      public void menuDeselected(MenuEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -      }
 -
 -      @Override
 -      public void menuCanceled(MenuEvent e)
 -      {
 -        // 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");
++    final JMenu fetchAttributes = new JMenu(
++            MessageManager.getString("label.fetch_chimera_attributes"));
++    fetchAttributes.setToolTipText(MessageManager
++            .getString("label.fetch_chimera_attributes_tip"));
+     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());
    }
  
    /**
     */
    void initChimera()
    {
 -    Desktop.addInternalFrame(this, jmb.getViewerTitle("Chimera", true),
 -            getBounds().width, getBounds().height);
 +    jmb.setFinishedInit(false);
-     jalview.gui.Desktop.addInternalFrame(this,
++    Desktop.addInternalFrame(this,
 +            jmb.getViewerTitle(getViewerName(), true), getBounds().width,
 +            getBounds().height);
  
      if (!jmb.launchChimera())
      {
      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
     */
    @Override
    {
      try
      {
 -      jalview.util.BrowserLauncher
 +      BrowserLauncher
                .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
--    } catch (Exception ex)
++    } catch (IOException ex)
      {
      }
    }
    }
  
    @Override
 -  protected AAStructureBindingModel getBindingModel()
 +  protected String getViewerName()
    {
 -    return jmb;
 +    return "Chimera";
 +  }
++
++  @Override
++  public void updateTitleAndMenus()
++  {
++    super.updateTitleAndMenus();
++    viewerActionMenu.setVisible(true);
+   }
  }
@@@ -49,70 -48,104 +48,105 @@@ public class ChimeraCommandsTes
    public void testBuildColourCommands()
    {
  
-     Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> map = new LinkedHashMap<Color, SortedMap<Integer, Map<String, List<int[]>>>>();
+     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");
 +    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");
++    ChimeraCommands.addColourRange(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");
++    ChimeraCommands.addColourRange(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");
++    ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A");
+     // same feature value, contiguous range
 -    ChimeraCommands.addRange(featureValues, "X", 0, 21, 25, "A");
++    ChimeraCommands.addColourRange(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");
++    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B");
+     // same feature value and chain, different model
 -    ChimeraCommands.addRange(featureValues, "X", 1, 26, 30, "A");
++    ChimeraCommands.addColourRange(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");
++    ChimeraCommands.addColourRange(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");
++    ChimeraCommands.addColourRange(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_");
    }
  }
@@@ -60,6 -60,6 +60,11 @@@ import org.testng.annotations.Test
   */
  public class AnnotationChooserTest
  {
++  /*
++   * number of automatically computed annotation rows
++   * (Conservation, Quality, Consensus, Occupancy)
++   */
++  private static final int AUTOCALCD = 4;
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
  
      types = AnnotationChooser.getAnnotationTypes(
              parentPanel.getAlignment(), false);
--    assertEquals("Not six annotation types", 6, types.size());
++    assertEquals("Not six annotation types", 7, types.size());
      assertTrue("IUPRED missing", types.contains("IUPRED"));
      assertTrue("JMol missing", types.contains("JMol"));
      assertTrue("Beauty missing", types.contains("Beauty"));
      assertTrue("Consensus missing", types.contains("Consensus"));
      assertTrue("Quality missing", types.contains("Quality"));
      assertTrue("Conservation missing", types.contains("Conservation"));
++    assertTrue("Occupancy missing", types.contains("Occupancy"));
    }
  
    /**
      AlignmentAnnotation[] anns = parentPanel.getAlignment()
              .getAlignmentAnnotation();
  
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertTrue(anns[AUTOCALCD + 4].visible); // JMol for seq1
  
      setSelected(getTypeCheckbox("JMol"), true);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertFalse(anns[5].visible); // JMol for seq3 - not selected but hidden
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertFalse(anns[7].visible); // JMol for seq1 - selected and hidden
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertFalse(anns[6].visible); // JMol for seq3 - not selected but hidden
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertFalse(anns[8].visible); // JMol for seq1 - selected and hidden
    }
  
    /**
      AlignmentAnnotation[] anns = parentPanel.getAlignment()
              .getAlignmentAnnotation();
  
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[AUTOCALCD + 4].visible); // JMol for seq1
  
      setSelected(getTypeCheckbox("JMol"), true);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertTrue(anns[5].visible); // JMol for seq3 not in selection group
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertFalse(anns[7].visible); // JMol for seq1 in selection group
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertTrue(anns[6].visible); // JMol for seq3 not in selection group
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertFalse(anns[8].visible); // JMol for seq1 in selection group
    }
  
    /**
  
      // select JMol - all hidden
      setSelected(typeCheckbox, true);
--    assertFalse(anns[5].visible); // JMol for seq3
--    assertFalse(anns[7].visible); // JMol for seq1
++    assertFalse(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertFalse(anns[AUTOCALCD + 4].visible); // JMol for seq1
  
      // deselect JMol - all unhidden
      setSelected(typeCheckbox, false);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertTrue(anns[6].visible); // JMol for seq3
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertTrue(anns[8].visible); // JMol for seq1
    }
  
    /**
      setSelected(allSequencesCheckbox, true);
      setSelected(hideCheckbox, true);
      setSelected(getTypeCheckbox("JMol"), true);
--    assertFalse(anns[5].visible); // JMol for seq3
--    assertFalse(anns[7].visible); // JMol for seq1
++    assertFalse(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertFalse(anns[AUTOCALCD + 4].visible); // JMol for seq1
      // ...now show them...
      setSelected(showCheckbox, true);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertTrue(anns[6].visible); // JMol for seq3
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertTrue(anns[8].visible); // JMol for seq1
    }
  
    /**
      setSelected(hideCheckbox, true);
      setSelected(getTypeCheckbox("JMol"), true);
  
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertFalse(anns[7].visible); // JMol for seq1
++    assertTrue(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertFalse(anns[AUTOCALCD + 4].visible); // JMol for seq1
      // ...now show them...
      setSelected(showCheckbox, true);
  
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertTrue(anns[6].visible); // JMol for seq3
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertTrue(anns[8].visible); // JMol for seq1
    }
  
    /**
      final Checkbox typeCheckbox = getTypeCheckbox("JMol");
      // select JMol - all shown
      setSelected(typeCheckbox, true);
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertTrue(anns[AUTOCALCD + 4].visible); // JMol for seq1
  
      // deselect JMol - all hidden
      setSelected(typeCheckbox, false);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertFalse(anns[5].visible); // JMol for seq3
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertFalse(anns[7].visible); // JMol for seq1
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertFalse(anns[6].visible); // JMol for seq3
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertFalse(anns[8].visible); // JMol for seq1
    }
  
    /**
  
      // select JMol - should remain visible
      setSelected(getTypeCheckbox("JMol"), true);
--    assertTrue(anns[5].visible); // JMol for seq3
--    assertTrue(anns[7].visible); // JMol for seq1
++    assertTrue(anns[AUTOCALCD + 2].visible); // JMol for seq3
++    assertTrue(anns[AUTOCALCD + 4].visible); // JMol for seq1
  
      // deselect JMol - should be hidden for selected sequences only
      setSelected(getTypeCheckbox("JMol"), false);
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertTrue(anns[3].visible); // IUPred for seq0
--    assertTrue(anns[4].visible); // Beauty
--    assertTrue(anns[5].visible); // JMol for seq3 not in selection group
--    assertTrue(anns[6].visible); // IUPRED for seq2
--    assertFalse(anns[7].visible); // JMol for seq1 in selection group
++    assertTrue(anns[3].visible); // Occupancy
++    assertTrue(anns[4].visible); // IUPred for seq0
++    assertTrue(anns[5].visible); // Beauty
++    assertTrue(anns[6].visible); // JMol for seq3 not in selection group
++    assertTrue(anns[7].visible); // IUPRED for seq2
++    assertFalse(anns[8].visible); // JMol for seq1 in selection group
    }
  
    /**
  
      AlignmentAnnotation[] anns = parentPanel.getAlignment()
              .getAlignmentAnnotation();
--    // remember 3 annotations to skip (Conservation/Quality/Consensus)
--    assertFalse(testee.isInActionScope(anns[3]));
--    assertFalse(testee.isInActionScope(anns[4]));
--    assertFalse(testee.isInActionScope(anns[5]));
--    assertTrue(testee.isInActionScope(anns[6]));
--    assertTrue(testee.isInActionScope(anns[7]));
++    assertFalse(testee.isInActionScope(anns[AUTOCALCD]));
++    assertFalse(testee.isInActionScope(anns[AUTOCALCD + 1]));
++    assertFalse(testee.isInActionScope(anns[AUTOCALCD + 2]));
++    assertTrue(testee.isInActionScope(anns[AUTOCALCD + 3]));
++    assertTrue(testee.isInActionScope(anns[AUTOCALCD + 4]));
    }
  
    /**
  
      AlignmentAnnotation[] anns = parentPanel.getAlignment()
              .getAlignmentAnnotation();
--    // remember 3 annotations to skip (Conservation/Quality/Consensus)
--    assertTrue(testee.isInActionScope(anns[3]));
--    assertTrue(testee.isInActionScope(anns[4]));
--    assertTrue(testee.isInActionScope(anns[5]));
--    assertFalse(testee.isInActionScope(anns[6]));
--    assertFalse(testee.isInActionScope(anns[7]));
++    assertTrue(testee.isInActionScope(anns[AUTOCALCD]));
++    assertTrue(testee.isInActionScope(anns[AUTOCALCD + 1]));
++    assertTrue(testee.isInActionScope(anns[AUTOCALCD + 2]));
++    assertFalse(testee.isInActionScope(anns[AUTOCALCD + 3]));
++    assertFalse(testee.isInActionScope(anns[AUTOCALCD + 4]));
    }
  
    /**
      assertTrue(anns[0].visible); // Conservation
      assertTrue(anns[1].visible); // Quality
      assertTrue(anns[2].visible); // Consensus
--    assertFalse(anns[3].visible); // IUPRED
--    assertTrue(anns[4].visible); // Beauty (not seq-related)
--    assertFalse(anns[5].visible); // JMol
--    assertFalse(anns[6].visible); // IUPRED
--    assertFalse(anns[7].visible); // JMol
++    assertTrue(anns[3].visible); // Occupancy
++    assertFalse(anns[4].visible); // IUPRED
++    assertTrue(anns[5].visible); // Beauty (not seq-related)
++    assertFalse(anns[6].visible); // JMol
++    assertFalse(anns[7].visible); // IUPRED
++    assertFalse(anns[8].visible); // JMol
  
      // reset - should all be visible
      testee.resetOriginalState();