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