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

@@@ -61,6 -61,7 +61,6 @@@ action.set_as_reference = Set as Refere
  action.remove = Remove
  action.remove_redundancy = Remove Redundancy...
  action.pairwise_alignment = Pairwise Alignment
 -action.by_rna_helixes = By RNA Helices
  action.user_defined = User Defined...
  action.by_conservation = By Conservation
  action.wrap = Wrap
@@@ -138,8 -139,7 +138,8 @@@ action.view_flanking_regions = Show fla
  label.view_flanking_regions = Show sequence data either side of the subsequences involved in this alignment
  label.structures_manager = Structures Manager
  label.nickname = Nickname:
 -label.url = URL:
 +label.url = URL
 +label.url\: = URL:
  label.input_file_url = Enter URL or Input File
  label.select_feature = Select feature
  label.name = Name
@@@ -179,30 -179,27 +179,30 @@@ label.score_model_conservation = Physic
  label.score_model_enhconservation = Physicochemical property conservation
  label.status_bar = Status bar
  label.out_to_textbox = Output to Textbox
 -label.clustalx = Clustalx
 +# delete Clustal - use FileFormat name instead
  label.clustal = Clustal
 -label.zappo = Zappo
 -label.taylor = Taylor
 +# label.colourScheme_<schemeName> as in JalviewColourScheme
 +label.colourScheme_clustal = Clustalx
 +label.colourScheme_blosum62 = BLOSUM62 Score
 +label.colourScheme_%_identity = Percentage Identity
 +label.colourScheme_zappo = Zappo
 +label.colourScheme_taylor = Taylor
 +label.colourScheme_hydrophobic = Hydrophobicity
 +label.colourScheme_helix_propensity = Helix Propensity
 +label.colourScheme_strand_propensity = Strand Propensity
 +label.colourScheme_turn_propensity = Turn Propensity
 +label.colourScheme_buried_index = Buried Index
 +label.colourScheme_purine/pyrimidine = Purine/Pyrimidine
 +label.colourScheme_nucleotide = Nucleotide
 +label.colourScheme_t-coffee_scores = T-Coffee Scores
 +label.colourScheme_rna_helices = By RNA Helices
  label.blc = BLC
  label.fasta = Fasta
  label.msf = MSF
  label.pfam = PFAM
  label.pileup = Pileup
  label.pir = PIR
 -label.hydrophobicity = Hydrophobicity
 -label.helix_propensity = Helix Propensity
 -label.strand_propensity = Strand Propensity
 -label.turn_propensity = Turn Propensity
 -label.buried_index = Buried Index
 -label.purine_pyrimidine = Purine/Pyrimidine
 -label.percentage_identity = Percentage Identity
 -label.blosum62 = BLOSUM62
 -label.blosum62_score = BLOSUM62 Score
 -label.tcoffee_scores = T-Coffee Scores
 -label.average_distance_bloslum62 = Average Distance Using BLOSUM62
 +label.average_distance_blosum62 = Average Distance Using BLOSUM62
  label.neighbour_blosum62 = Neighbour Joining Using BLOSUM62
  label.show_annotations = Show annotations
  label.hide_annotations = Hide annotations
@@@ -214,7 -211,7 +214,7 @@@ label.hide_all = Hide al
  label.add_reference_annotations = Add reference annotations
  label.find_tip = Search alignment, selection or sequence ids for a subsequence (ignoring gaps).<br>Accepts regular expressions - search Help for 'regex' for details.
  label.colour_text = Colour Text
 -label.show_non_conversed = Show nonconserved
 +label.show_non_conserved = Show nonconserved
  label.overview_window = Overview Window
  label.none = None
  label.above_identity_threshold = Above Identity Threshold
@@@ -326,7 -323,7 +326,7 @@@ label.size = Size
  label.style = Style:
  label.calculating = Calculating....
  label.modify_conservation_visibility = Modify conservation visibility
 -label.colour_residues_above_occurence = Colour residues above % occurence
 +label.colour_residues_above_occurrence = Colour residues above % occurrence
  label.set_this_label_text = set this label text
  label.sequences_from = Sequences from {0}
  label.successfully_loaded_file  = Successfully loaded file {0}
@@@ -414,6 -411,7 +414,6 @@@ label.couldnt_import_as_vamsas_session 
  label.vamsas_document_import_failed = Vamsas Document Import Failed
  label.couldnt_locate = Couldn't locate {0}
  label.url_not_found = URL not found
 -label.no_link_selected = No link selected
  label.new_sequence_url_link = New sequence URL link
  label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
  label.wrapped_view_no_edit = Wrapped view - no edit
@@@ -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
@@@ -672,6 -670,8 +674,6 @@@ action.set_text_colour = Text Colour..
  label.structure = Structure
  label.show_pdbstruct_dialog = 3D Structure Data...
  label.view_rna_structure = VARNA 2D Structure
 -label.clustalx_colours = Clustalx colours
 -label.above_identity_percentage = Above % Identity
  label.create_sequence_details_report_annotation_for = Annotation for {0}
  label.sequence_details_for = Sequence Details for {0}
  label.sequence_name = Sequence Name
@@@ -716,13 -716,14 +718,15 @@@ 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
  label.save_colour_scheme_with_unique_name_added_to_colour_menu = Save your colour scheme with a unique name and it will be added to the Colour menu
  label.case_sensitive = Case Sensitive
 -label.lower_case_colour = Lower Case Colour
 +label.lower_case_colour = Colour All Lower Case
 +label.lower_case_tip = Chosen colour applies to all lower case symbols
  label.index_by_host = Index by Host
  label.index_by_type = Index by Type
  label.enable_jabaws_services = Enable JABAWS Services
@@@ -771,7 -772,7 +775,7 @@@ label.original_data_for_params = Origin
  label.points_for_params = Points for {0}
  label.transformed_points_for_params = Transformed points for {0}
  label.graduated_color_for_params = Graduated Feature Colour for {0}
 -label.select_backgroud_colour = Select Background Colour
 +label.select_background_colour = Select Background Colour
  label.invalid_font = Invalid Font
  label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
  label.separate_multiple_query_values = Enter one or more {0}s separated by a semi-colon ";"
@@@ -961,6 -962,7 +965,6 @@@ error.implementation_error_maplist_is_n
  error.implementation_error_cannot_have_null_alignment = Implementation error: Cannot have null alignment property key
  error.implementation_error_null_fileparse = Implementation error. Null FileParse in copy constructor
  error.implementation_error_cannot_map_alignment_sequences = IMPLEMENTATION ERROR: Cannot map an alignment of sequences from different datasets into a single alignment in the vamsas document.
 -error.implementation_error_cannot_duplicate_colour_scheme = Serious implementation error: cannot duplicate colourscheme {0}
  error.implementation_error_structure_selection_manager_null = Implementation error. Structure selection manager's context is 'null'
  exception.ssm_context_is_null = SSM context is null
  error.idstring_seqstrings_only_one_per_sequence = idstrings and seqstrings contain one string each per sequence
