more graduated feature support
[jalview.git] / src / jalview / gui / FeatureRenderer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.4)
3  * Copyright (C) 2008 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   public Color getColour(String featureType)
823   {
824     Object fc = featureColours.get(featureType);
825     if (fc == null)
826     {
827       jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
828       Color col = ucs.createColourFromName(featureType);
829       featureColours.put(featureType, col);
830       return col;
831     }
832     else if (fc instanceof Color)
833     {
834       return (Color) fc;
835     }
836     else
837     {
838       if (fc instanceof GraduatedColor)
839       {
840         return ((GraduatedColor) fc).getMaxColor();
841       }
842       // TODO: raise an implementation error here.
843       return null; // Color.white;
844     }
845   }
846
847   /**
848    * implement graduated colouring for features with scores
849    * 
850    * @param feature
851    * @return render colour for the given feature
852    */
853   public Color getColour(SequenceFeature feature)
854   {
855     Object fc = featureColours.get(feature.type);
856     if (fc == null)
857     {
858       jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
859       Color col = ucs.createColourFromName(feature.type);
860       featureColours.put(feature.type, col);
861       return col;
862     }
863     else if (fc instanceof Color)
864     {
865       return (Color) fc;
866     }
867     else
868     {
869       if (fc instanceof GraduatedColor)
870       {
871         return ((GraduatedColor) fc).findColor(feature);
872       }
873       // TODO: raise an implementation error here.
874       return null; // Color.white;
875     }
876   }
877   private boolean showFeature(SequenceFeature sequenceFeature)
878   {
879     Object fc = featureColours.get(sequenceFeature.type);
880     if (fc instanceof GraduatedColor)
881     {
882       return ((GraduatedColor) fc).isColored(sequenceFeature);
883     } else { return true; }
884   }
885
886
887   static String lastFeatureAdded;
888
889   static String lastFeatureGroupAdded;
890
891   static String lastDescriptionAdded;
892
893   int featureIndex = 0;
894
895   boolean amendFeatures(final SequenceI[] sequences,
896           final SequenceFeature[] features, boolean newFeatures,
897           final AlignmentPanel ap)
898   {
899
900     featureIndex = 0;
901
902     JPanel bigPanel = new JPanel(new BorderLayout());
903     final JComboBox overlaps;
904     final JTextField name = new JTextField(25);
905     final JTextField source = new JTextField(25);
906     final JTextArea description = new JTextArea(3, 25);
907     final JSpinner start = new JSpinner();
908     final JSpinner end = new JSpinner();
909     start.setPreferredSize(new Dimension(80, 20));
910     end.setPreferredSize(new Dimension(80, 20));
911
912     final JPanel colour = new JPanel();
913     colour.setBorder(BorderFactory.createEtchedBorder());
914     colour.setMaximumSize(new Dimension(40, 10));
915     colour.addMouseListener(new MouseAdapter()
916     {
917       public void mousePressed(MouseEvent evt)
918       {
919         // TODO: use featurecolourchooser here
920         Color col = JColorChooser.showDialog(Desktop.desktop,
921                 "Select Feature Colour", colour.getBackground());
922         if (col != null)
923           colour.setBackground(col);
924
925       }
926     });
927
928     JPanel tmp = new JPanel();
929     JPanel panel = new JPanel(new GridLayout(3, 1));
930
931     // /////////////////////////////////////
932     // /MULTIPLE FEATURES AT SELECTED RESIDUE
933     if (!newFeatures && features.length > 1)
934     {
935       panel = new JPanel(new GridLayout(4, 1));
936       tmp = new JPanel();
937       tmp.add(new JLabel("Select Feature: "));
938       overlaps = new JComboBox();
939       for (int i = 0; i < features.length; i++)
940       {
941         overlaps.addItem(features[i].getType() + "/"
942                 + features[i].getBegin() + "-" + features[i].getEnd()
943                 + " (" + features[i].getFeatureGroup() + ")");
944       }
945
946       tmp.add(overlaps);
947
948       overlaps.addItemListener(new ItemListener()
949       {
950         public void itemStateChanged(ItemEvent e)
951         {
952           int index = overlaps.getSelectedIndex();
953           if (index != -1)
954           {
955             featureIndex = index;
956             name.setText(features[index].getType());
957             description.setText(features[index].getDescription());
958             source.setText(features[index].getFeatureGroup());
959             start.setValue(new Integer(features[index].getBegin()));
960             end.setValue(new Integer(features[index].getEnd()));
961
962             SearchResults highlight = new SearchResults();
963             highlight.addResult(sequences[0], features[index].getBegin(),
964                     features[index].getEnd());
965
966             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
967
968           }
969           Color col = getColour(name.getText());
970           if (col == null)
971           {
972             col = new jalview.schemes.UserColourScheme()
973                     .createColourFromName(name.getText());
974           }
975
976           colour.setBackground(col);
977         }
978       });
979
980       panel.add(tmp);
981     }
982     // ////////
983     // ////////////////////////////////////
984
985     tmp = new JPanel();
986     panel.add(tmp);
987     tmp.add(new JLabel("Name: ", JLabel.RIGHT));
988     tmp.add(name);
989
990     tmp = new JPanel();
991     panel.add(tmp);
992     tmp.add(new JLabel("Group: ", JLabel.RIGHT));
993     tmp.add(source);
994
995     tmp = new JPanel();
996     panel.add(tmp);
997     tmp.add(new JLabel("Colour: ", JLabel.RIGHT));
998     tmp.add(colour);
999     colour.setPreferredSize(new Dimension(150, 15));
1000
1001     bigPanel.add(panel, BorderLayout.NORTH);
1002
1003     panel = new JPanel();
1004     panel.add(new JLabel("Description: ", JLabel.RIGHT));
1005     description.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
1006     description.setLineWrap(true);
1007     panel.add(new JScrollPane(description));
1008
1009     if (!newFeatures)
1010     {
1011       bigPanel.add(panel, BorderLayout.SOUTH);
1012
1013       panel = new JPanel();
1014       panel.add(new JLabel(" Start:", JLabel.RIGHT));
1015       panel.add(start);
1016       panel.add(new JLabel("  End:", JLabel.RIGHT));
1017       panel.add(end);
1018       bigPanel.add(panel, BorderLayout.CENTER);
1019     }
1020     else
1021     {
1022       bigPanel.add(panel, BorderLayout.CENTER);
1023     }
1024
1025     if (lastFeatureAdded == null)
1026     {
1027       if (features[0].type != null)
1028       {
1029         lastFeatureAdded = features[0].type;
1030       }
1031       else
1032       {
1033         lastFeatureAdded = "feature_1";
1034       }
1035     }
1036
1037     if (lastFeatureGroupAdded == null)
1038     {
1039       if (features[0].featureGroup != null)
1040       {
1041         lastFeatureGroupAdded = features[0].featureGroup;
1042       }
1043       else
1044       {
1045         lastFeatureGroupAdded = "Jalview";
1046       }
1047     }
1048
1049     if (newFeatures)
1050     {
1051       name.setText(lastFeatureAdded);
1052       source.setText(lastFeatureGroupAdded);
1053     }
1054     else
1055     {
1056       name.setText(features[0].getType());
1057       source.setText(features[0].getFeatureGroup());
1058     }
1059
1060     start.setValue(new Integer(features[0].getBegin()));
1061     end.setValue(new Integer(features[0].getEnd()));
1062     description.setText(features[0].getDescription());
1063     colour.setBackground(getColour(name.getText()));
1064
1065     Object[] options;
1066     if (!newFeatures)
1067     {
1068       options = new Object[]
1069       { "Amend", "Delete", "Cancel" };
1070     }
1071     else
1072     {
1073       options = new Object[]
1074       { "OK", "Cancel" };
1075     }
1076
1077     String title = newFeatures ? "Create New Sequence Feature(s)"
1078             : "Amend/Delete Features for " + sequences[0].getName();
1079
1080     int reply = JOptionPane.showInternalOptionDialog(Desktop.desktop,
1081             bigPanel, title, JOptionPane.YES_NO_CANCEL_OPTION,
1082             JOptionPane.QUESTION_MESSAGE, null, options, "OK");
1083
1084     jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
1085
1086     if (reply == JOptionPane.OK_OPTION && name.getText().length() > 0)
1087     {
1088       // This ensures that the last sequence
1089       // is refreshed and new features are rendered
1090       lastSeq = null;
1091       lastFeatureAdded = name.getText().trim();
1092       lastFeatureGroupAdded = source.getText().trim();
1093       lastDescriptionAdded = description.getText().replaceAll("\n", " ");
1094
1095       if (lastFeatureGroupAdded.length() < 1)
1096         lastFeatureGroupAdded = null;
1097     }
1098
1099     if (!newFeatures)
1100     {
1101       SequenceFeature sf = features[featureIndex];
1102
1103       if (reply == JOptionPane.NO_OPTION)
1104       {
1105         sequences[0].getDatasetSequence().deleteFeature(sf);
1106       }
1107       else if (reply == JOptionPane.YES_OPTION)
1108       {
1109         sf.type = lastFeatureAdded;
1110         sf.featureGroup = lastFeatureGroupAdded;
1111         sf.description = lastDescriptionAdded;
1112
1113         setColour(sf.type, colour.getBackground());
1114         av.featuresDisplayed.put(sf.type, new Integer(colour
1115                 .getBackground().getRGB()));
1116
1117         try
1118         {
1119           sf.begin = ((Integer) start.getValue()).intValue();
1120           sf.end = ((Integer) end.getValue()).intValue();
1121         } catch (NumberFormatException ex)
1122         {
1123         }
1124
1125         ffile.parseDescriptionHTML(sf, false);
1126       }
1127     }
1128     else
1129     // NEW FEATURES ADDED
1130     {
1131       if (reply == JOptionPane.OK_OPTION && lastFeatureAdded.length() > 0)
1132       {
1133         for (int i = 0; i < sequences.length; i++)
1134         {
1135           features[i].type = lastFeatureAdded;
1136           if (lastFeatureGroupAdded != null)
1137             features[i].featureGroup = lastFeatureGroupAdded;
1138           features[i].description = lastDescriptionAdded;
1139           sequences[i].addSequenceFeature(features[i]);
1140           ffile.parseDescriptionHTML(features[i], false);
1141         }
1142
1143         if (av.featuresDisplayed == null)
1144         {
1145           av.featuresDisplayed = new Hashtable();
1146         }
1147
1148         if (lastFeatureGroupAdded != null)
1149         {
1150           if (featureGroups == null)
1151             featureGroups = new Hashtable();
1152           featureGroups.put(lastFeatureGroupAdded, new Boolean(true));
1153         }
1154
1155         Color col = colour.getBackground();
1156         setColour(lastFeatureAdded, colour.getBackground());
1157         av.featuresDisplayed.put(lastFeatureAdded,
1158                 new Integer(col.getRGB()));
1159
1160         findAllFeatures(false);
1161
1162         ap.paintAlignment(true);
1163
1164         return true;
1165       }
1166       else
1167       {
1168         return false;
1169       }
1170     }
1171
1172     ap.paintAlignment(true);
1173
1174     return true;
1175   }
1176
1177   public void setColour(String featureType, Color col)
1178   {
1179     Object c = featureColours.get(featureType);
1180     if (c == null || c instanceof Color || (c instanceof GraduatedColor && !((GraduatedColor)c).getMaxColor().equals(col)))
1181     {
1182       featureColours.put(featureType, col);
1183     }
1184   }
1185
1186   public void setTransparency(float value)
1187   {
1188     transparency = value;
1189   }
1190
1191   public float getTransparency()
1192   {
1193     return transparency;
1194   }
1195
1196   /**
1197    * Replace current ordering with new ordering
1198    * 
1199    * @param data {
1200    *                String(Type), Colour(Type), Boolean(Displayed) }
1201    */
1202   public void setFeaturePriority(Object[][] data)
1203   {
1204     setFeaturePriority(data, true);
1205   }
1206
1207   /**
1208    * 
1209    * @param data {
1210    *                String(Type), Colour(Type), Boolean(Displayed) }
1211    * @param visibleNew
1212    *                when true current featureDisplay list will be cleared
1213    */
1214   public void setFeaturePriority(Object[][] data, boolean visibleNew)
1215   {
1216     if (visibleNew)
1217     {
1218       if (av.featuresDisplayed != null)
1219       {
1220         av.featuresDisplayed.clear();
1221       }
1222       else
1223       {
1224         av.featuresDisplayed = new Hashtable();
1225       }
1226     }
1227     if (data == null)
1228     {
1229       return;
1230     }
1231
1232     // The feature table will display high priority
1233     // features at the top, but theses are the ones
1234     // we need to render last, so invert the data
1235     renderOrder = new String[data.length];
1236
1237     if (data.length > 0)
1238     {
1239       for (int i = 0; i < data.length; i++)
1240       {
1241         String type = data[i][0].toString();
1242         setColour(type, (Color) data[i][1]);
1243         if (((Boolean) data[i][2]).booleanValue())
1244         {
1245           av.featuresDisplayed.put(type, new Integer(getColour(type)
1246                   .getRGB()));
1247         }
1248
1249         renderOrder[data.length - i - 1] = type;
1250       }
1251     }
1252
1253   }
1254
1255   Hashtable featureOrder = null;
1256
1257   /**
1258    * analogous to colour - store a normalized ordering for all feature types in
1259    * this rendering context.
1260    * 
1261    * @param type
1262    *                Feature type string
1263    * @param position
1264    *                normalized priority - 0 means always appears on top, 1 means
1265    *                always last.
1266    */
1267   public float setOrder(String type, float position)
1268   {
1269     if (featureOrder == null)
1270     {
1271       featureOrder = new Hashtable();
1272     }
1273     featureOrder.put(type, new Float(position));
1274     return position;
1275   }
1276
1277   /**
1278    * get the global priority (0 (top) to 1 (bottom))
1279    * 
1280    * @param type
1281    * @return [0,1] or -1 for a type without a priority
1282    */
1283   public float getOrder(String type)
1284   {
1285     if (featureOrder != null)
1286     {
1287       if (featureOrder.containsKey(type))
1288       {
1289         return ((Float) featureOrder.get(type)).floatValue();
1290       }
1291     }
1292     return -1;
1293   }
1294
1295   /**
1296    * @param listener
1297    * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
1298    */
1299   public void addPropertyChangeListener(PropertyChangeListener listener)
1300   {
1301     changeSupport.addPropertyChangeListener(listener);
1302   }
1303
1304   /**
1305    * @param listener
1306    * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
1307    */
1308   public void removePropertyChangeListener(PropertyChangeListener listener)
1309   {
1310     changeSupport.removePropertyChangeListener(listener);
1311   }
1312 }