558c1cab0cc336e3f730d8bc0622e443885c06a5
[jalview.git] / src / jalview / gui / SeqCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.ScaleRenderer;
29 import jalview.renderer.ScaleRenderer.ScaleMark;
30 import jalview.viewmodel.ViewportListenerI;
31 import jalview.viewmodel.ViewportRanges;
32
33 import java.awt.AlphaComposite;
34 import java.awt.BasicStroke;
35 import java.awt.BorderLayout;
36 import java.awt.Color;
37 import java.awt.FontMetrics;
38 import java.awt.Graphics;
39 import java.awt.Graphics2D;
40 import java.awt.RenderingHints;
41 import java.awt.Shape;
42 import java.awt.image.BufferedImage;
43 import java.beans.PropertyChangeEvent;
44 import java.util.List;
45
46 import javax.swing.JComponent;
47
48 /**
49  * DOCUMENT ME!
50  * 
51  * @author $author$
52  * @version $Revision$
53  */
54 public class SeqCanvas extends JComponent implements ViewportListenerI
55 {
56   final FeatureRenderer fr;
57
58   final SequenceRenderer sr;
59
60   BufferedImage img;
61
62   Graphics2D gg;
63
64   AlignViewport av;
65
66   boolean fastPaint = false;
67
68   int LABEL_WEST;
69
70   int LABEL_EAST;
71
72   int cursorX = 0;
73
74   int cursorY = 0;
75
76   /**
77    * Creates a new SeqCanvas object.
78    * 
79    * @param av
80    *          DOCUMENT ME!
81    */
82   public SeqCanvas(AlignmentPanel ap)
83   {
84     this.av = ap.av;
85     updateViewport();
86     fr = new FeatureRenderer(ap);
87     sr = new SequenceRenderer(av);
88     setLayout(new BorderLayout());
89     PaintRefresher.Register(this, av.getSequenceSetId());
90     setBackground(Color.white);
91
92     av.getRanges().addPropertyChangeListener(this);
93   }
94
95   public SequenceRenderer getSequenceRenderer()
96   {
97     return sr;
98   }
99
100   public FeatureRenderer getFeatureRenderer()
101   {
102     return fr;
103   }
104
105   int charHeight = 0, charWidth = 0;
106
107   private void updateViewport()
108   {
109     charHeight = av.getCharHeight();
110     charWidth = av.getCharWidth();
111   }
112
113   /**
114    * DOCUMENT ME!
115    * 
116    * @param g
117    *          DOCUMENT ME!
118    * @param startx
119    *          DOCUMENT ME!
120    * @param endx
121    *          DOCUMENT ME!
122    * @param ypos
123    *          DOCUMENT ME!
124    */
125   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
126   {
127     updateViewport();
128     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
129             endx))
130     {
131       int mpos = mark.column; // (i - startx - 1)
132       if (mpos < 0)
133       {
134         continue;
135       }
136       String mstring = mark.text;
137
138       if (mark.major)
139       {
140         if (mstring != null)
141         {
142           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
143         }
144         g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
145                 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
146                 ypos - 2);
147       }
148     }
149   }
150
151   /**
152    * DOCUMENT ME!
153    * 
154    * @param g
155    *          DOCUMENT ME!
156    * @param startx
157    *          DOCUMENT ME!
158    * @param endx
159    *          DOCUMENT ME!
160    * @param ypos
161    *          DOCUMENT ME!
162    */
163   void drawWestScale(Graphics g, int startx, int endx, int ypos)
164   {
165     FontMetrics fm = getFontMetrics(av.getFont());
166     ypos += charHeight;
167
168     if (av.hasHiddenColumns())
169     {
170       startx = av.getAlignment().getHiddenColumns()
171               .adjustForHiddenColumns(startx);
172       endx = av.getAlignment().getHiddenColumns()
173               .adjustForHiddenColumns(endx);
174     }
175
176     int maxwidth = av.getAlignment().getWidth();
177     if (av.hasHiddenColumns())
178     {
179       maxwidth = av.getAlignment().getHiddenColumns()
180               .findColumnPosition(maxwidth) - 1;
181     }
182
183     // WEST SCALE
184     for (int i = 0; i < av.getAlignment().getHeight(); i++)
185     {
186       SequenceI seq = av.getAlignment().getSequenceAt(i);
187       int index = startx;
188       int value = -1;
189
190       while (index < endx)
191       {
192         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
193         {
194           index++;
195
196           continue;
197         }
198
199         value = av.getAlignment().getSequenceAt(i).findPosition(index);
200
201         break;
202       }
203
204       if (value != -1)
205       {
206         int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
207                 - charWidth / 2;
208         g.drawString(value + "", x, (ypos + (i * charHeight))
209                 - (charHeight / 5));
210       }
211     }
212   }
213
214   /**
215    * DOCUMENT ME!
216    * 
217    * @param g
218    *          DOCUMENT ME!
219    * @param startx
220    *          DOCUMENT ME!
221    * @param endx
222    *          DOCUMENT ME!
223    * @param ypos
224    *          DOCUMENT ME!
225    */
226   void drawEastScale(Graphics g, int startx, int endx, int ypos)
227   {
228     ypos += charHeight;
229
230     if (av.hasHiddenColumns())
231     {
232       endx = av.getAlignment().getHiddenColumns()
233               .adjustForHiddenColumns(endx);
234     }
235
236     SequenceI seq;
237     // EAST SCALE
238     for (int i = 0; i < av.getAlignment().getHeight(); i++)
239     {
240       seq = av.getAlignment().getSequenceAt(i);
241       int index = endx;
242       int value = -1;
243
244       while (index > startx)
245       {
246         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
247         {
248           index--;
249
250           continue;
251         }
252
253         value = seq.findPosition(index);
254
255         break;
256       }
257
258       if (value != -1)
259       {
260         g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
261                 - (charHeight / 5));
262       }
263     }
264   }
265
266   boolean fastpainting = false;
267
268   /**
269    * need to make this thread safe move alignment rendering in response to
270    * slider adjustment
271    * 
272    * @param horizontal
273    *          shift along
274    * @param vertical
275    *          shift up or down in repaint
276    */
277   public void fastPaint(int horizontal, int vertical)
278   {
279     if (fastpainting || gg == null)
280     {
281       return;
282     }
283     fastpainting = true;
284     fastPaint = true;
285     updateViewport();
286
287     ViewportRanges ranges = av.getRanges();
288     int sr = ranges.getStartRes();
289     int er = ranges.getEndRes();
290     int ss = ranges.getStartSeq();
291     int es = ranges.getEndSeq();
292     int transX = 0;
293     int transY = 0;
294
295     gg.copyArea(horizontal * charWidth, vertical * charHeight,
296             img.getWidth(), img.getHeight(), -horizontal * charWidth,
297             -vertical * charHeight);
298
299     if (horizontal > 0) // scrollbar pulled right, image to the left
300     {
301       transX = (er - sr - horizontal) * charWidth;
302       sr = er - horizontal;
303     }
304     else if (horizontal < 0)
305     {
306       er = sr - horizontal;
307     }
308     else if (vertical > 0) // scroll down
309     {
310       ss = es - vertical;
311
312       if (ss < ranges.getStartSeq())
313       { // ie scrolling too fast, more than a page at a time
314         ss = ranges.getStartSeq();
315       }
316       else
317       {
318         transY = img.getHeight() - ((vertical + 1) * charHeight);
319       }
320     }
321     else if (vertical < 0)
322     {
323       es = ss - vertical;
324
325       if (es > ranges.getEndSeq())
326       {
327         es = ranges.getEndSeq();
328       }
329     }
330
331     gg.translate(transX, transY);
332     drawPanel(gg, sr, er, ss, es, 0);
333     gg.translate(-transX, -transY);
334
335     repaint();
336     fastpainting = false;
337   }
338
339   /**
340    * Definitions of startx and endx (hopefully): SMJS This is what I'm working
341    * towards! startx is the first residue (starting at 0) to display. endx is
342    * the last residue to display (starting at 0). starty is the first sequence
343    * to display (starting at 0). endy is the last sequence to display (starting
344    * at 0). NOTE 1: The av limits are set in setFont in this class and in the
345    * adjustment listener in SeqPanel when the scrollbars move.
346    */
347
348   // Set this to false to force a full panel paint
349   @Override
350   public void paintComponent(Graphics g)
351   {
352     super.paintComponent(g);
353
354     updateViewport();
355
356     // img is a cached version of the last view we drew
357     // selectImage will hold any selection we have
358     // lcimg is a local *copy* of img which we'll draw selectImage on top of
359
360     BufferedImage selectImage = drawSelectionGroup();
361
362     if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
363             || (getVisibleRect().height != g.getClipBounds().height))
364     {
365       BufferedImage lcimg = buildLocalImage(selectImage);
366       g.drawImage(lcimg, 0, 0, this);
367
368       fastPaint = false;
369       return;
370     }
371
372     int width = getWidth();
373     int height = getHeight();
374
375     width -= (width % charWidth);
376     height -= (height % charHeight);
377
378     if ((width < 1) || (height < 1))
379     {
380       return;
381     }
382
383     if (img == null || width != img.getWidth() || height != img.getHeight())
384     {
385       img = setupImage();
386       gg = (Graphics2D) img.getGraphics();
387       gg.setFont(av.getFont());
388     }
389     if (img == null)
390     {
391       return;
392     }
393
394     
395     if (av.antiAlias)
396     {
397       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
398               RenderingHints.VALUE_ANTIALIAS_ON);
399     }
400     
401     gg.setColor(Color.white);
402     gg.fillRect(0, 0, img.getWidth(), img.getHeight());
403     
404     ViewportRanges ranges = av.getRanges();
405     if (av.getWrapAlignment())
406     {
407       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
408     }
409     else
410     {
411       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
412               ranges.getStartSeq(), ranges.getEndSeq(), 0);
413     }
414
415     BufferedImage lcimg = buildLocalImage(selectImage);
416     g.drawImage(lcimg, 0, 0, this);
417   }
418
419   /*
420    * Make a local image by combining the cached image img
421    * with any selection
422    */
423   private BufferedImage buildLocalImage(BufferedImage selectImage)
424   {
425     // clone the cached image
426     BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
427             img.getType());
428     Graphics2D g2d = lcimg.createGraphics();
429     g2d.drawImage(img, 0, 0, null);
430
431     // overlay selection group on lcimg
432     if (selectImage != null)
433     {
434       g2d.setComposite(
435               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
436       g2d.drawImage(selectImage, 0, 0, this);
437     }
438     g2d.dispose();
439
440     return lcimg;
441   }
442
443   private void paintSeqGroup()
444   {
445     fastPaint = true;
446     repaint();
447   }
448
449   private BufferedImage setupImage()
450   {
451     BufferedImage lcimg = null;
452
453     int width = getWidth();
454     int height = getHeight();
455
456     width -= (width % charWidth);
457     height -= (height % charHeight);
458
459     if ((width < 1) || (height < 1))
460     {
461       return null;
462     }
463
464     try
465     {
466       lcimg = new BufferedImage(width, height,
467               BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
468     } catch (OutOfMemoryError er)
469     {
470       System.gc();
471       System.err.println(
472               "Selection Group image OutOfMemory Redraw Error.\n" + er);
473       new OOMWarning("Creating alignment image for display", er);
474
475       return null;
476     }
477
478     return lcimg;
479   }
480
481   /**
482    * DOCUMENT ME!
483    * 
484    * @param cwidth
485    *          DOCUMENT ME!
486    * 
487    * @return DOCUMENT ME!
488    */
489   public int getWrappedCanvasWidth(int cwidth)
490   {
491     FontMetrics fm = getFontMetrics(av.getFont());
492
493     LABEL_EAST = 0;
494     LABEL_WEST = 0;
495
496     if (av.getScaleRightWrapped())
497     {
498       LABEL_EAST = fm.stringWidth(getMask());
499     }
500
501     if (av.getScaleLeftWrapped())
502     {
503       LABEL_WEST = fm.stringWidth(getMask());
504     }
505
506     return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
507   }
508
509   /**
510    * Generates a string of zeroes.
511    * 
512    * @return String
513    */
514   String getMask()
515   {
516     String mask = "00";
517     int maxWidth = 0;
518     int tmp;
519     for (int i = 0; i < av.getAlignment().getHeight(); i++)
520     {
521       tmp = av.getAlignment().getSequenceAt(i).getEnd();
522       if (tmp > maxWidth)
523       {
524         maxWidth = tmp;
525       }
526     }
527
528     for (int i = maxWidth; i > 0; i /= 10)
529     {
530       mask += "0";
531     }
532     return mask;
533   }
534
535   /**
536    * DOCUMENT ME!
537    * 
538    * @param g
539    *          DOCUMENT ME!
540    * @param canvasWidth
541    *          DOCUMENT ME!
542    * @param canvasHeight
543    *          DOCUMENT ME!
544    * @param startRes
545    *          DOCUMENT ME!
546    */
547   public void drawWrappedPanel(Graphics g, int canvasWidth,
548           int canvasHeight, int startRes)
549   {
550     updateViewport();
551     AlignmentI al = av.getAlignment();
552
553     FontMetrics fm = getFontMetrics(av.getFont());
554
555     LABEL_EAST = 0;
556     LABEL_WEST = 0;
557
558     if (av.getScaleRightWrapped())
559     {
560       LABEL_EAST = fm.stringWidth(getMask());
561     }
562
563     if (av.getScaleLeftWrapped())
564     {
565       LABEL_WEST = fm.stringWidth(getMask());
566     }
567
568     int hgap = charHeight;
569     if (av.getScaleAboveWrapped())
570     {
571       hgap += charHeight;
572     }
573
574     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
575     int cHeight = av.getAlignment().getHeight() * charHeight;
576
577     av.setWrappedWidth(cWidth);
578
579     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
580
581     int endx;
582     int ypos = hgap;
583     int maxwidth = av.getAlignment().getWidth();
584
585     if (av.hasHiddenColumns())
586     {
587       maxwidth = av.getAlignment().getHiddenColumns()
588               .findColumnPosition(maxwidth);
589     }
590
591     while ((ypos <= canvasHeight) && (startRes < maxwidth))
592     {
593       endx = startRes + cWidth - 1;
594
595       if (endx > maxwidth)
596       {
597         endx = maxwidth;
598       }
599
600       g.setFont(av.getFont());
601       g.setColor(Color.black);
602
603       if (av.getScaleLeftWrapped())
604       {
605         drawWestScale(g, startRes, endx, ypos);
606       }
607
608       if (av.getScaleRightWrapped())
609       {
610         g.translate(canvasWidth - LABEL_EAST, 0);
611         drawEastScale(g, startRes, endx, ypos);
612         g.translate(-(canvasWidth - LABEL_EAST), 0);
613       }
614
615       g.translate(LABEL_WEST, 0);
616
617       if (av.getScaleAboveWrapped())
618       {
619         drawNorthScale(g, startRes, endx, ypos);
620       }
621
622       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
623       {
624         g.setColor(Color.blue);
625         int res;
626         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
627         List<Integer> positions = hidden.findHiddenRegionPositions();
628         for (int pos : positions)
629         {
630           res = pos - startRes;
631
632           if (res < 0 || res > endx - startRes)
633           {
634             continue;
635           }
636
637           gg.fillPolygon(
638                   new int[] { res * charWidth - charHeight / 4,
639                       res * charWidth + charHeight / 4, res * charWidth },
640                   new int[] { ypos - (charHeight / 2),
641                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
642                   3);
643
644         }
645       }
646
647       // When printing we have an extra clipped region,
648       // the Printable page which we need to account for here
649       Shape clip = g.getClip();
650
651       if (clip == null)
652       {
653         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
654       }
655       else
656       {
657         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
658                 (int) clip.getBounds().getHeight());
659       }
660
661       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
662
663       if (av.isShowAnnotation())
664       {
665         g.translate(0, cHeight + ypos + 3);
666         if (annotations == null)
667         {
668           annotations = new AnnotationPanel(av);
669         }
670
671         annotations.renderer.drawComponent(annotations, av, g, -1,
672                 startRes, endx + 1);
673         g.translate(0, -cHeight - ypos - 3);
674       }
675       g.setClip(clip);
676       g.translate(-LABEL_WEST, 0);
677
678       ypos += cHeight + getAnnotationHeight() + hgap;
679
680       startRes += cWidth;
681     }
682   }
683
684   AnnotationPanel annotations;
685
686   int getAnnotationHeight()
687   {
688     if (!av.isShowAnnotation())
689     {
690       return 0;
691     }
692
693     if (annotations == null)
694     {
695       annotations = new AnnotationPanel(av);
696     }
697
698     return annotations.adjustPanelHeight();
699   }
700
701   /**
702    * DOCUMENT ME!
703    * 
704    * @param g1
705    *          DOCUMENT ME!
706    * @param startRes
707    *          DOCUMENT ME!
708    * @param endRes
709    *          DOCUMENT ME!
710    * @param startSeq
711    *          DOCUMENT ME!
712    * @param endSeq
713    *          DOCUMENT ME!
714    * @param offset
715    *          DOCUMENT ME!
716    */
717   public void drawPanel(Graphics g1, int startRes, int endRes,
718           int startSeq, int endSeq, int offset)
719   {
720     updateViewport();
721     if (!av.hasHiddenColumns())
722     {
723       draw(g1, startRes, endRes, startSeq, endSeq, offset);
724     }
725     else
726     {
727       int screenY = 0;
728       int blockStart = startRes;
729       int blockEnd = endRes;
730
731       for (int[] region : av.getAlignment().getHiddenColumns()
732               .getHiddenColumnsCopy())
733       {
734         int hideStart = region[0];
735         int hideEnd = region[1];
736
737         if (hideStart <= blockStart)
738         {
739           blockStart += (hideEnd - hideStart) + 1;
740           continue;
741         }
742
743         blockEnd = hideStart - 1;
744
745         g1.translate(screenY * charWidth, 0);
746
747         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
748
749         if (av.getShowHiddenMarkers())
750         {
751           g1.setColor(Color.blue);
752
753           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
754                   0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
755                   (endSeq - startSeq + 1) * charHeight + offset);
756         }
757
758         g1.translate(-screenY * charWidth, 0);
759         screenY += blockEnd - blockStart + 1;
760         blockStart = hideEnd + 1;
761
762         if (screenY > (endRes - startRes))
763         {
764           // already rendered last block
765           return;
766         }
767       }
768
769       if (screenY <= (endRes - startRes))
770       {
771         // remaining visible region to render
772         blockEnd = blockStart + (endRes - startRes) - screenY;
773         g1.translate(screenY * charWidth, 0);
774         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
775
776         g1.translate(-screenY * charWidth, 0);
777       }
778     }
779
780   }
781
782   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
783   // int x1, int x2, int y1, int y2, int startx, int starty,
784   private void draw(Graphics g, int startRes, int endRes, int startSeq,
785           int endSeq, int offset)
786   {
787     g.setFont(av.getFont());
788     sr.prepare(g, av.isRenderGaps());
789
790     SequenceI nextSeq;
791
792     // / First draw the sequences
793     // ///////////////////////////
794     for (int i = startSeq; i <= endSeq; i++)
795     {
796       nextSeq = av.getAlignment().getSequenceAt(i);
797       if (nextSeq == null)
798       {
799         // occasionally, a race condition occurs such that the alignment row is
800         // empty
801         continue;
802       }
803       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
804               startRes, endRes, offset + ((i - startSeq) * charHeight));
805
806       if (av.isShowSequenceFeatures())
807       {
808         fr.drawSequence(g, nextSeq, startRes, endRes, offset
809                 + ((i - startSeq) * charHeight), false);
810       }
811
812       // / Highlight search Results once all sequences have been drawn
813       // ////////////////////////////////////////////////////////
814       if (av.hasSearchResults())
815       {
816         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
817                 startRes, endRes);
818         if (visibleResults != null)
819         {
820           for (int r = 0; r < visibleResults.length; r += 2)
821           {
822             sr.drawHighlightedText(nextSeq, visibleResults[r],
823                     visibleResults[r + 1], (visibleResults[r] - startRes)
824                             * charWidth, offset
825                             + ((i - startSeq) * charHeight));
826           }
827         }
828       }
829
830       if (av.cursorMode && cursorY == i && cursorX >= startRes
831               && cursorX <= endRes)
832       {
833         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
834                 offset + ((i - startSeq) * charHeight));
835       }
836     }
837
838     if (av.getSelectionGroup() != null
839             || av.getAlignment().getGroups().size() > 0)
840     {
841       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
842     }
843
844   }
845
846   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
847           int startSeq, int endSeq, int offset)
848   {
849     Graphics2D g = (Graphics2D) g1;
850     //
851     // ///////////////////////////////////
852     // Now outline any areas if necessary
853     // ///////////////////////////////////
854
855     SequenceGroup group = null;
856
857     int sx = -1;
858     int sy = -1;
859     int ex = -1;
860     int groupIndex = -1;
861     int visWidth = (endRes - startRes + 1) * charWidth;
862
863     if (av.getAlignment().getGroups().size() > 0)
864     {
865       group = av.getAlignment().getGroups().get(0);
866       groupIndex = 0;
867     }
868
869     if (group != null)
870     {
871       do
872       {
873         int oldY = -1;
874         int i = 0;
875         boolean inGroup = false;
876         int top = -1;
877         int bottom = -1;
878
879         for (i = startSeq; i <= endSeq; i++)
880         {
881           // position of start residue of group relative to startRes, in pixels
882           sx = (group.getStartRes() - startRes) * charWidth;
883           sy = offset + ((i - startSeq) * charHeight);
884           // width of group in pixels
885           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
886
887           if (sx + ex < 0 || sx > visWidth)
888           {
889             continue;
890           }
891
892           if ((sx <= (endRes - startRes) * charWidth)
893                   && group.getSequences(null).contains(
894                           av.getAlignment().getSequenceAt(i)))
895           {
896             if ((bottom == -1)
897                     && !group.getSequences(null).contains(
898                             av.getAlignment().getSequenceAt(i + 1)))
899             {
900               bottom = sy + charHeight;
901             }
902
903             if (!inGroup)
904             {
905               if (((top == -1) && (i == 0))
906                       || !group.getSequences(null).contains(
907                               av.getAlignment().getSequenceAt(i - 1)))
908               {
909                 top = sy;
910               }
911
912               oldY = sy;
913               inGroup = true;
914
915               g.setStroke(new BasicStroke());
916               g.setColor(group.getOutlineColour());
917             }
918           }
919           else
920           {
921             if (inGroup)
922             {
923               // if start position is visible, draw vertical line to left of
924               // group
925               if (sx >= 0 && sx < visWidth)
926               {
927                 g.drawLine(sx, oldY, sx, sy);
928               }
929
930               // if end position is visible, draw vertical line to right of
931               // group
932               if (sx + ex < visWidth)
933               {
934                 g.drawLine(sx + ex, oldY, sx + ex, sy);
935               }
936
937               if (sx < 0)
938               {
939                 ex += sx;
940                 sx = 0;
941               }
942
943               if (sx + ex > visWidth)
944               {
945                 ex = visWidth;
946               }
947
948               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
949               {
950                 ex = (endRes - startRes + 1) * charWidth;
951               }
952
953               // draw horizontal line at top of group
954               if (top != -1)
955               {
956                 g.drawLine(sx, top, sx + ex, top);
957                 top = -1;
958               }
959
960               // draw horizontal line at bottom of group
961               if (bottom != -1)
962               {
963                 g.drawLine(sx, bottom, sx + ex, bottom);
964                 bottom = -1;
965               }
966
967               inGroup = false;
968             }
969           }
970         }
971
972         if (inGroup)
973         {
974           sy = offset + ((i - startSeq) * charHeight);
975           if (sx >= 0 && sx < visWidth)
976           {
977             g.drawLine(sx, oldY, sx, sy);
978           }
979
980           if (sx + ex < visWidth)
981           {
982             g.drawLine(sx + ex, oldY, sx + ex, sy);
983           }
984
985           if (sx < 0)
986           {
987             ex += sx;
988             sx = 0;
989           }
990
991           if (sx + ex > visWidth)
992           {
993             ex = visWidth;
994           }
995           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
996           {
997             ex = (endRes - startRes + 1) * charWidth;
998           }
999
1000           if (top != -1)
1001           {
1002             g.drawLine(sx, top, sx + ex, top);
1003             top = -1;
1004           }
1005
1006           if (bottom != -1)
1007           {
1008             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1009             bottom = -1;
1010           }
1011
1012           inGroup = false;
1013         }
1014
1015         groupIndex++;
1016
1017         g.setStroke(new BasicStroke());
1018
1019         if (groupIndex >= av.getAlignment().getGroups().size())
1020         {
1021           break;
1022         }
1023
1024         group = av.getAlignment().getGroups().get(groupIndex);
1025
1026       } while (groupIndex < av.getAlignment().getGroups().size());
1027
1028     }
1029
1030   }
1031
1032   /*
1033    * Draw the selection group as a separate image and overlay
1034    */
1035   private BufferedImage drawSelectionGroup()
1036   {
1037     int verticalOffset = 0;
1038     if (av.getWrapAlignment())
1039     {
1040       verticalOffset = charHeight;
1041       if (av.getScaleAboveWrapped())
1042       {
1043         verticalOffset += charHeight;
1044       }
1045     }
1046
1047     // get a new image of the correct size
1048     BufferedImage selectionImage = setupImage();
1049
1050     if (selectionImage == null)
1051     {
1052       return null;
1053     }
1054
1055     SequenceGroup group = av.getSelectionGroup();
1056     if (group == null)
1057     {
1058       // nothing to draw
1059       return null;
1060     }
1061
1062     // set up drawing colour
1063     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1064     g.translate(LABEL_WEST, 0);
1065     // set background to transparent
1066     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1067     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1068
1069     g.setComposite(AlphaComposite.Src);
1070     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1071             BasicStroke.JOIN_ROUND, 3f, new float[]
1072     { 5f, 3f }, 0f));
1073     g.setColor(Color.RED);
1074
1075     int visWidth = av.getRanges().getViewportWidth() * charWidth;
1076
1077     int startRes = av.getRanges().getStartRes();
1078
1079     // set x start and end positions of group
1080     int startx = (group.getStartRes() - startRes) * charWidth;
1081     int endx = (group.getEndRes() - startRes + 1) * charWidth;
1082
1083     int oldY = -1;
1084     int i = 0;
1085     boolean inGroup = false;
1086     int top = -1;
1087     int bottom = -1;
1088
1089     // get sequences to determine y positions of group
1090     int startSeq = av.getRanges().getStartSeq();
1091     for (i = startSeq; i <= av.getRanges().getEndSeq(); ++i)
1092     {
1093       int sy = verticalOffset + (i - startSeq) * charHeight;
1094
1095       if (group.getSequences(null)
1096               .contains(av.getAlignment().getSequenceAt(i)))
1097       {
1098         if ((bottom == -1) && !group.getSequences(null)
1099                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1100         {
1101           bottom = sy + charHeight;
1102         }
1103
1104         if (!inGroup)
1105         {
1106           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1107                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1108           {
1109             top = sy;
1110           }
1111
1112           oldY = sy;
1113           inGroup = true;
1114         }
1115       }
1116       else
1117       {
1118         if (inGroup)
1119         {
1120           // if start position is visible, draw vertical line to left of
1121           // group
1122           if (startx >= 0 && startx < visWidth * charWidth)
1123           {
1124             g.drawLine(startx, oldY, startx, sy);
1125           }
1126
1127           // if end position is visible, draw vertical line to right of
1128           // group
1129           if (endx <= visWidth * charWidth)
1130           {
1131             g.drawLine(endx, oldY, endx, sy);
1132           }
1133
1134           if (endx > visWidth * charWidth)
1135           {
1136             endx = visWidth * charWidth;
1137           }
1138
1139           // draw horizontal line at top of group
1140           if (top != -1)
1141           {
1142             g.drawLine(startx, top, endx, top);
1143             top = -1;
1144           }
1145
1146           // draw horizontal line at bottom of group
1147           if (bottom != -1)
1148           {
1149             g.drawLine(startx, bottom, endx, bottom);
1150             bottom = -1;
1151           }
1152
1153           inGroup = false;
1154         }
1155       }
1156     }
1157     if (inGroup)
1158     {
1159       int sy = verticalOffset + (i - startSeq) * charHeight;
1160       if (startx >= 0 && startx < visWidth)
1161       {
1162         g.drawLine(startx, oldY, startx, sy);
1163       }
1164
1165       if (endx < visWidth)
1166       {
1167         g.drawLine(endx, oldY, endx, sy);
1168       }
1169
1170       if (endx > visWidth)
1171       {
1172         endx = visWidth;
1173       }
1174
1175       if (top != -1)
1176       {
1177         g.drawLine(startx, top, endx, top);
1178         top = -1;
1179       }
1180
1181       if (bottom != -1)
1182       {
1183         g.drawLine(startx, bottom - 1, endx, bottom - 1);
1184         bottom = -1;
1185       }
1186
1187       inGroup = false;
1188     }
1189     g.translate(-LABEL_WEST, 0);
1190     return selectionImage;
1191   }
1192
1193   /**
1194    * DOCUMENT ME!
1195    * 
1196    * @param results
1197    *          DOCUMENT ME!
1198    */
1199   public void highlightSearchResults(SearchResultsI results)
1200   {
1201     img = null;
1202
1203     av.setSearchResults(results);
1204
1205     repaint();
1206   }
1207
1208   @Override
1209   public void propertyChange(PropertyChangeEvent evt)
1210   {
1211     String eventName = evt.getPropertyName();
1212
1213     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1214     {
1215       paintSeqGroup();
1216     }
1217     else if (av.getWrapAlignment())
1218     {
1219       if (eventName.equals(ViewportRanges.STARTRES))
1220       {
1221         repaint();
1222       }
1223     }
1224     else
1225     {
1226       int scrollX = 0;
1227       if (eventName.equals(ViewportRanges.STARTRES))
1228       {
1229         // Make sure we're not trying to draw a panel
1230         // larger than the visible window
1231         ViewportRanges vpRanges = av.getRanges();
1232         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1233         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1234         if (scrollX > range)
1235         {
1236           scrollX = range;
1237         }
1238         else if (scrollX < -range)
1239         {
1240           scrollX = -range;
1241         }
1242       }
1243
1244       // Both scrolling and resizing change viewport ranges: scrolling changes
1245       // both start and end points, but resize only changes end values.
1246       // Here we only want to fastpaint on a scroll, with resize using a normal
1247       // paint, so scroll events are identified as changes to the horizontal or
1248       // vertical start value.
1249       if (eventName.equals(ViewportRanges.STARTRES))
1250       {
1251         // scroll - startres and endres both change
1252         fastPaint(scrollX, 0);
1253       }
1254       else if (eventName.equals(ViewportRanges.STARTSEQ))
1255       {
1256         // scroll
1257         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1258       }
1259     }
1260   }
1261 }