JAL-2069 update spike branch with latest
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Nov 2017 11:50:54 +0000 (11:50 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Nov 2017 11:50:54 +0000 (11:50 +0000)
30 files changed:
README
help/html/releases.html
resources/lang/Messages.properties
src/jalview/api/FeatureColourI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/FeatureAttributes.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqPanel.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemes/FeatureColour.java
src/jalview/urls/CustomUrlProvider.java
src/jalview/util/ColorUtils.java
src/jalview/util/UrlConstants.java
src/jalview/util/matcher/KeyedMatcher.java
src/jalview/util/matcher/KeyedMatcherSet.java
src/jalview/util/matcher/KeyedMatcherSetI.java
src/jalview/util/matcher/Matcher.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java
test/jalview/schemes/FeatureColourTest.java
test/jalview/util/matcher/KeyedMatcherSetTest.java
test/jalview/util/matcher/MatcherTest.java

diff --git a/README b/README
index cbc93b1..eaf226b 100755 (executable)
--- a/README
+++ b/README
@@ -25,7 +25,10 @@ To run application:
 
 java -Djava.ext.dirs=JALVIEW_HOME/lib -cp JALVIEW_HOME/jalview.jar jalview.bin.Jalview
 
-Replace JALVIEW_HOME with the full path to Jalview Installation Directory.
+Replace JALVIEW_HOME with the full path to Jalview Installation Directory. If building from source:
+
+java -Djava.ext.dirs=JALVIEW_BUILD/dist -cp JALVIEW_BUILD/dist/jalview.jar jalview.bin.Jalview
+
 
 ##################
 
index 92af377..7be088e 100755 (executable)
@@ -131,7 +131,8 @@ li:before {
             <li><!-- JAL-2636 -->Scale mark not shown when close to right hand end of alignment</li>
             <li><!-- JAL-2684 -->Pairwise alignment only aligns selected regions of each selected sequence</li>
             <li><!-- JAL-2973 -->Alignment ruler height set incorrectly after canceling the Alignment Window's Font dialog</li>
-            <li><!-- JAL-2036 -->Show cross-references not enabled after restoring project until a new view is created</li>           
+            <li><!-- JAL-2036 -->Show cross-references not enabled after restoring project until a new view is created</li>
+            <li><!-- JAL-2756 -->Warning popup about use of SEQUENCE_ID in URL links appears when only default EMBL-EBI link is configured (since 2.10.2b2)</li>            
            </ul>
           <strong><em>Applet</em></strong><br/>
            <ul>
index 851585a..35ead5e 100644 (file)
@@ -283,7 +283,6 @@ label.sequence = Sequence
 label.view_pdb_structure = View PDB Structure
 label.min = Min:
 label.max = Max:
-label.colour_by_label = Colour by label
 label.new_feature = New Feature
 label.match_case = Match Case
 label.view_alignment_editor = View in alignment editor
@@ -532,7 +531,7 @@ label.threshold_feature_above_threshold = Above Threshold
 label.threshold_feature_below_threshold = Below Threshold
 label.adjust_threshold = Adjust threshold
 label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
-label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
+label.colour_by_label_tip = Display features of the same type with a different label using a different colour. (e.g. domain features)
 label.select_colour_minimum_value = Select Colour for Minimum Value
 label.select_colour_maximum_value = Select Colour for Maximum Value
 label.open_url_param = Open URL {0}
@@ -1335,8 +1334,18 @@ label.matchCondition_le = <=
 label.matchCondition_gt = >
 label.matchCondition_ge = >=
 label.numeric_required = The value should be numeric
-label.no_attributes_known = No attributes known
+label.no_attributes = No attributes known
+label.no_numeric_attributes = No numeric attributes known
 label.filters = Filters
 label.match_condition = Match condition
 label.join_conditions = Join conditions with
 label.feature_to_filter = Feature to filter
+label.colour_by_value = Colour by value
+label.colour_by_text = Colour by text
+label.score = Score
+label.attribute = Attribute
+label.colour_by_label = Colour by label
+label.variable_colour = Variable colour
+label.no_colour = No colour:
+label.select_no_value_colour = Select colour when no value
+label.select_new_colour = Select new colour
index 0ded079..3eebf6c 100644 (file)
@@ -56,6 +56,14 @@ public interface FeatureColourI
   Color getMaxColour();
 
   /**
+   * Returns the 'no value' colour (used when a feature lacks score, or the
+   * attribute, being used for colouring)
+   * 
+   * @return
+   */
+  Color getNoColour();
+
+  /**
    * Answers true if the feature has a single colour, i.e. if isColourByLabel()
    * and isGraduatedColour() both answer false
    * 
@@ -156,7 +164,10 @@ public interface FeatureColourI
   Color getColor(SequenceFeature feature);
 
   /**
-   * Update the min-max range for a graduated colour scheme
+   * Update the min-max range for a graduated colour scheme. Note that the
+   * colour scheme may be configured to colour by feature score, or a
+   * (numeric-valued) attribute - the caller should ensure that the correct
+   * range is being set.
    * 
    * @param min
    * @param max
@@ -169,4 +180,26 @@ public interface FeatureColourI
    * @return
    */
   String toJalviewFormat(String featureType);
+
+  /**
+   * Answers true if colour is by attribute text or numerical value
+   * 
+   * @return
+   */
+  boolean isColourByAttribute();
+
+  /**
+   * Answers the name of the attribute used for colouring if any, or null
+   * 
+   * @return
+   */
+  String getAttributeName();
+
+  /**
+   * Sets the name of the attribute used for colouring if any, or null to remove
+   * this property
+   * 
+   * @return
+   */
+  void setAttributeName(String name);
 }
index 46bd4fd..76f2705 100644 (file)
@@ -901,10 +901,7 @@ public class APopupMenu extends java.awt.PopupMenu
               .formatMessage("label.annotation_for_displayid", new Object[]
               { seq.getDisplayId(true) }));
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.seqPanel.seqCanvas.fr != null)
-                      ? ap.seqPanel.seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.seqPanel.seqCanvas.fr);
       contents.append("</p>");
     }
     Frame frame = new Frame();
index ffbd497..2110632 100755 (executable)
@@ -442,10 +442,31 @@ public class SequenceFeature implements FeatureLocationI
       }
 
       otherDetails.put(key, value);
-      FeatureAttributes.getInstance().addAttribute(this.type, key);
+      recordAttribute(key, value);
     }
   }
 
+  /**
+   * Notifies the addition of a feature attribute. This lets us keep track of
+   * which attributes are present on each feature type, and also the range of
+   * numerical-valued attributes.
+   * 
+   * @param key
+   * @param value
+   */
+  protected void recordAttribute(String key, Object value)
+  {
+    String attDesc = null;
+    if (source != null)
+    {
+      attDesc = FeatureSources.getInstance().getSource(source)
+              .getAttributeName(key);
+    }
+
+    FeatureAttributes.getInstance().addAttribute(this.type, key, attDesc,
+            value.toString());
+  }
+
   /*
    * The following methods are added to maintain the castor Uniprot mapping file
    * for the moment.
index d4e9fb0..3dc4f19 100644 (file)
@@ -5,8 +5,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.TreeMap;
 
 /**
  * A singleton class to hold the set of attributes known for each feature type
@@ -15,7 +14,91 @@ public class FeatureAttributes
 {
   private static FeatureAttributes instance = new FeatureAttributes();
 
-  private Map<String, Set<String>> attributes;
+  private Map<String, Map<String, AttributeData>> attributes;
+
+  private class AttributeData
+  {
+    /*
+     * description(s) for this attribute, if known
+     * (different feature source might have differing descriptions)
+     */
+    List<String> description;
+
+    /*
+     * minimum value (of any numeric values recorded)
+     */
+    float min = 0f;
+
+    /*
+     * maximum value (of any numeric values recorded)
+     */
+    float max = 0f;
+
+    /*
+     * flag is set true if any numeric value is detected for this attribute
+     */
+    boolean hasValue = false;
+
+    /**
+     * Note one instance of this attribute, recording unique, non-null names,
+     * and the min/max of any numerical values
+     * 
+     * @param desc
+     * @param value
+     */
+    void addInstance(String desc, String value)
+    {
+      addDescription(desc);
+
+      if (value != null)
+      {
+        try
+        {
+          float f = Float.valueOf(value);
+          min = Float.min(min, f);
+          max = Float.max(max, f);
+          hasValue = true;
+        } catch (NumberFormatException e)
+        {
+          // ok, wasn't a number, ignore for min-max purposes
+        }
+      }
+    }
+
+    /**
+     * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
+     * @return
+     */
+    public String getDescription()
+    {
+      if (description != null && description.size() == 1)
+      {
+        return description.get(0);
+      }
+      return null;
+    }
+
+    /**
+     * Adds the given description to the list of known descriptions (without
+     * duplication)
+     * 
+     * @param desc
+     */
+    public void addDescription(String desc)
+    {
+      if (desc != null)
+      {
+        if (description == null)
+        {
+          description = new ArrayList<>();
+        }
+        if (!description.contains(desc))
+        {
+          description.add(desc);
+        }
+      }
+    }
+  }
 
   /**
    * Answers the singleton instance of this class
@@ -46,7 +129,7 @@ public class FeatureAttributes
       return Collections.<String> emptyList();
     }
 
-    return new ArrayList<>(attributes.get(featureType));
+    return new ArrayList<>(attributes.get(featureType).keySet());
   }
 
   /**
@@ -58,7 +141,6 @@ public class FeatureAttributes
    */
   public boolean hasAttributes(String featureType)
   {
-
     if (attributes.containsKey(featureType))
     {
       if (!attributes.get(featureType).isEmpty())
@@ -70,24 +152,113 @@ public class FeatureAttributes
   }
 
   /**
-   * Records the given attribute name for the given feature type
+   * Records the given attribute name and description for the given feature
+   * type, and updates the min-max for any numeric value
    * 
    * @param featureType
    * @param attName
+   * @param description
+   * @param value
    */
-  public void addAttribute(String featureType, String attName)
+  public void addAttribute(String featureType, String attName,
+          String description, String value)
   {
     if (featureType == null || attName == null)
     {
       return;
     }
 
-    if (!attributes.containsKey(featureType))
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<String, AttributeData>(
+              String.CASE_INSENSITIVE_ORDER);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
     {
-      attributes.put(featureType, new TreeSet<String>(
-              String.CASE_INSENSITIVE_ORDER));
+      attData = new AttributeData();
+      atts.put(attName, attData);
     }
+    attData.addInstance(description, value);
+  }
 
-    attributes.get(featureType).add(attName);
+  /**
+   * Answers the description of the given attribute for the given feature type,
+   * if known and unique, else null
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public String getDescription(String featureType, String attName)
+  {
+    String desc = null;
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null)
+      {
+        desc = attData.getDescription();
+      }
+    }
+    return desc;
+  }
+
+  /**
+   * Answers the [min, max] value range of the given attribute for the given
+   * feature type, if known, else null. Attributes which only have text values
+   * would normally return null, however text values which happen to be numeric
+   * could result in a 'min-max' range.
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public float[] getMinMax(String featureType, String attName)
+  {
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null && attData.hasValue)
+      {
+        return new float[] { attData.min, attData.max };
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Records the given attribute description for the given feature type
+   * 
+   * @param featureType
+   * @param attName
+   * @param description
+   */
+  public void addDescription(String featureType, String attName,
+          String description)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+  
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<String, AttributeData>(
+              String.CASE_INSENSITIVE_ORDER);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addDescription(description);
   }
 }
