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