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