Merge branch 'bug/JAL-2791exportFilteredFeature' into merge/JAL-2791
[jalview.git] / src / jalview / viewmodel / seqfeatures / FeatureRendererModel.java
index e912720..4af6fde 100644 (file)
 package jalview.viewmodel.seqfeatures;
 
 import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
 import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
-import jalview.schemes.GraduatedColor;
-import jalview.viewmodel.AlignmentViewport;
+import jalview.schemes.FeatureColour;
+import jalview.util.ColorUtils;
 
 import java.awt.Color;
 import java.beans.PropertyChangeListener;
@@ -35,6 +38,7 @@ import java.beans.PropertyChangeSupport;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -42,30 +46,60 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-public abstract class FeatureRendererModel implements
-        jalview.api.FeatureRenderer
+public abstract class FeatureRendererModel
+        implements jalview.api.FeatureRenderer
 {
+  /*
+   * a data bean to hold one row of feature settings from the gui
+   */
+  public static class FeatureSettingsBean
+  {
+    public final String featureType;
 
-  /**
+    public final FeatureColourI featureColour;
+
+    public final FeatureMatcherSetI filter;
+
+    public final Boolean show;
+
+    public FeatureSettingsBean(String type, FeatureColourI colour,
+            FeatureMatcherSetI theFilter, Boolean isShown)
+    {
+      featureType = type;
+      featureColour = colour;
+      filter = theFilter;
+      show = isShown;
+    }
+  }
+
+  /*
    * global transparency for feature
    */
   protected float transparency = 1.0f;
 
-  protected Map<String, Object> featureColours = new ConcurrentHashMap<String, Object>();
-
-  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<String, Boolean>();
+  /*
+   * colour scheme for each feature type
+   */
+  protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
 
-  protected Object currentColour;
+  /*
+   * visibility flag for each feature group
+   */
+  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
 
   /*
-   * ordering of rendering, where last means on top
+   * filters for each feature type
    */
+  protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+
   protected String[] renderOrder;
 
+  Map<String, Float> featureOrder = null;
+
   protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
           this);
 
-  protected AlignmentViewport av;
+  protected AlignViewportI av;
 
   @Override
   public AlignViewportI getViewport()
@@ -100,6 +134,7 @@ public abstract class FeatureRendererModel implements
     this.renderOrder = frs.renderOrder;
     this.featureGroups = frs.featureGroups;
     this.featureColours = frs.featureColours;
+    this.featureFilters = frs.featureFilters;
     this.transparency = frs.transparency;
     this.featureOrder = frs.featureOrder;
     if (av != null && av != fr.getViewport())
@@ -117,11 +152,10 @@ public abstract class FeatureRendererModel implements
           synchronized (fd)
           {
             fd.clear();
-            java.util.Iterator<String> fdisp = _fr.getFeaturesDisplayed()
-                    .getVisibleFeatures();
-            while (fdisp.hasNext())
+            for (String type : _fr.getFeaturesDisplayed()
+                    .getVisibleFeatures())
             {
-              fd.setVisible(fdisp.next());
+              fd.setVisible(type);
             }
           }
         }
@@ -157,7 +191,7 @@ public abstract class FeatureRendererModel implements
     {
       av.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
     }
-    List<String> nft = new ArrayList<String>();
+    List<String> nft = new ArrayList<>();
     for (String featureType : featureTypes)
     {
       if (!fdi.isRegistered(featureType))
@@ -193,9 +227,9 @@ public abstract class FeatureRendererModel implements
     renderOrder = neworder;
   }
 
-  protected Hashtable minmax = new Hashtable();
+  protected Map<String, float[][]> minmax = new Hashtable<>();
 
-  public Hashtable getMinMax()
+  public Map<String, float[][]> getMinMax()
   {
     return minmax;
   }