@@@ -1025,6 -1027,7 +1029,6 @@@ exception.replace_null_regex_pointer = 
  exception.bad_pattern_to_regex_perl_code = bad pattern to Regex.perlCode: {0}
  exception.no_stub_implementation_for_interface = There is no stub implementation for the interface: {0}
  exception.cannot_set_endpoint_address_unknown_port = Cannot set Endpoint Address for Unknown Port {0}
 -exception.querying_matching_opening_parenthesis_for_non_closing_parenthesis = Querying matching opening parenthesis for non-closing parenthesis character {0}
  exception.mismatched_unseen_closing_char = Mismatched (unseen) closing character {0}
  exception.mismatched_closing_char = Mismatched closing character {0}
  exception.mismatched_opening_char = Mismatched opening character {0} at {1}
@@@ -1035,6 -1038,7 +1039,6 @@@ exception.couldnt_parse_responde_from_a
  exception.application_test_npe = Application test: throwing an NullPointerException It should arrive at the console
  exception.overwriting_vamsas_id_binding = Overwriting vamsas id binding
  exception.overwriting_jalview_id_binding = Overwriting jalview id binding
 -error.implementation_error_unknown_file_format_string = Implementation error: Unknown file format string
  exception.failed_to_resolve_gzip_stream = Failed to resolve GZIP stream
  exception.problem_opening_file_also_tried = Problem opening {0} (also tried {1}) : {2}
  exception.problem_opening_file = Problem opening {0} : {1}
@@@ -1230,6 -1234,7 +1234,6 @@@ action.export_hidden_columns = Export H
  action.export_hidden_sequences = Export Hidden Sequences
  action.export_features = Export Features
  label.export_settings = Export Settings
 -label.save_as_biojs_html = Save as BioJs HTML
  label.pdb_web-service_error = PDB Web-service Error
  label.structure_chooser_manual_association = Structure Chooser - Manual association
  label.structure_chooser_filter_time = Structure Chooser - Filter time ({0})
