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