JAL-2835 support filter/colour by nested attribute names
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 15 Nov 2017 10:42:49 +0000 (10:42 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 15 Nov 2017 10:42:49 +0000 (10:42 +0000)
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/FeatureAttributes.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/gui/FeatureSettings.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/schemes/FeatureColour.java
test/jalview/datamodel/features/FeatureAttributesTest.java [new file with mode: 0644]

index 3eebf6c..93773cc 100644 (file)
@@ -189,17 +189,18 @@ public interface FeatureColourI
   boolean isColourByAttribute();
 
   /**
-   * Answers the name of the attribute used for colouring if any, or null
+   * Answers the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null
    * 
    * @return
    */
-  String getAttributeName();
+  String[] getAttributeName();
 
   /**
-   * Sets the name of the attribute used for colouring if any, or null to remove
-   * this property
+   * Sets the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null to remove this property
    * 
    * @return
    */
-  void setAttributeName(String name);
+  void setAttributeName(String... name);
 }
index 40c7d4d..ef0abbd 100644 (file)
@@ -133,7 +133,7 @@ public interface FeatureRenderer
   List<String> getGroups(boolean visible);
 
   /**
-   * change visibility for a range of groups
+   * Set visibility for a list of groups
    * 
    * @param toset
    * @param visible
@@ -141,7 +141,7 @@ public interface FeatureRenderer
   void setGroupVisibility(List<String> toset, boolean visible);
 
   /**
-   * change visibiilty of given group
+   * Set visibility of the given feature group
    * 
    * @param group
    * @param visible
@@ -149,9 +149,9 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * Returns features at the specified aligned column on the given sequence.
-   * Non-positional features are not included. If the column has a gap, then
-   * enclosing features are included (but not contact features).
+   * Returns visible features at the specified aligned column on the given
+   * sequence. Non-positional features are not included. If the column has a gap,
+   * then enclosing features are included (but not contact features).
    * 
    * @param sequence
    * @param column
index 2110632..8a6cb61 100755 (executable)
@@ -59,18 +59,6 @@ public class SequenceFeature implements FeatureLocationI
   private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
 
   /*
-   * map of otherDetails special keys, and their value fields' delimiter
-   */
-  private static final Map<String, String> INFO_KEYS = new HashMap<>();
-
-  static
-  {
-    INFO_KEYS.put("CSQ", ",");
-    // todo capture second level metadata (CSQ FORMAT)
-    // and delimiter "|" so as to report in a table within a table?
-  }
-
-  /*
    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
    * name1=value1;name2=value2,value3;...etc
    */
@@ -184,7 +172,7 @@ public class SequenceFeature implements FeatureLocationI
 
     if (sf.otherDetails != null)
     {
-      otherDetails = new HashMap<String, Object>();
+      otherDetails = new HashMap<>();
       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
       {
         otherDetails.put(entry.getKey(), entry.getValue());
@@ -192,7 +180,7 @@ public class SequenceFeature implements FeatureLocationI
     }
     if (sf.links != null && sf.links.size() > 0)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
       {
         links.addElement(sf.links.elementAt(i));
@@ -359,7 +347,7 @@ public class SequenceFeature implements FeatureLocationI
   {
     if (links == null)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
     }
 
     if (!links.contains(labelLink))
@@ -394,18 +382,25 @@ public class SequenceFeature implements FeatureLocationI
 
   /**
    * Answers the value of the specified attribute as string, or null if no such
-   * value
+   * value. If more than one attribute name is provided, tries to resolve as keys
+   * to nested maps. For example, if attribute "CSQ" holds a map of key-value
+   * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
+   * in that map.
    * 
    * @param key
    * @return
    */
-  public String getValueAsString(String key)
+  public String getValueAsString(String... key)
   {
     if (otherDetails == null)
     {
       return null;
     }
-    Object value = otherDetails.get(key);
+    Object value = otherDetails.get(key[0]);
+    if (key.length > 1 && value instanceof Map<?, ?>)
+    {
+      value = ((Map) value).get(key[1]);
+    }
     return value == null ? null : value.toString();
   }
 
@@ -438,7 +433,7 @@ public class SequenceFeature implements FeatureLocationI
     {
       if (otherDetails == null)
       {
-        otherDetails = new HashMap<String, Object>();
+        otherDetails = new HashMap<>();
       }
 
       otherDetails.put(key, value);
@@ -463,8 +458,8 @@ public class SequenceFeature implements FeatureLocationI
               .getAttributeName(key);
     }
 
-    FeatureAttributes.getInstance().addAttribute(this.type, key, attDesc,
-            value.toString());
+    FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
+            key);
   }
 
   /*
@@ -642,30 +637,33 @@ public class SequenceFeature implements FeatureLocationI
         {
           continue; // to avoid double reporting
         }
-        if (INFO_KEYS.containsKey(key))
+
+        Object value = entry.getValue();
+        if (value instanceof Map<?, ?>)
         {
           /*
-           * split selected INFO data by delimiter over multiple lines
+           * expand values in a Map attribute across separate lines
            */
