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