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