JAL-2674 2nd attempt to refactor seqcanvas drawing to iterator
[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       final int screenYMax = endRes - startRes;
856       int blockStart = startRes;
857       int blockEnd = endRes; // equals blockStart + screenYMax - screenY;
858
859       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
860       VisibleBlocksVisBoundsIterator regions = (VisibleBlocksVisBoundsIterator) hidden
861               .getVisibleBlocksIterator(startRes, endRes, true);// hidden.iterator();
862       while (regions.hasNext())
863       {
864         int[] region = regions.next();
865         blockEnd = region[1];
866         blockStart = region[0];
867 /*        int hideStart = region[0];
868         int hideEnd = region[1];
869
870         if (hideStart <= blockStart)
871         {
872           blockStart += (hideEnd - hideStart) + 1; // convert startRes to an
873                                                    // absolute value
874           blockEnd += (hideEnd - hideStart) + 1;
875           continue;
876         }
877 */
878         /*
879          * draw up to just before the next hidden region, or the end of
880          * the visible region, whichever comes first
881          */
882 //        blockEnd = Math.min(hideStart - 1,
883 //                blockEnd);
884         g1.translate(screenY * charWidth, 0);
885
886         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
887
888         /*
889          * draw the downline of the hidden column marker (ScalePanel draws the
890          * triangle on top) if we reached it
891          */
892         if (av.getShowHiddenMarkers()
893                 && (regions.hasNext() || regions.endsAtHidden()))// blockEnd ==
894                                                            // hideStart - 1)
895         {
896           g1.setColor(Color.blue);
897
898           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
899                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
900                   (endSeq - startSeq + 1) * charHeight + yOffset);
901         }
902
903         g1.translate(-screenY * charWidth, 0);
904         screenY += blockEnd - blockStart + 1;
905         /*       blockStart = hideEnd + 1;
906         blockEnd = blockStart + screenYMax - screenY;
907         
908         if (screenY > screenYMax)
909         {
910           // already rendered last block
911           return;
912         }*/
913       }
914
915       /*      if (screenY <= screenYMax)
916       {
917         // remaining visible region to render
918         blockEnd = blockStart + screenYMax - screenY;
919         g1.translate(screenY * charWidth, 0);
920         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
921       
922         g1.translate(-screenY * charWidth, 0);
923       }*/
924     }
925
926   }
927
928   /**
929    * Draws a region of the visible alignment
930    * 
931    * @param g1
932    * @param startRes
933    *          offset of the first column in the visible region (0..)
934    * @param endRes
935    *          offset of the last column in the visible region (0..)
936    * @param startSeq
937    *          offset of the first sequence in the visible region (0..)
938    * @param endSeq
939    *          offset of the last sequence in the visible region (0..)
940    * @param yOffset
941    *          vertical offset at which to draw (for wrapped alignments)
942    */
943   private void draw(Graphics g, int startRes, int endRes, int startSeq,
944           int endSeq, int offset)
945   {
946     g.setFont(av.getFont());
947     seqRdr.prepare(g, av.isRenderGaps());
948
949     SequenceI nextSeq;
950
951     // / First draw the sequences
952     // ///////////////////////////
953     for (int i = startSeq; i <= endSeq; i++)
954     {
955       nextSeq = av.getAlignment().getSequenceAt(i);
956       if (nextSeq == null)
957       {
958         // occasionally, a race condition occurs such that the alignment row is
959         // empty
960         continue;
961       }
962       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
963               startRes, endRes, offset + ((i - startSeq) * charHeight));
964
965       if (av.isShowSequenceFeatures())
966       {
967         fr.drawSequence(g, nextSeq, startRes, endRes,
968                 offset + ((i - startSeq) * charHeight), false);
969       }
970
971       /*
972        * highlight search Results once sequence has been drawn
973        */
974       if (av.hasSearchResults())
975       {
976         SearchResultsI searchResults = av.getSearchResults();
977         int[] visibleResults = searchResults.getResults(nextSeq,
978                 startRes, endRes);
979         if (visibleResults != null)
980         {
981           for (int r = 0; r < visibleResults.length; r += 2)
982           {
983             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
984                     visibleResults[r + 1], (visibleResults[r] - startRes)
985                             * charWidth, offset
986                             + ((i - startSeq) * charHeight));
987           }
988         }
989       }
990
991       if (av.cursorMode && cursorY == i && cursorX >= startRes
992               && cursorX <= endRes)
993       {
994         seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
995                 offset + ((i - startSeq) * charHeight));
996       }
997     }
998
999     if (av.getSelectionGroup() != null
1000             || av.getAlignment().getGroups().size() > 0)
1001     {
1002       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1003     }
1004
1005   }
1006
1007   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1008           int startSeq, int endSeq, int offset)
1009   {
1010     Graphics2D g = (Graphics2D) g1;
1011     //
1012     // ///////////////////////////////////
1013     // Now outline any areas if necessary
1014     // ///////////////////////////////////
1015
1016     SequenceGroup group = null;
1017     int groupIndex = -1;
1018
1019     if (av.getAlignment().getGroups().size() > 0)
1020     {
1021       group = av.getAlignment().getGroups().get(0);
1022       groupIndex = 0;
1023     }
1024
1025     if (group != null)
1026     {
1027       g.setStroke(new BasicStroke());
1028       g.setColor(group.getOutlineColour());
1029       
1030       do
1031       {
1032         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1033                 endSeq, offset);
1034
1035         groupIndex++;
1036
1037         g.setStroke(new BasicStroke());
1038
1039         if (groupIndex >= av.getAlignment().getGroups().size())
1040         {
1041           break;
1042         }
1043
1044         group = av.getAlignment().getGroups().get(groupIndex);
1045
1046       } while (groupIndex < av.getAlignment().getGroups().size());
1047
1048     }
1049
1050   }
1051
1052
1053   /*
1054    * Draw the selection group as a separate image and overlay
1055    */
1056   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1057           int startSeq, int endSeq)
1058   {
1059     // get a new image of the correct size
1060     BufferedImage selectionImage = setupImage();
1061
1062     if (selectionImage == null)
1063     {
1064       return null;
1065     }
1066
1067     SequenceGroup group = av.getSelectionGroup();
1068     if (group == null)
1069     {
1070       // nothing to draw
1071       return null;
1072     }
1073
1074     // set up drawing colour
1075     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1076
1077     setupSelectionGroup(g, selectionImage);
1078
1079     if (!av.getWrapAlignment())
1080     {
1081       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1082               0);
1083     }
1084     else
1085     {
1086       drawWrappedSelection(g, group, getWidth(), getHeight(),
1087               av.getRanges().getStartRes());
1088     }
1089
1090     g.dispose();
1091     return selectionImage;
1092   }
1093
1094   /*
1095    * Set up graphics for selection group
1096    */
1097   private void setupSelectionGroup(Graphics2D g,
1098           BufferedImage selectionImage)
1099   {
1100     // set background to transparent
1101     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1102     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1103
1104     // set up foreground to draw red dashed line
1105     g.setComposite(AlphaComposite.Src);
1106     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1107             BasicStroke.JOIN_ROUND, 3f, new float[]
1108     { 5f, 3f }, 0f));
1109     g.setColor(Color.RED);
1110   }
1111
1112   /*
1113    * Draw a selection group over an unwrapped alignment
1114    * @param g graphics object to draw with
1115    * @param group selection group
1116    * @param startRes start residue of area to draw
1117    * @param endRes end residue of area to draw
1118    * @param startSeq start sequence of area to draw
1119    * @param endSeq end sequence of area to draw
1120    * @param offset vertical offset (used when called from wrapped alignment code)
1121    */
1122   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1123           int startRes, int endRes, int startSeq, int endSeq, int offset)
1124   {
1125     if (!av.hasHiddenColumns())
1126     {
1127       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
1128               offset);
1129     }
1130     else
1131     {
1132       // package into blocks of visible columns
1133       int screenY = 0;
1134       int blockStart = startRes;
1135       int blockEnd = endRes;
1136
1137       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
1138       Iterator<int[]> regions = hidden.iterator();
1139       while (regions.hasNext())
1140       {
1141         int[] region = regions.next();
1142         int hideStart = region[0];
1143         int hideEnd = region[1];
1144
1145         if (hideStart <= blockStart)
1146         {
1147           blockStart += (hideEnd - hideStart) + 1;
1148           continue;
1149         }
1150
1151         blockEnd = hideStart - 1;
1152
1153         g.translate(screenY * charWidth, 0);
1154         drawPartialGroupOutline(g, group,
1155                 blockStart, blockEnd, startSeq, endSeq, offset);
1156
1157         g.translate(-screenY * charWidth, 0);
1158         screenY += blockEnd - blockStart + 1;
1159         blockStart = hideEnd + 1;
1160
1161         if (screenY > (endRes - startRes))
1162         {
1163           // already rendered last block
1164           break;
1165         }
1166       }
1167
1168       if (screenY <= (endRes - startRes))
1169       {
1170         // remaining visible region to render
1171         blockEnd = blockStart + (endRes - startRes) - screenY;
1172         g.translate(screenY * charWidth, 0);
1173         drawPartialGroupOutline(g, group,
1174                 blockStart, blockEnd, startSeq, endSeq, offset);
1175         
1176         g.translate(-screenY * charWidth, 0);
1177       }
1178     }
1179   }
1180
1181   /*
1182    * Draw the selection group as a separate image and overlay
1183    */
1184   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
1185           int startRes, int endRes, int startSeq, int endSeq,
1186           int verticalOffset)
1187   {
1188     int visWidth = (endRes - startRes + 1) * charWidth;
1189
1190     int oldY = -1;
1191     int i = 0;
1192     boolean inGroup = false;
1193     int top = -1;
1194     int bottom = -1;
1195
1196     int sx = -1;
1197     int sy = -1;
1198     int xwidth = -1;
1199
1200     for (i = startSeq; i <= endSeq; i++)
1201     {
1202       // position of start residue of group relative to startRes, in pixels
1203       sx = (group.getStartRes() - startRes) * charWidth;
1204
1205       // width of group in pixels
1206       xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1207               - 1;
1208
1209       sy = verticalOffset + (i - startSeq) * charHeight;
1210
1211       if (sx + xwidth < 0 || sx > visWidth)
1212       {
1213         continue;
1214       }
1215
1216       if ((sx <= (endRes - startRes) * charWidth)
1217               && group.getSequences(null)
1218                       .contains(av.getAlignment().getSequenceAt(i)))
1219       {
1220         if ((bottom == -1) && !group.getSequences(null)
1221                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1222         {
1223           bottom = sy + charHeight;
1224         }
1225
1226         if (!inGroup)
1227         {
1228           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1229                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1230           {
1231             top = sy;
1232           }
1233
1234           oldY = sy;
1235           inGroup = true;
1236         }
1237       }
1238       else
1239       {
1240         if (inGroup)
1241         {
1242           // if start position is visible, draw vertical line to left of
1243           // group
1244           if (sx >= 0 && sx < visWidth)
1245           {
1246             g.drawLine(sx, oldY, sx, sy);
1247           }
1248
1249           // if end position is visible, draw vertical line to right of
1250           // group
1251           if (sx + xwidth < visWidth)
1252           {
1253             g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1254           }
1255
1256           if (sx < 0)
1257           {
1258             xwidth += sx;
1259             sx = 0;
1260           }
1261
1262           // don't let width extend beyond current block, or group extent
1263           // fixes JAL-2672
1264           if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1265           {
1266             xwidth = (endRes - startRes + 1) * charWidth - sx;
1267           }
1268           
1269           // draw horizontal line at top of group
1270           if (top != -1)
1271           {
1272             g.drawLine(sx, top, sx + xwidth, top);
1273             top = -1;
1274           }
1275
1276           // draw horizontal line at bottom of group
1277           if (bottom != -1)
1278           {
1279             g.drawLine(sx, bottom, sx + xwidth, bottom);
1280             bottom = -1;
1281           }
1282
1283           inGroup = false;
1284         }
1285       }
1286     }
1287
1288     if (inGroup)
1289     {
1290       sy = verticalOffset + ((i - startSeq) * charHeight);
1291       if (sx >= 0 && sx < visWidth)
1292       {
1293         g.drawLine(sx, oldY, sx, sy);
1294       }
1295
1296       if (sx + xwidth < visWidth)
1297       {
1298         g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1299       }
1300
1301       if (sx < 0)
1302       {
1303         xwidth += sx;
1304         sx = 0;
1305       }
1306
1307       if (sx + xwidth > visWidth)
1308       {
1309         xwidth = visWidth;
1310       }
1311       else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1312       {
1313         xwidth = (endRes - startRes + 1) * charWidth;
1314       }
1315
1316       if (top != -1)
1317       {
1318         g.drawLine(sx, top, sx + xwidth, top);
1319         top = -1;
1320       }
1321
1322       if (bottom != -1)
1323       {
1324         g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
1325         bottom = -1;
1326       }
1327
1328       inGroup = false;
1329     }
1330   }
1331   
1332   /**
1333    * Highlights search results in the visible region by rendering as white text
1334    * on a black background. Any previous highlighting is removed. Answers true
1335    * if any highlight was left on the visible alignment (so status bar should be
1336    * set to match), else false.
1337    * <p>
1338    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1339    * alignment had to be scrolled to show the highlighted region, then it should
1340    * be fully redrawn, otherwise a fast paint can be performed. This argument
1341    * could be removed if fast paint of scrolled wrapped alignment is coded in
1342    * future (JAL-2609).
1343    * 
1344    * @param results
1345    * @param noFastPaint
1346    * @return
1347    */
1348   public boolean highlightSearchResults(SearchResultsI results,
1349           boolean noFastPaint)
1350   {
1351     if (fastpainting)
1352     {
1353       return false;
1354     }
1355     boolean wrapped = av.getWrapAlignment();
1356
1357     try
1358     {
1359       fastPaint = !noFastPaint;
1360       fastpainting = fastPaint;
1361
1362       updateViewport();
1363
1364       /*
1365        * to avoid redrawing the whole visible region, we instead
1366        * redraw just the minimal regions to remove previous highlights
1367        * and add new ones
1368        */
1369       SearchResultsI previous = av.getSearchResults();
1370       av.setSearchResults(results);
1371       boolean redrawn = false;
1372       boolean drawn = false;
1373       if (wrapped)
1374       {
1375         redrawn = drawMappedPositionsWrapped(previous);
1376         drawn = drawMappedPositionsWrapped(results);
1377         redrawn |= drawn;
1378       }
1379       else
1380       {
1381         redrawn = drawMappedPositions(previous);
1382         drawn = drawMappedPositions(results);
1383         redrawn |= drawn;
1384       }
1385
1386       /*
1387        * if highlights were either removed or added, repaint
1388        */
1389       if (redrawn)
1390       {
1391         repaint();
1392       }
1393
1394       /*
1395        * return true only if highlights were added
1396        */
1397       return drawn;
1398
1399     } finally
1400     {
1401       fastpainting = false;
1402     }
1403   }
1404
1405   /**
1406    * Redraws the minimal rectangle in the visible region (if any) that includes
1407    * mapped positions of the given search results. Whether or not positions are
1408    * highlighted depends on the SearchResults set on the Viewport. This allows
1409    * this method to be called to either clear or set highlighting. Answers true
1410    * if any positions were drawn (in which case a repaint is still required),
1411    * else false.
1412    * 
1413    * @param results
1414    * @return
1415    */
1416   protected boolean drawMappedPositions(SearchResultsI results)
1417   {
1418     if (results == null)
1419     {
1420       return false;
1421     }
1422
1423     /*
1424      * calculate the minimal rectangle to redraw that 
1425      * includes both new and existing search results
1426      */
1427     int firstSeq = Integer.MAX_VALUE;
1428     int lastSeq = -1;
1429     int firstCol = Integer.MAX_VALUE;
1430     int lastCol = -1;
1431     boolean matchFound = false;
1432
1433     ViewportRanges ranges = av.getRanges();
1434     int firstVisibleColumn = ranges.getStartRes();
1435     int lastVisibleColumn = ranges.getEndRes();
1436     AlignmentI alignment = av.getAlignment();
1437     if (av.hasHiddenColumns())
1438     {
1439       firstVisibleColumn = alignment.getHiddenColumns()
1440               .adjustForHiddenColumns(firstVisibleColumn);
1441       lastVisibleColumn = alignment.getHiddenColumns()
1442               .adjustForHiddenColumns(lastVisibleColumn);
1443     }
1444
1445     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1446             .getEndSeq(); seqNo++)
1447     {
1448       SequenceI seq = alignment.getSequenceAt(seqNo);
1449
1450       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1451               lastVisibleColumn);
1452       if (visibleResults != null)
1453       {
1454         for (int i = 0; i < visibleResults.length - 1; i += 2)
1455         {
1456           int firstMatchedColumn = visibleResults[i];
1457           int lastMatchedColumn = visibleResults[i + 1];
1458           if (firstMatchedColumn <= lastVisibleColumn
1459                   && lastMatchedColumn >= firstVisibleColumn)
1460           {
1461             /*
1462              * found a search results match in the visible region - 
1463              * remember the first and last sequence matched, and the first
1464              * and last visible columns in the matched positions
1465              */
1466             matchFound = true;
1467             firstSeq = Math.min(firstSeq, seqNo);
1468             lastSeq = Math.max(lastSeq, seqNo);
1469             firstMatchedColumn = Math.max(firstMatchedColumn,
1470                     firstVisibleColumn);
1471             lastMatchedColumn = Math.min(lastMatchedColumn,
1472                     lastVisibleColumn);
1473             firstCol = Math.min(firstCol, firstMatchedColumn);
1474             lastCol = Math.max(lastCol, lastMatchedColumn);
1475           }
1476         }
1477       }
1478     }
1479
1480     if (matchFound)
1481     {
1482       if (av.hasHiddenColumns())
1483       {
1484         firstCol = alignment.getHiddenColumns()
1485                 .findColumnPosition(firstCol);
1486         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1487       }
1488       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1489       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1490       gg.translate(transX, transY);
1491       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1492       gg.translate(-transX, -transY);
1493     }
1494
1495     return matchFound;
1496   }
1497
1498   @Override
1499   public void propertyChange(PropertyChangeEvent evt)
1500   {
1501     String eventName = evt.getPropertyName();
1502
1503     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1504     {
1505       fastPaint = true;
1506       repaint();
1507     }
1508     else if (av.getWrapAlignment())
1509     {
1510       if (eventName.equals(ViewportRanges.STARTRES))
1511       {
1512         repaint();
1513       }
1514     }
1515     else
1516     {
1517       int scrollX = 0;
1518       if (eventName.equals(ViewportRanges.STARTRES))
1519       {
1520         // Make sure we're not trying to draw a panel
1521         // larger than the visible window
1522         ViewportRanges vpRanges = av.getRanges();
1523         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1524         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1525         if (scrollX > range)
1526         {
1527           scrollX = range;
1528         }
1529         else if (scrollX < -range)
1530         {
1531           scrollX = -range;
1532         }
1533       }
1534
1535       // Both scrolling and resizing change viewport ranges: scrolling changes
1536       // both start and end points, but resize only changes end values.
1537       // Here we only want to fastpaint on a scroll, with resize using a normal
1538       // paint, so scroll events are identified as changes to the horizontal or
1539       // vertical start value.
1540       if (eventName.equals(ViewportRanges.STARTRES))
1541       {
1542         // scroll - startres and endres both change
1543         fastPaint(scrollX, 0);
1544       }
1545       else if (eventName.equals(ViewportRanges.STARTSEQ))
1546       {
1547         // scroll
1548         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1549       }
1550     }
1551   }
1552
1553   /**
1554    * Redraws any positions in the search results in the visible region of a
1555    * wrapped alignment. Any highlights are drawn depending on the search results
1556    * set on the Viewport, not the <code>results</code> argument. This allows
1557    * this method to be called either to clear highlights (passing the previous
1558    * search results), or to draw new highlights.
1559    * 
1560    * @param results
1561    * @return
1562    */
1563   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1564   {
1565     if (results == null)
1566     {
1567       return false;
1568     }
1569   
1570     boolean matchFound = false;
1571
1572     int wrappedWidth = av.getWrappedWidth();
1573     int wrappedHeight = getRepeatHeightWrapped();
1574
1575     ViewportRanges ranges = av.getRanges();
1576     int canvasHeight = getHeight();
1577     int repeats = canvasHeight / wrappedHeight;
1578     if (canvasHeight / wrappedHeight > 0)
1579     {
1580       repeats++;
1581     }
1582
1583     int firstVisibleColumn = ranges.getStartRes();
1584     int lastVisibleColumn = ranges.getStartRes() + repeats
1585             * ranges.getViewportWidth() - 1;
1586
1587     AlignmentI alignment = av.getAlignment();
1588     if (av.hasHiddenColumns())
1589     {
1590       firstVisibleColumn = alignment.getHiddenColumns()
1591               .adjustForHiddenColumns(firstVisibleColumn);
1592       lastVisibleColumn = alignment.getHiddenColumns()
1593               .adjustForHiddenColumns(lastVisibleColumn);
1594     }
1595
1596     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1597
1598     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1599             .getEndSeq(); seqNo++)
1600     {
1601       SequenceI seq = alignment.getSequenceAt(seqNo);
1602
1603       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1604               lastVisibleColumn);
1605       if (visibleResults != null)
1606       {
1607         for (int i = 0; i < visibleResults.length - 1; i += 2)
1608         {
1609           int firstMatchedColumn = visibleResults[i];
1610           int lastMatchedColumn = visibleResults[i + 1];
1611           if (firstMatchedColumn <= lastVisibleColumn
1612                   && lastMatchedColumn >= firstVisibleColumn)
1613           {
1614             /*
1615              * found a search results match in the visible region
1616              */
1617             firstMatchedColumn = Math.max(firstMatchedColumn,
1618                     firstVisibleColumn);
1619             lastMatchedColumn = Math.min(lastMatchedColumn,
1620                     lastVisibleColumn);
1621
1622             /*
1623              * draw each mapped position separately (as contiguous positions may
1624              * wrap across lines)
1625              */
1626             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1627             {
1628               int displayColumn = mappedPos;
1629               if (av.hasHiddenColumns())
1630               {
1631                 displayColumn = alignment.getHiddenColumns()
1632                         .findColumnPosition(displayColumn);
1633               }
1634
1635               /*
1636                * transX: offset from left edge of canvas to residue position
1637                */
1638               int transX = labelWidthWest
1639                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1640                       * av.getCharWidth();
1641
1642               /*
1643                * transY: offset from top edge of canvas to residue position
1644                */
1645               int transY = gapHeight;
1646               transY += (displayColumn - ranges.getStartRes())
1647                       / wrappedWidth * wrappedHeight;
1648               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1649
1650               /*
1651                * yOffset is from graphics origin to start of visible region
1652                */
1653               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1654               if (transY < getHeight())
1655               {
1656                 matchFound = true;
1657                 gg.translate(transX, transY);
1658                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1659                         yOffset);
1660                 gg.translate(-transX, -transY);
1661               }
1662             }
1663           }
1664         }
1665       }
1666     }
1667   
1668     return matchFound;
1669   }
1670
1671   /**
1672    * Answers the height in pixels of a repeating section of the wrapped
1673    * alignment, including space above, scale above if shown, sequences, and
1674    * annotation panel if shown
1675    * 
1676    * @return
1677    */
1678   protected int getRepeatHeightWrapped()
1679   {
1680     // gap (and maybe scale) above
1681     int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1682
1683     // add sequences
1684     repeatHeight += av.getRanges().getViewportHeight() * charHeight;
1685
1686     // add annotations panel height if shown
1687     repeatHeight += getAnnotationHeight();
1688
1689     return repeatHeight;
1690   }
1691 }