-          String delimiter = INFO_KEYS.get(key);
-          String[] values = entry.getValue().toString().split(delimiter);
-          for (String value : values)
+          Map<?, ?> values = (Map<?, ?>) value;
+          for (Entry<?, ?> e : values.entrySet())
           {
-            sb.append(String.format(ROW_DATA, key, "", value));
+            sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
+                    .getValue().toString()));
           }
         }
         else
-        { // tried <td title="key"> but it failed to provide a tooltip :-(
+        {
+          // tried <td title="key"> but it failed to provide a tooltip :-(
           String attDesc = null;
           if (metadata != null)
           {
             attDesc = metadata.getAttributeName(key);
           }
-          String value = entry.getValue().toString();
-          if (isValueInteresting(key, value, metadata))
+          String s = entry.getValue().toString();
+          if (isValueInteresting(key, s, metadata))
           {
             sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
-                    : attDesc, value));
+                    : attDesc, s));
           }
         }
       }
index 3dc4f19..7221d62 100644 (file)
@@ -2,9 +2,11 @@ package jalview.datamodel.features;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.TreeMap;
 
 /**
@@ -14,7 +16,45 @@ public class FeatureAttributes
 {
   private static FeatureAttributes instance = new FeatureAttributes();
 
-  private Map<String, Map<String, AttributeData>> attributes;
+  /*
+   * map, by feature type, of a map, by attribute name, of
+   * attribute description and min-max range (if known)
+   */
+  private Map<String, Map<String[], AttributeData>> attributes;
+
+  /*
+   * a case-insensitive comparator so that attributes are ordered e.g.
+   * AC
+   * af
+   * CSQ:AFR_MAF
+   * CSQ:Allele
+   */
+  private Comparator<String[]> comparator = new Comparator<String[]>()
+  {
+    @Override
+    public int compare(String[] o1, String[] o2)
+    {
+      int i = 0;
+      while (i < o1.length || i < o2.length)
+      {
+        if (o2.length <= i)
+        {
+          return o1.length <= i ? 0 : 1;
+        }
+        if (o1.length <= i)
+        {
+          return -1;
+        }
+        int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
+        if (comp != 0)
+        {
+          return comp;
+        }
+        i++;
+      }
+      return 0; // same length and all matched
+    }
+  };
 
   private class AttributeData
   {
@@ -116,17 +156,19 @@ public class FeatureAttributes
   }
 
   /**
-   * Answers the attributes known for the given feature type, in alphabetical
-   * order (not case sensitive), or an empty set if no attributes are known
+   * Answers the attribute names known for the given feature type, in
+   * alphabetical order (not case sensitive), or an empty set if no attributes
+   * are known. An attribute name is typically 'simple' e.g. "AC", but may be
+   * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
    * 
    * @param featureType
    * @return
    */
