JAL-1858 handle hidden columns when computing region to redraw
[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.SearchResults;
26 import jalview.datamodel.SearchResultsI;
27 import jalview.datamodel.SequenceGroup;
28 import jalview.datamodel.SequenceI;
29 import jalview.renderer.ScaleRenderer;
30 import jalview.renderer.ScaleRenderer.ScaleMark;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
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   int imgWidth;
65
66   int imgHeight;
67
68   AlignViewport av;
69
70   boolean fastPaint = false;
71
72   int LABEL_WEST;
73
74   int LABEL_EAST;
75
76   int cursorX = 0;
77
78   int cursorY = 0;
79
80   /**
81    * Creates a new SeqCanvas object.
82    * 
83    * @param av
84    *          DOCUMENT ME!
85    */
86   public SeqCanvas(AlignmentPanel ap)
87   {
88     this.av = ap.av;
89     updateViewport();
90     fr = new FeatureRenderer(ap);
91     sr = new SequenceRenderer(av);
92     setLayout(new BorderLayout());
93     PaintRefresher.Register(this, av.getSequenceSetId());
94     setBackground(Color.white);
95
96     av.getRanges().addPropertyChangeListener(this);
97   }
98
99   public SequenceRenderer getSequenceRenderer()
100   {
101     return sr;
102   }
103
104   public FeatureRenderer getFeatureRenderer()
105   {
106     return fr;
107   }
108
109   int charHeight = 0, charWidth = 0;
110
111   private void updateViewport()
112   {
113     charHeight = av.getCharHeight();
114     charWidth = av.getCharWidth();
115   }
116
117   /**
118    * DOCUMENT ME!
119    * 
120    * @param g
121    *          DOCUMENT ME!
122    * @param startx
123    *          DOCUMENT ME!
124    * @param endx
125    *          DOCUMENT ME!
126    * @param ypos
127    *          DOCUMENT ME!
128    */
129   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
130   {
131     updateViewport();
132     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
133             endx))
134     {
135       int mpos = mark.column; // (i - startx - 1)
136       if (mpos < 0)
137       {
138         continue;
139       }
140       String mstring = mark.text;
141
142       if (mark.major)
143       {
144         if (mstring != null)
145         {
146           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
147         }
148         g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
149                 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
150                 ypos - 2);
151       }
152     }
153   }
154
155   /**
156    * DOCUMENT ME!
157    * 
158    * @param g
159    *          DOCUMENT ME!
160    * @param startx
161    *          DOCUMENT ME!
162    * @param endx
163    *          DOCUMENT ME!
164    * @param ypos
165    *          DOCUMENT ME!
166    */
167   void drawWestScale(Graphics g, int startx, int endx, int ypos)
168   {
169     FontMetrics fm = getFontMetrics(av.getFont());
170     ypos += charHeight;
171
172     if (av.hasHiddenColumns())
173     {
174       startx = av.getAlignment().getHiddenColumns()
175               .adjustForHiddenColumns(startx);
176       endx = av.getAlignment().getHiddenColumns()
177               .adjustForHiddenColumns(endx);
178     }
179
180     int maxwidth = av.getAlignment().getWidth();
181     if (av.hasHiddenColumns())
182     {
183       maxwidth = av.getAlignment().getHiddenColumns()
184               .findColumnPosition(maxwidth) - 1;
185     }
186
187     // WEST SCALE
188     for (int i = 0; i < av.getAlignment().getHeight(); i++)
189     {
190       SequenceI seq = av.getAlignment().getSequenceAt(i);
191       int index = startx;
192       int value = -1;
193
194       while (index < endx)
195       {
196         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
197         {
198           index++;
199
200           continue;
201         }
202
203         value = av.getAlignment().getSequenceAt(i).findPosition(index);
204
205         break;
206       }
207
208       if (value != -1)
209       {
210         int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
211                 - charWidth / 2;
212         g.drawString(value + "", x, (ypos + (i * charHeight))
213                 - (charHeight / 5));
214       }
215     }
216   }
217
218   /**
219    * DOCUMENT ME!
220    * 
221    * @param g
222    *          DOCUMENT ME!
223    * @param startx
224    *          DOCUMENT ME!
225    * @param endx
226    *          DOCUMENT ME!
227    * @param ypos
228    *          DOCUMENT ME!
229    */
230   void drawEastScale(Graphics g, int startx, int endx, int ypos)
231   {
232     ypos += charHeight;
233
234     if (av.hasHiddenColumns())
235     {
236       endx = av.getAlignment().getHiddenColumns()
237               .adjustForHiddenColumns(endx);
238     }
239
240     SequenceI seq;
241     // EAST SCALE
242     for (int i = 0; i < av.getAlignment().getHeight(); i++)
243     {
244       seq = av.getAlignment().getSequenceAt(i);
245       int index = endx;
246       int value = -1;
247
248       while (index > startx)
249       {
250         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
251         {
252           index--;
253
254           continue;
255         }
256
257         value = seq.findPosition(index);
258
259         break;
260       }
261
262       if (value != -1)
263       {
264         g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
265                 - (charHeight / 5));
266       }
267     }
268   }
269
270   boolean fastpainting = false;
271
272   /**
273    * need to make this thread safe move alignment rendering in response to
274    * slider adjustment
275    * 
276    * @param horizontal
277    *          shift along
278    * @param vertical
279    *          shift up or down in repaint
280    */
281   public void fastPaint(int horizontal, int vertical)
282   {
283     if (fastpainting || gg == null)
284     {
285       return;
286     }
287     fastpainting = true;
288     fastPaint = true;
289     updateViewport();
290     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
291             imgHeight, -horizontal * charWidth, -vertical * charHeight);
292
293     ViewportRanges ranges = av.getRanges();
294     int startRes = ranges.getStartRes();
295     int endRes = ranges.getEndRes();
296     int startSeq = ranges.getStartSeq();
297     int endSeq = ranges.getEndSeq();
298     int transX = 0;
299     int transY = 0;
300
301     if (horizontal > 0) // scrollbar pulled right, image to the left
302     {
303       transX = (endRes - startRes - horizontal) * charWidth;
304       startRes = endRes - horizontal;
305     }
306     else if (horizontal < 0)
307     {
308       endRes = startRes - horizontal;
309     }
310     else if (vertical > 0) // scroll down
311     {
312       startSeq = endSeq - vertical;
313
314       if (startSeq < ranges.getStartSeq())
315       { // ie scrolling too fast, more than a page at a time
316         startSeq = ranges.getStartSeq();
317       }
318       else
319       {
320         transY = imgHeight - ((vertical + 1) * charHeight);
321       }
322     }
323     else if (vertical < 0)
324     {
325       endSeq = startSeq - vertical;
326
327       if (endSeq > ranges.getEndSeq())
328       {
329         endSeq = ranges.getEndSeq();
330       }
331     }
332
333     gg.translate(transX, transY);
334     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
335     gg.translate(-transX, -transY);
336
337     repaint();
338     fastpainting = false;
339   }
340
341   @Override
342   public void paintComponent(Graphics g)
343   {
344     updateViewport();
345     BufferedImage lcimg = img; // take reference since other threads may null
346     // img and call later.
347     super.paintComponent(g);
348
349     if (lcimg != null
350             && (fastPaint
351                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
352                     .getClipBounds().height)))
353     {
354       g.drawImage(lcimg, 0, 0, this);
355       fastPaint = false;
356       return;
357     }
358
359     // this draws the whole of the alignment
360     imgWidth = getWidth();
361     imgHeight = getHeight();
362
363     imgWidth -= (imgWidth % charWidth);
364     imgHeight -= (imgHeight % charHeight);
365
366     if ((imgWidth < 1) || (imgHeight < 1))
367     {
368       return;
369     }
370
371     if (lcimg == null || imgWidth != lcimg.getWidth()
372             || imgHeight != lcimg.getHeight())
373     {
374       try
375       {
376         lcimg = img = new BufferedImage(imgWidth, imgHeight,
377                 BufferedImage.TYPE_INT_RGB);
378         gg = (Graphics2D) img.getGraphics();
379         gg.setFont(av.getFont());
380       } catch (OutOfMemoryError er)
381       {
382         System.gc();
383         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
384         new OOMWarning("Creating alignment image for display", er);
385
386         return;
387       }
388     }
389
390     if (av.antiAlias)
391     {
392       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
393               RenderingHints.VALUE_ANTIALIAS_ON);
394     }
395
396     gg.setColor(Color.white);
397     gg.fillRect(0, 0, imgWidth, imgHeight);
398
399     ViewportRanges ranges = av.getRanges();
400     if (av.getWrapAlignment())
401     {
402       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
403     }
404     else
405     {
406       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
407               ranges.getStartSeq(), ranges.getEndSeq(), 0);
408     }
409
410     g.drawImage(lcimg, 0, 0, this);
411
412   }
413
414   /**
415    * DOCUMENT ME!
416    * 
417    * @param cwidth
418    *          DOCUMENT ME!
419    * 
420    * @return DOCUMENT ME!
421    */
422   public int getWrappedCanvasWidth(int cwidth)
423   {
424     FontMetrics fm = getFontMetrics(av.getFont());
425
426     LABEL_EAST = 0;
427     LABEL_WEST = 0;
428
429     if (av.getScaleRightWrapped())
430     {
431       LABEL_EAST = fm.stringWidth(getMask());
432     }
433
434     if (av.getScaleLeftWrapped())
435     {
436       LABEL_WEST = fm.stringWidth(getMask());
437     }
438
439     return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
440   }
441
442   /**
443    * Generates a string of zeroes.
444    * 
445    * @return String
446    */
447   String getMask()
448   {
449     String mask = "00";
450     int maxWidth = 0;
451     int tmp;
452     for (int i = 0; i < av.getAlignment().getHeight(); i++)
453     {
454       tmp = av.getAlignment().getSequenceAt(i).getEnd();
455       if (tmp > maxWidth)
456       {
457         maxWidth = tmp;
458       }
459     }
460
461     for (int i = maxWidth; i > 0; i /= 10)
462     {
463       mask += "0";
464     }
465     return mask;
466   }
467
468   /**
469    * DOCUMENT ME!
470    * 
471    * @param g
472    *          DOCUMENT ME!
473    * @param canvasWidth
474    *          DOCUMENT ME!
475    * @param canvasHeight
476    *          DOCUMENT ME!
477    * @param startRes
478    *          DOCUMENT ME!
479    */
480   public void drawWrappedPanel(Graphics g, int canvasWidth,
481           int canvasHeight, int startRes)
482   {
483     updateViewport();
484     AlignmentI al = av.getAlignment();
485
486     FontMetrics fm = getFontMetrics(av.getFont());
487
488     if (av.getScaleRightWrapped())
489     {
490       LABEL_EAST = fm.stringWidth(getMask());
491     }
492
493     if (av.getScaleLeftWrapped())
494     {
495       LABEL_WEST = fm.stringWidth(getMask());
496     }
497
498     int hgap = charHeight;
499     if (av.getScaleAboveWrapped())
500     {
501       hgap += charHeight;
502     }
503
504     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
505     int cHeight = av.getAlignment().getHeight() * charHeight;
506
507     av.setWrappedWidth(cWidth);
508
509     av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
510
511     int endx;
512     int ypos = hgap;
513     int maxwidth = av.getAlignment().getWidth() - 1;
514
515     if (av.hasHiddenColumns())
516     {
517       maxwidth = av.getAlignment().getHiddenColumns()
518               .findColumnPosition(maxwidth) - 1;
519     }
520
521     while ((ypos <= canvasHeight) && (startRes < maxwidth))
522     {
523       endx = startRes + cWidth - 1;
524
525       if (endx > maxwidth)
526       {
527         endx = maxwidth;
528       }
529
530       g.setFont(av.getFont());
531       g.setColor(Color.black);
532
533       if (av.getScaleLeftWrapped())
534       {
535         drawWestScale(g, startRes, endx, ypos);
536       }
537
538       if (av.getScaleRightWrapped())
539       {
540         g.translate(canvasWidth - LABEL_EAST, 0);
541         drawEastScale(g, startRes, endx, ypos);
542         g.translate(-(canvasWidth - LABEL_EAST), 0);
543       }
544
545       g.translate(LABEL_WEST, 0);
546
547       if (av.getScaleAboveWrapped())
548       {
549         drawNorthScale(g, startRes, endx, ypos);
550       }
551
552       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
553       {
554         g.setColor(Color.blue);
555         int res;
556         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
557         for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
558         {
559           res = hidden.findHiddenRegionPosition(i) - startRes;
560
561           if (res < 0 || res > endx - startRes)
562           {
563             continue;
564           }
565
566           gg.fillPolygon(
567                   new int[] { res * charWidth - charHeight / 4,
568                       res * charWidth + charHeight / 4, res * charWidth },
569                   new int[] { ypos - (charHeight / 2),
570                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
571                   3);
572
573         }
574       }
575
576       // When printing we have an extra clipped region,
577       // the Printable page which we need to account for here
578       Shape clip = g.getClip();
579
580       if (clip == null)
581       {
582         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
583       }
584       else
585       {
586         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
587                 (int) clip.getBounds().getHeight());
588       }
589
590       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
591
592       if (av.isShowAnnotation())
593       {
594         g.translate(0, cHeight + ypos + 3);
595         if (annotations == null)
596         {
597           annotations = new AnnotationPanel(av);
598         }
599
600         annotations.renderer.drawComponent(annotations, av, g, -1,
601                 startRes, endx + 1);
602         g.translate(0, -cHeight - ypos - 3);
603       }
604       g.setClip(clip);
605       g.translate(-LABEL_WEST, 0);
606
607       ypos += cHeight + getAnnotationHeight() + hgap;
608
609       startRes += cWidth;
610     }
611   }
612
613   AnnotationPanel annotations;
614
615   int getAnnotationHeight()
616   {
617     if (!av.isShowAnnotation())
618     {
619       return 0;
620     }
621
622     if (annotations == null)
623     {
624       annotations = new AnnotationPanel(av);
625     }
626
627     return annotations.adjustPanelHeight();
628   }
629
630   /**
631    * Draws the visible region of the alignment on the graphics context. If there
632    * are hidden column markers in the visible region, then each sub-region
633    * between the markers is drawn separately, followed by the hidden column
634    * marker.
635    * 
636    * @param g1
637    * @param startRes
638    *          offset of the first column in the visible region (0..)
639    * @param endRes
640    *          offset of the last column in the visible region (0..)
641    * @param startSeq
642    *          offset of the first sequence in the visible region (0..)
643    * @param endSeq
644    *          offset of the last sequence in the visible region (0..)
645    * @param yOffset
646    *          vertical offset at which to draw (for wrapped alignments)
647    */
648   public void drawPanel(Graphics g1, final int startRes, final int endRes,
649           final int startSeq, final int endSeq, final int yOffset)
650   {
651     updateViewport();
652     if (!av.hasHiddenColumns())
653     {
654       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
655     }
656     else
657     {
658       List<int[]> regions = av.getAlignment().getHiddenColumns()
659               .getHiddenRegions();
660
661       int screenY = 0;
662       final int screenYMax = endRes - startRes;
663       int blockStart = startRes;
664       int blockEnd = endRes;
665
666       for (int[] region : regions)
667       {
668         int hideStart = region[0];
669         int hideEnd = region[1];
670
671         if (hideStart <= blockStart)
672         {
673           blockStart += (hideEnd - hideStart) + 1;
674           continue;
675         }
676
677         /*
678          * draw up to just before the next hidden region, or the end of
679          * the visible region, whichever comes first
680          */
681         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
682                 - screenY);
683
684         g1.translate(screenY * charWidth, 0);
685
686         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
687
688         /*
689          * draw the downline of the hidden column marker (ScalePanel draws the
690          * triangle on top) if we reached it
691          */
692         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
693         {
694           g1.setColor(Color.blue);
695
696           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
697                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
698                   (endSeq - startSeq + 1) * charHeight + yOffset);
699         }
700
701         g1.translate(-screenY * charWidth, 0);
702         screenY += blockEnd - blockStart + 1;
703         blockStart = hideEnd + 1;
704
705         if (screenY > screenYMax)
706         {
707           // already rendered last block
708           return;
709         }
710       }
711
712       if (screenY <= screenYMax)
713       {
714         // remaining visible region to render
715         blockEnd = blockStart + screenYMax - screenY;
716         g1.translate(screenY * charWidth, 0);
717         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
718
719         g1.translate(-screenY * charWidth, 0);
720       }
721     }
722
723   }
724
725   /**
726    * Draws a region of the visible alignment
727    * 
728    * @param g1
729    * @param startRes
730    *          offset of the first column in the visible region (0..)
731    * @param endRes
732    *          offset of the last column in the visible region (0..)
733    * @param startSeq
734    *          offset of the first sequence in the visible region (0..)
735    * @param endSeq
736    *          offset of the last sequence in the visible region (0..)
737    * @param yOffset
738    *          vertical offset at which to draw (for wrapped alignments)
739    */
740   private void draw(Graphics g, int startRes, int endRes, int startSeq,
741           int endSeq, int offset)
742   {
743     g.setFont(av.getFont());
744     sr.prepare(g, av.isRenderGaps());
745
746     SequenceI nextSeq;
747
748     // / First draw the sequences
749     // ///////////////////////////
750     for (int i = startSeq; i <= endSeq; i++)
751     {
752       nextSeq = av.getAlignment().getSequenceAt(i);
753       if (nextSeq == null)
754       {
755         // occasionally, a race condition occurs such that the alignment row is
756         // empty
757         continue;
758       }
759       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
760               startRes, endRes, offset + ((i - startSeq) * charHeight));
761
762       if (av.isShowSequenceFeatures())
763       {
764         fr.drawSequence(g, nextSeq, startRes, endRes, offset
765                 + ((i - startSeq) * charHeight), false);
766       }
767
768       /*
769        * highlight search Results once sequence has been drawn
770        */
771       if (av.hasSearchResults())
772       {
773         SearchResultsI searchResults = av.getSearchResults();
774         int[] visibleResults = searchResults.getResults(nextSeq,
775                 startRes, endRes);
776         if (visibleResults != null)
777         {
778           for (int r = 0; r < visibleResults.length; r += 2)
779           {
780             sr.drawHighlightedText(nextSeq, visibleResults[r],
781                     visibleResults[r + 1], (visibleResults[r] - startRes)
782                             * charWidth, offset
783                             + ((i - startSeq) * charHeight));
784           }
785         }
786       }
787
788       if (av.cursorMode && cursorY == i && cursorX >= startRes
789               && cursorX <= endRes)
790       {
791         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
792                 offset + ((i - startSeq) * charHeight));
793       }
794     }
795
796     if (av.getSelectionGroup() != null
797             || av.getAlignment().getGroups().size() > 0)
798     {
799       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
800     }
801
802   }
803
804   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
805           int startSeq, int endSeq, int offset)
806   {
807     Graphics2D g = (Graphics2D) g1;
808     //
809     // ///////////////////////////////////
810     // Now outline any areas if necessary
811     // ///////////////////////////////////
812     SequenceGroup group = av.getSelectionGroup();
813
814     int sx = -1;
815     int sy = -1;
816     int ex = -1;
817     int groupIndex = -1;
818     int visWidth = (endRes - startRes + 1) * charWidth;
819
820     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
821     {
822       group = av.getAlignment().getGroups().get(0);
823       groupIndex = 0;
824     }
825
826     if (group != null)
827     {
828       do
829       {
830         int oldY = -1;
831         int i = 0;
832         boolean inGroup = false;
833         int top = -1;
834         int bottom = -1;
835
836         for (i = startSeq; i <= endSeq; i++)
837         {
838           sx = (group.getStartRes() - startRes) * charWidth;
839           sy = offset + ((i - startSeq) * charHeight);
840           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
841
842           if (sx + ex < 0 || sx > visWidth)
843           {
844             continue;
845           }
846
847           if ((sx <= (endRes - startRes) * charWidth)
848                   && group.getSequences(null).contains(
849                           av.getAlignment().getSequenceAt(i)))
850           {
851             if ((bottom == -1)
852                     && !group.getSequences(null).contains(
853                             av.getAlignment().getSequenceAt(i + 1)))
854             {
855               bottom = sy + charHeight;
856             }
857
858             if (!inGroup)
859             {
860               if (((top == -1) && (i == 0))
861                       || !group.getSequences(null).contains(
862                               av.getAlignment().getSequenceAt(i - 1)))
863               {
864                 top = sy;
865               }
866
867               oldY = sy;
868               inGroup = true;
869
870               if (group == av.getSelectionGroup())
871               {
872                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
873                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
874                         0f));
875                 g.setColor(Color.RED);
876               }
877               else
878               {
879                 g.setStroke(new BasicStroke());
880                 g.setColor(group.getOutlineColour());
881               }
882             }
883           }
884           else
885           {
886             if (inGroup)
887             {
888               if (sx >= 0 && sx < visWidth)
889               {
890                 g.drawLine(sx, oldY, sx, sy);
891               }
892
893               if (sx + ex < visWidth)
894               {
895                 g.drawLine(sx + ex, oldY, sx + ex, sy);
896               }
897
898               if (sx < 0)
899               {
900                 ex += sx;
901                 sx = 0;
902               }
903
904               if (sx + ex > visWidth)
905               {
906                 ex = visWidth;
907               }
908
909               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
910               {
911                 ex = (endRes - startRes + 1) * charWidth;
912               }
913
914               if (top != -1)
915               {
916                 g.drawLine(sx, top, sx + ex, top);
917                 top = -1;
918               }
919
920               if (bottom != -1)
921               {
922                 g.drawLine(sx, bottom, sx + ex, bottom);
923                 bottom = -1;
924               }
925
926               inGroup = false;
927             }
928           }
929         }
930
931         if (inGroup)
932         {
933           sy = offset + ((i - startSeq) * charHeight);
934           if (sx >= 0 && sx < visWidth)
935           {
936             g.drawLine(sx, oldY, sx, sy);
937           }
938
939           if (sx + ex < visWidth)
940           {
941             g.drawLine(sx + ex, oldY, sx + ex, sy);
942           }
943
944           if (sx < 0)
945           {
946             ex += sx;
947             sx = 0;
948           }
949
950           if (sx + ex > visWidth)
951           {
952             ex = visWidth;
953           }
954           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
955           {
956             ex = (endRes - startRes + 1) * charWidth;
957           }
958
959           if (top != -1)
960           {
961             g.drawLine(sx, top, sx + ex, top);
962             top = -1;
963           }
964
965           if (bottom != -1)
966           {
967             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
968             bottom = -1;
969           }
970
971           inGroup = false;
972         }
973
974         groupIndex++;
975
976         g.setStroke(new BasicStroke());
977
978         if (groupIndex >= av.getAlignment().getGroups().size())
979         {
980           break;
981         }
982
983         group = av.getAlignment().getGroups().get(groupIndex);
984
985       } while (groupIndex < av.getAlignment().getGroups().size());
986
987     }
988
989   }
990
991   /**
992    * Highlights search results in the visible region by rendering as white text
993    * on a black background. Any previous highlighting is removed.
994    * 
995    * @param results
996    */
997   public void highlightSearchResults(SearchResultsI results)
998   {
999     updateViewport();
1000     fastpainting = true;
1001     fastPaint = true;
1002
1003     /*
1004      * to avoid redrawing the whole visible region, we instead
1005      * calculate the minimal redraw to remove previous highlights
1006      * and add new ones;
1007      * first make a temporary SearchResults that includes 
1008      * both the new and any existing search results
1009      */
1010     SearchResultsI combined = results;
1011     SearchResultsI previous = av.getSearchResults();
1012     if (previous != null)
1013     {
1014       combined = new SearchResults();
1015       combined.addSearchResults(results);
1016       combined.addSearchResults(previous);
1017     }
1018
1019     /*
1020      * calculate the minimal rectangle to redraw that 
1021      * includes both new and existing search results
1022      */
1023     int firstSeq = Integer.MAX_VALUE;
1024     int lastSeq = -1;
1025     int firstCol = Integer.MAX_VALUE;
1026     int lastCol = -1;
1027     boolean matchFound = false;
1028
1029     ViewportRanges ranges = av.getRanges();
1030     int firstVisibleColumn = ranges.getStartRes();
1031     int lastVisibleColumn = ranges.getEndRes();
1032     if (av.hasHiddenColumns())
1033     {
1034       firstVisibleColumn = av.getAlignment().getHiddenColumns()
1035               .adjustForHiddenColumns(firstVisibleColumn);
1036       lastVisibleColumn = av.getAlignment().getHiddenColumns()
1037               .adjustForHiddenColumns(lastVisibleColumn);
1038     }
1039
1040     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1041             .getEndSeq(); seqNo++)
1042     {
1043       SequenceI seq = av.getAlignment().getSequenceAt(seqNo);
1044
1045       int[] visibleResults = combined.getResults(seq, firstVisibleColumn,
1046               lastVisibleColumn);
1047       if (visibleResults != null)
1048       {
1049         for (int i = 0; i < visibleResults.length - 1; i += 2)
1050         {
1051           int firstMatchedColumn = visibleResults[i];
1052           int lastMatchedColumn = visibleResults[i + 1];
1053           if (firstMatchedColumn <= lastVisibleColumn
1054                   && lastMatchedColumn >= firstVisibleColumn)
1055           {
1056             /*
1057              * found a search results match in the visible region - 
1058              * remember the first and last sequence matched, and the first
1059              * and last visible columns in the matched positions
1060              */
1061             matchFound = true;
1062             firstSeq = Math.min(firstSeq, seqNo);
1063             lastSeq = Math.max(lastSeq, seqNo);
1064             firstMatchedColumn = Math.max(firstMatchedColumn,
1065                     firstVisibleColumn);
1066             lastMatchedColumn = Math.min(lastMatchedColumn,
1067                     lastVisibleColumn);
1068             firstCol = Math.min(firstCol, firstMatchedColumn);
1069             lastCol = Math.max(lastCol, lastMatchedColumn);
1070           }
1071         }
1072       }
1073     }
1074
1075     av.setSearchResults(results);
1076
1077     if (matchFound)
1078     {
1079       if (av.hasHiddenColumns())
1080       {
1081         firstCol = av.getAlignment().getHiddenColumns()
1082                 .findColumnPosition(firstCol);
1083         lastCol = av.getAlignment().getHiddenColumns()
1084                 .findColumnPosition(lastCol);
1085       }
1086       int transX = (firstCol - firstVisibleColumn) * av.getCharWidth();
1087       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1088       gg.translate(transX, transY);
1089       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1090       gg.translate(-transX, -transY);
1091
1092       repaint();
1093     }
1094
1095     fastpainting = false; // todo in finally block?
1096   }
1097
1098   @Override
1099   public void propertyChange(PropertyChangeEvent evt)
1100   {
1101     if (!av.getWrapAlignment())
1102     {
1103       if (evt.getPropertyName().equals("startres")
1104               || evt.getPropertyName().equals("endres"))
1105       {
1106         // Make sure we're not trying to draw a panel
1107         // larger than the visible window
1108         ViewportRanges vpRanges = av.getRanges();
1109         int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1110         if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
1111         {
1112           scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
1113         }
1114         else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
1115         {
1116           scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
1117         }
1118         fastPaint(scrollX, 0);
1119       }
1120       else if (evt.getPropertyName().equals("startseq")
1121               || evt.getPropertyName().equals("endseq"))
1122       {
1123         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1124       }
1125     }
1126   }
1127 }