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