Merge branch 'releases/Release_2_11_4_Branch'
[jalview.git] / 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
290     ViewportRanges ranges = av.getRanges();
291     int sr = ranges.getStartRes();
292     int er = ranges.getEndRes();
293     int ss = ranges.getStartSeq();
294     int es = ranges.getEndSeq();
295     int transX = 0;
296     int transY = 0;
297
298     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
299             imgHeight, -horizontal * charWidth, -vertical * charHeight);
300
301     if (horizontal > 0) // scrollbar pulled right, image to the left
302     {
303       transX = (er - sr - horizontal) * charWidth;
304       sr = er - horizontal;
305     }
306     else if (horizontal < 0)
307     {
308       er = sr - horizontal;
309     }
310     else if (vertical > 0) // scroll down
311     {
312       ss = es - vertical;
313
314       if (ss < ranges.getStartSeq())
315       { // ie scrolling too fast, more than a page at a time
316         ss = ranges.getStartSeq();
317       }
318       else
319       {
320         transY = imgHeight - ((vertical + 1) * charHeight);
321       }
322     }
323     else if (vertical < 0)
324     {
325       es = ss - vertical;
326
327       if (es > ranges.getEndSeq())
328       {
329         es = ranges.getEndSeq();
330       }
331     }
332
333     gg.translate(transX, transY);
334     drawPanel(gg, sr, er, ss, es, 0);
335     gg.translate(-transX, -transY);
336
337     repaint();
338     fastpainting = false;
339   }
340
341   /**
342    * Definitions of startx and endx (hopefully): SMJS This is what I'm working
343    * towards! startx is the first residue (starting at 0) to display. endx is
344    * the last residue to display (starting at 0). starty is the first sequence
345    * to display (starting at 0). endy is the last sequence to display (starting
346    * at 0). NOTE 1: The av limits are set in setFont in this class and in the
347    * adjustment listener in SeqPanel when the scrollbars move.
348    */
349
350   // Set this to false to force a full panel paint
351   @Override
352   public void paintComponent(Graphics g)
353   {
354     updateViewport();
355     BufferedImage lcimg = img; // take reference since other threads may null
356     // img and call later.
357     super.paintComponent(g);
358
359     if (lcimg != null
360             && (fastPaint
361                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
362                     .getClipBounds().height)))
363     {
364       g.drawImage(lcimg, 0, 0, this);
365       fastPaint = false;
366       return;
367     }
368
369     // this draws the whole of the alignment
370     imgWidth = getWidth();
371     imgHeight = getHeight();
372
373     imgWidth -= (imgWidth % charWidth);
374     imgHeight -= (imgHeight % charHeight);
375
376     if ((imgWidth < 1) || (imgHeight < 1))
377     {
378       return;
379     }
380
381     if (lcimg == null || imgWidth != lcimg.getWidth()
382             || imgHeight != lcimg.getHeight())
383     {
384       try
385       {
386         lcimg = img = new BufferedImage(imgWidth, imgHeight,
387                 BufferedImage.TYPE_INT_RGB);
388         gg = (Graphics2D) img.getGraphics();
389         gg.setFont(av.getFont());
390       } catch (OutOfMemoryError er)
391       {
392         System.gc();
393         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
394         new OOMWarning("Creating alignment image for display", er);
395
396         return;
397       }
398     }
399
400     if (av.antiAlias)
401     {
402       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
403               RenderingHints.VALUE_ANTIALIAS_ON);
404     }
405
406     gg.setColor(Color.white);
407     gg.fillRect(0, 0, imgWidth, imgHeight);
408
409     ViewportRanges ranges = av.getRanges();
410     if (av.getWrapAlignment())
411     {
412       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
413     }
414     else
415     {
416       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
417               ranges.getStartSeq(), ranges.getEndSeq(), 0);
418     }
419
420     g.drawImage(lcimg, 0, 0, this);
421
422   }
423
424   /**
425    * DOCUMENT ME!
426    * 
427    * @param cwidth
428    *          DOCUMENT ME!
429    * 
430    * @return DOCUMENT ME!
431    */
432   public int getWrappedCanvasWidth(int cwidth)
433   {
434     FontMetrics fm = getFontMetrics(av.getFont());
435
436     LABEL_EAST = 0;
437     LABEL_WEST = 0;
438
439     if (av.getScaleRightWrapped())
440     {
441       LABEL_EAST = fm.stringWidth(getMask());
442     }
443
444     if (av.getScaleLeftWrapped())
445     {
446       LABEL_WEST = fm.stringWidth(getMask());
447     }
448
449     return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
450   }
451
452   /**
453    * Generates a string of zeroes.
454    * 
455    * @return String
456    */
457   String getMask()
458   {
459     String mask = "00";
460     int maxWidth = 0;
461     int tmp;
462     for (int i = 0; i < av.getAlignment().getHeight(); i++)
463     {
464       tmp = av.getAlignment().getSequenceAt(i).getEnd();
465       if (tmp > maxWidth)
466       {
467         maxWidth = tmp;
468       }
469     }
470
471     for (int i = maxWidth; i > 0; i /= 10)
472     {
473       mask += "0";
474     }
475     return mask;
476   }
477
478   /**
479    * DOCUMENT ME!
480    * 
481    * @param g
482    *          DOCUMENT ME!
483    * @param canvasWidth
484    *          DOCUMENT ME!
485    * @param canvasHeight
486    *          DOCUMENT ME!
487    * @param startRes
488    *          DOCUMENT ME!
489    */
490   public void drawWrappedPanel(Graphics g, int canvasWidth,
491           int canvasHeight, int startRes)
492   {
493     updateViewport();
494     AlignmentI al = av.getAlignment();
495
496     FontMetrics fm = getFontMetrics(av.getFont());
497
498     LABEL_EAST = 0;
499     LABEL_WEST = 0;
500
501     if (av.getScaleRightWrapped())
502     {
503       LABEL_EAST = fm.stringWidth(getMask());
504     }
505
506     if (av.getScaleLeftWrapped())
507     {
508       LABEL_WEST = fm.stringWidth(getMask());
509     }
510
511     int hgap = charHeight;
512     if (av.getScaleAboveWrapped())
513     {
514       hgap += charHeight;
515     }
516
517     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
518     int cHeight = av.getAlignment().getHeight() * charHeight;
519
520     av.setWrappedWidth(cWidth);
521
522     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
523
524     int endx;
525     int ypos = hgap;
526     int maxwidth = av.getAlignment().getWidth();
527
528     if (av.hasHiddenColumns())
529     {
530       maxwidth = av.getAlignment().getHiddenColumns()
531               .findColumnPosition(maxwidth);
532     }
533
534     while ((ypos <= canvasHeight) && (startRes < maxwidth))
535     {
536       endx = startRes + cWidth - 1;
537
538       if (endx > maxwidth)
539       {
540         endx = maxwidth;
541       }
542
543       g.setFont(av.getFont());
544       g.setColor(Color.black);
545
546       if (av.getScaleLeftWrapped())
547       {
548         drawWestScale(g, startRes, endx, ypos);
549       }
550
551       if (av.getScaleRightWrapped())
552       {
553         g.translate(canvasWidth - LABEL_EAST, 0);
554         drawEastScale(g, startRes, endx, ypos);
555         g.translate(-(canvasWidth - LABEL_EAST), 0);
556       }
557
558       g.translate(LABEL_WEST, 0);
559
560       if (av.getScaleAboveWrapped())
561       {
562         drawNorthScale(g, startRes, endx, ypos);
563       }
564
565       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
566       {
567         g.setColor(Color.blue);
568         int res;
569         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
570         List<Integer> positions = hidden.findHiddenRegionPositions();
571         for (int pos : positions)
572         {
573           res = pos - startRes;
574
575           if (res < 0 || res > endx - startRes)
576           {
577             continue;
578           }
579
580           gg.fillPolygon(
581                   new int[] { res * charWidth - charHeight / 4,
582                       res * charWidth + charHeight / 4, res * charWidth },
583                   new int[] { ypos - (charHeight / 2),
584                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
585                   3);
586
587         }
588       }
589
590       // When printing we have an extra clipped region,
591       // the Printable page which we need to account for here
592       Shape clip = g.getClip();
593
594       if (clip == null)
595       {
596         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
597       }
598       else
599       {
600         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
601                 (int) clip.getBounds().getHeight());
602       }
603
604       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
605
606       if (av.isShowAnnotation())
607       {
608         g.translate(0, cHeight + ypos + 3);
609         if (annotations == null)
610         {
611           annotations = new AnnotationPanel(av);
612         }
613
614         annotations.renderer.drawComponent(annotations, av, g, -1,
615                 startRes, endx + 1);
616         g.translate(0, -cHeight - ypos - 3);
617       }
618       g.setClip(clip);
619       g.translate(-LABEL_WEST, 0);
620
621       ypos += cHeight + getAnnotationHeight() + hgap;
622
623       startRes += cWidth;
624     }
625   }
626
627   AnnotationPanel annotations;
628
629   int getAnnotationHeight()
630   {
631     if (!av.isShowAnnotation())
632     {
633       return 0;
634     }
635
636     if (annotations == null)
637     {
638       annotations = new AnnotationPanel(av);
639     }
640
641     return annotations.adjustPanelHeight();
642   }
643
644   /**
645    * DOCUMENT ME!
646    * 
647    * @param g1
648    *          DOCUMENT ME!
649    * @param startRes
650    *          DOCUMENT ME!
651    * @param endRes
652    *          DOCUMENT ME!
653    * @param startSeq
654    *          DOCUMENT ME!
655    * @param endSeq
656    *          DOCUMENT ME!
657    * @param offset
658    *          DOCUMENT ME!
659    */
660   public void drawPanel(Graphics g1, int startRes, int endRes,
661           int startSeq, int endSeq, int offset)
662   {
663     updateViewport();
664     if (!av.hasHiddenColumns())
665     {
666       draw(g1, startRes, endRes, startSeq, endSeq, offset);
667     }
668     else
669     {
670       int screenY = 0;
671       int blockStart = startRes;
672       int blockEnd = endRes;
673
674       for (int[] region : av.getAlignment().getHiddenColumns()
675               .getHiddenColumnsCopy())
676       {
677         int hideStart = region[0];
678         int hideEnd = region[1];
679
680         if (hideStart <= blockStart)
681         {
682           blockStart += (hideEnd - hideStart) + 1;
683           continue;
684         }
685
686         blockEnd = hideStart - 1;
687
688         g1.translate(screenY * charWidth, 0);
689
690         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
691
692         if (av.getShowHiddenMarkers())
693         {
694           g1.setColor(Color.blue);
695
696           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
697                   0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
698                   (endSeq - startSeq + 1) * charHeight + offset);
699         }
700
701         g1.translate(-screenY * charWidth, 0);
702         screenY += blockEnd - blockStart + 1;
703         blockStart = hideEnd + 1;
704
705         if (screenY > (endRes - startRes))
706         {
707           // already rendered last block
708           return;
709         }
710       }
711
712       if (screenY <= (endRes - startRes))
713       {
714         // remaining visible region to render
715         blockEnd = blockStart + (endRes - startRes) - screenY;
716         g1.translate(screenY * charWidth, 0);
717         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
718
719         g1.translate(-screenY * charWidth, 0);
720       }
721     }
722
723   }
724
725   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
726   // int x1, int x2, int y1, int y2, int startx, int starty,
727   private void draw(Graphics g, int startRes, int endRes, int startSeq,
728           int endSeq, int offset)
729   {
730     g.setFont(av.getFont());
731     sr.prepare(g, av.isRenderGaps());
732
733     SequenceI nextSeq;
734
735     // / First draw the sequences
736     // ///////////////////////////
737     for (int i = startSeq; i <= endSeq; i++)
738     {
739       nextSeq = av.getAlignment().getSequenceAt(i);
740       if (nextSeq == null)
741       {
742         // occasionally, a race condition occurs such that the alignment row is
743         // empty
744         continue;
745       }
746       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
747               startRes, endRes, offset + ((i - startSeq) * charHeight));
748
749       if (av.isShowSequenceFeatures())
750       {
751         fr.drawSequence(g, nextSeq, startRes, endRes, offset
752                 + ((i - startSeq) * charHeight), false);
753       }
754
755       // / Highlight search Results once all sequences have been drawn
756       // ////////////////////////////////////////////////////////
757       if (av.hasSearchResults())
758       {
759         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
760                 startRes, endRes);
761         if (visibleResults != null)
762         {
763           for (int r = 0; r < visibleResults.length; r += 2)
764           {
765             sr.drawHighlightedText(nextSeq, visibleResults[r],
766                     visibleResults[r + 1], (visibleResults[r] - startRes)
767                             * charWidth, offset
768                             + ((i - startSeq) * charHeight));
769           }
770         }
771       }
772
773       if (av.cursorMode && cursorY == i && cursorX >= startRes
774               && cursorX <= endRes)
775       {
776         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
777                 offset + ((i - startSeq) * charHeight));
778       }
779     }
780
781     if (av.getSelectionGroup() != null
782             || av.getAlignment().getGroups().size() > 0)
783     {
784       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
785     }
786
787   }
788
789   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
790           int startSeq, int endSeq, int offset)
791   {
792     Graphics2D g = (Graphics2D) g1;
793     //
794     // ///////////////////////////////////
795     // Now outline any areas if necessary
796     // ///////////////////////////////////
797     SequenceGroup group = av.getSelectionGroup();
798
799     int sx = -1;
800     int sy = -1;
801     int ex = -1;
802     int groupIndex = -1;
803     int visWidth = (endRes - startRes + 1) * charWidth;
804
805     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
806     {
807       group = av.getAlignment().getGroups().get(0);
808       groupIndex = 0;
809     }
810
811     if (group != null)
812     {
813       do
814       {
815         int oldY = -1;
816         int i = 0;
817         boolean inGroup = false;
818         int top = -1;
819         int bottom = -1;
820
821         for (i = startSeq; i <= endSeq; i++)
822         {
823           sx = (group.getStartRes() - startRes) * charWidth;
824           sy = offset + ((i - startSeq) * charHeight);
825           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
826
827           if (sx + ex < 0 || sx > visWidth)
828           {
829             continue;
830           }
831
832           if ((sx <= (endRes - startRes) * charWidth)
833                   && group.getSequences(null).contains(
834                           av.getAlignment().getSequenceAt(i)))
835           {
836             if ((bottom == -1)
837                     && !group.getSequences(null).contains(
838                             av.getAlignment().getSequenceAt(i + 1)))
839             {
840               bottom = sy + charHeight;
841             }
842
843             if (!inGroup)
844             {
845               if (((top == -1) && (i == 0))
846                       || !group.getSequences(null).contains(
847                               av.getAlignment().getSequenceAt(i - 1)))
848               {
849                 top = sy;
850               }
851
852               oldY = sy;
853               inGroup = true;
854
855               if (group == av.getSelectionGroup())
856               {
857                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
858                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
859                         0f));
860                 g.setColor(Color.RED);
861               }
862               else
863               {
864                 g.setStroke(new BasicStroke());
865                 g.setColor(group.getOutlineColour());
866               }
867             }
868           }
869           else
870           {
871             if (inGroup)
872             {
873               if (sx >= 0 && sx < visWidth)
874               {
875                 g.drawLine(sx, oldY, sx, sy);
876               }
877
878               if (sx + ex < visWidth)
879               {
880                 g.drawLine(sx + ex, oldY, sx + ex, sy);
881               }
882
883               if (sx < 0)
884               {
885                 ex += sx;
886                 sx = 0;
887               }
888
889               if (sx + ex > visWidth)
890               {
891                 ex = visWidth;
892               }
893
894               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
895               {
896                 ex = (endRes - startRes + 1) * charWidth;
897               }
898
899               if (top != -1)
900               {
901                 g.drawLine(sx, top, sx + ex, top);
902                 top = -1;
903               }
904
905               if (bottom != -1)
906               {
907                 g.drawLine(sx, bottom, sx + ex, bottom);
908                 bottom = -1;
909               }
910
911               inGroup = false;
912             }
913           }
914         }
915
916         if (inGroup)
917         {
918           sy = offset + ((i - startSeq) * charHeight);
919           if (sx >= 0 && sx < visWidth)
920           {
921             g.drawLine(sx, oldY, sx, sy);
922           }
923
924           if (sx + ex < visWidth)
925           {
926             g.drawLine(sx + ex, oldY, sx + ex, sy);
927           }
928
929           if (sx < 0)
930           {
931             ex += sx;
932             sx = 0;
933           }
934
935           if (sx + ex > visWidth)
936           {
937             ex = visWidth;
938           }
939           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
940           {
941             ex = (endRes - startRes + 1) * charWidth;
942           }
943
944           if (top != -1)
945           {
946             g.drawLine(sx, top, sx + ex, top);
947             top = -1;
948           }
949
950           if (bottom != -1)
951           {
952             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
953             bottom = -1;
954           }
955
956           inGroup = false;
957         }
958
959         groupIndex++;
960
961         g.setStroke(new BasicStroke());
962
963         if (groupIndex >= av.getAlignment().getGroups().size())
964         {
965           break;
966         }
967
968         group = av.getAlignment().getGroups().get(groupIndex);
969
970       } while (groupIndex < av.getAlignment().getGroups().size());
971
972     }
973
974   }
975
976   /**
977    * DOCUMENT ME!
978    * 
979    * @param results
980    *          DOCUMENT ME!
981    */
982   public void highlightSearchResults(SearchResultsI results)
983   {
984     img = null;
985
986     av.setSearchResults(results);
987
988     repaint();
989   }
990
991   @Override
992   public void propertyChange(PropertyChangeEvent evt)
993   {
994     String eventName = evt.getPropertyName();
995
996     if (av.getWrapAlignment())
997     {
998       if (eventName.equals(ViewportRanges.STARTRES))
999       {
1000         repaint();
1001       }
1002     }
1003     else
1004     {
1005       int scrollX = 0;
1006       if (eventName.equals(ViewportRanges.STARTRES))
1007       {
1008         // Make sure we're not trying to draw a panel
1009         // larger than the visible window
1010         ViewportRanges vpRanges = av.getRanges();
1011         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1012         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1013         if (scrollX > range)
1014         {
1015           scrollX = range;
1016         }
1017         else if (scrollX < -range)
1018         {
1019           scrollX = -range;
1020         }
1021       }
1022
1023       // Both scrolling and resizing change viewport ranges: scrolling changes
1024       // both start and end points, but resize only changes end values.
1025       // Here we only want to fastpaint on a scroll, with resize using a normal
1026       // paint, so scroll events are identified as changes to the horizontal or
1027       // vertical start value.
1028       if (eventName.equals(ViewportRanges.STARTRES))
1029       {
1030         // scroll - startres and endres both change
1031         fastPaint(scrollX, 0);
1032       }
1033       else if (eventName.equals(ViewportRanges.STARTSEQ))
1034       {
1035         // scroll
1036         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1037       }
1038     }
1039   }
1040 }