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