Merge branch 'hotfix/JAL-1524' into Release_2_8_2_Branch
[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 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("Implementation Error: Unrecognised render object "
944             + fc.getClass() + " for features of type " + featureType);
945   }
946
947   /**
948    * calculate the render colour for a specific feature using current feature
949    * settings.
950    * 
951    * @param feature
952    * @return render colour for the given feature
953    */
954   public Color getColour(SequenceFeature feature)
955   {
956     Object fc = getFeatureStyle(feature.getType());
957     if (fc instanceof Color)
958     {
959       return (Color) fc;
960     }
961     else
962     {
963       if (fc instanceof GraduatedColor)
964       {
965         return ((GraduatedColor) fc).findColor(feature);
966       }
967     }
968     throw new Error("Implementation Error: Unrecognised render object "
969             + fc.getClass() + " for features of type " + feature.getType());
970   }
971
972   private boolean showFeature(SequenceFeature sequenceFeature)
973   {
974     Object fc = getFeatureStyle(sequenceFeature.type);
975     if (fc instanceof GraduatedColor)
976     {
977       return ((GraduatedColor) fc).isColored(sequenceFeature);
978     }
979     else
980     {
981       return true;
982     }
983   }
984
985   // // /////////////
986   // // Feature Editing Dialog
987   // // Will be refactored in next release.
988
989   static String lastFeatureAdded;
990
991   static String lastFeatureGroupAdded;
992
993   static String lastDescriptionAdded;
994
995   Object oldcol, fcol;
996
997   int featureIndex = 0;
998
999   boolean amendFeatures(final SequenceI[] sequences,
1000           final SequenceFeature[] features, boolean newFeatures,
1001           final AlignmentPanel ap)
1002   {
1003
1004     featureIndex = 0;
1005
1006     final JPanel bigPanel = new JPanel(new BorderLayout());
1007     final JComboBox overlaps;
1008     final JTextField name = new JTextField(25);
1009     final JTextField source = new JTextField(25);
1010     final JTextArea description = new JTextArea(3, 25);
1011     final JSpinner start = new JSpinner();
1012     final JSpinner end = new JSpinner();
1013     start.setPreferredSize(new Dimension(80, 20));
1014     end.setPreferredSize(new Dimension(80, 20));
1015     final FeatureRenderer me = this;
1016     final JLabel colour = new JLabel();
1017     colour.setOpaque(true);
1018     // colour.setBorder(BorderFactory.createEtchedBorder());
1019     colour.setMaximumSize(new Dimension(30, 16));
1020     colour.addMouseListener(new MouseAdapter()
1021     {
1022       FeatureColourChooser fcc = null;
1023
1024       public void mousePressed(MouseEvent evt)
1025       {
1026         if (fcol instanceof Color)
1027         {
1028           Color col = JColorChooser.showDialog(Desktop.desktop,
1029                   "Select Feature Colour", ((Color) fcol));
1030           if (col != null)
1031           {
1032             fcol = col;
1033             updateColourButton(bigPanel, colour, col);
1034           }
1035         }
1036         else
1037         {
1038
1039           if (fcc == null)
1040           {
1041             final String type = features[featureIndex].getType();
1042             fcc = new FeatureColourChooser(me, type);
1043             fcc.setRequestFocusEnabled(true);
1044             fcc.requestFocus();
1045
1046             fcc.addActionListener(new ActionListener()
1047             {
1048
1049               public void actionPerformed(ActionEvent e)
1050               {
1051                 fcol = fcc.getLastColour();
1052                 fcc = null;
1053                 setColour(type, fcol);
1054                 updateColourButton(bigPanel, colour, fcol);
1055               }
1056             });
1057
1058           }
1059         }
1060       }
1061     });
1062     JPanel tmp = new JPanel();
1063     JPanel panel = new JPanel(new GridLayout(3, 1));
1064
1065     // /////////////////////////////////////
1066     // /MULTIPLE FEATURES AT SELECTED RESIDUE
1067     if (!newFeatures && features.length > 1)
1068     {
1069       panel = new JPanel(new GridLayout(4, 1));
1070       tmp = new JPanel();
1071       tmp.add(new JLabel(MessageManager.getString("label.select_feature")));
1072       overlaps = new JComboBox();
1073       for (int i = 0; i < features.length; i++)
1074       {
1075         overlaps.addItem(features[i].getType() + "/"
1076                 + features[i].getBegin() + "-" + features[i].getEnd()
1077                 + " (" + features[i].getFeatureGroup() + ")");
1078       }
1079
1080       tmp.add(overlaps);
1081
1082       overlaps.addItemListener(new ItemListener()
1083       {
1084         public void itemStateChanged(ItemEvent e)
1085         {
1086           int index = overlaps.getSelectedIndex();
1087           if (index != -1)
1088           {
1089             featureIndex = index;
1090             name.setText(features[index].getType());
1091             description.setText(features[index].getDescription());
1092             source.setText(features[index].getFeatureGroup());
1093             start.setValue(new Integer(features[index].getBegin()));
1094             end.setValue(new Integer(features[index].getEnd()));
1095
1096             SearchResults highlight = new SearchResults();
1097             highlight.addResult(sequences[0], features[index].getBegin(),
1098                     features[index].getEnd());
1099
1100             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
1101
1102           }
1103           Object col = getFeatureStyle(name.getText());
1104           if (col == null)
1105           {
1106             col = new jalview.schemes.UserColourScheme()
1107                     .createColourFromName(name.getText());
1108           }
1109           oldcol = fcol = col;
1110           updateColourButton(bigPanel, colour, col);
1111         }
1112       });
1113
1114       panel.add(tmp);
1115     }
1116     // ////////
1117     // ////////////////////////////////////
1118
1119     tmp = new JPanel();
1120     panel.add(tmp);
1121     tmp.add(new JLabel(MessageManager.getString("label.name"), JLabel.RIGHT));
1122     tmp.add(name);
1123
1124     tmp = new JPanel();
1125     panel.add(tmp);
1126     tmp.add(new JLabel(MessageManager.getString("label.group") + ":",
1127             JLabel.RIGHT));
1128     tmp.add(source);
1129
1130     tmp = new JPanel();
1131     panel.add(tmp);
1132     tmp.add(new JLabel(MessageManager.getString("label.colour"),
1133             JLabel.RIGHT));
1134     tmp.add(colour);
1135     colour.setPreferredSize(new Dimension(150, 15));
1136     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
1137     colour.setForeground(Color.black);
1138     colour.setHorizontalAlignment(SwingConstants.CENTER);
1139     colour.setVerticalAlignment(SwingConstants.CENTER);
1140     colour.setHorizontalTextPosition(SwingConstants.CENTER);
1141     colour.setVerticalTextPosition(SwingConstants.CENTER);
1142     bigPanel.add(panel, BorderLayout.NORTH);
1143
1144     panel = new JPanel();
1145     panel.add(new JLabel(MessageManager.getString("label.description"),
1146             JLabel.RIGHT));
1147     description.setFont(JvSwingUtils.getTextAreaFont());
1148     description.setLineWrap(true);
1149     panel.add(new JScrollPane(description));
1150
1151     if (!newFeatures)
1152     {
1153       bigPanel.add(panel, BorderLayout.SOUTH);
1154
1155       panel = new JPanel();
1156       panel.add(new JLabel(MessageManager.getString("label.start"),
1157               JLabel.RIGHT));
1158       panel.add(start);
1159       panel.add(new JLabel(MessageManager.getString("label.end"),
1160               JLabel.RIGHT));
1161       panel.add(end);
1162       bigPanel.add(panel, BorderLayout.CENTER);
1163     }
1164     else
1165     {
1166       bigPanel.add(panel, BorderLayout.CENTER);
1167     }
1168
1169     if (lastFeatureAdded == null)
1170     {
1171       if (features[0].type != null)
1172       {
1173         lastFeatureAdded = features[0].type;
1174       }
1175       else
1176       {
1177         lastFeatureAdded = "feature_1";
1178       }
1179     }
1180
1181     if (lastFeatureGroupAdded == null)
1182     {
1183       if (features[0].featureGroup != null)
1184       {
1185         lastFeatureGroupAdded = features[0].featureGroup;
1186       }
1187       else
1188       {
1189         lastFeatureGroupAdded = "Jalview";
1190       }
1191     }
1192
1193     if (newFeatures)
1194     {
1195       name.setText(lastFeatureAdded);
1196       source.setText(lastFeatureGroupAdded);
1197     }
1198     else
1199     {
1200       name.setText(features[0].getType());
1201       source.setText(features[0].getFeatureGroup());
1202     }
1203
1204     start.setValue(new Integer(features[0].getBegin()));
1205     end.setValue(new Integer(features[0].getEnd()));
1206     description.setText(features[0].getDescription());
1207     updateColourButton(bigPanel, colour,
1208             (oldcol = fcol = getFeatureStyle(name.getText())));
1209     Object[] options;
1210     if (!newFeatures)
1211     {
1212       options = new Object[]
1213       { "Amend", "Delete", "Cancel" };
1214     }
1215     else
1216     {
1217       options = new Object[]
1218       { "OK", "Cancel" };
1219     }
1220
1221     String title = newFeatures ? "Create New Sequence Feature(s)"
1222             : "Amend/Delete Features for " + sequences[0].getName();
1223
1224     int reply = JOptionPane.showInternalOptionDialog(Desktop.desktop,
1225             bigPanel, title, JOptionPane.YES_NO_CANCEL_OPTION,
1226             JOptionPane.QUESTION_MESSAGE, null, options, "OK");
1227
1228     jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
1229
1230     if (reply == JOptionPane.OK_OPTION && name.getText().length() > 0)
1231     {
1232       // This ensures that the last sequence
1233       // is refreshed and new features are rendered
1234       lastSeq = null;
1235       lastFeatureAdded = name.getText().trim();
1236       lastFeatureGroupAdded = source.getText().trim();
1237       lastDescriptionAdded = description.getText().replaceAll("\n", " ");
1238       // TODO: determine if the null feature group is valid
1239       if (lastFeatureGroupAdded.length() < 1)
1240         lastFeatureGroupAdded = null;
1241     }
1242
1243     if (!newFeatures)
1244     {
1245       SequenceFeature sf = features[featureIndex];
1246
1247       if (reply == JOptionPane.NO_OPTION)
1248       {
1249         sequences[0].getDatasetSequence().deleteFeature(sf);
1250       }
1251       else if (reply == JOptionPane.YES_OPTION)
1252       {
1253         sf.type = lastFeatureAdded;
1254         sf.featureGroup = lastFeatureGroupAdded;
1255         sf.description = lastDescriptionAdded;
1256
1257         setColour(sf.type, fcol);
1258         av.featuresDisplayed.put(sf.type, getColour(sf.type));
1259
1260         try
1261         {
1262           sf.begin = ((Integer) start.getValue()).intValue();
1263           sf.end = ((Integer) end.getValue()).intValue();
1264         } catch (NumberFormatException ex)
1265         {
1266         }
1267
1268         ffile.parseDescriptionHTML(sf, false);
1269       }
1270     }
1271     else
1272     // NEW FEATURES ADDED
1273     {
1274       if (reply == JOptionPane.OK_OPTION && lastFeatureAdded.length() > 0)
1275       {
1276         for (int i = 0; i < sequences.length; i++)
1277         {
1278           features[i].type = lastFeatureAdded;
1279           if (lastFeatureGroupAdded != null)
1280             features[i].featureGroup = lastFeatureGroupAdded;
1281           features[i].description = lastDescriptionAdded;
1282           sequences[i].addSequenceFeature(features[i]);
1283           ffile.parseDescriptionHTML(features[i], false);
1284         }
1285
1286         if (av.featuresDisplayed == null)
1287         {
1288           av.featuresDisplayed = new Hashtable();
1289         }
1290
1291         if (lastFeatureGroupAdded != null)
1292         {
1293           if (featureGroups == null)
1294             featureGroups = new Hashtable();
1295           featureGroups.put(lastFeatureGroupAdded, new Boolean(true));
1296         }
1297         setColour(lastFeatureAdded, fcol);
1298         av.featuresDisplayed.put(lastFeatureAdded,
1299                 getColour(lastFeatureAdded));
1300
1301         findAllFeatures(false);
1302
1303         ap.paintAlignment(true);
1304
1305         return true;
1306       }
1307       else
1308       {
1309         return false;
1310       }
1311     }
1312
1313     ap.paintAlignment(true);
1314
1315     return true;
1316   }
1317
1318   /**
1319    * update the amend feature button dependent on the given style
1320    * 
1321    * @param bigPanel
1322    * @param col
1323    * @param col2
1324    */
1325   protected void updateColourButton(JPanel bigPanel, JLabel colour,
1326           Object col2)
1327   {
1328     colour.removeAll();
1329     colour.setIcon(null);
1330     colour.setToolTipText(null);
1331     colour.setText("");
1332
1333     if (col2 instanceof Color)
1334     {
1335       colour.setBackground((Color) col2);
1336     }
1337     else
1338     {
1339       colour.setBackground(bigPanel.getBackground());
1340       colour.setForeground(Color.black);
1341       FeatureSettings.renderGraduatedColor(colour, (GraduatedColor) col2);
1342       // colour.setForeground(colour.getBackground());
1343     }
1344   }
1345
1346   public void setColour(String featureType, Object col)
1347   {
1348     // overwrite
1349     // Color _col = (col instanceof Color) ? ((Color) col) : (col instanceof
1350     // GraduatedColor) ? ((GraduatedColor) col).getMaxColor() : null;
1351     // Object c = featureColours.get(featureType);
1352     // if (c == null || c instanceof Color || (c instanceof GraduatedColor &&
1353     // !((GraduatedColor)c).getMaxColor().equals(_col)))
1354     {
1355       featureColours.put(featureType, col);
1356     }
1357   }
1358
1359   public void setTransparency(float value)
1360   {
1361     transparency = value;
1362   }
1363
1364   public float getTransparency()
1365   {
1366     return transparency;
1367   }
1368
1369   /**
1370    * Replace current ordering with new ordering
1371    * 
1372    * @param data
1373    *          { String(Type), Colour(Type), Boolean(Displayed) }
1374    */
1375   public void setFeaturePriority(Object[][] data)
1376   {
1377     setFeaturePriority(data, true);
1378   }
1379
1380   /**
1381    * 
1382    * @param data
1383    *          { String(Type), Colour(Type), Boolean(Displayed) }
1384    * @param visibleNew
1385    *          when true current featureDisplay list will be cleared
1386    */
1387   public void setFeaturePriority(Object[][] data, boolean visibleNew)
1388   {
1389     if (visibleNew)
1390     {
1391       if (av.featuresDisplayed != null)
1392       {
1393         av.featuresDisplayed.clear();
1394       }
1395       else
1396       {
1397         av.featuresDisplayed = new Hashtable();
1398       }
1399     }
1400     if (data == null)
1401     {
1402       return;
1403     }
1404
1405     // The feature table will display high priority
1406     // features at the top, but theses are the ones
1407     // we need to render last, so invert the data
1408     renderOrder = new String[data.length];
1409
1410     if (data.length > 0)
1411     {
1412       for (int i = 0; i < data.length; i++)
1413       {
1414         String type = data[i][0].toString();
1415         setColour(type, data[i][1]); // todo : typesafety - feature color
1416         // interface object
1417         if (((Boolean) data[i][2]).booleanValue())
1418         {
1419           av.featuresDisplayed.put(type, new Integer(getColour(type)
1420                   .getRGB()));
1421         }
1422
1423         renderOrder[data.length - i - 1] = type;
1424       }
1425     }
1426
1427   }
1428
1429   Map featureOrder = null;
1430
1431   /**
1432    * analogous to colour - store a normalized ordering for all feature types in
1433    * this rendering context.
1434    * 
1435    * @param type
1436    *          Feature type string
1437    * @param position
1438    *          normalized priority - 0 means always appears on top, 1 means
1439    *          always last.
1440    */
1441   public float setOrder(String type, float position)
1442   {
1443     if (featureOrder == null)
1444     {
1445       featureOrder = new Hashtable();
1446     }
1447     featureOrder.put(type, new Float(position));
1448     return position;
1449   }
1450
1451   /**
1452    * get the global priority (0 (top) to 1 (bottom))
1453    * 
1454    * @param type
1455    * @return [0,1] or -1 for a type without a priority
1456    */
1457   public float getOrder(String type)
1458   {
1459     if (featureOrder != null)
1460     {
1461       if (featureOrder.containsKey(type))
1462       {
1463         return ((Float) featureOrder.get(type)).floatValue();
1464       }
1465     }
1466     return -1;
1467   }
1468
1469   /**
1470    * @param listener
1471    * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
1472    */
1473   public void addPropertyChangeListener(PropertyChangeListener listener)
1474   {
1475     changeSupport.addPropertyChangeListener(listener);
1476   }
1477
1478   /**
1479    * @param listener
1480    * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
1481    */
1482   public void removePropertyChangeListener(PropertyChangeListener listener)
1483   {
1484     changeSupport.removePropertyChangeListener(listener);
1485   }
1486 }