858b8c8b808572d0ccb6bed7a7665c6c651d798f
[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.BasicStroke;
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.awt.RenderingHints;
40 import java.awt.Shape;
41 import java.awt.image.BufferedImage;
42 import java.beans.PropertyChangeEvent;
43 import java.util.List;
44
45 import javax.swing.JComponent;
46
47 /**
48  * DOCUMENT ME!
49  * 
50  * @author $author$
51  * @version $Revision$
52  */
53 public class SeqCanvas extends JComponent implements ViewportListenerI
54 {
55   final FeatureRenderer fr;
56
57   final SequenceRenderer sr;
58
59   BufferedImage img;
60
61   Graphics2D gg;
62
63   int imgWidth;
64
65   int imgHeight;
66
67   AlignViewport av;
68
69   boolean fastPaint = false;
70
71   int LABEL_WEST;
72
73   int LABEL_EAST;
74
75   int cursorX = 0;
76
77   int cursorY = 0;
78
79   /**
80    * Creates a new SeqCanvas object.
81    * 
82    * @param av
83    *          DOCUMENT ME!
84    */
85   public SeqCanvas(AlignmentPanel ap)
86   {
87     this.av = ap.av;
88     updateViewport();
89     fr = new FeatureRenderer(ap);
90     sr = new SequenceRenderer(av);
91     setLayout(new BorderLayout());
92     PaintRefresher.Register(this, av.getSequenceSetId());
93     setBackground(Color.white);
94
95     av.getRanges().addPropertyChangeListener(this);
96   }
97
98   public SequenceRenderer getSequenceRenderer()
99   {
100     return sr;
101   }
102
103   public FeatureRenderer getFeatureRenderer()
104   {
105     return fr;
106   }
107
108   int charHeight = 0, charWidth = 0;
109
110   private void updateViewport()
111   {
112     charHeight = av.getCharHeight();
113     charWidth = av.getCharWidth();
114   }
115
116   /**
117    * DOCUMENT ME!
118    * 
119    * @param g
120    *          DOCUMENT ME!
121    * @param startx
122    *          DOCUMENT ME!
123    * @param endx
124    *          DOCUMENT ME!
125    * @param ypos
126    *          DOCUMENT ME!
127    */
128   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
129   {
130     updateViewport();
131     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
132             endx))
133     {
134       int mpos = mark.column; // (i - startx - 1)
135       if (mpos < 0)
136       {
137         continue;
138       }
139       String mstring = mark.text;
140
141       if (mark.major)
142       {
143         if (mstring != null)
144         {
145           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
146         }
147         g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
148                 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
149                 ypos - 2);
150       }
151     }
152   }
153
154   /**
155    * DOCUMENT ME!
156    * 
157    * @param g
158    *          DOCUMENT ME!
159    * @param startx
160    *          DOCUMENT ME!
161    * @param endx
162    *          DOCUMENT ME!
163    * @param ypos
164    *          DOCUMENT ME!
165    */
166   void drawWestScale(Graphics g, int startx, int endx, int ypos)
167   {
168     FontMetrics fm = getFontMetrics(av.getFont());
169     ypos += charHeight;
170
171     if (av.hasHiddenColumns())
172     {
173       startx = av.getAlignment().getHiddenColumns()
174               .adjustForHiddenColumns(startx);
175       endx = av.getAlignment().getHiddenColumns()
176               .adjustForHiddenColumns(endx);
177     }
178
179     int maxwidth = av.getAlignment().getWidth();
180     if (av.hasHiddenColumns())
181     {
182       maxwidth = av.getAlignment().getHiddenColumns()
183               .findColumnPosition(maxwidth) - 1;
184     }
185
186     // WEST SCALE
187     for (int i = 0; i < av.getAlignment().getHeight(); i++)
188     {
189       SequenceI seq = av.getAlignment().getSequenceAt(i);
190       int index = startx;
191       int value = -1;
192
193       while (index < endx)
194       {
195         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
196         {
197           index++;
198
199           continue;
200         }
201
202         value = av.getAlignment().getSequenceAt(i).findPosition(index);
203
204         break;
205       }
206
207       if (value != -1)
208       {
209         int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
210                 - charWidth / 2;
211         g.drawString(value + "", x, (ypos + (i * charHeight))
212                 - (charHeight / 5));
213       }
214     }
215   }
216
217   /**
218    * DOCUMENT ME!
219    * 
220    * @param g
221    *          DOCUMENT ME!
222    * @param startx
223    *          DOCUMENT ME!
224    * @param endx
225    *          DOCUMENT ME!
226    * @param ypos
227    *          DOCUMENT ME!
228    */
229   void drawEastScale(Graphics g, int startx, int endx, int ypos)
230   {
231     ypos += charHeight;
232
233     if (av.hasHiddenColumns())
234     {
235       endx = av.getAlignment().getHiddenColumns()
236               .adjustForHiddenColumns(endx);
237     }
238
239     SequenceI seq;
240     // EAST SCALE
241     for (int i = 0; i < av.getAlignment().getHeight(); i++)
242     {
243       seq = av.getAlignment().getSequenceAt(i);
244       int index = endx;
245       int value = -1;
246
247       while (index > startx)
248       {
249         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
250         {
251           index--;
252
253           continue;
254         }
255
256         value = seq.findPosition(index);
257
258         break;
259       }
260
261       if (value != -1)
262       {
263         g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
264                 - (charHeight / 5));
265       }
266     }
267   }
268
269   boolean fastpainting = false;
270
271   /**
272    * need to make this thread safe move alignment rendering in response to
273    * slider adjustment
274    * 
275    * @param horizontal
276    *          shift along
277    * @param vertical
278    *          shift up or down in repaint
279    */
280   public void fastPaint(int horizontal, int vertical)
281   {
282     if (fastpainting || gg == null)
283     {
284       return;
285     }
286     fastpainting = true;
287     fastPaint = true;
288     updateViewport();
289     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
290             imgHeight, -horizontal * charWidth, -vertical * charHeight);
291
292     ViewportRanges ranges = av.getRanges();
293     int startRes = ranges.getStartRes();
294     int endRes = ranges.getEndRes();
295     int startSeq = ranges.getStartSeq();
296     int endSeq = ranges.getEndSeq();
297     int transX = 0;
298     int transY = 0;
299
300     if (horizontal > 0) // scrollbar pulled right, image to the left
301     {
302       transX = (endRes - startRes - horizontal) * charWidth;
303       startRes = endRes - horizontal;
304     }
305     else if (horizontal < 0)
306     {
307       endRes = startRes - horizontal;
308     }
309     else if (vertical > 0) // scroll down
310     {
311       startSeq = endSeq - vertical;
312
313       if (startSeq < ranges.getStartSeq())
314       { // ie scrolling too fast, more than a page at a time
315         startSeq = ranges.getStartSeq();
316       }
317       else
318       {
319         transY = imgHeight - ((vertical + 1) * charHeight);
320       }
321     }
322     else if (vertical < 0)
323     {
324       endSeq = startSeq - vertical;
325
326       if (endSeq > ranges.getEndSeq())
327       {
328         endSeq = ranges.getEndSeq();
329       }
330     }
331
332     gg.translate(transX, transY);
333     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
334     gg.translate(-transX, -transY);
335
336     repaint();
337     fastpainting = false;
338   }
339
340   @Override
341   public void paintComponent(Graphics g)
342   {
343     updateViewport();
344     BufferedImage lcimg = img; // take reference since other threads may null
345     // img and call later.
346     super.paintComponent(g);
347
348     if (lcimg != null
349             && (fastPaint
350                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
351                     .getClipBounds().height)))
352     {
353       g.drawImage(lcimg, 0, 0, this);
354       fastPaint = false;
355       return;
356     }
357
358     // this draws the whole of the alignment
359     imgWidth = getWidth();
360     imgHeight = getHeight();
361
362     imgWidth -= (imgWidth % charWidth);
363     imgHeight -= (imgHeight % charHeight);
364
365     if ((imgWidth < 1) || (imgHeight < 1))
366     {
367       return;
368     }
369
370     if (lcimg == null || imgWidth != lcimg.getWidth()
371             || imgHeight != lcimg.getHeight())
372     {
373       try
374       {
375         lcimg = img = new BufferedImage(imgWidth, imgHeight,
376                 BufferedImage.TYPE_INT_RGB);
377         gg = (Graphics2D) img.getGraphics();
378         gg.setFont(av.getFont());
379       } catch (OutOfMemoryError er)
380       {
381         System.gc();
382         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
383         new OOMWarning("Creating alignment image for display", er);
384
385         return;
386       }
387     }
388
389     if (av.antiAlias)
390     {
391       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
392               RenderingHints.VALUE_ANTIALIAS_ON);
393     }
394
395     gg.setColor(Color.white);
396     gg.fillRect(0, 0, imgWidth, imgHeight);
397
398     ViewportRanges ranges = av.getRanges();
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     g.drawImage(lcimg, 0, 0, this);
410
411   }
412
413   /**
414    * DOCUMENT ME!
415    * 
416    * @param cwidth
417    *          DOCUMENT ME!
418    * 
419    * @return DOCUMENT ME!
420    */
421   public int getWrappedCanvasWidth(int cwidth)
422   {
423     FontMetrics fm = getFontMetrics(av.getFont());
424
425     LABEL_EAST = 0;
426     LABEL_WEST = 0;
427
428     if (av.getScaleRightWrapped())
429     {
430       LABEL_EAST = fm.stringWidth(getMask());
431     }
432
433     if (av.getScaleLeftWrapped())
434     {
435       LABEL_WEST = fm.stringWidth(getMask());
436     }
437
438     return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
439   }
440
441   /**
442    * Generates a string of zeroes.
443    * 
444    * @return String
445    */
446   String getMask()
447   {
448     String mask = "00";
449     int maxWidth = 0;
450     int tmp;
451     for (int i = 0; i < av.getAlignment().getHeight(); i++)
452     {
453       tmp = av.getAlignment().getSequenceAt(i).getEnd();
454       if (tmp > maxWidth)
455       {
456         maxWidth = tmp;
457       }
458     }
459
460     for (int i = maxWidth; i > 0; i /= 10)
461     {
462       mask += "0";
463     }
464     return mask;
465   }
466
467   /**
468    * DOCUMENT ME!
469    * 
470    * @param g
471    *          DOCUMENT ME!
472    * @param canvasWidth
473    *          DOCUMENT ME!
474    * @param canvasHeight
475    *          DOCUMENT ME!
476    * @param startRes
477    *          DOCUMENT ME!
478    */
479   public void drawWrappedPanel(Graphics g, int canvasWidth,
480           int canvasHeight, int startRes)
481   {
482     updateViewport();
483     AlignmentI al = av.getAlignment();
484
485     FontMetrics fm = getFontMetrics(av.getFont());
486
487     if (av.getScaleRightWrapped())
488     {
489       LABEL_EAST = fm.stringWidth(getMask());
490     }
491
492     if (av.getScaleLeftWrapped())
493     {
494       LABEL_WEST = fm.stringWidth(getMask());
495     }
496
497     int hgap = charHeight;
498     if (av.getScaleAboveWrapped())
499     {
500       hgap += charHeight;
501     }
502
503     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
504     int cHeight = av.getAlignment().getHeight() * charHeight;
505
506     av.setWrappedWidth(cWidth);
507
508     av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
509
510     int endx;
511     int ypos = hgap;
512     int maxwidth = av.getAlignment().getWidth() - 1;
513
514     if (av.hasHiddenColumns())
515     {
516       maxwidth = av.getAlignment().getHiddenColumns()
517               .findColumnPosition(maxwidth) - 1;
518     }
519
520     while ((ypos <= canvasHeight) && (startRes < maxwidth))
521     {
522       endx = startRes + cWidth - 1;
523
524       if (endx > maxwidth)
525       {
526         endx = maxwidth;
527       }
528
529       g.setFont(av.getFont());
530       g.setColor(Color.black);
531
532       if (av.getScaleLeftWrapped())
533       {
534         drawWestScale(g, startRes, endx, ypos);
535       }
536
537       if (av.getScaleRightWrapped())
538       {
539         g.translate(canvasWidth - LABEL_EAST, 0);
540         drawEastScale(g, startRes, endx, ypos);
541         g.translate(-(canvasWidth - LABEL_EAST), 0);
542       }
543
544       g.translate(LABEL_WEST, 0);
545
546       if (av.getScaleAboveWrapped())
547       {
548         drawNorthScale(g, startRes, endx, ypos);
549       }
550
551       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
552       {
553         g.setColor(Color.blue);
554         int res;
555         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
556         for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
557         {
558           res = hidden.findHiddenRegionPosition(i) - startRes;
559
560           if (res < 0 || res > endx - startRes)
561           {
562             continue;
563           }
564
565           gg.fillPolygon(
566                   new int[] { res * charWidth - charHeight / 4,
567                       res * charWidth + charHeight / 4, res * charWidth },
568                   new int[] { ypos - (charHeight / 2),
569                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
570                   3);
571
572         }
573       }
574
575       // When printing we have an extra clipped region,
576       // the Printable page which we need to account for here
577       Shape clip = g.getClip();
578
579       if (clip == null)
580       {
581         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
582       }
583       else
584       {
585         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
586                 (int) clip.getBounds().getHeight());
587       }
588
589       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
590
591       if (av.isShowAnnotation())
592       {
593         g.translate(0, cHeight + ypos + 3);
594         if (annotations == null)
595         {
596           annotations = new AnnotationPanel(av);
597         }
598
599         annotations.renderer.drawComponent(annotations, av, g, -1,
600                 startRes, endx + 1);
601         g.translate(0, -cHeight - ypos - 3);
602       }
603       g.setClip(clip);
604       g.translate(-LABEL_WEST, 0);
605
606       ypos += cHeight + getAnnotationHeight() + hgap;
607
608       startRes += cWidth;
609     }
610   }
611
612   AnnotationPanel annotations;
613
614   int getAnnotationHeight()
615   {
616     if (!av.isShowAnnotation())
617     {
618       return 0;
619     }
620
621     if (annotations == null)
622     {
623       annotations = new AnnotationPanel(av);
624     }
625
626     return annotations.adjustPanelHeight();
627   }
628
629   /**
630    * Draws the visible region of the alignment on the graphics context. If there
631    * are hidden column markers in the visible region, then each sub-region
632    * between the markers is drawn separately, followed by the hidden column
633    * marker.
634    * 
635    * @param g1
636    * @param startRes
637    *          offset of the first column in the visible region (0..)
638    * @param endRes
639    *          offset of the last column in the visible region (0..)
640    * @param startSeq
641    *          offset of the first sequence in the visible region (0..)
642    * @param endSeq
643    *          offset of the last sequence in the visible region (0..)
644    * @param yOffset
645    *          vertical offset at which to draw (for wrapped alignments)
646    */
647   public void drawPanel(Graphics g1, final int startRes, final int endRes,
648           final int startSeq, final int endSeq, final int yOffset)
649   {
650     updateViewport();
651     if (!av.hasHiddenColumns())
652     {
653       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
654     }
655     else
656     {
657       List<int[]> regions = av.getAlignment().getHiddenColumns()
658               .getHiddenRegions();
659
660       int screenY = 0;
661       final int screenYMax = endRes - startRes;
662       int blockStart = startRes;
663       int blockEnd = endRes;
664
665       for (int[] region : regions)
666       {
667         int hideStart = region[0];
668         int hideEnd = region[1];
669
670         if (hideStart <= blockStart)
671         {
672           blockStart += (hideEnd - hideStart) + 1;
673           continue;
674         }
675
676         /*
677          * draw up to just before the next hidden region, or the end of
678          * the visible region, whichever comes first
679          */
680         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
681                 - screenY);
682
683         g1.translate(screenY * charWidth, 0);
684
685         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
686
687         /*
688          * draw the downline of the hidden column marker (ScalePanel draws the
689          * triangle on top) if we reached it
690          */
691         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
692         {
693           g1.setColor(Color.blue);
694
695           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
696                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
697                   (endSeq - startSeq + 1) * charHeight + yOffset);
698         }
699
700         g1.translate(-screenY * charWidth, 0);
701         screenY += blockEnd - blockStart + 1;
702         blockStart = hideEnd + 1;
703
704         if (screenY > screenYMax)
705         {
706           // already rendered last block
707           return;
708         }
709       }
710
711       if (screenY <= screenYMax)
712       {
713         // remaining visible region to render
714         blockEnd = blockStart + screenYMax - screenY;
715         g1.translate(screenY * charWidth, 0);
716         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
717
718         g1.translate(-screenY * charWidth, 0);
719       }
720     }
721
722   }
723
724   /**
725    * Draws a region of the visible alignment
726    * 
727    * @param g1
728    * @param startRes
729    *          offset of the first column in the visible region (0..)
730    * @param endRes
731    *          offset of the last column in the visible region (0..)
732    * @param startSeq
733    *          offset of the first sequence in the visible region (0..)
734    * @param endSeq
735    *          offset of the last sequence in the visible region (0..)
736    * @param yOffset
737    *          vertical offset at which to draw (for wrapped alignments)
738    */
739   private void draw(Graphics g, int startRes, int endRes, int startSeq,
740           int endSeq, int offset)
741   {
742     g.setFont(av.getFont());
743     sr.prepare(g, av.isRenderGaps());
744
745     SequenceI nextSeq;
746
747     // / First draw the sequences
748     // ///////////////////////////
749     for (int i = startSeq; i <= endSeq; i++)
750     {
751       nextSeq = av.getAlignment().getSequenceAt(i);
752       if (nextSeq == null)
753       {
754         // occasionally, a race condition occurs such that the alignment row is
755         // empty
756         continue;
757       }
758       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
759               startRes, endRes, offset + ((i - startSeq) * charHeight));
760
761       if (av.isShowSequenceFeatures())
762       {
763         fr.drawSequence(g, nextSeq, startRes, endRes, offset
764                 + ((i - startSeq) * charHeight), false);
765       }
766
767       /*
768        * highlight search Results once sequence has been drawn
769        */
770       if (av.hasSearchResults())
771       {
772         SearchResultsI searchResults = av.getSearchResults();
773         int[] visibleResults = searchResults.getResults(nextSeq,
774                 startRes, endRes);
775         if (visibleResults != null)
776         {
777           for (int r = 0; r < visibleResults.length; r += 2)
778           {
779             sr.drawHighlightedText(nextSeq, visibleResults[r],
780                     visibleResults[r + 1], (visibleResults[r] - startRes)
781                             * charWidth, offset
782                             + ((i - startSeq) * charHeight));
783           }
784         }
785       }
786
787       if (av.cursorMode && cursorY == i && cursorX >= startRes
788               && cursorX <= endRes)
789       {
790         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
791                 offset + ((i - startSeq) * charHeight));
792       }
793     }
794
795     if (av.getSelectionGroup() != null
796             || av.getAlignment().getGroups().size() > 0)
797     {
798       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
799     }
800
801   }
802
803   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
804           int startSeq, int endSeq, int offset)
805   {
806     Graphics2D g = (Graphics2D) g1;
807     //
808     // ///////////////////////////////////
809     // Now outline any areas if necessary
810     // ///////////////////////////////////
811     SequenceGroup group = av.getSelectionGroup();
812
813     int sx = -1;
814     int sy = -1;
815     int ex = -1;
816     int groupIndex = -1;
817     int visWidth = (endRes - startRes + 1) * charWidth;
818
819     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
820     {
821       group = av.getAlignment().getGroups().get(0);
822       groupIndex = 0;
823     }
824
825     if (group != null)
826     {
827       do
828       {
829         int oldY = -1;
830         int i = 0;
831         boolean inGroup = false;
832         int top = -1;
833         int bottom = -1;
834
835         for (i = startSeq; i <= endSeq; i++)
836         {
837           sx = (group.getStartRes() - startRes) * charWidth;
838           sy = offset + ((i - startSeq) * charHeight);
839           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
840
841           if (sx + ex < 0 || sx > visWidth)
842           {
843             continue;
844           }
845
846           if ((sx <= (endRes - startRes) * charWidth)
847                   && group.getSequences(null).contains(
848                           av.getAlignment().getSequenceAt(i)))
849           {
850             if ((bottom == -1)
851                     && !group.getSequences(null).contains(
852                             av.getAlignment().getSequenceAt(i + 1)))
853             {
854               bottom = sy + charHeight;
855             }
856
857             if (!inGroup)
858             {
859               if (((top == -1) && (i == 0))
860                       || !group.getSequences(null).contains(
861                               av.getAlignment().getSequenceAt(i - 1)))
862               {
863                 top = sy;
864               }
865
866               oldY = sy;
867               inGroup = true;
868
869               if (group == av.getSelectionGroup())
870               {
871                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
872                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
873                         0f));
874                 g.setColor(Color.RED);
875               }
876               else
877               {
878                 g.setStroke(new BasicStroke());
879                 g.setColor(group.getOutlineColour());
880               }
881             }
882           }
883           else
884           {
885             if (inGroup)
886             {
887               if (sx >= 0 && sx < visWidth)
888               {
889                 g.drawLine(sx, oldY, sx, sy);
890               }
891
892               if (sx + ex < visWidth)
893               {
894                 g.drawLine(sx + ex, oldY, sx + ex, sy);
895               }
896
897               if (sx < 0)
898               {
899                 ex += sx;
900                 sx = 0;
901               }
902
903               if (sx + ex > visWidth)
904               {
905                 ex = visWidth;
906               }
907
908               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
909               {
910                 ex = (endRes - startRes + 1) * charWidth;
911               }
912
913               if (top != -1)
914               {
915                 g.drawLine(sx, top, sx + ex, top);
916                 top = -1;
917               }
918
919               if (bottom != -1)
920               {
921                 g.drawLine(sx, bottom, sx + ex, bottom);
922                 bottom = -1;
923               }
924
925               inGroup = false;
926             }
927           }
928         }
929
930         if (inGroup)
931         {
932           sy = offset + ((i - startSeq) * charHeight);
933           if (sx >= 0 && sx < visWidth)
934           {
935             g.drawLine(sx, oldY, sx, sy);
936           }
937
938           if (sx + ex < visWidth)
939           {
940             g.drawLine(sx + ex, oldY, sx + ex, sy);
941           }
942
943           if (sx < 0)
944           {
945             ex += sx;
946             sx = 0;
947           }
948
949           if (sx + ex > visWidth)
950           {
951             ex = visWidth;
952           }
953           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
954           {
955             ex = (endRes - startRes + 1) * charWidth;
956           }
957
958           if (top != -1)
959           {
960             g.drawLine(sx, top, sx + ex, top);
961             top = -1;
962           }
963
964           if (bottom != -1)
965           {
966             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
967             bottom = -1;
968           }
969
970           inGroup = false;
971         }
972
973         groupIndex++;
974
975         g.setStroke(new BasicStroke());
976
977         if (groupIndex >= av.getAlignment().getGroups().size())
978         {
979           break;
980         }
981
982         group = av.getAlignment().getGroups().get(groupIndex);
983
984       } while (groupIndex < av.getAlignment().getGroups().size());
985
986     }
987
988   }
989
990   /**
991    * Highlights search results in the visible region by rendering as white text
992    * on a black background. Any previous highlighting is removed.
993    * 
994    * @param results
995    */
996   public void highlightSearchResults(SearchResultsI results)
997   {
998     updateViewport();
999
1000     /*
1001      * for now, don't attempt fastpaint if wrapped format
1002      */
1003     if (av.getWrapAlignment())
1004     {
1005       av.setSearchResults(results);
1006       repaint();
1007       return;
1008     }
1009     
1010     fastpainting = true;
1011     fastPaint = true;
1012
1013     try
1014     {
1015       /*
1016        * to avoid redrawing the whole visible region, we instead
1017        * redraw just the minimal regions to remove previous highlights
1018        * and add new ones
1019        */
1020       SearchResultsI previous = av.getSearchResults();
1021       av.setSearchResults(results);
1022       boolean redrawn = drawMappedPositions(previous);
1023       redrawn |= drawMappedPositions(results);
1024       if (redrawn)
1025       {
1026         repaint();
1027       }
1028     } finally
1029     {
1030       fastpainting = false;
1031     }
1032   }
1033
1034   /**
1035    * Redraws the minimal rectangle in the visible region (if any) that includes
1036    * mapped positions of the given search results. Whether or not positions are
1037    * highlighted depends on the SearchResults set on the Viewport. This allows
1038    * this method to be called to either clear or set highlighting. Answers true
1039    * if any positions were drawn (in which case a repaint is still required),
1040    * else false.
1041    * 
1042    * @param results
1043    * @return
1044    */
1045   protected boolean drawMappedPositions(SearchResultsI results)
1046   {
1047     if (results == null)
1048     {
1049       return false;
1050     }
1051
1052     /*
1053      * calculate the minimal rectangle to redraw that 
1054      * includes both new and existing search results
1055      */
1056     int firstSeq = Integer.MAX_VALUE;
1057     int lastSeq = -1;
1058     int firstCol = Integer.MAX_VALUE;
1059     int lastCol = -1;
1060     boolean matchFound = false;
1061
1062     ViewportRanges ranges = av.getRanges();
1063     int firstVisibleColumn = ranges.getStartRes();
1064     int lastVisibleColumn = ranges.getEndRes();
1065     AlignmentI alignment = av.getAlignment();
1066     if (av.hasHiddenColumns())
1067     {
1068       firstVisibleColumn = alignment.getHiddenColumns()
1069               .adjustForHiddenColumns(firstVisibleColumn);
1070       lastVisibleColumn = alignment.getHiddenColumns()
1071               .adjustForHiddenColumns(lastVisibleColumn);
1072     }
1073
1074     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1075             .getEndSeq(); seqNo++)
1076     {
1077       SequenceI seq = alignment.getSequenceAt(seqNo);
1078
1079       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1080               lastVisibleColumn);
1081       if (visibleResults != null)
1082       {
1083         for (int i = 0; i < visibleResults.length - 1; i += 2)
1084         {
1085           int firstMatchedColumn = visibleResults[i];
1086           int lastMatchedColumn = visibleResults[i + 1];
1087           if (firstMatchedColumn <= lastVisibleColumn
1088                   && lastMatchedColumn >= firstVisibleColumn)
1089           {
1090             /*
1091              * found a search results match in the visible region - 
1092              * remember the first and last sequence matched, and the first
1093              * and last visible columns in the matched positions
1094              */
1095             matchFound = true;
1096             firstSeq = Math.min(firstSeq, seqNo);
1097             lastSeq = Math.max(lastSeq, seqNo);
1098             firstMatchedColumn = Math.max(firstMatchedColumn,
1099                     firstVisibleColumn);
1100             lastMatchedColumn = Math.min(lastMatchedColumn,
1101                     lastVisibleColumn);
1102             firstCol = Math.min(firstCol, firstMatchedColumn);
1103             lastCol = Math.max(lastCol, lastMatchedColumn);
1104           }
1105         }
1106       }
1107     }
1108
1109     if (matchFound)
1110     {
1111       if (av.hasHiddenColumns())
1112       {
1113         firstCol = alignment.getHiddenColumns()
1114                 .findColumnPosition(firstCol);
1115         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1116       }
1117       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1118       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1119       gg.translate(transX, transY);
1120       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1121       gg.translate(-transX, -transY);
1122     }
1123
1124     return matchFound;
1125   }
1126
1127   @Override
1128   public void propertyChange(PropertyChangeEvent evt)
1129   {
1130     if (!av.getWrapAlignment())
1131     {
1132       if (evt.getPropertyName().equals("startres")
1133               || evt.getPropertyName().equals("endres"))
1134       {
1135         // Make sure we're not trying to draw a panel
1136         // larger than the visible window
1137         ViewportRanges vpRanges = av.getRanges();
1138         int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1139         if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
1140         {
1141           scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
1142         }
1143         else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
1144         {
1145           scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
1146         }
1147         fastPaint(scrollX, 0);
1148       }
1149       else if (evt.getPropertyName().equals("startseq")
1150               || evt.getPropertyName().equals("endseq"))
1151       {
1152         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1153       }
1154     }
1155   }
1156 }