@@ -209,14 +243,15 @@ public abstract class FeatureRendererModel implements
    */
   protected final byte[] normaliseScore(SequenceFeature sequenceFeature)
   {
-    float[] mm = ((float[][]) minmax.get(sequenceFeature.type))[0];
+    float[] mm = minmax.get(sequenceFeature.type)[0];
     final byte[] r = new byte[] { 0, (byte) 255 };
     if (mm != null)
     {
       if (r[0] != 0 || mm[0] < 0.0)
       {
         r[0] = 1;
-        r[1] = (byte) ((int) 128.0 + 127.0 * (sequenceFeature.score / mm[1]));
+        r[1] = (byte) ((int) 128.0
+                + 127.0 * (sequenceFeature.score / mm[1]));
       }
       else
       {
@@ -265,46 +300,44 @@ public abstract class FeatureRendererModel implements
   }
 
   @Override
-  public List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
+  public List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    ArrayList<SequenceFeature> tmp = new ArrayList<SequenceFeature>();
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-
-    if (features != null)
+    /*
+     * include features at the position provided their feature type is 
+     * displayed, and feature group is null or marked for display
+     */
+    List<SequenceFeature> result = new ArrayList<>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
     {
-      for (int i = 0; i < features.length; i++)
-      {
-        if (!av.areFeaturesDisplayed()
-                || !av.getFeaturesDisplayed().isVisible(
-                        features[i].getType()))
-        {
-          continue;
-        }
+      return result;
+    }
 
-        if (features[i].featureGroup != null
-                && featureGroups != null
-                && featureGroups.containsKey(features[i].featureGroup)
-                && !featureGroups.get(features[i].featureGroup)
-                        .booleanValue())
-        {
-          continue;
-        }
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.findFeatures(column, column,
+            visibleTypes);
 
-        if ((features[i].getBegin() <= res)
-                && (features[i].getEnd() >= res))
-        {
-          tmp.add(features[i]);
-        }
+    /*
+     * include features unless their feature group is not displayed, or
+     * they are hidden (have no colour) based on a filter or colour threshold
+     */
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
+      {
+        result.add(sf);
       }
     }
-    return tmp;
+    return result;
   }
 
   /**
    * Searches alignment for all features and updates colours
    * 
    * @param newMadeVisible
-   *          if true newly added feature types will be rendered immediatly
+   *          if true newly added feature types will be rendered immediately
    *          TODO: check to see if this method should actually be proxied so
    *          repaint events can be propagated by the renderer code
    */
@@ -326,8 +359,7 @@ public abstract class FeatureRendererModel implements
     }
     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
 
-    ArrayList<String> allfeatures = new ArrayList<String>();
-    ArrayList<String> oldfeatures = new ArrayList<String>();
+    Set<String> oldfeatures = new HashSet<>();
     if (renderOrder != null)
     {
       for (int i = 0; i < renderOrder.length; i++)
@@ -338,94 +370,110 @@ public abstract class FeatureRendererModel implements
         }
       }
     }
-    if (minmax == null)
-    {
-      minmax = new Hashtable();
-    }
+
     AlignmentI alignment = av.getAlignment();
+    List<String> allfeatures = new ArrayList<>();
+
     for (int i = 0; i < alignment.getHeight(); i++)
     {
       SequenceI asq = alignment.getSequenceAt(i);
-      SequenceFeature[] features = asq.getSequenceFeatures();
-
-      if (features == null)
-      {
-        continue;
-      }
-
-      int index = 0;
-      while (index < features.length)
+      for (String group : asq.getFeatures().getFeatureGroups(true))
       {
-        if (!featuresDisplayed.isRegistered(features[index].getType()))
+        boolean groupDisplayed = true;
+        if (group != null)
         {
-          String fgrp = features[index].getFeatureGroup();
-          if (fgrp != null)
+          if (featureGroups.containsKey(group))
           {
-            Boolean groupDisplayed = featureGroups.get(fgrp);
-            if (groupDisplayed == null)
-            {
-              groupDisplayed = Boolean.valueOf(newMadeVisible);
-              featureGroups.put(fgrp, groupDisplayed);
-            }
-            if (!groupDisplayed.booleanValue())
-            {
-              index++;
-              continue;
-            }
+            groupDisplayed = featureGroups.get(group);
           }
-          if (!(features[index].begin == 0 && features[index].end == 0))
+          else
           {
-            // If beginning and end are 0, the feature is for the whole sequence
-            // and we don't want to render the feature in the normal way
-
-            if (newMadeVisible
-                    && !oldfeatures.contains(features[index].getType()))
-            {
-              // this is a new feature type on the alignment. Mark it for
-              // display.
-              featuresDisplayed.setVisible(features[index].getType());
-              setOrder(features[index].getType(), 0);
-            }
+            groupDisplayed = newMadeVisible;
+            featureGroups.put(group, groupDisplayed);
           }
         }
-        if (!allfeatures.contains(features[index].getType()))
-        {
-          allfeatures.add(features[index].getType());
-        }
-        if (!Float.isNaN(features[index].score))
+        if (groupDisplayed)
         {
-          int nonpos = features[index].getBegin() >= 1 ? 0 : 1;
-          float[][] mm = (float[][]) minmax.get(features[index].getType());
-          if (mm == null)
+          Set<String> types = asq.getFeatures().getFeatureTypesForGroups(
+                  true, group);
+          for (String type : types)
           {
-            mm = new float[][] { null, null };
-            minmax.put(features[index].getType(), mm);
-          }
-          if (mm[nonpos] == null)
-          {
-            mm[nonpos] = new float[] { features[index].score,
-                features[index].score };
-
-          }
-          else
-          {
-            if (mm[nonpos][0] > features[index].score)
+            if (!allfeatures.contains(type)) // or use HashSet and no test?
             {
-              mm[nonpos][0] = features[index].score;
-            }
-            if (mm[nonpos][1] < features[index].score)
-            {
-              mm[nonpos][1] = features[index].score;
+              allfeatures.add(type);
             }
+            updateMinMax(asq, type, true); // todo: for all features?
           }
         }
-        index++;
       }
     }
