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