JAL-974 - broken copy of feature settings
[jalview.git] / src / jalview / gui / FeatureRenderer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.gui;
19
20 import java.util.*;
21 import java.util.concurrent.ConcurrentHashMap;
22
23 import java.awt.*;
24 import java.awt.event.*;
25 import java.awt.image.*;
26 import java.beans.PropertyChangeListener;
27 import java.beans.PropertyChangeSupport;
28
29 import javax.swing.*;
30
31 import jalview.datamodel.*;
32 import jalview.schemes.GraduatedColor;
33
34 /**
35  * DOCUMENT ME!
36  * 
37  * @author $author$
38  * @version $Revision$
39  */
40 public class FeatureRenderer implements jalview.api.FeatureRenderer
41 {
42   AlignmentPanel ap;
43
44   AlignViewport av;
45
46   Color resBoxColour;
47
48   /**
49    * global transparency for feature
50    */
51   float transparency = 1.0f;
52
53   FontMetrics fm;
54
55   int charOffset;
56  
57   Map featureColours = new ConcurrentHashMap();
58
59   // A higher level for grouping features of a
60   // particular type
61   Map featureGroups = new ConcurrentHashMap();
62
63   // This is actually an Integer held in the hashtable,
64   // Retrieved using the key feature type
65   Object currentColour;
66
67   String[] renderOrder;
68
69   PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
70
71   Vector allfeatures;
72
73   /**
74    * Creates a new FeatureRenderer object.
75    * 
76    * @param av
77    *          DOCUMENT ME!
78    */
79   public FeatureRenderer(AlignmentPanel ap)
80   {
81     this.ap = ap;
82     this.av = ap.av;
83   }
84
85   public class FeatureRendererSettings implements Cloneable
86   {
87     String[] renderOrder;
88
89     Map featureGroups;
90
91     Map featureColours;
92
93     float transparency;
94
95     Map featureOrder;
96
97     public FeatureRendererSettings(String[] renderOrder,
98             Hashtable featureGroups, Hashtable featureColours,
99             float transparency, Hashtable featureOrder)
100     {
101       super();
102       this.renderOrder = renderOrder;
103       this.featureGroups = featureGroups;
104       this.featureColours = featureColours;
105       this.transparency = transparency;
106       this.featureOrder = featureOrder;
107     }
108
109     /**
110      * create an independent instance of the feature renderer settings
111      * 
112      * @param fr
113      */
114     public FeatureRendererSettings(FeatureRenderer fr)
115     {
116       renderOrder = null;
117       featureGroups = new ConcurrentHashMap();
118       featureColours = new ConcurrentHashMap();
119       featureOrder = new ConcurrentHashMap();
120       if (fr.renderOrder != null)
121       {
122         this.renderOrder = new String[fr.renderOrder.length];
123         System.arraycopy(fr.renderOrder, 0, renderOrder, 0,
124                 fr.renderOrder.length);
125       }
126       if (fr.featureGroups != null)
127       {
128         this.featureGroups = new ConcurrentHashMap(fr.featureGroups);
129       }
130       if (fr.featureColours != null)
131       {
132         this.featureColours = new ConcurrentHashMap(fr.featureColours);
133       }
134       Iterator en = fr.featureColours.keySet().iterator();
135       while (en.hasNext())
136       {
137         Object next = en.next();
138         Object val = featureColours.get(next);
139         if (val instanceof GraduatedColor)
140         {
141           featureColours
142                   .put(next, new GraduatedColor((GraduatedColor) val));
143         }
144       }
145       this.transparency = fr.transparency;
146       if (fr.featureOrder != null)
147       {
148         this.featureOrder = new ConcurrentHashMap(fr.featureOrder);
149       }
150     }
151   }
152
153   public FeatureRendererSettings getSettings()
154   {
155     return new FeatureRendererSettings(this);
156   }
157
158   public void transferSettings(FeatureRendererSettings fr)
159   {
160     this.renderOrder = fr.renderOrder;
161     this.featureGroups = fr.featureGroups;
162     this.featureColours = fr.featureColours;
163     this.transparency = fr.transparency;
164     this.featureOrder = fr.featureOrder;
165   }
166
167   public void transferSettings(FeatureRenderer fr)
168   {
169     FeatureRendererSettings frs = new FeatureRendererSettings(fr);
170     this.renderOrder = frs.renderOrder;
171     this.featureGroups = frs.featureGroups;
172     this.featureColours = frs.featureColours;
173     this.transparency = frs.transparency;
174     this.featureOrder = frs.featureOrder;
175     if (av != null && av!=fr.av)
176     {
177       // copy over the displayed feature settings
178       if (fr.av != null)
179       {
180         if (fr.av.featuresDisplayed != null)
181         {
182           // update display settings
183           if (av.featuresDisplayed == null)
184           {
185             av.featuresDisplayed = new Hashtable(fr.av.featuresDisplayed);
186           }
187           else
188           {
189             av.featuresDisplayed.clear();
190             Enumeration en = fr.av.featuresDisplayed.keys();
191             while (en.hasMoreElements())
192             {
193               av.featuresDisplayed.put(en.nextElement(), Boolean.TRUE);
194             }
195
196           }
197         }
198       }
199   }
200   }
201
202   BufferedImage offscreenImage;
203
204   boolean offscreenRender = false;
205
206   public Color findFeatureColour(Color initialCol, SequenceI seq, int res)
207   {
208     return new Color(findFeatureColour(initialCol.getRGB(), seq, res));
209   }
210
211   /**
212    * This is used by the Molecule Viewer and Overview to get the accurate
213    * colourof the rendered sequence
214    */
215   public synchronized int findFeatureColour(int initialCol, SequenceI seq, int column)
216   {
217     if (!av.showSequenceFeatures)
218     {
219       return initialCol;
220     }
221
222     if (seq != lastSeq)
223     {
224       lastSeq = seq;
225       sequenceFeatures = lastSeq.getDatasetSequence().getSequenceFeatures();
226       if (sequenceFeatures != null)
227       {
228         sfSize = sequenceFeatures.length;
229       }
230     }
231
232     if (sequenceFeatures != lastSeq.getDatasetSequence()
233             .getSequenceFeatures())
234     {
235       sequenceFeatures = lastSeq.getDatasetSequence().getSequenceFeatures();
236       if (sequenceFeatures != null)
237       {
238         sfSize = sequenceFeatures.length;
239       }
240     }
241
242     if (sequenceFeatures == null || sfSize == 0)
243     {
244       return initialCol;
245     }
246
247     if (jalview.util.Comparison.isGap(lastSeq.getCharAt(column)))
248     {
249       return Color.white.getRGB();
250     }
251
252     // Only bother making an offscreen image if transparency is applied
253     if (transparency != 1.0f && offscreenImage == null)
254     {
255       offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
256     }
257
258     currentColour = null;
259     // TODO: non-threadsafe - each rendering thread needs its own instance of the feature renderer - or this should be synchronized.
260     offscreenRender = true;
261
262     if (offscreenImage != null)
263     {
264       offscreenImage.setRGB(0, 0, initialCol);
265       drawSequence(offscreenImage.getGraphics(), lastSeq, column, column, 0);
266
267       return offscreenImage.getRGB(0, 0);
268     }
269     else
270     {
271       drawSequence(null, lastSeq, lastSeq.findPosition(column), -1, -1);
272
273       if (currentColour == null)
274       {
275         return initialCol;
276       }
277       else
278       {
279         return ((Integer) currentColour).intValue();
280       }
281     }
282
283   }
284
285   /**
286    * DOCUMENT ME!
287    * 
288    * @param g
289    *          DOCUMENT ME!
290    * @param seq
291    *          DOCUMENT ME!
292    * @param sg
293    *          DOCUMENT ME!
294    * @param start
295    *          DOCUMENT ME!
296    * @param end
297    *          DOCUMENT ME!
298    * @param x1
299    *          DOCUMENT ME!
300    * @param y1
301    *          DOCUMENT ME!
302    * @param width
303    *          DOCUMENT ME!
304    * @param height
305    *          DOCUMENT ME!
306    */
307   // String type;
308   // SequenceFeature sf;
309   SequenceI lastSeq;
310
311   SequenceFeature[] sequenceFeatures;
312
313   int sfSize, sfindex, spos, epos;
314
315   /**
316    * show scores as heights
317    */
318   protected boolean varyHeight = false;
319
320   synchronized public void drawSequence(Graphics g, SequenceI seq,
321           int start, int end, int y1)
322   {
323
324     if (seq.getDatasetSequence().getSequenceFeatures() == null
325             || seq.getDatasetSequence().getSequenceFeatures().length == 0)
326     {
327       return;
328     }
329
330     if (g != null)
331     {
332       fm = g.getFontMetrics();
333     }
334
335     if (av.featuresDisplayed == null || renderOrder == null
336             || newFeatureAdded)
337     {
338       findAllFeatures();
339       if (av.featuresDisplayed.size() < 1)
340       {
341         return;
342       }
343
344       sequenceFeatures = seq.getDatasetSequence().getSequenceFeatures();
345     }
346
347     if (lastSeq == null
348             || seq != lastSeq
349             || seq.getDatasetSequence().getSequenceFeatures() != sequenceFeatures)
350     {
351       lastSeq = seq;
352       sequenceFeatures = seq.getDatasetSequence().getSequenceFeatures();
353     }
354
355     if (transparency != 1 && g != null)
356     {
357       Graphics2D g2 = (Graphics2D) g;
358       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
359               transparency));
360     }
361
362     if (!offscreenRender)
363     {
364       spos = lastSeq.findPosition(start);
365       epos = lastSeq.findPosition(end);
366     }
367
368     sfSize = sequenceFeatures.length;
369     String type;
370     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
371     {
372       type = renderOrder[renderIndex];
373
374       if (type == null || !av.featuresDisplayed.containsKey(type))
375       {
376         continue;
377       }
378
379       // loop through all features in sequence to find
380       // current feature to render
381       for (sfindex = 0; sfindex < sfSize; sfindex++)
382       {
383         if (!sequenceFeatures[sfindex].type.equals(type))
384         {
385           continue;
386         }
387
388         if (featureGroups != null
389                 && sequenceFeatures[sfindex].featureGroup != null
390                 && sequenceFeatures[sfindex].featureGroup.length() != 0
391                 && featureGroups
392                         .containsKey(sequenceFeatures[sfindex].featureGroup)
393                 && !((Boolean) featureGroups
394                         .get(sequenceFeatures[sfindex].featureGroup))
395                         .booleanValue())
396         {
397           continue;
398         }
399
400         if (!offscreenRender
401                 && (sequenceFeatures[sfindex].getBegin() > epos || sequenceFeatures[sfindex]
402                         .getEnd() < spos))
403         {
404           continue;
405         }
406
407         if (offscreenRender && offscreenImage == null)
408         {
409           if (sequenceFeatures[sfindex].begin <= start
410                   && sequenceFeatures[sfindex].end >= start)
411           {
412             // this is passed out to the overview and other sequence renderers
413             // (e.g. molecule viewer) to get displayed colour for rendered
414             // sequence
415             currentColour = new Integer(
416                     getColour(sequenceFeatures[sfindex]).getRGB());
417             // used to be retreived from av.featuresDisplayed
418             // currentColour = av.featuresDisplayed
419             // .get(sequenceFeatures[sfindex].type);
420
421           }
422         }
423         else if (sequenceFeatures[sfindex].type.equals("disulfide bond"))
424         {
425
426           renderFeature(g, seq,
427                   seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
428                   seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
429                   getColour(sequenceFeatures[sfindex])
430                   // new Color(((Integer) av.featuresDisplayed
431                   // .get(sequenceFeatures[sfindex].type)).intValue())
432                   , start, end, y1);
433           renderFeature(g, seq,
434                   seq.findIndex(sequenceFeatures[sfindex].end) - 1,
435                   seq.findIndex(sequenceFeatures[sfindex].end) - 1,
436                   getColour(sequenceFeatures[sfindex])
437                   // new Color(((Integer) av.featuresDisplayed
438                   // .get(sequenceFeatures[sfindex].type)).intValue())
439                   , start, end, y1);
440
441         }
442         else if (showFeature(sequenceFeatures[sfindex]))
443         {
444           if (av.showSeqFeaturesHeight
445                   && sequenceFeatures[sfindex].score != Float.NaN)
446           {
447             renderScoreFeature(g, seq,
448                     seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
449                     seq.findIndex(sequenceFeatures[sfindex].end) - 1,
450                     getColour(sequenceFeatures[sfindex]), start, end, y1,
451                     normaliseScore(sequenceFeatures[sfindex]));
452           }
453           else
454           {
455             renderFeature(g, seq,
456                     seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
457                     seq.findIndex(sequenceFeatures[sfindex].end) - 1,
458                     getColour(sequenceFeatures[sfindex]), start, end, y1);
459           }
460         }
461
462       }
463
464     }
465
466     if (transparency != 1.0f && g != null)
467     {
468       Graphics2D g2 = (Graphics2D) g;
469       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
470               1.0f));
471     }
472   }
473
474   Hashtable minmax = new Hashtable();
475
476   /**
477    * normalise a score against the max/min bounds for the feature type.
478    * 
479    * @param sequenceFeature
480    * @return byte[] { signed, normalised signed (-127 to 127) or unsigned
481    *         (0-255) value.
482    */
483   private final byte[] normaliseScore(SequenceFeature sequenceFeature)
484   {
485     float[] mm = ((float[][]) minmax.get(sequenceFeature.type))[0];
486     final byte[] r = new byte[]
487     { 0, (byte) 255 };
488     if (mm != null)
489     {
490       if (r[0] != 0 || mm[0] < 0.0)
491       {
492         r[0] = 1;
493         r[1] = (byte) ((int) 128.0 + 127.0 * (sequenceFeature.score / mm[1]));
494       }
495       else
496       {
497         r[1] = (byte) ((int) 255.0 * (sequenceFeature.score / mm[1]));
498       }
499     }
500     return r;
501   }
502
503   char s;
504
505   int i;
506
507   void renderFeature(Graphics g, SequenceI seq, int fstart, int fend,
508           Color featureColour, int start, int end, int y1)
509   {
510
511     if (((fstart <= end) && (fend >= start)))
512     {
513       if (fstart < start)
514       { // fix for if the feature we have starts before the sequence start,
515         fstart = start; // but the feature end is still valid!!
516       }
517
518       if (fend >= end)
519       {
520         fend = end;
521       }
522       int pady = (y1 + av.charHeight) - av.charHeight / 5;
523       for (i = fstart; i <= fend; i++)
524       {
525         s = seq.getCharAt(i);
526
527         if (jalview.util.Comparison.isGap(s))
528         {
529           continue;
530         }
531
532         g.setColor(featureColour);
533
534         g.fillRect((i - start) * av.charWidth, y1, av.charWidth,
535                 av.charHeight);
536
537         if (offscreenRender || !av.validCharWidth)
538         {
539           continue;
540         }
541
542         g.setColor(Color.white);
543         charOffset = (av.charWidth - fm.charWidth(s)) / 2;
544         g.drawString(String.valueOf(s), charOffset
545                 + (av.charWidth * (i - start)), pady);
546
547       }
548     }
549   }
550
551   void renderScoreFeature(Graphics g, SequenceI seq, int fstart, int fend,
552           Color featureColour, int start, int end, int y1, byte[] bs)
553   {
554
555     if (((fstart <= end) && (fend >= start)))
556     {
557       if (fstart < start)
558       { // fix for if the feature we have starts before the sequence start,
559         fstart = start; // but the feature end is still valid!!
560       }
561
562       if (fend >= end)
563       {
564         fend = end;
565       }
566       int pady = (y1 + av.charHeight) - av.charHeight / 5;
567       int ystrt = 0, yend = av.charHeight;
568       if (bs[0] != 0)
569       {
570         // signed - zero is always middle of residue line.
571         if (bs[1] < 128)
572         {
573           yend = av.charHeight * (128 - bs[1]) / 512;
574           ystrt = av.charHeight - yend / 2;
575         }
576         else
577         {
578           ystrt = av.charHeight / 2;
579           yend = av.charHeight * (bs[1] - 128) / 512;
580         }
581       }
582       else
583       {
584         yend = av.charHeight * bs[1] / 255;
585         ystrt = av.charHeight - yend;
586
587       }
588       for (i = fstart; i <= fend; i++)
589       {
590         s = seq.getCharAt(i);
591
592         if (jalview.util.Comparison.isGap(s))
593         {
594           continue;
595         }
596
597         g.setColor(featureColour);
598         int x = (i - start) * av.charWidth;
599         g.drawRect(x, y1, av.charWidth, av.charHeight);
600         g.fillRect(x, y1 + ystrt, av.charWidth, yend);
601
602         if (offscreenRender || !av.validCharWidth)
603         {
604           continue;
605         }
606
607         g.setColor(Color.black);
608         charOffset = (av.charWidth - fm.charWidth(s)) / 2;
609         g.drawString(String.valueOf(s), charOffset
610                 + (av.charWidth * (i - start)), pady);
611
612       }
613     }
614   }
615
616   boolean newFeatureAdded = false;
617
618   /**
619    * Called when alignment in associated view has new/modified features to
620    * discover and display.
621    * 
622    */
623   public void featuresAdded()
624   {
625     lastSeq = null;
626     findAllFeatures();
627   }
628
629   boolean findingFeatures = false;
630
631   /**
632    * search the alignment for all new features, give them a colour and display
633    * them. Then fires a PropertyChangeEvent on the changeSupport object.
634    * 
635    */
636   void findAllFeatures()
637   {
638     synchronized (firing)
639     {
640       if (firing.equals(Boolean.FALSE))
641       {
642         firing = Boolean.TRUE;
643         findAllFeatures(true); // add all new features as visible
644         changeSupport.firePropertyChange("changeSupport", null, null);
645         firing = Boolean.FALSE;
646       }
647     }
648   }
649
650   /**
651    * Searches alignment for all features and updates colours
652    * 
653    * @param newMadeVisible
654    *          if true newly added feature types will be rendered immediatly
655    */
656   synchronized void findAllFeatures(boolean newMadeVisible)
657   {
658     newFeatureAdded = false;
659
660     if (findingFeatures)
661     {
662       newFeatureAdded = true;
663       return;
664     }
665
666     findingFeatures = true;
667
668     if (av.featuresDisplayed == null)
669     {
670       av.featuresDisplayed = new Hashtable();
671     }
672
673     allfeatures = new Vector();
674     Vector oldfeatures = new Vector();
675     if (renderOrder != null)
676     {
677       for (int i = 0; i < renderOrder.length; i++)
678       {
679         if (renderOrder[i] != null)
680         {
681           oldfeatures.addElement(renderOrder[i]);
682         }
683       }
684     }
685     if (minmax == null)
686     {
687       minmax = new Hashtable();
688     }
689     AlignmentI alignment=av.getAlignment();
690     for (int i = 0; i < alignment.getHeight(); i++)
691     {
692       SequenceFeature[] features = alignment.getSequenceAt(i)
693               .getDatasetSequence().getSequenceFeatures();
694
695       if (features == null)
696       {
697         continue;
698       }
699
700       int index = 0;
701       while (index < features.length)
702       {
703         if (!av.featuresDisplayed.containsKey(features[index].getType()))
704         {
705
706           if (featureGroups.containsKey(features[index].getType()))
707           {
708             boolean visible = ((Boolean) featureGroups
709                     .get(features[index].featureGroup)).booleanValue();
710
711             if (!visible)
712             {
713               index++;
714               continue;
715             }
716           }
717
718           if (!(features[index].begin == 0 && features[index].end == 0))
719           {
720             // If beginning and end are 0, the feature is for the whole sequence
721             // and we don't want to render the feature in the normal way
722
723             if (newMadeVisible
724                     && !oldfeatures.contains(features[index].getType()))
725             {
726               // this is a new feature type on the alignment. Mark it for
727               // display.
728               av.featuresDisplayed.put(features[index].getType(),
729                       new Integer(getColour(features[index].getType())
730                               .getRGB()));
731               setOrder(features[index].getType(), 0);
732             }
733           }
734         }
735         if (!allfeatures.contains(features[index].getType()))
736         {
737           allfeatures.addElement(features[index].getType());
738         }
739         if (features[index].score != Float.NaN)
740         {
741           int nonpos = features[index].getBegin() >= 1 ? 0 : 1;
742           float[][] mm = (float[][]) minmax.get(features[index].getType());
743           if (mm == null)
744           {
745             mm = new float[][]
746             { null, null };
747             minmax.put(features[index].getType(), mm);
748           }
749           if (mm[nonpos] == null)
750           {
751             mm[nonpos] = new float[]
752             { features[index].score, features[index].score };
753
754           }
755           else
756           {
757             if (mm[nonpos][0] > features[index].score)
758             {
759               mm[nonpos][0] = features[index].score;
760             }
761             if (mm[nonpos][1] < features[index].score)
762             {
763               mm[nonpos][1] = features[index].score;
764             }
765           }
766         }
767         index++;
768       }
769     }
770     updateRenderOrder(allfeatures);
771     findingFeatures = false;
772   }
773
774   protected Boolean firing = Boolean.FALSE;
775
776   /**
777    * replaces the current renderOrder with the unordered features in
778    * allfeatures. The ordering of any types in both renderOrder and allfeatures
779    * is preserved, and all new feature types are rendered on top of the existing
780    * types, in the order given by getOrder or the order given in allFeatures.
781    * Note. this operates directly on the featureOrder hash for efficiency. TODO:
782    * eliminate the float storage for computing/recalling the persistent ordering
783    * New Cability: updates min/max for colourscheme range if its dynamic
784    * 
785    * @param allFeatures
786    */
787   private void updateRenderOrder(Vector allFeatures)
788   {
789     Vector allfeatures = new Vector(allFeatures);
790     String[] oldRender = renderOrder;
791     renderOrder = new String[allfeatures.size()];
792     Object mmrange, fc = null;
793     boolean initOrders = (featureOrder == null);
794     int opos = 0;
795     if (oldRender != null && oldRender.length > 0)
796     {
797       for (int j = 0; j < oldRender.length; j++)
798       {
799         if (oldRender[j] != null)
800         {
801           if (initOrders)
802           {
803             setOrder(oldRender[j], (1 - (1 + (float) j)
804                     / (float) oldRender.length));
805           }
806           if (allfeatures.contains(oldRender[j]))
807           {
808             renderOrder[opos++] = oldRender[j]; // existing features always
809             // appear below new features
810             allfeatures.removeElement(oldRender[j]);
811             if (minmax != null)
812             {
813               mmrange = minmax.get(oldRender[j]);
814               if (mmrange != null)
815               {
816                 fc = featureColours.get(oldRender[j]);
817                 if (fc != null && fc instanceof GraduatedColor
818                         && ((GraduatedColor) fc).isAutoScale())
819                 {
820                   ((GraduatedColor) fc).updateBounds(
821                           ((float[][]) mmrange)[0][0],
822                           ((float[][]) mmrange)[0][1]);
823                 }
824               }
825             }
826           }
827         }
828       }
829     }
830     if (allfeatures.size() == 0)
831     {
832       // no new features - leave order unchanged.
833       return;
834     }
835     int i = allfeatures.size() - 1;
836     int iSize = i;
837     boolean sort = false;
838     String[] newf = new String[allfeatures.size()];
839     float[] sortOrder = new float[allfeatures.size()];
840     Enumeration en = allfeatures.elements();
841     // sort remaining elements
842     while (en.hasMoreElements())
843     {
844       newf[i] = en.nextElement().toString();
845       if (minmax != null)
846       {
847         // update from new features minmax if necessary
848         mmrange = minmax.get(newf[i]);
849         if (mmrange != null)
850         {
851           fc = featureColours.get(newf[i]);
852           if (fc != null && fc instanceof GraduatedColor
853                   && ((GraduatedColor) fc).isAutoScale())
854           {
855             ((GraduatedColor) fc).updateBounds(((float[][]) mmrange)[0][0],
856                     ((float[][]) mmrange)[0][1]);
857           }
858         }
859       }
860       if (initOrders || !featureOrder.containsKey(newf[i]))
861       {
862         int denom = initOrders ? allfeatures.size() : featureOrder.size();
863         // new unordered feature - compute persistent ordering at head of
864         // existing features.
865         setOrder(newf[i], i / (float) denom);
866       }
867       // set order from newly found feature from persisted ordering.
868       sortOrder[i] = 2 - ((Float) featureOrder.get(newf[i])).floatValue();
869       if (i < iSize)
870       {
871         // only sort if we need to
872         sort = sort || sortOrder[i] > sortOrder[i + 1];
873       }
874       i--;
875     }
876     if (iSize > 1 && sort)
877     {
878       jalview.util.QuickSort.sort(sortOrder, newf);
879     }
880     sortOrder = null;
881     System.arraycopy(newf, 0, renderOrder, opos, newf.length);
882   }
883
884   /**
885    * get a feature style object for the given type string. Creates a
886    * java.awt.Color for a featureType with no existing colourscheme. TODO:
887    * replace return type with object implementing standard abstract colour/style
888    * interface
889    * 
890    * @param featureType
891    * @return java.awt.Color or GraduatedColor
892    */
893   public Object getFeatureStyle(String featureType)
894   {
895     Object fc = featureColours.get(featureType);
896     if (fc == null)
897     {
898       jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
899       Color col = ucs.createColourFromName(featureType);
900       featureColours.put(featureType, fc = col);
901     }
902     return fc;
903   }
904
905   /**
906    * return a nominal colour for this feature
907    * 
908    * @param featureType
909    * @return standard color, or maximum colour for graduated colourscheme
910    */
911   public Color getColour(String featureType)
912   {
913     Object fc = getFeatureStyle(featureType);
914
915     if (fc instanceof Color)
916     {
917       return (Color) fc;
918     }
919     else
920     {
921       if (fc instanceof GraduatedColor)
922       {
923         return ((GraduatedColor) fc).getMaxColor();
924       }
925     }
926     throw new Error("Implementation Error: Unrecognised render object "
927             + fc.getClass() + " for features of type " + featureType);
928   }
929
930   /**
931    * calculate the render colour for a specific feature using current feature
932    * settings.
933    * 
934    * @param feature
935    * @return render colour for the given feature
936    */
937   public Color getColour(SequenceFeature feature)
938   {
939     Object fc = getFeatureStyle(feature.getType());
940     if (fc instanceof Color)
941     {
942       return (Color) fc;
943     }
944     else
945     {
946       if (fc instanceof GraduatedColor)
947       {
948         return ((GraduatedColor) fc).findColor(feature);
949       }
950     }
951     throw new Error("Implementation Error: Unrecognised render object "
952             + fc.getClass() + " for features of type " + feature.getType());
953   }
954
955   private boolean showFeature(SequenceFeature sequenceFeature)
956   {
957     Object fc = getFeatureStyle(sequenceFeature.type);
958     if (fc instanceof GraduatedColor)
959     {
960       return ((GraduatedColor) fc).isColored(sequenceFeature);
961     }
962     else
963     {
964       return true;
965     }
966   }
967
968   // // /////////////
969   // // Feature Editing Dialog
970   // // Will be refactored in next release.
971
972   static String lastFeatureAdded;
973
974   static String lastFeatureGroupAdded;
975
976   static String lastDescriptionAdded;
977
978   Object oldcol, fcol;
979
980   int featureIndex = 0;
981
982   boolean amendFeatures(final SequenceI[] sequences,
983           final SequenceFeature[] features, boolean newFeatures,
984           final AlignmentPanel ap)
985   {
986
987     featureIndex = 0;
988
989     final JPanel bigPanel = new JPanel(new BorderLayout());
990     final JComboBox overlaps;
991     final JTextField name = new JTextField(25);
992     final JTextField source = new JTextField(25);
993     final JTextArea description = new JTextArea(3, 25);
994     final JSpinner start = new JSpinner();
995     final JSpinner end = new JSpinner();
996     start.setPreferredSize(new Dimension(80, 20));
997     end.setPreferredSize(new Dimension(80, 20));
998     final FeatureRenderer me = this;
999     final JLabel colour = new JLabel();
1000     colour.setOpaque(true);
1001     // colour.setBorder(BorderFactory.createEtchedBorder());
1002     colour.setMaximumSize(new Dimension(30, 16));
1003     colour.addMouseListener(new MouseAdapter()
1004     {
1005       FeatureColourChooser fcc = null;
1006
1007       public void mousePressed(MouseEvent evt)
1008       {
1009         if (fcol instanceof Color)
1010         {
1011           Color col = JColorChooser.showDialog(Desktop.desktop,
1012                   "Select Feature Colour", ((Color) fcol));
1013           if (col != null)
1014           {
1015             fcol = col;
1016             updateColourButton(bigPanel, colour, col);
1017           }
1018         }
1019         else
1020         {
1021
1022           if (fcc == null)
1023           {
1024             final String type = features[featureIndex].getType();
1025             fcc = new FeatureColourChooser(me, type);
1026             fcc.setRequestFocusEnabled(true);
1027             fcc.requestFocus();
1028
1029             fcc.addActionListener(new ActionListener()
1030             {
1031
1032               public void actionPerformed(ActionEvent e)
1033               {
1034                 fcol = fcc.getLastColour();
1035                 fcc = null;
1036                 setColour(type, fcol);
1037                 updateColourButton(bigPanel, colour, fcol);
1038               }
1039             });
1040
1041           }
1042         }
1043       }
1044     });
1045     JPanel tmp = new JPanel();
1046     JPanel panel = new JPanel(new GridLayout(3, 1));
1047
1048     // /////////////////////////////////////
1049     // /MULTIPLE FEATURES AT SELECTED RESIDUE
1050     if (!newFeatures && features.length > 1)
1051     {
1052       panel = new JPanel(new GridLayout(4, 1));
1053       tmp = new JPanel();
1054       tmp.add(new JLabel("Select Feature: "));
1055       overlaps = new JComboBox();
1056       for (int i = 0; i < features.length; i++)
1057       {
1058         overlaps.addItem(features[i].getType() + "/"
1059                 + features[i].getBegin() + "-" + features[i].getEnd()
1060                 + " (" + features[i].getFeatureGroup() + ")");
1061       }
1062
1063       tmp.add(overlaps);
1064
1065       overlaps.addItemListener(new ItemListener()
1066       {
1067         public void itemStateChanged(ItemEvent e)
1068         {
1069           int index = overlaps.getSelectedIndex();
1070           if (index != -1)
1071           {
1072             featureIndex = index;
1073             name.setText(features[index].getType());
1074             description.setText(features[index].getDescription());
1075             source.setText(features[index].getFeatureGroup());
1076             start.setValue(new Integer(features[index].getBegin()));
1077             end.setValue(new Integer(features[index].getEnd()));
1078
1079             SearchResults highlight = new SearchResults();
1080             highlight.addResult(sequences[0], features[index].getBegin(),
1081                     features[index].getEnd());
1082
1083             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
1084
1085           }
1086           Object col = getFeatureStyle(name.getText());
1087           if (col == null)
1088           {
1089             col = new jalview.schemes.UserColourScheme()
1090                     .createColourFromName(name.getText());
1091           }
1092           oldcol = fcol = col;
1093           updateColourButton(bigPanel, colour, col);
1094         }
1095       });
1096
1097       panel.add(tmp);
1098     }
1099     // ////////
1100     // ////////////////////////////////////
1101
1102     tmp = new JPanel();
1103     panel.add(tmp);
1104     tmp.add(new JLabel("Name: ", JLabel.RIGHT));
1105     tmp.add(name);
1106
1107     tmp = new JPanel();
1108     panel.add(tmp);
1109     tmp.add(new JLabel("Group: ", JLabel.RIGHT));
1110     tmp.add(source);
1111
1112     tmp = new JPanel();
1113     panel.add(tmp);
1114     tmp.add(new JLabel("Colour: ", JLabel.RIGHT));
1115     tmp.add(colour);
1116     colour.setPreferredSize(new Dimension(150, 15));
1117     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
1118     colour.setForeground(Color.black);
1119     colour.setHorizontalAlignment(SwingConstants.CENTER);
1120     colour.setVerticalAlignment(SwingConstants.CENTER);
1121     colour.setHorizontalTextPosition(SwingConstants.CENTER);
1122     colour.setVerticalTextPosition(SwingConstants.CENTER);
1123     bigPanel.add(panel, BorderLayout.NORTH);
1124
1125     panel = new JPanel();
1126     panel.add(new JLabel("Description: ", JLabel.RIGHT));
1127     description.setFont(JvSwingUtils.getTextAreaFont());
1128     description.setLineWrap(true);
1129     panel.add(new JScrollPane(description));
1130
1131     if (!newFeatures)
1132     {
1133       bigPanel.add(panel, BorderLayout.SOUTH);
1134
1135       panel = new JPanel();
1136       panel.add(new JLabel(" Start:", JLabel.RIGHT));
1137       panel.add(start);
1138       panel.add(new JLabel("  End:", JLabel.RIGHT));
1139       panel.add(end);
1140       bigPanel.add(panel, BorderLayout.CENTER);
1141     }
1142     else
1143     {
1144       bigPanel.add(panel, BorderLayout.CENTER);
1145     }
1146
1147     if (lastFeatureAdded == null)
1148     {
1149       if (features[0].type != null)
1150       {
1151         lastFeatureAdded = features[0].type;
1152       }
1153       else
1154       {
1155         lastFeatureAdded = "feature_1";
1156       }
1157     }
1158
1159     if (lastFeatureGroupAdded == null)
1160     {
1161       if (features[0].featureGroup != null)
1162       {
1163         lastFeatureGroupAdded = features[0].featureGroup;
1164       }
1165       else
1166       {
1167         lastFeatureGroupAdded = "Jalview";
1168       }
1169     }
1170
1171     if (newFeatures)
1172     {
1173       name.setText(lastFeatureAdded);
1174       source.setText(lastFeatureGroupAdded);
1175     }
1176     else
1177     {
1178       name.setText(features[0].getType());
1179       source.setText(features[0].getFeatureGroup());
1180     }
1181
1182     start.setValue(new Integer(features[0].getBegin()));
1183     end.setValue(new Integer(features[0].getEnd()));
1184     description.setText(features[0].getDescription());
1185     updateColourButton(bigPanel, colour,
1186             (oldcol = fcol = getFeatureStyle(name.getText())));
1187     Object[] options;
1188     if (!newFeatures)
1189     {
1190       options = new Object[]
1191       { "Amend", "Delete", "Cancel" };
1192     }
1193     else
1194     {
1195       options = new Object[]
1196       { "OK", "Cancel" };
1197     }
1198
1199     String title = newFeatures ? "Create New Sequence Feature(s)"
1200             : "Amend/Delete Features for " + sequences[0].getName();
1201
1202     int reply = JOptionPane.showInternalOptionDialog(Desktop.desktop,
1203             bigPanel, title, JOptionPane.YES_NO_CANCEL_OPTION,
1204             JOptionPane.QUESTION_MESSAGE, null, options, "OK");
1205
1206     jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
1207
1208     if (reply == JOptionPane.OK_OPTION && name.getText().length() > 0)
1209     {
1210       // This ensures that the last sequence
1211       // is refreshed and new features are rendered
1212       lastSeq = null;
1213       lastFeatureAdded = name.getText().trim();
1214       lastFeatureGroupAdded = source.getText().trim();
1215       lastDescriptionAdded = description.getText().replaceAll("\n", " ");
1216       // TODO: determine if the null feature group is valid
1217       if (lastFeatureGroupAdded.length() < 1)
1218         lastFeatureGroupAdded = null;
1219     }
1220
1221     if (!newFeatures)
1222     {
1223       SequenceFeature sf = features[featureIndex];
1224
1225       if (reply == JOptionPane.NO_OPTION)
1226       {
1227         sequences[0].getDatasetSequence().deleteFeature(sf);
1228       }
1229       else if (reply == JOptionPane.YES_OPTION)
1230       {
1231         sf.type = lastFeatureAdded;
1232         sf.featureGroup = lastFeatureGroupAdded;
1233         sf.description = lastDescriptionAdded;
1234
1235         setColour(sf.type, fcol);
1236         av.featuresDisplayed.put(sf.type, getColour(sf.type));
1237
1238         try
1239         {
1240           sf.begin = ((Integer) start.getValue()).intValue();
1241           sf.end = ((Integer) end.getValue()).intValue();
1242         } catch (NumberFormatException ex)
1243         {
1244         }
1245
1246         ffile.parseDescriptionHTML(sf, false);
1247       }
1248     }
1249     else
1250     // NEW FEATURES ADDED
1251     {
1252       if (reply == JOptionPane.OK_OPTION && lastFeatureAdded.length() > 0)
1253       {
1254         for (int i = 0; i < sequences.length; i++)
1255         {
1256           features[i].type = lastFeatureAdded;
1257           if (lastFeatureGroupAdded != null)
1258             features[i].featureGroup = lastFeatureGroupAdded;
1259           features[i].description = lastDescriptionAdded;
1260           sequences[i].addSequenceFeature(features[i]);
1261           ffile.parseDescriptionHTML(features[i], false);
1262         }
1263
1264         if (av.featuresDisplayed == null)
1265         {
1266           av.featuresDisplayed = new Hashtable();
1267         }
1268
1269         if (lastFeatureGroupAdded != null)
1270         {
1271           if (featureGroups == null)
1272             featureGroups = new Hashtable();
1273           featureGroups.put(lastFeatureGroupAdded, new Boolean(true));
1274         }
1275         setColour(lastFeatureAdded, fcol);
1276         av.featuresDisplayed.put(lastFeatureAdded,
1277                 getColour(lastFeatureAdded));
1278
1279         findAllFeatures(false);
1280
1281         ap.paintAlignment(true);
1282
1283         return true;
1284       }
1285       else
1286       {
1287         return false;
1288       }
1289     }
1290
1291     ap.paintAlignment(true);
1292
1293     return true;
1294   }
1295
1296   /**
1297    * update the amend feature button dependent on the given style
1298    * 
1299    * @param bigPanel
1300    * @param col
1301    * @param col2
1302    */
1303   protected void updateColourButton(JPanel bigPanel, JLabel colour,
1304           Object col2)
1305   {
1306     colour.removeAll();
1307     colour.setIcon(null);
1308     colour.setToolTipText(null);
1309     colour.setText("");
1310
1311     if (col2 instanceof Color)
1312     {
1313       colour.setBackground((Color) col2);
1314     }
1315     else
1316     {
1317       colour.setBackground(bigPanel.getBackground());
1318       colour.setForeground(Color.black);
1319       FeatureSettings.renderGraduatedColor(colour, (GraduatedColor) col2);
1320       // colour.setForeground(colour.getBackground());
1321     }
1322   }
1323
1324   public void setColour(String featureType, Object col)
1325   {
1326     // overwrite
1327     // Color _col = (col instanceof Color) ? ((Color) col) : (col instanceof
1328     // GraduatedColor) ? ((GraduatedColor) col).getMaxColor() : null;
1329     // Object c = featureColours.get(featureType);
1330     // if (c == null || c instanceof Color || (c instanceof GraduatedColor &&
1331     // !((GraduatedColor)c).getMaxColor().equals(_col)))
1332     {
1333       featureColours.put(featureType, col);
1334     }
1335   }
1336
1337   public void setTransparency(float value)
1338   {
1339     transparency = value;
1340   }
1341
1342   public float getTransparency()
1343   {
1344     return transparency;
1345   }
1346
1347   /**
1348    * Replace current ordering with new ordering
1349    * 
1350    * @param data
1351    *          { String(Type), Colour(Type), Boolean(Displayed) }
1352    */
1353   public void setFeaturePriority(Object[][] data)
1354   {
1355     setFeaturePriority(data, true);
1356   }
1357
1358   /**
1359    * 
1360    * @param data
1361    *          { String(Type), Colour(Type), Boolean(Displayed) }
1362    * @param visibleNew
1363    *          when true current featureDisplay list will be cleared
1364    */
1365   public void setFeaturePriority(Object[][] data, boolean visibleNew)
1366   {
1367     if (visibleNew)
1368     {
1369       if (av.featuresDisplayed != null)
1370       {
1371         av.featuresDisplayed.clear();
1372       }
1373       else
1374       {
1375         av.featuresDisplayed = new Hashtable();
1376       }
1377     }
1378     if (data == null)
1379     {
1380       return;
1381     }
1382
1383     // The feature table will display high priority
1384     // features at the top, but theses are the ones
1385     // we need to render last, so invert the data
1386     renderOrder = new String[data.length];
1387
1388     if (data.length > 0)
1389     {
1390       for (int i = 0; i < data.length; i++)
1391       {
1392         String type = data[i][0].toString();
1393         setColour(type, data[i][1]); // todo : typesafety - feature color
1394         // interface object
1395         if (((Boolean) data[i][2]).booleanValue())
1396         {
1397           av.featuresDisplayed.put(type, new Integer(getColour(type)
1398                   .getRGB()));
1399         }
1400
1401         renderOrder[data.length - i - 1] = type;
1402       }
1403     }
1404
1405   }
1406
1407   Map featureOrder = null;
1408
1409   /**
1410    * analogous to colour - store a normalized ordering for all feature types in
1411    * this rendering context.
1412    * 
1413    * @param type
1414    *          Feature type string
1415    * @param position
1416    *          normalized priority - 0 means always appears on top, 1 means
1417    *          always last.
1418    */
1419   public float setOrder(String type, float position)
1420   {
1421     if (featureOrder == null)
1422     {
1423       featureOrder = new Hashtable();
1424     }
1425     featureOrder.put(type, new Float(position));
1426     return position;
1427   }
1428
1429   /**
1430    * get the global priority (0 (top) to 1 (bottom))
1431    * 
1432    * @param type
1433    * @return [0,1] or -1 for a type without a priority
1434    */
1435   public float getOrder(String type)
1436   {
1437     if (featureOrder != null)
1438     {
1439       if (featureOrder.containsKey(type))
1440       {
1441         return ((Float) featureOrder.get(type)).floatValue();
1442       }
1443     }
1444     return -1;
1445   }
1446
1447   /**
1448    * @param listener
1449    * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
1450    */
1451   public void addPropertyChangeListener(PropertyChangeListener listener)
1452   {
1453     changeSupport.addPropertyChangeListener(listener);
1454   }
1455
1456   /**
1457    * @param listener
1458    * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
1459    */
1460   public void removePropertyChangeListener(PropertyChangeListener listener)
1461   {
1462     changeSupport.removePropertyChangeListener(listener);
1463   }
1464 }