JAL-2349 JAL-4033 refactored contact matrix geom calc to allow reporting of mapped...
[jalview.git] / src / jalview / gui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import java.awt.AlphaComposite;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.FontMetrics;
27 import java.awt.Graphics;
28 import java.awt.Graphics2D;
29 import java.awt.Image;
30 import java.awt.Rectangle;
31 import java.awt.RenderingHints;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.AdjustmentEvent;
35 import java.awt.event.AdjustmentListener;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.MouseListener;
38 import java.awt.event.MouseMotionListener;
39 import java.awt.event.MouseWheelEvent;
40 import java.awt.event.MouseWheelListener;
41 import java.awt.image.BufferedImage;
42 import java.beans.PropertyChangeEvent;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46
47 import javax.swing.JMenuItem;
48 import javax.swing.JPanel;
49 import javax.swing.JPopupMenu;
50 import javax.swing.Scrollable;
51 import javax.swing.ToolTipManager;
52
53 import jalview.api.AlignViewportI;
54 import jalview.datamodel.AlignmentAnnotation;
55 import jalview.datamodel.AlignmentI;
56 import jalview.datamodel.Annotation;
57 import jalview.datamodel.ColumnSelection;
58 import jalview.datamodel.ContactListI;
59 import jalview.datamodel.HiddenColumns;
60 import jalview.datamodel.SequenceI;
61 import jalview.gui.JalviewColourChooser.ColourChooserListener;
62 import jalview.renderer.AnnotationRenderer;
63 import jalview.renderer.AwtRenderPanelI;
64 import jalview.renderer.ContactGeometry;
65 import jalview.schemes.ResidueProperties;
66 import jalview.util.Comparison;
67 import jalview.util.MessageManager;
68 import jalview.util.Platform;
69 import jalview.viewmodel.ViewportListenerI;
70 import jalview.viewmodel.ViewportRanges;
71
72 /**
73  * AnnotationPanel displays visible portion of annotation rows below unwrapped
74  * alignment
75  * 
76  * @author $author$
77  * @version $Revision$
78  */
79 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
80         MouseListener, MouseWheelListener, MouseMotionListener,
81         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
82 {
83   enum DragMode
84   {
85     Select, Resize, Undefined
86   };
87
88   String HELIX = MessageManager.getString("label.helix");
89
90   String SHEET = MessageManager.getString("label.sheet");
91
92   /**
93    * For RNA secondary structure "stems" aka helices
94    */
95   String STEM = MessageManager.getString("label.rna_helix");
96
97   String LABEL = MessageManager.getString("label.label");
98
99   String REMOVE = MessageManager.getString("label.remove_annotation");
100
101   String COLOUR = MessageManager.getString("action.colour");
102
103   public final Color HELIX_COLOUR = Color.red.darker();
104
105   public final Color SHEET_COLOUR = Color.green.darker().darker();
106
107   public final Color STEM_COLOUR = Color.blue.darker();
108
109   /** DOCUMENT ME!! */
110   public AlignViewport av;
111
112   AlignmentPanel ap;
113
114   public int activeRow = -1;
115
116   public BufferedImage image;
117
118   public volatile BufferedImage fadedImage;
119
120   // private Graphics2D gg;
121
122   public FontMetrics fm;
123
124   public int imgWidth = 0;
125
126   boolean fastPaint = false;
127
128   // Used For mouse Dragging and resizing graphs
129   int graphStretch = -1;
130
131   int mouseDragLastX = -1;
132
133   int mouseDragLastY = -1;
134
135   DragMode dragMode = DragMode.Undefined;
136
137   boolean mouseDragging = false;
138
139   // for editing cursor
140   int cursorX = 0;
141
142   int cursorY = 0;
143
144   public final AnnotationRenderer renderer;
145
146   private MouseWheelListener[] _mwl;
147
148   /**
149    * Creates a new AnnotationPanel object.
150    * 
151    * @param ap
152    *          DOCUMENT ME!
153    */
154   public AnnotationPanel(AlignmentPanel ap)
155   {
156     ToolTipManager.sharedInstance().registerComponent(this);
157     ToolTipManager.sharedInstance().setInitialDelay(0);
158     ToolTipManager.sharedInstance().setDismissDelay(10000);
159     this.ap = ap;
160     av = ap.av;
161     this.setLayout(null);
162     addMouseListener(this);
163     addMouseMotionListener(this);
164     ap.annotationScroller.getVerticalScrollBar()
165             .addAdjustmentListener(this);
166     // save any wheel listeners on the scroller, so we can propagate scroll
167     // events to them.
168     _mwl = ap.annotationScroller.getMouseWheelListeners();
169     // and then set our own listener to consume all mousewheel events
170     ap.annotationScroller.addMouseWheelListener(this);
171     renderer = new AnnotationRenderer();
172
173     av.getRanges().addPropertyChangeListener(this);
174   }
175
176   public AnnotationPanel(AlignViewport av)
177   {
178     this.av = av;
179     renderer = new AnnotationRenderer();
180   }
181
182   @Override
183   public void mouseWheelMoved(MouseWheelEvent e)
184   {
185     if (e.isShiftDown())
186     {
187       e.consume();
188       double wheelRotation = e.getPreciseWheelRotation();
189       if (wheelRotation > 0)
190       {
191         av.getRanges().scrollRight(true);
192       }
193       else if (wheelRotation < 0)
194       {
195         av.getRanges().scrollRight(false);
196       }
197     }
198     else
199     {
200       // TODO: find the correct way to let the event bubble up to
201       // ap.annotationScroller
202       for (MouseWheelListener mwl : _mwl)
203       {
204         if (mwl != null)
205         {
206           mwl.mouseWheelMoved(e);
207         }
208         if (e.isConsumed())
209         {
210           break;
211         }
212       }
213     }
214   }
215
216   @Override
217   public Dimension getPreferredScrollableViewportSize()
218   {
219     Dimension ps = getPreferredSize();
220     return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
221   }
222
223   @Override
224   public int getScrollableBlockIncrement(Rectangle visibleRect,
225           int orientation, int direction)
226   {
227     return 30;
228   }
229
230   @Override
231   public boolean getScrollableTracksViewportHeight()
232   {
233     return false;
234   }
235
236   @Override
237   public boolean getScrollableTracksViewportWidth()
238   {
239     return true;
240   }
241
242   @Override
243   public int getScrollableUnitIncrement(Rectangle visibleRect,
244           int orientation, int direction)
245   {
246     return 30;
247   }
248
249   /*
250    * (non-Javadoc)
251    * 
252    * @see
253    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
254    * .AdjustmentEvent)
255    */
256   @Override
257   public void adjustmentValueChanged(AdjustmentEvent evt)
258   {
259     // update annotation label display
260     ap.getAlabels().setScrollOffset(-evt.getValue());
261   }
262
263   /**
264    * Calculates the height of the annotation displayed in the annotation panel.
265    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
266    * all annotation associated components are updated correctly.
267    * 
268    */
269   public int adjustPanelHeight()
270   {
271     int height = av.calcPanelHeight();
272     this.setPreferredSize(new Dimension(1, height));
273     if (ap != null)
274     {
275       // revalidate only when the alignment panel is fully constructed
276       ap.validate();
277     }
278
279     return height;
280   }
281
282   /**
283    * DOCUMENT ME!
284    * 
285    * @param evt
286    *          DOCUMENT ME!
287    */
288   @Override
289   public void actionPerformed(ActionEvent evt)
290   {
291     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
292     if (aa == null)
293     {
294       return;
295     }
296     Annotation[] anot = aa[activeRow].annotations;
297
298     if (anot.length < av.getColumnSelection().getMax())
299     {
300       Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
301               + 2];
302       System.arraycopy(anot, 0, temp, 0, anot.length);
303       anot = temp;
304       aa[activeRow].annotations = anot;
305     }
306
307     String action = evt.getActionCommand();
308     if (action.equals(REMOVE))
309     {
310       for (int index : av.getColumnSelection().getSelected())
311       {
312         if (av.getAlignment().getHiddenColumns().isVisible(index))
313         {
314           anot[index] = null;
315         }
316       }
317     }
318     else if (action.equals(LABEL))
319     {
320       String exMesg = collectAnnotVals(anot, LABEL);
321       String label = JvOptionPane.showInputDialog(
322               MessageManager.getString("label.enter_label"), exMesg);
323
324       if (label == null)
325       {
326         return;
327       }
328
329       if ((label.length() > 0) && !aa[activeRow].hasText)
330       {
331         aa[activeRow].hasText = true;
332       }
333
334       for (int index : av.getColumnSelection().getSelected())
335       {
336         if (!av.getAlignment().getHiddenColumns().isVisible(index))
337         {
338           continue;
339         }
340
341         if (anot[index] == null)
342         {
343           anot[index] = new Annotation(label, "", ' ', 0);
344         }
345         else
346         {
347           anot[index].displayCharacter = label;
348         }
349       }
350     }
351     else if (action.equals(COLOUR))
352     {
353       final Annotation[] fAnot = anot;
354       String title = MessageManager
355               .getString("label.select_foreground_colour");
356       ColourChooserListener listener = new ColourChooserListener()
357       {
358         @Override
359         public void colourSelected(Color c)
360         {
361           HiddenColumns hiddenColumns = av.getAlignment()
362                   .getHiddenColumns();
363           for (int index : av.getColumnSelection().getSelected())
364           {
365             if (hiddenColumns.isVisible(index))
366             {
367               if (fAnot[index] == null)
368               {
369                 fAnot[index] = new Annotation("", "", ' ', 0);
370               }
371               fAnot[index].colour = c;
372             }
373           }
374         };
375       };
376       JalviewColourChooser.showColourChooser(this, title, Color.black,
377               listener);
378     }
379     else
380     // HELIX, SHEET or STEM
381     {
382       char type = 0;
383       String symbol = "\u03B1"; // alpha
384
385       if (action.equals(HELIX))
386       {
387         type = 'H';
388       }
389       else if (action.equals(SHEET))
390       {
391         type = 'E';
392         symbol = "\u03B2"; // beta
393       }
394
395       // Added by LML to color stems
396       else if (action.equals(STEM))
397       {
398         type = 'S';
399         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
400         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
401       }
402
403       if (!aa[activeRow].hasIcons)
404       {
405         aa[activeRow].hasIcons = true;
406       }
407
408       String label = JvOptionPane.showInputDialog(MessageManager
409               .getString("label.enter_label_for_the_structure"), symbol);
410
411       if (label == null)
412       {
413         return;
414       }
415
416       if ((label.length() > 0) && !aa[activeRow].hasText)
417       {
418         aa[activeRow].hasText = true;
419         if (action.equals(STEM))
420         {
421           aa[activeRow].showAllColLabels = true;
422         }
423       }
424       for (int index : av.getColumnSelection().getSelected())
425       {
426         if (!av.getAlignment().getHiddenColumns().isVisible(index))
427         {
428           continue;
429         }
430
431         if (anot[index] == null)
432         {
433           anot[index] = new Annotation(label, "", type, 0);
434         }
435
436         anot[index].secondaryStructure = type != 'S' ? type
437                 : label.length() == 0 ? ' ' : label.charAt(0);
438         anot[index].displayCharacter = label;
439
440       }
441     }
442
443     av.getAlignment().validateAnnotation(aa[activeRow]);
444     ap.alignmentChanged();
445     ap.alignFrame.setMenusForViewport();
446     adjustPanelHeight();
447     repaint();
448
449     return;
450   }
451
452   /**
453    * Returns any existing annotation concatenated as a string. For each
454    * annotation, takes the description, if any, else the secondary structure
455    * character (if type is HELIX, SHEET or STEM), else the display character (if
456    * type is LABEL).
457    * 
458    * @param anots
459    * @param type
460    * @return
461    */
462   private String collectAnnotVals(Annotation[] anots, String type)
463   {
464     // TODO is this method wanted? why? 'last' is not used
465
466     StringBuilder collatedInput = new StringBuilder(64);
467     String last = "";
468     ColumnSelection viscols = av.getColumnSelection();
469     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
470
471     /*
472      * the selection list (read-only view) is in selection order, not
473      * column order; make a copy so we can sort it
474      */
475     List<Integer> selected = new ArrayList<>(viscols.getSelected());
476     Collections.sort(selected);
477     for (int index : selected)
478     {
479       // always check for current display state - just in case
480       if (!hidden.isVisible(index))
481       {
482         continue;
483       }
484       String tlabel = null;
485       if (anots[index] != null)
486       { // LML added stem code
487         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
488                 || type.equals(LABEL))
489         {
490           tlabel = anots[index].description;
491           if (tlabel == null || tlabel.length() < 1)
492           {
493             if (type.equals(HELIX) || type.equals(SHEET)
494                     || type.equals(STEM))
495             {
496               tlabel = "" + anots[index].secondaryStructure;
497             }
498             else
499             {
500               tlabel = "" + anots[index].displayCharacter;
501             }
502           }
503         }
504         if (tlabel != null && !tlabel.equals(last))
505         {
506           if (last.length() > 0)
507           {
508             collatedInput.append(" ");
509           }
510           collatedInput.append(tlabel);
511         }
512       }
513     }
514     return collatedInput.toString();
515   }
516
517   /**
518    * Action on right mouse pressed on Mac is to show a pop-up menu for the
519    * annotation. Action on left mouse pressed is to find which annotation is
520    * pressed and mark the start of a column selection or graph resize operation.
521    * 
522    * @param evt
523    */
524   @Override
525   public void mousePressed(MouseEvent evt)
526   {
527
528     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
529     if (aa == null)
530     {
531       return;
532     }
533     mouseDragLastX = evt.getX();
534     mouseDragLastY = evt.getY();
535
536     /*
537      * add visible annotation heights until we reach the y
538      * position, to find which annotation it is in
539      */
540     int height = 0;
541     activeRow = -1;
542
543     final int y = evt.getY();
544     for (int i = 0; i < aa.length; i++)
545     {
546       if (aa[i].visible)
547       {
548         height += aa[i].height;
549       }
550
551       if (y < height)
552       {
553         if (aa[i].editable)
554         {
555           activeRow = i;
556         }
557         else if (aa[i].graph != 0)
558         {
559           /*
560            * we have clicked on a resizable graph annotation
561            */
562           graphStretch = i;
563         }
564         break;
565       }
566     }
567
568     /*
569      * isPopupTrigger fires in mousePressed on Mac,
570      * not until mouseRelease on Windows
571      */
572     if (evt.isPopupTrigger() && activeRow != -1)
573     {
574       showPopupMenu(y, evt.getX());
575       return;
576     }
577
578     ap.getScalePanel().mousePressed(evt);
579   }
580
581   /**
582    * Construct and display a context menu at the right-click position
583    * 
584    * @param y
585    * @param x
586    */
587   void showPopupMenu(final int y, int x)
588   {
589     if (av.getColumnSelection() == null
590             || av.getColumnSelection().isEmpty())
591     {
592       return;
593     }
594
595     JPopupMenu pop = new JPopupMenu(
596             MessageManager.getString("label.structure_type"));
597     JMenuItem item;
598     /*
599      * Just display the needed structure options
600      */
601     if (av.getAlignment().isNucleotide())
602     {
603       item = new JMenuItem(STEM);
604       item.addActionListener(this);
605       pop.add(item);
606     }
607     else
608     {
609       item = new JMenuItem(HELIX);
610       item.addActionListener(this);
611       pop.add(item);
612       item = new JMenuItem(SHEET);
613       item.addActionListener(this);
614       pop.add(item);
615     }
616     item = new JMenuItem(LABEL);
617     item.addActionListener(this);
618     pop.add(item);
619     item = new JMenuItem(COLOUR);
620     item.addActionListener(this);
621     pop.add(item);
622     item = new JMenuItem(REMOVE);
623     item.addActionListener(this);
624     pop.add(item);
625     pop.show(this, x, y);
626   }
627
628   /**
629    * Action on mouse up is to clear mouse drag data and call mouseReleased on
630    * ScalePanel, to deal with defining the selection group (if any) defined by
631    * the mouse drag
632    * 
633    * @param evt
634    */
635   @Override
636   public void mouseReleased(MouseEvent evt)
637   {
638     graphStretch = -1;
639     mouseDragLastX = -1;
640     mouseDragLastY = -1;
641     mouseDragging = false;
642     if (dragMode == DragMode.Resize)
643     {
644       ap.adjustAnnotationHeight();
645     }
646     dragMode = DragMode.Undefined;
647     ap.getScalePanel().mouseReleased(evt);
648
649     /*
650      * isPopupTrigger is set in mouseReleased on Windows
651      * (in mousePressed on Mac)
652      */
653     if (evt.isPopupTrigger() && activeRow != -1)
654     {
655       showPopupMenu(evt.getY(), evt.getX());
656     }
657
658   }
659
660   /**
661    * DOCUMENT ME!
662    * 
663    * @param evt
664    *          DOCUMENT ME!
665    */
666   @Override
667   public void mouseEntered(MouseEvent evt)
668   {
669     this.mouseDragging = false;
670     ap.getScalePanel().mouseEntered(evt);
671   }
672
673   /**
674    * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
675    * with column selection on a mouse drag
676    * 
677    * @param evt
678    */
679   @Override
680   public void mouseExited(MouseEvent evt)
681   {
682     ap.getScalePanel().mouseExited(evt);
683   }
684
685   /**
686    * Action on starting or continuing a mouse drag. There are two possible
687    * actions:
688    * <ul>
689    * <li>drag up or down on a graphed annotation increases or decreases the
690    * height of the graph</li>
691    * <li>dragging left or right selects the columns dragged across</li>
692    * </ul>
693    * A drag on a graph annotation is treated as column selection if it starts
694    * with more horizontal than vertical movement, and as resize if it starts
695    * with more vertical than horizontal movement. Once started, the drag does
696    * not change mode.
697    * 
698    * @param evt
699    */
700   @Override
701   public void mouseDragged(MouseEvent evt)
702   {
703     /*
704      * if dragMode is Undefined:
705      * - set to Select if dx > dy
706      * - set to Resize if dy > dx
707      * - do nothing if dx == dy
708      */
709     final int x = evt.getX();
710     final int y = evt.getY();
711     if (dragMode == DragMode.Undefined)
712     {
713       int dx = Math.abs(x - mouseDragLastX);
714       int dy = Math.abs(y - mouseDragLastY);
715       if (graphStretch == -1 || dx > dy)
716       {
717         /*
718          * mostly horizontal drag, or not a graph annotation
719          */
720         dragMode = DragMode.Select;
721       }
722       else if (dy > dx)
723       {
724         /*
725          * mostly vertical drag
726          */
727         dragMode = DragMode.Resize;
728       }
729     }
730
731     if (dragMode == DragMode.Undefined)
732     {
733       /*
734        * drag is diagonal - defer deciding whether to
735        * treat as up/down or left/right
736        */
737       return;
738     }
739
740     try
741     {
742       if (dragMode == DragMode.Resize)
743       {
744         /*
745          * resize graph annotation if mouse was dragged up or down
746          */
747         int deltaY = mouseDragLastY - evt.getY();
748         if (deltaY != 0)
749         {
750           AlignmentAnnotation graphAnnotation = av.getAlignment()
751                   .getAlignmentAnnotation()[graphStretch];
752           int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
753           graphAnnotation.graphHeight = newHeight;
754           adjustPanelHeight();
755           ap.paintAlignment(false, false);
756         }
757       }
758       else
759       {
760         /*
761          * for mouse drag left or right, delegate to 
762          * ScalePanel to adjust the column selection
763          */
764         ap.getScalePanel().mouseDragged(evt);
765       }
766     } finally
767     {
768       mouseDragLastX = x;
769       mouseDragLastY = y;
770     }
771   }
772
773   /**
774    * Constructs the tooltip, and constructs and displays a status message, for
775    * the current mouse position
776    * 
777    * @param evt
778    */
779   @Override
780   public void mouseMoved(MouseEvent evt)
781   {
782     int yPos = evt.getY();
783     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
784     int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
785     int row = rowAndOffset[0];
786
787     if (row == -1)
788     {
789       this.setToolTipText(null);
790       return;
791     }
792
793     int column = (evt.getX() / av.getCharWidth())
794             + av.getRanges().getStartRes();
795     column = Math.min(column, av.getRanges().getEndRes());
796
797     if (av.hasHiddenColumns())
798     {
799       column = av.getAlignment().getHiddenColumns()
800               .visibleToAbsoluteColumn(column);
801     }
802
803     AlignmentAnnotation ann = aa[row];
804     if (row > -1 && ann.annotations != null
805             && column < ann.annotations.length)
806     {
807       String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av);
808       setToolTipText(toolTip == null ? null
809               : JvSwingUtils.wrapTooltip(true, toolTip));
810       String msg = getStatusMessage(av.getAlignment(), column, ann,
811               rowAndOffset[1], av);
812       ap.alignFrame.setStatus(msg);
813     }
814     else
815     {
816       this.setToolTipText(null);
817       ap.alignFrame.setStatus(" ");
818     }
819   }
820
821   /**
822    * Answers the index in the annotations array of the visible annotation at the
823    * given y position. This is done by adding the heights of visible annotations
824    * until the y position has been exceeded. Answers -1 if no annotations are
825    * visible, or the y position is below all annotations.
826    * 
827    * @param yPos
828    * @param aa
829    * @return
830    */
831   static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
832   {
833     if (aa == null)
834     {
835       return -1;
836     }
837     return getRowIndexAndOffset(yPos, aa)[0];
838   }
839
840   static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
841   {
842     int[] res = new int[2];
843     if (aa == null)
844     {
845       res[0] = -1;
846       res[1] = 0;
847       return res;
848     }
849     int row = -1;
850     int height = 0, lheight = 0;
851
852     for (int i = 0; i < aa.length; i++)
853     {
854       if (aa[i].visible)
855       {
856         lheight = height;
857         height += aa[i].height;
858       }
859
860       if (height > yPos)
861       {
862         row = i;
863         res[0] = row;
864         res[1] = yPos - lheight;
865         break;
866       }
867     }
868     return res;
869   }
870
871   /**
872    * Answers a tooltip for the annotation at the current mouse position, not
873    * wrapped in &lt;html&gt; tags (apply if wanted). Answers null if there is no
874    * tooltip to show.
875    * 
876    * @param ann
877    * @param column
878    * @param anns
879    * @param rowAndOffset
880    */
881   static String buildToolTip(AlignmentAnnotation ann, int column,
882           AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av)
883   {
884     String tooltip = null;
885     if (ann.graphGroup > -1)
886     {
887       StringBuilder tip = new StringBuilder(32);
888       boolean first = true;
889       for (int i = 0; i < anns.length; i++)
890       {
891         if (anns[i].graphGroup == ann.graphGroup
892                 && anns[i].annotations[column] != null)
893         {
894           if (!first)
895           {
896             tip.append("<br>");
897           }
898           first = false;
899           tip.append(anns[i].label);
900           String description = anns[i].annotations[column].description;
901           if (description != null && description.length() > 0)
902           {
903             tip.append(" ").append(description);
904           }
905         }
906       }
907       tooltip = first ? null : tip.toString();
908     }
909     else if (column < ann.annotations.length
910             && ann.annotations[column] != null)
911     {
912       tooltip = ann.annotations[column].description;
913     }
914     // TODO abstract tooltip generator so different implementations can be built
915     if (ann.graph == AlignmentAnnotation.CUSTOMRENDERER)
916     {
917       ContactListI clist = av.getContactList(ann, column);
918       if (clist != null)
919       {
920         ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
921         ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset,
922                 rowAndOffset);
923         tooltip += "Contact from " + ci.cStart + " to " + ci.cEnd;
924       }
925     }
926     return tooltip;
927   }
928
929   /**
930    * Constructs and returns the status bar message
931    * 
932    * @param al
933    * @param column
934    * @param ann
935    * @param rowAndOffset
936    */
937   static String getStatusMessage(AlignmentI al, int column,
938           AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
939   {
940     /*
941      * show alignment column and annotation description if any
942      */
943     StringBuilder text = new StringBuilder(32);
944     text.append(MessageManager.getString("label.column")).append(" ")
945             .append(column + 1);
946
947     if (column < ann.annotations.length && ann.annotations[column] != null)
948     {
949       String description = ann.annotations[column].description;
950       if (description != null && description.trim().length() > 0)
951       {
952         text.append("  ").append(description);
953       }
954     }
955
956     /*
957      * if the annotation is sequence-specific, show the sequence number
958      * in the alignment, and (if not a gap) the residue and position
959      */
960     SequenceI seqref = ann.sequenceRef;
961     if (seqref != null)
962     {
963       int seqIndex = al.findIndex(seqref);
964       if (seqIndex != -1)
965       {
966         text.append(", ").append(MessageManager.getString("label.sequence"))
967                 .append(" ").append(seqIndex + 1);
968         char residue = seqref.getCharAt(column);
969         if (!Comparison.isGap(residue))
970         {
971           text.append(" ");
972           String name;
973           if (al.isNucleotide())
974           {
975             name = ResidueProperties.nucleotideName
976                     .get(String.valueOf(residue));
977             text.append(" Nucleotide: ")
978                     .append(name != null ? name : residue);
979           }
980           else
981           {
982             name = 'X' == residue ? "X"
983                     : ('*' == residue ? "STOP"
984                             : ResidueProperties.aa2Triplet
985                                     .get(String.valueOf(residue)));
986             text.append(" Residue: ").append(name != null ? name : residue);
987           }
988           int residuePos = seqref.findPosition(column);
989           text.append(" (").append(residuePos).append(")");
990         }
991       }
992     }
993
994     return text.toString();
995   }
996
997   /**
998    * DOCUMENT ME!
999    * 
1000    * @param evt
1001    *          DOCUMENT ME!
1002    */
1003   @Override
1004   public void mouseClicked(MouseEvent evt)
1005   {
1006     // if (activeRow != -1)
1007     // {
1008     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1009     // AlignmentAnnotation anot = aa[activeRow];
1010     // }
1011   }
1012
1013   // TODO mouseClicked-content and drawCursor are quite experimental!
1014   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
1015           int y1)
1016   {
1017     int pady = av.getCharHeight() / 5;
1018     int charOffset = 0;
1019     graphics.setColor(Color.black);
1020     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
1021
1022     if (av.validCharWidth)
1023     {
1024       graphics.setColor(Color.white);
1025
1026       char s = seq.getCharAt(res);
1027
1028       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
1029       graphics.drawString(String.valueOf(s), charOffset + x1,
1030               (y1 + av.getCharHeight()) - pady);
1031     }
1032
1033   }
1034
1035   private volatile boolean imageFresh = false;
1036
1037   private Rectangle visibleRect = new Rectangle(),
1038           clipBounds = new Rectangle();
1039
1040   /**
1041    * DOCUMENT ME!
1042    * 
1043    * @param g
1044    *          DOCUMENT ME!
1045    */
1046   @Override
1047   public void paintComponent(Graphics g)
1048   {
1049
1050     // BH: note that this method is generally recommended to
1051     // call super.paintComponent(g). Otherwise, the children of this
1052     // component will not be rendered. That is not needed here
1053     // because AnnotationPanel does not have any children. It is
1054     // just a JPanel contained in a JViewPort.
1055
1056     computeVisibleRect(visibleRect);
1057
1058     g.setColor(Color.white);
1059     g.fillRect(0, 0, visibleRect.width, visibleRect.height);
1060
1061     if (image != null)
1062     {
1063       // BH 2018 optimizing generation of new Rectangle().
1064       if (fastPaint
1065               || (visibleRect.width != (clipBounds = g
1066                       .getClipBounds(clipBounds)).width)
1067               || (visibleRect.height != clipBounds.height))
1068       {
1069
1070         g.drawImage(image, 0, 0, this);
1071         fastPaint = false;
1072         return;
1073       }
1074     }
1075     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
1076             + 1) * av.getCharWidth();
1077     if (imgWidth < 1)
1078     {
1079       return;
1080     }
1081     Graphics2D gg;
1082     if (image == null || imgWidth != image.getWidth(this)
1083             || image.getHeight(this) != getHeight())
1084     {
1085       try
1086       {
1087         image = new BufferedImage(imgWidth,
1088                 ap.getAnnotationPanel().getHeight(),
1089                 BufferedImage.TYPE_INT_RGB);
1090       } catch (OutOfMemoryError oom)
1091       {
1092         try
1093         {
1094           System.gc();
1095         } catch (Exception x)
1096         {
1097         }
1098         ;
1099         new OOMWarning(
1100                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
1101                 oom);
1102         return;
1103       }
1104       gg = (Graphics2D) image.getGraphics();
1105
1106       if (av.antiAlias)
1107       {
1108         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1109                 RenderingHints.VALUE_ANTIALIAS_ON);
1110       }
1111
1112       gg.setFont(av.getFont());
1113       fm = gg.getFontMetrics();
1114       gg.setColor(Color.white);
1115       gg.fillRect(0, 0, imgWidth, image.getHeight());
1116       imageFresh = true;
1117     }
1118     else
1119     {
1120       gg = (Graphics2D) image.getGraphics();
1121
1122     }
1123
1124     drawComponent(gg, av.getRanges().getStartRes(),
1125             av.getRanges().getEndRes() + 1);
1126     gg.dispose();
1127     imageFresh = false;
1128     g.drawImage(image, 0, 0, this);
1129   }
1130
1131   /**
1132    * set true to enable redraw timing debug output on stderr
1133    */
1134   private final boolean debugRedraw = false;
1135
1136   /**
1137    * non-Thread safe repaint
1138    * 
1139    * @param horizontal
1140    *          repaint with horizontal shift in alignment
1141    */
1142   public void fastPaint(int horizontal)
1143   {
1144     if ((horizontal == 0) || image == null
1145             || av.getAlignment().getAlignmentAnnotation() == null
1146             || av.getAlignment().getAlignmentAnnotation().length < 1
1147             || av.isCalcInProgress())
1148     {
1149       repaint();
1150       return;
1151     }
1152
1153     int sr = av.getRanges().getStartRes();
1154     int er = av.getRanges().getEndRes() + 1;
1155     int transX = 0;
1156
1157     Graphics2D gg = (Graphics2D) image.getGraphics();
1158
1159     if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
1160     {
1161       // scroll is less than imgWidth away so can re-use buffered graphics
1162       gg.copyArea(0, 0, imgWidth, getHeight(),
1163               -horizontal * av.getCharWidth(), 0);
1164
1165       if (horizontal > 0) // scrollbar pulled right, image to the left
1166       {
1167         transX = (er - sr - horizontal) * av.getCharWidth();
1168         sr = er - horizontal;
1169       }
1170       else if (horizontal < 0)
1171       {
1172         er = sr - horizontal;
1173       }
1174     }
1175     gg.translate(transX, 0);
1176
1177     drawComponent(gg, sr, er);
1178
1179     gg.translate(-transX, 0);
1180
1181     gg.dispose();
1182
1183     fastPaint = true;
1184
1185     // Call repaint on alignment panel so that repaints from other alignment
1186     // panel components can be aggregated. Otherwise performance of the overview
1187     // window and others may be adversely affected.
1188     av.getAlignPanel().repaint();
1189   }
1190
1191   private volatile boolean lastImageGood = false;
1192
1193   /**
1194    * DOCUMENT ME!
1195    * 
1196    * @param g
1197    *          DOCUMENT ME!
1198    * @param startRes
1199    *          DOCUMENT ME!
1200    * @param endRes
1201    *          DOCUMENT ME!
1202    */
1203   public void drawComponent(Graphics g, int startRes, int endRes)
1204   {
1205     BufferedImage oldFaded = fadedImage;
1206     if (av.isCalcInProgress())
1207     {
1208       if (image == null)
1209       {
1210         lastImageGood = false;
1211         return;
1212       }
1213       // We'll keep a record of the old image,
1214       // and draw a faded image until the calculation
1215       // has completed
1216       if (lastImageGood
1217               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1218                       || fadedImage.getHeight() != image.getHeight()))
1219       {
1220         // System.err.println("redraw faded image ("+(fadedImage==null ?
1221         // "null image" : "") + " lastGood="+lastImageGood+")");
1222         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1223                 BufferedImage.TYPE_INT_RGB);
1224
1225         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1226
1227         fadedG.setColor(Color.white);
1228         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1229
1230         fadedG.setComposite(
1231                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1232         fadedG.drawImage(image, 0, 0, this);
1233
1234       }
1235       // make sure we don't overwrite the last good faded image until all
1236       // calculations have finished
1237       lastImageGood = false;
1238
1239     }
1240     else
1241     {
1242       if (fadedImage != null)
1243       {
1244         oldFaded = fadedImage;
1245       }
1246       fadedImage = null;
1247     }
1248
1249     g.setColor(Color.white);
1250     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1251
1252     g.setFont(av.getFont());
1253     if (fm == null)
1254     {
1255       fm = g.getFontMetrics();
1256     }
1257
1258     if ((av.getAlignment().getAlignmentAnnotation() == null)
1259             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1260     {
1261       g.setColor(Color.white);
1262       g.fillRect(0, 0, getWidth(), getHeight());
1263       g.setColor(Color.black);
1264       if (av.validCharWidth)
1265       {
1266         g.drawString(MessageManager
1267                 .getString("label.alignment_has_no_annotations"), 20, 15);
1268       }
1269
1270       return;
1271     }
1272     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1273             endRes);
1274     if (!lastImageGood && fadedImage == null)
1275     {
1276       fadedImage = oldFaded;
1277     }
1278   }
1279
1280   @Override
1281   public FontMetrics getFontMetrics()
1282   {
1283     return fm;
1284   }
1285
1286   @Override
1287   public Image getFadedImage()
1288   {
1289     return fadedImage;
1290   }
1291
1292   @Override
1293   public int getFadedImageWidth()
1294   {
1295     return imgWidth;
1296   }
1297
1298   private int[] bounds = new int[2];
1299
1300   @Override
1301   public int[] getVisibleVRange()
1302   {
1303     if (ap != null && ap.getAlabels() != null)
1304     {
1305       int sOffset = -ap.getAlabels().getScrollOffset();
1306       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1307       bounds[0] = sOffset;
1308       bounds[1] = visHeight;
1309       return bounds;
1310     }
1311     else
1312     {
1313       return null;
1314     }
1315   }
1316
1317   /**
1318    * Try to ensure any references held are nulled
1319    */
1320   public void dispose()
1321   {
1322     av = null;
1323     ap = null;
1324     image = null;
1325     fadedImage = null;
1326     // gg = null;
1327     _mwl = null;
1328
1329     /*
1330      * I created the renderer so I will dispose of it
1331      */
1332     if (renderer != null)
1333     {
1334       renderer.dispose();
1335     }
1336   }
1337
1338   @Override
1339   public void propertyChange(PropertyChangeEvent evt)
1340   {
1341     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1342     // Both scrolling and resizing change viewport ranges: scrolling changes
1343     // both start and end points, but resize only changes end values.
1344     // Here we only want to fastpaint on a scroll, with resize using a normal
1345     // paint, so scroll events are identified as changes to the horizontal or
1346     // vertical start value.
1347     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1348     {
1349       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1350     }
1351     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1352     {
1353       fastPaint(((int[]) evt.getNewValue())[0]
1354               - ((int[]) evt.getOldValue())[0]);
1355     }
1356     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1357     {
1358       repaint();
1359     }
1360   }
1361
1362   /**
1363    * computes the visible height of the annotation panel
1364    * 
1365    * @param adjustPanelHeight
1366    *          - when false, just adjust existing height according to other
1367    *          windows
1368    * @param annotationHeight
1369    * @return height to use for the ScrollerPreferredVisibleSize
1370    */
1371   public int adjustForAlignFrame(boolean adjustPanelHeight,
1372           int annotationHeight)
1373   {
1374     /*
1375      * Estimate available height in the AlignFrame for alignment +
1376      * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1377      * hscroll, status bar, insets. 
1378      */
1379     int stuff = (ap.getViewName() != null ? 30 : 0)
1380             + (Platform.isAMacAndNotJS() ? 120 : 140);
1381     int availableHeight = ap.alignFrame.getHeight() - stuff;
1382     int rowHeight = av.getCharHeight();
1383
1384     if (adjustPanelHeight)
1385     {
1386       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1387
1388       /*
1389        * If not enough vertical space, maximize annotation height while keeping
1390        * at least two rows of alignment visible
1391        */
1392       if (annotationHeight + alignmentHeight > availableHeight)
1393       {
1394         annotationHeight = Math.min(annotationHeight,
1395                 availableHeight - 2 * rowHeight);
1396       }
1397     }
1398     else
1399     {
1400       // maintain same window layout whilst updating sliders
1401       annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1402               availableHeight - 2 * rowHeight);
1403     }
1404     return annotationHeight;
1405   }
1406 }