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