Merge branch 'bug/JAL-2778again' into releases/Release_2_10_4_Branch
[jalview.git] / src / jalview / gui / SeqCanvas.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 jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.ScaleRenderer;
29 import jalview.renderer.ScaleRenderer.ScaleMark;
30 import jalview.util.Comparison;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.AlphaComposite;
35 import java.awt.BasicStroke;
36 import java.awt.BorderLayout;
37 import java.awt.Color;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Graphics2D;
41 import java.awt.RenderingHints;
42 import java.awt.Shape;
43 import java.awt.image.BufferedImage;
44 import java.beans.PropertyChangeEvent;
45 import java.util.List;
46
47 import javax.swing.JComponent;
48
49 /**
50  * The Swing component on which the alignment sequences, and annotations (if
51  * shown), are drawn. This includes scales above, left and right (if shown) in
52  * Wrapped mode, but not the scale above in Unwrapped mode.
53  * 
54  */
55 public class SeqCanvas extends JComponent implements ViewportListenerI
56 {
57   private static final String ZEROS = "0000000000";
58
59   final FeatureRenderer fr;
60
61   BufferedImage img;
62
63   AlignViewport av;
64
65   int cursorX = 0;
66
67   int cursorY = 0;
68
69   private final SequenceRenderer seqRdr;
70
71   private boolean fastPaint = false;
72
73   private boolean fastpainting = false;
74
75   private AnnotationPanel annotations;
76
77   /*
78    * measurements for drawing a wrapped alignment
79    */
80   private int labelWidthEast; // label right width in pixels if shown
81
82   private int labelWidthWest; // label left width in pixels if shown
83
84   private int wrappedSpaceAboveAlignment; // gap between widths
85
86   private int wrappedRepeatHeightPx; // height in pixels of wrapped width
87
88   private int wrappedVisibleWidths; // number of wrapped widths displayed
89
90   private Graphics2D gg;
91
92   /**
93    * Creates a new SeqCanvas object.
94    * 
95    * @param ap
96    */
97   public SeqCanvas(AlignmentPanel ap)
98   {
99     this.av = ap.av;
100     fr = new FeatureRenderer(ap);
101     seqRdr = new SequenceRenderer(av);
102     setLayout(new BorderLayout());
103     PaintRefresher.Register(this, av.getSequenceSetId());
104     setBackground(Color.white);
105
106     av.getRanges().addPropertyChangeListener(this);
107   }
108
109   public SequenceRenderer getSequenceRenderer()
110   {
111     return seqRdr;
112   }
113
114   public FeatureRenderer getFeatureRenderer()
115   {
116     return fr;
117   }
118
119   /**
120    * Draws the scale above a region of a wrapped alignment, consisting of a
121    * column number every major interval (10 columns).
122    * 
123    * @param g
124    *          the graphics context to draw on, positioned at the start (bottom
125    *          left) of the line on which to draw any scale marks
126    * @param startx
127    *          start alignment column (0..)
128    * @param endx
129    *          end alignment column (0..)
130    * @param ypos
131    *          y offset to draw at
132    */
133   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
134   {
135     int charHeight = av.getCharHeight();
136     int charWidth = av.getCharWidth();
137
138     /*
139      * white fill the scale space (for the fastPaint case)
140      */
141     g.setColor(Color.white);
142     g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
143             charHeight * 3 / 2 + 2);
144     g.setColor(Color.black);
145
146     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
147             endx);
148     for (ScaleMark mark : marks)
149     {
150       int mpos = mark.column; // (i - startx - 1)
151       if (mpos < 0)
152       {
153         continue;
154       }
155       String mstring = mark.text;
156
157       if (mark.major)
158       {
159         if (mstring != null)
160         {
161           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
162         }
163
164         /*
165          * draw a tick mark below the column number, centred on the column;
166          * height of tick mark is 4 pixels less than half a character
167          */
168         int xpos = (mpos * charWidth) + (charWidth / 2);
169         g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
170       }
171     }
172   }
173
174   /**
175    * Draw the scale to the left or right of a wrapped alignment
176    * 
177    * @param g
178    *          graphics context, positioned at the start of the scale to be drawn
179    * @param startx
180    *          first column of wrapped width (0.. excluding any hidden columns)
181    * @param endx
182    *          last column of wrapped width (0.. excluding any hidden columns)
183    * @param ypos
184    *          vertical offset at which to begin the scale
185    * @param left
186    *          if true, scale is left of residues, if false, scale is right
187    */
188   void drawVerticalScale(Graphics g, final int startx, final int endx,
189           final int ypos, final boolean left)
190   {
191     int charHeight = av.getCharHeight();
192     int charWidth = av.getCharWidth();
193
194     int yPos = ypos + charHeight;
195     int startX = startx;
196     int endX = endx;
197
198     if (av.hasHiddenColumns())
199     {
200       HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
201       startX = hiddenColumns.adjustForHiddenColumns(startx);
202       endX = hiddenColumns.adjustForHiddenColumns(endx);
203     }
204     FontMetrics fm = getFontMetrics(av.getFont());
205
206     for (int i = 0; i < av.getAlignment().getHeight(); i++)
207     {
208       SequenceI seq = av.getAlignment().getSequenceAt(i);
209
210       /*
211        * find sequence position of first non-gapped position -
212        * to the right if scale left, to the left if scale right
213        */
214       int index = left ? startX : endX;
215       int value = -1;
216       while (index >= startX && index <= endX)
217       {
218         if (!Comparison.isGap(seq.getCharAt(index)))
219         {
220           value = seq.findPosition(index);
221           break;
222         }
223         if (left)
224         {
225           index++;
226         }
227         else
228         {
229           index--;
230         }
231       }
232
233       /*
234        * white fill the space for the scale
235        */
236       g.setColor(Color.white);
237       int y = (yPos + (i * charHeight)) - (charHeight / 5);
238       // fillRect origin is top left of rectangle
239       g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast,
240               charHeight + 1);
241
242       if (value != -1)
243       {
244         /*
245          * draw scale value, right justified within its width less half a
246          * character width padding on the right
247          */
248         int labelSpace = left ? labelWidthWest : labelWidthEast;
249         labelSpace -= charWidth / 2; // leave space to the right
250         String valueAsString = String.valueOf(value);
251         int labelLength = fm.stringWidth(valueAsString);
252         int xOffset = labelSpace - labelLength;
253         g.setColor(Color.black);
254         g.drawString(valueAsString, xOffset, y);
255       }
256     }
257   }
258
259   /**
260    * Does a fast paint of an alignment in response to a scroll. Most of the
261    * visible region is simply copied and shifted, and then any newly visible
262    * columns or rows are drawn. The scroll may be horizontal or vertical, but
263    * not both at once. Scrolling may be the result of
264    * <ul>
265    * <li>dragging a scroll bar</li>
266    * <li>clicking in the scroll bar</li>
267    * <li>scrolling by trackpad, middle mouse button, or other device</li>
268    * <li>by moving the box in the Overview window</li>
269    * <li>programmatically to make a highlighted position visible</li>
270    * </ul>
271    * 
272    * @param horizontal
273    *          columns to shift right (positive) or left (negative)
274    * @param vertical
275    *          rows to shift down (positive) or up (negative)
276    */
277   public void fastPaint(int horizontal, int vertical)
278   {
279     if (fastpainting || gg == null || img == null)
280     {
281       return;
282     }
283     fastpainting = true;
284     fastPaint = true;
285
286     try
287     {
288       int charHeight = av.getCharHeight();
289       int charWidth = av.getCharWidth();
290     
291       ViewportRanges ranges = av.getRanges();
292       int startRes = ranges.getStartRes();
293       int endRes = ranges.getEndRes();
294       int startSeq = ranges.getStartSeq();
295       int endSeq = ranges.getEndSeq();
296       int transX = 0;
297       int transY = 0;
298       
299       gg.copyArea(horizontal * charWidth, vertical * charHeight,
300               img.getWidth(), img.getHeight(), -horizontal * charWidth,
301               -vertical * charHeight);
302
303       if (horizontal > 0) // scrollbar pulled right, image to the left
304       {
305         transX = (endRes - startRes - horizontal) * charWidth;
306         startRes = endRes - horizontal;
307       }
308       else if (horizontal < 0)
309       {
310         endRes = startRes - horizontal;
311       }
312
313       if (vertical > 0) // scroll down
314       {
315         startSeq = endSeq - vertical;
316
317         if (startSeq < ranges.getStartSeq())
318         { // ie scrolling too fast, more than a page at a time
319           startSeq = ranges.getStartSeq();
320         }
321         else
322         {
323           transY = img.getHeight() - ((vertical + 1) * charHeight);
324         }
325       }
326       else if (vertical < 0)
327       {
328         endSeq = startSeq - vertical;
329
330         if (endSeq > ranges.getEndSeq())
331         {
332           endSeq = ranges.getEndSeq();
333         }
334       }
335
336       gg.translate(transX, transY);
337       drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
338       gg.translate(-transX, -transY);
339
340       // Call repaint on alignment panel so that repaints from other alignment
341       // panel components can be aggregated. Otherwise performance of the
342       // overview window and others may be adversely affected.
343       av.getAlignPanel().repaint();
344     } finally
345     {
346       fastpainting = false;
347     }
348   }
349
350   @Override
351   public void paintComponent(Graphics g)
352   {
353     super.paintComponent(g);    
354     
355     int charHeight = av.getCharHeight();
356     int charWidth = av.getCharWidth();
357     
358     ViewportRanges ranges = av.getRanges();
359     
360     int width = getWidth();
361     int height = getHeight();
362     
363     width -= (width % charWidth);
364     height -= (height % charHeight);
365     
366     // selectImage is the selection group outline image
367     BufferedImage selectImage = drawSelectionGroup(
368             ranges.getStartRes(), ranges.getEndRes(),
369             ranges.getStartSeq(), ranges.getEndSeq());
370     
371     if ((img != null) && (fastPaint
372             || (getVisibleRect().width != g.getClipBounds().width)
373             || (getVisibleRect().height != g.getClipBounds().height)))
374     {
375       BufferedImage lcimg = buildLocalImage(selectImage);
376       g.drawImage(lcimg, 0, 0, this);
377       fastPaint = false;
378     }
379     else if ((width > 0) && (height > 0))
380     {
381       // img is a cached version of the last view we drew, if any
382       // if we have no img or the size has changed, make a new one
383       if (img == null || width != img.getWidth()
384               || height != img.getHeight())
385       {
386         img = setupImage();
387         if (img == null)
388         {
389           return;
390         }
391         gg = (Graphics2D) img.getGraphics();
392         gg.setFont(av.getFont());
393       }
394     
395       if (av.antiAlias)
396       {
397         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
398                 RenderingHints.VALUE_ANTIALIAS_ON);
399       }
400     
401       gg.setColor(Color.white);
402       gg.fillRect(0, 0, img.getWidth(), img.getHeight());
403     
404       if (av.getWrapAlignment())
405       {
406         drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
407       }
408       else
409       {
410         drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
411                 ranges.getStartSeq(), ranges.getEndSeq(), 0);
412       }
413     
414       // lcimg is a local *copy* of img which we'll draw selectImage on top of
415       BufferedImage lcimg = buildLocalImage(selectImage);
416       g.drawImage(lcimg, 0, 0, this);
417
418     }
419
420     if (av.cursorMode)
421     {
422       drawCursor(g, ranges.getStartRes(), ranges.getEndRes(),
423               ranges.getStartSeq(), ranges.getEndSeq());
424     }
425   }
426   
427   /**
428    * Draw an alignment panel for printing
429    * 
430    * @param g1
431    *          Graphics object to draw with
432    * @param startRes
433    *          start residue of print area
434    * @param endRes
435    *          end residue of print area
436    * @param startSeq
437    *          start sequence of print area
438    * @param endSeq
439    *          end sequence of print area
440    */
441   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
442           int startSeq, int endSeq)
443   {
444     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
445
446     BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
447             startSeq, endSeq);
448     if (selectImage != null)
449     {
450       ((Graphics2D) g1).setComposite(AlphaComposite
451               .getInstance(AlphaComposite.SRC_OVER));
452       g1.drawImage(selectImage, 0, 0, this);
453     }
454   }
455
456   /**
457    * Draw a wrapped alignment panel for printing
458    * 
459    * @param g
460    *          Graphics object to draw with
461    * @param canvasWidth
462    *          width of drawing area
463    * @param canvasHeight
464    *          height of drawing area
465    * @param startRes
466    *          start residue of print area
467    */
468   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
469           int canvasHeight, int startRes)
470   {
471     SequenceGroup group = av.getSelectionGroup();
472
473     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
474
475     if (group != null)
476     {
477       BufferedImage selectImage = null;
478       try
479       {
480         selectImage = new BufferedImage(canvasWidth, canvasHeight,
481                 BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
482       } catch (OutOfMemoryError er)
483       {
484         System.gc();
485         System.err.println("Print image OutOfMemory Error.\n" + er);
486         new OOMWarning("Creating wrapped alignment image for printing", er);
487       }
488       if (selectImage != null)
489       {
490         Graphics2D g2 = selectImage.createGraphics();
491         setupSelectionGroup(g2, selectImage);
492         drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
493                 startRes);
494
495         g2.setComposite(
496                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
497         g.drawImage(selectImage, 0, 0, this);
498         g2.dispose();
499       }
500     }
501   }
502
503   /*
504    * Make a local image by combining the cached image img
505    * with any selection
506    */
507   private BufferedImage buildLocalImage(BufferedImage selectImage)
508   {
509     // clone the cached image
510           BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
511                     img.getType());
512
513     // BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
514     // img.getType());
515     Graphics2D g2d = lcimg.createGraphics();
516     g2d.drawImage(img, 0, 0, null);
517
518     // overlay selection group on lcimg
519     if (selectImage != null)
520     {
521       g2d.setComposite(
522               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
523       g2d.drawImage(selectImage, 0, 0, this);
524     }
525
526     g2d.dispose();
527
528     return lcimg;
529   }
530
531   /*
532    * Set up a buffered image of the correct height and size for the sequence canvas
533    */
534   private BufferedImage setupImage()
535   {
536     BufferedImage lcimg = null;
537
538     int charWidth = av.getCharWidth();
539     int charHeight = av.getCharHeight();
540     
541     int width = getWidth();
542     int height = getHeight();
543
544     width -= (width % charWidth);
545     height -= (height % charHeight);
546
547     if ((width < 1) || (height < 1))
548     {
549       return null;
550     }
551
552     try
553     {
554         lcimg = new BufferedImage(width, height,
555                 BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
556     } catch (OutOfMemoryError er)
557     {
558       System.gc();
559       System.err.println(
560               "Group image OutOfMemory Redraw Error.\n" + er);
561       new OOMWarning("Creating alignment image for display", er);
562
563       return null;
564     }
565
566     return lcimg;
567   }
568
569   /**
570    * Returns the visible width of the canvas in residues, after allowing for
571    * East or West scales (if shown)
572    * 
573    * @param canvasWidth
574    *          the width in pixels (possibly including scales)
575    * 
576    * @return
577    */
578   public int getWrappedCanvasWidth(int canvasWidth)
579   {
580     int charWidth = av.getCharWidth();
581
582     FontMetrics fm = getFontMetrics(av.getFont());
583
584     int labelWidth = 0;
585     
586     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
587     {
588       labelWidth = getLabelWidth(fm);
589     }
590
591     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
592
593     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
594
595     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
596   }
597
598   /**
599    * Returns a pixel width sufficient to show the largest sequence coordinate
600    * (end position) in the alignment, calculated as the FontMetrics width of
601    * zeroes "0000000" limited to the number of decimal digits to be shown (3 for
602    * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for
603    * half a character width space on either side.
604    * 
605    * @param fm
606    * @return
607    */
608   protected int getLabelWidth(FontMetrics fm)
609   {
610     /*
611      * find the biggest sequence end position we need to show
612      * (note this is not necessarily the sequence length)
613      */
614     int maxWidth = 0;
615     AlignmentI alignment = av.getAlignment();
616     for (int i = 0; i < alignment.getHeight(); i++)
617     {
618       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
619     }
620
621     int length = 0;
622     for (int i = maxWidth; i > 0; i /= 10)
623     {
624       length++;
625     }
626
627     return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth();
628   }
629
630   /**
631    * Draws as many widths of a wrapped alignment as can fit in the visible
632    * window
633    * 
634    * @param g
635    * @param canvasWidth
636    *          available width in pixels
637    * @param canvasHeight
638    *          available height in pixels
639    * @param startColumn
640    *          the first column (0...) of the alignment to draw
641    */
642   public void drawWrappedPanel(Graphics g, int canvasWidth,
643           int canvasHeight, final int startColumn)
644   {
645     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
646             canvasHeight);
647
648     av.setWrappedWidth(wrappedWidthInResidues);
649
650     ViewportRanges ranges = av.getRanges();
651     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
652
653     /*
654      * draw one width at a time (including any scales or annotation shown),
655      * until we have run out of either alignment or vertical space available
656      */
657     int ypos = wrappedSpaceAboveAlignment;
658     int maxWidth = ranges.getVisibleAlignmentWidth();
659
660     int start = startColumn;
661     int currentWidth = 0;
662     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
663     {
664       int endColumn = Math
665               .min(maxWidth, start + wrappedWidthInResidues - 1);
666       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
667       ypos += wrappedRepeatHeightPx;
668       start += wrappedWidthInResidues;
669       currentWidth++;
670     }
671
672     drawWrappedDecorators(g, startColumn);
673   }
674
675   /**
676    * Calculates and saves values needed when rendering a wrapped alignment.
677    * These depend on many factors, including
678    * <ul>
679    * <li>canvas width and height</li>
680    * <li>number of visible sequences, and height of annotations if shown</li>
681    * <li>font and character width</li>
682    * <li>whether scales are shown left, right or above the alignment</li>
683    * </ul>
684    * 
685    * @param canvasWidth
686    * @param canvasHeight
687    * @return the number of residue columns in each width
688    */
689   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
690   {
691     int charHeight = av.getCharHeight();
692
693     /*
694      * vertical space in pixels between wrapped widths of alignment
695      * - one character height, or two if scale above is drawn
696      */
697     wrappedSpaceAboveAlignment = charHeight
698             * (av.getScaleAboveWrapped() ? 2 : 1);
699
700     /*
701      * height in pixels of the wrapped widths
702      */
703     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
704     // add sequences
705     wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
706             * charHeight;
707     // add annotations panel height if shown
708     wrappedRepeatHeightPx += getAnnotationHeight();
709
710     /*
711      * number of visible widths (the last one may be part height),
712      * ensuring a part height includes at least one sequence
713      */
714     ViewportRanges ranges = av.getRanges();
715     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
716     int remainder = canvasHeight % wrappedRepeatHeightPx;
717     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
718     {
719       wrappedVisibleWidths++;
720     }
721
722     /*
723      * compute width in residues; this also sets East and West label widths
724      */
725     int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
726
727     /*
728      *  limit visibleWidths to not exceed width of alignment
729      */
730     int xMax = ranges.getVisibleAlignmentWidth();
731     int startToEnd = xMax - ranges.getStartRes();
732     int maxWidths = startToEnd / wrappedWidthInResidues;
733     if (startToEnd % wrappedWidthInResidues > 0)
734     {
735       maxWidths++;
736     }
737     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
738
739     return wrappedWidthInResidues;
740   }
741
742   /**
743    * Draws one width of a wrapped alignment, including sequences and
744    * annnotations, if shown, but not scales or hidden column markers
745    * 
746    * @param g
747    * @param ypos
748    * @param startColumn
749    * @param endColumn
750    * @param canvasHeight
751    */
752   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
753           int endColumn, int canvasHeight)
754   {
755     ViewportRanges ranges = av.getRanges();
756     int viewportWidth = ranges.getViewportWidth();
757
758     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
759
760     /*
761      * move right before drawing by the width of the scale left (if any)
762      * plus column offset from left margin (usually zero, but may be non-zero
763      * when fast painting is drawing just a few columns)
764      */
765     int charWidth = av.getCharWidth();
766     int xOffset = labelWidthWest
767             + ((startColumn - ranges.getStartRes()) % viewportWidth)
768             * charWidth;
769     g.translate(xOffset, 0);
770
771     // When printing we have an extra clipped region,
772     // the Printable page which we need to account for here
773     Shape clip = g.getClip();
774
775     if (clip == null)
776     {
777       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
778     }
779     else
780     {
781       g.setClip(0, (int) clip.getBounds().getY(),
782               viewportWidth * charWidth, (int) clip.getBounds().getHeight());
783     }
784
785     /*
786      * white fill the region to be drawn (so incremental fast paint doesn't
787      * scribble over an existing image)
788      */
789     g.setColor(Color.white);
790     g.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
791             wrappedRepeatHeightPx);
792
793     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
794             ypos);
795
796     int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
797
798     if (av.isShowAnnotation())
799     {
800       g.translate(0, cHeight + ypos + 3);
801       if (annotations == null)
802       {
803         annotations = new AnnotationPanel(av);
804       }
805
806       annotations.renderer.drawComponent(annotations, av, g, -1,
807               startColumn, endx + 1);
808       g.translate(0, -cHeight - ypos - 3);
809     }
810     g.setClip(clip);
811     g.translate(-xOffset, 0);
812   }
813
814   /**
815    * Draws scales left, right and above (if shown), and any hidden column
816    * markers, on all widths of the wrapped alignment
817    * 
818    * @param g
819    * @param startColumn
820    */
821   protected void drawWrappedDecorators(Graphics g, final int startColumn)
822   {
823     int charWidth = av.getCharWidth();
824
825     g.setFont(av.getFont());
826     g.setColor(Color.black);
827
828     int ypos = wrappedSpaceAboveAlignment;
829     ViewportRanges ranges = av.getRanges();
830     int viewportWidth = ranges.getViewportWidth();
831     int maxWidth = ranges.getVisibleAlignmentWidth();
832     int widthsDrawn = 0;
833     int startCol = startColumn;
834
835     while (widthsDrawn < wrappedVisibleWidths)
836     {
837       int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1);
838
839       if (av.getScaleLeftWrapped())
840       {
841         drawVerticalScale(g, startCol, endColumn - 1, ypos, true);
842       }
843
844       if (av.getScaleRightWrapped())
845       {
846         int x = labelWidthWest + viewportWidth * charWidth;
847         g.translate(x, 0);
848         drawVerticalScale(g, startCol, endColumn, ypos, false);
849         g.translate(-x, 0);
850       }
851
852       /*
853        * white fill region of scale above and hidden column markers
854        * (to support incremental fast paint of image)
855        */
856       g.translate(labelWidthWest, 0);
857       g.setColor(Color.white);
858       g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
859               * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
860       g.setColor(Color.black);
861       g.translate(-labelWidthWest, 0);
862
863       g.translate(labelWidthWest, 0);
864
865       if (av.getScaleAboveWrapped())
866       {
867         drawNorthScale(g, startCol, endColumn, ypos);
868       }
869
870       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
871       {
872         drawHiddenColumnMarkers(g, ypos, startCol, endColumn);
873       }
874
875       g.translate(-labelWidthWest, 0);
876
877       ypos += wrappedRepeatHeightPx;
878       startCol += viewportWidth;
879       widthsDrawn++;
880     }
881   }
882
883   /**
884    * Draws markers (triangles) above hidden column positions between startColumn
885    * and endColumn.
886    * 
887    * @param g
888    * @param ypos
889    * @param startColumn
890    * @param endColumn
891    */
892   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
893           int startColumn, int endColumn)
894   {
895     int charHeight = av.getCharHeight();
896     int charWidth = av.getCharWidth();
897
898     g.setColor(Color.blue);
899     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
900     List<Integer> positions = hidden.findHiddenRegionPositions();
901     for (int pos : positions)
902     {
903       int res = pos - startColumn;
904
905       if (res < 0 || res > endColumn - startColumn + 1)
906       {
907         continue;
908       }
909
910       /*
911        * draw a downward-pointing triangle at the hidden columns location
912        * (before the following visible column)
913        */
914       int xMiddle = res * charWidth;
915       int[] xPoints = new int[] { xMiddle - charHeight / 4,
916           xMiddle + charHeight / 4, xMiddle };
917       int yTop = ypos - (charHeight / 2);
918       int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
919       g.fillPolygon(xPoints, yPoints, 3);
920     }
921   }
922
923   /*
924    * Draw a selection group over a wrapped alignment
925    */
926   private void drawWrappedSelection(Graphics2D g, SequenceGroup group,
927           int canvasWidth,
928           int canvasHeight, int startRes)
929   {
930     int charHeight = av.getCharHeight();
931     int charWidth = av.getCharWidth();
932       
933     // height gap above each panel
934     int hgap = charHeight;
935     if (av.getScaleAboveWrapped())
936     {
937       hgap += charHeight;
938     }
939
940     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
941             / charWidth;
942     int cHeight = av.getAlignment().getHeight() * charHeight;
943
944     int startx = startRes;
945     int endx;
946     int ypos = hgap; // vertical offset
947     int maxwidth = av.getAlignment().getWidth();
948
949     if (av.hasHiddenColumns())
950     {
951       maxwidth = av.getAlignment().getHiddenColumns()
952               .findColumnPosition(maxwidth);
953     }
954
955     // chop the wrapped alignment extent up into panel-sized blocks and treat
956     // each block as if it were a block from an unwrapped alignment
957     while ((ypos <= canvasHeight) && (startx < maxwidth))
958     {
959       // set end value to be start + width, or maxwidth, whichever is smaller
960       endx = startx + cWidth - 1;
961
962       if (endx > maxwidth)
963       {
964         endx = maxwidth;
965       }
966
967       g.translate(labelWidthWest, 0);
968
969       drawUnwrappedSelection(g, group, startx, endx, 0,
970               av.getAlignment().getHeight() - 1,
971               ypos);
972
973       g.translate(-labelWidthWest, 0);
974
975       // update vertical offset
976       ypos += cHeight + getAnnotationHeight() + hgap;
977
978       // update horizontal offset
979       startx += cWidth;
980     }
981   }
982
983   int getAnnotationHeight()
984   {
985     if (!av.isShowAnnotation())
986     {
987       return 0;
988     }
989
990     if (annotations == null)
991     {
992       annotations = new AnnotationPanel(av);
993     }
994
995     return annotations.adjustPanelHeight();
996   }
997
998   /**
999    * Draws the visible region of the alignment on the graphics context. If there
1000    * are hidden column markers in the visible region, then each sub-region
1001    * between the markers is drawn separately, followed by the hidden column
1002    * marker.
1003    * 
1004    * @param g1
1005    *          the graphics context, positioned at the first residue to be drawn
1006    * @param startRes
1007    *          offset of the first column to draw (0..)
1008    * @param endRes
1009    *          offset of the last column to draw (0..)
1010    * @param startSeq
1011    *          offset of the first sequence to draw (0..)
1012    * @param endSeq
1013    *          offset of the last sequence to draw (0..)
1014    * @param yOffset
1015    *          vertical offset at which to draw (for wrapped alignments)
1016    */
1017   public void drawPanel(Graphics g1, final int startRes, final int endRes,
1018           final int startSeq, final int endSeq, final int yOffset)
1019   {
1020     int charHeight = av.getCharHeight();
1021     int charWidth = av.getCharWidth();
1022
1023     if (!av.hasHiddenColumns())
1024     {
1025       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
1026     }
1027     else
1028     {
1029       int screenY = 0;
1030       final int screenYMax = endRes - startRes;
1031       int blockStart = startRes;
1032       int blockEnd = endRes;
1033
1034       for (int[] region : av.getAlignment().getHiddenColumns()
1035               .getHiddenColumnsCopy())
1036       {
1037         int hideStart = region[0];
1038         int hideEnd = region[1];
1039
1040         if (hideStart <= blockStart)
1041         {
1042           blockStart += (hideEnd - hideStart) + 1;
1043           continue;
1044         }
1045
1046         /*
1047          * draw up to just before the next hidden region, or the end of
1048          * the visible region, whichever comes first
1049          */
1050         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
1051                 - screenY);
1052
1053         g1.translate(screenY * charWidth, 0);
1054
1055         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1056
1057         /*
1058          * draw the downline of the hidden column marker (ScalePanel draws the
1059          * triangle on top) if we reached it
1060          */
1061         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
1062         {
1063           g1.setColor(Color.blue);
1064
1065           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
1066                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
1067                   (endSeq - startSeq + 1) * charHeight + yOffset);
1068         }
1069
1070         g1.translate(-screenY * charWidth, 0);
1071         screenY += blockEnd - blockStart + 1;
1072         blockStart = hideEnd + 1;
1073
1074         if (screenY > screenYMax)
1075         {
1076           // already rendered last block
1077           return;
1078         }
1079       }
1080
1081       if (screenY <= screenYMax)
1082       {
1083         // remaining visible region to render
1084         blockEnd = blockStart + screenYMax - screenY;
1085         g1.translate(screenY * charWidth, 0);
1086         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1087
1088         g1.translate(-screenY * charWidth, 0);
1089       }
1090     }
1091
1092   }
1093
1094   /**
1095    * Draws a region of the visible alignment
1096    * 
1097    * @param g1
1098    * @param startRes
1099    *          offset of the first column in the visible region (0..)
1100    * @param endRes
1101    *          offset of the last column in the visible region (0..)
1102    * @param startSeq
1103    *          offset of the first sequence in the visible region (0..)
1104    * @param endSeq
1105    *          offset of the last sequence in the visible region (0..)
1106    * @param yOffset
1107    *          vertical offset at which to draw (for wrapped alignments)
1108    */
1109   private void draw(Graphics g, int startRes, int endRes, int startSeq,
1110           int endSeq, int offset)
1111   {
1112     int charHeight = av.getCharHeight();
1113     int charWidth = av.getCharWidth();
1114
1115     g.setFont(av.getFont());
1116     seqRdr.prepare(g, av.isRenderGaps());
1117
1118     SequenceI nextSeq;
1119
1120     // / First draw the sequences
1121     // ///////////////////////////
1122     for (int i = startSeq; i <= endSeq; i++)
1123     {
1124       nextSeq = av.getAlignment().getSequenceAt(i);
1125       if (nextSeq == null)
1126       {
1127         // occasionally, a race condition occurs such that the alignment row is
1128         // empty
1129         continue;
1130       }
1131       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
1132               startRes, endRes, offset + ((i - startSeq) * charHeight));
1133
1134       if (av.isShowSequenceFeatures())
1135       {
1136         fr.drawSequence(g, nextSeq, startRes, endRes,
1137                 offset + ((i - startSeq) * charHeight), false);
1138       }
1139
1140       /*
1141        * highlight search Results once sequence has been drawn
1142        */
1143       if (av.hasSearchResults())
1144       {
1145         SearchResultsI searchResults = av.getSearchResults();
1146         int[] visibleResults = searchResults.getResults(nextSeq, startRes,
1147                 endRes);
1148         if (visibleResults != null)
1149         {
1150           for (int r = 0; r < visibleResults.length; r += 2)
1151           {
1152             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
1153                     visibleResults[r + 1],
1154                     (visibleResults[r] - startRes) * charWidth,
1155                     offset + ((i - startSeq) * charHeight));
1156           }
1157         }
1158       }
1159     }
1160
1161     if (av.getSelectionGroup() != null
1162             || av.getAlignment().getGroups().size() > 0)
1163     {
1164       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1165     }
1166
1167   }
1168
1169   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1170           int startSeq, int endSeq, int offset)
1171   {
1172     Graphics2D g = (Graphics2D) g1;
1173     //
1174     // ///////////////////////////////////
1175     // Now outline any areas if necessary
1176     // ///////////////////////////////////
1177
1178     SequenceGroup group = null;
1179     int groupIndex = -1;
1180
1181     if (av.getAlignment().getGroups().size() > 0)
1182     {
1183       group = av.getAlignment().getGroups().get(0);
1184       groupIndex = 0;
1185     }
1186
1187     if (group != null)
1188     {
1189       g.setStroke(new BasicStroke());
1190       g.setColor(group.getOutlineColour());
1191       
1192       do
1193       {
1194         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1195                 endSeq, offset);
1196
1197         groupIndex++;
1198
1199         g.setStroke(new BasicStroke());
1200
1201         if (groupIndex >= av.getAlignment().getGroups().size())
1202         {
1203           break;
1204         }
1205
1206         group = av.getAlignment().getGroups().get(groupIndex);
1207
1208       } while (groupIndex < av.getAlignment().getGroups().size());
1209
1210     }
1211
1212   }
1213
1214
1215   /*
1216    * Draw the selection group as a separate image and overlay
1217    */
1218   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1219           int startSeq, int endSeq)
1220   {
1221     // get a new image of the correct size
1222     BufferedImage selectionImage = setupImage();
1223
1224     if (selectionImage == null)
1225     {
1226       return null;
1227     }
1228
1229     SequenceGroup group = av.getSelectionGroup();
1230     if (group == null)
1231     {
1232       // nothing to draw
1233       return null;
1234     }
1235
1236     // set up drawing colour
1237     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1238
1239     setupSelectionGroup(g, selectionImage);
1240
1241     if (!av.getWrapAlignment())
1242     {
1243       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1244               0);
1245     }
1246     else
1247     {
1248       drawWrappedSelection(g, group, getWidth(), getHeight(),
1249               av.getRanges().getStartRes());
1250     }
1251
1252     g.dispose();
1253     return selectionImage;
1254   }
1255
1256   /**
1257    * Draw the cursor as a separate image and overlay
1258    * 
1259    * @param startRes
1260    *          start residue of area to draw cursor in
1261    * @param endRes
1262    *          end residue of area to draw cursor in
1263    * @param startSeq
1264    *          start sequence of area to draw cursor in
1265    * @param endSeq
1266    *          end sequence of are to draw cursor in
1267    * @return a transparent image of the same size as the sequence canvas, with
1268    *         the cursor drawn on it, if any
1269    */
1270   private void drawCursor(Graphics g, int startRes, int endRes,
1271           int startSeq,
1272           int endSeq)
1273   {
1274     // convert the cursorY into a position on the visible alignment
1275     int cursor_ypos = cursorY;
1276
1277     // don't do work unless we have to
1278     if (cursor_ypos >= startSeq && cursor_ypos <= endSeq)
1279     {
1280       int yoffset = 0;
1281       int xoffset = 0;
1282       int startx = startRes;
1283       int endx = endRes;
1284
1285       // convert the cursorX into a position on the visible alignment
1286       int cursor_xpos = av.getAlignment().getHiddenColumns()
1287               .findColumnPosition(cursorX);
1288
1289       if (av.getAlignment().getHiddenColumns().isVisible(cursorX))
1290       {
1291
1292         if (av.getWrapAlignment())
1293         {
1294           // work out the correct offsets for the cursor
1295           int charHeight = av.getCharHeight();
1296           int charWidth = av.getCharWidth();
1297           int canvasWidth = getWidth();
1298           int canvasHeight = getHeight();
1299
1300           // height gap above each panel
1301           int hgap = charHeight;
1302           if (av.getScaleAboveWrapped())
1303           {
1304             hgap += charHeight;
1305           }
1306
1307           int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
1308                   / charWidth;
1309           int cHeight = av.getAlignment().getHeight() * charHeight;
1310
1311           endx = startx + cWidth - 1;
1312           int ypos = hgap; // vertical offset
1313
1314           // iterate down the wrapped panels
1315           while ((ypos <= canvasHeight) && (endx < cursor_xpos))
1316           {
1317             // update vertical offset
1318             ypos += cHeight + getAnnotationHeight() + hgap;
1319
1320             // update horizontal offset
1321             startx += cWidth;
1322             endx = startx + cWidth - 1;
1323           }
1324           yoffset = ypos;
1325           xoffset = labelWidthWest;
1326         }
1327
1328         // now check if cursor is within range for x values
1329         if (cursor_xpos >= startx && cursor_xpos <= endx)
1330         {
1331           // get the character the cursor is drawn at
1332           SequenceI seq = av.getAlignment().getSequenceAt(cursorY);
1333           char s = seq.getCharAt(cursorX);
1334
1335           seqRdr.drawCursor(g, s,
1336                   xoffset + (cursor_xpos - startx) * av.getCharWidth(),
1337                   yoffset + (cursor_ypos - startSeq) * av.getCharHeight());
1338         }
1339       }
1340     }
1341   }
1342
1343
1344   /*
1345    * Set up graphics for selection group
1346    */
1347   private void setupSelectionGroup(Graphics2D g,
1348           BufferedImage selectionImage)
1349   {
1350     // set background to transparent
1351     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1352     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1353
1354     // set up foreground to draw red dashed line
1355     g.setComposite(AlphaComposite.Src);
1356     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1357             BasicStroke.JOIN_ROUND, 3f, new float[]
1358     { 5f, 3f }, 0f));
1359     g.setColor(Color.RED);
1360   }
1361
1362   /*
1363    * Draw a selection group over an unwrapped alignment
1364    * @param g graphics object to draw with
1365    * @param group selection group
1366    * @param startRes start residue of area to draw
1367    * @param endRes end residue of area to draw
1368    * @param startSeq start sequence of area to draw
1369    * @param endSeq end sequence of area to draw
1370    * @param offset vertical offset (used when called from wrapped alignment code)
1371    */
1372   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1373           int startRes, int endRes, int startSeq, int endSeq, int offset)
1374   {
1375     int charWidth = av.getCharWidth();
1376           
1377     if (!av.hasHiddenColumns())
1378     {
1379       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
1380               offset);
1381     }
1382     else
1383     {
1384       // package into blocks of visible columns
1385       int screenY = 0;
1386       int blockStart = startRes;
1387       int blockEnd = endRes;
1388
1389       for (int[] region : av.getAlignment().getHiddenColumns()
1390               .getHiddenColumnsCopy())
1391       {
1392         int hideStart = region[0];
1393         int hideEnd = region[1];
1394
1395         if (hideStart <= blockStart)
1396         {
1397           blockStart += (hideEnd - hideStart) + 1;
1398           continue;
1399         }
1400
1401         blockEnd = hideStart - 1;
1402
1403         g.translate(screenY * charWidth, 0);
1404         drawPartialGroupOutline(g, group,
1405                 blockStart, blockEnd, startSeq, endSeq, offset);
1406
1407         g.translate(-screenY * charWidth, 0);
1408         screenY += blockEnd - blockStart + 1;
1409         blockStart = hideEnd + 1;
1410
1411         if (screenY > (endRes - startRes))
1412         {
1413           // already rendered last block
1414           break;
1415         }
1416       }
1417
1418       if (screenY <= (endRes - startRes))
1419       {
1420         // remaining visible region to render
1421         blockEnd = blockStart + (endRes - startRes) - screenY;
1422         g.translate(screenY * charWidth, 0);
1423         drawPartialGroupOutline(g, group,
1424                 blockStart, blockEnd, startSeq, endSeq, offset);
1425         
1426         g.translate(-screenY * charWidth, 0);
1427       }
1428     }
1429   }
1430
1431   /*
1432    * Draw the selection group as a separate image and overlay
1433    */
1434   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
1435           int startRes, int endRes, int startSeq, int endSeq,
1436           int verticalOffset)
1437   {
1438     int charHeight = av.getCharHeight();
1439     int charWidth = av.getCharWidth();
1440     int visWidth = (endRes - startRes + 1) * charWidth;
1441
1442     int oldY = -1;
1443     int i = 0;
1444     boolean inGroup = false;
1445     int top = -1;
1446     int bottom = -1;
1447     int sy = -1;
1448
1449     List<SequenceI> seqs = group.getSequences(null);
1450
1451     // position of start residue of group relative to startRes, in pixels
1452     int sx = (group.getStartRes() - startRes) * charWidth;
1453
1454     // width of group in pixels
1455     int xwidth = (((group.getEndRes() + 1) - group.getStartRes())
1456             * charWidth) - 1;
1457
1458     if (!(sx + xwidth < 0 || sx > visWidth))
1459     {
1460       for (i = startSeq; i <= endSeq; i++)
1461       {
1462         sy = verticalOffset + (i - startSeq) * charHeight;
1463
1464         if ((sx <= (endRes - startRes) * charWidth)
1465                 && seqs.contains(av.getAlignment().getSequenceAt(i)))
1466         {
1467           if ((bottom == -1)
1468                   && !seqs.contains(av.getAlignment().getSequenceAt(i + 1)))
1469           {
1470             bottom = sy + charHeight;
1471           }
1472
1473           if (!inGroup)
1474           {
1475             if (((top == -1) && (i == 0)) || !seqs
1476                     .contains(av.getAlignment().getSequenceAt(i - 1)))
1477             {
1478               top = sy;
1479             }
1480
1481             oldY = sy;
1482             inGroup = true;
1483           }
1484         }
1485         else if (inGroup)
1486         {
1487           drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
1488           drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
1489
1490           // reset top and bottom
1491           top = -1;
1492           bottom = -1;
1493           inGroup = false;
1494         }
1495       }
1496       if (inGroup)
1497       {
1498         sy = verticalOffset + ((i - startSeq) * charHeight);
1499         drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
1500         drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
1501       }
1502     }
1503   }
1504
1505   /**
1506    * Draw horizontal selection group boundaries at top and bottom positions
1507    * 
1508    * @param g
1509    *          graphics object to draw on
1510    * @param sx
1511    *          start x position
1512    * @param xwidth
1513    *          width of gap
1514    * @param visWidth
1515    *          visWidth maximum available width
1516    * @param top
1517    *          position to draw top of group at
1518    * @param bottom
1519    *          position to draw bottom of group at
1520    */
1521   private void drawHorizontals(Graphics2D g, int sx, int xwidth,
1522           int visWidth, int top, int bottom)
1523   {
1524     int width = xwidth;
1525     int startx = sx;
1526     if (startx < 0)
1527     {
1528       width += startx;
1529       startx = 0;
1530     }
1531
1532     // don't let width extend beyond current block, or group extent
1533     // fixes JAL-2672
1534     if (startx + width >= visWidth)
1535     {
1536       width = visWidth - startx;
1537     }
1538
1539     if (top != -1)
1540     {
1541       g.drawLine(startx, top, startx + width, top);
1542     }
1543
1544     if (bottom != -1)
1545     {
1546       g.drawLine(startx, bottom - 1, startx + width, bottom - 1);
1547     }
1548   }
1549
1550   /**
1551    * Draw vertical lines at sx and sx+xwidth providing they lie within
1552    * [0,visWidth)
1553    * 
1554    * @param g
1555    *          graphics object to draw on
1556    * @param sx
1557    *          start x position
1558    * @param xwidth
1559    *          width of gap
1560    * @param visWidth
1561    *          visWidth maximum available width
1562    * @param oldY
1563    *          top y value
1564    * @param sy
1565    *          bottom y value
1566    */
1567   private void drawVerticals(Graphics2D g, int sx, int xwidth, int visWidth,
1568           int oldY, int sy)
1569   {
1570     // if start position is visible, draw vertical line to left of
1571     // group
1572     if (sx >= 0 && sx < visWidth)
1573     {
1574       g.drawLine(sx, oldY, sx, sy);
1575     }
1576
1577     // if end position is visible, draw vertical line to right of
1578     // group
1579     if (sx + xwidth < visWidth)
1580     {
1581       g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1582     }
1583   }
1584   
1585   /**
1586    * Highlights search results in the visible region by rendering as white text
1587    * on a black background. Any previous highlighting is removed. Answers true
1588    * if any highlight was left on the visible alignment (so status bar should be
1589    * set to match), else false.
1590    * <p>
1591    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1592    * alignment had to be scrolled to show the highlighted region, then it should
1593    * be fully redrawn, otherwise a fast paint can be performed. This argument
1594    * could be removed if fast paint of scrolled wrapped alignment is coded in
1595    * future (JAL-2609).
1596    * 
1597    * @param results
1598    * @param noFastPaint
1599    * @return
1600    */
1601   public boolean highlightSearchResults(SearchResultsI results,
1602           boolean noFastPaint)
1603   {
1604     if (fastpainting)
1605     {
1606       return false;
1607     }
1608     boolean wrapped = av.getWrapAlignment();
1609     try
1610     {
1611       fastPaint = !noFastPaint;
1612       fastpainting = fastPaint;
1613
1614       /*
1615        * to avoid redrawing the whole visible region, we instead
1616        * redraw just the minimal regions to remove previous highlights
1617        * and add new ones
1618        */
1619       SearchResultsI previous = av.getSearchResults();
1620       av.setSearchResults(results);
1621       boolean redrawn = false;
1622       boolean drawn = false;
1623       if (wrapped)
1624       {
1625         redrawn = drawMappedPositionsWrapped(previous);
1626         drawn = drawMappedPositionsWrapped(results);
1627         redrawn |= drawn;
1628       }
1629       else
1630       {
1631         redrawn = drawMappedPositions(previous);
1632         drawn = drawMappedPositions(results);
1633         redrawn |= drawn;
1634       }
1635
1636       /*
1637        * if highlights were either removed or added, repaint
1638        */
1639       if (redrawn)
1640       {
1641         repaint();
1642       }
1643
1644       /*
1645        * return true only if highlights were added
1646        */
1647       return drawn;
1648
1649     } finally
1650     {
1651       fastpainting = false;
1652     }
1653   }
1654
1655   /**
1656    * Redraws the minimal rectangle in the visible region (if any) that includes
1657    * mapped positions of the given search results. Whether or not positions are
1658    * highlighted depends on the SearchResults set on the Viewport. This allows
1659    * this method to be called to either clear or set highlighting. Answers true
1660    * if any positions were drawn (in which case a repaint is still required),
1661    * else false.
1662    * 
1663    * @param results
1664    * @return
1665    */
1666   protected boolean drawMappedPositions(SearchResultsI results)
1667   {
1668     if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
1669     {
1670       return false;
1671     }
1672
1673     /*
1674      * calculate the minimal rectangle to redraw that 
1675      * includes both new and existing search results
1676      */
1677     int firstSeq = Integer.MAX_VALUE;
1678     int lastSeq = -1;
1679     int firstCol = Integer.MAX_VALUE;
1680     int lastCol = -1;
1681     boolean matchFound = false;
1682
1683     ViewportRanges ranges = av.getRanges();
1684     int firstVisibleColumn = ranges.getStartRes();
1685     int lastVisibleColumn = ranges.getEndRes();
1686     AlignmentI alignment = av.getAlignment();
1687     if (av.hasHiddenColumns())
1688     {
1689       firstVisibleColumn = alignment.getHiddenColumns()
1690               .adjustForHiddenColumns(firstVisibleColumn);
1691       lastVisibleColumn = alignment.getHiddenColumns()
1692               .adjustForHiddenColumns(lastVisibleColumn);
1693     }
1694
1695     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1696             .getEndSeq(); seqNo++)
1697     {
1698       SequenceI seq = alignment.getSequenceAt(seqNo);
1699
1700       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1701               lastVisibleColumn);
1702       if (visibleResults != null)
1703       {
1704         for (int i = 0; i < visibleResults.length - 1; i += 2)
1705         {
1706           int firstMatchedColumn = visibleResults[i];
1707           int lastMatchedColumn = visibleResults[i + 1];
1708           if (firstMatchedColumn <= lastVisibleColumn
1709                   && lastMatchedColumn >= firstVisibleColumn)
1710           {
1711             /*
1712              * found a search results match in the visible region - 
1713              * remember the first and last sequence matched, and the first
1714              * and last visible columns in the matched positions
1715              */
1716             matchFound = true;
1717             firstSeq = Math.min(firstSeq, seqNo);
1718             lastSeq = Math.max(lastSeq, seqNo);
1719             firstMatchedColumn = Math.max(firstMatchedColumn,
1720                     firstVisibleColumn);
1721             lastMatchedColumn = Math.min(lastMatchedColumn,
1722                     lastVisibleColumn);
1723             firstCol = Math.min(firstCol, firstMatchedColumn);
1724             lastCol = Math.max(lastCol, lastMatchedColumn);
1725           }
1726         }
1727       }
1728     }
1729
1730     if (matchFound)
1731     {
1732       if (av.hasHiddenColumns())
1733       {
1734         firstCol = alignment.getHiddenColumns()
1735                 .findColumnPosition(firstCol);
1736         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1737       }
1738       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1739       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1740       gg.translate(transX, transY);
1741       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1742       gg.translate(-transX, -transY);
1743     }
1744
1745     return matchFound;
1746   }
1747
1748   @Override
1749   public void propertyChange(PropertyChangeEvent evt)
1750   {
1751     String eventName = evt.getPropertyName();
1752
1753     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1754     {
1755       fastPaint = true;
1756       repaint();
1757       return;
1758     }
1759     else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT))
1760     {
1761       fastPaint = false;
1762       repaint();
1763       return;
1764     }
1765
1766     int scrollX = 0;
1767     if (eventName.equals(ViewportRanges.STARTRES)
1768             || eventName.equals(ViewportRanges.STARTRESANDSEQ))
1769     {
1770       // Make sure we're not trying to draw a panel
1771       // larger than the visible window
1772       if (eventName.equals(ViewportRanges.STARTRES))
1773       {
1774         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1775       }
1776       else
1777       {
1778         scrollX = ((int[]) evt.getNewValue())[0]
1779                 - ((int[]) evt.getOldValue())[0];
1780       }
1781       ViewportRanges vpRanges = av.getRanges();
1782
1783       int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1784       if (scrollX > range)
1785       {
1786         scrollX = range;
1787       }
1788       else if (scrollX < -range)
1789       {
1790         scrollX = -range;
1791       }
1792     }
1793     // Both scrolling and resizing change viewport ranges: scrolling changes
1794     // both start and end points, but resize only changes end values.
1795     // Here we only want to fastpaint on a scroll, with resize using a normal
1796     // paint, so scroll events are identified as changes to the horizontal or
1797     // vertical start value.
1798     if (eventName.equals(ViewportRanges.STARTRES))
1799     {
1800       if (av.getWrapAlignment())
1801         {
1802           fastPaintWrapped(scrollX);
1803         }
1804         else
1805         {
1806           fastPaint(scrollX, 0);
1807         }
1808     }
1809     else if (eventName.equals(ViewportRanges.STARTSEQ))
1810     {
1811       // scroll
1812       fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1813     }
1814     else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
1815     {
1816       if (av.getWrapAlignment())
1817       {
1818         fastPaintWrapped(scrollX);
1819       }
1820       else
1821       {
1822         fastPaint(scrollX, 0);
1823       }
1824       // bizarrely, we only need to scroll on the x value here as fastpaint
1825       // copies the full height of the image anyway. Passing in the y value
1826       // causes nasty repaint artefacts, which only disappear on a full
1827       // repaint.
1828     }
1829   }
1830
1831   /**
1832    * Does a minimal update of the image for a scroll movement. This method
1833    * handles scroll movements of up to one width of the wrapped alignment (one
1834    * click in the vertical scrollbar). Larger movements (for example after a
1835    * scroll to highlight a mapped position) trigger a full redraw instead.
1836    * 
1837    * @param scrollX
1838    *          number of positions scrolled (right if positive, left if negative)
1839    */
1840   protected void fastPaintWrapped(int scrollX)
1841   {
1842     ViewportRanges ranges = av.getRanges();
1843
1844     if (Math.abs(scrollX) > ranges.getViewportWidth())
1845     {
1846       /*
1847        * shift of more than one view width is 
1848        * overcomplicated to handle in this method
1849        */
1850       fastPaint = false;
1851       repaint();
1852       return;
1853     }
1854
1855     if (fastpainting || gg == null)
1856     {
1857       return;
1858     }
1859
1860     fastPaint = true;
1861     fastpainting = true;
1862
1863     try
1864     {
1865       calculateWrappedGeometry(getWidth(), getHeight());
1866
1867       /*
1868        * relocate the regions of the alignment that are still visible
1869        */
1870       shiftWrappedAlignment(-scrollX);
1871
1872       /*
1873        * add new columns (sequence, annotation)
1874        * - at top left if scrollX < 0 
1875        * - at right of last two widths if scrollX > 0
1876        */
1877       if (scrollX < 0)
1878       {
1879         int startRes = ranges.getStartRes();
1880         drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
1881                 - scrollX - 1, getHeight());
1882       }
1883       else
1884       {
1885         fastPaintWrappedAddRight(scrollX);
1886       }
1887
1888       /*
1889        * draw all scales (if  shown) and hidden column markers
1890        */
1891       drawWrappedDecorators(gg, ranges.getStartRes());
1892
1893       repaint();
1894     } finally
1895     {
1896       fastpainting = false;
1897     }
1898   }
1899
1900   /**
1901    * Draws the specified number of columns at the 'end' (bottom right) of a
1902    * wrapped alignment view, including sequences and annotations if shown, but
1903    * not scales. Also draws the same number of columns at the right hand end of
1904    * the second last width shown, if the last width is not full height (so
1905    * cannot simply be copied from the graphics image).
1906    * 
1907    * @param columns
1908    */
1909   protected void fastPaintWrappedAddRight(int columns)
1910   {
1911     if (columns == 0)
1912     {
1913       return;
1914     }
1915
1916     ViewportRanges ranges = av.getRanges();
1917     int viewportWidth = ranges.getViewportWidth();
1918     int charWidth = av.getCharWidth();
1919
1920     /**
1921      * draw full height alignment in the second last row, last columns, if the
1922      * last row was not full height
1923      */
1924     int visibleWidths = wrappedVisibleWidths;
1925     int canvasHeight = getHeight();
1926     boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
1927
1928     if (lastWidthPartHeight)
1929     {
1930       int widthsAbove = Math.max(0, visibleWidths - 2);
1931       int ypos = wrappedRepeatHeightPx * widthsAbove
1932               + wrappedSpaceAboveAlignment;
1933       int endRes = ranges.getEndRes();
1934       endRes += widthsAbove * viewportWidth;
1935       int startRes = endRes - columns;
1936       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1937               * charWidth;
1938
1939       /*
1940        * white fill first to erase annotations
1941        */
1942       gg.translate(xOffset, 0);
1943       gg.setColor(Color.white);
1944       gg.fillRect(labelWidthWest, ypos,
1945               (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
1946       gg.translate(-xOffset, 0);
1947
1948       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1949     }
1950
1951     /*
1952      * draw newly visible columns in last wrapped width (none if we
1953      * have reached the end of the alignment)
1954      * y-offset for drawing last width is height of widths above,
1955      * plus one gap row
1956      */
1957     int widthsAbove = visibleWidths - 1;
1958     int ypos = wrappedRepeatHeightPx * widthsAbove
1959             + wrappedSpaceAboveAlignment;
1960     int endRes = ranges.getEndRes();
1961     endRes += widthsAbove * viewportWidth;
1962     int startRes = endRes - columns + 1;
1963
1964     /*
1965      * white fill first to erase annotations
1966      */
1967     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1968             * charWidth;
1969     gg.translate(xOffset, 0);
1970     gg.setColor(Color.white);
1971     int width = viewportWidth * charWidth - xOffset;
1972     gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
1973     gg.translate(-xOffset, 0);
1974
1975     gg.setFont(av.getFont());
1976     gg.setColor(Color.black);
1977
1978     if (startRes < ranges.getVisibleAlignmentWidth())
1979     {
1980       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1981     }
1982
1983     /*
1984      * and finally, white fill any space below the visible alignment
1985      */
1986     int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
1987     if (heightBelow > 0)
1988     {
1989       gg.setColor(Color.white);
1990       gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
1991     }
1992   }
1993
1994   /**
1995    * Shifts the visible alignment by the specified number of columns - left if
1996    * negative, right if positive. Copies and moves sequences and annotations (if
1997    * shown). Scales, hidden column markers and any newly visible columns must be
1998    * drawn separately.
1999    * 
2000    * @param positions
2001    */
2002   protected void shiftWrappedAlignment(int positions)
2003   {
2004     if (positions == 0)
2005     {
2006       return;
2007     }
2008     int charWidth = av.getCharWidth();
2009
2010     int canvasHeight = getHeight();
2011     ViewportRanges ranges = av.getRanges();
2012     int viewportWidth = ranges.getViewportWidth();
2013     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
2014             * charWidth;
2015     int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
2016     int xMax = ranges.getVisibleAlignmentWidth();
2017
2018     if (positions > 0)
2019     {
2020       /*
2021        * shift right (after scroll left)
2022        * for each wrapped width (starting with the last), copy (width-positions) 
2023        * columns from the left margin to the right margin, and copy positions 
2024        * columns from the right margin of the row above (if any) to the 
2025        * left margin of the current row
2026        */
2027
2028       /*
2029        * get y-offset of last wrapped width, first row of sequences
2030        */
2031       int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
2032       y += wrappedSpaceAboveAlignment;
2033       int copyFromLeftStart = labelWidthWest;
2034       int copyFromRightStart = copyFromLeftStart + widthToCopy;
2035
2036       while (y >= 0)
2037       {
2038         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
2039                 positions * charWidth, 0);
2040         if (y > 0)
2041         {
2042           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
2043                   positions * charWidth, heightToCopy, -widthToCopy,
2044                   wrappedRepeatHeightPx);
2045         }
2046
2047         y -= wrappedRepeatHeightPx;
2048       }
2049     }
2050     else
2051     {
2052       /*
2053        * shift left (after scroll right)
2054        * for each wrapped width (starting with the first), copy (width-positions) 
2055        * columns from the right margin to the left margin, and copy positions 
2056        * columns from the left margin of the row below (if any) to the 
2057        * right margin of the current row
2058        */
2059       int xpos = av.getRanges().getStartRes();
2060       int y = wrappedSpaceAboveAlignment;
2061       int copyFromRightStart = labelWidthWest - positions * charWidth;
2062
2063       while (y < canvasHeight)
2064       {
2065         gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
2066                 positions * charWidth, 0);
2067         if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
2068                 && (xpos + viewportWidth <= xMax))
2069         {
2070           gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
2071                   * charWidth, heightToCopy, widthToCopy,
2072                   -wrappedRepeatHeightPx);
2073         }
2074
2075         y += wrappedRepeatHeightPx;
2076         xpos += viewportWidth;
2077       }
2078     }
2079   }
2080
2081   
2082   /**
2083    * Redraws any positions in the search results in the visible region of a
2084    * wrapped alignment. Any highlights are drawn depending on the search results
2085    * set on the Viewport, not the <code>results</code> argument. This allows
2086    * this method to be called either to clear highlights (passing the previous
2087    * search results), or to draw new highlights.
2088    * 
2089    * @param results
2090    * @return
2091    */
2092   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
2093   {
2094     if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
2095     {
2096       return false;
2097     }
2098     int charHeight = av.getCharHeight();
2099
2100     boolean matchFound = false;
2101
2102     calculateWrappedGeometry(getWidth(), getHeight());
2103     int wrappedWidth = av.getWrappedWidth();
2104     int wrappedHeight = wrappedRepeatHeightPx;
2105
2106     ViewportRanges ranges = av.getRanges();
2107     int canvasHeight = getHeight();
2108     int repeats = canvasHeight / wrappedHeight;
2109     if (canvasHeight / wrappedHeight > 0)
2110     {
2111       repeats++;
2112     }
2113
2114     int firstVisibleColumn = ranges.getStartRes();
2115     int lastVisibleColumn = ranges.getStartRes() + repeats
2116             * ranges.getViewportWidth() - 1;
2117
2118     AlignmentI alignment = av.getAlignment();
2119     if (av.hasHiddenColumns())
2120     {
2121       firstVisibleColumn = alignment.getHiddenColumns()
2122               .adjustForHiddenColumns(firstVisibleColumn);
2123       lastVisibleColumn = alignment.getHiddenColumns()
2124               .adjustForHiddenColumns(lastVisibleColumn);
2125     }
2126
2127     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
2128
2129     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
2130             .getEndSeq(); seqNo++)
2131     {
2132       SequenceI seq = alignment.getSequenceAt(seqNo);
2133
2134       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
2135               lastVisibleColumn);
2136       if (visibleResults != null)
2137       {
2138         for (int i = 0; i < visibleResults.length - 1; i += 2)
2139         {
2140           int firstMatchedColumn = visibleResults[i];
2141           int lastMatchedColumn = visibleResults[i + 1];
2142           if (firstMatchedColumn <= lastVisibleColumn
2143                   && lastMatchedColumn >= firstVisibleColumn)
2144           {
2145             /*
2146              * found a search results match in the visible region
2147              */
2148             firstMatchedColumn = Math.max(firstMatchedColumn,
2149                     firstVisibleColumn);
2150             lastMatchedColumn = Math.min(lastMatchedColumn,
2151                     lastVisibleColumn);
2152
2153             /*
2154              * draw each mapped position separately (as contiguous positions may
2155              * wrap across lines)
2156              */
2157             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
2158             {
2159               int displayColumn = mappedPos;
2160               if (av.hasHiddenColumns())
2161               {
2162                 displayColumn = alignment.getHiddenColumns()
2163                         .findColumnPosition(displayColumn);
2164               }
2165
2166               /*
2167                * transX: offset from left edge of canvas to residue position
2168                */
2169               int transX = labelWidthWest
2170                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
2171                       * av.getCharWidth();
2172
2173               /*
2174                * transY: offset from top edge of canvas to residue position
2175                */
2176               int transY = gapHeight;
2177               transY += (displayColumn - ranges.getStartRes())
2178                       / wrappedWidth * wrappedHeight;
2179               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
2180
2181               /*
2182                * yOffset is from graphics origin to start of visible region
2183                */
2184               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
2185               if (transY < getHeight())
2186               {
2187                 matchFound = true;
2188                 gg.translate(transX, transY);
2189                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
2190                         yOffset);
2191                 gg.translate(-transX, -transY);
2192               }
2193             }
2194           }
2195         }
2196       }
2197     }
2198   
2199     return matchFound;
2200   }
2201
2202   /**
2203    * Answers the width in pixels of the left scale labels (0 if not shown)
2204    * 
2205    * @return
2206    */
2207   int getLabelWidthWest()
2208   {
2209     return labelWidthWest;
2210   }
2211 }