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