index 2d1ba12..128481c 100644 (file)
@@ -2338,7 +2338,7 @@ public class Desktop extends jalview.jbgui.GDesktop
           {
             String link = li.next();
             if (link.contains(SEQUENCE_ID)
-                    && !link.equals(UrlConstants.DEFAULT_STRING))
+                    && !UrlConstants.isDefaultString(link))
             {
               check = true;
               int barPos = link.indexOf("|");
index 89b64a7..3fc3116 100644 (file)
@@ -22,27 +22,35 @@ package jalview.gui;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.schemes.FeatureColour;
 import jalview.util.MessageManager;
 
-import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
-import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
 import javax.swing.JColorChooser;
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
+import javax.swing.JMenuItem;
 import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButton;
 import javax.swing.JSlider;
 import javax.swing.JTextField;
 import javax.swing.border.LineBorder;
@@ -51,7 +59,8 @@ import javax.swing.event.ChangeListener;
 
 public class FeatureColourChooser extends JalviewDialog
 {
-  // FeatureSettings fs;
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
   private FeatureRenderer fr;
 
   private FeatureColourI cs;
@@ -62,11 +71,11 @@ public class FeatureColourChooser extends JalviewDialog
 
   private boolean adjusting = false;
 
-  final private float min;
+  private float min;
 
-  final private float max;
+  private float max;
 
-  final private float scaleFactor;
+  private float scaleFactor;
 
   private String type = null;
 
@@ -74,27 +83,50 @@ public class FeatureColourChooser extends JalviewDialog
 
   private JPanel maxColour = new JPanel();
 
+  private JPanel noColour = new JPanel();
+
   private JComboBox<String> threshold = new JComboBox<>();
 
   private JSlider slider = new JSlider();
 
   private JTextField thresholdValue = new JTextField(20);
 
-  // TODO implement GUI for tolower flag
-  // JCheckBox toLower = new JCheckBox();
-
   private JCheckBox thresholdIsMin = new JCheckBox();
 
-  private JCheckBox colourByLabel = new JCheckBox();
-
   private GraphLine threshline;
 
   private Color oldmaxColour;
 
   private Color oldminColour;
 
+  private Color oldNoColour;
+
   private ActionListener colourEditor = null;
 
+  /*
+   * radio buttons to select what to colour by
+   * label, attribute text, score, attribute value
+   */
+  private JRadioButton byDescription = new JRadioButton();
+
+  private JRadioButton byAttributeText = new JRadioButton();
+
+  private JRadioButton byScore = new JRadioButton();
+
+  private JRadioButton byAttributeValue = new JRadioButton();
+
+  private ActionListener changeColourAction;
+
+  /*
+   * choice of attribute (if any) for 'colour by text'
+   */
+  private JComboBox<String> textAttributeCombo;
+
+  /*
+   * choice of attribute (if any) for 'colour by value'
+   */
+  private JComboBox<String> valueAttributeCombo;
+
   /**
    * Constructor
    * 
@@ -123,7 +155,7 @@ public class FeatureColourChooser extends JalviewDialog
     String title = MessageManager
             .formatMessage("label.graduated_color_for_params", new String[]
             { theType });
-    initDialogFrame(this, true, blocking, title, 480, 185);
+    initDialogFrame(this, true, blocking, title, 450, 300);
 
     slider.addChangeListener(new ChangeListener()
     {
@@ -179,7 +211,10 @@ public class FeatureColourChooser extends JalviewDialog
     }
     else
     {
-      // promote original color to a graduated color
+      /*
+       * promote original simple color to a graduated color
+       * - by score if there is a score range, else by label
+       */
       Color bl = oldcs.getColour();
       if (bl == null)
       {
@@ -187,10 +222,11 @@ public class FeatureColourChooser extends JalviewDialog
       }
       // original colour becomes the maximum colour
       cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
-      cs.setColourByLabel(false);
+      cs.setColourByLabel(mm[0] == mm[1]);
     }
     minColour.setBackground(oldminColour = cs.getMinColour());
     maxColour.setBackground(oldmaxColour = cs.getMaxColour());
+    noColour.setBackground(oldNoColour = cs.getNoColour());
     adjusting = true;
 
     try
@@ -198,10 +234,46 @@ public class FeatureColourChooser extends JalviewDialog
       jbInit();
     } catch (Exception ex)
     {
+      ex.printStackTrace();
+      return;
     }
-    // update the gui from threshold state
+
+    /*
+     * set the initial state of options on screen
+     */
     thresholdIsMin.setSelected(!cs.isAutoScaled());
-    colourByLabel.setSelected(cs.isColourByLabel());
+
+    if (cs.isColourByLabel())
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeText.setSelected(true);
+        textAttributeCombo.setEnabled(true);
+        textAttributeCombo.setSelectedItem(cs.getAttributeName());
+      }
+      else
+      {
+        byDescription.setSelected(true);
+        textAttributeCombo.setEnabled(false);
+      }
+    }
+    else
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeValue.setSelected(true);
+        String attributeName = cs.getAttributeName();
+        valueAttributeCombo.setSelectedItem(attributeName);
+        valueAttributeCombo.setEnabled(true);
+        setAttributeMinMax(attributeName);
+      }
+      else
+      {
+        byScore.setSelected(true);
+        valueAttributeCombo.setEnabled(false);
+      }
+    }
+
     if (cs.hasThreshold())
     {
       // initialise threshold slider and selector
@@ -220,39 +292,193 @@ public class FeatureColourChooser extends JalviewDialog
     waitForInput();
   }
 
-  private void jbInit() throws Exception
+  /**
+   * Configures the initial layout
+   */
+  private void jbInit()
   {
-    this.setLayout(new GridLayout(4, 1));
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    this.setBackground(Color.white);
+
+    changeColourAction = new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        changeColour(true);
+      }
+    };
+
+    /*
+     * this panel
+     *     detailsPanel
+     *         colourByTextPanel
+     *         colourByScorePanel
+     *     okCancelPanel
+     */
+    JPanel detailsPanel = new JPanel();
+    detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
 
-    JPanel colourByPanel = initColoursPanel();
+    JPanel colourByTextPanel = initColourByTextPanel();
+    detailsPanel.add(colourByTextPanel);
 
-    JPanel thresholdPanel = initThresholdPanel();
+    JPanel colourByValuePanel = initColourByValuePanel();
+    detailsPanel.add(colourByValuePanel);
 
-    JPanel okCancelPanel = initOkCancelPanel();
+    /*
+     * 4 radio buttons select between colour by description, by
+     * attribute text, by score, or by attribute value
+     */
+    ButtonGroup bg = new ButtonGroup();
+    bg.add(byDescription);
+    bg.add(byAttributeText);
+    bg.add(byScore);
+    bg.add(byAttributeValue);
 
-    this.add(colourByPanel);
-    this.add(thresholdPanel);
+    JPanel okCancelPanel = initOkCancelPanel();
 
+    this.add(detailsPanel);
     this.add(okCancelPanel);
   }
 
   /**
-   * Lay out fields for threshold options
+   * Lay out fields for graduated colour by value
    * 
    * @return
    */
-  protected JPanel initThresholdPanel()
+  protected JPanel initColourByValuePanel()
   {
-    JPanel thresholdPanel = new JPanel();
-    thresholdPanel.setLayout(new FlowLayout());
-    threshold.addActionListener(new ActionListener()
+    JPanel byValuePanel = new JPanel();
+    byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
+    byValuePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
+            .getString("label.colour_by_value")));
+    byValuePanel.setBackground(Color.white);
+
+    /*
+     * first row - choose colour by score or by attribute, choose attribute
+     */
+    JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byWhatPanel.setBackground(Color.white);
+    byValuePanel.add(byWhatPanel);
+
+    byScore.setText(MessageManager.getString("label.score"));
+    byWhatPanel.add(byScore);
+    byScore.addActionListener(changeColourAction);
+
+    byAttributeValue.setText(MessageManager
+.getString("label.attribute"));
+    byAttributeValue.addActionListener(changeColourAction);
+    byWhatPanel.add(byAttributeValue);
+
+    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
+            type);
+    valueAttributeCombo = populateAttributesDropdown(type, attNames,
+            true);
+
+    /*
+     * if no numeric atttibutes found, disable colour by attribute value
+     */
+    if (valueAttributeCombo.getItemCount() == 0)
+    {
+      byAttributeValue.setEnabled(false);
+    }
+
+    byWhatPanel.add(valueAttributeCombo);
+
+    /*
+     * second row - min/max/no colours
+     */
+    JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    colourRangePanel.setBackground(Color.white);
+    byValuePanel.add(colourRangePanel);
+
+    minColour.setFont(JvSwingUtils.getLabelFont());
+    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    minColour.setPreferredSize(new Dimension(40, 20));
+    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
+    minColour.addMouseListener(new MouseAdapter()
     {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void mousePressed(MouseEvent e)
       {
-        changeColour(true);
+        if (minColour.isEnabled())
+        {
+          minColour_actionPerformed();
+        }
+      }
+    });
+
+    maxColour.setFont(JvSwingUtils.getLabelFont());
+    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    maxColour.setPreferredSize(new Dimension(40, 20));
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+    maxColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (maxColour.isEnabled())
+        {
+          maxColour_actionPerformed();
+        }
+      }
+    });
+    maxColour.setBorder(new LineBorder(Color.black));
+
+    noColour.setFont(JvSwingUtils.getLabelFont());
+    noColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    noColour.setPreferredSize(new Dimension(40, 20));
+    noColour.setToolTipText("Colour if feature has no attribute value");
+    noColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (e.isPopupTrigger()) // Mac: mouseReleased
+        {
+          showNoColourPopup(e);
+          return;
+        }
+        if (noColour.isEnabled())
+        {
+          noColour_actionPerformed();
+        }
+      }
+
+      @Override
+      public void mouseReleased(MouseEvent e)
+      {
+        if (e.isPopupTrigger()) // Windows: mouseReleased
+        {
+          showNoColourPopup(e);
+          e.consume();
+          return;
+        }
       }
     });
