Merge branch 'bug/JAL-2811' into bug/JAL-2831
[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       repaint();
341     } finally
342     {
343       fastpainting = false;
344     }
345   }
346
347   @Override
348   public void paintComponent(Graphics g)
349   {
350     super.paintComponent(g);    
351     
352     int charHeight = av.getCharHeight();
353     int charWidth = av.getCharWidth();
354
355     ViewportRanges ranges = av.getRanges();
356
357     int width = getWidth();
358     int height = getHeight();
359
360     width -= (width % charWidth);
361     height -= (height % charHeight);
362
363     // selectImage is the selection group outline image
364     BufferedImage selectImage = drawSelectionGroup(
365             ranges.getStartRes(), ranges.getEndRes(),
366             ranges.getStartSeq(), ranges.getEndSeq());
367
368     BufferedImage cursorImage = drawCursor(ranges.getStartRes(),
369             ranges.getEndRes(), 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, cursorImage);
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, cursorImage);
416       g.drawImage(lcimg, 0, 0, this);
417     }
418   }
419   
420   /**
421    * Draw an alignment panel for printing
422    * 
423    * @param g1
424    *          Graphics object to draw with
425    * @param startRes
426    *          start residue of print area
427    * @param endRes
428    *          end residue of print area
429    * @param startSeq
430    *          start sequence of print area
431    * @param endSeq
432    *          end sequence of print area
433    */
434   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
435           int startSeq, int endSeq)
436   {
437     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
438
439     BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
440             startSeq, endSeq);
441     if (selectImage != null)
442     {
443       ((Graphics2D) g1).setComposite(AlphaComposite
444               .getInstance(AlphaComposite.SRC_OVER));
445       g1.drawImage(selectImage, 0, 0, this);
446     }
447   }
448
449   /**
450    * Draw a wrapped alignment panel for printing
451    * 
452    * @param g
453    *          Graphics object to draw with
454    * @param canvasWidth
455    *          width of drawing area
456    * @param canvasHeight
457    *          height of drawing area
458    * @param startRes
459    *          start residue of print area
460    */
461   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
462           int canvasHeight, int startRes)
463   {
464     SequenceGroup group = av.getSelectionGroup();
465
466     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
467
468     if (group != null)
469     {
470       BufferedImage selectImage = null;
471       try
472       {
473         selectImage = new BufferedImage(canvasWidth, canvasHeight,
474                 BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
475       } catch (OutOfMemoryError er)
476       {
477         System.gc();
478         System.err.println("Print image OutOfMemory Error.\n" + er);
479         new OOMWarning("Creating wrapped alignment image for printing", er);
480       }
481       if (selectImage != null)
482       {
483         Graphics2D g2 = selectImage.createGraphics();
484         setupSelectionGroup(g2, selectImage);
485         drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
486                 startRes);
487
488         g2.setComposite(
489                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
490         g.drawImage(selectImage, 0, 0, this);
491         g2.dispose();
492       }
493     }
494   }
495
496   /*
497    * Make a local image by combining the cached image img
498    * with any selection
499    */
500   private BufferedImage buildLocalImage(BufferedImage selectImage,
501           BufferedImage cursorImage)
502   {
503     // clone the cached image
504     BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
505             img.getType());
506     Graphics2D g2d = lcimg.createGraphics();
507     g2d.drawImage(img, 0, 0, null);
508
509     // overlay selection group on lcimg
510     if (selectImage != null)
511     {
512       g2d.setComposite(
513               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
514       g2d.drawImage(selectImage, 0, 0, this);
515     }
516     // overlay cursor on lcimg
517     if (cursorImage != null)
518     {
519       g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
520       g2d.drawImage(cursorImage, 0, 0, this);
521     }
522     g2d.dispose();
523
524     return lcimg;
525   }
526
527   /*
528    * Set up a buffered image of the correct height and size for the sequence canvas
529    */
530   private BufferedImage setupImage()
531   {
532     BufferedImage lcimg = null;
533
534     int charWidth = av.getCharWidth();
535     int charHeight = av.getCharHeight();
536     
537     int width = getWidth();
538     int height = getHeight();
539
540     width -= (width % charWidth);
541     height -= (height % charHeight);
542
543     if ((width < 1) || (height < 1))
544     {
545       return null;
546     }
547
548     try
549     {
550       lcimg = new BufferedImage(width, height,
551               BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
552     } catch (OutOfMemoryError er)
553     {
554       System.gc();
555       System.err.println(
556               "Group image OutOfMemory Redraw Error.\n" + er);
557       new OOMWarning("Creating alignment image for display", er);
558
559       return null;
560     }
561
562     return lcimg;
563   }
564
565   /**
566    * Returns the visible width of the canvas in residues, after allowing for
567    * East or West scales (if shown)
568    * 
569    * @param canvasWidth
570    *          the width in pixels (possibly including scales)
571    * 
572    * @return
573    */
574   public int getWrappedCanvasWidth(int canvasWidth)
575   {
576     int charWidth = av.getCharWidth();
577
578     FontMetrics fm = getFontMetrics(av.getFont());
579
580     int labelWidth = 0;
581     
582     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
583     {
584       labelWidth = getLabelWidth(fm);
585     }
586
587     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
588
589     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
590
591     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
592   }
593
594   /**
595    * Returns a pixel width sufficient to show the largest sequence coordinate
596    * (end position) in the alignment, calculated as the FontMetrics width of
597    * zeroes "0000000" limited to the number of decimal digits to be shown (3 for
598    * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for
599    * half a character width space on either side.
600    * 
601    * @param fm
602    * @return
603    */
604   protected int getLabelWidth(FontMetrics fm)
605   {
606     /*
607      * find the biggest sequence end position we need to show
608      * (note this is not necessarily the sequence length)
609      */
610     int maxWidth = 0;
611     AlignmentI alignment = av.getAlignment();
612     for (int i = 0; i < alignment.getHeight(); i++)
613     {
614       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
615     }
616
617     int length = 0;
618     for (int i = maxWidth; i > 0; i /= 10)
619     {
620       length++;
621     }
622
623     return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth();
624   }
625
626   /**
627    * Draws as many widths of a wrapped alignment as can fit in the visible
628    * window
629    * 
630    * @param g
631    * @param canvasWidth
632    *          available width in pixels
633    * @param canvasHeight
634    *          available height in pixels
635    * @param startColumn
636    *          the first column (0...) of the alignment to draw
637    */
638   public void drawWrappedPanel(Graphics g, int canvasWidth,
639           int canvasHeight, final int startColumn)
640   {
641     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
642             canvasHeight);
643
644     av.setWrappedWidth(wrappedWidthInResidues);
645
646     ViewportRanges ranges = av.getRanges();
647     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
648
649     /*
650      * draw one width at a time (including any scales or annotation shown),
651      * until we have run out of either alignment or vertical space available
652      */
653     int ypos = wrappedSpaceAboveAlignment;
654     int maxWidth = ranges.getVisibleAlignmentWidth();
655
656     int start = startColumn;
657     int currentWidth = 0;
658     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
659     {
660       int endColumn = Math
661               .min(maxWidth, start + wrappedWidthInResidues - 1);
662       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
663       ypos += wrappedRepeatHeightPx;
664       start += wrappedWidthInResidues;
665       currentWidth++;
666     }
667
668     drawWrappedDecorators(g, startColumn);
669   }
670
671   /**
672    * Calculates and saves values needed when rendering a wrapped alignment.
673    * These depend on many factors, including
674    * <ul>
675    * <li>canvas width and height</li>
676    * <li>number of visible sequences, and height of annotations if shown</li>
677    * <li>font and character width</li>
678    * <li>whether scales are shown left, right or above the alignment</li>
679    * </ul>
680    * 
681    * @param canvasWidth
682    * @param canvasHeight
683    * @return the number of residue columns in each width
684    */
685   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
686   {
687     int charHeight = av.getCharHeight();
688
689     /*
690      * vertical space in pixels between wrapped widths of alignment
691      * - one character height, or two if scale above is drawn
692      */
693     wrappedSpaceAboveAlignment = charHeight
694             * (av.getScaleAboveWrapped() ? 2 : 1);
695
696     /*
697      * height in pixels of the wrapped widths
698      */
699     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
700     // add sequences
701     wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
702             * charHeight;
703     // add annotations panel height if shown
704     wrappedRepeatHeightPx += getAnnotationHeight();
705
706     /*
707      * number of visible widths (the last one may be part height),
708      * ensuring a part height includes at least one sequence
709      */
710     ViewportRanges ranges = av.getRanges();
711     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
712     int remainder = canvasHeight % wrappedRepeatHeightPx;
713     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
714     {
715       wrappedVisibleWidths++;
716     }
717
718     /*
719      * compute width in residues; this also sets East and West label widths
720      */
721     int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
722
723     /*
724      *  limit visibleWidths to not exceed width of alignment
725      */
726     int xMax = ranges.getVisibleAlignmentWidth();
727     int startToEnd = xMax - ranges.getStartRes();
728     int maxWidths = startToEnd / wrappedWidthInResidues;
729     if (startToEnd % wrappedWidthInResidues > 0)
730     {
731       maxWidths++;
732     }
733     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
734
735     return wrappedWidthInResidues;
736   }
737
738   /**
739    * Draws one width of a wrapped alignment, including sequences and
740    * annnotations, if shown, but not scales or hidden column markers
741    * 
742    * @param g
743    * @param ypos
744    * @param startColumn
745    * @param endColumn
746    * @param canvasHeight
747    */
748   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
749           int endColumn, int canvasHeight)
750   {
751     ViewportRanges ranges = av.getRanges();
752     int viewportWidth = ranges.getViewportWidth();
753
754     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
755
756     /*
757      * move right before drawing by the width of the scale left (if any)
758      * plus column offset from left margin (usually zero, but may be non-zero
759      * when fast painting is drawing just a few columns)
760      */
761     int charWidth = av.getCharWidth();
762     int xOffset = labelWidthWest
763             + ((startColumn - ranges.getStartRes()) % viewportWidth)
764             * charWidth;
765     g.translate(xOffset, 0);
766
767     // When printing we have an extra clipped region,
768     // the Printable page which we need to account for here
769     Shape clip = g.getClip();
770
771     if (clip == null)
772     {
773       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
774     }
775     else
776     {
777       g.setClip(0, (int) clip.getBounds().getY(),
778               viewportWidth * charWidth, (int) clip.getBounds().getHeight());
779     }
780
781     /*
782      * white fill the region to be drawn (so incremental fast paint doesn't
783      * scribble over an existing image)
784      */
785     gg.setColor(Color.white);
786     gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
787             wrappedRepeatHeightPx);
788
789     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
790             ypos);
791
792     int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
793
794     if (av.isShowAnnotation())
795     {
796       g.translate(0, cHeight + ypos + 3);
797       if (annotations == null)
798       {
799         annotations = new AnnotationPanel(av);
800       }
801
802       annotations.renderer.drawComponent(annotations, av, g, -1,
803               startColumn, endx + 1);
804       g.translate(0, -cHeight - ypos - 3);
805     }
806     g.setClip(clip);
807     g.translate(-xOffset, 0);
808   }
809
810   /**
811    * Draws scales left, right and above (if shown), and any hidden column
812    * markers, on all widths of the wrapped alignment
813    * 
814    * @param g
815    * @param startColumn
816    */
817   protected void drawWrappedDecorators(Graphics g, final int startColumn)
818   {
819     int charWidth = av.getCharWidth();
820
821     g.setFont(av.getFont());
822     g.setColor(Color.black);
823
824     int ypos = wrappedSpaceAboveAlignment;
825     ViewportRanges ranges = av.getRanges();
826     int viewportWidth = ranges.getViewportWidth();
827     int maxWidth = ranges.getVisibleAlignmentWidth();
828     int widthsDrawn = 0;
829     int startCol = startColumn;
830
831     while (widthsDrawn < wrappedVisibleWidths)
832     {
833       int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1);
834
835       if (av.getScaleLeftWrapped())
836       {
837         drawVerticalScale(g, startCol, endColumn - 1, ypos, true);
838       }
839
840       if (av.getScaleRightWrapped())
841       {
842         int x = labelWidthWest + viewportWidth * charWidth;
843         g.translate(x, 0);
844         drawVerticalScale(g, startCol, endColumn, ypos, false);
845         g.translate(-x, 0);
846       }
847
848       /*
849        * white fill region of scale above and hidden column markers
850        * (to support incremental fast paint of image)
851        */
852       g.translate(labelWidthWest, 0);
853       g.setColor(Color.white);
854       g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
855               * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
856       g.setColor(Color.black);
857       g.translate(-labelWidthWest, 0);
858
859       g.translate(labelWidthWest, 0);
860
861       if (av.getScaleAboveWrapped())
862       {
863         drawNorthScale(g, startCol, endColumn, ypos);
864       }
865
866       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
867       {
868         drawHiddenColumnMarkers(g, ypos, startCol, endColumn);
869       }
870
871       g.translate(-labelWidthWest, 0);
872
873       ypos += wrappedRepeatHeightPx;
874       startCol += viewportWidth;
875       widthsDrawn++;
876     }
877   }
878
879   /**
880    * Draws markers (triangles) above hidden column positions between startColumn
881    * and endColumn.
882    * 
883    * @param g
884    * @param ypos
885    * @param startColumn
886    * @param endColumn
887    */
888   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
889           int startColumn, int endColumn)
890   {
891     int charHeight = av.getCharHeight();
892     int charWidth = av.getCharWidth();
893
894     g.setColor(Color.blue);
895     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
896     List<Integer> positions = hidden.findHiddenRegionPositions();
897     for (int pos : positions)
898     {
899       int res = pos - startColumn;
900
901       if (res < 0 || res > endColumn - startColumn + 1)
902       {
903         continue;
904       }
905
906       /*
907        * draw a downward-pointing triangle at the hidden columns location
908        * (before the following visible column)
909        */
910       int xMiddle = res * charWidth;
911       int[] xPoints = new int[] { xMiddle - charHeight / 4,
912           xMiddle + charHeight / 4, xMiddle };
913       int yTop = ypos - (charHeight / 2);
914       int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
915       g.fillPolygon(xPoints, yPoints, 3);
916     }
917   }
918
919   /*
920    * Draw a selection group over a wrapped alignment
921    */
922   private void drawWrappedSelection(Graphics2D g, SequenceGroup group,
923           int canvasWidth,
924           int canvasHeight, int startRes)
925   {
926     int charHeight = av.getCharHeight();
927     int charWidth = av.getCharWidth();
928       
929     // height gap above each panel
930     int hgap = charHeight;
931     if (av.getScaleAboveWrapped())
932     {
933       hgap += charHeight;
934     }
935
936     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
937             / charWidth;
938     int cHeight = av.getAlignment().getHeight() * charHeight;
939
940     int startx = startRes;
941     int endx;
942     int ypos = hgap; // vertical offset
943     int maxwidth = av.getAlignment().getWidth();
944
945     if (av.hasHiddenColumns())
946     {
947       maxwidth = av.getAlignment().getHiddenColumns()
948               .findColumnPosition(maxwidth);
949     }
950
951     // chop the wrapped alignment extent up into panel-sized blocks and treat
952     // each block as if it were a block from an unwrapped alignment
953     while ((ypos <= canvasHeight) && (startx < maxwidth))
954     {
955       // set end value to be start + width, or maxwidth, whichever is smaller
956       endx = startx + cWidth - 1;
957
958       if (endx > maxwidth)
959       {
960         endx = maxwidth;
961       }
962
963       g.translate(labelWidthWest, 0);
964
965       drawUnwrappedSelection(g, group, startx, endx, 0,
966               av.getAlignment().getHeight() - 1,
967               ypos);
968
969       g.translate(-labelWidthWest, 0);
970
971       // update vertical offset
972       ypos += cHeight + getAnnotationHeight() + hgap;
973
974       // update horizontal offset
975       startx += cWidth;
976     }
977   }
978
979   int getAnnotationHeight()
980   {
981     if (!av.isShowAnnotation())
982     {
983       return 0;
984     }
985
986     if (annotations == null)
987     {
988       annotations = new AnnotationPanel(av);
989     }
990
991     return annotations.adjustPanelHeight();
992   }
993
994   /**
995    * Draws the visible region of the alignment on the graphics context. If there
996    * are hidden column markers in the visible region, then each sub-region
997    * between the markers is drawn separately, followed by the hidden column
998    * marker.
999    * 
1000    * @param g1
1001    *          the graphics context, positioned at the first residue to be drawn
1002    * @param startRes
1003    *          offset of the first column to draw (0..)
1004    * @param endRes
1005    *          offset of the last column to draw (0..)
1006    * @param startSeq
1007    *          offset of the first sequence to draw (0..)
1008    * @param endSeq
1009    *          offset of the last sequence to draw (0..)
1010    * @param yOffset
1011    *          vertical offset at which to draw (for wrapped alignments)
1012    */
1013   public void drawPanel(Graphics g1, final int startRes, final int endRes,
1014           final int startSeq, final int endSeq, final int yOffset)
1015   {
1016     int charHeight = av.getCharHeight();
1017     int charWidth = av.getCharWidth();
1018
1019     if (!av.hasHiddenColumns())
1020     {
1021       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
1022     }
1023     else
1024     {
1025       int screenY = 0;
1026       final int screenYMax = endRes - startRes;
1027       int blockStart = startRes;
1028       int blockEnd = endRes;
1029
1030       for (int[] region : av.getAlignment().getHiddenColumns()
1031               .getHiddenColumnsCopy())
1032       {
1033         int hideStart = region[0];
1034         int hideEnd = region[1];
1035
1036         if (hideStart <= blockStart)
1037         {
1038           blockStart += (hideEnd - hideStart) + 1;
1039           continue;
1040         }
1041
1042         /*
1043          * draw up to just before the next hidden region, or the end of
1044          * the visible region, whichever comes first
1045          */
1046         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
1047                 - screenY);
1048
1049         g1.translate(screenY * charWidth, 0);
1050
1051         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1052
1053         /*
1054          * draw the downline of the hidden column marker (ScalePanel draws the
1055          * triangle on top) if we reached it
1056          */
1057         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
1058         {
1059           g1.setColor(Color.blue);
1060
1061           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
1062                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
1063                   (endSeq - startSeq + 1) * charHeight + yOffset);
1064         }
1065
1066         g1.translate(-screenY * charWidth, 0);
1067         screenY += blockEnd - blockStart + 1;
1068         blockStart = hideEnd + 1;
1069
1070         if (screenY > screenYMax)
1071         {
1072           // already rendered last block
1073           return;
1074         }
1075       }
1076
1077       if (screenY <= screenYMax)
1078       {
1079         // remaining visible region to render
1080         blockEnd = blockStart + screenYMax - screenY;
1081         g1.translate(screenY * charWidth, 0);
1082         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1083
1084         g1.translate(-screenY * charWidth, 0);
1085       }
1086     }
1087
1088   }
1089
1090   /**
1091    * Draws a region of the visible alignment
1092    * 
1093    * @param g1
1094    * @param startRes
1095    *          offset of the first column in the visible region (0..)
1096    * @param endRes
1097    *          offset of the last column in the visible region (0..)
1098    * @param startSeq
1099    *          offset of the first sequence in the visible region (0..)
1100    * @param endSeq
1101    *          offset of the last sequence in the visible region (0..)
1102    * @param yOffset
1103    *          vertical offset at which to draw (for wrapped alignments)
1104    */
1105   private void draw(Graphics g, int startRes, int endRes, int startSeq,
1106           int endSeq, int offset)
1107   {
1108     int charHeight = av.getCharHeight();
1109     int charWidth = av.getCharWidth();
1110
1111     g.setFont(av.getFont());
1112     seqRdr.prepare(g, av.isRenderGaps());
1113
1114     SequenceI nextSeq;
1115
1116     // / First draw the sequences
1117     // ///////////////////////////
1118     for (int i = startSeq; i <= endSeq; i++)
1119     {
1120       nextSeq = av.getAlignment().getSequenceAt(i);
1121       if (nextSeq == null)
1122       {
1123         // occasionally, a race condition occurs such that the alignment row is
1124         // empty
1125         continue;
1126       }
1127       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
1128               startRes, endRes, offset + ((i - startSeq) * charHeight));
1129
1130       if (av.isShowSequenceFeatures())
1131       {
1132         fr.drawSequence(g, nextSeq, startRes, endRes,
1133                 offset + ((i - startSeq) * charHeight), false);
1134       }
1135
1136       /*
1137        * highlight search Results once sequence has been drawn
1138        */
1139       if (av.hasSearchResults())
1140       {
1141         SearchResultsI searchResults = av.getSearchResults();
1142         int[] visibleResults = searchResults.getResults(nextSeq,
1143                 startRes, endRes);
1144         if (visibleResults != null)
1145         {
1146           for (int r = 0; r < visibleResults.length; r += 2)
1147           {
1148             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
1149                     visibleResults[r + 1], (visibleResults[r] - startRes)
1150                             * charWidth, offset
1151                             + ((i - startSeq) * charHeight));
1152           }
1153         }
1154       }
1155
1156       /*      if (av.cursorMode && cursorY == i && cursorX >= startRes
1157               && cursorX <= endRes)
1158       {
1159         seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
1160                 offset + ((i - startSeq) * charHeight));
1161       }*/
1162     }
1163
1164     if (av.getSelectionGroup() != null
1165             || av.getAlignment().getGroups().size() > 0)
1166     {
1167       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1168     }
1169
1170   }
1171
1172   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1173           int startSeq, int endSeq, int offset)
1174   {
1175     Graphics2D g = (Graphics2D) g1;
1176     //
1177     // ///////////////////////////////////
1178     // Now outline any areas if necessary
1179     // ///////////////////////////////////
1180
1181     SequenceGroup group = null;
1182     int groupIndex = -1;
1183
1184     if (av.getAlignment().getGroups().size() > 0)
1185     {
1186       group = av.getAlignment().getGroups().get(0);
1187       groupIndex = 0;
1188     }
1189
1190     if (group != null)
1191     {
1192       g.setStroke(new BasicStroke());
1193       g.setColor(group.getOutlineColour());
1194       
1195       do
1196       {
1197         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1198                 endSeq, offset);
1199
1200         groupIndex++;
1201
1202         g.setStroke(new BasicStroke());
1203
1204         if (groupIndex >= av.getAlignment().getGroups().size())
1205         {
1206           break;
1207         }
1208
1209         group = av.getAlignment().getGroups().get(groupIndex);
1210
1211       } while (groupIndex < av.getAlignment().getGroups().size());
1212
1213     }
1214
1215   }
1216
1217
1218   /*
1219    * Draw the selection group as a separate image and overlay
1220    */
1221   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1222           int startSeq, int endSeq)
1223   {
1224     // get a new image of the correct size
1225     BufferedImage selectionImage = setupImage();
1226
1227     if (selectionImage == null)
1228     {
1229       return null;
1230     }
1231
1232     SequenceGroup group = av.getSelectionGroup();
1233     if (group == null)
1234     {
1235       // nothing to draw
1236       return null;
1237     }
1238
1239     // set up drawing colour
1240     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1241
1242     setupSelectionGroup(g, selectionImage);
1243
1244     if (!av.getWrapAlignment())
1245     {
1246       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1247               0);
1248     }
1249     else
1250     {
1251       drawWrappedSelection(g, group, getWidth(), getHeight(),
1252               av.getRanges().getStartRes());
1253     }
1254
1255     g.dispose();
1256     return selectionImage;
1257   }
1258
1259   /**
1260    * Draw the cursor as a separate image and overlay
1261    * 
1262    * @param startRes
1263    *          start residue of area to draw cursor in
1264    * @param endRes
1265    *          end residue of area to draw cursor in
1266    * @param startSeq
1267    *          start sequence of area to draw cursor in
1268    * @param endSeq
1269    *          end sequence of are to draw cursor in
1270    * @return a transparent image of the same size as the sequence canvas, with
1271    *         the cursor drawn on it, if any
1272    */
1273   private BufferedImage drawCursor(int startRes, int endRes, int startSeq,
1274           int endSeq)
1275   {
1276     // define our cursor image
1277     BufferedImage cursorImage = null;
1278
1279     // don't do work unless we have to
1280     if (av.cursorMode && cursorY >= startSeq && cursorY <= endSeq)
1281     {
1282       int yoffset = 0;
1283       int xoffset = 0;
1284       int startx = startRes;
1285       int endx = endRes;
1286       if (av.getWrapAlignment())
1287       {
1288         // work out the correct offsets for the cursor
1289         int charHeight = av.getCharHeight();
1290         int charWidth = av.getCharWidth();
1291         int canvasWidth = getWidth();
1292         int canvasHeight = getHeight();
1293
1294         // height gap above each panel
1295         int hgap = charHeight;
1296         if (av.getScaleAboveWrapped())
1297         {
1298           hgap += charHeight;
1299         }
1300
1301         int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
1302                 / charWidth;
1303         int cHeight = av.getAlignment().getHeight() * charHeight;
1304
1305         endx = startx + cWidth - 1;
1306         int ypos = hgap; // vertical offset
1307
1308         // iterate down the wrapped panels
1309         while ((ypos <= canvasHeight) && (endx < cursorX))
1310         {
1311           // update vertical offset
1312           ypos += cHeight + getAnnotationHeight() + hgap;
1313
1314           // update horizontal offset
1315           startx += cWidth;
1316           endx = startx + cWidth - 1;
1317         }
1318         yoffset = ypos;
1319         xoffset = labelWidthWest;
1320       }
1321
1322       // now check if cursor is within range for x values
1323       if (cursorX >= startx && cursorX <= endx)
1324       {
1325         // get a new image of the correct size
1326         cursorImage = setupImage();
1327         Graphics2D g = (Graphics2D) cursorImage.getGraphics();
1328
1329         // get the character the cursor is drawn at
1330         SequenceI seq = av.getAlignment().getSequenceAt(cursorY);
1331         char s = seq.getCharAt(cursorX);
1332
1333         seqRdr.drawCursor(g, s,
1334                 xoffset + (cursorX - startx) * av.getCharWidth(),
1335                 yoffset + (cursorY - startSeq) * av.getCharHeight());
1336         g.dispose();
1337       }
1338     }
1339
1340     return cursorImage;
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           
1441     int visWidth = (endRes - startRes + 1) * charWidth;
1442
1443     int oldY = -1;
1444     int i = 0;
1445     boolean inGroup = false;
1446     int top = -1;
1447     int bottom = -1;
1448
1449     int sx = -1;
1450     int sy = -1;
1451     int xwidth = -1;
1452
1453     for (i = startSeq; i <= endSeq; i++)
1454     {
1455       // position of start residue of group relative to startRes, in pixels
1456       sx = (group.getStartRes() - startRes) * charWidth;
1457
1458       // width of group in pixels
1459       xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1460               - 1;
1461
1462       sy = verticalOffset + (i - startSeq) * charHeight;
1463
1464       if (sx + xwidth < 0 || sx > visWidth)
1465       {
1466         continue;
1467       }
1468
1469       if ((sx <= (endRes - startRes) * charWidth)
1470               && group.getSequences(null)
1471                       .contains(av.getAlignment().getSequenceAt(i)))
1472       {
1473         if ((bottom == -1) && !group.getSequences(null)
1474                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1475         {
1476           bottom = sy + charHeight;
1477         }
1478
1479         if (!inGroup)
1480         {
1481           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1482                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1483           {
1484             top = sy;
1485           }
1486
1487           oldY = sy;
1488           inGroup = true;
1489         }
1490       }
1491       else
1492       {
1493         if (inGroup)
1494         {
1495           // if start position is visible, draw vertical line to left of
1496           // group
1497           if (sx >= 0 && sx < visWidth)
1498           {
1499             g.drawLine(sx, oldY, sx, sy);
1500           }
1501
1502           // if end position is visible, draw vertical line to right of
1503           // group
1504           if (sx + xwidth < visWidth)
1505           {
1506             g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1507           }
1508
1509           if (sx < 0)
1510           {
1511             xwidth += sx;
1512             sx = 0;
1513           }
1514
1515           // don't let width extend beyond current block, or group extent
1516           // fixes JAL-2672
1517           if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1518           {
1519             xwidth = (endRes - startRes + 1) * charWidth - sx;
1520           }
1521           
1522           // draw horizontal line at top of group
1523           if (top != -1)
1524           {
1525             g.drawLine(sx, top, sx + xwidth, top);
1526             top = -1;
1527           }
1528
1529           // draw horizontal line at bottom of group
1530           if (bottom != -1)
1531           {
1532             g.drawLine(sx, bottom, sx + xwidth, bottom);
1533             bottom = -1;
1534           }
1535
1536           inGroup = false;
1537         }
1538       }
1539     }
1540
1541     if (inGroup)
1542     {
1543       sy = verticalOffset + ((i - startSeq) * charHeight);
1544       if (sx >= 0 && sx < visWidth)
1545       {
1546         g.drawLine(sx, oldY, sx, sy);
1547       }
1548
1549       if (sx + xwidth < visWidth)
1550       {
1551         g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1552       }
1553
1554       if (sx < 0)
1555       {
1556         xwidth += sx;
1557         sx = 0;
1558       }
1559
1560       if (sx + xwidth > visWidth)
1561       {
1562         xwidth = visWidth;
1563       }
1564       else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1565       {
1566         xwidth = (endRes - startRes + 1) * charWidth;
1567       }
1568
1569       if (top != -1)
1570       {
1571         g.drawLine(sx, top, sx + xwidth, top);
1572         top = -1;
1573       }
1574
1575       if (bottom != -1)
1576       {
1577         g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
1578         bottom = -1;
1579       }
1580
1581       inGroup = false;
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)
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   /**
1833    * Does a minimal update of the image for a scroll movement. This method
1834    * handles scroll movements of up to one width of the wrapped alignment (one
1835    * click in the vertical scrollbar). Larger movements (for example after a
1836    * scroll to highlight a mapped position) trigger a full redraw instead.
1837    * 
1838    * @param scrollX
1839    *          number of positions scrolled (right if positive, left if negative)
1840    */
1841   protected void fastPaintWrapped(int scrollX)
1842   {
1843     ViewportRanges ranges = av.getRanges();
1844
1845     if (Math.abs(scrollX) > ranges.getViewportWidth())
1846     {
1847       /*
1848        * shift of more than one view width is 
1849        * overcomplicated to handle in this method
1850        */
1851       fastPaint = false;
1852       repaint();
1853       return;
1854     }
1855
1856     if (fastpainting || gg == null)
1857     {
1858       return;
1859     }
1860
1861     fastPaint = true;
1862     fastpainting = true;
1863
1864     try
1865     {
1866       calculateWrappedGeometry(getWidth(), getHeight());
1867
1868       /*
1869        * relocate the regions of the alignment that are still visible
1870        */
1871       shiftWrappedAlignment(-scrollX);
1872
1873       /*
1874        * add new columns (sequence, annotation)
1875        * - at top left if scrollX < 0 
1876        * - at right of last two widths if scrollX > 0
1877        */
1878       if (scrollX < 0)
1879       {
1880         int startRes = ranges.getStartRes();
1881         drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
1882                 - scrollX - 1, getHeight());
1883       }
1884       else
1885       {
1886         fastPaintWrappedAddRight(scrollX);
1887       }
1888
1889       /*
1890        * draw all scales (if  shown) and hidden column markers
1891        */
1892       drawWrappedDecorators(gg, ranges.getStartRes());
1893
1894       repaint();
1895     } finally
1896     {
1897       fastpainting = false;
1898     }
1899   }
1900
1901   /**
1902    * Draws the specified number of columns at the 'end' (bottom right) of a
1903    * wrapped alignment view, including sequences and annotations if shown, but
1904    * not scales. Also draws the same number of columns at the right hand end of
1905    * the second last width shown, if the last width is not full height (so
1906    * cannot simply be copied from the graphics image).
1907    * 
1908    * @param columns
1909    */
1910   protected void fastPaintWrappedAddRight(int columns)
1911   {
1912     if (columns == 0)
1913     {
1914       return;
1915     }
1916
1917     ViewportRanges ranges = av.getRanges();
1918     int viewportWidth = ranges.getViewportWidth();
1919     int charWidth = av.getCharWidth();
1920
1921     /**
1922      * draw full height alignment in the second last row, last columns, if the
1923      * last row was not full height
1924      */
1925     int visibleWidths = wrappedVisibleWidths;
1926     int canvasHeight = getHeight();
1927     boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
1928
1929     if (lastWidthPartHeight)
1930     {
1931       int widthsAbove = Math.max(0, visibleWidths - 2);
1932       int ypos = wrappedRepeatHeightPx * widthsAbove
1933               + wrappedSpaceAboveAlignment;
1934       int endRes = ranges.getEndRes();
1935       endRes += widthsAbove * viewportWidth;
1936       int startRes = endRes - columns;
1937       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1938               * charWidth;
1939
1940       /*
1941        * white fill first to erase annotations
1942        */
1943       gg.translate(xOffset, 0);
1944       gg.setColor(Color.white);
1945       gg.fillRect(labelWidthWest, ypos,
1946               (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
1947       gg.translate(-xOffset, 0);
1948
1949       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1950     }
1951
1952     /*
1953      * draw newly visible columns in last wrapped width (none if we
1954      * have reached the end of the alignment)
1955      * y-offset for drawing last width is height of widths above,
1956      * plus one gap row
1957      */
1958     int widthsAbove = visibleWidths - 1;
1959     int ypos = wrappedRepeatHeightPx * widthsAbove
1960             + wrappedSpaceAboveAlignment;
1961     int endRes = ranges.getEndRes();
1962     endRes += widthsAbove * viewportWidth;
1963     int startRes = endRes - columns + 1;
1964
1965     /*
1966      * white fill first to erase annotations
1967      */
1968     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1969             * charWidth;
1970     gg.translate(xOffset, 0);
1971     gg.setColor(Color.white);
1972     int width = viewportWidth * charWidth - xOffset;
1973     gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
1974     gg.translate(-xOffset, 0);
1975
1976     gg.setFont(av.getFont());
1977     gg.setColor(Color.black);
1978
1979     if (startRes < ranges.getVisibleAlignmentWidth())
1980     {
1981       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1982     }
1983
1984     /*
1985      * and finally, white fill any space below the visible alignment
1986      */
1987     int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
1988     if (heightBelow > 0)
1989     {
1990       gg.setColor(Color.white);
1991       gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
1992     }
1993   }
1994
1995   /**
1996    * Shifts the visible alignment by the specified number of columns - left if
1997    * negative, right if positive. Copies and moves sequences and annotations (if
1998    * shown). Scales, hidden column markers and any newly visible columns must be
1999    * drawn separately.
2000    * 
2001    * @param positions
2002    */
2003   protected void shiftWrappedAlignment(int positions)
2004   {
2005     if (positions == 0)
2006     {
2007       return;
2008     }
2009     int charWidth = av.getCharWidth();
2010
2011     int canvasHeight = getHeight();
2012     ViewportRanges ranges = av.getRanges();
2013     int viewportWidth = ranges.getViewportWidth();
2014     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
2015             * charWidth;
2016     int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
2017     int xMax = ranges.getVisibleAlignmentWidth();
2018
2019     if (positions > 0)
2020     {
2021       /*
2022        * shift right (after scroll left)
2023        * for each wrapped width (starting with the last), copy (width-positions) 
2024        * columns from the left margin to the right margin, and copy positions 
2025        * columns from the right margin of the row above (if any) to the 
2026        * left margin of the current row
2027        */
2028
2029       /*
2030        * get y-offset of last wrapped width, first row of sequences
2031        */
2032       int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
2033       y += wrappedSpaceAboveAlignment;
2034       int copyFromLeftStart = labelWidthWest;
2035       int copyFromRightStart = copyFromLeftStart + widthToCopy;
2036
2037       while (y >= 0)
2038       {
2039         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
2040                 positions * charWidth, 0);
2041         if (y > 0)
2042         {
2043           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
2044                   positions * charWidth, heightToCopy, -widthToCopy,
2045                   wrappedRepeatHeightPx);
2046         }
2047
2048         y -= wrappedRepeatHeightPx;
2049       }
2050     }
2051     else
2052     {
2053       /*
2054        * shift left (after scroll right)
2055        * for each wrapped width (starting with the first), copy (width-positions) 
2056        * columns from the right margin to the left margin, and copy positions 
2057        * columns from the left margin of the row below (if any) to the 
2058        * right margin of the current row
2059        */
2060       int xpos = av.getRanges().getStartRes();
2061       int y = wrappedSpaceAboveAlignment;
2062       int copyFromRightStart = labelWidthWest - positions * charWidth;
2063
2064       while (y < canvasHeight)
2065       {
2066         gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
2067                 positions * charWidth, 0);
2068         if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
2069                 && (xpos + viewportWidth <= xMax))
2070         {
2071           gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
2072                   * charWidth, heightToCopy, widthToCopy,
2073                   -wrappedRepeatHeightPx);
2074         }
2075
2076         y += wrappedRepeatHeightPx;
2077         xpos += viewportWidth;
2078       }
2079     }
2080   }
2081
2082   
2083   /**
2084    * Redraws any positions in the search results in the visible region of a
2085    * wrapped alignment. Any highlights are drawn depending on the search results
2086    * set on the Viewport, not the <code>results</code> argument. This allows
2087    * this method to be called either to clear highlights (passing the previous
2088    * search results), or to draw new highlights.
2089    * 
2090    * @param results
2091    * @return
2092    */
2093   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
2094   {
2095     if (results == null)
2096     {
2097       return false;
2098     }
2099     int charHeight = av.getCharHeight();
2100
2101     boolean matchFound = false;
2102
2103     calculateWrappedGeometry(getWidth(), getHeight());
2104     int wrappedWidth = av.getWrappedWidth();
2105     int wrappedHeight = wrappedRepeatHeightPx;
2106
2107     ViewportRanges ranges = av.getRanges();
2108     int canvasHeight = getHeight();
2109     int repeats = canvasHeight / wrappedHeight;
2110     if (canvasHeight / wrappedHeight > 0)
2111     {
2112       repeats++;
2113     }
2114
2115     int firstVisibleColumn = ranges.getStartRes();
2116     int lastVisibleColumn = ranges.getStartRes() + repeats
2117             * ranges.getViewportWidth() - 1;
2118
2119     AlignmentI alignment = av.getAlignment();
2120     if (av.hasHiddenColumns())
2121     {
2122       firstVisibleColumn = alignment.getHiddenColumns()
2123               .adjustForHiddenColumns(firstVisibleColumn);
2124       lastVisibleColumn = alignment.getHiddenColumns()
2125               .adjustForHiddenColumns(lastVisibleColumn);
2126     }
2127
2128     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
2129
2130     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
2131             .getEndSeq(); seqNo++)
2132     {
2133       SequenceI seq = alignment.getSequenceAt(seqNo);
2134
2135       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
2136               lastVisibleColumn);
2137       if (visibleResults != null)
2138       {
2139         for (int i = 0; i < visibleResults.length - 1; i += 2)
2140         {
2141           int firstMatchedColumn = visibleResults[i];
2142           int lastMatchedColumn = visibleResults[i + 1];
2143           if (firstMatchedColumn <= lastVisibleColumn
2144                   && lastMatchedColumn >= firstVisibleColumn)
2145           {
2146             /*
2147              * found a search results match in the visible region
2148              */
2149             firstMatchedColumn = Math.max(firstMatchedColumn,
2150                     firstVisibleColumn);
2151             lastMatchedColumn = Math.min(lastMatchedColumn,
2152                     lastVisibleColumn);
2153
2154             /*
2155              * draw each mapped position separately (as contiguous positions may
2156              * wrap across lines)
2157              */
2158             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
2159             {
2160               int displayColumn = mappedPos;
2161               if (av.hasHiddenColumns())
2162               {
2163                 displayColumn = alignment.getHiddenColumns()
2164                         .findColumnPosition(displayColumn);
2165               }
2166
2167               /*
2168                * transX: offset from left edge of canvas to residue position
2169                */
2170               int transX = labelWidthWest
2171                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
2172                       * av.getCharWidth();
2173
2174               /*
2175                * transY: offset from top edge of canvas to residue position
2176                */
2177               int transY = gapHeight;
2178               transY += (displayColumn - ranges.getStartRes())
2179                       / wrappedWidth * wrappedHeight;
2180               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
2181
2182               /*
2183                * yOffset is from graphics origin to start of visible region
2184                */
2185               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
2186               if (transY < getHeight())
2187               {
2188                 matchFound = true;
2189                 gg.translate(transX, transY);
2190                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
2191                         yOffset);
2192                 gg.translate(-transX, -transY);
2193               }
2194             }
2195           }
2196         }
2197       }
2198     }
2199   
2200     return matchFound;
2201   }
2202
2203   /**
2204    * Answers the width in pixels of the left scale labels (0 if not shown)
2205    * 
2206    * @return
2207    */
2208   int getLabelWidthWest()
2209   {
2210     return labelWidthWest;
2211   }
2212 }