JAL-2665 Fixed wrapped mode group over >1 panel; and hidden cols
[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   public void drawWrappedSelection(Graphics2D g, SequenceGroup group,
687           int canvasWidth,
688           int canvasHeight, int startRes)
689   {
690     int hgap = charHeight;
691     if (av.getScaleAboveWrapped())
692     {
693       hgap += charHeight;
694     }
695
696     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
697     int cHeight = av.getAlignment().getHeight() * charHeight;
698
699     int endx;
700     int ypos = hgap;
701     int maxwidth = av.getAlignment().getWidth();
702
703     if (av.hasHiddenColumns())
704     {
705       maxwidth = av.getAlignment().getHiddenColumns()
706               .findColumnPosition(maxwidth);
707     }
708
709     while ((ypos <= canvasHeight) && (startRes < maxwidth))
710     {
711       endx = startRes + cWidth - 1;
712
713       if (endx > maxwidth)
714       {
715         endx = maxwidth;
716       }
717
718       g.translate(LABEL_WEST, 0);
719
720       drawUnwrappedSelection(g, group, startRes, endx, 0,
721               av.getAlignment().getHeight() - 1,
722               ypos);
723
724       g.translate(-LABEL_WEST, 0);
725
726       ypos += cHeight + getAnnotationHeight() + hgap;
727
728       startRes += cWidth;
729     }
730   }
731
732   AnnotationPanel annotations;
733
734   int getAnnotationHeight()
735   {
736     if (!av.isShowAnnotation())
737     {
738       return 0;
739     }
740
741     if (annotations == null)
742     {
743       annotations = new AnnotationPanel(av);
744     }
745
746     return annotations.adjustPanelHeight();
747   }
748
749   /**
750    * DOCUMENT ME!
751    * 
752    * @param g1
753    *          DOCUMENT ME!
754    * @param startRes
755    *          DOCUMENT ME!
756    * @param endRes
757    *          DOCUMENT ME!
758    * @param startSeq
759    *          DOCUMENT ME!
760    * @param endSeq
761    *          DOCUMENT ME!
762    * @param offset
763    *          DOCUMENT ME!
764    */
765   public void drawPanel(Graphics g1, int startRes, int endRes,
766           int startSeq, int endSeq, int offset)
767   {
768     updateViewport();
769     if (!av.hasHiddenColumns())
770     {
771       draw(g1, startRes, endRes, startSeq, endSeq, offset);
772     }
773     else
774     {
775       int screenY = 0;
776       int blockStart = startRes;
777       int blockEnd = endRes;
778
779       for (int[] region : av.getAlignment().getHiddenColumns()
780               .getHiddenColumnsCopy())
781       {
782         int hideStart = region[0];
783         int hideEnd = region[1];
784
785         if (hideStart <= blockStart)
786         {
787           blockStart += (hideEnd - hideStart) + 1;
788           continue;
789         }
790
791         blockEnd = hideStart - 1;
792
793         g1.translate(screenY * charWidth, 0);
794
795         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
796
797         if (av.getShowHiddenMarkers())
798         {
799           g1.setColor(Color.blue);
800
801           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
802                   0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
803                   (endSeq - startSeq + 1) * charHeight + offset);
804         }
805
806         g1.translate(-screenY * charWidth, 0);
807         screenY += blockEnd - blockStart + 1;
808         blockStart = hideEnd + 1;
809
810         if (screenY > (endRes - startRes))
811         {
812           // already rendered last block
813           return;
814         }
815       }
816
817       if (screenY <= (endRes - startRes))
818       {
819         // remaining visible region to render
820         blockEnd = blockStart + (endRes - startRes) - screenY;
821         g1.translate(screenY * charWidth, 0);
822         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
823
824         g1.translate(-screenY * charWidth, 0);
825       }
826     }
827
828   }
829
830   private void draw(Graphics g, int startRes, int endRes, int startSeq,
831           int endSeq, int offset)
832   {
833     g.setFont(av.getFont());
834     sr.prepare(g, av.isRenderGaps());
835
836     SequenceI nextSeq;
837
838     // / First draw the sequences
839     // ///////////////////////////
840     for (int i = startSeq; i <= endSeq; i++)
841     {
842       nextSeq = av.getAlignment().getSequenceAt(i);
843       if (nextSeq == null)
844       {
845         // occasionally, a race condition occurs such that the alignment row is
846         // empty
847         continue;
848       }
849       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
850               startRes, endRes, offset + ((i - startSeq) * charHeight));
851
852       if (av.isShowSequenceFeatures())
853       {
854         fr.drawSequence(g, nextSeq, startRes, endRes, offset
855                 + ((i - startSeq) * charHeight), false);
856       }
857
858       // / Highlight search Results once all sequences have been drawn
859       // ////////////////////////////////////////////////////////
860       if (av.hasSearchResults())
861       {
862         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
863                 startRes, endRes);
864         if (visibleResults != null)
865         {
866           for (int r = 0; r < visibleResults.length; r += 2)
867           {
868             sr.drawHighlightedText(nextSeq, visibleResults[r],
869                     visibleResults[r + 1], (visibleResults[r] - startRes)
870                             * charWidth, offset
871                             + ((i - startSeq) * charHeight));
872           }
873         }
874       }
875
876       if (av.cursorMode && cursorY == i && cursorX >= startRes
877               && cursorX <= endRes)
878       {
879         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
880                 offset + ((i - startSeq) * charHeight));
881       }
882     }
883
884     if (av.getSelectionGroup() != null
885             || av.getAlignment().getGroups().size() > 0)
886     {
887       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
888     }
889
890   }
891
892   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
893           int startSeq, int endSeq, int offset)
894   {
895     Graphics2D g = (Graphics2D) g1;
896     //
897     // ///////////////////////////////////
898     // Now outline any areas if necessary
899     // ///////////////////////////////////
900
901     SequenceGroup group = null;
902
903     int sx = -1;
904     int sy = -1;
905     int ex = -1;
906     int groupIndex = -1;
907     int visWidth = (endRes - startRes + 1) * charWidth;
908
909     if (av.getAlignment().getGroups().size() > 0)
910     {
911       group = av.getAlignment().getGroups().get(0);
912       groupIndex = 0;
913     }
914
915     if (group != null)
916     {
917       do
918       {
919         int oldY = -1;
920         int i = 0;
921         boolean inGroup = false;
922         int top = -1;
923         int bottom = -1;
924
925         for (i = startSeq; i <= endSeq; i++)
926         {
927           // position of start residue of group relative to startRes, in pixels
928           sx = (group.getStartRes() - startRes) * charWidth;
929           sy = offset + ((i - startSeq) * charHeight);
930           // width of group in pixels
931           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
932                   - 1;
933
934           if (sx + ex < 0 || sx > visWidth)
935           {
936             continue;
937           }
938
939           if ((sx <= (endRes - startRes) * charWidth)
940                   && group.getSequences(null).contains(
941                           av.getAlignment().getSequenceAt(i)))
942           {
943             if ((bottom == -1)
944                     && !group.getSequences(null).contains(
945                             av.getAlignment().getSequenceAt(i + 1)))
946             {
947               bottom = sy + charHeight;
948             }
949
950             if (!inGroup)
951             {
952               if (((top == -1) && (i == 0))
953                       || !group.getSequences(null).contains(
954                               av.getAlignment().getSequenceAt(i - 1)))
955               {
956                 top = sy;
957               }
958
959               oldY = sy;
960               inGroup = true;
961
962               g.setStroke(new BasicStroke());
963               g.setColor(group.getOutlineColour());
964             }
965           }
966           else
967           {
968             if (inGroup)
969             {
970               // if start position is visible, draw vertical line to left of
971               // group
972               if (sx >= 0 && sx < visWidth)
973               {
974                 g.drawLine(sx, oldY, sx, sy);
975               }
976
977               // if end position is visible, draw vertical line to right of
978               // group
979               if (sx + ex < visWidth)
980               {
981                 g.drawLine(sx + ex, oldY, sx + ex, sy);
982               }
983
984               if (sx < 0)
985               {
986                 ex += sx;
987                 sx = 0;
988               }
989
990               if (sx + ex > visWidth)
991               {
992                 ex = visWidth;
993               }
994               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
995               {
996                 ex = (endRes - startRes + 1) * charWidth;
997               }
998
999               // draw horizontal line at top of group
1000               if (top != -1)
1001               {
1002                 g.drawLine(sx, top, sx + ex, top);
1003                 top = -1;
1004               }
1005
1006               // draw horizontal line at bottom of group
1007               if (bottom != -1)
1008               {
1009                 g.drawLine(sx, bottom, sx + ex, bottom);
1010                 bottom = -1;
1011               }
1012
1013               inGroup = false;
1014             }
1015           }
1016         }
1017
1018         if (inGroup)
1019         {
1020           sy = offset + ((i - startSeq) * charHeight);
1021           if (sx >= 0 && sx < visWidth)
1022           {
1023             g.drawLine(sx, oldY, sx, sy);
1024           }
1025
1026           if (sx + ex < visWidth)
1027           {
1028             g.drawLine(sx + ex, oldY, sx + ex, sy);
1029           }
1030
1031           if (sx < 0)
1032           {
1033             ex += sx;
1034             sx = 0;
1035           }
1036
1037           if (sx + ex > visWidth)
1038           {
1039             ex = visWidth;
1040           }
1041           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1042           {
1043             ex = (endRes - startRes + 1) * charWidth;
1044           }
1045
1046           if (top != -1)
1047           {
1048             g.drawLine(sx, top, sx + ex, top);
1049             top = -1;
1050           }
1051
1052           if (bottom != -1)
1053           {
1054             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1055             bottom = -1;
1056           }
1057
1058           inGroup = false;
1059         }
1060
1061         groupIndex++;
1062
1063         g.setStroke(new BasicStroke());
1064
1065         if (groupIndex >= av.getAlignment().getGroups().size())
1066         {
1067           break;
1068         }
1069
1070         group = av.getAlignment().getGroups().get(groupIndex);
1071
1072       } while (groupIndex < av.getAlignment().getGroups().size());
1073
1074     }
1075
1076   }
1077
1078
1079   /*
1080    * Draw the selection group as a separate image and overlay
1081    */
1082   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1083           int startSeq, int endSeq)
1084   {
1085     // get a new image of the correct size
1086     BufferedImage selectionImage = setupImage();
1087
1088     if (selectionImage == null)
1089     {
1090       return null;
1091     }
1092
1093     SequenceGroup group = av.getSelectionGroup();
1094     if (group == null)
1095     {
1096       // nothing to draw
1097       return null;
1098     }
1099
1100     // set up drawing colour
1101     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1102     // g.translate(LABEL_WEST, 0);
1103     // set background to transparent
1104     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1105     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1106
1107     g.setComposite(AlphaComposite.Src);
1108     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1109             BasicStroke.JOIN_ROUND, 3f, new float[]
1110     { 5f, 3f }, 0f));
1111     g.setColor(Color.RED);
1112
1113     if (!av.getWrapAlignment())
1114     {
1115       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1116               0);
1117     }
1118     else
1119     {
1120       drawWrappedSelection(g, group, getWidth(), getHeight(),
1121               av.getRanges().getStartRes());
1122     }
1123
1124     g.dispose();
1125     return selectionImage;
1126   }
1127
1128   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1129           int startRes, int endRes, int startSeq, int endSeq, int offset)
1130   {
1131     if (!av.hasHiddenColumns())
1132     {
1133       drawSelectionGroupPart(g, group, startRes, endRes, startSeq, endSeq,
1134               offset);
1135     }
1136     else
1137     {
1138       // package into blocks of visible columns
1139       int screenY = 0;
1140       int blockStart = startRes;
1141       int blockEnd = endRes;
1142
1143       for (int[] region : av.getAlignment().getHiddenColumns()
1144               .getHiddenColumnsCopy())
1145       {
1146         int hideStart = region[0];
1147         int hideEnd = region[1];
1148
1149         if (hideStart <= blockStart)
1150         {
1151           blockStart += (hideEnd - hideStart) + 1;
1152           continue;
1153         }
1154
1155         blockEnd = hideStart - 1;
1156
1157         g.translate(screenY * charWidth, 0);
1158         drawSelectionGroupPart(g, group,
1159                 blockStart, blockEnd, startSeq, endSeq, offset);
1160
1161         g.translate(-screenY * charWidth, 0);
1162         screenY += blockEnd - blockStart + 1;
1163         blockStart = hideEnd + 1;
1164
1165         if (screenY > (endRes - startRes))
1166         {
1167           // already rendered last block
1168           break;
1169         }
1170       }
1171
1172       if (screenY <= (endRes - startRes))
1173       {
1174         // remaining visible region to render
1175         blockEnd = blockStart + (endRes - startRes) - screenY;
1176         g.translate(screenY * charWidth, 0);
1177         drawSelectionGroupPart(g, group,
1178                 blockStart, blockEnd, startSeq, endSeq, offset);
1179         
1180         g.translate(-screenY * charWidth, 0);
1181       }
1182     }
1183   }
1184
1185   /*
1186    * Draw the selection group as a separate image and overlay
1187    */
1188   private void drawSelectionGroupPart(Graphics2D g, SequenceGroup group,
1189           int startRes, int endRes, int startSeq, int endSeq,
1190           int verticalOffset)
1191   {
1192     int visWidth = (endRes - startRes + 1) * charWidth;
1193
1194     int oldY = -1;
1195     int i = 0;
1196     boolean inGroup = false;
1197     int top = -1;
1198     int bottom = -1;
1199
1200     int sx = -1;
1201     int sy = -1;
1202     int xwidth = -1;
1203
1204     for (i = startSeq; i <= endSeq; i++)
1205     {
1206       // position of start residue of group relative to startRes, in pixels
1207       sx = (group.getStartRes() - startRes) * charWidth;
1208
1209       // width of group in pixels
1210       xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1211               - 1;
1212
1213       sy = verticalOffset + (i - startSeq) * charHeight;
1214
1215       if (sx + xwidth < 0 || sx > visWidth)
1216       {
1217         continue;
1218       }
1219
1220       if ((sx <= (endRes - startRes) * charWidth)
1221               && group.getSequences(null)
1222                       .contains(av.getAlignment().getSequenceAt(i)))
1223       {
1224         if ((bottom == -1) && !group.getSequences(null)
1225                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1226         {
1227           bottom = sy + charHeight;
1228         }
1229
1230         if (!inGroup)
1231         {
1232           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1233                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1234           {
1235             top = sy;
1236           }
1237
1238           oldY = sy;
1239           inGroup = true;
1240         }
1241       }
1242       else
1243       {
1244         if (inGroup)
1245         {
1246           // if start position is visible, draw vertical line to left of
1247           // group
1248           if (sx >= 0 && sx < visWidth)
1249           {
1250             g.drawLine(sx, oldY, sx, sy);
1251           }
1252
1253           // if end position is visible, draw vertical line to right of
1254           // group
1255           if (sx + xwidth < visWidth)
1256           {
1257             g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1258           }
1259
1260           if (sx < 0)
1261           {
1262             xwidth += sx;
1263             sx = 0;
1264           }
1265
1266           if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1267           {
1268             xwidth = (endRes - startRes + 1) * charWidth - sx;
1269           }
1270           
1271           // draw horizontal line at top of group
1272           if (top != -1)
1273           {
1274             g.drawLine(sx, top, sx + xwidth, top);
1275             top = -1;
1276           }
1277
1278           // draw horizontal line at bottom of group
1279           if (bottom != -1)
1280           {
1281             g.drawLine(sx, bottom, sx + xwidth, bottom);
1282             bottom = -1;
1283           }
1284
1285           inGroup = false;
1286         }
1287       }
1288     }
1289
1290     if (inGroup)
1291     {
1292       sy = verticalOffset + ((i - startSeq) * charHeight);
1293       if (sx >= 0 && sx < visWidth)
1294       {
1295         g.drawLine(sx, oldY, sx, sy);
1296       }
1297
1298       if (sx + xwidth < visWidth)
1299       {
1300         g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1301       }
1302
1303       if (sx < 0)
1304       {
1305         xwidth += sx;
1306         sx = 0;
1307       }
1308
1309       if (sx + xwidth > visWidth)
1310       {
1311         xwidth = visWidth;
1312       }
1313       else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1314       {
1315         xwidth = (endRes - startRes + 1) * charWidth;
1316       }
1317
1318       if (top != -1)
1319       {
1320         g.drawLine(sx, top, sx + xwidth, top);
1321         top = -1;
1322       }
1323
1324       if (bottom != -1)
1325       {
1326         g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
1327         bottom = -1;
1328       }
1329
1330       inGroup = false;
1331     }
1332   }
1333
1334   /**
1335    * DOCUMENT ME!
1336    * 
1337    * @param results
1338    *          DOCUMENT ME!
1339    */
1340   public void highlightSearchResults(SearchResultsI results)
1341   {
1342     img = null;
1343
1344     av.setSearchResults(results);
1345
1346     repaint();
1347   }
1348
1349   @Override
1350   public void propertyChange(PropertyChangeEvent evt)
1351   {
1352     String eventName = evt.getPropertyName();
1353
1354     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1355     {
1356       paintSeqGroup();
1357     }
1358     else if (av.getWrapAlignment())
1359     {
1360       if (eventName.equals(ViewportRanges.STARTRES))
1361       {
1362         repaint();
1363       }
1364     }
1365     else
1366     {
1367       int scrollX = 0;
1368       if (eventName.equals(ViewportRanges.STARTRES))
1369       {
1370         // Make sure we're not trying to draw a panel
1371         // larger than the visible window
1372         ViewportRanges vpRanges = av.getRanges();
1373         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1374         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1375         if (scrollX > range)
1376         {
1377           scrollX = range;
1378         }
1379         else if (scrollX < -range)
1380         {
1381           scrollX = -range;
1382         }
1383       }
1384
1385       // Both scrolling and resizing change viewport ranges: scrolling changes
1386       // both start and end points, but resize only changes end values.
1387       // Here we only want to fastpaint on a scroll, with resize using a normal
1388       // paint, so scroll events are identified as changes to the horizontal or
1389       // vertical start value.
1390       if (eventName.equals(ViewportRanges.STARTRES))
1391       {
1392         // scroll - startres and endres both change
1393         fastPaint(scrollX, 0);
1394       }
1395       else if (eventName.equals(ViewportRanges.STARTSEQ))
1396       {
1397         // scroll
1398         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1399       }
1400     }
1401   }
1402 }