+    noColour.setBorder(new LineBorder(Color.black));
+
+    JLabel minText = new JLabel(MessageManager.getString("label.min"));
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
+    maxText.setFont(JvSwingUtils.getLabelFont());
+    JLabel noText = new JLabel(MessageManager.getString("label.no_colour"));
+    noText.setFont(JvSwingUtils.getLabelFont());
+
+    colourRangePanel.add(minText);
+    colourRangePanel.add(minColour);
+    colourRangePanel.add(maxText);
+    colourRangePanel.add(maxColour);
+    colourRangePanel.add(noText);
+    colourRangePanel.add(noColour);
+
+    /*
+     * third row - threshold options and value
+     */
+    JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    thresholdPanel.setBackground(Color.white);
+    byValuePanel.add(thresholdPanel);
+
+    threshold.addActionListener(changeColourAction);
     threshold.setToolTipText(MessageManager
             .getString("label.threshold_feature_display_by_score"));
     threshold.addItem(MessageManager
@@ -288,25 +514,65 @@ public class FeatureColourChooser extends JalviewDialog
             MessageManager.getString("label.adjust_threshold"));
     thresholdValue.setEnabled(false);
     thresholdValue.setColumns(7);
-    thresholdPanel.setBackground(Color.white);
+
+    thresholdPanel.add(threshold);
+    thresholdPanel.add(slider);
+    thresholdPanel.add(thresholdValue);
+
+    /*
+     * 4th row - threshold is min / max
+     */
+    JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    isMinMaxPanel.setBackground(Color.white);
+    byValuePanel.add(isMinMaxPanel);
     thresholdIsMin.setBackground(Color.white);
     thresholdIsMin
             .setText(MessageManager.getString("label.threshold_minmax"));
     thresholdIsMin.setToolTipText(MessageManager
             .getString("label.toggle_absolute_relative_display_threshold"));
-    thresholdIsMin.addActionListener(new ActionListener()
+    thresholdIsMin.addActionListener(changeColourAction);
+    isMinMaxPanel.add(thresholdIsMin);
+
+    return byValuePanel;
+  }
+
+  /**
+   * Show a popup menu with options to make 'no value colour' the same as Min
+   * Colour or Max Colour
+   * 
+   * @param evt
+   */
+  protected void showNoColourPopup(MouseEvent evt)
+  {
+    JPopupMenu pop = new JPopupMenu();
+
+    JMenuItem copyMin = new JMenuItem(
+            MessageManager.getString("label.min_colour"));
+    copyMin.addActionListener((new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        noColour.setBackground(minColour.getBackground());
+        changeColour(true);
+      }
+    }));
+    pop.add(copyMin);
+
+    JMenuItem copyMax = new JMenuItem(
+            MessageManager.getString("label.max_colour"));
+    copyMax.addActionListener((new ActionListener()
     {
       @Override
-      public void actionPerformed(ActionEvent actionEvent)
+      public void actionPerformed(ActionEvent e)
       {
+        noColour.setBackground(maxColour.getBackground());
         changeColour(true);
       }
-    });
-    thresholdPanel.add(threshold);
-    thresholdPanel.add(slider);
-    thresholdPanel.add(thresholdValue);
-    thresholdPanel.add(thresholdIsMin);
-    return thresholdPanel;
+    }));
+    pop.add(copyMax);
+
+    pop.show(noColour, evt.getX(), evt.getY());
   }
 
   /**
@@ -324,76 +590,41 @@ public class FeatureColourChooser extends JalviewDialog
   }
 
   /**
-   * Lay out Colour by Label and min/max colour widgets
+   * Lay out Colour by Label and attribute choice elements
    * 
    * @return
    */
-  protected JPanel initColoursPanel()
+  protected JPanel initColourByTextPanel()
   {
-    JPanel colourByPanel = new JPanel();
-    colourByPanel.setLayout(new FlowLayout());
-    colourByPanel.setBackground(Color.white);
-    minColour.setFont(JvSwingUtils.getLabelFont());
-    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
-    minColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (minColour.isEnabled())
-        {
-          minColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setFont(JvSwingUtils.getLabelFont());
-    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
-    maxColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (maxColour.isEnabled())
-        {
-          maxColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setBorder(new LineBorder(Color.black));
-    JLabel minText = new JLabel(MessageManager.getString("label.min"));
-    minText.setFont(JvSwingUtils.getLabelFont());
-    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
-    maxText.setFont(JvSwingUtils.getLabelFont());
+    JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byTextPanel.setBackground(Color.white);
+    byTextPanel.setBorder(BorderFactory.createTitledBorder(MessageManager
+            .getString("label.colour_by_text")));
+
+    byDescription.setText(MessageManager.getString("label.label"));
+    byDescription.setToolTipText(MessageManager
+            .getString("label.colour_by_label_tip"));
+    byDescription.addActionListener(changeColourAction);
+    byTextPanel.add(byDescription);
+
+    byAttributeText.setText(MessageManager.getString("label.attribute"));
+    byAttributeText.addActionListener(changeColourAction);
+    byTextPanel.add(byAttributeText);
+
+    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
+            type);
+    textAttributeCombo = populateAttributesDropdown(type, attNames, false);
+    byTextPanel.add(textAttributeCombo);
 
-    JPanel colourPanel = new JPanel();
-    colourPanel.setBackground(Color.white);
-    colourPanel.add(minText);
-    colourPanel.add(minColour);
-    colourPanel.add(maxText);
-    colourPanel.add(maxColour);
-    colourByPanel.add(colourByLabel, BorderLayout.WEST);
-    colourByPanel.add(colourPanel, BorderLayout.EAST);
-
-    colourByLabel.setBackground(Color.white);
-    colourByLabel
-            .setText(MessageManager.getString("label.colour_by_label"));
-    colourByLabel
-            .setToolTipText(MessageManager
-                    .getString("label.display_features_same_type_different_label_using_different_colour"));
-    colourByLabel.addActionListener(new ActionListener()
+    /*
+     * disable colour by attribute if no attributes
+     */
+    if (attNames.isEmpty())
     {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        changeColour(true);
-      }
-    });
+      byAttributeText.setEnabled(false);
+    }
 
-    return colourByPanel;
+    return byTextPanel;
   }
 
   /**
@@ -433,6 +664,24 @@ public class FeatureColourChooser extends JalviewDialog
   }
 
   /**
+   * Action on clicking the 'no colour' - open a colour chooser dialog, and set
+   * the selected colour (if the user does not cancel out of the dialog)
+   */
+  protected void noColour_actionPerformed()
+  {
+    Color col = JColorChooser.showDialog(this,
+            MessageManager.getString("label.select_no_value_colour"),
+            noColour.getBackground());
+    if (col != null)
+    {
+      noColour.setBackground(col);
+      noColour.setForeground(col);
+    }
+    noColour.repaint();
+    changeColour(true);
+  }
+
+  /**
    * Constructs and sets the selected colour options as the colour for the
    * feature type, and repaints the alignment, and optionally the Overview
    * and/or structure viewer if open
@@ -462,6 +711,9 @@ public class FeatureColourChooser extends JalviewDialog
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
 
+    /*
+     * make the feature colour
+     */
     FeatureColourI acg;
     if (cs.isColourByLabel())
     {
@@ -470,8 +722,23 @@ public class FeatureColourChooser extends JalviewDialog
     else
     {
       acg = new FeatureColour(oldminColour = minColour.getBackground(),
-              oldmaxColour = maxColour.getBackground(), min, max);
+              oldmaxColour = maxColour.getBackground(),
+              oldNoColour = noColour.getBackground(), min, max);
+    }
+    String attribute = null;
+    textAttributeCombo.setEnabled(false);
+    valueAttributeCombo.setEnabled(false);
+    if (byAttributeText.isSelected())
+    {
+      attribute = (String) textAttributeCombo.getSelectedItem();
+      textAttributeCombo.setEnabled(true);
+    }
+    else if (byAttributeValue.isSelected())
+    {
+      attribute = (String) valueAttributeCombo.getSelectedItem();
+      valueAttributeCombo.setEnabled(true);
     }
+    acg.setAttributeName(attribute);
 
     if (!hasThreshold)
     {
@@ -504,7 +771,7 @@ public class FeatureColourChooser extends JalviewDialog
       slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
-      thresholdIsMin.setEnabled(!colourByLabel.isSelected());
+      thresholdIsMin.setEnabled(!byDescription.isSelected());
       adjusting = false;
     }
 
@@ -526,27 +793,37 @@ public class FeatureColourChooser extends JalviewDialog
     {
       acg.setAutoScaled(true);
     }
-    acg.setColourByLabel(colourByLabel.isSelected());
+    acg.setColourByLabel(byDescription.isSelected()
+            || byAttributeText.isSelected());
+
     if (acg.isColourByLabel())
     {
       maxColour.setEnabled(false);
       minColour.setEnabled(false);
+      noColour.setEnabled(false);
       maxColour.setBackground(this.getBackground());
       maxColour.setForeground(this.getBackground());
       minColour.setBackground(this.getBackground());
       minColour.setForeground(this.getBackground());
-
+      noColour.setBackground(this.getBackground());
+      noColour.setForeground(this.getBackground());
     }
     else
     {
       maxColour.setEnabled(true);
       minColour.setEnabled(true);
+      noColour.setEnabled(true);
       maxColour.setBackground(oldmaxColour);
-      minColour.setBackground(oldminColour);
       maxColour.setForeground(oldmaxColour);
+      minColour.setBackground(oldminColour);
       minColour.setForeground(oldminColour);
+      noColour.setBackground(oldNoColour);
+      noColour.setForeground(oldNoColour);
     }
 
+    /*
+     * save the colour, and repaint stuff
+     */
     fr.setColour(type, acg);
     cs = acg;
     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
@@ -653,4 +930,82 @@ public class FeatureColourChooser extends JalviewDialog
     return cs;
   }
 
+  /**
+   * 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).
+   * 
+   * @param featureType
+   * @param attNames
+   * @param withNumericRange
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          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)
+    {
+      if (withNumericRange)
+      {
+        float[] minMax = fa.getMinMax(featureType, attName);
+        if (minMax == null)
+        {
+          continue;
+        }
+      }
+      validAtts.add(attName);
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+      {
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+      }
+      tooltips.add(desc == null ? "" : desc);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            validAtts, tooltips);
+
+    attCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        setAttributeMinMax(attCombo.getSelectedItem().toString());
+        changeColour(true);
+      }
+    });
+
+    if (validAtts.isEmpty())
+    {
+      attCombo.setToolTipText(MessageManager
+              .getString(withNumericRange ? "label.no_numeric_attributes"
+                      : "label.no_attributes"));
+    }
+
+    return attCombo;
+  }
+
+  /**
+   * Updates the min-max range and scale to be that for the given attribute name
+   * 
+   * @param attributeName
+   */
+  protected void setAttributeMinMax(String attributeName)
+  {
+    float[] minMax = FeatureAttributes.getInstance().getMinMax(type,
+            attributeName);
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+      scaleFactor = (max == min) ? 1f : 100f / (max - min);
+    }
+  }
+
 }