-  public List<String> getAttributes(String featureType)
+  public List<String[]> getAttributes(String featureType)
   {
     if (!attributes.containsKey(featureType))
     {
-      return Collections.<String> emptyList();
+      return Collections.<String[]> emptyList();
     }
 
     return new ArrayList<>(attributes.get(featureType).keySet());
@@ -156,23 +198,39 @@ public class FeatureAttributes
    * type, and updates the min-max for any numeric value
    * 
    * @param featureType
-   * @param attName
    * @param description
    * @param value
+   * @param attName
    */
-  public void addAttribute(String featureType, String attName,
-          String description, String value)
+  public void addAttribute(String featureType, String description,
+          Object value, String... attName)
   {
     if (featureType == null || attName == null)
     {
       return;
     }
 
-    Map<String, AttributeData> atts = attributes.get(featureType);
+    /*
+     * if attribute value is a map, drill down one more level to
+     * record its sub-fields
+     */
+    if (value instanceof Map<?, ?>)
+    {
+      for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+      {
+        String[] attNames = new String[attName.length + 1];
+        System.arraycopy(attName, 0, attNames, 0, attName.length);
+        attNames[attName.length] = entry.getKey().toString();
+        addAttribute(featureType, description, entry.getValue(), attNames);
+      }
+      return;
+    }
+
+    String valueAsString = value.toString();
+    Map<String[], AttributeData> atts = attributes.get(featureType);
     if (atts == null)
     {
-      atts = new TreeMap<String, AttributeData>(
-              String.CASE_INSENSITIVE_ORDER);
+      atts = new TreeMap<>(comparator);
       attributes.put(featureType, atts);
     }
     AttributeData attData = atts.get(attName);
@@ -181,7 +239,7 @@ public class FeatureAttributes
       attData = new AttributeData();
       atts.put(attName, attData);
     }
-    attData.addInstance(description, value);
+    attData.addInstance(description, valueAsString);
   }
 
   /**
@@ -192,10 +250,10 @@ public class FeatureAttributes
    * @param attName
    * @return
    */