+
+    // uncomment to add new features in alphebetical order (but JAL-2575)
+    // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER);
+    if (newMadeVisible)
+    {
+      for (String type : allfeatures)
+      {
+        if (!oldfeatures.contains(type))
+        {
+          featuresDisplayed.setVisible(type);
+          setOrder(type, 0);
+        }
+      }
+    }
+
     updateRenderOrder(allfeatures);
     findingFeatures = false;
   }
 
+  /**
+   * Updates the global (alignment) min and max values for a feature type from
+   * the score for a sequence, if the score is not NaN. Values are stored
+   * separately for positional and non-positional features.
+   * 
+   * @param seq
+   * @param featureType
+   * @param positional
+   */
+  protected void updateMinMax(SequenceI seq, String featureType,
+          boolean positional)
+  {
+    float min = seq.getFeatures().getMinimumScore(featureType, positional);
+    if (Float.isNaN(min))
+    {
+      return;
+    }
+
+    float max = seq.getFeatures().getMaximumScore(featureType, positional);
+
+    /*
+     * stored values are 
+     * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} }
+     */
+    if (minmax == null)
+    {
+      minmax = new Hashtable<>();
+    }
+    synchronized (minmax)
+    {
+      float[][] mm = minmax.get(featureType);
+      int index = positional ? 0 : 1;
+      if (mm == null)
+      {
+        mm = new float[][] { null, null };
+        minmax.put(featureType, mm);
+      }
+      if (mm[index] == null)
+      {
+        mm[index] = new float[] { min, max };
+      }
+      else
+      {
+        mm[index][0] = Math.min(mm[index][0], min);
+        mm[index][1] = Math.max(mm[index][1], max);
+      }
+    }
+  }
   protected Boolean firing = Boolean.FALSE;
 
   /**
@@ -441,10 +489,9 @@ public abstract class FeatureRendererModel implements
    */
   private void updateRenderOrder(List<String> allFeatures)
   {
-    List<String> allfeatures = new ArrayList<String>(allFeatures);
+    List<String> allfeatures = new ArrayList<>(allFeatures);
     String[] oldRender = renderOrder;
     renderOrder = new String[allfeatures.size()];
-    Object mmrange, fc = null;
     boolean initOrders = (featureOrder == null);
     int opos = 0;
     if (oldRender != null && oldRender.length > 0)
@@ -455,7 +502,8 @@ public abstract class FeatureRendererModel implements
         {
           if (initOrders)
           {
-            setOrder(oldRender[j], (1 - (1 + (float) j) / oldRender.length));
+            setOrder(oldRender[j],
+                    (1 - (1 + (float) j) / oldRender.length));
           }
           if (allfeatures.contains(oldRender[j]))
           {
@@ -464,16 +512,14 @@ public abstract class FeatureRendererModel implements
             allfeatures.remove(oldRender[j]);
             if (minmax != null)
             {
-              mmrange = minmax.get(oldRender[j]);
+              float[][] mmrange = minmax.get(oldRender[j]);
               if (mmrange != null)
               {
-                fc = featureColours.get(oldRender[j]);
-                if (fc != null && fc instanceof GraduatedColor
-                        && ((GraduatedColor) fc).isAutoScale())
+                FeatureColourI fc = featureColours.get(oldRender[j]);
+                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                        && !fc.isColourByAttribute())
                 {
-                  ((GraduatedColor) fc).updateBounds(
-                          ((float[][]) mmrange)[0][0],
-                          ((float[][]) mmrange)[0][1]);
+                  fc.updateBounds(mmrange[0][0], mmrange[0][1]);
                 }
               }
             }
@@ -497,15 +543,14 @@ public abstract class FeatureRendererModel implements
       if (minmax != null)
       {
         // update from new features minmax if necessary
-        mmrange = minmax.get(newf[i]);
+        float[][] mmrange = minmax.get(newf[i]);
         if (mmrange != null)
         {
-          fc = featureColours.get(newf[i]);
-          if (fc != null && fc instanceof GraduatedColor
-                  && ((GraduatedColor) fc).isAutoScale())
+          FeatureColourI fc = featureColours.get(newf[i]);
+          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                  && !fc.isColourByAttribute())
           {
-            ((GraduatedColor) fc).updateBounds(((float[][]) mmrange)[0][0],
-                    ((float[][]) mmrange)[0][1]);
+            fc.updateBounds(mmrange[0][0], mmrange[0][1]);
           }
         }
       }
@@ -517,7 +562,7 @@ public abstract class FeatureRendererModel implements
         setOrder(newf[i], i / (float) denom);
       }
       // set order from newly found feature from persisted ordering.
