JAL-2665 partial fix for selection running over 2 wrapped panels
[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   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
831   // int x1, int x2, int y1, int y2, int startx, int starty,
832   private void draw(Graphics g, int startRes, int endRes, int startSeq,
833           int endSeq, int offset)
834   {
835     g.setFont(av.getFont());
836     sr.prepare(g, av.isRenderGaps());
837
838     SequenceI nextSeq;
839
840     // / First draw the sequences
841     // ///////////////////////////
842     for (int i = startSeq; i <= endSeq; i++)
843     {
844       nextSeq = av.getAlignment().getSequenceAt(i);
845       if (nextSeq == null)
846       {
847         // occasionally, a race condition occurs such that the alignment row is
848         // empty
849         continue;
850       }
851       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
852               startRes, endRes, offset + ((i - startSeq) * charHeight));
853
854       if (av.isShowSequenceFeatures())
855       {
856         fr.drawSequence(g, nextSeq, startRes, endRes, offset
857                 + ((i - startSeq) * charHeight), false);
858       }
859
860       // / Highlight search Results once all sequences have been drawn
861       // ////////////////////////////////////////////////////////
862       if (av.hasSearchResults())
863       {
864         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
865                 startRes, endRes);
866         if (visibleResults != null)
867         {
868           for (int r = 0; r < visibleResults.length; r += 2)
869           {
870             sr.drawHighlightedText(nextSeq, visibleResults[r],
871                     visibleResults[r + 1], (visibleResults[r] - startRes)
872                             * charWidth, offset
873                             + ((i - startSeq) * charHeight));
874           }
875         }
876       }
877
878       if (av.cursorMode && cursorY == i && cursorX >= startRes
879               && cursorX <= endRes)
880       {
881         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
882                 offset + ((i - startSeq) * charHeight));
883       }
884     }
885
886     if (av.getSelectionGroup() != null
887             || av.getAlignment().getGroups().size() > 0)
888     {
889       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
890     }
891
892   }
893
894   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
895           int startSeq, int endSeq, int offset)
896   {
897     Graphics2D g = (Graphics2D) g1;
898     //
899     // ///////////////////////////////////
900     // Now outline any areas if necessary
901     // ///////////////////////////////////
902
903     SequenceGroup group = null;
904
905     int sx = -1;
906     int sy = -1;
907     int ex = -1;
908     int groupIndex = -1;
909     int visWidth = (endRes - startRes + 1) * charWidth;
910
911     if (av.getAlignment().getGroups().size() > 0)
912     {
913       group = av.getAlignment().getGroups().get(0);
914       groupIndex = 0;
915     }
916
917     if (group != null)
918     {
919       do
920       {
921         int oldY = -1;
922         int i = 0;
923         boolean inGroup = false;
924         int top = -1;
925         int bottom = -1;
926
927         for (i = startSeq; i <= endSeq; i++)
928         {
929           // position of start residue of group relative to startRes, in pixels
930           sx = (group.getStartRes() - startRes) * charWidth;
931           sy = offset + ((i - startSeq) * charHeight);
932           // width of group in pixels
933           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
934
935           if (sx + ex < 0 || sx > visWidth)
936           {
937             continue;
938           }
939
940           if ((sx <= (endRes - startRes) * charWidth)
941                   && group.getSequences(null).contains(
942                           av.getAlignment().getSequenceAt(i)))
943           {
944             if ((bottom == -1)
945                     && !group.getSequences(null).contains(
946                             av.getAlignment().getSequenceAt(i + 1)))
947             {
948               bottom = sy + charHeight;
949             }
950
951             if (!inGroup)
952             {
953               if (((top == -1) && (i == 0))
954                       || !group.getSequences(null).contains(
955                               av.getAlignment().getSequenceAt(i - 1)))
956               {
957                 top = sy;
958               }
959
960               oldY = sy;
961               inGroup = true;
962
963               g.setStroke(new BasicStroke());
964               g.setColor(group.getOutlineColour());
965             }
966           }
967           else
968           {
969             if (inGroup)
970             {
971               // if start position is visible, draw vertical line to left of
972               // group
973               if (sx >= 0 && sx < visWidth)
974               {
975                 g.drawLine(sx, oldY, sx, sy);
976               }
977
978               // if end position is visible, draw vertical line to right of
979               // group
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
996               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
997               {
998                 ex = (endRes - startRes + 1) * charWidth;
999               }
1000
1001               // draw horizontal line at top of group
1002               if (top != -1)
1003               {
1004                 g.drawLine(sx, top, sx + ex, top);
1005                 top = -1;
1006               }
1007
1008               // draw horizontal line at bottom of group
1009               if (bottom != -1)
1010               {
1011                 g.drawLine(sx, bottom, sx + ex, bottom);
1012                 bottom = -1;
1013               }
1014
1015               inGroup = false;
1016             }
1017           }
1018         }
1019
1020         if (inGroup)
1021         {
1022           sy = offset + ((i - startSeq) * charHeight);
1023           if (sx >= 0 && sx < visWidth)
1024           {
1025             g.drawLine(sx, oldY, sx, sy);
1026           }
1027
1028           if (sx + ex < visWidth)
1029           {
1030             g.drawLine(sx + ex, oldY, sx + ex, sy);
1031           }
1032
1033           if (sx < 0)
1034           {
1035             ex += sx;
1036             sx = 0;
1037           }
1038
1039           if (sx + ex > visWidth)
1040           {
1041             ex = visWidth;
1042           }
1043           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1044           {
1045             ex = (endRes - startRes + 1) * charWidth;
1046           }
1047
1048           if (top != -1)
1049           {
1050             g.drawLine(sx, top, sx + ex, top);
1051             top = -1;
1052           }
1053
1054           if (bottom != -1)
1055           {
1056             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1057             bottom = -1;
1058           }
1059
1060           inGroup = false;
1061         }
1062
1063         groupIndex++;
1064
1065         g.setStroke(new BasicStroke());
1066
1067         if (groupIndex >= av.getAlignment().getGroups().size())
1068         {
1069           break;
1070         }
1071
1072         group = av.getAlignment().getGroups().get(groupIndex);
1073
1074       } while (groupIndex < av.getAlignment().getGroups().size());
1075
1076     }
1077
1078   }
1079
1080
1081   /*
1082    * Draw the selection group as a separate image and overlay
1083    */
1084   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1085           int startSeq, int endSeq)
1086   {
1087     // get a new image of the correct size
1088     BufferedImage selectionImage = setupImage();
1089
1090     if (selectionImage == null)
1091     {
1092       return null;
1093     }
1094
1095     SequenceGroup group = av.getSelectionGroup();
1096     if (group == null)
1097     {
1098       // nothing to draw
1099       return null;
1100     }
1101     
1102     /*if (!av.getWrapAlignment())
1103     {
1104       LABEL_EAST = 0;
1105       LABEL_WEST = 0;
1106     }*/
1107
1108     // set up drawing colour
1109     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1110     // g.translate(LABEL_WEST, 0);
1111     // set background to transparent
1112     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1113     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1114
1115     g.setComposite(AlphaComposite.Src);
1116     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1117             BasicStroke.JOIN_ROUND, 3f, new float[]
1118     { 5f, 3f }, 0f));
1119     g.setColor(Color.RED);
1120
1121     if (!av.getWrapAlignment())
1122     {
1123       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1124               0);
1125     }
1126     else
1127     {
1128       drawWrappedSelection(g, group, getWidth(), getHeight(),
1129               av.getRanges().getStartRes());
1130     }
1131
1132     g.dispose();
1133     return selectionImage;
1134   }
1135
1136   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1137           int startRes, int endRes, int startSeq, int endSeq, int offset)
1138   {
1139     if (!av.hasHiddenColumns())
1140     {
1141       drawSelectionGroupPart(g, group, startRes, endRes, startSeq, endSeq,
1142               offset);
1143     }
1144     else
1145     {
1146       // package into blocks of visible columns
1147       int screenY = 0;
1148       int blockStart = startRes;
1149       int blockEnd = endRes;
1150
1151       for (int[] region : av.getAlignment().getHiddenColumns()
1152               .getHiddenColumnsCopy())
1153       {
1154         int hideStart = region[0];
1155         int hideEnd = region[1];
1156
1157         if (hideStart <= blockStart)
1158         {
1159           blockStart += (hideEnd - hideStart) + 1;
1160           continue;
1161         }
1162
1163         blockEnd = hideStart - 1;
1164
1165         g.translate(screenY * charWidth, 0);
1166
1167         drawSelectionGroupPart(g, group,
1168                 blockStart, blockEnd, startSeq, endSeq, offset);
1169
1170         g.translate(-screenY * charWidth, 0);
1171         screenY += blockEnd - blockStart + 1;
1172         blockStart = hideEnd + 1;
1173
1174         if (screenY > (endRes - startRes))
1175         {
1176           // already rendered last block
1177           break;
1178         }
1179       }
1180
1181       if (screenY <= (endRes - startRes))
1182       {
1183         // remaining visible region to render
1184         blockEnd = blockStart + (endRes - startRes) - screenY;
1185         g.translate(screenY * charWidth, 0);
1186         drawSelectionGroupPart(g, group,
1187                 blockStart, blockEnd, startSeq, endSeq, offset);
1188         
1189         g.translate(-screenY * charWidth, 0);
1190       }
1191     }
1192     // g.translate(-LABEL_WEST, 0);
1193   }
1194
1195   /*
1196    * Draw the selection group as a separate image and overlay
1197    */
1198   private void drawSelectionGroupPart(Graphics2D g, SequenceGroup group,
1199           int startRes, int endRes, int startSeq, int endSeq,
1200           int verticalOffset)
1201   {
1202     // set up values in case the alignment is wrapped
1203     /*int verticalOffset = 0;
1204     int horizontalOffset = 0;
1205     if (av.getWrapAlignment())
1206     {
1207       int hgap = charHeight;
1208       if (av.getScaleAboveWrapped())
1209       {
1210         hgap += charHeight;
1211       }
1212       
1213       // get the start res of the group and work out the offsets for it in the wrapped alignment
1214       int groupstart = group.getStartRes();
1215       int cWidth = (getWidth() - LABEL_EAST - LABEL_WEST) / charWidth;
1216       
1217       // group is in which slice of alignment? res position / width in residues
1218       int slice = groupstart / cWidth;
1219       // vertical offset is increased by slice number * number of sequences * height of each sequence
1220       verticalOffset = slice * (av.getAlignment().getHeight() * charHeight
1221               + getAnnotationHeight() + hgap) + hgap;
1222     
1223       // horizontal offset is number of residues to subtract from group residue
1224       // position
1225       horizontalOffset = (slice * cWidth);
1226     }*/
1227
1228     int visWidth = (endRes - startRes + 1) * charWidth;
1229
1230     int oldY = -1;
1231     int i = 0;
1232     boolean inGroup = false;
1233     int top = -1;
1234     int bottom = -1;
1235
1236     int sx = -1;
1237     int sy = -1;
1238     int ex = -1;
1239
1240     // position of start residue of group relative to startRes, in pixels
1241     sx = (group.getStartRes() - startRes) * charWidth;
1242
1243     // width of group in pixels
1244     ex = (((group.getEndRes() + 1) - group.getStartRes())
1245             * charWidth) - 1;
1246
1247     for (i = startSeq; i <= endSeq; i++)
1248     {
1249       sy = verticalOffset + (i - startSeq) * charHeight;
1250
1251       if (sx + ex < 0 || sx > visWidth)
1252       {
1253         continue;
1254       }
1255
1256       if ((sx <= (endRes - startRes) * charWidth)
1257               && group.getSequences(null)
1258                       .contains(av.getAlignment().getSequenceAt(i)))
1259       {
1260         if ((bottom == -1) && !group.getSequences(null)
1261                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1262         {
1263           bottom = sy + charHeight;
1264         }
1265
1266         if (!inGroup)
1267         {
1268           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1269                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1270           {
1271             top = sy;
1272           }
1273
1274           oldY = sy;
1275           inGroup = true;
1276         }
1277       }
1278       else
1279       {
1280         if (inGroup)
1281         {
1282           // if start position is visible, draw vertical line to left of
1283           // group
1284           if (sx >= 0 && sx < visWidth)
1285           {
1286             g.drawLine(sx, oldY, sx, sy);
1287           }
1288
1289           // if end position is visible, draw vertical line to right of
1290           // group
1291           if (sx + ex < visWidth)
1292           {
1293             g.drawLine(sx + ex, oldY, sx + ex, sy);
1294           }
1295
1296           if (sx < 0)
1297           {
1298             ex += sx;
1299             sx = 0;
1300           }
1301
1302           if (sx + ex > visWidth)
1303           {
1304             ex = visWidth;
1305           }
1306
1307           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1308           {
1309             ex = (endRes - startRes + 1) * charWidth;
1310           }
1311
1312           // draw horizontal line at top of group
1313           if (top != -1)
1314           {
1315             g.drawLine(sx, top, sx + ex, top);
1316             top = -1;
1317           }
1318
1319           // draw horizontal line at bottom of group
1320           if (bottom != -1)
1321           {
1322             g.drawLine(sx, bottom, sx + ex, bottom);
1323             bottom = -1;
1324           }
1325
1326           inGroup = false;
1327         }
1328       }
1329     }
1330
1331     if (inGroup)
1332     {
1333       sy = verticalOffset + ((i - startSeq) * charHeight);
1334       if (sx >= 0 && sx < visWidth)
1335       {
1336         g.drawLine(sx, oldY, sx, sy);
1337       }
1338
1339       if (sx + ex < visWidth)
1340       {
1341         g.drawLine(sx + ex, oldY, sx + ex, sy);
1342       }
1343
1344       if (sx < 0)
1345       {
1346         ex += sx;
1347         sx = 0;
1348       }
1349
1350       if (sx + ex > visWidth)
1351       {
1352         ex = visWidth;
1353       }
1354       else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1355       {
1356         ex = (endRes - startRes + 1) * charWidth;
1357       }
1358
1359       if (top != -1)
1360       {
1361         g.drawLine(sx, top, sx + ex, top);
1362         top = -1;
1363       }
1364
1365       if (bottom != -1)
1366       {
1367         g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1368         bottom = -1;
1369       }
1370
1371       inGroup = false;
1372     }
1373   }
1374
1375   /**
1376    * DOCUMENT ME!
1377    * 
1378    * @param results
1379    *          DOCUMENT ME!
1380    */
1381   public void highlightSearchResults(SearchResultsI results)
1382   {
1383     img = null;
1384
1385     av.setSearchResults(results);
1386
1387     repaint();
1388   }
1389
1390   @Override
1391   public void propertyChange(PropertyChangeEvent evt)
1392   {
1393     String eventName = evt.getPropertyName();
1394
1395     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1396     {
1397       paintSeqGroup();
1398     }
1399     else if (av.getWrapAlignment())
1400     {
1401       if (eventName.equals(ViewportRanges.STARTRES))
1402       {
1403         repaint();
1404       }
1405     }
1406     else
1407     {
1408       int scrollX = 0;
1409       if (eventName.equals(ViewportRanges.STARTRES))
1410       {
1411         // Make sure we're not trying to draw a panel
1412         // larger than the visible window
1413         ViewportRanges vpRanges = av.getRanges();
1414         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1415         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1416         if (scrollX > range)
1417         {
1418           scrollX = range;
1419         }
1420         else if (scrollX < -range)
1421         {
1422           scrollX = -range;
1423         }
1424       }
1425
1426       // Both scrolling and resizing change viewport ranges: scrolling changes
1427       // both start and end points, but resize only changes end values.
1428       // Here we only want to fastpaint on a scroll, with resize using a normal
1429       // paint, so scroll events are identified as changes to the horizontal or
1430       // vertical start value.
1431       if (eventName.equals(ViewportRanges.STARTRES))
1432       {
1433         // scroll - startres and endres both change
1434         fastPaint(scrollX, 0);
1435       }
1436       else if (eventName.equals(ViewportRanges.STARTSEQ))
1437       {
1438         // scroll
1439         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1440       }
1441     }
1442   }
1443 }