-  public String getDescription(String featureType, String attName)
+  public String getDescription(String featureType, String... attName)
   {
     String desc = null;
-    Map<String, AttributeData> atts = attributes.get(featureType);
+    Map<String[], AttributeData> atts = attributes.get(featureType);
     if (atts != null)
     {
       AttributeData attData = atts.get(attName);
@@ -217,9 +275,9 @@ public class FeatureAttributes
    * @param attName
    * @return
    */
-  public float[] getMinMax(String featureType, String attName)
+  public float[] getMinMax(String featureType, String... attName)
   {
-    Map<String, AttributeData> atts = attributes.get(featureType);
+    Map<String[], AttributeData> atts = attributes.get(featureType);
     if (atts != null)
     {
       AttributeData attData = atts.get(attName);
@@ -238,19 +296,18 @@ public class FeatureAttributes
    * @param attName
    * @param description
    */
-  public void addDescription(String featureType, String attName,
-          String description)
+  public void addDescription(String featureType, String description,
+          String... attName)
   {
     if (featureType == null || attName == null)
     {
       return;
     }
   
-    Map<String, AttributeData> atts = attributes.get(featureType);
+    Map<String[], AttributeData> atts = attributes.get(featureType);
     if (atts == null)
     {
-      atts = new TreeMap<String, AttributeData>(
-              String.CASE_INSENSITIVE_ORDER);
+      atts = new TreeMap<>(comparator);
       attributes.put(featureType, atts);
     }
     AttributeData attData = atts.get(attName);
index 6c85e8c..da3819c 100644 (file)
@@ -57,6 +57,8 @@ import javax.swing.event.ChangeListener;
 
 public class FeatureColourChooser extends JalviewDialog
 {
+  private static final String COLON = ":";
+
   private static final int MAX_TOOLTIP_LENGTH = 50;
 
   private static int NO_COLOUR_OPTION = 0;
@@ -163,9 +165,8 @@ public class FeatureColourChooser extends JalviewDialog
     this.fr = frender;
     this.type = theType;
     ap = fr.ap;
-    String title = MessageManager
-            .formatMessage("label.graduated_color_for_params", new String[]
-            { theType });
+    String title = MessageManager.formatMessage("label.variable_color_for",
+            new String[] { theType });
     initDialogFrame(this, true, blocking, title, 470, 300);
 
     slider.addChangeListener(new ChangeListener()
@@ -260,7 +261,9 @@ public class FeatureColourChooser extends JalviewDialog
       {
         byAttributeText.setSelected(true);
         textAttributeCombo.setEnabled(true);
-        textAttributeCombo.setSelectedItem(cs.getAttributeName());
+        String[] attributeName = cs.getAttributeName();
+        textAttributeCombo
+                .setSelectedItem(String.join(COLON, attributeName));
       }
       else
       {
@@ -273,8 +276,9 @@ public class FeatureColourChooser extends JalviewDialog
       if (cs.isColourByAttribute())
       {
         byAttributeValue.setSelected(true);
-        String attributeName = cs.getAttributeName();
-        valueAttributeCombo.setSelectedItem(attributeName);
+        String[] attributeName = cs.getAttributeName();
+        valueAttributeCombo
+                .setSelectedItem(String.join(COLON, attributeName));
         valueAttributeCombo.setEnabled(true);
         updateMinMax();
       }
@@ -389,7 +393,8 @@ public class FeatureColourChooser extends JalviewDialog
     else if (byAttributeValue.isSelected())
     {
       String attName = (String) valueAttributeCombo.getSelectedItem();
-      minMax = FeatureAttributes.getInstance().getMinMax(type, attName);
+      String[] attNames = attName.split(COLON);
+      minMax = FeatureAttributes.getInstance().getMinMax(type, attNames);
     }
     if (minMax != null)
     {
@@ -428,8 +433,8 @@ public class FeatureColourChooser extends JalviewDialog
     byAttributeValue.addActionListener(changeMinMaxAction);
     byWhatPanel.add(byAttributeValue);
 
-    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
-            type);
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(type);
     valueAttributeCombo = populateAttributesDropdown(type, attNames, true);
 
     /*
@@ -632,8 +637,8 @@ public class FeatureColourChooser extends JalviewDialog
     byAttributeText.addActionListener(changeColourAction);
     byTextPanel.add(byAttributeText);
 
-    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
-            type);
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(type);
     textAttributeCombo = populateAttributesDropdown(type, attNames, false);
     byTextPanel.add(textAttributeCombo);
 
@@ -735,13 +740,18 @@ public class FeatureColourChooser extends JalviewDialog
     {
       attribute = (String) textAttributeCombo.getSelectedItem();
       textAttributeCombo.setEnabled(true);
+      acg.setAttributeName(attribute.split(COLON));
     }
     else if (byAttributeValue.isSelected())
     {
       attribute = (String) valueAttributeCombo.getSelectedItem();
       valueAttributeCombo.setEnabled(true);
+      acg.setAttributeName(attribute.split(COLON));
+    }
+    else
+    {
+      acg.setAttributeName((String) null);
     }
-    acg.setAttributeName(attribute);
 
     if (!hasThreshold)
     {
@@ -933,23 +943,26 @@ public class FeatureColourChooser extends JalviewDialog
   /**
    * A helper method to build the drop-down choice of attributes for a feature.
    * Where metadata is available with a description for an attribute, that is
-   * added as a tooltip. The list may be restricted to attributes for which we
-   * hold a range of numerical values (so suitable candidates for a graduated
-   * colour scheme).
+   * added as a tooltip. The list may optionally be restricted to attributes for
+   * which we hold a range of numerical values (so suitable candidates for a
+   * graduated colour scheme).
+   * <p>
+   * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
+   * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
    * 
    * @param featureType
    * @param attNames
    * @param withNumericRange
    */
   protected JComboBox<String> populateAttributesDropdown(
-          String featureType, List<String> attNames,
+          String featureType, List<String[]> attNames,
           boolean withNumericRange)
   {
     List<String> validAtts = new ArrayList<>();
     List<String> tooltips = new ArrayList<>();
 
     FeatureAttributes fa = FeatureAttributes.getInstance();
-    for (String attName : attNames)
+    for (String[] attName : attNames)
     {
       if (withNumericRange)
       {
@@ -959,7 +972,7 @@ public class FeatureColourChooser extends JalviewDialog
           continue;
         }
       }
-      validAtts.add(attName);
+      validAtts.add(String.join(COLON, attName));
       String desc = fa.getDescription(featureType, attName);
       if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
       {
index 01c40d5..ed98830 100644 (file)
@@ -120,6 +120,8 @@ import javax.swing.table.TableCellRenderer;
 public class FeatureSettings extends JPanel
         implements FeatureSettingsControllerI
 {
+  private static final String COLON = ":";
+
   private static final int MIN_WIDTH = 400;
 
   private static final int MIN_HEIGHT = 400;
@@ -1486,8 +1488,8 @@ public class FeatureSettings extends JPanel
      * look up attributes known for feature type
      */
     String selectedType = (String) filteredFeatureChoice.getSelectedItem();
-    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
-            selectedType);
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(selectedType);
 
     /*
      * if this feature type has filters set, load them first
@@ -1507,7 +1509,8 @@ public class FeatureSettings extends JPanel
     /*
      * and an empty filter for the user to populate (add)
      */
-    KeyedMatcherI noFilter = new KeyedMatcher("", Condition.values()[0], "");
+    KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "",
+            (String) null);
     filters.add(noFilter);
 
     /*
@@ -1516,11 +1519,11 @@ public class FeatureSettings extends JPanel
     int filterIndex = 0;
     for (KeyedMatcherI filter : filters)
     {
-      String key = filter.getKey();
+      String[] attName = filter.getKey();
       Condition condition = filter.getMatcher()
               .getCondition();
       String pattern = filter.getMatcher().getPattern();
-      JPanel row = addFilter(key, attNames, condition, pattern, filterIndex);
+      JPanel row = addFilter(attName, attNames, condition, pattern, filterIndex);
       row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
       chooseFiltersPanel.add(row);
       filterIndex++;
@@ -1539,18 +1542,18 @@ public class FeatureSettings extends JPanel
    * <li>a text field for input of a match pattern</li>
    * <li>optionally, a 'remove' button</li>
    * </ul>
-   * If attribute, condition or pattern are not null, they are set as defaults
-   * for the input fields. The 'remove' button is added unless the pattern is
-   * null or empty (incomplete filter condition).
+   * If attribute, condition or pattern are not null, they are set as defaults for
+   * the input fields. The 'remove' button is added unless the pattern is null or
+   * empty (incomplete filter condition).
    * 
-   * @param attribute
+   * @param attName
    * @param attNames
    * @param cond
    * @param pattern
    * @param filterIndex
    * @return
    */
-  protected JPanel addFilter(String attribute, List<String> attNames,
+  protected JPanel addFilter(String[] attName, List<String[]> attNames,
           Condition cond, String pattern, int filterIndex)
   {
     JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
@@ -1593,13 +1596,13 @@ public class FeatureSettings extends JPanel
       }
     };
 
-    if ("".equals(attribute))
+    if (attName == null) // the 'add a condition' row
     {
       attCombo.setSelectedItem(null);
     }
     else
     {
-      attCombo.setSelectedItem(attribute);
+      attCombo.setSelectedItem(String.join(COLON, attName));
     }
     attCombo.addItemListener(itemListener);
 
@@ -1667,22 +1670,24 @@ public class FeatureSettings extends JPanel
    * @param attNames
    */
   protected JComboBox<String> populateAttributesDropdown(
-          String featureType, List<String> attNames)
+          String featureType, List<String[]> attNames)
   {
+    List<String> displayNames = new ArrayList<>();
     List<String> tooltips = new ArrayList<>();
     FeatureAttributes fa = FeatureAttributes.getInstance();
-    for (String attName : attNames)
+    for (String[] attName : attNames)
     {
       String desc = fa.getDescription(featureType, attName);
       if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
       {
         desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
       }
+      displayNames.add(String.join(COLON, attName));
       tooltips.add(desc == null ? "" : desc);
     }
 
     JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
-            attNames, tooltips);
+            displayNames, tooltips);
     if (attNames.isEmpty())
     {
       attCombo.setToolTipText(MessageManager
@@ -1760,7 +1765,8 @@ public class FeatureSettings extends JPanel
     String attName = (String) attCombo.getSelectedItem();
     Condition cond = (Condition) condCombo.getSelectedItem();
     String pattern = valueField.getText();
-    KeyedMatcherI km = new KeyedMatcher(attName, cond, pattern);
+    KeyedMatcherI km = new KeyedMatcher(cond, pattern,
+            attName.split(COLON));
 
     filters.set(filterIndex, km);
   }
@@ -2134,7 +2140,7 @@ public class FeatureSettings extends JPanel
 
     if (gcol.isColourByAttribute())
     {
-      tx.append(gcol.getAttributeName());
+      tx.append(String.join(":", gcol.getAttributeName()));
     }
     else if (!gcol.isColourByLabel())
     {
index 1f92428..6b82671 100644 (file)
@@ -210,11 +210,12 @@ public class SequenceAnnotationReport
         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
         if (fc != null && fc.isColourByAttribute())
         {
-          String attName = fc.getAttributeName();
+          String[] attName = fc.getAttributeName();
           String attVal = feature.getValueAsString(attName);
           if (attVal != null)
           {
-            sb.append("; ").append(attName).append("=").append(attVal);
+            sb.append("; ").append(String.join(":", attName)).append("=")
+                    .append(attVal);
           }
         }
       }
@@ -301,7 +302,7 @@ public class SequenceAnnotationReport
    */
   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
   {
-    Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
+    Map<String, List<String>> urlSets = new LinkedHashMap<>();
     UrlLink urlLink = new UrlLink(link);
     if (!urlLink.isValid())
     {
index 168ab54..71a89b0 100644 (file)
@@ -78,10 +78,10 @@ public class FeatureColour implements FeatureColourI
   private boolean colourByLabel;
 
   /*
-   * if not null, the value of this named attribute is used for
-   * colourByLabel or graduatedColour
+   * if not null, the value of [attribute, [sub-attribute] ...]
+   *  is used for colourByLabel or graduatedColour
    */
-  private String byAttributeName;
+  private String[] attributeName;
 
   private float threshold;
 
@@ -371,7 +371,7 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
-    byAttributeName = fc.byAttributeName;
+    attributeName = fc.attributeName;
     setAboveThreshold(fc.isAboveThreshold());
     setBelowThreshold(fc.isBelowThreshold());
     setThreshold(fc.getThreshold());
@@ -593,8 +593,8 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      String label = byAttributeName == null ? feature.getDescription()
-              : feature.getValueAsString(byAttributeName);
+      String label = attributeName == null ? feature.getDescription()
+              : feature.getValueAsString(attributeName);
       return label == null ? noColour : ColorUtils
               .createColourFromName(label);
     }
@@ -611,11 +611,11 @@ public class FeatureColour implements FeatureColourI
      * no such attribute is assigned the 'no value' colour
      */
     float scr = feature.getScore();
-    if (byAttributeName != null)
+    if (attributeName != null)
     {
       try
       {
-        String attVal = feature.getValueAsString(byAttributeName);
+        String attVal = feature.getValueAsString(attributeName);
         scr = Float.valueOf(attVal);
       } catch (Throwable e)
       {
@@ -746,19 +746,19 @@ public class FeatureColour implements FeatureColourI
   @Override
   public boolean isColourByAttribute()
   {
-    return byAttributeName != null;
+    return attributeName != null;
   }
 
   @Override
-  public String getAttributeName()
+  public String[] getAttributeName()
   {
-    return byAttributeName;
+    return attributeName;
   }
 
   @Override
-  public void setAttributeName(String name)
+  public void setAttributeName(String... name)
   {
-    byAttributeName = name;
+    attributeName = name;
   }
 
 }
diff --git a/test/jalview/datamodel/features/FeatureAttributesTest.java b/test/jalview/datamodel/features/FeatureAttributesTest.java
new file mode 100644 (file)
index 0000000..e464326
--- /dev/null
@@ -0,0 +1,41 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Comparator;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class FeatureAttributesTest
+{
+
+  /**
+   * Test the method that keeps attribute names in non-case-sensitive order,
+   * including handling of 'compound' names
+   */
+  @Test(groups="Functional")
+  public void testAttributeNameComparator()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    Comparator<String[]> comp = (Comparator<String[]>) PA.getValue(fa,
+            "comparator");
+
+    assertEquals(
+            comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "a" },
+            new String[] { "csq" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq",
+        "b" }) < 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] {
+        "csq", "ac" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] {
+        "csq", "AF" }) < 0);
+  }
+}