index d724b8c..3be597c 100644 (file)
@@ -84,6 +84,7 @@ import java.util.Vector;
 import javax.help.HelpSetException;
 import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
 import javax.swing.Icon;
 import javax.swing.JButton;
@@ -122,6 +123,8 @@ public class FeatureSettings extends JPanel
 
   private static final int MIN_HEIGHT = 400;
 
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
   DasSourceBrowser dassourceBrowser;
 
   DasSequenceFeatureFetcher dasFeatureFetcher;
@@ -414,84 +417,63 @@ public class FeatureSettings extends JPanel
 
     });
     men.add(dens);
-    if (minmax != null)
+
+    /*
+     * variable colour options include colour by label, by score,
+     * by selected attribute text, or attribute value
+     */
+    final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
+            MessageManager.getString("label.variable_colour"));
+    mxcol.setSelected(!featureColour.isSimpleColour());
+    men.add(mxcol);
+    mxcol.addActionListener(new ActionListener()
     {
-      final float[][] typeMinMax = minmax.get(type);
-      /*
-       * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
-       * this is broken at the moment and isn't that useful anyway!
-       * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
-       * ActionListener() {
-       * 
-       * public void actionPerformed(ActionEvent e) {
-       * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
-       * null); } else { minmax.put(type, typeMinMax); } }
-       * 
-       * });
-       * 
-       * men.add(chb);
-       */
-      if (typeMinMax != null && typeMinMax[0] != null)
-      {
-        // if (table.getValueAt(row, column));
-        // graduated colourschemes for those where minmax exists for the
-        // positional features
-        final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
-                "Graduated Colour");
-        mxcol.setSelected(!featureColour.isSimpleColour());
-        men.add(mxcol);
-        mxcol.addActionListener(new ActionListener()
-        {
-          JColorChooser colorChooser;
+      JColorChooser colorChooser;
 
-          @Override
-          public void actionPerformed(ActionEvent e)
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (e.getSource() == mxcol)
+        {
+          if (featureColour.isSimpleColour())
           {
-            if (e.getSource() == mxcol)
-            {
-              if (featureColour.isSimpleColour())
-              {
-                FeatureColourChooser fc = new FeatureColourChooser(me.fr,
-                        type);
-                fc.addActionListener(this);
-              }
-              else
-              {
-                // bring up simple color chooser
-                colorChooser = new JColorChooser();
-                JDialog dialog = JColorChooser.createDialog(me,
-                        "Select new Colour", true, // modal
-                        colorChooser, this, // OK button handler
-                        null); // no CANCEL button handler
-                colorChooser.setColor(featureColour.getMaxColour());
-                dialog.setVisible(true);
-              }
-            }
-            else
-            {
-              if (e.getSource() instanceof FeatureColourChooser)
-              {
-                FeatureColourChooser fc = (FeatureColourChooser) e
-                        .getSource();
-                table.setValueAt(fc.getLastColour(), selectedRow, 1);
-                table.validate();
-              }
-              else
-              {
-                // probably the color chooser!
-                table.setValueAt(new FeatureColour(colorChooser.getColor()),
-                        selectedRow, 1);
-                table.validate();
-                me.updateFeatureRenderer(
-                        ((FeatureTableModel) table.getModel()).getData(),
-                        false);
-              }
-            }
+            FeatureColourChooser fc = new FeatureColourChooser(me.fr, type);
+            fc.addActionListener(this);
           }
-
-        });
+          else
+          {
+            // bring up simple color chooser
+            colorChooser = new JColorChooser();
+            JDialog dialog = JColorChooser.createDialog(me,
+                    "Select new Colour", true, // modal
+                    colorChooser, this, // OK button handler
+                    null); // no CANCEL button handler
+            colorChooser.setColor(featureColour.getMaxColour());
+            dialog.setVisible(true);
+          }
+        }
+        else
+        {
+          if (e.getSource() instanceof FeatureColourChooser)
+          {
+            FeatureColourChooser fc = (FeatureColourChooser) e.getSource();
+            table.setValueAt(fc.getLastColour(), selectedRow, 1);
+            table.validate();
+          }
+          else
+          {
+            // probably the color chooser!
+            table.setValueAt(new FeatureColour(colorChooser.getColor()),
+                    selectedRow, 1);
+            table.validate();
+            me.updateFeatureRenderer(
+                    ((FeatureTableModel) table.getModel()).getData(), false);
+          }
+        }
       }
-    }
+
+    });
+
     JMenuItem selCols = new JMenuItem(
             MessageManager.getString("label.select_columns_containing"));
     selCols.addActionListener(new ActionListener()
@@ -1379,7 +1361,8 @@ public class FeatureSettings extends JPanel
     /*
      * the panel with the filters for the selected feature type
      */
-    JPanel filtersPanel = new JPanel(new GridLayout(0, 1));
+    JPanel filtersPanel = new JPanel();
+    filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
     filtersPanel.setBackground(Color.white);
     filtersPanel.setBorder(BorderFactory
             .createTitledBorder(MessageManager.getString("label.filters")));
@@ -1414,7 +1397,9 @@ public class FeatureSettings extends JPanel
     /*
      * panel with filters - populated by refreshFiltersDisplay
      */
-    chooseFiltersPanel = new JPanel(new GridLayout(0, 1));
+    chooseFiltersPanel = new JPanel();
+    chooseFiltersPanel.setLayout(new BoxLayout(chooseFiltersPanel,
+            BoxLayout.Y_AXIS));
     filtersPanel.add(chooseFiltersPanel);
 
     /*
@@ -1471,7 +1456,7 @@ public class FeatureSettings extends JPanel
     }
     if (!found)
     {
-      filteredFeatureChoice
+      filteredFeatureChoice // todo i18n
               .addItem("No filterable feature attributes known");
     }
 
@@ -1491,14 +1476,12 @@ public class FeatureSettings extends JPanel
      * clear the panel and list of filter conditions
      */
     chooseFiltersPanel.removeAll();
-
-    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
-
     filters.clear();
 
     /*
      * look up attributes known for feature type
      */
+    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
     List<String> attNames = FeatureAttributes.getInstance().getAttributes(
             selectedType);
 
@@ -1514,11 +1497,7 @@ public class FeatureSettings extends JPanel
       {
         orFilters.setSelected(true);
       }
-      Iterator<KeyedMatcherI> matchers = featureFilters.getMatchers();
-      while (matchers.hasNext())
-      {
-        filters.add(matchers.next());
-      }
+      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
     }
 
     /*
@@ -1530,16 +1509,16 @@ public class FeatureSettings extends JPanel
     /*
      * render the conditions in rows, each in its own JPanel
      */
-    int i = 0;
+    int filterIndex = 0;
     for (KeyedMatcherI filter : filters)
     {
       String key = filter.getKey();
       Condition condition = filter.getMatcher()
               .getCondition();
       String pattern = filter.getMatcher().getPattern();
-      JPanel row = addFilter(key, attNames, condition, pattern, i);
+      JPanel row = addFilter(key, attNames, condition, pattern, filterIndex);
       chooseFiltersPanel.add(row);
-      i++;
+      filterIndex++;
     }
 
     filtersPane.validate();
@@ -1562,11 +1541,11 @@ public class FeatureSettings extends JPanel
    * @param attNames
    * @param cond
    * @param pattern
-   * @param i
+   * @param filterIndex
    * @return
    */
   protected JPanel addFilter(String attribute, List<String> attNames,
-          Condition cond, String pattern, int i)
+          Condition cond, String pattern, int filterIndex)
   {
     JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
     filterRow.setBackground(Color.white);
@@ -1574,7 +1553,13 @@ public class FeatureSettings extends JPanel
     /*
      * inputs for attribute, condition, pattern
      */
-    JComboBox<String> attCombo = new JComboBox<>();
+    /*
+     * drop-down choice of attribute, with description as a tooltip 
+     * if we can obtain it
+     */
+    String featureType = (String) filteredFeatureChoice.getSelectedItem();
+    final JComboBox<String> attCombo = populateAttributesDropdown(
+            featureType, attNames);
     JComboBox<Condition> condCombo = new JComboBox<>();
     JTextField patternField = new JTextField(8);
 
@@ -1590,7 +1575,7 @@ public class FeatureSettings extends JPanel
         {
           if (validateFilter(patternField, condCombo))
           {
-            updateFilter(attCombo, condCombo, patternField, i);
+            updateFilter(attCombo, condCombo, patternField, filterIndex);
             filtersChanged();
           }
         }
@@ -1605,32 +1590,16 @@ public class FeatureSettings extends JPanel
       }
     };
 
-    /*
-     * drop-down choice of attribute
-     */
-    if (attNames.isEmpty())
+    if ("".equals(attribute))
     {
-      attCombo.addItem("---");
-      attCombo.setToolTipText(MessageManager
-              .getString("label.no_attributes_known"));
+      attCombo.setSelectedItem(null);
     }
     else
     {
-      attCombo.setToolTipText("");
-      for (String attName : attNames)
-      {
-        attCombo.addItem(attName);
-      }
-      if ("".equals(attribute))
-      {
-        attCombo.setSelectedItem(null);
-      }
-      else
-      {
-        attCombo.setSelectedItem(attribute);
-      }
-      attCombo.addItemListener(itemListener);
+      attCombo.setSelectedItem(attribute);
     }
