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