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