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