quantitative shading of features by GraduatedColor
[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             currentColour = av.featuresDisplayed
353                     .get(sequenceFeatures[sfindex].type);
354           }
355         }
356         else if (sequenceFeatures[sfindex].type.equals("disulfide bond"))
357         {
358
359           renderFeature(
360                   g,
361                   seq,
362                   seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
363                   seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
364                   new Color(((Integer) av.featuresDisplayed
365                           .get(sequenceFeatures[sfindex].type)).intValue()),
366                   start, end, y1);
367           renderFeature(
368                   g,
369                   seq,
370                   seq.findIndex(sequenceFeatures[sfindex].end) - 1,
371                   seq.findIndex(sequenceFeatures[sfindex].end) - 1,
372                   new Color(((Integer) av.featuresDisplayed
373                           .get(sequenceFeatures[sfindex].type)).intValue()),
374                   start, end, y1);
375
376         }
377         else
378           if (showFeature(sequenceFeatures[sfindex]))
379           {
380               if (av.showSeqFeaturesHeight
381                       && sequenceFeatures[sfindex].score != Float.NaN)
382               {
383                 renderScoreFeature(g, seq, seq
384                         .findIndex(sequenceFeatures[sfindex].begin) - 1,
385                         seq.findIndex(sequenceFeatures[sfindex].end) - 1,
386                         getColour(sequenceFeatures[sfindex]), start, end,
387                         y1, normaliseScore(sequenceFeatures[sfindex]));
388               }
389               else
390               {
391                 renderFeature(g, seq, seq
392                         .findIndex(sequenceFeatures[sfindex].begin) - 1,
393                         seq.findIndex(sequenceFeatures[sfindex].end) - 1,
394                         getColour(sequenceFeatures[sfindex]), start, end,
395                         y1);
396               }
397           }
398
399       }
400
401     }
402
403     if (transparency != 1.0f && g != null)
404     {
405       Graphics2D g2 = (Graphics2D) g;
406       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
407               1.0f));
408     }
409   }
410
411   Hashtable minmax = new Hashtable();
412
413   /**
414    * normalise a score against the max/min bounds for the feature type.
415    * 
416    * @param sequenceFeature
417    * @return byte[] { signed, normalised signed (-127 to 127) or unsigned
418    *         (0-255) value.
419    */
420   private final byte[] normaliseScore(SequenceFeature sequenceFeature)
421   {
422     float[] mm = ((float[][]) minmax.get(sequenceFeature.type))[0];
423     final byte[] r = new byte[]
424     { 0, (byte) 255 };
425     if (mm != null)
426     {
427       if (r[0] != 0 || mm[0] < 0.0)
428       {
429         r[0] = 1;
430         r[1] = (byte) ((int) 128.0 + 127.0 * (sequenceFeature.score / mm[1]));
431       }
432       else
433       {
434         r[1] = (byte) ((int) 255.0 * (sequenceFeature.score / mm[1]));
435       }
436     }
437     return r;
438   }
439
440   char s;
441
442   int i;
443
444   void renderFeature(Graphics g, SequenceI seq, int fstart, int fend,
445           Color featureColour, int start, int end, int y1)
446   {
447
448     if (((fstart <= end) && (fend >= start)))
449     {
450       if (fstart < start)
451       { // fix for if the feature we have starts before the sequence start,
452         fstart = start; // but the feature end is still valid!!
453       }
454
455       if (fend >= end)
456       {
457         fend = end;
458       }
459       int pady = (y1 + av.charHeight) - av.charHeight / 5;
460       for (i = fstart; i <= fend; i++)
461       {
462         s = seq.getCharAt(i);
463
464         if (jalview.util.Comparison.isGap(s))
465         {
466           continue;
467         }
468
469         g.setColor(featureColour);
470
471         g.fillRect((i - start) * av.charWidth, y1, av.charWidth,
472                 av.charHeight);
473
474         if (offscreenRender || !av.validCharWidth)
475         {
476           continue;
477         }
478
479         g.setColor(Color.white);
480         charOffset = (av.charWidth - fm.charWidth(s)) / 2;
481         g.drawString(String.valueOf(s), charOffset
482                 + (av.charWidth * (i - start)), pady);
483
484       }
485     }
486   }
487
488   void renderScoreFeature(Graphics g, SequenceI seq, int fstart, int fend,
489           Color featureColour, int start, int end, int y1, byte[] bs)
490   {
491
492     if (((fstart <= end) && (fend >= start)))
493     {
494       if (fstart < start)
495       { // fix for if the feature we have starts before the sequence start,
496         fstart = start; // but the feature end is still valid!!
497       }
498
499       if (fend >= end)
500       {
501         fend = end;
502       }
503       int pady = (y1 + av.charHeight) - av.charHeight / 5;
504       int ystrt = 0, yend = av.charHeight;
505       if (bs[0] != 0)
506       {
507         // signed - zero is always middle of residue line.
508         if (bs[1] < 128)
509         {
510           yend = av.charHeight * (128 - bs[1]) / 512;
511           ystrt = av.charHeight - yend / 2;
512         }
513         else
514         {
515           ystrt = av.charHeight / 2;
516           yend = av.charHeight * (bs[1] - 128) / 512;
517         }
518       }
519       else
520       {
521         yend = av.charHeight * bs[1] / 255;
522         ystrt = av.charHeight - yend;
523
524       }
525       for (i = fstart; i <= fend; i++)
526       {
527         s = seq.getCharAt(i);
528
529         if (jalview.util.Comparison.isGap(s))
530         {
531           continue;
532         }
533
534         g.setColor(featureColour);
535         int x = (i - start) * av.charWidth;
536         g.drawRect(x, y1, av.charWidth, av.charHeight);
537         g.fillRect(x, y1 + ystrt, av.charWidth, yend);
538
539         if (offscreenRender || !av.validCharWidth)
540         {
541           continue;
542         }
543
544         g.setColor(Color.black);
545         charOffset = (av.charWidth - fm.charWidth(s)) / 2;
546         g.drawString(String.valueOf(s), charOffset
547                 + (av.charWidth * (i - start)), pady);
548
549       }
550     }
551   }
552
553   boolean newFeatureAdded = false;
554
555   /**
556    * Called when alignment in associated view has new/modified features to
557    * discover and display.
558    * 
559    */
560   public void featuresAdded()
561   {
562     lastSeq = null;
563     findAllFeatures();
564   }
565
566   boolean findingFeatures = false;
567
568   /**
569    * search the alignment for all new features, give them a colour and display
570    * them. Then fires a PropertyChangeEvent on the changeSupport object.
571    * 
572    */
573   void findAllFeatures()
574   {
575     synchronized (firing)
576     {
577       if (firing.equals(Boolean.FALSE))
578       {
579         firing = Boolean.TRUE;
580         findAllFeatures(true); // add all new features as visible
581         changeSupport.firePropertyChange("changeSupport", null, null);
582         firing = Boolean.FALSE;
583       }
584     }
585   }
586
587   /**
588    * Searches alignment for all features and updates colours
589    * 
590    * @param newMadeVisible
591    *                if true newly added feature types will be rendered
592    *                immediatly
593    */
594   synchronized void findAllFeatures(boolean newMadeVisible)
595   {
596     newFeatureAdded = false;
597
598     if (findingFeatures)
599     {
600       newFeatureAdded = true;
601       return;
602     }
603
604     findingFeatures = true;
605
606     if (av.featuresDisplayed == null)
607     {
608       av.featuresDisplayed = new Hashtable();
609     }
610
611     allfeatures = new Vector();
612     Vector oldfeatures = new Vector();
613     if (renderOrder != null)
614     {
615       for (int i = 0; i < renderOrder.length; i++)
616       {
617         if (renderOrder[i] != null)
618         {
619           oldfeatures.addElement(renderOrder[i]);
620         }
621       }
622     }
623     if (minmax == null)
624     {
625       minmax = new Hashtable();
626     }
627     for (int i = 0; i < av.alignment.getHeight(); i++)
628     {
629       SequenceFeature[] features = av.alignment.getSequenceAt(i)
630               .getDatasetSequence().getSequenceFeatures();
631
632       if (features == null)
633       {
634         continue;
635       }
636
637       int index = 0;
638       while (index < features.length)
639       {
640         if (!av.featuresDisplayed.containsKey(features[index].getType()))
641         {
642
643           if (featureGroups.containsKey(features[index].getType()))
644           {
645             boolean visible = ((Boolean) featureGroups
646                     .get(features[index].featureGroup)).booleanValue();
647
648             if (!visible)
649             {
650               index++;
651               continue;
652             }
653           }
654
655           if (!(features[index].begin == 0 && features[index].end == 0))
656           {
657             // If beginning and end are 0, the feature is for the whole sequence
658             // and we don't want to render the feature in the normal way
659
660             if (newMadeVisible
661                     && !oldfeatures.contains(features[index].getType()))
662             {
663               // this is a new feature type on the alignment. Mark it for
664               // display.
665               av.featuresDisplayed.put(features[index].getType(),
666                       new Integer(getColour(features[index].getType())
667                               .getRGB()));
668               setOrder(features[index].getType(), 0);
669             }
670           }
671         }
672         if (!allfeatures.contains(features[index].getType()))
673         {
674           allfeatures.addElement(features[index].getType());
675         }
676         if (features[index].score != Float.NaN)
677         {
678           int nonpos= features[index].getBegin()>=1 ? 0 : 1;
679           float[][] mm = (float[][]) minmax.get(features[index].getType());
680           if (mm == null)
681           {
682             mm = new float[][] {null, null };
683             minmax.put(features[index].getType(), mm);
684           }
685           if (mm[nonpos]==null)
686           { 
687             mm[nonpos] = new float[] { features[index].score, features[index].score };
688             
689           }
690           else
691           {
692             if (mm[nonpos][0] > features[index].score)
693             {
694               mm[nonpos][0] = features[index].score;
695             }
696             if (mm[nonpos][1] < features[index].score)
697             {
698               mm[nonpos][1] = features[index].score;
699             }
700           }
701         }
702         index++;
703       }
704     }
705     updateRenderOrder(allfeatures);
706     findingFeatures = false;
707   }
708
709   protected Boolean firing = Boolean.FALSE;
710
711   /**
712    * replaces the current renderOrder with the unordered features in
713    * allfeatures. The ordering of any types in both renderOrder and allfeatures
714    * is preserved, and all new feature types are rendered on top of the existing
715    * types, in the order given by getOrder or the order given in allFeatures.
716    * Note. this operates directly on the featureOrder hash for efficiency. TODO:
717    * eliminate the float storage for computing/recalling the persistent ordering
718    * 
719    * @param allFeatures
720    */
721   private void updateRenderOrder(Vector allFeatures)
722   {
723     Vector allfeatures = new Vector(allFeatures);
724     String[] oldRender = renderOrder;
725     renderOrder = new String[allfeatures.size()];
726     boolean initOrders = (featureOrder == null);
727     int opos = 0;
728     if (oldRender != null && oldRender.length > 0)
729     {
730       for (int j = 0; j < oldRender.length; j++)
731       {
732         if (oldRender[j] != null)
733         {
734           if (initOrders)
735           {
736             setOrder(oldRender[j], (1 - (1 + (float) j)
737                     / (float) oldRender.length));
738           }
739           if (allfeatures.contains(oldRender[j]))
740           {
741             renderOrder[opos++] = oldRender[j]; // existing features always
742             // appear below new features
743             allfeatures.removeElement(oldRender[j]);
744           }
745         }
746       }
747     }
748     if (allfeatures.size() == 0)
749     {
750       // no new features - leave order unchanged.
751       return;
752     }
753     int i = allfeatures.size() - 1;
754     int iSize = i;
755     boolean sort = false;
756     String[] newf = new String[allfeatures.size()];
757     float[] sortOrder = new float[allfeatures.size()];
758     Enumeration en = allfeatures.elements();
759     // sort remaining elements
760     while (en.hasMoreElements())
761     {
762       newf[i] = en.nextElement().toString();
763       if (initOrders || !featureOrder.containsKey(newf[i]))
764       {
765         int denom = initOrders ? allfeatures.size() : featureOrder.size();
766         // new unordered feature - compute persistent ordering at head of
767         // existing features.
768         setOrder(newf[i], i / (float) denom);
769       }
770       // set order from newly found feature from persisted ordering.
771       sortOrder[i] = 2 - ((Float) featureOrder.get(newf[i])).floatValue();
772       if (i < iSize)
773       {
774         // only sort if we need to
775         sort = sort || sortOrder[i] > sortOrder[i + 1];
776       }
777       i--;
778     }
779     if (iSize > 1 && sort)
780       jalview.util.QuickSort.sort(sortOrder, newf);
781     sortOrder = null;
782     System.arraycopy(newf, 0, renderOrder, opos, newf.length);
783   }
784
785   public Color getColour(String featureType)
786   {
787     Object fc = featureColours.get(featureType);
788     if (fc == null)
789     {
790       jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
791       Color col = ucs.createColourFromName(featureType);
792       featureColours.put(featureType, col);
793       return col;
794     }
795     else if (fc instanceof Color)
796     {
797       return (Color) fc;
798     }
799     else
800     {
801       if (fc instanceof GraduatedColor)
802       {
803         return ((GraduatedColor) fc).getMinColor();
804       }
805       // TODO: raise an implementation error here.
806       return null; // Color.white;
807     }
808   }
809
810   /**
811    * implement graduated colouring for features with scores
812    * 
813    * @param feature
814    * @return render colour for the given feature
815    */
816   public Color getColour(SequenceFeature feature)
817   {
818     Object fc = featureColours.get(feature.type);
819     if (fc == null)
820     {
821       jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme();
822       Color col = ucs.createColourFromName(feature.type);
823       featureColours.put(feature.type, col);
824       return col;
825     }
826     else if (fc instanceof Color)
827     {
828       return (Color) fc;
829     }
830     else
831     {
832       if (fc instanceof GraduatedColor)
833       {
834         return ((GraduatedColor) fc).findColor(feature);
835       }
836       // TODO: raise an implementation error here.
837       return null; // Color.white;
838     }
839   }
840   private boolean showFeature(SequenceFeature sequenceFeature)
841   {
842     Object fc = featureColours.get(sequenceFeature.type);
843     if (fc instanceof GraduatedColor)
844     {
845       return ((GraduatedColor) fc).isColored(sequenceFeature);
846     } else { return true; }
847   }
848
849
850   static String lastFeatureAdded;
851
852   static String lastFeatureGroupAdded;
853
854   static String lastDescriptionAdded;
855
856   int featureIndex = 0;
857
858   boolean amendFeatures(final SequenceI[] sequences,
859           final SequenceFeature[] features, boolean newFeatures,
860           final AlignmentPanel ap)
861   {
862
863     featureIndex = 0;
864
865     JPanel bigPanel = new JPanel(new BorderLayout());
866     final JComboBox overlaps;
867     final JTextField name = new JTextField(25);
868     final JTextField source = new JTextField(25);
869     final JTextArea description = new JTextArea(3, 25);
870     final JSpinner start = new JSpinner();
871     final JSpinner end = new JSpinner();
872     start.setPreferredSize(new Dimension(80, 20));
873     end.setPreferredSize(new Dimension(80, 20));
874
875     final JPanel colour = new JPanel();
876     colour.setBorder(BorderFactory.createEtchedBorder());
877     colour.setMaximumSize(new Dimension(40, 10));
878     colour.addMouseListener(new MouseAdapter()
879     {
880       public void mousePressed(MouseEvent evt)
881       {
882         // TODO: use featurecolourchooser here
883         Color col = JColorChooser.showDialog(Desktop.desktop,
884                 "Select Feature Colour", colour.getBackground());
885         if (col != null)
886           colour.setBackground(col);
887
888       }
889     });
890
891     JPanel tmp = new JPanel();
892     JPanel panel = new JPanel(new GridLayout(3, 1));
893
894     // /////////////////////////////////////
895     // /MULTIPLE FEATURES AT SELECTED RESIDUE
896     if (!newFeatures && features.length > 1)
897     {
898       panel = new JPanel(new GridLayout(4, 1));
899       tmp = new JPanel();
900       tmp.add(new JLabel("Select Feature: "));
901       overlaps = new JComboBox();
902       for (int i = 0; i < features.length; i++)
903       {
904         overlaps.addItem(features[i].getType() + "/"
905                 + features[i].getBegin() + "-" + features[i].getEnd()
906                 + " (" + features[i].getFeatureGroup() + ")");
907       }
908
909       tmp.add(overlaps);
910
911       overlaps.addItemListener(new ItemListener()
912       {
913         public void itemStateChanged(ItemEvent e)
914         {
915           int index = overlaps.getSelectedIndex();
916           if (index != -1)
917           {
918             featureIndex = index;
919             name.setText(features[index].getType());
920             description.setText(features[index].getDescription());
921             source.setText(features[index].getFeatureGroup());
922             start.setValue(new Integer(features[index].getBegin()));
923             end.setValue(new Integer(features[index].getEnd()));
924
925             SearchResults highlight = new SearchResults();
926             highlight.addResult(sequences[0], features[index].getBegin(),
927                     features[index].getEnd());
928
929             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
930
931           }
932           Color col = getColour(name.getText());
933           if (col == null)
934           {
935             col = new jalview.schemes.UserColourScheme()
936                     .createColourFromName(name.getText());
937           }
938
939           colour.setBackground(col);
940         }
941       });
942
943       panel.add(tmp);
944     }
945     // ////////
946     // ////////////////////////////////////
947
948     tmp = new JPanel();
949     panel.add(tmp);
950     tmp.add(new JLabel("Name: ", JLabel.RIGHT));
951     tmp.add(name);
952
953     tmp = new JPanel();
954     panel.add(tmp);
955     tmp.add(new JLabel("Group: ", JLabel.RIGHT));
956     tmp.add(source);
957
958     tmp = new JPanel();
959     panel.add(tmp);
960     tmp.add(new JLabel("Colour: ", JLabel.RIGHT));
961     tmp.add(colour);
962     colour.setPreferredSize(new Dimension(150, 15));
963
964     bigPanel.add(panel, BorderLayout.NORTH);
965
966     panel = new JPanel();
967     panel.add(new JLabel("Description: ", JLabel.RIGHT));
968     description.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
969     description.setLineWrap(true);
970     panel.add(new JScrollPane(description));
971
972     if (!newFeatures)
973     {
974       bigPanel.add(panel, BorderLayout.SOUTH);
975
976       panel = new JPanel();
977       panel.add(new JLabel(" Start:", JLabel.RIGHT));
978       panel.add(start);
979       panel.add(new JLabel("  End:", JLabel.RIGHT));
980       panel.add(end);
981       bigPanel.add(panel, BorderLayout.CENTER);
982     }
983     else
984     {
985       bigPanel.add(panel, BorderLayout.CENTER);
986     }
987
988     if (lastFeatureAdded == null)
989     {
990       if (features[0].type != null)
991       {
992         lastFeatureAdded = features[0].type;
993       }
994       else
995       {
996         lastFeatureAdded = "feature_1";
997       }
998     }
999
1000     if (lastFeatureGroupAdded == null)
1001     {
1002       if (features[0].featureGroup != null)
1003       {
1004         lastFeatureGroupAdded = features[0].featureGroup;
1005       }
1006       else
1007       {
1008         lastFeatureGroupAdded = "Jalview";
1009       }
1010     }
1011
1012     if (newFeatures)
1013     {
1014       name.setText(lastFeatureAdded);
1015       source.setText(lastFeatureGroupAdded);
1016     }
1017     else
1018     {
1019       name.setText(features[0].getType());
1020       source.setText(features[0].getFeatureGroup());
1021     }
1022
1023     start.setValue(new Integer(features[0].getBegin()));
1024     end.setValue(new Integer(features[0].getEnd()));
1025     description.setText(features[0].getDescription());
1026     colour.setBackground(getColour(name.getText()));
1027
1028     Object[] options;
1029     if (!newFeatures)
1030     {
1031       options = new Object[]
1032       { "Amend", "Delete", "Cancel" };
1033     }
1034     else
1035     {
1036       options = new Object[]
1037       { "OK", "Cancel" };
1038     }
1039
1040     String title = newFeatures ? "Create New Sequence Feature(s)"
1041             : "Amend/Delete Features for " + sequences[0].getName();
1042
1043     int reply = JOptionPane.showInternalOptionDialog(Desktop.desktop,
1044             bigPanel, title, JOptionPane.YES_NO_CANCEL_OPTION,
1045             JOptionPane.QUESTION_MESSAGE, null, options, "OK");
1046
1047     jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
1048
1049     if (reply == JOptionPane.OK_OPTION && name.getText().length() > 0)
1050     {
1051       // This ensures that the last sequence
1052       // is refreshed and new features are rendered
1053       lastSeq = null;
1054       lastFeatureAdded = name.getText().trim();
1055       lastFeatureGroupAdded = source.getText().trim();
1056       lastDescriptionAdded = description.getText().replaceAll("\n", " ");
1057
1058       if (lastFeatureGroupAdded.length() < 1)
1059         lastFeatureGroupAdded = null;
1060     }
1061
1062     if (!newFeatures)
1063     {
1064       SequenceFeature sf = features[featureIndex];
1065
1066       if (reply == JOptionPane.NO_OPTION)
1067       {
1068         sequences[0].getDatasetSequence().deleteFeature(sf);
1069       }
1070       else if (reply == JOptionPane.YES_OPTION)
1071       {
1072         sf.type = lastFeatureAdded;
1073         sf.featureGroup = lastFeatureGroupAdded;
1074         sf.description = lastDescriptionAdded;
1075
1076         setColour(sf.type, colour.getBackground());
1077         av.featuresDisplayed.put(sf.type, new Integer(colour
1078                 .getBackground().getRGB()));
1079
1080         try
1081         {
1082           sf.begin = ((Integer) start.getValue()).intValue();
1083           sf.end = ((Integer) end.getValue()).intValue();
1084         } catch (NumberFormatException ex)
1085         {
1086         }
1087
1088         ffile.parseDescriptionHTML(sf, false);
1089       }
1090     }
1091     else
1092     // NEW FEATURES ADDED
1093     {
1094       if (reply == JOptionPane.OK_OPTION && lastFeatureAdded.length() > 0)
1095       {
1096         for (int i = 0; i < sequences.length; i++)
1097         {
1098           features[i].type = lastFeatureAdded;
1099           if (lastFeatureGroupAdded != null)
1100             features[i].featureGroup = lastFeatureGroupAdded;
1101           features[i].description = lastDescriptionAdded;
1102           sequences[i].addSequenceFeature(features[i]);
1103           ffile.parseDescriptionHTML(features[i], false);
1104         }
1105
1106         if (av.featuresDisplayed == null)
1107         {
1108           av.featuresDisplayed = new Hashtable();
1109         }
1110
1111         if (lastFeatureGroupAdded != null)
1112         {
1113           if (featureGroups == null)
1114             featureGroups = new Hashtable();
1115           featureGroups.put(lastFeatureGroupAdded, new Boolean(true));
1116         }
1117
1118         Color col = colour.getBackground();
1119         setColour(lastFeatureAdded, colour.getBackground());
1120         av.featuresDisplayed.put(lastFeatureAdded,
1121                 new Integer(col.getRGB()));
1122
1123         findAllFeatures(false);
1124
1125         ap.paintAlignment(true);
1126
1127         return true;
1128       }
1129       else
1130       {
1131         return false;
1132       }
1133     }
1134
1135     ap.paintAlignment(true);
1136
1137     return true;
1138   }
1139
1140   public void setColour(String featureType, Color col)
1141   {
1142     Object c = featureColours.get(featureType);
1143     if (c == null || c instanceof Color || (c instanceof GraduatedColor && !((GraduatedColor)c).getMaxColor().equals(col)))
1144     {
1145       featureColours.put(featureType, col);
1146     }
1147   }
1148
1149   public void setTransparency(float value)
1150   {
1151     transparency = value;
1152   }
1153
1154   public float getTransparency()
1155   {
1156     return transparency;
1157   }
1158
1159   /**
1160    * Replace current ordering with new ordering
1161    * 
1162    * @param data {
1163    *                String(Type), Colour(Type), Boolean(Displayed) }
1164    */
1165   public void setFeaturePriority(Object[][] data)
1166   {
1167     setFeaturePriority(data, true);
1168   }
1169
1170   /**
1171    * 
1172    * @param data {
1173    *                String(Type), Colour(Type), Boolean(Displayed) }
1174    * @param visibleNew
1175    *                when true current featureDisplay list will be cleared
1176    */
1177   public void setFeaturePriority(Object[][] data, boolean visibleNew)
1178   {
1179     if (visibleNew)
1180     {
1181       if (av.featuresDisplayed != null)
1182       {
1183         av.featuresDisplayed.clear();
1184       }
1185       else
1186       {
1187         av.featuresDisplayed = new Hashtable();
1188       }
1189     }
1190     if (data == null)
1191     {
1192       return;
1193     }
1194
1195     // The feature table will display high priority
1196     // features at the top, but theses are the ones
1197     // we need to render last, so invert the data
1198     renderOrder = new String[data.length];
1199
1200     if (data.length > 0)
1201     {
1202       for (int i = 0; i < data.length; i++)
1203       {
1204         String type = data[i][0].toString();
1205         setColour(type, (Color) data[i][1]);
1206         if (((Boolean) data[i][2]).booleanValue())
1207         {
1208           av.featuresDisplayed.put(type, new Integer(getColour(type)
1209                   .getRGB()));
1210         }
1211
1212         renderOrder[data.length - i - 1] = type;
1213       }
1214     }
1215
1216   }
1217
1218   Hashtable featureOrder = null;
1219
1220   /**
1221    * analogous to colour - store a normalized ordering for all feature types in
1222    * this rendering context.
1223    * 
1224    * @param type
1225    *                Feature type string
1226    * @param position
1227    *                normalized priority - 0 means always appears on top, 1 means
1228    *                always last.
1229    */
1230   public float setOrder(String type, float position)
1231   {
1232     if (featureOrder == null)
1233     {
1234       featureOrder = new Hashtable();
1235     }
1236     featureOrder.put(type, new Float(position));
1237     return position;
1238   }
1239
1240   /**
1241    * get the global priority (0 (top) to 1 (bottom))
1242    * 
1243    * @param type
1244    * @return [0,1] or -1 for a type without a priority
1245    */
1246   public float getOrder(String type)
1247   {
1248     if (featureOrder != null)
1249     {
1250       if (featureOrder.containsKey(type))
1251       {
1252         return ((Float) featureOrder.get(type)).floatValue();
1253       }
1254     }
1255     return -1;
1256   }
1257
1258   /**
1259    * @param listener
1260    * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
1261    */
1262   public void addPropertyChangeListener(PropertyChangeListener listener)
1263   {
1264     changeSupport.addPropertyChangeListener(listener);
1265   }
1266
1267   /**
1268    * @param listener
1269    * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
1270    */
1271   public void removePropertyChangeListener(PropertyChangeListener listener)
1272   {
1273     changeSupport.removePropertyChangeListener(listener);
1274   }
1275 }