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