JAL-2609 tiny tidy
[jalview.git] / src / jalview / gui / SeqCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.ScaleRenderer;
29 import jalview.renderer.ScaleRenderer.ScaleMark;
30 import jalview.util.Comparison;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.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       // fillRect origin is top left of rectangle
241       g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast,
242               charHeight + 1);
243
244       if (value != -1)
245       {
246
247         /*
248          * draw scale value, right justified, with half a character width
249          * separation from the sequence data
250          */
251         String valueAsString = String.valueOf(value);
252         int justify = fm.stringWidth(valueAsString) + charWidth;
253         int xpos = left ? labelWidthWest - justify + charWidth / 2
254                 : labelWidthEast - justify + charWidth / 2;
255         g.setColor(Color.black);
256         g.drawString(valueAsString, xpos, y);
257       }
258     }
259   }
260
261   /**
262    * Does a fast paint of an alignment in response to a scroll. Most of the
263    * visible region is simply copied and shifted, and then any newly visible
264    * columns or rows are drawn. The scroll may be horizontal or vertical, but
265    * not both at once. Scrolling may be the result of
266    * <ul>
267    * <li>dragging a scroll bar</li>
268    * <li>clicking in the scroll bar</li>
269    * <li>scrolling by trackpad, middle mouse button, or other device</li>
270    * <li>by moving the box in the Overview window</li>
271    * <li>programmatically to make a highlighted position visible</li>
272    * </ul>
273    * 
274    * @param horizontal
275    *          columns to shift right (positive) or left (negative)
276    * @param vertical
277    *          rows to shift down (positive) or up (negative)
278    */
279   public void fastPaint(int horizontal, int vertical)
280   {
281     if (fastpainting || gg == null)
282     {
283       return;
284     }
285     fastpainting = true;
286     fastPaint = true;
287
288     try
289     {
290       int charHeight = av.getCharHeight();
291       int charWidth = av.getCharWidth();
292
293       ViewportRanges ranges = av.getRanges();
294       int startRes = ranges.getStartRes();
295       int endRes = ranges.getEndRes();
296       int startSeq = ranges.getStartSeq();
297       int endSeq = ranges.getEndSeq();
298       int transX = 0;
299       int transY = 0;
300
301       gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
302               imgHeight, -horizontal * charWidth, -vertical * charHeight);
303
304       if (horizontal > 0) // scrollbar pulled right, image to the left
305       {
306         transX = (endRes - startRes - horizontal) * charWidth;
307         startRes = endRes - horizontal;
308       }
309       else if (horizontal < 0)
310       {
311         endRes = startRes - horizontal;
312       }
313       else if (vertical > 0) // scroll down
314       {
315         startSeq = endSeq - vertical;
316
317         if (startSeq < ranges.getStartSeq())
318         { // ie scrolling too fast, more than a page at a time
319           startSeq = ranges.getStartSeq();
320         }
321         else
322         {
323           transY = imgHeight - ((vertical + 1) * charHeight);
324         }
325       }
326       else if (vertical < 0)
327       {
328         endSeq = startSeq - vertical;
329
330         if (endSeq > ranges.getEndSeq())
331         {
332           endSeq = ranges.getEndSeq();
333         }
334       }
335
336       gg.translate(transX, transY);
337       drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
338       gg.translate(-transX, -transY);
339
340       repaint();
341     } finally
342     {
343       fastpainting = false;
344     }
345   }
346
347   @Override
348   public void paintComponent(Graphics g)
349   {
350     int charHeight = av.getCharHeight();
351     int charWidth = av.getCharWidth();
352     BufferedImage lcimg = img; // take reference since other threads may null
353     // img and call later.
354     super.paintComponent(g);
355
356     if (lcimg != null && (fastPaint
357             || (getVisibleRect().width != g.getClipBounds().width)
358             || (getVisibleRect().height != g.getClipBounds().height)))
359     {
360       g.drawImage(lcimg, 0, 0, this);
361       fastPaint = false;
362       return;
363     }
364
365     // this draws the whole of the alignment
366     imgWidth = getWidth();
367     imgHeight = getHeight();
368
369     imgWidth -= (imgWidth % charWidth);
370     imgHeight -= (imgHeight % charHeight);
371
372     if ((imgWidth < 1) || (imgHeight < 1))
373     {
374       return;
375     }
376
377     if (lcimg == null || imgWidth != lcimg.getWidth()
378             || imgHeight != lcimg.getHeight())
379     {
380       try
381       {
382         lcimg = img = new BufferedImage(imgWidth, imgHeight,
383                 BufferedImage.TYPE_INT_RGB);
384         gg = (Graphics2D) img.getGraphics();
385         gg.setFont(av.getFont());
386       } catch (OutOfMemoryError er)
387       {
388         System.gc();
389         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
390         new OOMWarning("Creating alignment image for display", er);
391
392         return;
393       }
394     }
395
396     if (av.antiAlias)
397     {
398       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
399               RenderingHints.VALUE_ANTIALIAS_ON);
400     }
401
402     gg.setColor(Color.white);
403     gg.fillRect(0, 0, imgWidth, imgHeight);
404
405     ViewportRanges ranges = av.getRanges();
406     if (av.getWrapAlignment())
407     {
408       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
409     }
410     else
411     {
412       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
413               ranges.getStartSeq(), ranges.getEndSeq(), 0);
414     }
415
416     g.drawImage(lcimg, 0, 0, this);
417
418   }
419
420   /**
421    * Returns the visible width of the canvas in residues, after allowing for
422    * East or West scales (if shown)
423    * 
424    * @param canvasWidth
425    *          the width in pixels (possibly including scales)
426    * 
427    * @return
428    */
429   public int getWrappedCanvasWidth(int canvasWidth)
430   {
431     int charWidth = av.getCharWidth();
432
433     FontMetrics fm = getFontMetrics(av.getFont());
434
435     labelWidthEast = 0;
436     labelWidthWest = 0;
437
438     if (av.getScaleRightWrapped())
439     {
440       labelWidthEast = getLabelWidth(fm);
441     }
442
443     if (av.getScaleLeftWrapped())
444     {
445       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
446               : getLabelWidth(fm);
447     }
448
449     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
450   }
451
452   /**
453    * Returns a pixel width suitable for showing the largest sequence coordinate
454    * (end position) in the alignment. Returns 2 plus the number of decimal
455    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
456    * 
457    * @param fm
458    * @return
459    */
460   protected int getLabelWidth(FontMetrics fm)
461   {
462     /*
463      * find the biggest sequence end position we need to show
464      * (note this is not necessarily the sequence length)
465      */
466     int maxWidth = 0;
467     AlignmentI alignment = av.getAlignment();
468     for (int i = 0; i < alignment.getHeight(); i++)
469     {
470       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
471     }
472
473     int length = 2;
474     for (int i = maxWidth; i > 0; i /= 10)
475     {
476       length++;
477     }
478
479     return fm.stringWidth(ZEROS.substring(0, length));
480   }
481
482   /**
483    * Draws as many widths of a wrapped alignment as can fit in the visible
484    * window
485    * 
486    * @param g
487    * @param canvasWidth
488    *          available width in pixels
489    * @param canvasHeight
490    *          available height in pixels
491    * @param startColumn
492    *          the first column (0...) of the alignment to draw
493    */
494   public void drawWrappedPanel(Graphics g, int canvasWidth,
495           int canvasHeight, final int startColumn)
496   {
497     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
498             canvasHeight);
499
500     av.setWrappedWidth(wrappedWidthInResidues);
501
502     ViewportRanges ranges = av.getRanges();
503     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
504
505     /*
506      * draw one width at a time (including any scales or annotation shown),
507      * until we have run out of either alignment or vertical space available
508      */
509     int ypos = wrappedSpaceAboveAlignment;
510     int maxWidth = ranges.getVisibleAlignmentWidth();
511
512     int start = startColumn;
513     int currentWidth = 0;
514     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
515     {
516       int endColumn = Math
517               .min(maxWidth, start + wrappedWidthInResidues - 1);
518       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
519       ypos += wrappedRepeatHeightPx;
520       start += wrappedWidthInResidues;
521       currentWidth++;
522     }
523
524     drawWrappedDecorators(g, startColumn);
525   }
526
527   /**
528    * Calculates and saves values needed when rendering a wrapped alignment.
529    * These depend on many factors, including
530    * <ul>
531    * <li>canvas width and height</li>
532    * <li>number of visible sequences, and height of annotations if shown</li>
533    * <li>font and character width</li>
534    * <li>whether scales are shown left, right or above the alignment</li>
535    * </ul>
536    * 
537    * @param canvasWidth
538    * @param canvasHeight
539    * @return the number of residue columns in each width
540    */
541   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
542   {
543     int charHeight = av.getCharHeight();
544     int charWidth = av.getCharWidth();
545
546     /*
547      * width of labels in pixels left and right (if shown)
548      */
549     int labelWidth = 0;
550     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
551     {
552       FontMetrics fm = getFontMetrics(av.getFont());
553       labelWidth = getLabelWidth(fm);
554     }
555     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
556     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
557
558     /*
559      * vertical space in pixels between wrapped widths of alignment
560      * - one character height, or two if scale above is drawn
561      */
562     wrappedSpaceAboveAlignment = charHeight
563             * (av.getScaleAboveWrapped() ? 2 : 1);
564
565     /*
566      * height in pixels of the wrapped widths
567      */
568     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
569     // add sequences
570     wrappedRepeatHeightPx += av.getRanges().getViewportHeight() * charHeight;
571     // add annotations panel height if shown
572     wrappedRepeatHeightPx += getAnnotationHeight();
573
574     /*
575      * number of residue columns we can show in each row;
576      * this is just canvas width less scale left and right (if shown), 
577      * as a whole multiple of character widths 
578      */
579     int wrappedWidthInResidues = (canvasWidth - labelWidthEast - labelWidthWest)
580             / charWidth;
581
582     /*
583      * number of visible widths (the last one may be part height),
584      * ensuring a part height includes at least one sequence
585      */
586     ViewportRanges ranges = av.getRanges();
587     int xMax = ranges.getVisibleAlignmentWidth();
588     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
589     int remainder = canvasHeight % wrappedRepeatHeightPx;
590     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
591     {
592       wrappedVisibleWidths++;
593     }
594
595     /*
596      *  limit visibleWidths to not exceed width of alignment
597      */
598     int maxWidths = (xMax - ranges.getStartRes()) / wrappedWidthInResidues;
599     if (xMax % wrappedWidthInResidues > 0)
600     {
601       maxWidths++;
602     }
603     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
604
605     return wrappedWidthInResidues;
606   }
607
608   /**
609    * Draws one width of a wrapped alignment, including sequences and
610    * annnotations, if shown, but not scales or hidden column markers
611    * 
612    * @param g
613    * @param ypos
614    * @param startColumn
615    * @param endColumn
616    * @param canvasHeight
617    */
618   protected void drawWrappedWidth(Graphics g, int ypos,
619           int startColumn, int endColumn, int canvasHeight)
620   {
621     int charHeight = av.getCharHeight();
622     int charWidth = av.getCharWidth();
623
624     ViewportRanges ranges = av.getRanges();
625     int viewportWidth = ranges.getViewportWidth();
626
627     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
628
629     /*
630      * move right before drawing by the width of the scale left (if any)
631      * plus column offset from left margin (usually zero, but may be non-zero
632      * when fast painting is drawing just a few columns)
633      */
634     int xOffset = labelWidthWest
635             + ((startColumn - ranges.getStartRes()) % viewportWidth)
636             * charWidth;
637     g.translate(xOffset, 0);
638
639     // When printing we have an extra clipped region,
640     // the Printable page which we need to account for here
641     Shape clip = g.getClip();
642
643     if (clip == null)
644     {
645       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
646     }
647     else
648     {
649       g.setClip(0, (int) clip.getBounds().getY(),
650               viewportWidth * charWidth, (int) clip.getBounds().getHeight());
651     }
652
653     /*
654      * white fill the region to be drawn (so incremental fast paint doesn't
655      * scribble over an existing image)
656      */
657     gg.setColor(Color.white);
658     gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
659             wrappedRepeatHeightPx);
660
661     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
662             ypos);
663
664     int cHeight = av.getAlignment().getHeight() * charHeight;
665
666     if (av.isShowAnnotation())
667     {
668       g.translate(0, cHeight + ypos + 3);
669       if (annotations == null)
670       {
671         annotations = new AnnotationPanel(av);
672       }
673
674       annotations.renderer.drawComponent(annotations, av, g, -1,
675               startColumn, endx + 1);
676       g.translate(0, -cHeight - ypos - 3);
677     }
678     g.setClip(clip);
679     g.translate(-xOffset, 0);
680   }
681
682   /**
683    * Draws scales left, right and above (if shown), and any hidden column
684    * markers, on all widths of the wrapped alignment
685    * 
686    * @param g
687    * @param startColumn
688    */
689   protected void drawWrappedDecorators(Graphics g, int startColumn)
690   {
691     int charWidth = av.getCharWidth();
692
693     g.setFont(av.getFont());
694     g.setColor(Color.black);
695
696     int ypos = wrappedSpaceAboveAlignment;
697     ViewportRanges ranges = av.getRanges();
698     int viewportWidth = ranges.getViewportWidth();
699     int maxWidth = ranges.getVisibleAlignmentWidth();
700     int widthsDrawn = 0;
701     while (widthsDrawn < wrappedVisibleWidths)
702     {
703       int endColumn = Math.min(maxWidth, startColumn + viewportWidth - 1);
704
705       if (av.getScaleLeftWrapped())
706       {
707         drawVerticalScale(g, startColumn, endColumn - 1, ypos, true);
708       }
709
710       if (av.getScaleRightWrapped())
711       {
712         int x = labelWidthWest + viewportWidth * charWidth;
713         g.translate(x, 0);
714         drawVerticalScale(g, startColumn, endColumn, ypos, false);
715         g.translate(-x, 0);
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       g.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 }