JAL-2609attemptedmerge tidy
[jalview.git] / src / jalview / gui / SeqCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.ScaleRenderer;
29 import jalview.renderer.ScaleRenderer.ScaleMark;
30 import jalview.util.Comparison;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.AlphaComposite;
35 import java.awt.BasicStroke;
36 import java.awt.BorderLayout;
37 import java.awt.Color;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Graphics2D;
41 import java.awt.RenderingHints;
42 import java.awt.Shape;
43 import java.awt.image.BufferedImage;
44 import java.beans.PropertyChangeEvent;
45 import java.util.List;
46
47 import javax.swing.JComponent;
48
49 /**
50  * 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;
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);
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;
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   /**
263    * Does a fast paint of an alignment in response to a scroll. Most of the
264    * visible region is simply copied and shifted, and then any newly visible
265    * columns or rows are drawn. The scroll may be horizontal or vertical, but
266    * not both at once. Scrolling may be the result of
267    * <ul>
268    * <li>dragging a scroll bar</li>
269    * <li>clicking in the scroll bar</li>
270    * <li>scrolling by trackpad, middle mouse button, or other device</li>
271    * <li>by moving the box in the Overview window</li>
272    * <li>programmatically to make a highlighted position visible</li>
273    * </ul>
274    * 
275    * @param horizontal
276    *          columns to shift right (positive) or left (negative)
277    * @param vertical
278    *          rows to shift down (positive) or up (negative)
279    */
280   public void fastPaint(int horizontal, int vertical)
281   {
282     if (fastpainting || gg == null || img == null)
283     {
284       return;
285     }
286     fastpainting = true;
287     fastPaint = true;
288
289     try
290     {
291       int charHeight = av.getCharHeight();
292       int charWidth = av.getCharWidth();
293     
294       ViewportRanges ranges = av.getRanges();
295       int startRes = ranges.getStartRes();
296       int endRes = ranges.getEndRes();
297       int startSeq = ranges.getStartSeq();
298       int endSeq = ranges.getEndSeq();
299       int transX = 0;
300       int transY = 0;
301
302     gg.copyArea(horizontal * charWidth, vertical * charHeight,
303             img.getWidth(), img.getHeight(), -horizontal * charWidth,
304             -vertical * charHeight);
305
306     if (horizontal > 0) // scrollbar pulled right, image to the left
307     {
308       transX = (endRes - startRes - horizontal) * charWidth;
309       startRes = endRes - horizontal;
310     }
311     else if (horizontal < 0)
312     {
313       endRes = startRes - horizontal;
314     }
315     else 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   /**
420    * Draw an alignment panel for printing
421    * 
422    * @param g1
423    *          Graphics object to draw with
424    * @param startRes
425    *          start residue of print area
426    * @param endRes
427    *          end residue of print area
428    * @param startSeq
429    *          start sequence of print area
430    * @param endSeq
431    *          end sequence of print area
432    */
433   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
434           int startSeq, int endSeq)
435   {
436     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
437
438     BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
439             startSeq, endSeq);
440     if (selectImage != null)
441     {
442       ((Graphics2D) g1).setComposite(AlphaComposite
443               .getInstance(AlphaComposite.SRC_OVER));
444       g1.drawImage(selectImage, 0, 0, this);
445     }
446   }
447
448   /**
449    * Draw a wrapped alignment panel for printing
450    * 
451    * @param g
452    *          Graphics object to draw with
453    * @param canvasWidth
454    *          width of drawing area
455    * @param canvasHeight
456    *          height of drawing area
457    * @param startRes
458    *          start residue of print area
459    */
460   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
461           int canvasHeight, int startRes)
462   {
463     SequenceGroup group = av.getSelectionGroup();
464
465     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
466
467     if (group != null)
468     {
469       BufferedImage selectImage = null;
470       try
471       {
472         selectImage = new BufferedImage(canvasWidth, canvasHeight,
473                 BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
474       } catch (OutOfMemoryError er)
475       {
476         System.gc();
477         System.err.println("Print image OutOfMemory Error.\n" + er);
478         new OOMWarning("Creating wrapped alignment image for printing", er);
479       }
480       if (selectImage != null)
481       {
482         Graphics2D g2 = selectImage.createGraphics();
483         setupSelectionGroup(g2, selectImage);
484         drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
485                 startRes);
486
487         g2.setComposite(
488                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
489         g.drawImage(selectImage, 0, 0, this);
490         g2.dispose();
491       }
492     }
493   }
494
495   /*
496    * Make a local image by combining the cached image img
497    * with any selection
498    */
499   private BufferedImage buildLocalImage(BufferedImage selectImage)
500   {
501     // clone the cached image
502     BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
503             img.getType());
504     Graphics2D g2d = lcimg.createGraphics();
505     g2d.drawImage(img, 0, 0, null);
506
507     // overlay selection group on lcimg
508     if (selectImage != null)
509     {
510       g2d.setComposite(
511               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
512       g2d.drawImage(selectImage, 0, 0, this);
513     }
514     g2d.dispose();
515
516     return lcimg;
517   }
518
519   /*
520    * Set up a buffered image of the correct height and size for the sequence canvas
521    */
522   private BufferedImage setupImage()
523   {
524     BufferedImage lcimg = null;
525
526     int charWidth = av.getCharWidth();
527     int charHeight = av.getCharHeight();
528     
529     int width = getWidth();
530     int height = getHeight();
531
532     width -= (width % charWidth);
533     height -= (height % charHeight);
534
535     if ((width < 1) || (height < 1))
536     {
537       return null;
538     }
539
540     try
541     {
542       lcimg = new BufferedImage(width, height,
543               BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
544     } catch (OutOfMemoryError er)
545     {
546       System.gc();
547       System.err.println(
548               "Group image OutOfMemory Redraw Error.\n" + er);
549       new OOMWarning("Creating alignment image for display", er);
550
551       return null;
552     }
553
554     return lcimg;
555   }
556
557   /**
558    * Returns the visible width of the canvas in residues, after allowing for
559    * East or West scales (if shown)
560    * 
561    * @param canvasWidth
562    *          the width in pixels (possibly including scales)
563    * 
564    * @return
565    */
566   public int getWrappedCanvasWidth(int canvasWidth)
567   {
568     int charWidth = av.getCharWidth();
569
570     FontMetrics fm = getFontMetrics(av.getFont());
571
572     labelWidthEast = 0;
573     labelWidthWest = 0;
574
575     if (av.getScaleRightWrapped())
576     {
577       labelWidthEast = getLabelWidth(fm);
578     }
579
580     if (av.getScaleLeftWrapped())
581     {
582       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
583               : getLabelWidth(fm);
584     }
585
586     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
587   }
588
589   /**
590    * Returns a pixel width suitable for showing the largest sequence coordinate
591    * (end position) in the alignment. Returns 2 plus the number of decimal
592    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
593    * 
594    * @param fm
595    * @return
596    */
597   protected int getLabelWidth(FontMetrics fm)
598   {
599     /*
600      * find the biggest sequence end position we need to show
601      * (note this is not necessarily the sequence length)
602      */
603     int maxWidth = 0;
604     AlignmentI alignment = av.getAlignment();
605     for (int i = 0; i < alignment.getHeight(); i++)
606     {
607       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
608     }
609
610     int length = 2;
611     for (int i = maxWidth; i > 0; i /= 10)
612     {
613       length++;
614     }
615
616     return fm.stringWidth(ZEROS.substring(0, length));
617   }
618
619   /**
620    * Draws as many widths of a wrapped alignment as can fit in the visible
621    * window
622    * 
623    * @param g
624    * @param canvasWidth
625    *          available width in pixels
626    * @param canvasHeight
627    *          available height in pixels
628    * @param startColumn
629    *          the first column (0...) of the alignment to draw
630    */
631   public void drawWrappedPanel(Graphics g, int canvasWidth,
632           int canvasHeight, final int startColumn)
633   {
634     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
635             canvasHeight);
636
637     av.setWrappedWidth(wrappedWidthInResidues);
638
639     ViewportRanges ranges = av.getRanges();
640     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
641
642     /*
643      * draw one width at a time (including any scales or annotation shown),
644      * until we have run out of either alignment or vertical space available
645      */
646     int ypos = wrappedSpaceAboveAlignment;
647     int maxWidth = ranges.getVisibleAlignmentWidth();
648
649     int start = startColumn;
650     int currentWidth = 0;
651     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
652     {
653       int endColumn = Math
654               .min(maxWidth, start + wrappedWidthInResidues - 1);
655       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
656       ypos += wrappedRepeatHeightPx;
657       start += wrappedWidthInResidues;
658       currentWidth++;
659     }
660
661     drawWrappedDecorators(g, startColumn);
662   }
663
664   /**
665    * Calculates and saves values needed when rendering a wrapped alignment.
666    * These depend on many factors, including
667    * <ul>
668    * <li>canvas width and height</li>
669    * <li>number of visible sequences, and height of annotations if shown</li>
670    * <li>font and character width</li>
671    * <li>whether scales are shown left, right or above the alignment</li>
672    * </ul>
673    * 
674    * @param canvasWidth
675    * @param canvasHeight
676    * @return the number of residue columns in each width
677    */
678   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
679   {
680     int charHeight = av.getCharHeight();
681     int charWidth = av.getCharWidth();
682
683     /*
684      * width of labels in pixels left and right (if shown)
685      */
686     int labelWidth = 0;
687     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
688     {
689       FontMetrics fm = getFontMetrics(av.getFont());
690       labelWidth = getLabelWidth(fm);
691     }
692     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
693     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
694
695     /*
696      * vertical space in pixels between wrapped widths of alignment
697      * - one character height, or two if scale above is drawn
698      */
699     wrappedSpaceAboveAlignment = charHeight
700             * (av.getScaleAboveWrapped() ? 2 : 1);
701
702     /*
703      * height in pixels of the wrapped widths
704      */
705     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
706     // add sequences
707     wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
708             * charHeight;
709     // add annotations panel height if shown
710     wrappedRepeatHeightPx += getAnnotationHeight();
711
712     /*
713      * number of residue columns we can show in each row;
714      * this is just canvas width less scale left and right (if shown), 
715      * as a whole multiple of character widths 
716      */
717     int wrappedWidthInResidues = (canvasWidth - labelWidthEast
718             - labelWidthWest) / charWidth;
719
720     /*
721      * number of visible widths (the last one may be part height),
722      * ensuring a part height includes at least one sequence
723      */
724     ViewportRanges ranges = av.getRanges();
725     int xMax = ranges.getVisibleAlignmentWidth();
726     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
727     int remainder = canvasHeight % wrappedRepeatHeightPx;
728     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
729     {
730       wrappedVisibleWidths++;
731     }
732
733     /*
734      *  limit visibleWidths to not exceed width of alignment
735      */
736     int maxWidths = (xMax - ranges.getStartRes()) / wrappedWidthInResidues;
737     if (xMax % wrappedWidthInResidues > 0)
738     {
739       maxWidths++;
740     }
741     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
742
743     return wrappedWidthInResidues;
744   }
745
746   /**
747    * Draws one width of a wrapped alignment, including sequences and
748    * annnotations, if shown, but not scales or hidden column markers
749    * 
750    * @param g
751    * @param ypos
752    * @param startColumn
753    * @param endColumn
754    * @param canvasHeight
755    */
756   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
757           int endColumn, int canvasHeight)
758   {
759     int charHeight = av.getCharHeight();
760     int charWidth = av.getCharWidth();
761
762     ViewportRanges ranges = av.getRanges();
763     int viewportWidth = ranges.getViewportWidth();
764
765     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
766
767     /*
768      * move right before drawing by the width of the scale left (if any)
769      * plus column offset from left margin (usually zero, but may be non-zero
770      * when fast painting is drawing just a few columns)
771      */
772     int xOffset = labelWidthWest
773             + ((startColumn - ranges.getStartRes()) % viewportWidth)
774             * charWidth;
775     g.translate(xOffset, 0);
776
777     // When printing we have an extra clipped region,
778     // the Printable page which we need to account for here
779     Shape clip = g.getClip();
780
781     if (clip == null)
782     {
783       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
784     }
785     else
786     {
787       g.setClip(0, (int) clip.getBounds().getY(),
788               viewportWidth * charWidth, (int) clip.getBounds().getHeight());
789     }
790
791     /*
792      * white fill the region to be drawn (so incremental fast paint doesn't
793      * scribble over an existing image)
794      */
795     gg.setColor(Color.white);
796     gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
797             wrappedRepeatHeightPx);
798
799     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
800             ypos);
801
802     int cHeight = av.getAlignment().getHeight() * charHeight;
803
804     if (av.isShowAnnotation())
805     {
806       g.translate(0, cHeight + ypos + 3);
807       if (annotations == null)
808       {
809         annotations = new AnnotationPanel(av);
810       }
811
812       annotations.renderer.drawComponent(annotations, av, g, -1,
813               startColumn, endx + 1);
814       g.translate(0, -cHeight - ypos - 3);
815     }
816     g.setClip(clip);
817     g.translate(-xOffset, 0);
818   }
819
820   /**
821    * Draws scales left, right and above (if shown), and any hidden column
822    * markers, on all widths of the wrapped alignment
823    * 
824    * @param g
825    * @param startColumn
826    */
827   protected void drawWrappedDecorators(Graphics g, int startColumn)
828   {
829     int charWidth = av.getCharWidth();
830
831     g.setFont(av.getFont());
832     g.setColor(Color.black);
833
834     int ypos = wrappedSpaceAboveAlignment;
835     ViewportRanges ranges = av.getRanges();
836     int viewportWidth = ranges.getViewportWidth();
837     int maxWidth = ranges.getVisibleAlignmentWidth();
838     int widthsDrawn = 0;
839     while (widthsDrawn < wrappedVisibleWidths)
840     {
841       int endColumn = Math.min(maxWidth, startColumn + viewportWidth - 1);
842
843       if (av.getScaleLeftWrapped())
844       {
845         drawVerticalScale(g, startColumn, endColumn - 1, ypos, true);
846       }
847
848       if (av.getScaleRightWrapped())
849       {
850         int x = labelWidthWest + viewportWidth * charWidth;
851         g.translate(x, 0);
852         drawVerticalScale(g, startColumn, endColumn, ypos, false);
853         g.translate(-x, 0);
854       }
855
856       /*
857        * white fill region of scale above and hidden column markers
858        * (to support incremental fast paint of image)
859        */
860       g.setColor(Color.white);
861       g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
862               * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
863       g.setColor(Color.black);
864
865       g.translate(labelWidthWest, 0);
866
867       if (av.getScaleAboveWrapped())
868       {
869         drawNorthScale(g, startColumn, endColumn, ypos);
870       }
871
872       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
873       {
874         drawHiddenColumnMarkers(g, ypos, startColumn, endColumn);
875       }
876
877       g.translate(-labelWidthWest, 0);
878
879       ypos += wrappedRepeatHeightPx;
880       startColumn += viewportWidth;
881       widthsDrawn++;
882     }
883   }
884
885   /**
886    * @param g
887    * @param ypos
888    * @param startColumn
889    * @param endColumn
890    */
891   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
892           int startColumn, int endColumn)
893   {
894     int charHeight = av.getCharHeight();
895     int charWidth = av.getCharWidth();
896
897     g.setColor(Color.blue);
898     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
899     List<Integer> positions = hidden.findHiddenRegionPositions();
900     for (int pos : positions)
901     {
902       int res = pos - startColumn;
903
904       if (res < 0 || res > endColumn - startColumn)
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               .findColumnPosition(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       final int screenYMax = endRes - startRes;
1030       int blockStart = startRes;
1031       int blockEnd = endRes;
1032
1033       for (int[] region : av.getAlignment().getHiddenColumns()
1034               .getHiddenColumnsCopy())
1035       {
1036         int hideStart = region[0];
1037         int hideEnd = region[1];
1038
1039         if (hideStart <= blockStart)
1040         {
1041           blockStart += (hideEnd - hideStart) + 1;
1042           continue;
1043         }
1044
1045         /*
1046          * draw up to just before the next hidden region, or the end of
1047          * the visible region, whichever comes first
1048          */
1049         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
1050                 - screenY);
1051
1052         g1.translate(screenY * charWidth, 0);
1053
1054         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1055
1056         /*
1057          * draw the downline of the hidden column marker (ScalePanel draws the
1058          * triangle on top) if we reached it
1059          */
1060         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
1061         {
1062           g1.setColor(Color.blue);
1063
1064           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
1065                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
1066                   (endSeq - startSeq + 1) * charHeight + yOffset);
1067         }
1068
1069         g1.translate(-screenY * charWidth, 0);
1070         screenY += blockEnd - blockStart + 1;
1071         blockStart = hideEnd + 1;
1072
1073         if (screenY > screenYMax)
1074         {
1075           // already rendered last block
1076           return;
1077         }
1078       }
1079
1080       if (screenY <= screenYMax)
1081       {
1082         // remaining visible region to render
1083         blockEnd = blockStart + screenYMax - screenY;
1084         g1.translate(screenY * charWidth, 0);
1085         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1086
1087         g1.translate(-screenY * charWidth, 0);
1088       }
1089     }
1090
1091   }
1092
1093   /**
1094    * Draws a region of the visible alignment
1095    * 
1096    * @param g1
1097    * @param startRes
1098    *          offset of the first column in the visible region (0..)
1099    * @param endRes
1100    *          offset of the last column in the visible region (0..)
1101    * @param startSeq
1102    *          offset of the first sequence in the visible region (0..)
1103    * @param endSeq
1104    *          offset of the last sequence in the visible region (0..)
1105    * @param yOffset
1106    *          vertical offset at which to draw (for wrapped alignments)
1107    */
1108   private void draw(Graphics g, int startRes, int endRes, int startSeq,
1109           int endSeq, int offset)
1110   {
1111     int charHeight = av.getCharHeight();
1112     int charWidth = av.getCharWidth();
1113
1114     g.setFont(av.getFont());
1115     seqRdr.prepare(g, av.isRenderGaps());
1116
1117     SequenceI nextSeq;
1118
1119     // / First draw the sequences
1120     // ///////////////////////////
1121     for (int i = startSeq; i <= endSeq; i++)
1122     {
1123       nextSeq = av.getAlignment().getSequenceAt(i);
1124       if (nextSeq == null)
1125       {
1126         // occasionally, a race condition occurs such that the alignment row is
1127         // empty
1128         continue;
1129       }
1130       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
1131               startRes, endRes, offset + ((i - startSeq) * charHeight));
1132
1133       if (av.isShowSequenceFeatures())
1134       {
1135         fr.drawSequence(g, nextSeq, startRes, endRes,
1136                 offset + ((i - startSeq) * charHeight), false);
1137       }
1138
1139       /*
1140        * highlight search Results once sequence has been drawn
1141        */
1142       if (av.hasSearchResults())
1143       {
1144         SearchResultsI searchResults = av.getSearchResults();
1145         int[] visibleResults = searchResults.getResults(nextSeq,
1146                 startRes, endRes);
1147         if (visibleResults != null)
1148         {
1149           for (int r = 0; r < visibleResults.length; r += 2)
1150           {
1151             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
1152                     visibleResults[r + 1], (visibleResults[r] - startRes)
1153                             * charWidth, offset
1154                             + ((i - startSeq) * charHeight));
1155           }
1156         }
1157       }
1158
1159       if (av.cursorMode && cursorY == i && cursorX >= startRes
1160               && cursorX <= endRes)
1161       {
1162         seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
1163                 offset + ((i - startSeq) * charHeight));
1164       }
1165     }
1166
1167     if (av.getSelectionGroup() != null
1168             || av.getAlignment().getGroups().size() > 0)
1169     {
1170       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1171     }
1172
1173   }
1174
1175   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1176           int startSeq, int endSeq, int offset)
1177   {
1178     Graphics2D g = (Graphics2D) g1;
1179     //
1180     // ///////////////////////////////////
1181     // Now outline any areas if necessary
1182     // ///////////////////////////////////
1183
1184     SequenceGroup group = null;
1185     int groupIndex = -1;
1186
1187     if (av.getAlignment().getGroups().size() > 0)
1188     {
1189       group = av.getAlignment().getGroups().get(0);
1190       groupIndex = 0;
1191     }
1192
1193     if (group != null)
1194     {
1195       g.setStroke(new BasicStroke());
1196       g.setColor(group.getOutlineColour());
1197       
1198       do
1199       {
1200         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1201                 endSeq, offset);
1202
1203         groupIndex++;
1204
1205         g.setStroke(new BasicStroke());
1206
1207         if (groupIndex >= av.getAlignment().getGroups().size())
1208         {
1209           break;
1210         }
1211
1212         group = av.getAlignment().getGroups().get(groupIndex);
1213
1214       } while (groupIndex < av.getAlignment().getGroups().size());
1215
1216     }
1217
1218   }
1219
1220
1221   /*
1222    * Draw the selection group as a separate image and overlay
1223    */
1224   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1225           int startSeq, int endSeq)
1226   {
1227     // get a new image of the correct size
1228     BufferedImage selectionImage = setupImage();
1229
1230     if (selectionImage == null)
1231     {
1232       return null;
1233     }
1234
1235     SequenceGroup group = av.getSelectionGroup();
1236     if (group == null)
1237     {
1238       // nothing to draw
1239       return null;
1240     }
1241
1242     // set up drawing colour
1243     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1244
1245     setupSelectionGroup(g, selectionImage);
1246
1247     if (!av.getWrapAlignment())
1248     {
1249       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1250               0);
1251     }
1252     else
1253     {
1254       drawWrappedSelection(g, group, getWidth(), getHeight(),
1255               av.getRanges().getStartRes());
1256     }
1257
1258     g.dispose();
1259     return selectionImage;
1260   }
1261
1262   /*
1263    * Set up graphics for selection group
1264    */
1265   private void setupSelectionGroup(Graphics2D g,
1266           BufferedImage selectionImage)
1267   {
1268     // set background to transparent
1269     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1270     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1271
1272     // set up foreground to draw red dashed line
1273     g.setComposite(AlphaComposite.Src);
1274     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1275             BasicStroke.JOIN_ROUND, 3f, new float[]
1276     { 5f, 3f }, 0f));
1277     g.setColor(Color.RED);
1278   }
1279
1280   /*
1281    * Draw a selection group over an unwrapped alignment
1282    * @param g graphics object to draw with
1283    * @param group selection group
1284    * @param startRes start residue of area to draw
1285    * @param endRes end residue of area to draw
1286    * @param startSeq start sequence of area to draw
1287    * @param endSeq end sequence of area to draw
1288    * @param offset vertical offset (used when called from wrapped alignment code)
1289    */
1290   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1291           int startRes, int endRes, int startSeq, int endSeq, int offset)
1292   {
1293         int charWidth = av.getCharWidth();
1294           
1295     if (!av.hasHiddenColumns())
1296     {
1297       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
1298               offset);
1299     }
1300     else
1301     {
1302       // package into blocks of visible columns
1303       int screenY = 0;
1304       int blockStart = startRes;
1305       int blockEnd = endRes;
1306
1307       for (int[] region : av.getAlignment().getHiddenColumns()
1308               .getHiddenColumnsCopy())
1309       {
1310         int hideStart = region[0];
1311         int hideEnd = region[1];
1312
1313         if (hideStart <= blockStart)
1314         {
1315           blockStart += (hideEnd - hideStart) + 1;
1316           continue;
1317         }
1318
1319         blockEnd = hideStart - 1;
1320
1321         g.translate(screenY * charWidth, 0);
1322         drawPartialGroupOutline(g, group,
1323                 blockStart, blockEnd, startSeq, endSeq, offset);
1324
1325         g.translate(-screenY * charWidth, 0);
1326         screenY += blockEnd - blockStart + 1;
1327         blockStart = hideEnd + 1;
1328
1329         if (screenY > (endRes - startRes))
1330         {
1331           // already rendered last block
1332           break;
1333         }
1334       }
1335
1336       if (screenY <= (endRes - startRes))
1337       {
1338         // remaining visible region to render
1339         blockEnd = blockStart + (endRes - startRes) - screenY;
1340         g.translate(screenY * charWidth, 0);
1341         drawPartialGroupOutline(g, group,
1342                 blockStart, blockEnd, startSeq, endSeq, offset);
1343         
1344         g.translate(-screenY * charWidth, 0);
1345       }
1346     }
1347   }
1348
1349   /*
1350    * Draw the selection group as a separate image and overlay
1351    */
1352   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
1353           int startRes, int endRes, int startSeq, int endSeq,
1354           int verticalOffset)
1355   {
1356         int charHeight = av.getCharHeight();
1357         int charWidth = av.getCharWidth();
1358           
1359     int visWidth = (endRes - startRes + 1) * charWidth;
1360
1361     int oldY = -1;
1362     int i = 0;
1363     boolean inGroup = false;
1364     int top = -1;
1365     int bottom = -1;
1366
1367     int sx = -1;
1368     int sy = -1;
1369     int xwidth = -1;
1370
1371     for (i = startSeq; i <= endSeq; i++)
1372     {
1373       // position of start residue of group relative to startRes, in pixels
1374       sx = (group.getStartRes() - startRes) * charWidth;
1375
1376       // width of group in pixels
1377       xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1378               - 1;
1379
1380       sy = verticalOffset + (i - startSeq) * charHeight;
1381
1382       if (sx + xwidth < 0 || sx > visWidth)
1383       {
1384         continue;
1385       }
1386
1387       if ((sx <= (endRes - startRes) * charWidth)
1388               && group.getSequences(null)
1389                       .contains(av.getAlignment().getSequenceAt(i)))
1390       {
1391         if ((bottom == -1) && !group.getSequences(null)
1392                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1393         {
1394           bottom = sy + charHeight;
1395         }
1396
1397         if (!inGroup)
1398         {
1399           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1400                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1401           {
1402             top = sy;
1403           }
1404
1405           oldY = sy;
1406           inGroup = true;
1407         }
1408       }
1409       else
1410       {
1411         if (inGroup)
1412         {
1413           // if start position is visible, draw vertical line to left of
1414           // group
1415           if (sx >= 0 && sx < visWidth)
1416           {
1417             g.drawLine(sx, oldY, sx, sy);
1418           }
1419
1420           // if end position is visible, draw vertical line to right of
1421           // group
1422           if (sx + xwidth < visWidth)
1423           {
1424             g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1425           }
1426
1427           if (sx < 0)
1428           {
1429             xwidth += sx;
1430             sx = 0;
1431           }
1432
1433           // don't let width extend beyond current block, or group extent
1434           // fixes JAL-2672
1435           if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1436           {
1437             xwidth = (endRes - startRes + 1) * charWidth - sx;
1438           }
1439           
1440           // draw horizontal line at top of group
1441           if (top != -1)
1442           {
1443             g.drawLine(sx, top, sx + xwidth, top);
1444             top = -1;
1445           }
1446
1447           // draw horizontal line at bottom of group
1448           if (bottom != -1)
1449           {
1450             g.drawLine(sx, bottom, sx + xwidth, bottom);
1451             bottom = -1;
1452           }
1453
1454           inGroup = false;
1455         }
1456       }
1457     }
1458
1459     if (inGroup)
1460     {
1461       sy = verticalOffset + ((i - startSeq) * charHeight);
1462       if (sx >= 0 && sx < visWidth)
1463       {
1464         g.drawLine(sx, oldY, sx, sy);
1465       }
1466
1467       if (sx + xwidth < visWidth)
1468       {
1469         g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1470       }
1471
1472       if (sx < 0)
1473       {
1474         xwidth += sx;
1475         sx = 0;
1476       }
1477
1478       if (sx + xwidth > visWidth)
1479       {
1480         xwidth = visWidth;
1481       }
1482       else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1483       {
1484         xwidth = (endRes - startRes + 1) * charWidth;
1485       }
1486
1487       if (top != -1)
1488       {
1489         g.drawLine(sx, top, sx + xwidth, top);
1490         top = -1;
1491       }
1492
1493       if (bottom != -1)
1494       {
1495         g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
1496         bottom = -1;
1497       }
1498
1499       inGroup = false;
1500     }
1501   }
1502   
1503   /**
1504    * Highlights search results in the visible region by rendering as white text
1505    * on a black background. Any previous highlighting is removed. Answers true
1506    * if any highlight was left on the visible alignment (so status bar should be
1507    * set to match), else false.
1508    * <p>
1509    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1510    * alignment had to be scrolled to show the highlighted region, then it should
1511    * be fully redrawn, otherwise a fast paint can be performed. This argument
1512    * could be removed if fast paint of scrolled wrapped alignment is coded in
1513    * future (JAL-2609).
1514    * 
1515    * @param results
1516    * @param noFastPaint
1517    * @return
1518    */
1519   public boolean highlightSearchResults(SearchResultsI results,
1520           boolean noFastPaint)
1521   {
1522     if (fastpainting)
1523     {
1524       return false;
1525     }
1526     boolean wrapped = av.getWrapAlignment();
1527     try
1528     {
1529       fastPaint = !noFastPaint;
1530       fastpainting = fastPaint;
1531
1532       /*
1533        * to avoid redrawing the whole visible region, we instead
1534        * redraw just the minimal regions to remove previous highlights
1535        * and add new ones
1536        */
1537       SearchResultsI previous = av.getSearchResults();
1538       av.setSearchResults(results);
1539       boolean redrawn = false;
1540       boolean drawn = false;
1541       if (wrapped)
1542       {
1543         redrawn = drawMappedPositionsWrapped(previous);
1544         drawn = drawMappedPositionsWrapped(results);
1545         redrawn |= drawn;
1546       }
1547       else
1548       {
1549         redrawn = drawMappedPositions(previous);
1550         drawn = drawMappedPositions(results);
1551         redrawn |= drawn;
1552       }
1553
1554       /*
1555        * if highlights were either removed or added, repaint
1556        */
1557       if (redrawn)
1558       {
1559         repaint();
1560       }
1561
1562       /*
1563        * return true only if highlights were added
1564        */
1565       return drawn;
1566
1567     } finally
1568     {
1569       fastpainting = false;
1570     }
1571   }
1572
1573   /**
1574    * Redraws the minimal rectangle in the visible region (if any) that includes
1575    * mapped positions of the given search results. Whether or not positions are
1576    * highlighted depends on the SearchResults set on the Viewport. This allows
1577    * this method to be called to either clear or set highlighting. Answers true
1578    * if any positions were drawn (in which case a repaint is still required),
1579    * else false.
1580    * 
1581    * @param results
1582    * @return
1583    */
1584   protected boolean drawMappedPositions(SearchResultsI results)
1585   {
1586     if (results == null)
1587     {
1588       return false;
1589     }
1590
1591     /*
1592      * calculate the minimal rectangle to redraw that 
1593      * includes both new and existing search results
1594      */
1595     int firstSeq = Integer.MAX_VALUE;
1596     int lastSeq = -1;
1597     int firstCol = Integer.MAX_VALUE;
1598     int lastCol = -1;
1599     boolean matchFound = false;
1600
1601     ViewportRanges ranges = av.getRanges();
1602     int firstVisibleColumn = ranges.getStartRes();
1603     int lastVisibleColumn = ranges.getEndRes();
1604     AlignmentI alignment = av.getAlignment();
1605     if (av.hasHiddenColumns())
1606     {
1607       firstVisibleColumn = alignment.getHiddenColumns()
1608               .adjustForHiddenColumns(firstVisibleColumn);
1609       lastVisibleColumn = alignment.getHiddenColumns()
1610               .adjustForHiddenColumns(lastVisibleColumn);
1611     }
1612
1613     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1614             .getEndSeq(); seqNo++)
1615     {
1616       SequenceI seq = alignment.getSequenceAt(seqNo);
1617
1618       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1619               lastVisibleColumn);
1620       if (visibleResults != null)
1621       {
1622         for (int i = 0; i < visibleResults.length - 1; i += 2)
1623         {
1624           int firstMatchedColumn = visibleResults[i];
1625           int lastMatchedColumn = visibleResults[i + 1];
1626           if (firstMatchedColumn <= lastVisibleColumn
1627                   && lastMatchedColumn >= firstVisibleColumn)
1628           {
1629             /*
1630              * found a search results match in the visible region - 
1631              * remember the first and last sequence matched, and the first
1632              * and last visible columns in the matched positions
1633              */
1634             matchFound = true;
1635             firstSeq = Math.min(firstSeq, seqNo);
1636             lastSeq = Math.max(lastSeq, seqNo);
1637             firstMatchedColumn = Math.max(firstMatchedColumn,
1638                     firstVisibleColumn);
1639             lastMatchedColumn = Math.min(lastMatchedColumn,
1640                     lastVisibleColumn);
1641             firstCol = Math.min(firstCol, firstMatchedColumn);
1642             lastCol = Math.max(lastCol, lastMatchedColumn);
1643           }
1644         }
1645       }
1646     }
1647
1648     if (matchFound)
1649     {
1650       if (av.hasHiddenColumns())
1651       {
1652         firstCol = alignment.getHiddenColumns()
1653                 .findColumnPosition(firstCol);
1654         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1655       }
1656       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1657       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1658       gg.translate(transX, transY);
1659       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1660       gg.translate(-transX, -transY);
1661     }
1662
1663     return matchFound;
1664   }
1665
1666   @Override
1667   public void propertyChange(PropertyChangeEvent evt)
1668   {
1669     String eventName = evt.getPropertyName();
1670
1671     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1672     {
1673       fastPaint = true;
1674       repaint();
1675     }
1676     else if (eventName.equals(ViewportRanges.STARTRES))
1677     {
1678       int scrollX = 0;
1679       if (eventName.equals(ViewportRanges.STARTRES))
1680       {
1681         // Make sure we're not trying to draw a panel
1682         // larger than the visible window
1683         ViewportRanges vpRanges = av.getRanges();
1684         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1685         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1686         if (scrollX > range)
1687         {
1688           scrollX = range;
1689         }
1690         else if (scrollX < -range)
1691         {
1692           scrollX = -range;
1693         }
1694
1695         // Both scrolling and resizing change viewport ranges: scrolling changes
1696         // both start and end points, but resize only changes end values.
1697         // Here we only want to fastpaint on a scroll, with resize using a normal
1698         // paint, so scroll events are identified as changes to the horizontal or
1699         // vertical start value.
1700         
1701         // scroll - startres and endres both change
1702           if (av.getWrapAlignment())
1703         {
1704           fastPaintWrapped(scrollX);
1705         }
1706         else
1707         {
1708           fastPaint(scrollX, 0);
1709         }
1710       }
1711       else if (eventName.equals(ViewportRanges.STARTSEQ))
1712       {
1713         // scroll
1714         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1715       }
1716     }
1717   }
1718
1719   /**
1720    * Does a minimal update of the image for a scroll movement. This method
1721    * handles scroll movements of up to one width of the wrapped alignment (one
1722    * click in the vertical scrollbar). Larger movements (for example after a
1723    * scroll to highlight a mapped position) trigger a full redraw instead.
1724    * 
1725    * @param scrollX
1726    *          number of positions scrolled (right if positive, left if negative)
1727    */
1728   protected void fastPaintWrapped(int scrollX)
1729   {
1730     ViewportRanges ranges = av.getRanges();
1731
1732     if (Math.abs(scrollX) > ranges.getViewportWidth())
1733     {
1734       /*
1735        * shift of more than one view width is 
1736        * overcomplicated to handle in this method
1737        */
1738       fastPaint = false;
1739       repaint();
1740       return;
1741     }
1742
1743     if (fastpainting || gg == null)
1744     {
1745       return;
1746     }
1747
1748     fastPaint = true;
1749     fastpainting = true;
1750
1751     try
1752     {
1753       calculateWrappedGeometry(getWidth(), getHeight());
1754
1755       /*
1756        * relocate the regions of the alignment that are still visible
1757        */
1758       shiftWrappedAlignment(-scrollX);
1759
1760       /*
1761        * add new columns (sequence, annotation)
1762        * - at top left if scrollX < 0 
1763        * - at right of last two widths if scrollX > 0
1764        */
1765       if (scrollX < 0)
1766       {
1767         int startRes = ranges.getStartRes();
1768         drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
1769                 - scrollX - 1, getHeight());
1770       }
1771       else
1772       {
1773         fastPaintWrappedAddRight(scrollX);
1774       }
1775
1776       /*
1777        * draw all scales (if  shown) and hidden column markers
1778        */
1779       drawWrappedDecorators(gg, ranges.getStartRes());
1780
1781       repaint();
1782     } finally
1783     {
1784       fastpainting = false;
1785     }
1786   }
1787
1788   /**
1789    * Draws the specified number of columns at the 'end' (bottom right) of a
1790    * wrapped alignment view, including sequences and annotations if shown, but
1791    * not scales. Also draws the same number of columns at the right hand end of
1792    * the second last width shown, if the last width is not full height (so
1793    * cannot simply be copied from the graphics image).
1794    * 
1795    * @param columns
1796    */
1797   protected void fastPaintWrappedAddRight(int columns)
1798   {
1799     if (columns == 0)
1800     {
1801       return;
1802     }
1803
1804     ViewportRanges ranges = av.getRanges();
1805     int viewportWidth = ranges.getViewportWidth();
1806     int charWidth = av.getCharWidth();
1807
1808     /**
1809      * draw full height alignment in the second last row, last columns, if the
1810      * last row was not full height
1811      */
1812     int visibleWidths = wrappedVisibleWidths;
1813     int canvasHeight = getHeight();
1814     boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
1815
1816     if (lastWidthPartHeight)
1817     {
1818       int widthsAbove = Math.max(0, visibleWidths - 2);
1819       int ypos = wrappedRepeatHeightPx * widthsAbove
1820               + wrappedSpaceAboveAlignment;
1821       int endRes = ranges.getEndRes();
1822       endRes += widthsAbove * viewportWidth;
1823       int startRes = endRes - columns;
1824       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1825               * charWidth;
1826
1827       /*
1828        * white fill first to erase annotations
1829        */
1830       gg.translate(xOffset, 0);
1831       gg.setColor(Color.white);
1832       gg.fillRect(labelWidthWest, ypos,
1833               (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
1834       gg.translate(-xOffset, 0);
1835
1836       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1837     }
1838
1839     /*
1840      * draw newly visible columns in last wrapped width (none if we
1841      * have reached the end of the alignment)
1842      * y-offset for drawing last width is height of widths above,
1843      * plus one gap row
1844      */
1845     int widthsAbove = visibleWidths - 1;
1846     int ypos = wrappedRepeatHeightPx * widthsAbove
1847             + wrappedSpaceAboveAlignment;
1848     int endRes = ranges.getEndRes();
1849     endRes += widthsAbove * viewportWidth;
1850     int startRes = endRes - columns + 1;
1851
1852     /*
1853      * white fill first to erase annotations
1854      */
1855     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1856             * charWidth;
1857     gg.translate(xOffset, 0);
1858     gg.setColor(Color.white);
1859     int width = viewportWidth * charWidth - xOffset;
1860     gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
1861     gg.translate(-xOffset, 0);
1862
1863     gg.setFont(av.getFont());
1864     gg.setColor(Color.black);
1865
1866     if (startRes < ranges.getVisibleAlignmentWidth())
1867     {
1868       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1869     }
1870
1871     /*
1872      * and finally, white fill any space below the visible alignment
1873      */
1874     int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
1875     if (heightBelow > 0)
1876     {
1877       gg.setColor(Color.white);
1878       gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
1879     }
1880   }
1881
1882   /**
1883    * Shifts the visible alignment by the specified number of columns - left if
1884    * negative, right if positive. Copies and moves sequences and annotations (if
1885    * shown). Scales, hidden column markers and any newly visible columns must be
1886    * drawn separately.
1887    * 
1888    * @param positions
1889    */
1890   protected void shiftWrappedAlignment(int positions)
1891   {
1892     if (positions == 0)
1893     {
1894       return;
1895     }
1896     int charWidth = av.getCharWidth();
1897
1898     int canvasHeight = getHeight();
1899     ViewportRanges ranges = av.getRanges();
1900     int viewportWidth = ranges.getViewportWidth();
1901     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
1902             * charWidth;
1903     int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
1904     int xMax = ranges.getVisibleAlignmentWidth();
1905
1906     if (positions > 0)
1907     {
1908       /*
1909        * shift right (after scroll left)
1910        * for each wrapped width (starting with the last), copy (width-positions) 
1911        * columns from the left margin to the right margin, and copy positions 
1912        * columns from the right margin of the row above (if any) to the 
1913        * left margin of the current row
1914        */
1915
1916       /*
1917        * get y-offset of last wrapped width, first row of sequences
1918        */
1919       int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
1920       y += wrappedSpaceAboveAlignment;
1921       int copyFromLeftStart = labelWidthWest;
1922       int copyFromRightStart = copyFromLeftStart + widthToCopy;
1923
1924       while (y >= 0)
1925       {
1926         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
1927                 positions * charWidth, 0);
1928         if (y > 0)
1929         {
1930           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
1931                   positions * charWidth, heightToCopy, -widthToCopy,
1932                   wrappedRepeatHeightPx);
1933         }
1934
1935         y -= wrappedRepeatHeightPx;
1936       }
1937     }
1938     else
1939     {
1940       /*
1941        * shift left (after scroll right)
1942        * for each wrapped width (starting with the first), copy (width-positions) 
1943        * columns from the right margin to the left margin, and copy positions 
1944        * columns from the left margin of the row below (if any) to the 
1945        * right margin of the current row
1946        */
1947       int xpos = av.getRanges().getStartRes();
1948       int y = wrappedSpaceAboveAlignment;
1949       int copyFromRightStart = labelWidthWest - positions * charWidth;
1950
1951       while (y < canvasHeight)
1952       {
1953         gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
1954                 positions * charWidth, 0);
1955         if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
1956                 && (xpos + viewportWidth <= xMax))
1957         {
1958           gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
1959                   * charWidth, heightToCopy, widthToCopy,
1960                   -wrappedRepeatHeightPx);
1961         }
1962
1963         y += wrappedRepeatHeightPx;
1964         xpos += viewportWidth;
1965       }
1966     }
1967   }
1968
1969   
1970   /**
1971    * Redraws any positions in the search results in the visible region of a
1972    * wrapped alignment. Any highlights are drawn depending on the search results
1973    * set on the Viewport, not the <code>results</code> argument. This allows
1974    * this method to be called either to clear highlights (passing the previous
1975    * search results), or to draw new highlights.
1976    * 
1977    * @param results
1978    * @return
1979    */
1980   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1981   {
1982     if (results == null)
1983     {
1984       return false;
1985     }
1986     int charHeight = av.getCharHeight();
1987
1988     boolean matchFound = false;
1989
1990     calculateWrappedGeometry(getWidth(), getHeight());
1991     int wrappedWidth = av.getWrappedWidth();
1992     int wrappedHeight = wrappedRepeatHeightPx;
1993
1994     ViewportRanges ranges = av.getRanges();
1995     int canvasHeight = getHeight();
1996     int repeats = canvasHeight / wrappedHeight;
1997     if (canvasHeight / wrappedHeight > 0)
1998     {
1999       repeats++;
2000     }
2001
2002     int firstVisibleColumn = ranges.getStartRes();
2003     int lastVisibleColumn = ranges.getStartRes() + repeats
2004             * ranges.getViewportWidth() - 1;
2005
2006     AlignmentI alignment = av.getAlignment();
2007     if (av.hasHiddenColumns())
2008     {
2009       firstVisibleColumn = alignment.getHiddenColumns()
2010               .adjustForHiddenColumns(firstVisibleColumn);
2011       lastVisibleColumn = alignment.getHiddenColumns()
2012               .adjustForHiddenColumns(lastVisibleColumn);
2013     }
2014
2015     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
2016
2017     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
2018             .getEndSeq(); seqNo++)
2019     {
2020       SequenceI seq = alignment.getSequenceAt(seqNo);
2021
2022       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
2023               lastVisibleColumn);
2024       if (visibleResults != null)
2025       {
2026         for (int i = 0; i < visibleResults.length - 1; i += 2)
2027         {
2028           int firstMatchedColumn = visibleResults[i];
2029           int lastMatchedColumn = visibleResults[i + 1];
2030           if (firstMatchedColumn <= lastVisibleColumn
2031                   && lastMatchedColumn >= firstVisibleColumn)
2032           {
2033             /*
2034              * found a search results match in the visible region
2035              */
2036             firstMatchedColumn = Math.max(firstMatchedColumn,
2037                     firstVisibleColumn);
2038             lastMatchedColumn = Math.min(lastMatchedColumn,
2039                     lastVisibleColumn);
2040
2041             /*
2042              * draw each mapped position separately (as contiguous positions may
2043              * wrap across lines)
2044              */
2045             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
2046             {
2047               int displayColumn = mappedPos;
2048               if (av.hasHiddenColumns())
2049               {
2050                 displayColumn = alignment.getHiddenColumns()
2051                         .findColumnPosition(displayColumn);
2052               }
2053
2054               /*
2055                * transX: offset from left edge of canvas to residue position
2056                */
2057               int transX = labelWidthWest
2058                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
2059                       * av.getCharWidth();
2060
2061               /*
2062                * transY: offset from top edge of canvas to residue position
2063                */
2064               int transY = gapHeight;
2065               transY += (displayColumn - ranges.getStartRes())
2066                       / wrappedWidth * wrappedHeight;
2067               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
2068
2069               /*
2070                * yOffset is from graphics origin to start of visible region
2071                */
2072               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
2073               if (transY < getHeight())
2074               {
2075                 matchFound = true;
2076                 gg.translate(transX, transY);
2077                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
2078                         yOffset);
2079                 gg.translate(-transX, -transY);
2080               }
2081             }
2082           }
2083         }
2084       }
2085     }
2086   
2087     return matchFound;
2088   }
2089 }