@@@ -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
diff --combined src/MCview/PDBChain.java
@@@ -39,6 -39,8 +39,8 @@@ import java.util.Vector
  
  public class PDBChain
  {
+   public static final String RESNUM_FEATURE = "RESNUM";
    /**
     * SequenceFeature group for PDB File features added to sequences
     */
          Residue tmpres = residues.lastElement();
          Atom tmpat = tmpres.atoms.get(0);
          // Make A new SequenceFeature for the current residue numbering
-         SequenceFeature sf = new SequenceFeature("RESNUM", tmpat.resName
+         SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE, tmpat.resName
                  + ":" + tmpat.resNumIns + " " + pdbid + id, "", offset
                  + count, offset + count, pdbid);
          resFeatures.addElement(sf);
        try
        {
          index = ResidueProperties.aa3Hash.get(b.at1.resName).intValue();
 -        b.startCol = cs.findColour(ResidueProperties.aa[index].charAt(0));
 +        b.startCol = cs.findColour(ResidueProperties.aa[index].charAt(0),
 +                0, null, null, 0f);
  
          index = ResidueProperties.aa3Hash.get(b.at2.resName).intValue();
 -        b.endCol = cs.findColour(ResidueProperties.aa[index].charAt(0));
 +        b.endCol = cs.findColour(ResidueProperties.aa[index].charAt(0), 0,
 +                null, null, 0f);
  
        } catch (Exception e)
        {
@@@ -42,6 -42,7 +42,7 @@@ import jalview.util.Comparison
  import jalview.util.DBRefUtils;
  import jalview.util.MapList;
  import jalview.util.MappingUtils;
+ import jalview.util.RangeComparator;
  import jalview.util.StringUtils;
  
  import java.io.UnsupportedEncodingException;
@@@ -60,7 -61,6 +61,7 @@@ import java.util.Map
  import java.util.Map.Entry;
  import java.util.NoSuchElementException;
  import java.util.Set;
 +import java.util.SortedMap;
  import java.util.TreeMap;
  
  /**
@@@ -2265,14 -2265,7 +2266,7 @@@ public class AlignmentUtil
       * ranges are assembled in order. Other cases should not use this method,
       * but instead construct an explicit mapping for CDS (e.g. EMBL parsing).
       */
-     Collections.sort(result, new Comparator<int[]>()
-     {
-       @Override
-       public int compare(int[] o1, int[] o2)
-       {
-         return Integer.compare(o1[0], o2[0]);
-       }
-     });
+     Collections.sort(result, new RangeComparator(true));
      return result;
    }
  
     * @param unmapped
     * @return
     */
 -  static Map<Integer, Map<SequenceI, Character>> buildMappedColumnsMap(
 +  static SortedMap<Integer, Map<SequenceI, Character>> buildMappedColumnsMap(
            AlignmentI unaligned, AlignmentI aligned, List<SequenceI> unmapped)
    {
      /*
       * {unalignedSequence, characterPerSequence} at that position.
       * TreeMap keeps the entries in ascending column order. 
       */
 -    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
 +    SortedMap<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
  
      /*
       * record any sequences that have no mapping so can't be realigned
@@@ -31,7 -31,6 +31,7 @@@ import jalview.datamodel.SearchResultsI
  import jalview.datamodel.SequenceCollectionI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
 +import jalview.renderer.ResidueShaderI;
  import jalview.schemes.ColourSchemeI;
  
  import java.awt.Color;
@@@ -80,14 -79,6 +80,14 @@@ public interface AlignViewportI extend
  
    ColourSchemeI getGlobalColourScheme();
  
 +  /**
 +   * Returns an object that describes colouring (including any thresholding or
 +   * fading) of the alignment
 +   * 
 +   * @return
 +   */
 +  ResidueShaderI getResidueShading();
 +
    AlignmentI getAlignment();
  
    ColumnSelection getColumnSelection();
    AlignmentAnnotation getAlignmentConsensusAnnotation();
  
    /**
+    * get the container for alignment gap annotation
+    * 
+    * @return
+    */
+   AlignmentAnnotation getAlignmentGapAnnotation();
+   /**
     * get the container for cDNA complement consensus annotation
     * 
     * @return
  
    /**
     * 
 -   * @return the alignment annotatino row for the structure consensus
 +   * @return the alignment annotation row for the structure consensus
     *         calculation
     */
    AlignmentAnnotation getAlignmentStrucConsensusAnnotation();
    void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus);
  
    /**
 -   * set global colourscheme
 +   * Sets the colour scheme for the background alignment (as distinct from
 +   * sub-groups, which may have their own colour schemes). A null value is used
 +   * for no residue colour (white).
     * 
 -   * @param rhc
 +   * @param cs
     */
 -  void setGlobalColourScheme(ColourSchemeI rhc);
 +  void setGlobalColourScheme(ColourSchemeI cs);
  
    Map<SequenceI, SequenceCollectionI> getHiddenRepSequences();
  
@@@ -23,6 -23,7 +23,7 @@@ package jalview.ext.rbvi.chimera
  import jalview.api.FeatureRenderer;
  import jalview.api.SequenceRenderer;
  import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.structure.StructureMapping;
  import jalview.structure.StructureMappingcommandSet;
@@@ -32,11 -33,10 +33,10 @@@ import jalview.util.Comparison
  
  import java.awt.Color;
  import java.util.ArrayList;
+ import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
- import java.util.SortedMap;
- import java.util.TreeMap;
  
  /**
   * Routines for generating Chimera commands for Jalview/Chimera binding
  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 };
    }
  
    /**
     * 'color' commands (one per distinct colour used). The format of each command
     * is
     * 
-    * <blockquote> color colorname #modelnumber:range.chain e.g. color #00ff00
-    * #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
-    * 
-    * @see http 
-    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec
-    *      .html </pre>
+    * <pre>
+    * <blockquote> 
+    * color colorname #modelnumber:range.chain 
+    * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
+    * </blockquote>
+    * </pre>
     * 
     * @param colourMap
     * @return
     */
    protected static List<String> buildColourCommands(
-           Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> colourMap)
+           Map<Object, AtomSpecModel> colourMap)
    {
      /*
       * This version concatenates all commands into a single String (semi-colon
      List<String> commands = new ArrayList<String>();
      StringBuilder sb = new StringBuilder(256);
      boolean firstColour = true;
-     for (Color colour : colourMap.keySet())
+     for (Object key : colourMap.keySet())
      {
+       Color colour = (Color) key;
        String colourCode = ColorUtils.toTkCode(colour);
        if (!firstColour)
        {
        }
        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;
          }
        }
      }
-     commands.add(sb.toString());
-     return commands;
+     return sb.toString();
    }
  
    /**
     * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
     * </pre>
     */
-   protected static Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> buildColoursMap(
+   protected static Map<Object, AtomSpecModel> buildColoursMap(
            StructureSelectionManager ssm, String[] files,
            SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
            AlignmentI alignment)
    {
-     Map<Color, SortedMap<Integer, Map<String, List<int[]>>>> colourMap = new LinkedHashMap<Color, SortedMap<Integer, Map<String, List<int[]>>>>();
+     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
      Color lastColour = null;
      for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
      {
                {
                  if (startPos != -1)
                  {
 -                  addRange(colourMap, lastColour, pdbfnum, startPos,
 +                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
                            lastPos, lastChain);
                  }
                  startPos = pos;
              // 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;
            }
    /**
     * Helper method to add one contiguous colour range to the colour map.
     * 
-    * @param colourMap
-    * @param colour
+    * @param map
+    * @param key
     * @param model
     * @param startPos
     * @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;
    }
  
  }
@@@ -28,6 -28,9 +28,9 @@@ 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;
@@@ -40,8 -43,13 +43,13 @@@ import jalview.structures.models.AAStru
  import jalview.util.MessageManager;
  
  import java.awt.Color;
+ import java.io.File;
+ import java.io.FileOutputStream;
+ import java.io.IOException;
+ import java.io.PrintWriter;
  import java.net.BindException;
  import java.util.ArrayList;
+ import java.util.Collections;
  import java.util.Hashtable;
  import java.util.LinkedHashMap;
  import java.util.List;
@@@ -54,6 -62,8 +62,8 @@@ import ext.edu.ucsf.rbvi.strucviz2.Stru
  
  public abstract class JalviewChimeraBinding extends AAStructureBindingModel
  {
+   public static final String CHIMERA_FEATURE_GROUP = "Chimera";
    // Chimera clause to exclude alternate locations in atom selection
    private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
  
     */
    private boolean loadingFinished = true;
  
 -  public String fileLoadingError;
 -
    /*
     * Map of ChimeraModel objects keyed by PDB full local file name
     */
    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;
  
    /*
    }
  
    /**
 -   * Construct a title string for the viewer window based on the data Jalview
 -   * knows about
 -   * 
 -   * @param verbose
 -   * @return
 -   */
 -  public String getViewerTitle(boolean verbose)
 -  {
 -    return getViewerTitle("Chimera", verbose);
 -  }
 -
 -  /**
     * Tells Chimera to display only the specified chains
     * 
     * @param toshow
        chimeraListener.shutdown();
        chimeraListener = null;
      }
-     lastCommand = null;
      viewer = null;
  
      if (chimeraMonitor != null)
      releaseUIResources();
    }
  
 +  @Override
    public void colourByChain()
    {
      colourBySequence = false;
     * <li>all others - white</li>
     * </ul>
     */
 +  @Override
    public void colourByCharge()
    {
      colourBySequence = false;
     * @param _hiddenCols
     *          an array of corresponding hidden columns for each alignment
     */
 +  @Override
    public void superposeStructures(AlignmentI[] _alignment,
            int[] _refStructure, ColumnSelection[] _hiddenCols)
    {
    }
  
    /**
-    * 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;
    }
  
    /**
            String progressMsg);
  
    /**
 -   * colour any structures associated with sequences in the given alignment
 -   * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
 -   * if colourBySequence is enabled.
 +   * Sends a set of colour commands to the structure viewer
 +   * 
 +   * @param colourBySequenceCommands
     */
 -  public void colourBySequence(boolean showFeatures,
 -          jalview.api.AlignmentViewPanel alignmentv)
 +  @Override
 +  protected void colourBySequence(
 +          StructureMappingcommandSet[] colourBySequenceCommands)
    {
 -    if (!colourBySequence || !loadingFinished)
 +    for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
      {
 -      return;
 -    }
 -    if (getSsm() == null)
 -    {
 -      return;
 -    }
 -    String[] files = getPdbFile();
 -
 -    SequenceRenderer sr = getSequenceRenderer(alignmentv);
 -
 -    FeatureRenderer fr = null;
 -    if (showFeatures)
 -    {
 -      fr = getFeatureRenderer(alignmentv);
 +      for (String command : cpdbbyseq.commands)
 +      {
 +        sendAsynchronousCommand(command, COLOURING_CHIMERA);
 +      }
      }
 -    AlignmentI alignment = alignmentv.getAlignment();
 +  }
  
 -    StructureMappingcommandSet colourBySequenceCommands = ChimeraCommands
 -            .getColourBySequenceCommand(getSsm(), files, getSequence(), sr,
 -                    fr, alignment);
 -    for (String command : colourBySequenceCommands.commands)
 -    {
 -      sendAsynchronousCommand(command, COLOURING_CHIMERA);
 -    }
 +  /**
 +   * @param files
 +   * @param sr
 +   * @param fr
 +   * @param alignment
 +   * @return
 +   */
 +  @Override
 +  protected StructureMappingcommandSet[] getColourBySequenceCommands(
 +          String[] files, SequenceRenderer sr, FeatureRenderer fr,
 +          AlignmentI alignment)
 +  {
 +    return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
 +            getSequence(), sr, fr, alignment);
    }
  
    /**
    // //////////////////////////
  
    /**
 -   * returns the current featureRenderer that should be used to colour the
 -   * structures
 -   * 
 -   * @param alignment
 -   * 
 -   * @return
 -   */
 -  public abstract FeatureRenderer getFeatureRenderer(
 -          AlignmentViewPanel alignment);
 -
 -  /**
     * instruct the Jalview binding to update the pdbentries vector if necessary
     * prior to matching the viewer's contents to the list of structure files
     * Jalview knows about.
     */
    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
    }
  
    /**
 -   * returns the current sequenceRenderer that should be used to colour the
 -   * structures
 -   * 
 -   * @param alignment
 -   * 
 -   * @return
 -   */
 -  public abstract SequenceRenderer getSequenceRenderer(
 -          AlignmentViewPanel alignment);
 -
 -  /**
     * Construct and send a command to highlight zero, one or more atoms. We do
     * this by sending an "rlabel" command to show the residue label at that
     * position.
       * 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 loadNotifiesHandled;
    }
  
 +  @Override
    public void setJalviewColourScheme(ColourSchemeI cs)
    {
      colourBySequence = false;
  
      List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
              false);
 -    for (String res : residueSet)
 +    for (String resName : residueSet)
      {
 -      Color col = cs.findColour(res.charAt(0));
 +      char res = resName.length() == 3 ? ResidueProperties
 +              .getSingleCharacterCode(resName) : resName.charAt(0);
 +      Color col = cs.findColour(res, 0, null, null, 0f);
        command.append("color " + col.getRed() / normalise + ","
                + col.getGreen() / normalise + "," + col.getBlue()
 -              / normalise + " ::" + res + ";");
 +              / normalise + " ::" + resName + ";");
      }
  
      sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
     *      .html
     * @param col
     */
 +  @Override
    public void setBackgroundColour(Color col)
    {
      viewerCommandHistory(false);
    }
  
    /**
+    * Returns a list of chains mapped in this viewer. Note this list is not
+    * currently scoped per structure.
+    * 
+    * @return
+    */
+   @Override
+   public List<String> getChainNames()
+   {
+     return chainNames;
+   }
+   /**
     * Send a 'focus' command to Chimera to recentre the visible display
     */
    public void focusView()
      }
    }
  
+   /**
+    * Constructs and send commands to Chimera to set attributes on residues for
+    * features visible in Jalview
+    * 
+    * @param avp
+    */
+   public void sendFeaturesToViewer(AlignmentViewPanel avp)
+   {
+     // TODO refactor as required to pull up to an interface
+     AlignmentI alignment = avp.getAlignment();
+     FeatureRenderer fr = getFeatureRenderer(avp);
  
-   @Override
-   public List<String> getChainNames()
+     /*
+      * fr is null if feature display is turned off
+      */
+     if (fr == null)
+     {
+       return;
+     }
+     String[] files = getPdbFile();
+     if (files == null)
+     {
+       return;
+     }
+     StructureMappingcommandSet commandSet = ChimeraCommands
+             .getSetAttributeCommandsForFeatures(getSsm(), files,
+                     getSequence(), fr, alignment);
+     String[] commands = commandSet.commands;
+     if (commands.length > 10)
+     {
+       sendCommandsByFile(commands);
+     }
+     else
+     {
+       for (String command : commands)
+       {
+         sendAsynchronousCommand(command, null);
+       }
+     }
+   }
+   /**
+    * Write commands to a temporary file, and send a command to Chimera to open
+    * the file as a commands script. For use when sending a large number of
+    * separate commands would overload the REST interface mechanism.
+    * 
+    * @param commands
+    */
+   protected void sendCommandsByFile(String[] commands)
    {
-     return chainNames;
+     try
+     {
+       File tmp = File.createTempFile("chim", ".com");
+       tmp.deleteOnExit();
+       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
+       for (String command : commands)
+       {
+         out.println(command);
+       }
+       out.flush();
+       out.close();
+       String path = tmp.getAbsolutePath();
+       sendAsynchronousCommand("open cmd:" + path, null);
+     } catch (IOException e)
+     {
+       System.err
+               .println("Sending commands to Chimera via file failed with "
+                       + e.getMessage());
+     }
    }
  
+   /**
+    * Get Chimera residues which have the named attribute, find the mapped
+    * positions in the Jalview sequence(s), and set as sequence features
+    * 
+    * @param attName
+    * @param alignmentPanel
+    */
+   public void copyStructureAttributesToFeatures(String attName,
+           AlignmentViewPanel alignmentPanel)
+   {
+     // todo pull up to AAStructureBindingModel (and interface?)
+     /*
+      * ask Chimera to list residues with the attribute, reporting its value
+      */
+     // this alternative command
+     // list residues spec ':*/attName' attr attName
+     // doesn't report 'None' values (which is good), but
+     // fails for 'average.bfactor' (which is bad):
+     String cmd = "list residues attr '" + attName + "'";
+     List<String> residues = sendChimeraCommand(cmd, true);
+     boolean featureAdded = createFeaturesForAttributes(attName, residues);
+     if (featureAdded)
+     {
+       alignmentPanel.getFeatureRenderer().featuresAdded();
+     }
+   }
+   /**
+    * Create features in Jalview for the given attribute name and structure
+    * residues.
+    * 
+    * <pre>
+    * The residue list should be 0, 1 or more reply lines of the format: 
+    *     residue id #0:5.A isHelix -155.000836316 index 5 
+    * or 
+    *     residue id #0:6.A isHelix None
+    * </pre>
+    * 
+    * @param attName
+    * @param residues
+    * @return
+    */
+   protected boolean createFeaturesForAttributes(String attName,
+           List<String> residues)
+   {
+     boolean featureAdded = false;
+     String featureGroup = getViewerFeatureGroup();
+     for (String residue : residues)
+     {
+       AtomSpec spec = null;
+       String[] tokens = residue.split(" ");
+       if (tokens.length < 5)
+       {
+         continue;
+       }
+       String atomSpec = tokens[2];
+       String attValue = tokens[4];
+       /*
+        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
+        */
+       if ("None".equalsIgnoreCase(attValue)
+               || "False".equalsIgnoreCase(attValue))
+       {
+         continue;
+       }
+       try
+       {
+         spec = AtomSpec.fromChimeraAtomspec(atomSpec);
+       } catch (IllegalArgumentException e)
+       {
+         System.err.println("Problem parsing atomspec " + atomSpec);
+         continue;
+       }
+       String chainId = spec.getChain();
+       String description = attValue;
+       float score = Float.NaN;
+       try
+       {
+         score = Float.valueOf(attValue);
+         description = chainId;
+       } catch (NumberFormatException e)
+       {
+         // was not a float value
+       }
+       String pdbFile = getPdbFileForModel(spec.getModelNumber());
+       spec.setPdbFile(pdbFile);
+       List<AtomSpec> atoms = Collections.singletonList(spec);
+       /*
+        * locate the mapped position in the alignment (if any)
+        */
+       SearchResults sr = getSsm()
+               .findAlignmentPositionsForStructurePositions(atoms);
+       /*
+        * expect one matched alignment position, or none 
+        * (if the structure position is not mapped)
+        */
+       for (SearchResultMatchI m : sr.getResults())
+       {
+         SequenceI seq = m.getSequence();
+         int start = m.getStart();
+         int end = m.getEnd();
+         SequenceFeature sf = new SequenceFeature(attName, description,
+                 start, end, score, featureGroup);
+         // todo: should SequenceFeature have an explicit property for chain?
+         // note: repeating the action shouldn't duplicate features
+         featureAdded |= seq.addSequenceFeature(sf);
+       }
+     }
+     return featureAdded;
+   }
+   /**
+    * Answers the feature group name to apply to features created in Jalview from
+    * Chimera attributes
+    * 
+    * @return
+    */
+   protected String getViewerFeatureGroup()
+   {
+     // todo pull up to interface
+     return CHIMERA_FEATURE_GROUP;
+   }
    public Hashtable<String, String> getChainFile()
    {
      return chainFile;
@@@ -26,6 -26,8 +26,8 @@@ import jalview.datamodel.SequenceGroup
  import jalview.schemes.AnnotationColourGradient;
  import jalview.util.MessageManager;
  
+ import java.awt.event.FocusAdapter;
+ import java.awt.event.FocusEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.util.Vector;
@@@ -134,6 -136,14 +136,14 @@@ public abstract class AnnotationRowFilt
    {
      this.av = av;
      this.ap = ap;
+     thresholdValue.addFocusListener(new FocusAdapter()
+     {
+       @Override
+       public void focusLost(FocusEvent e)
+       {
+         thresholdValue_actionPerformed();
+       }
+     });
    }
  
    public AnnotationRowFilter()
            continue;
          }
  
 +        AnnotationColourGradient scheme = null;
          if (currentColours.isSelected())
          {
 -          sg.cs = new AnnotationColourGradient(currentAnn, sg.cs,
 -                  selectedThresholdOption);
 -          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
 -                  .isSelected());
 -
 +          scheme = new AnnotationColourGradient(currentAnn,
 +                  sg.getColourScheme(), selectedThresholdOption);
          }
          else
          {
 -          sg.cs = new AnnotationColourGradient(currentAnn,
 +          scheme = new AnnotationColourGradient(currentAnn,
                    minColour.getBackground(), maxColour.getBackground(),
                    selectedThresholdOption);
 -          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
 -                  .isSelected());
          }
 -
 +        scheme.setSeqAssociated(seqAssociated.isSelected());
 +        sg.setColourScheme(scheme);
        }
      }
      return false;
  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;
 -import jalview.io.JalviewFileChooser;
 -import jalview.io.JalviewFileView;
  import jalview.io.StructureFile;
 -import jalview.schemes.BuriedColourScheme;
 -import jalview.schemes.ColourSchemeI;
 -import jalview.schemes.HelixColourScheme;
 -import jalview.schemes.HydrophobicColourScheme;
 -import jalview.schemes.PurinePyrimidineColourScheme;
 -import jalview.schemes.StrandColourScheme;
 -import jalview.schemes.TaylorColourScheme;
 -import jalview.schemes.TurnColourScheme;
 -import jalview.schemes.ZappoColourScheme;
  import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.BrowserLauncher;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.ws.dbsources.Pdb;
  
  import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
 -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;
  
    /*
    /**
     * Initialise menu options.
     */
 -  private void initMenus()
 +  @Override
 +  protected void initMenus()
    {
 +    super.initMenus();
 +
      viewerActionMenu.setText(MessageManager.getString("label.chimera"));
 +
      viewerColour.setText(MessageManager
              .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());
    }
  
    /**
      }
    }
  
 -  /**
 -   * Answers true if this viewer already involves the given PDB ID
 -   */
 -  @Override
 -  protected boolean hasPdbId(String pdbId)
 -  {
 -    return jmb.hasPdbId(pdbId);
 -  }
 -
    private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
            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);
      useAlignmentPanelForColourbyseq(ap);
 +
      if (pdbentrys.length > 1)
      {
        alignAddedStructures = true;
     */
    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())
      {
                          + 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
     */
    @Override
        {
          String prompt = MessageManager.formatMessage(
                  "label.confirm_close_chimera",
 -                new Object[] { jmb.getViewerTitle("Chimera", false) });
 +                        new Object[] { jmb.getViewerTitle(getViewerName(),
 +                                false) });
          prompt = JvSwingUtils.wrapTooltip(true, prompt);
          int confirm = JvOptionPane.showConfirmDialog(this, prompt,
                  MessageManager.getString("label.close_viewer"),
  
      if (files.length() > 0)
      {
+       jmb.setFinishedInit(false);
        if (!addingStructures)
        {
          try
            try
            {
              int pos = filePDBpos.get(num).intValue();
 -            long startTime = startProgressBar("Chimera "
 +            long startTime = startProgressBar(getViewerName() + " "
                      + MessageManager.getString("status.opening_file_for")
                      + " " + pe.getId());
              jmb.openFile(pe);
            }
          }
        }
        jmb.refreshGUI();
        jmb.setFinishedInit(true);
        jmb.setLoadingFromArchive(false);
    }
  
    @Override
 -  public void pdbFile_actionPerformed(ActionEvent actionEvent)
 -  {
 -    JalviewFileChooser chooser = new JalviewFileChooser(
 -            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 -
 -    chooser.setFileView(new JalviewFileView());
 -    chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
 -    chooser.setToolTipText(MessageManager.getString("action.save"));
 -
 -    int value = chooser.showSaveDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 -    {
 -      BufferedReader in = null;
 -      try
 -      {
 -        // TODO: cope with multiple PDB files in view
 -        in = new BufferedReader(new FileReader(jmb.getPdbFile()[0]));
 -        File outFile = chooser.getSelectedFile();
 -
 -        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
 -        String data;
 -        while ((data = in.readLine()) != null)
 -        {
 -          if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
 -          {
 -            out.println(data);
 -          }
 -        }
 -        out.close();
 -      } catch (Exception ex)
 -      {
 -        ex.printStackTrace();
 -      } finally
 -      {
 -        if (in != null)
 -        {
 -          try
 -          {
 -            in.close();
 -          } catch (IOException e)
 -          {
 -            e.printStackTrace();
 -          }
 -        }
 -      }
 -    }
 -  }
 -
 -  @Override
 -  public void viewMapping_actionPerformed(ActionEvent actionEvent)
 -  {
 -    jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
 -    try
 -    {
 -      cap.appendText(jmb.printMappings());
 -    } catch (OutOfMemoryError e)
 -    {
 -      new OOMWarning(
 -              "composing sequence-structure alignments for display in text box.",
 -              e);
 -      cap.dispose();
 -      return;
 -    }
 -    jalview.gui.Desktop.addInternalFrame(cap,
 -            MessageManager.getString("label.pdb_sequence_mapping"), 550,
 -            600);
 -  }
 -
 -  @Override
    public void eps_actionPerformed(ActionEvent e)
    {
      throw new Error(
    }
  
    @Override
 -  public void viewerColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    if (viewerColour.isSelected())
 -    {
 -      // disable automatic sequence colouring.
 -      jmb.setColourBySequence(false);
 -    }
 -  }
 -
 -  @Override
 -  public void seqColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    jmb.setColourBySequence(seqColour.isSelected());
 -    if (_colourwith == null)
 -    {
 -      _colourwith = new Vector<AlignmentPanel>();
 -    }
 -    if (jmb.isColourBySequence())
 -    {
 -      if (!jmb.isLoadingFromArchive())
 -      {
 -        if (_colourwith.size() == 0 && getAlignmentPanel() != null)
 -        {
 -          // Make the currently displayed alignment panel the associated view
 -          _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
 -        }
 -      }
 -      // Set the colour using the current view for the associated alignframe
 -      for (AlignmentPanel ap : _colourwith)
 -      {
 -        jmb.colourBySequence(ap.av.isShowSequenceFeatures(), ap);
 -      }
 -    }
 -  }
 -
 -  @Override
 -  public void chainColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    chainColour.setSelected(true);
 -    jmb.colourByChain();
 -  }
 -
 -  @Override
 -  public void chargeColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    chargeColour.setSelected(true);
 -    jmb.colourByCharge();
 -  }
 -
 -  @Override
 -  public void zappoColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    zappoColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new ZappoColourScheme());
 -  }
 -
 -  @Override
 -  public void taylorColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    taylorColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new TaylorColourScheme());
 -  }
 -
 -  @Override
 -  public void hydroColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    hydroColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new HydrophobicColourScheme());
 -  }
 -
 -  @Override
 -  public void helixColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    helixColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new HelixColourScheme());
 -  }
 -
 -  @Override
 -  public void strandColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    strandColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new StrandColourScheme());
 -  }
 -
 -  @Override
 -  public void turnColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    turnColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new TurnColourScheme());
 -  }
 -
 -  @Override
 -  public void buriedColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    buriedColour.setSelected(true);
 -    jmb.setJalviewColourScheme(new BuriedColourScheme());
 -  }
 -
 -  @Override
 -  public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    setJalviewColourScheme(new PurinePyrimidineColourScheme());
 -  }
 -
 -  @Override
 -  public void userColour_actionPerformed(ActionEvent actionEvent)
 -  {
 -    userColour.setSelected(true);
 -    new UserDefinedColours(this, null);
 -  }
 -
 -  @Override
 -  public void backGround_actionPerformed(ActionEvent actionEvent)
 -  {
 -    java.awt.Color col = JColorChooser
 -            .showDialog(this, MessageManager
 -                    .getString("label.select_backgroud_colour"), null);
 -    if (col != null)
 -    {
 -      jmb.setBackgroundColour(col);
 -    }
 -  }
 -
 -  @Override
    public void showHelp_actionPerformed(ActionEvent actionEvent)
    {
      try
      {
 -      jalview.util.BrowserLauncher
 +      BrowserLauncher
                .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
--    } catch (Exception ex)
++    } catch (IOException ex)
      {
      }
    }
  
 -  public void updateTitleAndMenus()
 -  {
 -    if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
 -    {
 -      repaint();
 -      return;
 -    }
 -    setChainMenuItems(jmb.getChainNames());
 -
 -    this.setTitle(jmb.getViewerTitle("Chimera", true));
 -    // if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
 -    // {
 -      viewerActionMenu.setVisible(true);
 -    // }
 -    if (!jmb.isLoadingFromArchive())
 -    {
 -      seqColour_actionPerformed(null);
 -    }
 -  }
 -
 -  /*
 -   * (non-Javadoc)
 -   * 
 -   * @see
 -   * jalview.jbgui.GStructureViewer#alignStructs_actionPerformed(java.awt.event
 -   * .ActionEvent)
 -   */
 -  @Override
 -  protected void alignStructs_actionPerformed(ActionEvent actionEvent)
 -  {
 -    alignStructs_withAllAlignPanels();
 -  }
 -
 -  private void alignStructs_withAllAlignPanels()
 -  {
 -    if (getAlignmentPanel() == null)
 -    {
 -      return;
 -    }
 -
 -    if (_alignwith.size() == 0)
 -    {
 -      _alignwith.add(getAlignmentPanel());
 -    }
 -
 -    try
 -    {
 -      AlignmentI[] als = new Alignment[_alignwith.size()];
 -      ColumnSelection[] alc = new ColumnSelection[_alignwith.size()];
 -      int[] alm = new int[_alignwith.size()];
 -      int a = 0;
 -
 -      for (AlignmentPanel ap : _alignwith)
 -      {
 -        als[a] = ap.av.getAlignment();
 -        alm[a] = -1;
 -        alc[a++] = ap.av.getColumnSelection();
 -      }
 -      jmb.superposeStructures(als, alm, alc);
 -    } catch (Exception e)
 -    {
 -      StringBuffer sp = new StringBuffer();
 -      for (AlignmentPanel ap : _alignwith)
 -      {
 -        sp.append("'" + ap.alignFrame.getTitle() + "' ");
 -      }
 -      Cache.log.info("Couldn't align structures with the " + sp.toString()
 -              + "associated alignment panels.", e);
 -    }
 -  }
 -
 -  @Override
 -  public void setJalviewColourScheme(ColourSchemeI ucs)
 -  {
 -    jmb.setJalviewColourScheme(ucs);
 -
 -  }
 -
 -  /**
 -   * 
 -   * @param alignment
 -   * @return first alignment panel displaying given alignment, or the default
 -   *         alignment panel
 -   */
 -  public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
 -  {
 -    for (AlignmentPanel ap : getAllAlignmentPanels())
 -    {
 -      if (ap.av.getAlignment() == alignment)
 -      {
 -        return ap;
 -      }
 -    }
 -    return getAlignmentPanel();
 -  }
 -
    @Override
    public AAStructureBindingModel getBinding()
    {
    }
  
    @Override
 -  protected AAStructureBindingModel getBindingModel()
 +  protected String getViewerName()
    {
 -    return jmb;
 +    return "Chimera";
 +  }
