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