+    attCombo.addItemListener(itemListener);
+
     filterRow.add(attCombo);
 
     /*
@@ -1676,7 +1645,7 @@ public class FeatureSettings extends JPanel
         @Override
         public void actionPerformed(ActionEvent e)
         {
-          filters.remove(i);
+          filters.remove(filterIndex);
           filtersChanged();
         }
       });
@@ -1687,6 +1656,39 @@ public class FeatureSettings extends JPanel
   }
 
   /**
+   * 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.
+   * 
+   * @param featureType
+   * @param attNames
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          String featureType, List<String> attNames)
+  {
+    List<String> tooltips = new ArrayList<>();
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    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) + "...";
+      }
+      tooltips.add(desc == null ? "" : desc);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            attNames, tooltips);
+    if (attNames.isEmpty())
+    {
+      attCombo.setToolTipText(MessageManager
+              .getString("label.no_attributes"));
+    }
+    return attCombo;
+  }
+
+  /**
    * Action on any change to feature filtering, namely
    * <ul>
    * <li>change of selected attribute</li>
@@ -2060,12 +2062,7 @@ public class FeatureSettings extends JPanel
             boolean isSelected, boolean hasFocus, int row, int column)
     {
       FeatureColourI cellColour = (FeatureColourI) color;
-      // JLabel comp = new JLabel();
-      // comp.
       setOpaque(true);
-      // comp.
-      // setBounds(getBounds());
-      Color newColor;
       setToolTipText(baseTT);
       setBackground(tbl.getBackground());
       if (!cellColour.isSimpleColour())
@@ -2073,14 +2070,12 @@ public class FeatureSettings extends JPanel
         Rectangle cr = tbl.getCellRect(row, column, false);
         FeatureSettings.renderGraduatedColor(this, cellColour,
                 (int) cr.getWidth(), (int) cr.getHeight());
-
       }
       else
       {
         this.setText("");
         this.setIcon(null);
-        newColor = cellColour.getColour();
-        setBackground(newColor);
+        setBackground(cellColour.getColour());
       }
       if (isSelected)
       {
@@ -2131,28 +2126,43 @@ public class FeatureSettings extends JPanel
           int w, int h)
   {
     boolean thr = false;
-    String tt = "";
-    String tx = "";
+    StringBuilder tt = new StringBuilder();
+    StringBuilder tx = new StringBuilder();
+
+    if (gcol.isColourByAttribute())
+    {
+      tx.append(gcol.getAttributeName());
+    }
+    else if (!gcol.isColourByLabel())
+    {
+      tx.append(MessageManager.getString("label.score"));
+    }
+    tx.append(" ");
     if (gcol.isAboveThreshold())
     {
       thr = true;
-      tx += ">";
-      tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
+      tx.append(">");
+      tt.append("Thresholded (Above ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isBelowThreshold())
     {
       thr = true;
-      tx += "<";
-      tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
+      tx.append("<");
+      tt.append("Thresholded (Below ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isColourByLabel())
     {
-      tt = "Coloured by label text. " + tt;
+      tt.append("Coloured by label text. ").append(tt);
       if (thr)
       {
-        tx += " ";
+        tx.append(" ");
+      }
+      if (!gcol.isColourByAttribute())
+      {
+        tx.append("Label");
       }
-      tx += "Label";
       comp.setIcon(null);
     }
     else
@@ -2168,16 +2178,17 @@ public class FeatureSettings extends JPanel
       // + ", " + minCol.getBlue() + ")");
     }
     comp.setHorizontalAlignment(SwingConstants.CENTER);
-    comp.setText(tx);
+    comp.setText(tx.toString());
     if (tt.length() > 0)
     {
       if (comp.getToolTipText() == null)
       {
-        comp.setToolTipText(tt);
+        comp.setToolTipText(tt.toString());
       }
       else
       {
-        comp.setToolTipText(tt + " " + comp.getToolTipText());
+        comp.setToolTipText(tt.append(" ").append(comp.getToolTipText())
+                .toString());
       }
     }
   }
@@ -2299,7 +2310,8 @@ class ColorEditor extends AbstractCellEditor
     button.setBorderPainted(false);
     // Set up the dialog that the button brings up.
     colorChooser = new JColorChooser();
-    dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
+    dialog = JColorChooser.createDialog(button,
+            MessageManager.getString("label.select_new_colour"), true, // modal
             colorChooser, this, // OK button handler
             null); // no CANCEL button handler
   }
index a4f79c2..a1726f1 100755 (executable)
@@ -108,8 +108,7 @@ public class IdPanel extends JPanel
       SequenceI sequence = av.getAlignment().getSequenceAt(seq);
       StringBuilder tip = new StringBuilder(64);
       seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
-              av.isShowDBRefs(), av.isShowNPFeats(),
-              sp.seqCanvas.fr.getMinMax());
+              av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
       setToolTipText(JvSwingUtils.wrapTooltip(true,
               sequence.getDisplayId(true) + " " + tip.toString()));
     }
index 0a765cb..ef96fa6 100644 (file)
@@ -24,14 +24,19 @@ import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.Rectangle;
 import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.List;
 import java.util.Objects;
 
 import javax.swing.AbstractButton;
 import javax.swing.JButton;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JMenu;
@@ -304,4 +309,46 @@ public final class JvSwingUtils
     comp.setFont(JvSwingUtils.getLabelFont());
   }
 
+  /**
+   * A helper method to build a drop-down choice of values, with tooltips for
+   * the entries
+   * 
+   * @param entries
+   * @param tooltips
+   */
+  public static JComboBox<String> buildComboWithTooltips(
+          List<String> entries, List<String> tooltips)
+  {
+    JComboBox<String> combo = new JComboBox<>();
+    final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+    combo.setRenderer(renderer);
+    for (String attName : entries)
+    {
+      combo.addItem(attName);
+    }
+    renderer.setTooltips(tooltips);
+    final MouseAdapter mouseListener = new MouseAdapter()
+    {
+      @Override
+      public void mouseEntered(MouseEvent e)
+      {
+        int j = combo.getSelectedIndex();
+        if (j > -1)
+        {
+          combo.setToolTipText(tooltips.get(j));
+        }
+      }
+      @Override
+      public void mouseExited(MouseEvent e)
+      {
+        combo.setToolTipText(null);
+      }
+    };
+    for (Component c : combo.getComponents())
+    {
+      c.addMouseListener(mouseListener);
+    }
+    return combo;
+  }
+
 }
index 6da7d4f..97d051b 100644 (file)
@@ -1635,10 +1635,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               new Object[]
               { seq.getDisplayId(true) }) + "</h2></p><p>");
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.getSeqPanel().seqCanvas.fr != null)
-                      ? ap.getSeqPanel().seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
       contents.append("</p>");
     }
     cap.setText("<html>" + contents.toString() + "</html>");
index 6148a2e..29f68c1 100644 (file)
@@ -75,12 +75,11 @@ import javax.swing.ToolTipManager;
 public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
-
 {
-  /** DOCUMENT ME!! */
+  private static final int MAX_TOOLTIP_LENGTH = 300;
+
   public SeqCanvas seqCanvas;
 
-  /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
   /*
@@ -147,35 +146,33 @@ public class SeqPanel extends JPanel
   SearchResultsI lastSearchResults;
 
   /**
-   * Creates a new SeqPanel object.
+   * Creates a new SeqPanel object
    * 
-   * @param avp
-   *          DOCUMENT ME!
-   * @param p
-   *          DOCUMENT ME!
+   * @param viewport
+   * @param alignPanel
    */
-  public SeqPanel(AlignViewport av, AlignmentPanel ap)
+  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
     linkImageURL = getClass().getResource("/images/link.gif");
     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
-    this.av = av;
+    this.av = viewport;
     setBackground(Color.white);
 
-    seqCanvas = new SeqCanvas(ap);
+    seqCanvas = new SeqCanvas(alignPanel);
     setLayout(new BorderLayout());
     add(seqCanvas, BorderLayout.CENTER);
 
-    this.ap = ap;
+    this.ap = alignPanel;
 
-    if (!av.isDataset())
+    if (!viewport.isDataset())
     {
       addMouseMotionListener(this);
       addMouseListener(this);
       addMouseWheelListener(this);
-      ssm = av.getStructureSelectionManager();
+      ssm = viewport.getStructureSelectionManager();
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
@@ -804,7 +801,7 @@ public class SeqPanel extends JPanel
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+              this.ap.getSeqPanel().seqCanvas.fr);
     }
     if (tooltipText.length() == 6) // <html>
     {
@@ -813,6 +810,11 @@ public class SeqPanel extends JPanel
     }
     else
     {
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      {
+        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+        tooltipText.append("...");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
index 6d819d3..1f92428 100644 (file)
  */
 package jalview.io;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.io.gff.GffConstants;
 import jalview.util.MessageManager;
 import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -87,14 +88,14 @@ public class SequenceAnnotationReport
       {
         return 1;
       }
-      int comp = s1 == null ? -1
-              : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
+      int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
+              .compareToIgnoreCase(s2));
       if (comp == 0)
       {
         String a1 = ref1.getAccessionId();
         String a2 = ref2.getAccessionId();
-        comp = a1 == null ? -1
-                : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
+        comp = a1 == null ? -1 : (a2 == null ? 1 : a1
+                .compareToIgnoreCase(a2));
       }
       return comp;
     }
@@ -115,9 +116,9 @@ public class SequenceAnnotationReport
     }
   };
 
-  public SequenceAnnotationReport(String linkImageURL)
+  public SequenceAnnotationReport(String linkURL)
   {
-    this.linkImageURL = linkImageURL;
+    this.linkImageURL = linkURL;
   }
 
   /**
@@ -129,13 +130,13 @@ public class SequenceAnnotationReport
    * @param minmax
    */
   public void appendFeatures(final StringBuilder sb, int rpos,
-          List<SequenceFeature> features, Map<String, float[][]> minmax)
+          List<SequenceFeature> features, FeatureRendererModel fr)
   {
     if (features != null)
     {
       for (SequenceFeature feature : features)
       {
-        appendFeature(sb, rpos, minmax, feature);
+        appendFeature(sb, rpos, fr, feature);
       }
     }
   }
@@ -149,7 +150,7 @@ public class SequenceAnnotationReport
    * @param feature
    */
   void appendFeature(final StringBuilder sb, int rpos,
-          Map<String, float[][]> minmax, SequenceFeature feature)
+          FeatureRendererModel fr, SequenceFeature feature)
   {
     if (feature.isContactFeature())
     {
@@ -162,60 +163,91 @@ public class SequenceAnnotationReport
         sb.append(feature.getType()).append(" ").append(feature.getBegin())
                 .append(":").append(feature.getEnd());
       }
+      return;
     }