++
++  @Override
++  public void updateTitleAndMenus()
++  {
++    super.updateTitleAndMenus();
++    viewerActionMenu.setVisible(true);
+   }
  }
@@@ -28,6 -28,8 +28,8 @@@ import jalview.ext.rbvi.chimera.Jalview
  import jalview.io.DataSourceType;
  import jalview.structure.StructureSelectionManager;
  
+ import javax.swing.SwingUtilities;
  public class JalviewChimeraBindingModel extends JalviewChimeraBinding
  {
    private ChimeraViewFrame cvf;
@@@ -95,7 -97,7 +97,7 @@@
      }
      if (!isLoadingFromArchive())
      {
 -      colourBySequence(ap.av.isShowSequenceFeatures(), ap);
 +      colourBySequence(ap);
      }
    }
  
    protected void sendAsynchronousCommand(final String command,
            final String progressMsg)
    {
-     Thread thread = new Thread(new Runnable()
+     final long handle = progressMsg == null ? 0 : cvf
+             .startProgressBar(progressMsg);
+     SwingUtilities.invokeLater(new Runnable()
      {
        @Override
        public void run()
        {
-         long stm = cvf.startProgressBar(progressMsg);
          try
          {
            sendChimeraCommand(command, false);
          } finally
          {
-           cvf.stopProgressBar(null, stm);
+           if (progressMsg != null)
+           {
+             cvf.stopProgressBar(null, handle);
+           }
          }
        }
      });
-     thread.start();
    }
  
    @Override
   */
  package jalview.viewmodel;
  
+ import java.awt.Color;
+ import java.beans.PropertyChangeSupport;
+ import java.util.ArrayDeque;
+ import java.util.ArrayList;
+ import java.util.BitSet;
+ import java.util.Deque;
+ import java.util.HashMap;
+ import java.util.Hashtable;
+ import java.util.List;
+ import java.util.Map;
  import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
  import jalview.analysis.Conservation;
  import jalview.api.AlignCalcManagerI;
@@@ -42,9 -53,9 +53,9 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceCollectionI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
 -import jalview.schemes.Blosum62ColourScheme;
 +import jalview.renderer.ResidueShader;
 +import jalview.renderer.ResidueShaderI;
  import jalview.schemes.ColourSchemeI;
 -import jalview.schemes.PIDColourScheme;
  import jalview.structure.CommandListener;
  import jalview.structure.StructureSelectionManager;
  import jalview.structure.VamsasSource;
@@@ -57,17 -68,6 +68,6 @@@ import jalview.workers.ComplementConsen
  import jalview.workers.ConsensusThread;
  import jalview.workers.StrucConsensusThread;
  
- import java.awt.Color;
- import java.beans.PropertyChangeSupport;
- import java.util.ArrayDeque;
- import java.util.ArrayList;
- import java.util.BitSet;
- import java.util.Deque;
- import java.util.HashMap;
- import java.util.Hashtable;
- import java.util.List;
- import java.util.Map;
  /**
   * base class holding visualization and analysis attributes and common logic for
   * an active alignment view displayed in the GUI
@@@ -597,7 -597,7 +597,7 @@@ public abstract class AlignmentViewpor
  
    protected boolean ignoreGapsInConsensusCalculation = false;
  
 -  protected ColourSchemeI globalColourScheme = null;
 +  protected ResidueShaderI residueShading;
  
    @Override
    public void setGlobalColourScheme(ColourSchemeI cs)
      // TODO: logic refactored from AlignFrame changeColour -
      // TODO: autorecalc stuff should be changed to rely on the worker system
      // check to see if we should implement a changeColour(cs) method rather than
 -    // put th logic in here
 +    // put the logic in here
      // - means that caller decides if they want to just modify state and defer
      // calculation till later or to do all calculations in thread.
      // via changecolour
 -    globalColourScheme = cs;
 -    boolean recalc = false;
 +
 +    /*
 +     * only instantiate alignment colouring once, thereafter update it;
 +     * this means that any conservation or PID threshold settings
 +     * persist when the alignment colour scheme is changed
 +     */
 +    if (residueShading == null)
 +    {
 +      residueShading = new ResidueShader(viewStyle);
 +    }
 +    residueShading.setColourScheme(cs);
 +
 +    // TODO: do threshold and increment belong in ViewStyle or ResidueShader?
 +    // ...problem: groups need these, but do not currently have a ViewStyle
 +
      if (cs != null)
      {
 -      recalc = getConservationSelected();
 -      if (getAbovePIDThreshold() || cs instanceof PIDColourScheme
 -              || cs instanceof Blosum62ColourScheme)
 -      {
 -        recalc = true;
 -        cs.setThreshold(viewStyle.getThreshold(),
 -                ignoreGapsInConsensusCalculation);
 -      }
 -      else
 +      if (getConservationSelected())
        {
 -        cs.setThreshold(0, ignoreGapsInConsensusCalculation);
 +        residueShading.setConservation(hconservation);
        }
 -      if (recalc)
 -      {
 -        cs.setConsensus(hconsensus);
 -        cs.setConservation(hconservation);
 -      }
 -      cs.setConservationApplied(getConservationSelected());
 -      cs.alignmentChanged(alignment, hiddenRepSequences);
 +      residueShading.alignmentChanged(alignment, hiddenRepSequences);
      }
 +
 +    /*
 +     * if 'apply colour to all groups' is selected... do so
 +     * (but don't transfer any colour threshold settings to groups)
 +     */
      if (getColourAppliesToAllGroups())
      {
        for (SequenceGroup sg : getAlignment().getGroups())
        {
 -        if (cs == null)
 -        {
 -          sg.cs = null;
 -          continue;
 -        }
 -        sg.cs = cs.applyTo(sg, getHiddenRepSequences());
 -        sg.setConsPercGaps(ConsPercGaps);
 -        if (getAbovePIDThreshold() || cs instanceof PIDColourScheme
 -                || cs instanceof Blosum62ColourScheme)
 -        {
 -          sg.cs.setThreshold(viewStyle.getThreshold(),
 -                  isIgnoreGapsConsensus());
 -          recalc = true;
 -        }
 -        else
 +        /*
 +         * retain any colour thresholds per group while
 +         * changing choice of colour scheme (JAL-2386)
 +         */
 +        sg.setColourScheme(cs);
 +        if (cs != null)
          {
 -          sg.cs.setThreshold(0, isIgnoreGapsConsensus());
 -        }
 -
 -        if (getConservationSelected())
 -        {
 -          sg.cs.setConservationApplied(true);
 -          recalc = true;
 -        }
 -        else
 -        {
 -          sg.cs.setConservation(null);
 -          // sg.cs.setThreshold(0, getIgnoreGapsConsensus());
 -        }
 -        if (recalc)
 -        {
 -          sg.recalcConservation();
 -        }
 -        else
 -        {
 -          sg.cs.alignmentChanged(sg, hiddenRepSequences);
 +          sg.getGroupColourScheme()
 +                  .alignmentChanged(sg, hiddenRepSequences);
          }
        }
      }
    @Override
    public ColourSchemeI getGlobalColourScheme()
    {
 -    return globalColourScheme;
 +    return residueShading == null ? null : residueShading
 +            .getColourScheme();
 +  }
 +
 +  @Override
 +  public ResidueShaderI getResidueShading()
 +  {
 +    return residueShading;
    }
  
    protected AlignmentAnnotation consensus;
  
    protected AlignmentAnnotation complementConsensus;
  
+   protected AlignmentAnnotation gapcounts;
    protected AlignmentAnnotation strucConsensus;
  
    protected AlignmentAnnotation conservation;
    }
  
    @Override
