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