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