-      sortOrder[i] = 2 - ((Float) featureOrder.get(newf[i])).floatValue();
+      sortOrder[i] = 2 - featureOrder.get(newf[i]).floatValue();
       if (i < iSize)
       {
         // only sort if we need to
@@ -535,120 +580,62 @@ public abstract class FeatureRendererModel implements
 
   /**
    * get a feature style object for the given type string. Creates a
-   * java.awt.Color for a featureType with no existing colourscheme. TODO:
-   * replace return type with object implementing standard abstract colour/style
-   * interface
+   * java.awt.Color for a featureType with no existing colourscheme.
    * 
    * @param featureType
-   * @return java.awt.Color or GraduatedColor
+   * @return
    */
   @Override
-  public Object getFeatureStyle(String featureType)
+  public FeatureColourI getFeatureStyle(String featureType)
   {
-    Object fc = featureColours.get(featureType);
+    FeatureColourI fc = featureColours.get(featureType);
     if (fc == null)
     {
-      jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
-      Color col = ucs.createColourFromName(featureType);
-      featureColours.put(featureType, fc = col);
+      Color col = ColorUtils.createColourFromName(featureType);
+      fc = new FeatureColour(col);
+      featureColours.put(featureType, fc);
     }
     return fc;
   }
 
-  /**
-   * return a nominal colour for this feature
-   * 
-   * @param featureType
-   * @return standard color, or maximum colour for graduated colourscheme
-   */
-  public Color getColour(String featureType)
+  @Override
+  public Color getColour(SequenceFeature feature)
   {
-    Object fc = getFeatureStyle(featureType);
-
-    if (fc instanceof Color)
-    {
-      return (Color) fc;
-    }
-    else
-    {
-      if (fc instanceof GraduatedColor)
-      {
-        return ((GraduatedColor) fc).getMaxColor();
-      }
-    }
-    throw new Error("Implementation Error: Unrecognised render object "
-            + fc.getClass() + " for features of type " + featureType);
+    FeatureColourI fc = getFeatureStyle(feature.getType());
+    return getColor(feature, fc);
   }
 
   /**
-   * calculate the render colour for a specific feature using current feature
-   * settings.
+   * Answers true if the feature type is currently selected to be displayed,
+   * else false
    * 
-   * @param feature
-   * @return render colour for the given feature
+   * @param type
+   * @return
    */
-  public Color getColour(SequenceFeature feature)
-  {
-    Object fc = getFeatureStyle(feature.getType());
-    if (fc instanceof Color)
-    {
-      return (Color) fc;
-    }
-    else
-    {
-      if (fc instanceof GraduatedColor)
-      {
-        return ((GraduatedColor) fc).findColor(feature);
-      }
-    }
-    throw new Error("Implementation Error: Unrecognised render object "
-            + fc.getClass() + " for features of type " + feature.getType());
-  }
-
-  protected boolean showFeature(SequenceFeature sequenceFeature)
-  {
-    Object fc = getFeatureStyle(sequenceFeature.type);
-    if (fc instanceof GraduatedColor)
-    {
-      return ((GraduatedColor) fc).isColored(sequenceFeature);
-    }
-    else
-    {
-      return true;
-    }
-  }
-
   protected boolean showFeatureOfType(String type)
   {
-    return av.getFeaturesDisplayed().isVisible(type);
+    return type == null ? false : (av.getFeaturesDisplayed() == null ? true
+            : av.getFeaturesDisplayed().isVisible(type));
   }
 
   @Override
-  public void setColour(String featureType, Object col)
+  public void setColour(String featureType, FeatureColourI col)
   {
-    // overwrite
-    // Color _col = (col instanceof Color) ? ((Color) col) : (col instanceof
-    // GraduatedColor) ? ((GraduatedColor) col).getMaxColor() : null;
-    // Object c = featureColours.get(featureType);
-    // if (c == null || c instanceof Color || (c instanceof GraduatedColor &&
-    // !((GraduatedColor)c).getMaxColor().equals(_col)))
-    {
-      featureColours.put(featureType, col);
-    }
+    featureColours.put(featureType, col);
   }
 
+  @Override
   public void setTransparency(float value)
   {
     transparency = value;
   }
 
+  @Override
   public float getTransparency()
   {
     return transparency;
   }
 
-  Map featureOrder = null;
-
   /**
    * analogous to colour - store a normalized ordering for all feature types in
    * this rendering context.
@@ -663,7 +650,7 @@ public abstract class FeatureRendererModel implements
   {
     if (featureOrder == null)
     {
-      featureOrder = new Hashtable();
+      featureOrder = new Hashtable<>();
     }
     featureOrder.put(type, new Float(position));
     return position;
@@ -681,14 +668,14 @@ public abstract class FeatureRendererModel implements
     {
       if (featureOrder.containsKey(type))
       {
-        return ((Float) featureOrder.get(type)).floatValue();
+        return featureOrder.get(type).floatValue();
       }
     }
     return -1;
   }
 
   @Override
-  public Map<String, Object> getFeatureColours()
+  public Map<String, FeatureColourI> getFeatureColours()
   {
     return featureColours;
   }
@@ -697,31 +684,33 @@ public abstract class FeatureRendererModel implements
    * Replace current ordering with new ordering
    * 
    * @param data
-   *          { String(Type), Colour(Type), Boolean(Displayed) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @return true if any visible features have been reordered, else false
    */
-  public boolean setFeaturePriority(Object[][] data)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data)
   {
     return setFeaturePriority(data, true);
   }
 
   /**
-   * Sets the priority order for features
+   * Sets the priority order for features, with the highest priority (displayed on
+   * top) at the start of the data array
    * 
    * @param data
-   *          { String(Type), Colour(Type), Boolean(Displayed) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @param visibleNew
    *          when true current featureDisplay list will be cleared
-   * @return true if any visible features have been reordered or recoloured,
-   *         else false (i.e. no need to repaint)
+   * @return true if any visible features have been reordered or recoloured, else
+   *         false (i.e. no need to repaint)
    */
