(JAL-958) - normalised sequence logo for applet
[jalview.git] / src / jalview / gui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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  */
18 package jalview.gui;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import java.awt.image.*;
23 import java.util.Hashtable;
24
25 import javax.swing.*;
26
27 import jalview.analysis.AAFrequency;
28 import jalview.analysis.StructureFrequency;
29 import jalview.datamodel.*;
30 import jalview.renderer.AnnotationRenderer;
31 import jalview.renderer.AwtRenderPanelI;
32
33 /**
34  * DOCUMENT ME!
35  * 
36  * @author $author$
37  * @version $Revision$
38  */
39 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
40         MouseListener, MouseMotionListener, ActionListener,
41         AdjustmentListener
42 {
43   final String HELIX = "Helix";
44
45   final String SHEET = "Sheet";
46
47   /**
48    * For RNA secondary structure "stems" aka helices
49    */
50   final String STEM = "RNA Helix";
51
52   final String LABEL = "Label";
53
54   final String REMOVE = "Remove Annotation";
55
56   final String COLOUR = "Colour";
57
58   public final Color HELIX_COLOUR = Color.red.darker();
59
60   public final Color SHEET_COLOUR = Color.green.darker().darker();
61
62   public final Color STEM_COLOUR = Color.blue.darker();
63
64   /** DOCUMENT ME!! */
65   public AlignViewport av;
66
67   AlignmentPanel ap;
68
69   public int activeRow = -1;
70
71   public BufferedImage image;
72
73   public BufferedImage fadedImage;
74
75   Graphics2D gg;
76
77   public FontMetrics fm;
78
79   public int imgWidth = 0;
80
81   boolean fastPaint = false;
82
83   // Used For mouse Dragging and resizing graphs
84   int graphStretch = -1;
85
86   int graphStretchY = -1;
87
88   int min; // used by mouseDragged to see if user
89
90   int max; // used by mouseDragged to see if user
91
92   boolean mouseDragging = false;
93
94   boolean MAC = false;
95
96   // for editing cursor
97   int cursorX = 0;
98
99   int cursorY = 0;
100
101   public final AnnotationRenderer renderer;
102
103   /**
104    * Creates a new AnnotationPanel object.
105    * 
106    * @param ap
107    *          DOCUMENT ME!
108    */
109   public AnnotationPanel(AlignmentPanel ap)
110   {
111
112     MAC = new jalview.util.Platform().isAMac();
113
114     ToolTipManager.sharedInstance().registerComponent(this);
115     ToolTipManager.sharedInstance().setInitialDelay(0);
116     ToolTipManager.sharedInstance().setDismissDelay(10000);
117     this.ap = ap;
118     av = ap.av;
119     this.setLayout(null);
120     addMouseListener(this);
121     addMouseMotionListener(this);
122     ap.annotationScroller.getVerticalScrollBar()
123             .addAdjustmentListener(this);
124     renderer = new AnnotationRenderer();
125   }
126
127   public AnnotationPanel(AlignViewport av)
128   {
129     this.av = av;
130     renderer = new AnnotationRenderer();
131   }
132
133   /**
134    * DOCUMENT ME!
135    * 
136    * @param evt
137    *          DOCUMENT ME!
138    */
139   public void adjustmentValueChanged(AdjustmentEvent evt)
140   {
141     ap.alabels.setScrollOffset(-evt.getValue());
142   }
143
144   /**
145    * Calculates the height of the annotation displayed in the annotation panel.
146    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
147    * all annotation associated components are updated correctly.
148    * 
149    */
150   public int adjustPanelHeight()
151   {
152     int height = calcPanelHeight();
153     this.setPreferredSize(new Dimension(1, height));
154     if (ap != null)
155     {
156       // revalidate only when the alignment panel is fully constructed
157       ap.validate();
158     }
159
160     return height;
161   }
162
163   /**
164    * calculate the height for visible annotation, revalidating bounds where
165    * necessary ABSTRACT GUI METHOD
166    * 
167    * @return total height of annotation
168    */
169   public int calcPanelHeight()
170   {
171     // setHeight of panels
172     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
173     int height = 0;
174
175     if (aa != null)
176     {
177       for (int i = 0; i < aa.length; i++)
178       {
179         if (aa[i] == null)
180         {
181           System.err.println("Null annotation row: ignoring.");
182           continue;
183         }
184         if (!aa[i].visible)
185         {
186           continue;
187         }
188
189         aa[i].height = 0;
190
191         if (aa[i].hasText)
192         {
193           aa[i].height += av.charHeight;
194         }
195
196         if (aa[i].hasIcons)
197         {
198           aa[i].height += 16;
199         }
200
201         if (aa[i].graph > 0)
202         {
203           aa[i].height += aa[i].graphHeight;
204         }
205
206         if (aa[i].height == 0)
207         {
208           aa[i].height = 20;
209         }
210
211         height += aa[i].height;
212       }
213     }
214     if (height == 0)
215     {
216       // set minimum
217       height = 20;
218     }
219     return height;
220   }
221
222   /**
223    * DOCUMENT ME!
224    * 
225    * @param evt
226    *          DOCUMENT ME!
227    */
228   public void actionPerformed(ActionEvent evt)
229   {
230     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
231     if (aa == null)
232     {
233       return;
234     }
235     Annotation[] anot = aa[activeRow].annotations;
236
237     if (anot.length < av.getColumnSelection().getMax())
238     {
239       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
240       System.arraycopy(anot, 0, temp, 0, anot.length);
241       anot = temp;
242       aa[activeRow].annotations = anot;
243     }
244
245     if (evt.getActionCommand().equals(REMOVE))
246     {
247       for (int i = 0; i < av.getColumnSelection().size(); i++)
248       {
249         anot[av.getColumnSelection().columnAt(i)] = null;
250       }
251     }
252     else if (evt.getActionCommand().equals(LABEL))
253     {
254       String exMesg = collectAnnotVals(anot, av.getColumnSelection(), LABEL);
255       String label = JOptionPane.showInputDialog(this, "Enter label",
256               exMesg);
257
258       if (label == null)
259       {
260         return;
261       }
262
263       if ((label.length() > 0) && !aa[activeRow].hasText)
264       {
265         aa[activeRow].hasText = true;
266       }
267
268       for (int i = 0; i < av.getColumnSelection().size(); i++)
269       {
270         int index = av.getColumnSelection().columnAt(i);
271
272         if (!av.colSel.isVisible(index))
273           continue;
274
275         if (anot[index] == null)
276         {
277           anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that
278           // null exceptions
279           // aren't raised
280           // elsewhere.
281         }
282         else
283         {
284           anot[index].displayCharacter = label;
285         }
286       }
287     }
288     else if (evt.getActionCommand().equals(COLOUR))
289     {
290       Color col = JColorChooser.showDialog(this,
291               "Choose foreground colour", Color.black);
292
293       for (int i = 0; i < av.getColumnSelection().size(); i++)
294       {
295         int index = av.getColumnSelection().columnAt(i);
296
297         if (!av.colSel.isVisible(index))
298           continue;
299
300         if (anot[index] == null)
301         {
302           anot[index] = new Annotation("", "", ' ', 0);
303         }
304
305         anot[index].colour = col;
306       }
307     }
308     else
309     // HELIX OR SHEET
310     {
311       char type = 0;
312       String symbol = "\u03B1";
313
314       if (evt.getActionCommand().equals(HELIX))
315       {
316         type = 'H';
317       }
318       else if (evt.getActionCommand().equals(SHEET))
319       {
320         type = 'E';
321         symbol = "\u03B2";
322       }
323
324       // Added by LML to color stems
325       else if (evt.getActionCommand().equals(STEM))
326       {
327         type = 'S';
328         symbol = "\u03C3";
329       }
330
331       if (!aa[activeRow].hasIcons)
332       {
333         aa[activeRow].hasIcons = true;
334       }
335
336       String label = JOptionPane.showInputDialog(
337               "Enter a label for the structure?", symbol);
338
339       if (label == null)
340       {
341         return;
342       }
343
344       if ((label.length() > 0) && !aa[activeRow].hasText)
345       {
346         aa[activeRow].hasText = true;
347       }
348
349       for (int i = 0; i < av.getColumnSelection().size(); i++)
350       {
351         int index = av.getColumnSelection().columnAt(i);
352
353         if (!av.colSel.isVisible(index))
354           continue;
355
356         if (anot[index] == null)
357         {
358           anot[index] = new Annotation(label, "", type, 0);
359         }
360
361         anot[index].secondaryStructure = type;
362         anot[index].displayCharacter = label;
363       }
364     }
365
366     adjustPanelHeight();
367     repaint();
368
369     return;
370   }
371
372   private String collectAnnotVals(Annotation[] anot,
373           ColumnSelection columnSelection, String label2)
374   {
375     String collatedInput = "";
376     String last = "";
377     for (int i = 0; i < columnSelection.size(); i++)
378     {
379       int index = columnSelection.columnAt(i);
380       // always check for current display state - just in case
381       if (!av.colSel.isVisible(index))
382         continue;
383       String tlabel = null;
384       if (anot[index] != null)
385       { // LML added stem code
386         if (label2.equals(HELIX) || label2.equals(SHEET)
387                 || label2.equals(STEM) || label2.equals(LABEL))
388         {
389           tlabel = anot[index].description;
390           if (tlabel == null || tlabel.length() < 1)
391           {
392             if (label2.equals(HELIX) || label2.equals(SHEET)
393                     || label2.equals(STEM))
394             {
395               tlabel = "" + anot[index].secondaryStructure;
396             }
397             else
398             {
399               tlabel = "" + anot[index].displayCharacter;
400             }
401           }
402         }
403         if (tlabel != null && !tlabel.equals(last))
404         {
405           if (last.length() > 0)
406           {
407             collatedInput += " ";
408           }
409           collatedInput += tlabel;
410         }
411       }
412     }
413     return collatedInput;
414   }
415
416   /**
417    * DOCUMENT ME!
418    * 
419    * @param evt
420    *          DOCUMENT ME!
421    */
422   public void mousePressed(MouseEvent evt)
423   {
424
425     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
426     if (aa == null)
427     {
428       return;
429     }
430
431     int height = 0;
432     activeRow = -1;
433
434     for (int i = 0; i < aa.length; i++)
435     {
436       if (aa[i].visible)
437       {
438         height += aa[i].height;
439       }
440
441       if (evt.getY() < height)
442       {
443         if (aa[i].editable)
444         {
445           activeRow = i;
446         }
447         else if (aa[i].graph > 0)
448         {
449           // Stretch Graph
450           graphStretch = i;
451           graphStretchY = evt.getY();
452         }
453
454         break;
455       }
456     }
457
458     if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1)
459     {
460       if (av.getColumnSelection() == null)
461       {
462         return;
463       }
464
465       JPopupMenu pop = new JPopupMenu("Structure type");
466       JMenuItem item;
467       /*
468        * Just display the needed structure options
469        */
470       if (av.alignment.isNucleotide() == true)
471       {
472         item = new JMenuItem(STEM);
473         item.addActionListener(this);
474         pop.add(item);
475       }
476       else
477       {
478         item = new JMenuItem(HELIX);
479         item.addActionListener(this);
480         pop.add(item);
481         item = new JMenuItem(SHEET);
482         item.addActionListener(this);
483         pop.add(item);
484       }
485       item = new JMenuItem(LABEL);
486       item.addActionListener(this);
487       pop.add(item);
488       item = new JMenuItem(COLOUR);
489       item.addActionListener(this);
490       pop.add(item);
491       item = new JMenuItem(REMOVE);
492       item.addActionListener(this);
493       pop.add(item);
494       pop.show(this, evt.getX(), evt.getY());
495
496       return;
497     }
498
499     if (aa == null)
500     {
501       return;
502     }
503
504     ap.scalePanel.mousePressed(evt);
505
506   }
507
508   /**
509    * DOCUMENT ME!
510    * 
511    * @param evt
512    *          DOCUMENT ME!
513    */
514   public void mouseReleased(MouseEvent evt)
515   {
516     graphStretch = -1;
517     graphStretchY = -1;
518     mouseDragging = false;
519     ap.scalePanel.mouseReleased(evt);
520   }
521
522   /**
523    * DOCUMENT ME!
524    * 
525    * @param evt
526    *          DOCUMENT ME!
527    */
528   public void mouseEntered(MouseEvent evt)
529   {
530     ap.scalePanel.mouseEntered(evt);
531   }
532
533   /**
534    * DOCUMENT ME!
535    * 
536    * @param evt
537    *          DOCUMENT ME!
538    */
539   public void mouseExited(MouseEvent evt)
540   {
541     ap.scalePanel.mouseExited(evt);
542   }
543
544   /**
545    * DOCUMENT ME!
546    * 
547    * @param evt
548    *          DOCUMENT ME!
549    */
550   public void mouseDragged(MouseEvent evt)
551   {
552     if (graphStretch > -1)
553     {
554       av.alignment.getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
555               - evt.getY();
556       if (av.alignment.getAlignmentAnnotation()[graphStretch].graphHeight < 0)
557       {
558         av.alignment.getAlignmentAnnotation()[graphStretch].graphHeight = 0;
559       }
560       graphStretchY = evt.getY();
561       adjustPanelHeight();
562       ap.paintAlignment(true);
563     }
564     else
565     {
566       ap.scalePanel.mouseDragged(evt);
567     }
568   }
569
570   /**
571    * DOCUMENT ME!
572    * 
573    * @param evt
574    *          DOCUMENT ME!
575    */
576   public void mouseMoved(MouseEvent evt)
577   {
578     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
579
580     if (aa == null)
581     {
582       this.setToolTipText(null);
583       return;
584     }
585
586     int row = -1;
587     int height = 0;
588
589     for (int i = 0; i < aa.length; i++)
590     {
591       if (aa[i].visible)
592       {
593         height += aa[i].height;
594       }
595
596       if (evt.getY() < height)
597       {
598         row = i;
599
600         break;
601       }
602     }
603
604     if (row == -1)
605     {
606       this.setToolTipText(null);
607       return;
608     }
609
610     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
611
612     if (av.hasHiddenColumns)
613     {
614       res = av.getColumnSelection().adjustForHiddenColumns(res);
615     }
616
617     if (row > -1 && aa[row].annotations != null
618             && res < (int) aa[row].annotations.length)
619     {
620       if (aa[row].graphGroup > -1)
621       {
622         StringBuffer tip = new StringBuffer("<html>");
623         for (int gg = 0; gg < aa.length; gg++)
624         {
625           if (aa[gg].graphGroup == aa[row].graphGroup
626                   && aa[gg].annotations[res] != null)
627           {
628             tip.append(aa[gg].label + " "
629                     + aa[gg].annotations[res].description + "<br>");
630           }
631         }
632         if (tip.length() != 6)
633         {
634           tip.setLength(tip.length() - 4);
635           this.setToolTipText(tip.toString() + "</html>");
636         }
637       }
638       else if (aa[row].annotations[res] != null
639               && aa[row].annotations[res].description != null
640               && aa[row].annotations[res].description.length() > 0)
641       {
642         this.setToolTipText(aa[row].annotations[res].description);
643       }
644       else
645       {
646         // clear the tooltip.
647         this.setToolTipText(null);
648       }
649
650       if (aa[row].annotations[res] != null)
651       {
652         StringBuffer text = new StringBuffer("Sequence position "
653                 + (res + 1));
654
655         if (aa[row].annotations[res].description != null)
656         {
657           text.append("  " + aa[row].annotations[res].description);
658         }
659
660         ap.alignFrame.statusBar.setText(text.toString());
661       }
662     }
663     else
664     {
665       this.setToolTipText(null);
666     }
667   }
668
669   /**
670    * DOCUMENT ME!
671    * 
672    * @param evt
673    *          DOCUMENT ME!
674    */
675   public void mouseClicked(MouseEvent evt)
676   {
677     if (activeRow != -1)
678     {
679       AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
680       AlignmentAnnotation anot = aa[activeRow];
681
682       if (anot.description.equals("secondary structure"))
683       {
684         // System.out.println(anot.description+" "+anot.getRNAStruc());
685       }
686     }
687   }
688
689   // TODO mouseClicked-content and drawCursor are quite experimental!
690   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
691           int y1)
692   {
693     int pady = av.charHeight / 5;
694     int charOffset = 0;
695     graphics.setColor(Color.black);
696     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
697
698     if (av.validCharWidth)
699     {
700       graphics.setColor(Color.white);
701
702       char s = seq.getCharAt(res);
703
704       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
705       graphics.drawString(String.valueOf(s), charOffset + x1,
706               (y1 + av.charHeight) - pady);
707     }
708
709   }
710
711   /**
712    * DOCUMENT ME!
713    * 
714    * @param g
715    *          DOCUMENT ME!
716    */
717   public void paintComponent(Graphics g)
718   {
719     g.setColor(Color.white);
720     g.fillRect(0, 0, getWidth(), getHeight());
721
722     if (image != null)
723     {
724       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
725               || (getVisibleRect().height != g.getClipBounds().height))
726       {
727         g.drawImage(image, 0, 0, this);
728         fastPaint = false;
729         return;
730       }
731     }
732     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
733     if (imgWidth < 1)
734       return;
735     if (image == null || imgWidth != image.getWidth()
736             || image.getHeight(this) != getHeight())
737     {
738       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
739               BufferedImage.TYPE_INT_RGB);
740       gg = (Graphics2D) image.getGraphics();
741
742       if (av.antiAlias)
743       {
744         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
745                 RenderingHints.VALUE_ANTIALIAS_ON);
746       }
747
748       gg.setFont(av.getFont());
749       fm = gg.getFontMetrics();
750       gg.setColor(Color.white);
751       gg.fillRect(0, 0, imgWidth, image.getHeight());
752     }
753
754     drawComponent(gg, av.startRes, av.endRes + 1);
755     g.drawImage(image, 0, 0, this);
756   }
757
758   /**
759    * non-Thread safe repaint
760    * 
761    * @param horizontal
762    *          repaint with horizontal shift in alignment
763    */
764   public void fastPaint(int horizontal)
765   {
766
767     if ((horizontal == 0) || gg == null
768             || av.alignment.getAlignmentAnnotation() == null
769             || av.alignment.getAlignmentAnnotation().length < 1
770             || av.updatingConsensus || av.updatingConservation)
771     {
772       repaint();
773       return;
774     }
775     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
776
777     int sr = av.startRes;
778     int er = av.endRes + 1;
779     int transX = 0;
780
781     if (horizontal > 0) // scrollbar pulled right, image to the left
782     {
783       transX = (er - sr - horizontal) * av.charWidth;
784       sr = er - horizontal;
785     }
786     else if (horizontal < 0)
787     {
788       er = sr - horizontal;
789     }
790
791     gg.translate(transX, 0);
792
793     drawComponent(gg, sr, er);
794
795     gg.translate(-transX, 0);
796
797     fastPaint = true;
798     repaint();
799
800   }
801
802   /**
803    * DOCUMENT ME!
804    * 
805    * @param g
806    *          DOCUMENT ME!
807    * @param startRes
808    *          DOCUMENT ME!
809    * @param endRes
810    *          DOCUMENT ME!
811    */
812   public void drawComponent(Graphics g, int startRes, int endRes)
813   {
814     if (av.updatingConsensus || av.updatingConservation)
815     {
816       if (image == null)
817       {
818         return;
819       }
820       // We'll keep a record of the old image,
821       // and draw a faded image until the calculation
822       // has completed
823       if (fadedImage == null || fadedImage.getWidth() != imgWidth
824               || fadedImage.getHeight() != image.getHeight())
825       {
826         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
827                 BufferedImage.TYPE_INT_RGB);
828
829         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
830
831         fadedG.setColor(Color.white);
832         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
833
834         fadedG.setComposite(AlphaComposite.getInstance(
835                 AlphaComposite.SRC_OVER, .3f));
836         fadedG.drawImage(image, 0, 0, this);
837
838       }
839
840     }
841     else
842     {
843       fadedImage = null;
844     }
845
846     g.setColor(Color.white);
847     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
848
849     g.setFont(av.getFont());
850     if (fm == null)
851     {
852       fm = g.getFontMetrics();
853     }
854
855     if ((av.alignment.getAlignmentAnnotation() == null)
856             || (av.alignment.getAlignmentAnnotation().length < 1))
857     {
858       g.setColor(Color.white);
859       g.fillRect(0, 0, getWidth(), getHeight());
860       g.setColor(Color.black);
861       if (av.validCharWidth)
862       {
863         g.drawString("Alignment has no annotations", 20, 15);
864       }
865
866       return;
867     }
868     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
869   }
870
871   @Override
872   public FontMetrics getFontMetrics()
873   {
874     return fm;
875   }
876
877   @Override
878   public Image getFadedImage()
879   {
880     return fadedImage;
881   }
882
883   @Override
884   public int getFadedImageWidth()
885   {
886     return imgWidth;
887   }
888 }