JAL-2808 JAL-2069 FeatureTypeSettings (with new Filters tab) replaces FeatureColourCh...
[jalview.git] / src / jalview / viewmodel / seqfeatures / FeatureRendererModel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.viewmodel.seqfeatures;
22
23 import jalview.api.AlignViewportI;
24 import jalview.api.FeatureColourI;
25 import jalview.api.FeaturesDisplayedI;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.datamodel.features.SequenceFeatures;
30 import jalview.renderer.seqfeatures.FeatureRenderer;
31 import jalview.schemes.FeatureColour;
32 import jalview.util.ColorUtils;
33 import jalview.util.matcher.KeyedMatcherSetI;
34
35 import java.awt.Color;
36 import java.beans.PropertyChangeListener;
37 import java.beans.PropertyChangeSupport;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.Hashtable;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Set;
47 import java.util.concurrent.ConcurrentHashMap;
48
49 public abstract class FeatureRendererModel
50         implements jalview.api.FeatureRenderer
51 {
52   /*
53    * column indices of fields in Feature Settings table
54    * todo: transfer valuers as data beans instead of Object[][]
55    */
56   public static final int TYPE_COLUMN = 0;
57
58   public static final int COLOUR_COLUMN = 1;
59
60   public static final int FILTER_COLUMN = 2;
61
62   public static final int SHOW_COLUMN = 3;
63
64   /*
65    * global transparency for feature
66    */
67   protected float transparency = 1.0f;
68
69   /*
70    * colour scheme for each feature type
71    */
72   protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
73
74   /*
75    * visibility flag for each feature group
76    */
77   protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
78
79   /*
80    * filters for each feature type
81    */
82   protected Map<String, KeyedMatcherSetI> featureFilters = new HashMap<>();
83
84   protected String[] renderOrder;
85
86   Map<String, Float> featureOrder = null;
87
88   protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
89           this);
90
91   protected AlignViewportI av;
92
93   @Override
94   public AlignViewportI getViewport()
95   {
96     return av;
97   }
98
99   public FeatureRendererSettings getSettings()
100   {
101     return new FeatureRendererSettings(this);
102   }
103
104   public void transferSettings(FeatureRendererSettings fr)
105   {
106     this.renderOrder = fr.renderOrder;
107     this.featureGroups = fr.featureGroups;
108     this.featureColours = fr.featureColours;
109     this.transparency = fr.transparency;
110     this.featureOrder = fr.featureOrder;
111   }
112
113   /**
114    * update from another feature renderer
115    * 
116    * @param fr
117    *          settings to copy
118    */
119   public void transferSettings(jalview.api.FeatureRenderer _fr)
120   {
121     FeatureRenderer fr = (FeatureRenderer) _fr;
122     FeatureRendererSettings frs = new FeatureRendererSettings(fr);
123     this.renderOrder = frs.renderOrder;
124     this.featureGroups = frs.featureGroups;
125     this.featureColours = frs.featureColours;
126     this.featureFilters = frs.featureFilters;
127     this.transparency = frs.transparency;
128     this.featureOrder = frs.featureOrder;
129     if (av != null && av != fr.getViewport())
130     {
131       // copy over the displayed feature settings
132       if (_fr.getFeaturesDisplayed() != null)
133       {
134         FeaturesDisplayedI fd = getFeaturesDisplayed();
135         if (fd == null)
136         {
137           setFeaturesDisplayedFrom(_fr.getFeaturesDisplayed());
138         }
139         else
140         {
141           synchronized (fd)
142           {
143             fd.clear();
144             for (String type : _fr.getFeaturesDisplayed()
145                     .getVisibleFeatures())
146             {
147               fd.setVisible(type);
148             }
149           }
150         }
151       }
152     }
153   }
154
155   public void setFeaturesDisplayedFrom(FeaturesDisplayedI featuresDisplayed)
156   {
157     av.setFeaturesDisplayed(new FeaturesDisplayed(featuresDisplayed));
158   }
159
160   @Override
161   public void setVisible(String featureType)
162   {
163     FeaturesDisplayedI fdi = av.getFeaturesDisplayed();
164     if (fdi == null)
165     {
166       av.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
167     }
168     if (!fdi.isRegistered(featureType))
169     {
170       pushFeatureType(Arrays.asList(new String[] { featureType }));
171     }
172     fdi.setVisible(featureType);
173   }
174
175   @Override
176   public void setAllVisible(List<String> featureTypes)
177   {
178     FeaturesDisplayedI fdi = av.getFeaturesDisplayed();
179     if (fdi == null)
180     {
181       av.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
182     }
183     List<String> nft = new ArrayList<>();
184     for (String featureType : featureTypes)
185     {
186       if (!fdi.isRegistered(featureType))
187       {
188         nft.add(featureType);
189       }
190     }
191     if (nft.size() > 0)
192     {
193       pushFeatureType(nft);
194     }
195     fdi.setAllVisible(featureTypes);
196   }
197
198   /**
199    * push a set of new types onto the render order stack. Note - this is a
200    * direct mechanism rather than the one employed in updateRenderOrder
201    * 
202    * @param types
203    */
204   private void pushFeatureType(List<String> types)
205   {
206
207     int ts = types.size();
208     String neworder[] = new String[(renderOrder == null ? 0
209             : renderOrder.length) + ts];
210     types.toArray(neworder);
211     if (renderOrder != null)
212     {
213       System.arraycopy(neworder, 0, neworder, renderOrder.length, ts);
214       System.arraycopy(renderOrder, 0, neworder, 0, renderOrder.length);
215     }
216     renderOrder = neworder;
217   }
218
219   protected Map<String, float[][]> minmax = new Hashtable<>();
220
221   public Map<String, float[][]> getMinMax()
222   {
223     return minmax;
224   }
225
226   /**
227    * normalise a score against the max/min bounds for the feature type.
228    * 
229    * @param sequenceFeature
230    * @return byte[] { signed, normalised signed (-127 to 127) or unsigned
231    *         (0-255) value.
232    */
233   protected final byte[] normaliseScore(SequenceFeature sequenceFeature)
234   {
235     float[] mm = minmax.get(sequenceFeature.type)[0];
236     final byte[] r = new byte[] { 0, (byte) 255 };
237     if (mm != null)
238     {
239       if (r[0] != 0 || mm[0] < 0.0)
240       {
241         r[0] = 1;
242         r[1] = (byte) ((int) 128.0
243                 + 127.0 * (sequenceFeature.score / mm[1]));
244       }
245       else
246       {
247         r[1] = (byte) ((int) 255.0 * (sequenceFeature.score / mm[1]));
248       }
249     }
250     return r;
251   }
252
253   boolean newFeatureAdded = false;
254
255   boolean findingFeatures = false;
256
257   protected boolean updateFeatures()
258   {
259     if (av.getFeaturesDisplayed() == null || renderOrder == null
260             || newFeatureAdded)
261     {
262       findAllFeatures();
263       if (av.getFeaturesDisplayed().getVisibleFeatureCount() < 1)
264       {
265         return false;
266       }
267     }
268     // TODO: decide if we should check for the visible feature count first
269     return true;
270   }
271
272   /**
273    * search the alignment for all new features, give them a colour and display
274    * them. Then fires a PropertyChangeEvent on the changeSupport object.
275    * 
276    */
277   protected void findAllFeatures()
278   {
279     synchronized (firing)
280     {
281       if (firing.equals(Boolean.FALSE))
282       {
283         firing = Boolean.TRUE;
284         findAllFeatures(true); // add all new features as visible
285         changeSupport.firePropertyChange("changeSupport", null, null);
286         firing = Boolean.FALSE;
287       }
288     }
289   }
290
291   @Override
292   public List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
293   {
294     /*
295      * include features at the position provided their feature type is 
296      * displayed, and feature group is null or marked for display
297      */
298     List<SequenceFeature> result = new ArrayList<>();
299     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
300     {
301       return result;
302     }
303
304     Set<String> visibleFeatures = getFeaturesDisplayed()
305             .getVisibleFeatures();
306     String[] visibleTypes = visibleFeatures
307             .toArray(new String[visibleFeatures.size()]);
308     List<SequenceFeature> features = sequence.findFeatures(column, column,
309             visibleTypes);
310
311     /*
312      * include features unless their feature group is not displayed, or
313      * they are hidden (have no colour) based on a filter or colour threshold
314      */
315     for (SequenceFeature sf : features)
316     {
317       if (!featureGroupNotShown(sf) && getColour(sf) != null)
318       {
319         result.add(sf);
320       }
321     }
322     return result;
323   }
324
325   /**
326    * Searches alignment for all features and updates colours
327    * 
328    * @param newMadeVisible
329    *          if true newly added feature types will be rendered immediately
330    *          TODO: check to see if this method should actually be proxied so
331    *          repaint events can be propagated by the renderer code
332    */
333   @Override
334   public synchronized void findAllFeatures(boolean newMadeVisible)
335   {
336     newFeatureAdded = false;
337
338     if (findingFeatures)
339     {
340       newFeatureAdded = true;
341       return;
342     }
343
344     findingFeatures = true;
345     if (av.getFeaturesDisplayed() == null)
346     {
347       av.setFeaturesDisplayed(new FeaturesDisplayed());
348     }
349     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
350
351     Set<String> oldfeatures = new HashSet<>();
352     if (renderOrder != null)
353     {
354       for (int i = 0; i < renderOrder.length; i++)
355       {
356         if (renderOrder[i] != null)
357         {
358           oldfeatures.add(renderOrder[i]);
359         }
360       }
361     }
362
363     AlignmentI alignment = av.getAlignment();
364     List<String> allfeatures = new ArrayList<>();
365
366     for (int i = 0; i < alignment.getHeight(); i++)
367     {
368       SequenceI asq = alignment.getSequenceAt(i);
369       for (String group : asq.getFeatures().getFeatureGroups(true))
370       {
371         boolean groupDisplayed = true;
372         if (group != null)
373         {
374           if (featureGroups.containsKey(group))
375           {
376             groupDisplayed = featureGroups.get(group);
377           }
378           else
379           {
380             groupDisplayed = newMadeVisible;
381             featureGroups.put(group, groupDisplayed);
382           }
383         }
384         if (groupDisplayed)
385         {
386           Set<String> types = asq.getFeatures().getFeatureTypesForGroups(
387                   true, group);
388           for (String type : types)
389           {
390             if (!allfeatures.contains(type)) // or use HashSet and no test?
391             {
392               allfeatures.add(type);
393             }
394             updateMinMax(asq, type, true); // todo: for all features?
395           }
396         }
397       }
398     }
399
400     // uncomment to add new features in alphebetical order (but JAL-2575)
401     // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER);
402     if (newMadeVisible)
403     {
404       for (String type : allfeatures)
405       {
406         if (!oldfeatures.contains(type))
407         {
408           featuresDisplayed.setVisible(type);
409           setOrder(type, 0);
410         }
411       }
412     }
413
414     updateRenderOrder(allfeatures);
415     findingFeatures = false;
416   }
417
418   /**
419    * Updates the global (alignment) min and max values for a feature type from
420    * the score for a sequence, if the score is not NaN. Values are stored
421    * separately for positional and non-positional features.
422    * 
423    * @param seq
424    * @param featureType
425    * @param positional
426    */
427   protected void updateMinMax(SequenceI seq, String featureType,
428           boolean positional)
429   {
430     float min = seq.getFeatures().getMinimumScore(featureType, positional);
431     if (Float.isNaN(min))
432     {
433       return;
434     }
435
436     float max = seq.getFeatures().getMaximumScore(featureType, positional);
437
438     /*
439      * stored values are 
440      * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} }
441      */
442     if (minmax == null)
443     {
444       minmax = new Hashtable<>();
445     }
446     synchronized (minmax)
447     {
448       float[][] mm = minmax.get(featureType);
449       int index = positional ? 0 : 1;
450       if (mm == null)
451       {
452         mm = new float[][] { null, null };
453         minmax.put(featureType, mm);
454       }
455       if (mm[index] == null)
456       {
457         mm[index] = new float[] { min, max };
458       }
459       else
460       {
461         mm[index][0] = Math.min(mm[index][0], min);
462         mm[index][1] = Math.max(mm[index][1], max);
463       }
464     }
465   }
466   protected Boolean firing = Boolean.FALSE;
467
468   /**
469    * replaces the current renderOrder with the unordered features in
470    * allfeatures. The ordering of any types in both renderOrder and allfeatures
471    * is preserved, and all new feature types are rendered on top of the existing
472    * types, in the order given by getOrder or the order given in allFeatures.
473    * Note. this operates directly on the featureOrder hash for efficiency. TODO:
474    * eliminate the float storage for computing/recalling the persistent ordering
475    * New Cability: updates min/max for colourscheme range if its dynamic
476    * 
477    * @param allFeatures
478    */
479   private void updateRenderOrder(List<String> allFeatures)
480   {
481     List<String> allfeatures = new ArrayList<>(allFeatures);
482     String[] oldRender = renderOrder;
483     renderOrder = new String[allfeatures.size()];
484     boolean initOrders = (featureOrder == null);
485     int opos = 0;
486     if (oldRender != null && oldRender.length > 0)
487     {
488       for (int j = 0; j < oldRender.length; j++)
489       {
490         if (oldRender[j] != null)
491         {
492           if (initOrders)
493           {
494             setOrder(oldRender[j],
495                     (1 - (1 + (float) j) / oldRender.length));
496           }
497           if (allfeatures.contains(oldRender[j]))
498           {
499             renderOrder[opos++] = oldRender[j]; // existing features always
500             // appear below new features
501             allfeatures.remove(oldRender[j]);
502             if (minmax != null)
503             {
504               float[][] mmrange = minmax.get(oldRender[j]);
505               if (mmrange != null)
506               {
507                 FeatureColourI fc = featureColours.get(oldRender[j]);
508                 if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
509                         && !fc.isColourByAttribute())
510                 {
511                   fc.updateBounds(mmrange[0][0], mmrange[0][1]);
512                 }
513               }
514             }
515           }
516         }
517       }
518     }
519     if (allfeatures.size() == 0)
520     {
521       // no new features - leave order unchanged.
522       return;
523     }
524     int i = allfeatures.size() - 1;
525     int iSize = i;
526     boolean sort = false;
527     String[] newf = new String[allfeatures.size()];
528     float[] sortOrder = new float[allfeatures.size()];
529     for (String newfeat : allfeatures)
530     {
531       newf[i] = newfeat;
532       if (minmax != null)
533       {
534         // update from new features minmax if necessary
535         float[][] mmrange = minmax.get(newf[i]);
536         if (mmrange != null)
537         {
538           FeatureColourI fc = featureColours.get(newf[i]);
539           if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
540                   && !fc.isColourByAttribute())
541           {
542             fc.updateBounds(mmrange[0][0], mmrange[0][1]);
543           }
544         }
545       }
546       if (initOrders || !featureOrder.containsKey(newf[i]))
547       {
548         int denom = initOrders ? allfeatures.size() : featureOrder.size();
549         // new unordered feature - compute persistent ordering at head of
550         // existing features.
551         setOrder(newf[i], i / (float) denom);
552       }
553       // set order from newly found feature from persisted ordering.
554       sortOrder[i] = 2 - featureOrder.get(newf[i]).floatValue();
555       if (i < iSize)
556       {
557         // only sort if we need to
558         sort = sort || sortOrder[i] > sortOrder[i + 1];
559       }
560       i--;
561     }
562     if (iSize > 1 && sort)
563     {
564       jalview.util.QuickSort.sort(sortOrder, newf);
565     }
566     sortOrder = null;
567     System.arraycopy(newf, 0, renderOrder, opos, newf.length);
568   }
569
570   /**
571    * get a feature style object for the given type string. Creates a
572    * java.awt.Color for a featureType with no existing colourscheme.
573    * 
574    * @param featureType
575    * @return
576    */
577   @Override
578   public FeatureColourI getFeatureStyle(String featureType)
579   {
580     FeatureColourI fc = featureColours.get(featureType);
581     if (fc == null)
582     {
583       Color col = ColorUtils.createColourFromName(featureType);
584       fc = new FeatureColour(col);
585       featureColours.put(featureType, fc);
586     }
587     return fc;
588   }
589
590   @Override
591   public Color getColour(SequenceFeature feature)
592   {
593     FeatureColourI fc = getFeatureStyle(feature.getType());
594     return getColor(feature, fc);
595   }
596
597   /**
598    * Answers true if the feature type is currently selected to be displayed,
599    * else false
600    * 
601    * @param type
602    * @return
603    */
604   protected boolean showFeatureOfType(String type)
605   {
606     return type == null ? false : (av.getFeaturesDisplayed() == null ? true
607             : av.getFeaturesDisplayed().isVisible(type));
608   }
609
610   @Override
611   public void setColour(String featureType, FeatureColourI col)
612   {
613     featureColours.put(featureType, col);
614   }
615
616   @Override
617   public void setTransparency(float value)
618   {
619     transparency = value;
620   }
621
622   @Override
623   public float getTransparency()
624   {
625     return transparency;
626   }
627
628   /**
629    * analogous to colour - store a normalized ordering for all feature types in
630    * this rendering context.
631    * 
632    * @param type
633    *          Feature type string
634    * @param position
635    *          normalized priority - 0 means always appears on top, 1 means
636    *          always last.
637    */
638   public float setOrder(String type, float position)
639   {
640     if (featureOrder == null)
641     {
642       featureOrder = new Hashtable<>();
643     }
644     featureOrder.put(type, new Float(position));
645     return position;
646   }
647
648   /**
649    * get the global priority (0 (top) to 1 (bottom))
650    * 
651    * @param type
652    * @return [0,1] or -1 for a type without a priority
653    */
654   public float getOrder(String type)
655   {
656     if (featureOrder != null)
657     {
658       if (featureOrder.containsKey(type))
659       {
660         return featureOrder.get(type).floatValue();
661       }
662     }
663     return -1;
664   }
665
666   @Override
667   public Map<String, FeatureColourI> getFeatureColours()
668   {
669     return featureColours;
670   }
671
672   /**
673    * Replace current ordering with new ordering
674    * 
675    * @param data
676    *          { String(Type), Colour(Type), Boolean(Displayed) }
677    * @return true if any visible features have been reordered, else false
678    */
679   public boolean setFeaturePriority(Object[][] data)
680   {
681     return setFeaturePriority(data, true);
682   }
683
684   /**
685    * Sets the priority order for features, with the highest priority (displayed
686    * on top) at the start of the data array
687    * 
688    * @param data
689    *          { String(Type), Colour(Type), Boolean(Displayed) }
690    * @param visibleNew
691    *          when true current featureDisplay list will be cleared
692    * @return true if any visible features have been reordered or recoloured,
693    *         else false (i.e. no need to repaint)
694    */
695   public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
696   {
697     /*
698      * note visible feature ordering and colours before update
699      */
700     List<String> visibleFeatures = getDisplayedFeatureTypes();
701     Map<String, FeatureColourI> visibleColours = new HashMap<>(
702             getFeatureColours());
703
704     FeaturesDisplayedI av_featuresdisplayed = null;
705     if (visibleNew)
706     {
707       if ((av_featuresdisplayed = av.getFeaturesDisplayed()) != null)
708       {
709         av.getFeaturesDisplayed().clear();
710       }
711       else
712       {
713         av.setFeaturesDisplayed(
714                 av_featuresdisplayed = new FeaturesDisplayed());
715       }
716     }
717     else
718     {
719       av_featuresdisplayed = av.getFeaturesDisplayed();
720     }
721     if (data == null)
722     {
723       return false;
724     }
725     // The feature table will display high priority
726     // features at the top, but these are the ones
727     // we need to render last, so invert the data
728     renderOrder = new String[data.length];
729
730     if (data.length > 0)
731     {
732       for (int i = 0; i < data.length; i++)
733       {
734         String type = data[i][TYPE_COLUMN].toString();
735         setColour(type, (FeatureColourI) data[i][COLOUR_COLUMN]);
736         if (((Boolean) data[i][SHOW_COLUMN]).booleanValue())
737         {
738           av_featuresdisplayed.setVisible(type);
739         }
740
741         renderOrder[data.length - i - 1] = type;
742       }
743     }
744
745     /*
746      * get the new visible ordering and return true if it has changed
747      * order or any colour has changed
748      */
749     List<String> reorderedVisibleFeatures = getDisplayedFeatureTypes();
750     if (!visibleFeatures.equals(reorderedVisibleFeatures))
751     {
752       /*
753        * the list of ordered visible features has changed
754        */
755       return true;
756     }
757
758     /*
759      * return true if any feature colour has changed
760      */
761     for (String feature : visibleFeatures)
762     {
763       if (visibleColours.get(feature) != getFeatureStyle(feature))
764       {
765         return true;
766       }
767     }
768     return false;
769   }
770
771   /**
772    * @param listener
773    * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
774    */
775   public void addPropertyChangeListener(PropertyChangeListener listener)
776   {
777     changeSupport.addPropertyChangeListener(listener);
778   }
779
780   /**
781    * @param listener
782    * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
783    */
784   public void removePropertyChangeListener(PropertyChangeListener listener)
785   {
786     changeSupport.removePropertyChangeListener(listener);
787   }
788
789   public Set<String> getAllFeatureColours()
790   {
791     return featureColours.keySet();
792   }
793
794   public void clearRenderOrder()
795   {
796     renderOrder = null;
797   }
798
799   public boolean hasRenderOrder()
800   {
801     return renderOrder != null;
802   }
803
804   /**
805    * Returns feature types in ordering of rendering, where last means on top
806    */
807   public List<String> getRenderOrder()
808   {
809     if (renderOrder == null)
810     {
811       return Arrays.asList(new String[] {});
812     }
813     return Arrays.asList(renderOrder);
814   }
815
816   public int getFeatureGroupsSize()
817   {
818     return featureGroups != null ? 0 : featureGroups.size();
819   }
820
821   @Override
822   public List<String> getFeatureGroups()
823   {
824     // conflict between applet and desktop - featureGroups returns the map in
825     // the desktop featureRenderer
826     return (featureGroups == null) ? Arrays.asList(new String[0])
827             : Arrays.asList(featureGroups.keySet().toArray(new String[0]));
828   }
829
830   public boolean checkGroupVisibility(String group,
831           boolean newGroupsVisible)
832   {
833     if (featureGroups == null)
834     {
835       // then an exception happens next..
836     }
837     if (featureGroups.containsKey(group))
838     {
839       return featureGroups.get(group).booleanValue();
840     }
841     if (newGroupsVisible)
842     {
843       featureGroups.put(group, new Boolean(true));
844       return true;
845     }
846     return false;
847   }
848
849   /**
850    * get visible or invisible groups
851    * 
852    * @param visible
853    *          true to return visible groups, false to return hidden ones.
854    * @return list of groups
855    */
856   @Override
857   public List<String> getGroups(boolean visible)
858   {
859     if (featureGroups != null)
860     {
861       List<String> gp = new ArrayList<>();
862
863       for (String grp : featureGroups.keySet())
864       {
865         Boolean state = featureGroups.get(grp);
866         if (state.booleanValue() == visible)
867         {
868           gp.add(grp);
869         }
870       }
871       return gp;
872     }
873     return null;
874   }
875
876   @Override
877   public void setGroupVisibility(String group, boolean visible)
878   {
879     featureGroups.put(group, new Boolean(visible));
880   }
881
882   @Override
883   public void setGroupVisibility(List<String> toset, boolean visible)
884   {
885     if (toset != null && toset.size() > 0 && featureGroups != null)
886     {
887       boolean rdrw = false;
888       for (String gst : toset)
889       {
890         Boolean st = featureGroups.get(gst);
891         featureGroups.put(gst, new Boolean(visible));
892         if (st != null)
893         {
894           rdrw = rdrw || (visible != st.booleanValue());
895         }
896       }
897       if (rdrw)
898       {
899         // set local flag indicating redraw needed ?
900       }
901     }
902   }
903
904   @Override
905   public Map<String, FeatureColourI> getDisplayedFeatureCols()
906   {
907     Map<String, FeatureColourI> fcols = new Hashtable<>();
908     if (getViewport().getFeaturesDisplayed() == null)
909     {
910       return fcols;
911     }
912     Set<String> features = getViewport().getFeaturesDisplayed()
913             .getVisibleFeatures();
914     for (String feature : features)
915     {
916       fcols.put(feature, getFeatureStyle(feature));
917     }
918     return fcols;
919   }
920
921   @Override
922   public FeaturesDisplayedI getFeaturesDisplayed()
923   {
924     return av.getFeaturesDisplayed();
925   }
926
927   /**
928    * Returns a (possibly empty) list of visible feature types, in render order
929    * (last is on top)
930    */
931   @Override
932   public List<String> getDisplayedFeatureTypes()
933   {
934     List<String> typ = getRenderOrder();
935     List<String> displayed = new ArrayList<>();
936     FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed();
937     if (feature_disp != null)
938     {
939       synchronized (feature_disp)
940       {
941         for (String type : typ)
942         {
943           if (feature_disp.isVisible(type))
944           {
945             displayed.add(type);
946           }
947         }
948       }
949     }
950     return displayed;
951   }
952
953   @Override
954   public List<String> getDisplayedFeatureGroups()
955   {
956     List<String> _gps = new ArrayList<>();
957     for (String gp : getFeatureGroups())
958     {
959       if (checkGroupVisibility(gp, false))
960       {
961         _gps.add(gp);
962       }
963     }
964     return _gps;
965   }
966
967   /**
968    * Answers true if the feature belongs to a feature group which is not
969    * currently displayed, else false
970    * 
971    * @param sequenceFeature
972    * @return
973    */
974   protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
975   {
976     return featureGroups != null
977             && sequenceFeature.featureGroup != null
978             && sequenceFeature.featureGroup.length() != 0
979             && featureGroups.containsKey(sequenceFeature.featureGroup)
980             && !featureGroups.get(sequenceFeature.featureGroup)
981                     .booleanValue();
982   }
983
984   /**
985    * {@inheritDoc}
986    */
987   @Override
988   public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
989           int resNo)
990   {
991     List<SequenceFeature> result = new ArrayList<>();
992     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
993     {
994       return result;
995     }
996
997     /*
998      * include features at the position provided their feature type is 
999      * displayed, and feature group is null or the empty string
1000      * or marked for display
1001      */
1002     Set<String> visibleFeatures = getFeaturesDisplayed()
1003             .getVisibleFeatures();
1004     String[] visibleTypes = visibleFeatures
1005             .toArray(new String[visibleFeatures.size()]);
1006     List<SequenceFeature> features = sequence.getFeatures().findFeatures(
1007             resNo, resNo, visibleTypes);
1008   
1009     for (SequenceFeature sf : features)
1010     {
1011       if (!featureGroupNotShown(sf) && getColour(sf) != null)
1012       {
1013         result.add(sf);
1014       }
1015     }
1016     return result;
1017   }
1018
1019   /**
1020    * Removes from the list of features any that duplicate the location of a
1021    * feature of the same type. Should be used only for features of the same,
1022    * simple, feature colour (which normally implies the same feature type). Does
1023    * not check visibility settings for feature type or feature group.
1024    * 
1025    * @param features
1026    */
1027   public void filterFeaturesForDisplay(List<SequenceFeature> features)
1028   {
1029     if (features.isEmpty())
1030     {
1031       return;
1032     }
1033     SequenceFeatures.sortFeatures(features, true);
1034     SequenceFeature lastFeature = null;
1035
1036     Iterator<SequenceFeature> it = features.iterator();
1037     while (it.hasNext())
1038     {
1039       SequenceFeature sf = it.next();
1040
1041       /*
1042        * a feature is redundant for rendering purposes if it has the
1043        * same extent as another (so would just redraw the same colour);
1044        * (checking type and isContactFeature as a fail-safe here, although
1045        * currently they are guaranteed to match in this context)
1046        */
1047       if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
1048               && sf.getEnd() == lastFeature.getEnd()
1049               && sf.isContactFeature() == lastFeature.isContactFeature()
1050               && sf.getType().equals(lastFeature.getType()))
1051       {
1052         it.remove();
1053       }
1054       lastFeature = sf;
1055     }
1056   }
1057
1058   @Override
1059   public Map<String, KeyedMatcherSetI> getFeatureFilters()
1060   {
1061     return new HashMap<>(featureFilters);
1062   }
1063
1064   @Override
1065   public void setFeatureFilters(Map<String, KeyedMatcherSetI> filters)
1066   {
1067     featureFilters = filters;
1068   }
1069
1070   @Override
1071   public KeyedMatcherSetI getFeatureFilter(String featureType)
1072   {
1073     return featureFilters.get(featureType);
1074   }
1075
1076   @Override
1077   public void setFeatureFilter(String featureType, KeyedMatcherSetI filter)
1078   {
1079     if (filter == null || filter.isEmpty())
1080     {
1081       featureFilters.remove(featureType);
1082     }
1083     else
1084     {
1085       featureFilters.put(featureType, filter);
1086     }
1087   }
1088
1089   /**
1090    * Answers the colour for the feature, or null if the feature is excluded by
1091    * feature type or group visibility, by filters, or by colour threshold
1092    * settings
1093    * 
1094    * @param sf
1095    * @param fc
1096    * @return
1097    */
1098   public Color getColor(SequenceFeature sf, FeatureColourI fc)
1099   {
1100     /*
1101      * is the feature type displayed?
1102      */
1103     if (!showFeatureOfType(sf.getType()))
1104     {
1105       return null;
1106     }
1107
1108     /*
1109      * is the feature group displayed?
1110      */
1111     if (featureGroupNotShown(sf))
1112     {
1113       return null;
1114     }
1115
1116     /*
1117      * does the feature pass filters?
1118      */
1119     if (!featureMatchesFilters(sf))
1120     {
1121       return null;
1122     }
1123   
1124     return fc.getColor(sf);
1125   }
1126
1127   /**
1128    * Answers true if there no are filters defined for the feature type, or this
1129    * feature matches the filters. Answers false if the feature fails to match
1130    * filters.
1131    * 
1132    * @param sf
1133    * @return
1134    */
1135   protected boolean featureMatchesFilters(SequenceFeature sf)
1136   {
1137     KeyedMatcherSetI filter = featureFilters.get(sf.getType());
1138     // TODO temporary fudge for Score and Label
1139     return filter == null ? true
1140             : filter.matches(
1141                     key -> "Label".equals(key[0]) ? sf.getDescription()
1142                             : ("Score".equals(key[0])
1143                                     ? String.valueOf(sf.getScore())
1144                                     : sf.getValueAsString(key)));
1145   }
1146
1147 }