-  public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data,
+          boolean visibleNew)
   {
     /*
      * note visible feature ordering and colours before update
      */
     List<String> visibleFeatures = getDisplayedFeatureTypes();
-    Map<String, Object> visibleColours = new HashMap<String, Object>(
+    Map<String, FeatureColourI> visibleColours = new HashMap<>(
             getFeatureColours());
 
     FeaturesDisplayedI av_featuresdisplayed = null;
@@ -733,7 +722,8 @@ public abstract class FeatureRendererModel implements
       }
       else
       {
-        av.setFeaturesDisplayed(av_featuresdisplayed = new FeaturesDisplayed());
+        av.setFeaturesDisplayed(
+                av_featuresdisplayed = new FeaturesDisplayed());
       }
     }
     else
@@ -753,10 +743,9 @@ public abstract class FeatureRendererModel implements
     {
       for (int i = 0; i < data.length; i++)
       {
-        String type = data[i][0].toString();
-        setColour(type, data[i][1]); // todo : typesafety - feature color
-        // interface object
-        if (((Boolean) data[i][2]).booleanValue())
+        String type = data[i].featureType;
+        setColour(type, data[i].featureColour);
+        if (data[i].show)
         {
           av_featuresdisplayed.setVisible(type);
         }
@@ -809,7 +798,7 @@ public abstract class FeatureRendererModel implements
     changeSupport.removePropertyChangeListener(listener);
   }
 
-  public Set getAllFeatureColours()
+  public Set<String> getAllFeatureColours()
   {
     return featureColours.keySet();
   }
@@ -824,6 +813,9 @@ public abstract class FeatureRendererModel implements
     return renderOrder != null;
   }
 