-    else
+
+    if (sb.length() > 6)
+    {
+      sb.append("<br>");
+    }
+    // TODO: remove this hack to display link only features
+    boolean linkOnly = feature.getValue("linkonly") != null;
+    if (!linkOnly)
     {
-      if (sb.length() > 6)
+      sb.append(feature.getType()).append(" ");
+      if (rpos != 0)
       {
-        sb.append("<br>");
+        // we are marking a positional feature
+        sb.append(feature.begin);
       }
-      // TODO: remove this hack to display link only features
-      boolean linkOnly = feature.getValue("linkonly") != null;
-      if (!linkOnly)
+      if (feature.begin != feature.end)
       {
-        sb.append(feature.getType()).append(" ");
-        if (rpos != 0)
-        {
-          // we are marking a positional feature
-          sb.append(feature.begin);
-        }
-        if (feature.begin != feature.end)
-        {
-          sb.append(" ").append(feature.end);
-        }
+        sb.append(" ").append(feature.end);
+      }
 
-        String description = feature.getDescription();
-        if (description != null && !description.equals(feature.getType()))
-        {
-          description = StringUtils.stripHtmlTags(description);
-          sb.append("; ").append(description);
-        }
-        // check score should be shown
-        if (!Float.isNaN(feature.getScore()))
+      String description = feature.getDescription();
+      if (description != null && !description.equals(feature.getType()))
+      {
+        description = StringUtils.stripHtmlTags(description);
+        sb.append("; ").append(description);
+      }
+
+      if (showScore(feature, fr))
+      {
+        sb.append(" Score=").append(String.valueOf(feature.getScore()));
+      }
+      String status = (String) feature.getValue("status");
+      if (status != null && status.length() > 0)
+      {
+        sb.append("; (").append(status).append(")");
+      }
+
+      /*
+       * add attribute value if coloured by attribute
+       */
+      if (fr != null)
+      {
+        FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
+        if (fc != null && fc.isColourByAttribute())
         {
-          float[][] rng = (minmax == null) ? null
-                  : minmax.get(feature.getType());
-          if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
+          String attName = fc.getAttributeName();
+          String attVal = feature.getValueAsString(attName);
+          if (attVal != null)
           {
-            sb.append(" Score=").append(String.valueOf(feature.getScore()));
+            sb.append("; ").append(attName).append("=").append(attVal);
           }
         }
-        String status = (String) feature.getValue("status");
-        if (status != null && status.length() > 0)
-        {
-          sb.append("; (").append(status).append(")");
-        }
-        String clinSig = (String) feature
-                .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
-        if (clinSig != null)
-        {
-          sb.append("; ").append(clinSig);
-        }
       }
     }
   }
 
   /**
+   * Answers true if score should be shown, else false. Score is shown if it is
+   * not NaN, and the feature type has a non-trivial min-max score range
+   */
+  boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
+  {
+    if (Float.isNaN(feature.getScore()))
+    {
+      return false;
+    }
+    if (fr == null)
+    {
+      return true;
+    }
+    float[][] minMax = fr.getMinMax().get(feature.getType());
+
+    /*
+     * minMax[0] is the [min, max] score range for positional features
+     */
+    if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
    * Format and appends any hyperlinks for the sequence feature to the string
    * buffer
    * 
@@ -238,19 +270,20 @@ public class SequenceAnnotationReport
           {
             for (List<String> urllink : createLinksFrom(null, urlstring))
             {
-              sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
-                      + urllink.get(0) + "\">"
+              sb.append("<br/> <a href=\""
+                      + urllink.get(3)
+                      + "\" target=\""
+                      + urllink.get(0)
+                      + "\">"
                       + (urllink.get(0).toLowerCase()
-                              .equals(urllink.get(1).toLowerCase())
-                                      ? urllink.get(0)
-                                      : (urllink.get(0) + ":"
-                                              + urllink.get(1)))
-                      + "</a></br>");
+                              .equals(urllink.get(1).toLowerCase()) ? urllink
+                              .get(0) : (urllink.get(0) + ":" + urllink
+                              .get(1))) + "</a></br>");
             }
           } catch (Exception x)
           {
-            System.err.println(
-                    "problem when creating links from " + urlstring);
+            System.err.println("problem when creating links from "
+                    + urlstring);
             x.printStackTrace();
           }
         }
@@ -283,10 +316,10 @@ public class SequenceAnnotationReport
 
   public void createSequenceAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
-            minmax, false);
+            fr, false);
   }
 
   /**
@@ -301,13 +334,13 @@ public class SequenceAnnotationReport
    *          whether to include database references for the sequence
    * @param showNpFeats
    *          whether to include non-positional sequence features
-   * @param minmax
+   * @param fr
    * @param summary
    * @return
    */
   int createSequenceAnnotationReport(final StringBuilder sb,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax, boolean summary)
