2faba525c44103ebbb4779a08599cdac0f6a811b
[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.getAlignment().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.getAlignment().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.getColumnSelection().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.getColumnSelection().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.getColumnSelection().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     aa[activeRow].validateRangeAndDisplay();
366
367     adjustPanelHeight();
368     repaint();
369
370     return;
371   }
372
373   private String collectAnnotVals(Annotation[] anot,
374           ColumnSelection columnSelection, String label2)
375   {
376     String collatedInput = "";
377     String last = "";
378     ColumnSelection viscols=av.getColumnSelection();
379     // TODO: refactor and save av.getColumnSelection for efficiency
380     for (int i = 0; i < columnSelection.size(); i++)
381     {
382       int index = columnSelection.columnAt(i);
383       // always check for current display state - just in case
384       if (!viscols.isVisible(index))
385         continue;
386       String tlabel = null;
387       if (anot[index] != null)
388       { // LML added stem code
389         if (label2.equals(HELIX) || label2.equals(SHEET)
390                 || label2.equals(STEM) || label2.equals(LABEL))
391         {
392           tlabel = anot[index].description;
393           if (tlabel == null || tlabel.length() < 1)
394           {
395             if (label2.equals(HELIX) || label2.equals(SHEET)
396                     || label2.equals(STEM))
397             {
398               tlabel = "" + anot[index].secondaryStructure;
399             }
400             else
401             {
402               tlabel = "" + anot[index].displayCharacter;
403             }
404           }
405         }
406         if (tlabel != null && !tlabel.equals(last))
407         {
408           if (last.length() > 0)
409           {
410             collatedInput += " ";
411           }
412           collatedInput += tlabel;
413         }
414       }
415     }
416     return collatedInput;
417   }
418
419   /**
420    * DOCUMENT ME!
421    * 
422    * @param evt
423    *          DOCUMENT ME!
424    */
425   public void mousePressed(MouseEvent evt)
426   {
427
428     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
429     if (aa == null)
430     {
431       return;
432     }
433
434     int height = 0;
435     activeRow = -1;
436
437     for (int i = 0; i < aa.length; i++)
438     {
439       if (aa[i].visible)
440       {
441         height += aa[i].height;
442       }
443
444       if (evt.getY() < height)
445       {
446         if (aa[i].editable)
447         {
448           activeRow = i;
449         }
450         else if (aa[i].graph > 0)
451         {
452           // Stretch Graph
453           graphStretch = i;
454           graphStretchY = evt.getY();
455         }
456
457         break;
458       }
459     }
460
461     if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1)
462     {
463       if (av.getColumnSelection() == null)
464       {
465         return;
466       }
467
468       JPopupMenu pop = new JPopupMenu("Structure type");
469       JMenuItem item;
470       /*
471        * Just display the needed structure options
472        */
473       if (av.getAlignment().isNucleotide() == true)
474       {
475         item = new JMenuItem(STEM);
476         item.addActionListener(this);
477         pop.add(item);
478       }
479       else
480       {
481         item = new JMenuItem(HELIX);
482         item.addActionListener(this);
483         pop.add(item);
484         item = new JMenuItem(SHEET);
485         item.addActionListener(this);
486         pop.add(item);
487       }
488       item = new JMenuItem(LABEL);
489       item.addActionListener(this);
490       pop.add(item);
491       item = new JMenuItem(COLOUR);
492       item.addActionListener(this);
493       pop.add(item);
494       item = new JMenuItem(REMOVE);
495       item.addActionListener(this);
496       pop.add(item);
497       pop.show(this, evt.getX(), evt.getY());
498
499       return;
500     }
501
502     if (aa == null)
503     {
504       return;
505     }
506
507     ap.scalePanel.mousePressed(evt);
508
509   }
510
511   /**
512    * DOCUMENT ME!
513    * 
514    * @param evt
515    *          DOCUMENT ME!
516    */
517   public void mouseReleased(MouseEvent evt)
518   {
519     graphStretch = -1;
520     graphStretchY = -1;
521     mouseDragging = false;
522     ap.scalePanel.mouseReleased(evt);
523   }
524
525   /**
526    * DOCUMENT ME!
527    * 
528    * @param evt
529    *          DOCUMENT ME!
530    */
531   public void mouseEntered(MouseEvent evt)
532   {
533     ap.scalePanel.mouseEntered(evt);
534   }
535
536   /**
537    * DOCUMENT ME!
538    * 
539    * @param evt
540    *          DOCUMENT ME!
541    */
542   public void mouseExited(MouseEvent evt)
543   {
544     ap.scalePanel.mouseExited(evt);
545   }
546
547   /**
548    * DOCUMENT ME!
549    * 
550    * @param evt
551    *          DOCUMENT ME!
552    */
553   public void mouseDragged(MouseEvent evt)
554   {
555     if (graphStretch > -1)
556     {
557       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
558               - evt.getY();
559       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
560       {
561         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
562       }
563       graphStretchY = evt.getY();
564       adjustPanelHeight();
565       ap.paintAlignment(true);
566     }
567     else
568     {
569       ap.scalePanel.mouseDragged(evt);
570     }
571   }
572
573   /**
574    * DOCUMENT ME!
575    * 
576    * @param evt
577    *          DOCUMENT ME!
578    */
579   public void mouseMoved(MouseEvent evt)
580   {
581     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
582
583     if (aa == null)
584     {
585       this.setToolTipText(null);
586       return;
587     }
588
589     int row = -1;
590     int height = 0;
591
592     for (int i = 0; i < aa.length; i++)
593     {
594       if (aa[i].visible)
595       {
596         height += aa[i].height;
597       }
598
599       if (evt.getY() < height)
600       {
601         row = i;
602
603         break;
604       }
605     }
606
607     if (row == -1)
608     {
609       this.setToolTipText(null);
610       return;
611     }
612
613     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
614
615     if (av.hasHiddenColumns())
616     {
617       res = av.getColumnSelection().adjustForHiddenColumns(res);
618     }
619
620     if (row > -1 && aa[row].annotations != null
621             && res < (int) aa[row].annotations.length)
622     {
623       if (aa[row].graphGroup > -1)
624       {
625         StringBuffer tip = new StringBuffer("<html>");
626         for (int gg = 0; gg < aa.length; gg++)
627         {
628           if (aa[gg].graphGroup == aa[row].graphGroup
629                   && aa[gg].annotations[res] != null)
630           {
631             tip.append(aa[gg].label + " "
632                     + aa[gg].annotations[res].description + "<br>");
633           }
634         }
635         if (tip.length() != 6)
636         {
637           tip.setLength(tip.length() - 4);
638           this.setToolTipText(tip.toString() + "</html>");
639         }
640       }
641       else if (aa[row].annotations[res] != null
642               && aa[row].annotations[res].description != null
643               && aa[row].annotations[res].description.length() > 0)
644       {
645         this.setToolTipText(aa[row].annotations[res].description);
646       }
647       else
648       {
649         // clear the tooltip.
650         this.setToolTipText(null);
651       }
652
653       if (aa[row].annotations[res] != null)
654       {
655         StringBuffer text = new StringBuffer("Sequence position "
656                 + (res + 1));
657
658         if (aa[row].annotations[res].description != null)
659         {
660           text.append("  " + aa[row].annotations[res].description);
661         }
662
663         ap.alignFrame.statusBar.setText(text.toString());
664       }
665     }
666     else
667     {
668       this.setToolTipText(null);
669     }
670   }
671
672   /**
673    * DOCUMENT ME!
674    * 
675    * @param evt
676    *          DOCUMENT ME!
677    */
678   public void mouseClicked(MouseEvent evt)
679   {
680     if (activeRow != -1)
681     {
682       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
683       AlignmentAnnotation anot = aa[activeRow];
684
685       if (anot.description.equals("secondary structure"))
686       {
687         // System.out.println(anot.description+" "+anot.getRNAStruc());
688       }
689     }
690   }
691
692   // TODO mouseClicked-content and drawCursor are quite experimental!
693   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
694           int y1)
695   {
696     int pady = av.charHeight / 5;
697     int charOffset = 0;
698     graphics.setColor(Color.black);
699     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
700
701     if (av.validCharWidth)
702     {
703       graphics.setColor(Color.white);
704
705       char s = seq.getCharAt(res);
706
707       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
708       graphics.drawString(String.valueOf(s), charOffset + x1,
709               (y1 + av.charHeight) - pady);
710     }
711
712   }
713
714   /**
715    * DOCUMENT ME!
716    * 
717    * @param g
718    *          DOCUMENT ME!
719    */
720   public void paintComponent(Graphics g)
721   {
722     g.setColor(Color.white);
723     g.fillRect(0, 0, getWidth(), getHeight());
724
725     if (image != null)
726     {
727       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
728               || (getVisibleRect().height != g.getClipBounds().height))
729       {
730         g.drawImage(image, 0, 0, this);
731         fastPaint = false;
732         return;
733       }
734     }
735     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
736     if (imgWidth < 1)
737       return;
738     if (image == null || imgWidth != image.getWidth()
739             || image.getHeight(this) != getHeight())
740     {
741       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
742               BufferedImage.TYPE_INT_RGB);
743       gg = (Graphics2D) image.getGraphics();
744
745       if (av.antiAlias)
746       {
747         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
748                 RenderingHints.VALUE_ANTIALIAS_ON);
749       }
750
751       gg.setFont(av.getFont());
752       fm = gg.getFontMetrics();
753       gg.setColor(Color.white);
754       gg.fillRect(0, 0, imgWidth, image.getHeight());
755     }
756
757     drawComponent(gg, av.startRes, av.endRes + 1);
758     g.drawImage(image, 0, 0, this);
759   }
760
761   /**
762    * non-Thread safe repaint
763    * 
764    * @param horizontal
765    *          repaint with horizontal shift in alignment
766    */
767   public void fastPaint(int horizontal)
768   {
769
770     if ((horizontal == 0) || gg == null
771             || av.getAlignment().getAlignmentAnnotation() == null
772             || av.getAlignment().getAlignmentAnnotation().length < 1
773             || av.isCalcInProgress())
774     {
775       repaint();
776       return;
777     }
778     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
779
780     int sr = av.startRes;
781     int er = av.endRes + 1;
782     int transX = 0;
783
784     if (horizontal > 0) // scrollbar pulled right, image to the left
785     {
786       transX = (er - sr - horizontal) * av.charWidth;
787       sr = er - horizontal;
788     }
789     else if (horizontal < 0)
790     {
791       er = sr - horizontal;
792     }
793
794     gg.translate(transX, 0);
795
796     drawComponent(gg, sr, er);
797
798     gg.translate(-transX, 0);
799
800     fastPaint = true;
801     repaint();
802
803   }
804
805   /**
806    * DOCUMENT ME!
807    * 
808    * @param g
809    *          DOCUMENT ME!
810    * @param startRes
811    *          DOCUMENT ME!
812    * @param endRes
813    *          DOCUMENT ME!
814    */
815   public void drawComponent(Graphics g, int startRes, int endRes)
816   {
817     if (av.isCalcInProgress())
818     {
819       if (image == null)
820       {
821         return;
822       }
823       // We'll keep a record of the old image,
824       // and draw a faded image until the calculation
825       // has completed
826       if (fadedImage == null || fadedImage.getWidth() != imgWidth
827               || fadedImage.getHeight() != image.getHeight())
828       {
829         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
830                 BufferedImage.TYPE_INT_RGB);
831
832         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
833
834         fadedG.setColor(Color.white);
835         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
836
837         fadedG.setComposite(AlphaComposite.getInstance(
838                 AlphaComposite.SRC_OVER, .3f));
839         fadedG.drawImage(image, 0, 0, this);
840
841       }
842
843     }
844     else
845     {
846       fadedImage = null;
847     }
848
849     g.setColor(Color.white);
850     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
851
852     g.setFont(av.getFont());
853     if (fm == null)
854     {
855       fm = g.getFontMetrics();
856     }
857
858     if ((av.getAlignment().getAlignmentAnnotation() == null)
859             || (av.getAlignment().getAlignmentAnnotation().length < 1))
860     {
861       g.setColor(Color.white);
862       g.fillRect(0, 0, getWidth(), getHeight());
863       g.setColor(Color.black);
864       if (av.validCharWidth)
865       {
866         g.drawString("Alignment has no annotations", 20, 15);
867       }
868
869       return;
870     }
871     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
872   }
873
874   @Override
875   public FontMetrics getFontMetrics()
876   {
877     return fm;
878   }
879
880   @Override
881   public Image getFadedImage()
882   {
883     return fadedImage;
884   }
885
886   @Override
887   public int getFadedImageWidth()
888   {
889     return imgWidth;
890   }
891 }