+  /**
+   * Returns feature types in ordering of rendering, where last means on top
+   */
   public List<String> getRenderOrder()
   {
     if (renderOrder == null)
@@ -843,11 +835,12 @@ public abstract class FeatureRendererModel implements
   {
     // conflict between applet and desktop - featureGroups returns the map in
     // the desktop featureRenderer
-    return (featureGroups == null) ? Arrays.asList(new String[0]) : Arrays
-            .asList(featureGroups.keySet().toArray(new String[0]));
+    return (featureGroups == null) ? Arrays.asList(new String[0])
+            : Arrays.asList(featureGroups.keySet().toArray(new String[0]));
   }
 
-  public boolean checkGroupVisibility(String group, boolean newGroupsVisible)
+  public boolean checkGroupVisibility(String group,
+          boolean newGroupsVisible)
   {
     if (featureGroups == null)
     {
@@ -873,13 +866,13 @@ public abstract class FeatureRendererModel implements
    * @return list of groups
    */
   @Override
-  public List getGroups(boolean visible)
+  public List<String> getGroups(boolean visible)
   {
     if (featureGroups != null)
     {
-      ArrayList gp = new ArrayList();
+      List<String> gp = new ArrayList<>();
 
-      for (Object grp : featureGroups.keySet())
+      for (String grp : featureGroups.keySet())
       {
         Boolean state = featureGroups.get(grp);
         if (state.booleanValue() == visible)
@@ -921,19 +914,18 @@ public abstract class FeatureRendererModel implements
   }
 
   @Override
-  public Hashtable getDisplayedFeatureCols()
+  public Map<String, FeatureColourI> getDisplayedFeatureCols()
   {
-    Hashtable fcols = new Hashtable();
+    Map<String, FeatureColourI> fcols = new Hashtable<>();
     if (getViewport().getFeaturesDisplayed() == null)
     {
       return fcols;
     }
-    Iterator<String> en = getViewport().getFeaturesDisplayed()
+    Set<String> features = getViewport().getFeaturesDisplayed()
             .getVisibleFeatures();
-    while (en.hasNext())
+    for (String feature : features)
     {
-      String col = en.next();
-      fcols.put(col, getColour(col));
+      fcols.put(feature, getFeatureStyle(feature));
     }
     return fcols;
   }
@@ -952,7 +944,7 @@ public abstract class FeatureRendererModel implements
   public List<String> getDisplayedFeatureTypes()
   {
     List<String> typ = getRenderOrder();
-    List<String> displayed = new ArrayList<String>();
+    List<String> displayed = new ArrayList<>();
     FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed();
     if (feature_disp != null)
     {
@@ -973,26 +965,223 @@ public abstract class FeatureRendererModel implements
   @Override
   public List<String> getDisplayedFeatureGroups()
   {
-    List<String> _gps = new ArrayList<String>();
-    boolean valid = false;
+    List<String> _gps = new ArrayList<>();
     for (String gp : getFeatureGroups())
     {
       if (checkGroupVisibility(gp, false))
       {
-        valid = true;
         _gps.add(gp);
       }
-      if (!valid)
+    }
+    return _gps;
+  }
+
+  /**
+   * Answers true if the feature belongs to a feature group which is not
+   * currently displayed, else false
+   * 
+   * @param sequenceFeature
+   * @return
+   */
+  protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
+  {
+    return featureGroups != null
+            && sequenceFeature.featureGroup != null
+            && sequenceFeature.featureGroup.length() != 0
+            && featureGroups.containsKey(sequenceFeature.featureGroup)
+            && !featureGroups.get(sequenceFeature.featureGroup)
+                    .booleanValue();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
+          int resNo)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
+    {
+      return result;
+    }
+
+    /*
+     * include features at the position provided their feature type is 
+     * displayed, and feature group is null or the empty string
+     * or marked for display
+     */
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.getFeatures().findFeatures(
+            resNo, resNo, visibleTypes);
+  
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
-        return null;
+        result.add(sf);
       }
-      else
+    }
+    return result;
+  }
+
+  /**
+   * Removes from the list of features any whose group is not shown, or that are
+   * visible and duplicate the location of a visible feature of the same type.
+   * Should be used only for features of the same, simple, feature colour (which
+   * normally implies the same feature type). No filtering is done if
+   * transparency, or any feature filters, are in force.
+   * 
+   * @param features
+   */
+  public void filterFeaturesForDisplay(List<SequenceFeature> features)
+  {
+    /*
+     * don't remove 'redundant' features if 
+     * - transparency is applied (feature count affects depth of feature colour)
+     * - filters are applied (not all features may be displayable)
+     */
+    if (features.isEmpty() || transparency != 1f
+            || !featureFilters.isEmpty())
+    {
+      return;
+    }
+
+    SequenceFeatures.sortFeatures(features, true);
+    SequenceFeature lastFeature = null;
+
+    Iterator<SequenceFeature> it = features.iterator();
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+      if (featureGroupNotShown(sf))
       {
-        // gps = new String[_gps.size()];
-        // _gps.toArray(gps);
+        it.remove();
+        continue;
       }
+
+      /*
+       * a feature is redundant for rendering purposes if it has the
+       * same extent as another (so would just redraw the same colour);
+       * (checking type and isContactFeature as a fail-safe here, although
+       * currently they are guaranteed to match in this context)
+       */
+      if (lastFeature != null
+              && sf.getBegin() == lastFeature.getBegin()
+              && sf.getEnd() == lastFeature.getEnd()
+              && sf.isContactFeature() == lastFeature.isContactFeature()
+              && sf.getType().equals(lastFeature.getType()))
+      {
+        it.remove();
+      }
+      lastFeature = sf;
     }
-    return _gps;
+  }
+
+  @Override
+  public Map<String, FeatureMatcherSetI> getFeatureFilters()
+  {
+    return featureFilters;
+  }
+
+  @Override
+  public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
+  {
+    featureFilters = filters;
+  }
+
+  @Override
+  public FeatureMatcherSetI getFeatureFilter(String featureType)
+  {
+    return featureFilters.get(featureType);
+  }
+
+  @Override
+  public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
+  {
+    if (filter == null || filter.isEmpty())
+    {
+      featureFilters.remove(featureType);
+    }
+    else
+    {
+      featureFilters.put(featureType, filter);
+    }
+  }
+
+  /**
+   * Answers the colour for the feature, or null if the feature is excluded by
+   * feature group visibility, by filters, or by colour threshold settings. This
+   * method does not take feature visibility into account.
+   * 
+   * @param sf
+   * @param fc
+   * @return
+   */
+  public Color getColor(SequenceFeature sf, FeatureColourI fc)
+  {
+    /*
+     * is the feature group displayed?
+     */
+    if (featureGroupNotShown(sf))
+    {
+      return null;
+    }
+
+    /*
+     * does the feature pass filters?
+     */
+    if (!featureMatchesFilters(sf))
+    {
+      return null;
+    }
+  
+    return fc.getColor(sf);
+  }
+
+  /**
+   * Answers true if there no are filters defined for the feature type, or this
+   * feature matches the filters. Answers false if the feature fails to match
+   * filters.
+   * 
+   * @param sf
+   * @return
+   */
+  protected boolean featureMatchesFilters(SequenceFeature sf)
+  {
+    FeatureMatcherSetI filter = featureFilters.get(sf.getType());
+    return filter == null ? true : filter.matches(sf);
+  }
+
+  @Override
+  public boolean isVisible(SequenceFeature feature)
+  {
+    if (feature == null)
+    {
+      return false;
+    }
+    if (getFeaturesDisplayed() == null
+            || !getFeaturesDisplayed().isVisible(feature.getType()))
+    {
+      return false;
+    }
+    if (featureGroupNotShown(feature))
+    {
+      return false;
+    }
+    FeatureColourI fc = featureColours.get(feature.getType());
+    if (fc != null && fc.isOutwithThreshold(feature))
+    {
+      return false;
+    }
+    if (!featureMatchesFilters(feature))
+    {
+      return false;
+    }
+    return true;
   }
 
 }