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