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