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