+   public AlignmentAnnotation getAlignmentGapAnnotation()
+   {
+     return gapcounts;
+   }
+   @Override
    public AlignmentAnnotation getComplementConsensusAnnotation()
    {
      return complementConsensus;
    public void updateConsensus(final AlignmentViewPanel ap)
    {
      // see note in mantis : issue number 8585
-     if (consensus == null || !autoCalculateConsensus)
+     if ((consensus == null || gapcounts == null) || !autoCalculateConsensus)
      {
        return;
      }
      hconsensus = null;
      hcomplementConsensus = null;
      // colour scheme may hold reference to consensus
 -    globalColourScheme = null;
 +    residueShading = null;
      // TODO remove listeners from changeSupport?
      changeSupport = null;
      setAlignment(null);
    }
  
    /**
 -   * Set the selection group for this window.
 +   * Set the selection group for this window. Also sets the current alignment as
 +   * the context for the group, if it does not already have one.
     * 
     * @param sg
     *          - group holding references to sequences in this alignment view
    public void setSelectionGroup(SequenceGroup sg)
    {
      selectionGroup = sg;
 +    if (sg != null && sg.getContext() == null)
 +    {
 +      sg.setContext(alignment);
 +    }
    }
  
    public void setHiddenColumns(ColumnSelection colsel)
      if (ap != null)
      {
        updateConsensus(ap);
 -      if (globalColourScheme != null)
 +      if (residueShading != null)
        {
 -        globalColourScheme.setThreshold(globalColourScheme.getThreshold(),
 +        residueShading.setThreshold(residueShading.getThreshold(),
                  ignoreGapsInConsensusCalculation);
        }
      }
        selectionGroup.setEndRes(alWidth - 1);
      }
  
 -    resetAllColourSchemes();
 +    updateAllColourSchemes();
      calculator.restartWorkers();
      // alignment.adjustSequenceAnnotations();
    }
    /**
     * reset scope and do calculations for all applied colourschemes on alignment
     */
 -  void resetAllColourSchemes()
 +  void updateAllColourSchemes()
    {
 -    ColourSchemeI cs = globalColourScheme;
 -    if (cs != null)
 +    ResidueShaderI rs = residueShading;
 +    if (rs != null)
      {
 -      cs.alignmentChanged(alignment, hiddenRepSequences);
 +      rs.alignmentChanged(alignment, hiddenRepSequences);
  
 -      cs.setConsensus(hconsensus);
 -      if (cs.conservationApplied())
 +      rs.setConsensus(hconsensus);
 +      if (rs.conservationApplied())
        {
 -        cs.setConservation(Conservation.calculateConservation("All",
 +        rs.setConservation(Conservation.calculateConservation("All",
                  alignment.getSequences(), 0, alignment.getWidth(), false,
                  getConsPercGaps(), false));
        }
        consensus = new AlignmentAnnotation("Consensus", "PID",
                new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
        initConsensus(consensus);
+       gapcounts = new AlignmentAnnotation("Occupancy",
+               "Number of aligned positions",
+               new Annotation[1], 0f, alignment.getHeight(),
+               AlignmentAnnotation.BAR_GRAPH);
+       initGapCounts(gapcounts);
  
        initComplementConsensus();
      }
      }
    }
  
+   // these should be extracted from the view model - style and settings for
+   // derived annotation
+   private void initGapCounts(AlignmentAnnotation counts)
+   {
+     counts.hasText = false;
+     counts.autoCalculated = true;
+     counts.graph = AlignmentAnnotation.BAR_GRAPH;
+     if (showConsensus)
+     {
+       alignment.addAnnotation(counts);
+     }
+   }
    private void initConservation()
    {
      if (showConservation)
    public void setViewStyle(ViewStyleI settingsForView)
    {
      viewStyle = new ViewStyle(settingsForView);
 +    if (residueShading != null)
 +    {
 +      residueShading.setConservationApplied(settingsForView
 +              .isConservationColourSelected());
 +    }
    }
  
    @Override
     */
    private boolean selectionIsDefinedGroup = false;
  
    @Override
    public boolean isSelectionDefinedGroup()
    {
@@@ -28,7 -28,7 +28,7 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.Annotation;
  import jalview.datamodel.ProfilesI;
  import jalview.datamodel.SequenceI;
 -import jalview.schemes.ColourSchemeI;
 +import jalview.renderer.ResidueShaderI;
  
  public class ConsensusThread extends AlignCalcWorker
  {
@@@ -50,7 -50,8 +50,8 @@@
      try
      {
        AlignmentAnnotation consensus = getConsensusAnnotation();
-       if (consensus == null || calcMan.isPending(this))
+       AlignmentAnnotation gap = getGapAnnotation();
+       if ((consensus == null && gap == null) || calcMan.isPending(this))
        {
          calcMan.workerComplete(this);
          return;
    {
      AlignmentAnnotation consensus = getConsensusAnnotation();
      consensus.annotations = new Annotation[aWidth];
+     AlignmentAnnotation gap = getGapAnnotation();
+     if (gap != null)
+     {
+       gap.annotations = new Annotation[aWidth];
+     }
    }
  
    /**
  
      SequenceI[] aseqs = getSequences();
      int width = alignment.getWidth();
-     ProfilesI hconsensus = AAFrequency.calculate(aseqs, width, 0,
-             width, true);
+     ProfilesI hconsensus = AAFrequency.calculate(aseqs, width, 0, width,
+             true);
  
      alignViewport.setSequenceConsensusHash(hconsensus);
      setColourSchemeConsensus(hconsensus);
     */
    protected void setColourSchemeConsensus(ProfilesI hconsensus)
    {
 -    ColourSchemeI globalColourScheme = alignViewport
 -            .getGlobalColourScheme();
 -    if (globalColourScheme != null)
 +    ResidueShaderI cs = alignViewport.getResidueShading();
 +    if (cs != null)
      {
 -      globalColourScheme.setConsensus(hconsensus);
 +      cs.setConsensus(hconsensus);
      }
    }
  
    }
  
    /**
+    * Get the Gap annotation for the alignment
+    * 
+    * @return
+    */
+   protected AlignmentAnnotation getGapAnnotation()
+   {
+     return alignViewport.getAlignmentGapAnnotation();
+   }
+   /**
     * update the consensus annotation from the sequence profile data using
     * current visualization settings.
     */
              && hconsensus != null)
      {
        deriveConsensus(consensus, hconsensus);
+       AlignmentAnnotation gap = getGapAnnotation();
+       if (gap != null)
+       {
+         deriveGap(gap, hconsensus);
+       }
      }
    }
  
  
      long nseq = getSequences().length;
      AAFrequency.completeConsensus(consensusAnnotation, hconsensus,
-             hconsensus.getStartColumn(),
-             hconsensus.getEndColumn() + 1,
+             hconsensus.getStartColumn(), hconsensus.getEndColumn() + 1,
              alignViewport.isIgnoreGapsConsensus(),
              alignViewport.isShowSequenceLogo(), nseq);
    }
  
    /**
+    * Convert the computed consensus data into a gap annotation row for display.
+    * 
+    * @param gapAnnotation
+    *          the annotation to be populated
+    * @param hconsensus
+    *          the computed consensus data
+    */
+   protected void deriveGap(AlignmentAnnotation gapAnnotation,
+           ProfilesI hconsensus)
+   {
+     long nseq = getSequences().length;
+     AAFrequency.completeGapAnnot(gapAnnotation, hconsensus,
+             hconsensus.getStartColumn(), hconsensus.getEndColumn() + 1,
+             nseq);
+   }
+   /**
     * Get the consensus data stored on the viewport.
     * 
     * @return
   */
  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 java.util.SortedMap;
  
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
@@@ -46,73 -45,107 +45,108 @@@ public class ChimeraCommandsTes
    }
  
    @Test(groups = { "Functional" })
    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();