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