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