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