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