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