+          FeatureRendererModel fr, boolean summary)
   {
     String tmp;
     sb.append("<i>");
@@ -324,7 +357,7 @@ public class SequenceAnnotationReport
     {
       ds = ds.getDatasetSequence();
     }
-    
+
     if (showDbRefs)
     {
       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
@@ -339,7 +372,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, minmax, sf);
+        appendFeature(sb, 0, fr, sf);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
@@ -428,8 +461,7 @@ public class SequenceAnnotationReport
     }
     if (moreSources)
     {
-      sb.append("<br>").append(source)
-              .append(COMMA).append(ELLIPSIS);
+      sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
     }
     if (ellipsis)
     {
@@ -443,10 +475,10 @@ public class SequenceAnnotationReport
 
   public void createTooltipAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
-    int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
-            showNpFeats, minmax, true);
+    int maxWidth = createSequenceAnnotationReport(tip, sequence,
+            showDbRefs, showNpFeats, fr, true);
 
     if (maxWidth > 60)
     {
index 6687e6a..795cd36 100644 (file)
@@ -304,7 +304,10 @@ public class FeatureRenderer extends FeatureRendererModel
       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
 
-      // filterFeaturesForDisplay(overlaps, fc);
+      if (fc.isSimpleColour())
+      {
+        filterFeaturesForDisplay(overlaps);
+      }
 
       for (SequenceFeature sf : overlaps)
       {
index 54d1c6c..df4ea39 100644 (file)
@@ -29,10 +29,28 @@ import java.awt.Color;
 import java.util.StringTokenizer;
 
 /**
- * A class that wraps either a simple colour or a graduated colour
+ * A class that represents a colour scheme for a feature type. Options supported
+ * are currently
+ * <ul>
+ * <li>a simple colour e.g. Red</li>
+ * <li>colour by label - a colour is generated from the feature description</li>
+ * <li>graduated colour by feature score</li>
+ * <ul>
+ * <li>minimum and maximum score range must be provided</li>
+ * <li>minimum and maximum value colours should be specified</li>
+ * <li>a colour for 'no value' may optionally be provided</li>
+ * <li>colours for intermediate scores are interpolated RGB values</li>
+ * <li>there is an optional threshold above/below which to colour values</li>
+ * <li>the range may be the full value range, or may be limited by the threshold
+ * value</li>
+ * </ul>
+ * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
+ * (numeric) value of a named attribute</li> </ul>
  */
 public class FeatureColour implements FeatureColourI
 {
+  static final Color DEFAULT_NO_COLOUR = Color.LIGHT_GRAY;
+
   private static final String BAR = "|";
 
   final private Color colour;
@@ -41,10 +59,30 @@ public class FeatureColour implements FeatureColourI
 
   final private Color maxColour;
 
+  /*
+   * colour to use for colour by attribute when the 
+   * attribute value is absent
+   */
+  final private Color noColour;
+
+  /*
+   * if true, then colour has a gradient based on a numerical 
+   * range (either feature score, or an attribute value)
+   */
   private boolean graduatedColour;
 
+  /*
+   * if true, colour values are generated from a text string,
+   * either feature description, or an attribute value
+   */
   private boolean colourByLabel;
 
+  /*
+   * if not null, the value of this named attribute is used for
+   * colourByLabel or graduatedColour
+   */
+  private String byAttributeName;
+
   private float threshold;
 
   private float base;
@@ -288,6 +326,7 @@ public class FeatureColour implements FeatureColourI
   {
     minColour = Color.WHITE;
     maxColour = Color.BLACK;
+    noColour = DEFAULT_NO_COLOUR;
     minRed = 0f;
     minGreen = 0f;
     minBlue = 0f;
@@ -298,7 +337,8 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Constructor given a colour range and a score range
+   * Constructor given a colour range and a score range, defaulting 'no value
+   * colour' to be the same as minimum colour
    * 
    * @param low
    * @param high
@@ -307,36 +347,7 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color low, Color high, float min, float max)
   {
-    if (low == null)
-    {
-      low = Color.white;
-    }
-    if (high == null)
-    {
-      high = Color.black;
-    }
-    graduatedColour = true;
-    colour = null;
-    minColour = low;
-    maxColour = high;
-    threshold = Float.NaN;
-    isHighToLow = min >= max;
-    minRed = low.getRed() / 255f;
-    minGreen = low.getGreen() / 255f;
-    minBlue = low.getBlue() / 255f;
-    deltaRed = (high.getRed() / 255f) - minRed;
-    deltaGreen = (high.getGreen() / 255f) - minGreen;
-    deltaBlue = (high.getBlue() / 255f) - minBlue;
-    if (isHighToLow)
-    {
-      base = max;
-      range = min - max;
-    }
-    else
-    {
-      base = min;
-      range = max - min;
-    }
+    this(low, high, low, min, max);
   }
 
   /**
@@ -350,6 +361,7 @@ public class FeatureColour implements FeatureColourI
     colour = fc.colour;
     minColour = fc.minColour;
     maxColour = fc.maxColour;
+    noColour = fc.noColour;
     minRed = fc.minRed;
     minGreen = fc.minGreen;
     minBlue = fc.minBlue;
@@ -359,6 +371,7 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
+    byAttributeName = fc.byAttributeName;
     setAboveThreshold(fc.isAboveThreshold());
     setBelowThreshold(fc.isBelowThreshold());
     setThreshold(fc.getThreshold());
@@ -376,10 +389,46 @@ public class FeatureColour implements FeatureColourI
   public FeatureColour(FeatureColour fc, float min, float max)
   {
     this(fc);
-    graduatedColour = true;
+    setGraduatedColour(true);
     updateBounds(min, max);
   }
 
+  public FeatureColour(Color low, Color high, Color noValueColour,
+          float min, float max)
+  {
+    if (low == null)
+    {
+      low = Color.white;
+    }
+    if (high == null)
+    {
+      high = Color.black;
+    }
+    graduatedColour = true;
+    colour = null;
+    minColour = low;
+    maxColour = high;
+    noColour = noValueColour;
+    threshold = Float.NaN;
+    isHighToLow = min >= max;
+    minRed = low.getRed() / 255f;
+    minGreen = low.getGreen() / 255f;
+    minBlue = low.getBlue() / 255f;
+    deltaRed = (high.getRed() / 255f) - minRed;
+    deltaGreen = (high.getGreen() / 255f) - minGreen;
+    deltaBlue = (high.getBlue() / 255f) - minBlue;
+    if (isHighToLow)
+    {
+      base = max;
+      range = min - max;
+    }
+    else
+    {
+      base = min;
+      range = max - min;
+    }
+  }
+
   @Override
   public boolean isGraduatedColour()
   {
@@ -418,6 +467,12 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
+  public Color getNoColour()
+  {
+    return noColour;
+  }
+
+  @Override
   public boolean isColourByLabel()
   {
     return colourByLabel;
@@ -506,10 +561,7 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Updates the base and range appropriately for the given minmax range
-   * 
-   * @param min
-   * @param max
+   * {@inheritDoc}
    */
   @Override
   public void updateBounds(float min, float max)
@@ -542,7 +594,10 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      return ColorUtils.createColourFromName(feature.getDescription());
+      String label = byAttributeName == null ? feature.getDescription()
+              : feature.getValueAsString(byAttributeName);
+      return label == null ? noColour : ColorUtils
+              .createColourFromName(label);
     }
 
     if (!isGraduatedColour())
@@ -552,17 +607,32 @@ public class FeatureColour implements FeatureColourI
 
     /*
      * graduated colour case, optionally with threshold
+     * may be based on feature score on an attribute value
      * Float.NaN is assigned minimum visible score colour
+     * no such attribute is assigned the 'no value' colour
      */
     float scr = feature.getScore();
+    if (byAttributeName != null)
+    {
+      try
+      {
+        String attVal = feature.getValueAsString(byAttributeName);
+        scr = Float.valueOf(attVal);
+      } catch (Throwable e)
+      {
+        scr = Float.NaN;
+      }
+    }
     if (Float.isNaN(scr))
     {
-      return getMinColour();
+      return noColour;
     }
+
     if (isAboveThreshold() && scr <= threshold)
     {
       return null;
     }
+
     if (isBelowThreshold() && scr >= threshold)
     {
       return null;
@@ -674,4 +744,22 @@ public class FeatureColour implements FeatureColourI
     return String.format("%s\t%s", featureType, colourString);
   }
 
+  @Override
+  public boolean isColourByAttribute()
+  {
+    return byAttributeName != null;
+  }
+
+  @Override
+  public String getAttributeName()
+  {
+    return byAttributeName;
+  }
+
+  @Override
+  public void setAttributeName(String name)
+  {
+    byAttributeName = name;
+  }
+
 }
index 0d5ef99..86d5660 100644 (file)
@@ -160,10 +160,24 @@ public class CustomUrlProvider extends UrlProviderImpl
    */
   private void upgradeOldLinks(HashMap<String, UrlLink> urls)
   {
+    boolean upgrade = false;
     // upgrade old SRS link
     if (urls.containsKey(SRS_LABEL))
     {
       urls.remove(SRS_LABEL);
+      upgrade = true;
+    }
+    // upgrade old EBI link - easier just to remove and re-add than faffing
+    // around checking exact url
+    if (urls.containsKey(UrlConstants.DEFAULT_LABEL))
+    {
+      // note because this is called separately for selected and nonselected
+      // urls, the default url will not always be present
+      urls.remove(UrlConstants.DEFAULT_LABEL);
+      upgrade = true;
+    }
+    if (upgrade)
+    {
       UrlLink link = new UrlLink(UrlConstants.DEFAULT_STRING);
       link.setLabel(UrlConstants.DEFAULT_LABEL);
       urls.put(UrlConstants.DEFAULT_LABEL, link);
index d4be322..60129fb 100644 (file)
 package jalview.util;
 
 import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Random;
 
 public class ColorUtils
 {
+  private static final int MAX_CACHE_SIZE = 1729;
+  /*
+   * a cache for colours generated from text strings
+   */
+  static Map<String, Color> myColours = new HashMap<>();
 
   /**
    * Generates a random color, will mix with input color. Code taken from
@@ -260,6 +267,10 @@ public class ColorUtils
     {
       return Color.white;
     }
+    if (myColours.containsKey(name))
+    {
+      return myColours.get(name);
+    }
     int lsize = name.length();
     int start = 0;
     int end = lsize / 3;
@@ -291,6 +302,11 @@ public class ColorUtils
 
     Color color = new Color(r, g, b);
 
+    if (myColours.size() < MAX_CACHE_SIZE)
+    {
+      myColours.put(name, color);
+    }
+
     return color;
   }
 
index d6ece8d..e5cfaee 100644 (file)
@@ -57,10 +57,20 @@ public class UrlConstants
   public static final String DEFAULT_STRING = DEFAULT_LABEL
           + "|https://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
 
+  private static final String COLON = ":";
+
   /*
    * not instantiable
    */
   private UrlConstants()
   {
   }
+
+  public static boolean isDefaultString(String link)
+  {
+    String sublink = link.substring(link.indexOf(COLON) + 1);
+    String subdefault = DEFAULT_STRING
+            .substring(DEFAULT_STRING.indexOf(COLON) + 1);
+    return sublink.equalsIgnoreCase(subdefault);
+  }
 }
index 5e42e1c..cd952e7 100644 (file)
@@ -79,7 +79,15 @@ public class KeyedMatcher implements KeyedMatcherI
   {
     StringBuilder sb = new StringBuilder();
     sb.append(key).append(" ").append(matcher.getCondition().toString())
-            .append(" ").append(matcher.getPattern());
+            .append(" ");
+    if (matcher.getCondition().isNumeric())
+    {
+      sb.append(matcher.getPattern());
+    }
+    else
+    {
+      sb.append("'").append(matcher.getPattern()).append("'");
+    }
 
     return sb.toString();
   }
index adc04ba..35a41c2 100644 (file)
@@ -1,7 +1,6 @@
 package jalview.util.matcher;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.function.Function;
 
@@ -91,9 +90,9 @@ public class KeyedMatcherSet implements KeyedMatcherSetI
   }
 
   @Override
-  public Iterator<KeyedMatcherI> getMatchers()
+  public Iterable<KeyedMatcherI> getMatchers()
   {
-    return matchConditions.iterator();
+    return matchConditions;
   }
 
   @Override
index 7cbebab..25dc96e 100644 (file)
@@ -1,6 +1,5 @@
 package jalview.util.matcher;
 
-import java.util.Iterator;
 import java.util.function.Function;
 
 /**
@@ -54,7 +53,7 @@ public interface KeyedMatcherSetI
    * 
    * @return
    */
-  Iterator<KeyedMatcherI> getMatchers();
+  Iterable<KeyedMatcherI> getMatchers();
 
   /**
    * Answers true if this object contains no conditions
index d8c9361..a213a17 100644 (file)
@@ -213,6 +213,17 @@ public class Matcher implements MatcherI
   @Override
   public String toString()
   {
-    return condition.name() + " " + pattern;
+    StringBuilder sb = new StringBuilder();
+    sb.append(condition.name()).append(" ");
+    if (condition.isNumeric())
+    {
+      sb.append(pattern);
+    }
+    else
+    {
+      sb.append("'").append(pattern).append("'");
+    }
+
+    return sb.toString();
   }
 }
index 6461748..c2f5bb7 100644 (file)
@@ -490,7 +490,8 @@ public abstract class FeatureRendererModel
               if (mmrange != null)
               {
                 FeatureColourI fc = featureColours.get(oldRender[j]);
-                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                        && !fc.isColourByAttribute())
                 {
                   fc.updateBounds(mmrange[0][0], mmrange[0][1]);
                 }
@@ -520,7 +521,8 @@ public abstract class FeatureRendererModel
         if (mmrange != null)
         {
           FeatureColourI fc = featureColours.get(newf[i]);
-          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                  && !fc.isColourByAttribute())
           {
             fc.updateBounds(mmrange[0][0], mmrange[0][1]);
           }
@@ -586,7 +588,8 @@ public abstract class FeatureRendererModel
    */
   protected boolean showFeatureOfType(String type)
   {
-    return type == null ? false : av.getFeaturesDisplayed().isVisible(type);
+    return type == null ? false : (av.getFeaturesDisplayed() == null ? true
+            : av.getFeaturesDisplayed().isVisible(type));
   }
 
   @Override
@@ -1000,23 +1003,19 @@ public abstract class FeatureRendererModel
 
   /**
    * Removes from the list of features any that duplicate the location of a
-   * feature of the same type (unless feature is filtered out, or a graduated
-   * colour scheme or colour by label is applied). Should be used only for
-   * features of the same feature colour (which normally implies the same
-   * feature type).
+   * feature of the same type. Should be used only for features of the same,
+   * simple, feature colour (which normally implies the same feature type). Does
+   * not check visibility settings for feature type or feature group.
    * 
    * @param features
-   * @param fc
    */
-  public void filterFeaturesForDisplay(List<SequenceFeature> features,
-          FeatureColourI fc)
+  public void filterFeaturesForDisplay(List<SequenceFeature> features)
   {
     if (features.isEmpty())
     {
       return;
     }
     SequenceFeatures.sortFeatures(features, true);
-    boolean simpleColour = fc == null || fc.isSimpleColour();
     SequenceFeature lastFeature = null;
 
     Iterator<SequenceFeature> it = features.iterator();
@@ -1030,15 +1029,12 @@ public abstract class FeatureRendererModel
        * (checking type and isContactFeature as a fail-safe here, although
        * currently they are guaranteed to match in this context)
        */
-      if (simpleColour)
+      if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+              && sf.getEnd() == lastFeature.getEnd()
+              && sf.isContactFeature() == lastFeature.isContactFeature()
+              && sf.getType().equals(lastFeature.getType()))
       {
-        if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
-                && sf.getEnd() == lastFeature.getEnd()
-                && sf.isContactFeature() == lastFeature.isContactFeature()
-                && sf.getType().equals(lastFeature.getType()))
-        {
-          it.remove();
-        }
+        it.remove();
       }
       lastFeature = sf;
     }
index 9e61bec..87e35c7 100644 (file)
@@ -23,15 +23,18 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 import jalview.io.gff.GffConstants;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
-import java.util.HashMap;
-import java.util.Hashtable;
+import java.awt.Color;
 import java.util.Map;
 
 import junit.extensions.PA;
@@ -95,8 +98,9 @@ public class SequenceAnnotationReportTest
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
             "group");
 
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
-    sar.appendFeature(sb, 1, minmax, sf);
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    sar.appendFeature(sb, 1, fr, sf);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -106,7 +110,7 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     // <br> is appended to a buffer > 6 in length
     assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
@@ -116,7 +120,7 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -132,8 +136,11 @@ public class SequenceAnnotationReportTest
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
+  /**
+   * A specific attribute value is included if it is used to colour the feature
+   */
   @Test(groups = "Functional")
-  public void testAppendFeature_clinicalSignificance()
+  public void testAppendFeature_colouredByAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -141,12 +148,35 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
     sf.setValue("clinical_significance", "Benign");
 
-    sar.appendFeature(sb, 1, null, sf);
-    assertEquals("METAL 1 3; Fe2-S; Benign", sb.toString());
+    /*
+     * first with no colour by attribute
+     */
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString());
+
+    /*
+     * then with colour by an attribute the feature lacks
+     */
+    FeatureColourI fc = new FeatureColour(Color.white, Color.black, 5, 10);
+    fc.setAttributeName("Pfam");
+    fr.setColour("METAL", fc);
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
+
+    /*
+     * then with colour by an attribute the feature has
+     */
+    fc.setAttributeName("clinical_significance");
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
+            sb.toString());
   }
 
   @Test(groups = "Functional")
-  public void testAppendFeature_withScoreStatusClinicalSignificance()
+  public void testAppendFeature_withScoreStatusAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -154,11 +184,17 @@ public class SequenceAnnotationReportTest
             "group");
     sf.setStatus("Confirmed");
     sf.setValue("clinical_significance", "Benign");
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    FeatureColourI fc = new FeatureColour(Color.white, Color.blue, 12, 22);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
 
-    assertEquals("METAL 1 3; Fe2-S Score=1.3; (Confirmed); Benign",
+    assertEquals(
+            "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
             sb.toString());
   }
 
