JAL-2665 Added hidden columns group selection - almost working
[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     ViewportRanges ranges = av.getRanges();
361     BufferedImage selectImage = drawSelectionGroup(
362             av.getRanges().getStartRes(), av.getRanges().getEndRes(),
363             ranges.getStartSeq(), ranges.getEndSeq());
364
365     if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
366             || (getVisibleRect().height != g.getClipBounds().height))
367     {
368       BufferedImage lcimg = buildLocalImage(selectImage);
369       g.drawImage(lcimg, 0, 0, this);
370
371       fastPaint = false;
372       return;
373     }
374
375     int width = getWidth();
376     int height = getHeight();
377
378     width -= (width % charWidth);
379     height -= (height % charHeight);
380
381     if ((width < 1) || (height < 1))
382     {
383       return;
384     }
385
386     if (img == null || width != img.getWidth() || height != img.getHeight())
387     {
388       img = setupImage();
389       gg = (Graphics2D) img.getGraphics();
390       gg.setFont(av.getFont());
391     }
392     if (img == null)
393     {
394       return;
395     }
396
397     
398     if (av.antiAlias)
399     {
400       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
401               RenderingHints.VALUE_ANTIALIAS_ON);
402     }
403     
404     gg.setColor(Color.white);
405     gg.fillRect(0, 0, img.getWidth(), img.getHeight());
406     
407     if (av.getWrapAlignment())
408     {
409       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
410     }
411     else
412     {
413       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
414               ranges.getStartSeq(), ranges.getEndSeq(), 0);
415     }
416
417     BufferedImage lcimg = buildLocalImage(selectImage);
418     g.drawImage(lcimg, 0, 0, this);
419   }
420
421   /*
422    * Make a local image by combining the cached image img
423    * with any selection
424    */
425   private BufferedImage buildLocalImage(BufferedImage selectImage)
426   {
427     // clone the cached image
428     BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
429             img.getType());
430     Graphics2D g2d = lcimg.createGraphics();
431     g2d.drawImage(img, 0, 0, null);
432
433     // overlay selection group on lcimg
434     if (selectImage != null)
435     {
436       g2d.setComposite(
437               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
438       g2d.drawImage(selectImage, 0, 0, this);
439     }
440     g2d.dispose();
441
442     return lcimg;
443   }
444
445   private void paintSeqGroup()
446   {
447     fastPaint = true;
448     repaint();
449   }
450
451   private BufferedImage setupImage()
452   {
453     BufferedImage lcimg = null;
454
455     int width = getWidth();
456     int height = getHeight();
457
458     width -= (width % charWidth);
459     height -= (height % charHeight);
460
461     if ((width < 1) || (height < 1))
462     {
463       return null;
464     }
465
466     try
467     {
468       lcimg = new BufferedImage(width, height,
469               BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
470     } catch (OutOfMemoryError er)
471     {
472       System.gc();
473       System.err.println(
474               "Selection Group image OutOfMemory Redraw Error.\n" + er);
475       new OOMWarning("Creating alignment image for display", er);
476
477       return null;
478     }
479
480     return lcimg;
481   }
482
483   /**
484    * DOCUMENT ME!
485    * 
486    * @param cwidth
487    *          DOCUMENT ME!
488    * 
489    * @return DOCUMENT ME!
490    */
491   public int getWrappedCanvasWidth(int cwidth)
492   {
493     FontMetrics fm = getFontMetrics(av.getFont());
494
495     LABEL_EAST = 0;
496     LABEL_WEST = 0;
497
498     if (av.getScaleRightWrapped())
499     {
500       LABEL_EAST = fm.stringWidth(getMask());
501     }
502
503     if (av.getScaleLeftWrapped())
504     {
505       LABEL_WEST = fm.stringWidth(getMask());
506     }
507
508     return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
509   }
510
511   /**
512    * Generates a string of zeroes.
513    * 
514    * @return String
515    */
516   String getMask()
517   {
518     String mask = "00";
519     int maxWidth = 0;
520     int tmp;
521     for (int i = 0; i < av.getAlignment().getHeight(); i++)
522     {
523       tmp = av.getAlignment().getSequenceAt(i).getEnd();
524       if (tmp > maxWidth)
525       {
526         maxWidth = tmp;
527       }
528     }
529
530     for (int i = maxWidth; i > 0; i /= 10)
531     {
532       mask += "0";
533     }
534     return mask;
535   }
536
537   /**
538    * DOCUMENT ME!
539    * 
540    * @param g
541    *          DOCUMENT ME!
542    * @param canvasWidth
543    *          DOCUMENT ME!
544    * @param canvasHeight
545    *          DOCUMENT ME!
546    * @param startRes
547    *          DOCUMENT ME!
548    */
549   public void drawWrappedPanel(Graphics g, int canvasWidth,
550           int canvasHeight, int startRes)
551   {
552     updateViewport();
553     AlignmentI al = av.getAlignment();
554
555     FontMetrics fm = getFontMetrics(av.getFont());
556
557     LABEL_EAST = 0;
558     LABEL_WEST = 0;
559
560     if (av.getScaleRightWrapped())
561     {
562       LABEL_EAST = fm.stringWidth(getMask());
563     }
564
565     if (av.getScaleLeftWrapped())
566     {
567       LABEL_WEST = fm.stringWidth(getMask());
568     }
569
570     int hgap = charHeight;
571     if (av.getScaleAboveWrapped())
572     {
573       hgap += charHeight;
574     }
575
576     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
577     int cHeight = av.getAlignment().getHeight() * charHeight;
578
579     av.setWrappedWidth(cWidth);
580
581     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
582
583     int endx;
584     int ypos = hgap;
585     int maxwidth = av.getAlignment().getWidth();
586
587     if (av.hasHiddenColumns())
588     {
589       maxwidth = av.getAlignment().getHiddenColumns()
590               .findColumnPosition(maxwidth);
591     }
592
593     while ((ypos <= canvasHeight) && (startRes < maxwidth))
594     {
595       endx = startRes + cWidth - 1;
596
597       if (endx > maxwidth)
598       {
599         endx = maxwidth;
600       }
601
602       g.setFont(av.getFont());
603       g.setColor(Color.black);
604
605       if (av.getScaleLeftWrapped())
606       {
607         drawWestScale(g, startRes, endx, ypos);
608       }
609
610       if (av.getScaleRightWrapped())
611       {
612         g.translate(canvasWidth - LABEL_EAST, 0);
613         drawEastScale(g, startRes, endx, ypos);
614         g.translate(-(canvasWidth - LABEL_EAST), 0);
615       }
616
617       g.translate(LABEL_WEST, 0);
618
619       if (av.getScaleAboveWrapped())
620       {
621         drawNorthScale(g, startRes, endx, ypos);
622       }
623
624       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
625       {
626         g.setColor(Color.blue);
627         int res;
628         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
629         List<Integer> positions = hidden.findHiddenRegionPositions();
630         for (int pos : positions)
631         {
632           res = pos - startRes;
633
634           if (res < 0 || res > endx - startRes)
635           {
636             continue;
637           }
638
639           gg.fillPolygon(
640                   new int[] { res * charWidth - charHeight / 4,
641                       res * charWidth + charHeight / 4, res * charWidth },
642                   new int[] { ypos - (charHeight / 2),
643                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
644                   3);
645
646         }
647       }
648
649       // When printing we have an extra clipped region,
650       // the Printable page which we need to account for here
651       Shape clip = g.getClip();
652
653       if (clip == null)
654       {
655         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
656       }
657       else
658       {
659         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
660                 (int) clip.getBounds().getHeight());
661       }
662
663       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
664
665       if (av.isShowAnnotation())
666       {
667         g.translate(0, cHeight + ypos + 3);
668         if (annotations == null)
669         {
670           annotations = new AnnotationPanel(av);
671         }
672
673         annotations.renderer.drawComponent(annotations, av, g, -1,
674                 startRes, endx + 1);
675         g.translate(0, -cHeight - ypos - 3);
676       }
677       g.setClip(clip);
678       g.translate(-LABEL_WEST, 0);
679
680       ypos += cHeight + getAnnotationHeight() + hgap;
681
682       startRes += cWidth;
683     }
684   }
685
686   AnnotationPanel annotations;
687
688   int getAnnotationHeight()
689   {
690     if (!av.isShowAnnotation())
691     {
692       return 0;
693     }
694
695     if (annotations == null)
696     {
697       annotations = new AnnotationPanel(av);
698     }
699
700     return annotations.adjustPanelHeight();
701   }
702
703   /**
704    * DOCUMENT ME!
705    * 
706    * @param g1
707    *          DOCUMENT ME!
708    * @param startRes
709    *          DOCUMENT ME!
710    * @param endRes
711    *          DOCUMENT ME!
712    * @param startSeq
713    *          DOCUMENT ME!
714    * @param endSeq
715    *          DOCUMENT ME!
716    * @param offset
717    *          DOCUMENT ME!
718    */
719   public void drawPanel(Graphics g1, int startRes, int endRes,
720           int startSeq, int endSeq, int offset)
721   {
722     updateViewport();
723     if (!av.hasHiddenColumns())
724     {
725       draw(g1, startRes, endRes, startSeq, endSeq, offset);
726     }
727     else
728     {
729       int screenY = 0;
730       int blockStart = startRes;
731       int blockEnd = endRes;
732
733       for (int[] region : av.getAlignment().getHiddenColumns()
734               .getHiddenColumnsCopy())
735       {
736         int hideStart = region[0];
737         int hideEnd = region[1];
738
739         if (hideStart <= blockStart)
740         {
741           blockStart += (hideEnd - hideStart) + 1;
742           continue;
743         }
744
745         blockEnd = hideStart - 1;
746
747         g1.translate(screenY * charWidth, 0);
748
749         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
750
751         if (av.getShowHiddenMarkers())
752         {
753           g1.setColor(Color.blue);
754
755           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
756                   0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
757                   (endSeq - startSeq + 1) * charHeight + offset);
758         }
759
760         g1.translate(-screenY * charWidth, 0);
761         screenY += blockEnd - blockStart + 1;
762         blockStart = hideEnd + 1;
763
764         if (screenY > (endRes - startRes))
765         {
766           // already rendered last block
767           return;
768         }
769       }
770
771       if (screenY <= (endRes - startRes))
772       {
773         // remaining visible region to render
774         blockEnd = blockStart + (endRes - startRes) - screenY;
775         g1.translate(screenY * charWidth, 0);
776         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
777
778         g1.translate(-screenY * charWidth, 0);
779       }
780     }
781
782   }
783
784   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
785   // int x1, int x2, int y1, int y2, int startx, int starty,
786   private void draw(Graphics g, int startRes, int endRes, int startSeq,
787           int endSeq, int offset)
788   {
789     g.setFont(av.getFont());
790     sr.prepare(g, av.isRenderGaps());
791
792     SequenceI nextSeq;
793
794     // / First draw the sequences
795     // ///////////////////////////
796     for (int i = startSeq; i <= endSeq; i++)
797     {
798       nextSeq = av.getAlignment().getSequenceAt(i);
799       if (nextSeq == null)
800       {
801         // occasionally, a race condition occurs such that the alignment row is
802         // empty
803         continue;
804       }
805       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
806               startRes, endRes, offset + ((i - startSeq) * charHeight));
807
808       if (av.isShowSequenceFeatures())
809       {
810         fr.drawSequence(g, nextSeq, startRes, endRes, offset
811                 + ((i - startSeq) * charHeight), false);
812       }
813
814       // / Highlight search Results once all sequences have been drawn
815       // ////////////////////////////////////////////////////////
816       if (av.hasSearchResults())
817       {
818         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
819                 startRes, endRes);
820         if (visibleResults != null)
821         {
822           for (int r = 0; r < visibleResults.length; r += 2)
823           {
824             sr.drawHighlightedText(nextSeq, visibleResults[r],
825                     visibleResults[r + 1], (visibleResults[r] - startRes)
826                             * charWidth, offset
827                             + ((i - startSeq) * charHeight));
828           }
829         }
830       }
831
832       if (av.cursorMode && cursorY == i && cursorX >= startRes
833               && cursorX <= endRes)
834       {
835         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
836                 offset + ((i - startSeq) * charHeight));
837       }
838     }
839
840     if (av.getSelectionGroup() != null
841             || av.getAlignment().getGroups().size() > 0)
842     {
843       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
844     }
845
846   }
847
848   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
849           int startSeq, int endSeq, int offset)
850   {
851     Graphics2D g = (Graphics2D) g1;
852     //
853     // ///////////////////////////////////
854     // Now outline any areas if necessary
855     // ///////////////////////////////////
856
857     SequenceGroup group = null;
858
859     int sx = -1;
860     int sy = -1;
861     int ex = -1;
862     int groupIndex = -1;
863     int visWidth = (endRes - startRes + 1) * charWidth;
864
865     if (av.getAlignment().getGroups().size() > 0)
866     {
867       group = av.getAlignment().getGroups().get(0);
868       groupIndex = 0;
869     }
870
871     if (group != null)
872     {
873       do
874       {
875         int oldY = -1;
876         int i = 0;
877         boolean inGroup = false;
878         int top = -1;
879         int bottom = -1;
880
881         for (i = startSeq; i <= endSeq; i++)
882         {
883           // position of start residue of group relative to startRes, in pixels
884           sx = (group.getStartRes() - startRes) * charWidth;
885           sy = offset + ((i - startSeq) * charHeight);
886           // width of group in pixels
887           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
888
889           if (sx + ex < 0 || sx > visWidth)
890           {
891             continue;
892           }
893
894           if ((sx <= (endRes - startRes) * charWidth)
895                   && group.getSequences(null).contains(
896                           av.getAlignment().getSequenceAt(i)))
897           {
898             if ((bottom == -1)
899                     && !group.getSequences(null).contains(
900                             av.getAlignment().getSequenceAt(i + 1)))
901             {
902               bottom = sy + charHeight;
903             }
904
905             if (!inGroup)
906             {
907               if (((top == -1) && (i == 0))
908                       || !group.getSequences(null).contains(
909                               av.getAlignment().getSequenceAt(i - 1)))
910               {
911                 top = sy;
912               }
913
914               oldY = sy;
915               inGroup = true;
916
917               g.setStroke(new BasicStroke());
918               g.setColor(group.getOutlineColour());
919             }
920           }
921           else
922           {
923             if (inGroup)
924             {
925               // if start position is visible, draw vertical line to left of
926               // group
927               if (sx >= 0 && sx < visWidth)
928               {
929                 g.drawLine(sx, oldY, sx, sy);
930               }
931
932               // if end position is visible, draw vertical line to right of
933               // group
934               if (sx + ex < visWidth)
935               {
936                 g.drawLine(sx + ex, oldY, sx + ex, sy);
937               }
938
939               if (sx < 0)
940               {
941                 ex += sx;
942                 sx = 0;
943               }
944
945               if (sx + ex > visWidth)
946               {
947                 ex = visWidth;
948               }
949
950               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
951               {
952                 ex = (endRes - startRes + 1) * charWidth;
953               }
954
955               // draw horizontal line at top of group
956               if (top != -1)
957               {
958                 g.drawLine(sx, top, sx + ex, top);
959                 top = -1;
960               }
961
962               // draw horizontal line at bottom of group
963               if (bottom != -1)
964               {
965                 g.drawLine(sx, bottom, sx + ex, bottom);
966                 bottom = -1;
967               }
968
969               inGroup = false;
970             }
971           }
972         }
973
974         if (inGroup)
975         {
976           sy = offset + ((i - startSeq) * charHeight);
977           if (sx >= 0 && sx < visWidth)
978           {
979             g.drawLine(sx, oldY, sx, sy);
980           }
981
982           if (sx + ex < visWidth)
983           {
984             g.drawLine(sx + ex, oldY, sx + ex, sy);
985           }
986
987           if (sx < 0)
988           {
989             ex += sx;
990             sx = 0;
991           }
992
993           if (sx + ex > visWidth)
994           {
995             ex = visWidth;
996           }
997           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
998           {
999             ex = (endRes - startRes + 1) * charWidth;
1000           }
1001
1002           if (top != -1)
1003           {
1004             g.drawLine(sx, top, sx + ex, top);
1005             top = -1;
1006           }
1007
1008           if (bottom != -1)
1009           {
1010             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1011             bottom = -1;
1012           }
1013
1014           inGroup = false;
1015         }
1016
1017         groupIndex++;
1018
1019         g.setStroke(new BasicStroke());
1020
1021         if (groupIndex >= av.getAlignment().getGroups().size())
1022         {
1023           break;
1024         }
1025
1026         group = av.getAlignment().getGroups().get(groupIndex);
1027
1028       } while (groupIndex < av.getAlignment().getGroups().size());
1029
1030     }
1031
1032   }
1033
1034
1035   /*
1036    * Draw the selection group as a separate image and overlay
1037    */
1038   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1039           int startSeq, int endSeq)
1040   {
1041     // get a new image of the correct size
1042     BufferedImage selectionImage = setupImage();
1043
1044     if (selectionImage == null)
1045     {
1046       return null;
1047     }
1048
1049     SequenceGroup group = av.getSelectionGroup();
1050     if (group == null)
1051     {
1052       // nothing to draw
1053       return null;
1054     }
1055     
1056     // set up drawing colour
1057     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1058     g.translate(LABEL_WEST, 0);
1059     // set background to transparent
1060     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1061     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1062
1063     g.setComposite(AlphaComposite.Src);
1064     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1065             BasicStroke.JOIN_ROUND, 3f, new float[]
1066     { 5f, 3f }, 0f));
1067     g.setColor(Color.RED);
1068
1069     if (!av.hasHiddenColumns())
1070     {
1071       drawSelectionGroupPart(g, group, startRes, endRes, startSeq, endSeq);
1072     }
1073     else
1074     {
1075       // package into blocks of visible columns
1076       // Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1077       // Graphics g1 = selectionImage.getGraphics();
1078
1079       int screenY = 0;
1080       int blockStart = startRes;
1081       int blockEnd = endRes;
1082
1083       for (int[] region : av.getAlignment().getHiddenColumns()
1084               .getHiddenColumnsCopy())
1085       {
1086         int hideStart = region[0];
1087         int hideEnd = region[1];
1088
1089         if (hideStart <= blockStart)
1090         {
1091           blockStart += (hideEnd - hideStart) + 1;
1092           continue;
1093         }
1094
1095         blockEnd = hideStart - 1;
1096
1097         g.translate(screenY * charWidth, 0);
1098
1099         drawSelectionGroupPart(g, group,
1100                 blockStart, blockEnd, startSeq, endSeq);
1101
1102         g.translate(-screenY * charWidth, 0);
1103         screenY += blockEnd - blockStart + 1;
1104         blockStart = hideEnd + 1;
1105
1106         if (screenY > (endRes - startRes))
1107         {
1108           // already rendered last block
1109           break;
1110         }
1111       }
1112
1113       if (screenY <= (endRes - startRes))
1114       {
1115         // remaining visible region to render
1116         blockEnd = blockStart + (endRes - startRes) - screenY;
1117         g.translate(screenY * charWidth, 0);
1118         drawSelectionGroupPart(g, group,
1119                 blockStart, blockEnd, startSeq, endSeq);
1120
1121         g.translate(-screenY * charWidth, 0);
1122       }
1123     }
1124     g.translate(-LABEL_WEST, 0);
1125     return selectionImage;
1126   }
1127
1128   /*
1129    * Draw the selection group as a separate image and overlay
1130    */
1131   private void drawSelectionGroupPart(Graphics2D g, SequenceGroup group,
1132           int startRes, int endRes, int startSeq, int endSeq)
1133   {
1134     // set up values in case the alignment is wrapped
1135     int verticalOffset = 0;
1136     int horizontalOffset = 0;
1137     if (av.getWrapAlignment())
1138     {
1139       int hgap = charHeight;
1140       if (av.getScaleAboveWrapped())
1141       {
1142         hgap += charHeight;
1143       }
1144       
1145       // get the start res of the group and work out the offsets for it in the wrapped alignment
1146       int groupstart = group.getStartRes();
1147       int cWidth = (getWidth() - LABEL_EAST - LABEL_WEST) / charWidth;
1148       
1149       // group is in which slice of alignment? res position / width in residues
1150       int slice = groupstart / cWidth;
1151       // vertical offset is increased by slice number * number of sequences * height of each sequence
1152       verticalOffset = slice * (av.getAlignment().getHeight() * charHeight
1153               + getAnnotationHeight() + hgap) + hgap;
1154
1155       // horizontal offset is number of residues to subtract from group residue
1156       // position
1157       horizontalOffset = (slice * cWidth);
1158     }
1159
1160     int visWidth = av.getRanges().getViewportWidth() * charWidth;
1161
1162     // set x start and end positions of group
1163     int startx = (group.getStartRes() - startRes - horizontalOffset)
1164             * charWidth;
1165     int endx = (group.getEndRes() - startRes + 1 - horizontalOffset)
1166             * charWidth;
1167
1168     int oldY = -1;
1169     int i = 0;
1170     boolean inGroup = false;
1171     int top = -1;
1172     int bottom = -1;
1173
1174     // get sequences to determine y positions of group
1175     for (i = startSeq; i <= endSeq; ++i)
1176     {
1177       int sy = verticalOffset + (i - startSeq) * charHeight;
1178
1179       if (group.getSequences(null)
1180               .contains(av.getAlignment().getSequenceAt(i)))
1181       {
1182         if ((bottom == -1) && !group.getSequences(null)
1183                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1184         {
1185           bottom = sy + charHeight;
1186         }
1187
1188         if (!inGroup)
1189         {
1190           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1191                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1192           {
1193             top = sy;
1194           }
1195
1196           oldY = sy;
1197           inGroup = true;
1198         }
1199       }
1200       else
1201       {
1202         if (inGroup)
1203         {
1204           // if start position is visible, draw vertical line to left of
1205           // group
1206           if (startx >= 0 && startx < visWidth * charWidth)
1207           {
1208             g.drawLine(startx, oldY, startx, sy);
1209           }
1210
1211           // if end position is visible, draw vertical line to right of
1212           // group
1213           if (endx <= visWidth * charWidth)
1214           {
1215             g.drawLine(endx, oldY, endx, sy);
1216           }
1217
1218           if (endx > visWidth * charWidth)
1219           {
1220             endx = visWidth * charWidth;
1221           }
1222
1223           // draw horizontal line at top of group
1224           if (top != -1)
1225           {
1226             g.drawLine(startx, top, endx, top);
1227             top = -1;
1228           }
1229
1230           // draw horizontal line at bottom of group
1231           if (bottom != -1)
1232           {
1233             g.drawLine(startx, bottom, endx, bottom);
1234             bottom = -1;
1235           }
1236
1237           inGroup = false;
1238         }
1239       }
1240     }
1241     if (inGroup)
1242     {
1243       int sy = verticalOffset + (i - startSeq) * charHeight;
1244       if (startx >= 0 && startx < visWidth)
1245       {
1246         g.drawLine(startx, oldY, startx, sy);
1247       }
1248
1249       if (endx < visWidth)
1250       {
1251         g.drawLine(endx, oldY, endx, sy);
1252       }
1253
1254       if (endx > visWidth)
1255       {
1256         endx = visWidth;
1257       }
1258
1259       if (top != -1)
1260       {
1261         g.drawLine(startx, top, endx, top);
1262         top = -1;
1263       }
1264
1265       if (bottom != -1)
1266       {
1267         g.drawLine(startx, bottom - 1, endx, bottom - 1);
1268         bottom = -1;
1269       }
1270
1271       inGroup = false;
1272     }
1273   }
1274
1275   /**
1276    * DOCUMENT ME!
1277    * 
1278    * @param results
1279    *          DOCUMENT ME!
1280    */
1281   public void highlightSearchResults(SearchResultsI results)
1282   {
1283     img = null;
1284
1285     av.setSearchResults(results);
1286
1287     repaint();
1288   }
1289
1290   @Override
1291   public void propertyChange(PropertyChangeEvent evt)
1292   {
1293     String eventName = evt.getPropertyName();
1294
1295     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1296     {
1297       paintSeqGroup();
1298     }
1299     else if (av.getWrapAlignment())
1300     {
1301       if (eventName.equals(ViewportRanges.STARTRES))
1302       {
1303         repaint();
1304       }
1305     }
1306     else
1307     {
1308       int scrollX = 0;
1309       if (eventName.equals(ViewportRanges.STARTRES))
1310       {
1311         // Make sure we're not trying to draw a panel
1312         // larger than the visible window
1313         ViewportRanges vpRanges = av.getRanges();
1314         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1315         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1316         if (scrollX > range)
1317         {
1318           scrollX = range;
1319         }
1320         else if (scrollX < -range)
1321         {
1322           scrollX = -range;
1323         }
1324       }
1325
1326       // Both scrolling and resizing change viewport ranges: scrolling changes
1327       // both start and end points, but resize only changes end values.
1328       // Here we only want to fastpaint on a scroll, with resize using a normal
1329       // paint, so scroll events are identified as changes to the horizontal or
1330       // vertical start value.
1331       if (eventName.equals(ViewportRanges.STARTRES))
1332       {
1333         // scroll - startres and endres both change
1334         fastPaint(scrollX, 0);
1335       }
1336       else if (eventName.equals(ViewportRanges.STARTSEQ))
1337       {
1338         // scroll
1339         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1340       }
1341     }
1342   }
1343 }