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