@@ -226,7 +262,7 @@ public class SequenceAnnotationReportTest
             null));
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, null);
-    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -244,10 +280,13 @@ public class SequenceAnnotationReportTest
      */
     seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
             null));
-    Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
     minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
     assertEquals(expected, sb.toString());
     
@@ -260,19 +299,20 @@ public class SequenceAnnotationReportTest
     sf.setValue("linkonly", Boolean.TRUE);
     seq.addSequenceFeature(sf);
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     assertEquals(expected, sb.toString()); // unchanged!
 
     /*
-     * 'clinical_significance' currently being specially included
+     * 'clinical_significance' attribute only included when
+     * used for feature colouring
      */
     SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
             5f, null);
     sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign");
     seq.addSequenceFeature(sf2);
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
-    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -280,18 +320,24 @@ public class SequenceAnnotationReportTest
      */
     seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1"));
     seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419"));
+
     // with showDbRefs = false
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, false, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, false, true, fr);
     assertEquals(expected, sb.toString()); // unchanged
-    // with showDbRefs = true
+
+    // with showDbRefs = true, colour Variant features by clinical_significance
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
-    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    FeatureColourI fc = new FeatureColour(Color.green, Color.pink, 2, 3);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("Variant", fc);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>"
+            + "Type1 ; Nonpos<br>Variant ; Havana; clinical_significance=benign</i>";
     assertEquals(expected, sb.toString());
     // with showNonPositionalFeatures = false
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, fr);
     expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
     assertEquals(expected, sb.toString());
 
index d3cddf9..438feba 100644 (file)
@@ -264,7 +264,7 @@ public class FeatureRendererTest
     FeatureRenderer fr = new FeatureRenderer(av);
 
     List<SequenceFeature> features = new ArrayList<>();
-    fr.filterFeaturesForDisplay(features, null); // empty list, does nothing
+    fr.filterFeaturesForDisplay(features); // empty list, does nothing
 
     SequenceI seq = av.getAlignment().getSequenceAt(0);
     SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
@@ -297,7 +297,7 @@ public class FeatureRendererTest
      * filter out duplicate (co-located) features
      * note: which gets removed is not guaranteed
      */
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    fr.filterFeaturesForDisplay(features);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf1) || features.contains(sf4));
     assertFalse(features.contains(sf1) && features.contains(sf4));
@@ -306,58 +306,17 @@ public class FeatureRendererTest
     assertTrue(features.contains(sf5));
 
     /*
-     * hide group 3 - sf3 is removed, sf2 is retained
-     */
-    fr.setGroupVisibility("group3", false);
-    features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
-    assertEquals(features.size(), 3);
-    assertTrue(features.contains(sf1) || features.contains(sf4));
-    assertFalse(features.contains(sf1) && features.contains(sf4));
-    assertTrue(features.contains(sf2));
-    assertFalse(features.contains(sf3));
-    assertTrue(features.contains(sf5));
-
-    /*
-     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+     * hide groups 2 and 3 makes no difference to this method
      */
     fr.setGroupVisibility("group2", false);
-    fr.setGroupVisibility("group3", true);
+    fr.setGroupVisibility("group3", false);
     features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, null);
+    fr.filterFeaturesForDisplay(features);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf1) || features.contains(sf4));
     assertFalse(features.contains(sf1) && features.contains(sf4));
-    assertFalse(features.contains(sf2));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf5));
-
-    /*
-     * no filtering of co-located features with graduated colour scheme
-     * filterFeaturesForDisplay does _not_ check colour threshold
-     * sf2 is removed as its group is hidden
-     */
-    features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
-            Color.white, 0f, 1f));
-    assertEquals(features.size(), 4);
-    assertTrue(features.contains(sf1));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf4));
-    assertTrue(features.contains(sf5));
-
-    /*
-     * co-located features with colour by label
-     * should not get filtered
-     */
-    features = seq.getSequenceFeatures();
-    FeatureColour fc = new FeatureColour(Color.black);
-    fc.setColourByLabel(true);
-    fr.filterFeaturesForDisplay(features, fc);
-    assertEquals(features.size(), 4);
-    assertTrue(features.contains(sf1));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf2) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
     assertTrue(features.contains(sf5));
   }
 }
index 7a72c15..3f60152 100644 (file)
@@ -33,6 +33,8 @@ import jalview.util.Format;
 
 import java.awt.Color;
 
+import junit.extensions.PA;
+
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -57,6 +59,8 @@ public class FeatureColourTest
     assertTrue(fc1.getColour().equals(Color.RED));
     assertFalse(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
 
     /*
      * min-max colour
@@ -68,9 +72,31 @@ public class FeatureColourTest
     assertTrue(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
     assertTrue(fc1.isAboveThreshold());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
     assertEquals(12f, fc1.getThreshold());
     assertEquals(Color.gray, fc1.getMinColour());
     assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.gray, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+
+    /*
+     * min-max-noValue colour
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
     assertEquals(10f, fc1.getMin());
     assertEquals(20f, fc1.getMax());
 
@@ -82,6 +108,102 @@ public class FeatureColourTest
     fc1 = new FeatureColour(fc);
     assertTrue(fc1.isColourByLabel());
     assertFalse(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+
+    /*
+     * colour by attribute (label)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isColourByLabel());
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByAttribute());
+    assertEquals("AF", fc1.getAttributeName());
+
+    /*
+     * colour by attribute (value)
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertEquals("AF", fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCopyConstructor_minMax()
+  {
+    /*
+     * graduated colour
+     */
+    FeatureColour fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds
+     */
+    FeatureColour fc1 = new FeatureColour(fc, 2f, 6f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
+    assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * update min-max bounds - high to low
+     */
+    fc1 = new FeatureColour(fc, 23f, 16f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(23f, fc1.getMin());
+    assertEquals(16f, fc1.getMax());
+    assertTrue((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * colour by label
+     */
+    fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    fc.setColourByLabel(true);
+    assertFalse(fc.isGraduatedColour());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds - converts to graduated colour
+     */
+    fc1 = new FeatureColour(fc, 2f, 6f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
   }
 
   @Test(groups = { "Functional" })
@@ -106,8 +228,11 @@ public class FeatureColourTest
   @Test(groups = { "Functional" })
   public void testGetColor_Graduated()
   {
-    // graduated colour from score 0 to 100, gray(128, 128, 128) to red(255, 0,
-    // 0)
+    /*
+     * graduated colour from 
+     * score 0 to 100
+     * gray(128, 128, 128) to red(255, 0, 0)
+     */
     FeatureColour fc = new FeatureColour(Color.GRAY, Color.RED, 0f, 100f);
     // feature score is 75 which is 3/4 of the way from GRAY to RED
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
@@ -340,4 +465,59 @@ public class FeatureColourTest
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_colourByAttributeText()
+  {
+    FeatureColour fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("consequence");
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
+            null);
+
+    /*
+     * if feature has no such attribute, use 'no value' colour
+     */
+    assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf));
+
+    /*
+     * if feature has attribute, generate colour from value
+     */
+    sf.setValue("consequence", "benign");
+    Color expected = ColorUtils.createColourFromName("benign");
+    assertEquals(expected, fc.getColor(sf));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_GraduatedByAttributeValue()
+  {
+    /*
+     * graduated colour based on attribute value for AF
+     * given a min-max range of 0-100
+     */
+    FeatureColour fc = new FeatureColour(new Color(50, 100, 150),
+            new Color(150, 200, 250), Color.yellow, 0f, 100f);
+    String attName = "AF";
+    fc.setAttributeName(attName);
+
+    /*
+     * first case: feature lacks the attribute - use 'no value' colour
+     */
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
+            null);
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * second case: attribute present but not numeric - treat as if absent
+     */
+    sf.setValue(attName, "twelve");
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * third case: valid attribute value
+     */
+    sf.setValue(attName, "20.0");
+    Color expected = new Color(70, 120, 170);
+    assertEquals(expected, fc.getColor(sf));
+  }
 }
index 0d2767d..3018cb6 100644 (file)
@@ -2,8 +2,10 @@ package jalview.util.matcher;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import java.util.Iterator;
 import java.util.function.Function;
 
 import org.testng.annotations.Test;
@@ -66,7 +68,7 @@ public class KeyedMatcherSetTest
     assertEquals(km1.toString(), "AF < 1.2");
 
     KeyedMatcher km2 = new KeyedMatcher("CLIN_SIG", Condition.NotContains, "path");
-    assertEquals(km2.toString(), "CLIN_SIG Does not contain PATH");
+    assertEquals(km2.toString(), "CLIN_SIG Does not contain 'PATH'");
 
     /*
      * AND them
@@ -77,7 +79,7 @@ public class KeyedMatcherSetTest
     assertEquals(kms.toString(), "(AF < 1.2)");
     kms.and(km2);
     assertEquals(kms.toString(),
-            "(AF < 1.2) AND (CLIN_SIG Does not contain PATH)");
+            "(AF < 1.2) AND (CLIN_SIG Does not contain 'PATH')");
 
     /*
      * OR them
@@ -88,7 +90,7 @@ public class KeyedMatcherSetTest
     assertEquals(kms.toString(), "(AF < 1.2)");
     kms.or(km2);
     assertEquals(kms.toString(),
-            "(AF < 1.2) OR (CLIN_SIG Does not contain PATH)");
+            "(AF < 1.2) OR (CLIN_SIG Does not contain 'PATH')");
   }
 
   @Test
@@ -121,4 +123,35 @@ public class KeyedMatcherSetTest
     kms.and(km);
     assertFalse(kms.isEmpty());
   }
+
+  @Test
+  public void testGetMatchers()
+  {
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+
+    /*
+     * empty iterable:
+     */
+    Iterator<KeyedMatcherI> iterator = kms.getMatchers().iterator();
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one matcher:
+     */
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.GE, -2F);
+    kms.and(km1);
+    iterator = kms.getMatchers().iterator();
+    assertSame(km1, iterator.next());
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two matchers:
+     */
+    KeyedMatcherI km2 = new KeyedMatcher("AF", Condition.LT, 8F);
+    kms.and(km2);
+    iterator = kms.getMatchers().iterator();
+    assertSame(km1, iterator.next());
+    assertSame(km2, iterator.next());
+    assertFalse(iterator.hasNext());
+  }
 }
index d988c3a..489cdce 100644 (file)
@@ -196,10 +196,10 @@ public class MatcherTest
     assertEquals(m.toString(), "LT 1.2E-6");
 
     m = new Matcher(Condition.NotMatches, "ABC");
-    assertEquals(m.toString(), "NotMatches ABC");
+    assertEquals(m.toString(), "NotMatches 'ABC'");
 
     m = new Matcher(Condition.Contains, -1.2f);
-    assertEquals(m.toString(), "Contains -1.2");
+    assertEquals(m.toString(), "Contains '-1.2'");
   }
 
   @Test