Merge branch 'develop' into features/JAL-2446NCList
[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.BasicStroke;
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.awt.RenderingHints;
40 import java.awt.Shape;
41 import java.awt.image.BufferedImage;
42 import java.beans.PropertyChangeEvent;
43 import java.util.List;
44
45 import javax.swing.JComponent;
46
47 /**
48  * DOCUMENT ME!
49  * 
50  * @author $author$
51  * @version $Revision$
52  */
53 public class SeqCanvas extends JComponent implements ViewportListenerI
54 {
55   private static String ZEROS = "0000000000";
56
57   final FeatureRenderer fr;
58
59   final SequenceRenderer sr;
60
61   BufferedImage img;
62
63   Graphics2D gg;
64
65   int imgWidth;
66
67   int imgHeight;
68
69   AlignViewport av;
70
71   boolean fastPaint = false;
72
73   int labelWidthWest;
74
75   int labelWidthEast;
76
77   int cursorX = 0;
78
79   int cursorY = 0;
80
81   /**
82    * Creates a new SeqCanvas object.
83    * 
84    * @param av
85    *          DOCUMENT ME!
86    */
87   public SeqCanvas(AlignmentPanel ap)
88   {
89     this.av = ap.av;
90     updateViewport();
91     fr = new FeatureRenderer(ap);
92     sr = new SequenceRenderer(av);
93     setLayout(new BorderLayout());
94     PaintRefresher.Register(this, av.getSequenceSetId());
95     setBackground(Color.white);
96
97     av.getRanges().addPropertyChangeListener(this);
98   }
99
100   public SequenceRenderer getSequenceRenderer()
101   {
102     return sr;
103   }
104
105   public FeatureRenderer getFeatureRenderer()
106   {
107     return fr;
108   }
109
110   int charHeight = 0, charWidth = 0;
111
112   private void updateViewport()
113   {
114     charHeight = av.getCharHeight();
115     charWidth = av.getCharWidth();
116   }
117
118   /**
119    * DOCUMENT ME!
120    * 
121    * @param g
122    *          DOCUMENT ME!
123    * @param startx
124    *          DOCUMENT ME!
125    * @param endx
126    *          DOCUMENT ME!
127    * @param ypos
128    *          DOCUMENT ME!
129    */
130   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
131   {
132     updateViewport();
133     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
134             endx))
135     {
136       int mpos = mark.column; // (i - startx - 1)
137       if (mpos < 0)
138       {
139         continue;
140       }
141       String mstring = mark.text;
142
143       if (mark.major)
144       {
145         if (mstring != null)
146         {
147           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
148         }
149         g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
150                 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
151                 ypos - 2);
152       }
153     }
154   }
155
156   /**
157    * DOCUMENT ME!
158    * 
159    * @param g
160    *          DOCUMENT ME!
161    * @param startx
162    *          DOCUMENT ME!
163    * @param endx
164    *          DOCUMENT ME!
165    * @param ypos
166    *          DOCUMENT ME!
167    */
168   void drawWestScale(Graphics g, int startx, int endx, int ypos)
169   {
170     FontMetrics fm = getFontMetrics(av.getFont());
171     ypos += charHeight;
172
173     if (av.hasHiddenColumns())
174     {
175       startx = av.getAlignment().getHiddenColumns()
176               .adjustForHiddenColumns(startx);
177       endx = av.getAlignment().getHiddenColumns()
178               .adjustForHiddenColumns(endx);
179     }
180
181     int maxwidth = av.getAlignment().getWidth();
182     if (av.hasHiddenColumns())
183     {
184       maxwidth = av.getAlignment().getHiddenColumns()
185               .findColumnPosition(maxwidth) - 1;
186     }
187
188     // WEST SCALE
189     for (int i = 0; i < av.getAlignment().getHeight(); i++)
190     {
191       SequenceI seq = av.getAlignment().getSequenceAt(i);
192       int index = startx;
193       int value = -1;
194
195       while (index < endx)
196       {
197         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
198         {
199           index++;
200
201           continue;
202         }
203
204         value = av.getAlignment().getSequenceAt(i).findPosition(index);
205
206         break;
207       }
208
209       if (value != -1)
210       {
211         int x = labelWidthWest - fm.stringWidth(String.valueOf(value))
212                 - charWidth / 2;
213         g.drawString(value + "", x, (ypos + (i * charHeight))
214                 - (charHeight / 5));
215       }
216     }
217   }
218
219   /**
220    * DOCUMENT ME!
221    * 
222    * @param g
223    *          DOCUMENT ME!
224    * @param startx
225    *          DOCUMENT ME!
226    * @param endx
227    *          DOCUMENT ME!
228    * @param ypos
229    *          DOCUMENT ME!
230    */
231   void drawEastScale(Graphics g, int startx, int endx, int ypos)
232   {
233     ypos += charHeight;
234
235     if (av.hasHiddenColumns())
236     {
237       endx = av.getAlignment().getHiddenColumns()
238               .adjustForHiddenColumns(endx);
239     }
240
241     SequenceI seq;
242     // EAST SCALE
243     for (int i = 0; i < av.getAlignment().getHeight(); i++)
244     {
245       seq = av.getAlignment().getSequenceAt(i);
246       int index = endx;
247       int value = -1;
248
249       while (index > startx)
250       {
251         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
252         {
253           index--;
254
255           continue;
256         }
257
258         value = seq.findPosition(index);
259
260         break;
261       }
262
263       if (value != -1)
264       {
265         g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
266                 - (charHeight / 5));
267       }
268     }
269   }
270
271   boolean fastpainting = false;
272
273   /**
274    * need to make this thread safe move alignment rendering in response to
275    * slider adjustment
276    * 
277    * @param horizontal
278    *          shift along
279    * @param vertical
280    *          shift up or down in repaint
281    */
282   public void fastPaint(int horizontal, int vertical)
283   {
284     if (fastpainting || gg == null)
285     {
286       return;
287     }
288     fastpainting = true;
289     fastPaint = true;
290     updateViewport();
291
292     ViewportRanges ranges = av.getRanges();
293     int startRes = ranges.getStartRes();
294     int endRes = ranges.getEndRes();
295     int startSeq = ranges.getStartSeq();
296     int endSeq = ranges.getEndSeq();
297     int transX = 0;
298     int transY = 0;
299
300     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
301             imgHeight, -horizontal * charWidth, -vertical * charHeight);
302
303     if (horizontal > 0) // scrollbar pulled right, image to the left
304     {
305       transX = (endRes - startRes - horizontal) * charWidth;
306       startRes = endRes - horizontal;
307     }
308     else if (horizontal < 0)
309     {
310       endRes = startRes - horizontal;
311     }
312     else if (vertical > 0) // scroll down
313     {
314       startSeq = endSeq - vertical;
315
316       if (startSeq < ranges.getStartSeq())
317       { // ie scrolling too fast, more than a page at a time
318         startSeq = ranges.getStartSeq();
319       }
320       else
321       {
322         transY = imgHeight - ((vertical + 1) * charHeight);
323       }
324     }
325     else if (vertical < 0)
326     {
327       endSeq = startSeq - vertical;
328
329       if (endSeq > ranges.getEndSeq())
330       {
331         endSeq = ranges.getEndSeq();
332       }
333     }
334
335     gg.translate(transX, transY);
336     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
337     gg.translate(-transX, -transY);
338
339     repaint();
340     fastpainting = false;
341   }
342
343   @Override
344   public void paintComponent(Graphics g)
345   {
346     updateViewport();
347     BufferedImage lcimg = img; // take reference since other threads may null
348     // img and call later.
349     super.paintComponent(g);
350
351     if (lcimg != null
352             && (fastPaint
353                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
354                     .getClipBounds().height)))
355     {
356       g.drawImage(lcimg, 0, 0, this);
357       fastPaint = false;
358       return;
359     }
360
361     // this draws the whole of the alignment
362     imgWidth = getWidth();
363     imgHeight = getHeight();
364
365     imgWidth -= (imgWidth % charWidth);
366     imgHeight -= (imgHeight % charHeight);
367
368     if ((imgWidth < 1) || (imgHeight < 1))
369     {
370       return;
371     }
372
373     if (lcimg == null || imgWidth != lcimg.getWidth()
374             || imgHeight != lcimg.getHeight())
375     {
376       try
377       {
378         lcimg = img = new BufferedImage(imgWidth, imgHeight,
379                 BufferedImage.TYPE_INT_RGB);
380         gg = (Graphics2D) img.getGraphics();
381         gg.setFont(av.getFont());
382       } catch (OutOfMemoryError er)
383       {
384         System.gc();
385         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
386         new OOMWarning("Creating alignment image for display", er);
387
388         return;
389       }
390     }
391
392     if (av.antiAlias)
393     {
394       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
395               RenderingHints.VALUE_ANTIALIAS_ON);
396     }
397
398     gg.setColor(Color.white);
399     gg.fillRect(0, 0, imgWidth, imgHeight);
400
401     ViewportRanges ranges = av.getRanges();
402     if (av.getWrapAlignment())
403     {
404       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
405     }
406     else
407     {
408       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
409               ranges.getStartSeq(), ranges.getEndSeq(), 0);
410     }
411
412     g.drawImage(lcimg, 0, 0, this);
413
414   }
415
416   /**
417    * DOCUMENT ME!
418    * 
419    * @param cwidth
420    *          DOCUMENT ME!
421    * 
422    * @return DOCUMENT ME!
423    */
424   public int getWrappedCanvasWidth(int cwidth)
425   {
426     FontMetrics fm = getFontMetrics(av.getFont());
427
428     labelWidthEast = 0;
429     labelWidthWest = 0;
430
431     if (av.getScaleRightWrapped())
432     {
433       labelWidthEast = getLabelWidth(fm);
434     }
435
436     if (av.getScaleLeftWrapped())
437     {
438       labelWidthWest = getLabelWidth(fm);
439     }
440
441     return (cwidth - labelWidthEast - labelWidthWest) / charWidth;
442   }
443
444   /**
445    * Returns a pixel width suitable for showing the largest sequence coordinate
446    * (end position) in the alignment. Returns 2 plus the number of decimal
447    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
448    * 
449    * @param fm
450    * @return
451    */
452   protected int getLabelWidth(FontMetrics fm)
453   {
454     int maxWidth = 0;
455     for (int i = 0; i < av.getAlignment().getHeight(); i++)
456     {
457       maxWidth = Math.max(maxWidth, av.getAlignment().getSequenceAt(i)
458               .getEnd());
459     }
460
461     int length = 2;
462     for (int i = maxWidth; i > 0; i /= 10)
463     {
464       length++;
465     }
466
467     return fm.stringWidth(ZEROS.substring(0, length));
468   }
469
470   /**
471    * DOCUMENT ME!
472    * 
473    * @param g
474    *          DOCUMENT ME!
475    * @param canvasWidth
476    *          DOCUMENT ME!
477    * @param canvasHeight
478    *          DOCUMENT ME!
479    * @param startRes
480    *          DOCUMENT ME!
481    */
482   public void drawWrappedPanel(Graphics g, int canvasWidth,
483           int canvasHeight, int startRes)
484   {
485     updateViewport();
486     AlignmentI al = av.getAlignment();
487
488     int labelWidth = 0;
489     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
490     {
491       FontMetrics fm = getFontMetrics(av.getFont());
492       labelWidth = getLabelWidth(fm);
493     }
494
495     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
496     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
497
498     int hgap = charHeight;
499     if (av.getScaleAboveWrapped())
500     {
501       hgap += charHeight;
502     }
503
504     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / 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     int annotationHeight = getAnnotationHeight();
522
523     while ((ypos <= canvasHeight) && (startRes < maxwidth))
524     {
525       endx = startRes + cWidth - 1;
526
527       if (endx > maxwidth)
528       {
529         endx = maxwidth;
530       }
531
532       g.setFont(av.getFont());
533       g.setColor(Color.black);
534
535       if (av.getScaleLeftWrapped())
536       {
537         drawWestScale(g, startRes, endx, ypos);
538       }
539
540       if (av.getScaleRightWrapped())
541       {
542         g.translate(canvasWidth - labelWidthEast, 0);
543         drawEastScale(g, startRes, endx, ypos);
544         g.translate(-(canvasWidth - labelWidthEast), 0);
545       }
546
547       g.translate(labelWidthWest, 0);
548
549       if (av.getScaleAboveWrapped())
550       {
551         drawNorthScale(g, startRes, endx, ypos);
552       }
553
554       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
555       {
556         g.setColor(Color.blue);
557         int res;
558         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
559         List<Integer> positions = hidden.findHiddenRegionPositions();
560         for (int pos : positions)
561         {
562           res = pos - startRes;
563
564           if (res < 0 || res > endx - startRes)
565           {
566             continue;
567           }
568
569           gg.fillPolygon(
570                   new int[] { res * charWidth - charHeight / 4,
571                       res * charWidth + charHeight / 4, res * charWidth },
572                   new int[] { ypos - (charHeight / 2),
573                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
574                   3);
575
576         }
577       }
578
579       // When printing we have an extra clipped region,
580       // the Printable page which we need to account for here
581       Shape clip = g.getClip();
582
583       if (clip == null)
584       {
585         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
586       }
587       else
588       {
589         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
590                 (int) clip.getBounds().getHeight());
591       }
592
593       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
594
595       if (av.isShowAnnotation())
596       {
597         g.translate(0, cHeight + ypos + 3);
598         if (annotations == null)
599         {
600           annotations = new AnnotationPanel(av);
601         }
602
603         annotations.renderer.drawComponent(annotations, av, g, -1,
604                 startRes, endx + 1);
605         g.translate(0, -cHeight - ypos - 3);
606       }
607       g.setClip(clip);
608       g.translate(-labelWidthWest, 0);
609
610       ypos += cHeight + annotationHeight + hgap;
611
612       startRes += cWidth;
613     }
614   }
615
616   AnnotationPanel annotations;
617
618   int getAnnotationHeight()
619   {
620     if (!av.isShowAnnotation())
621     {
622       return 0;
623     }
624
625     if (annotations == null)
626     {
627       annotations = new AnnotationPanel(av);
628     }
629
630     return annotations.adjustPanelHeight();
631   }
632
633   /**
634    * Draws the visible region of the alignment on the graphics context. If there
635    * are hidden column markers in the visible region, then each sub-region
636    * between the markers is drawn separately, followed by the hidden column
637    * marker.
638    * 
639    * @param g1
640    * @param startRes
641    *          offset of the first column in the visible region (0..)
642    * @param endRes
643    *          offset of the last column in the visible region (0..)
644    * @param startSeq
645    *          offset of the first sequence in the visible region (0..)
646    * @param endSeq
647    *          offset of the last sequence in the visible region (0..)
648    * @param yOffset
649    *          vertical offset at which to draw (for wrapped alignments)
650    */
651   public void drawPanel(Graphics g1, final int startRes, final int endRes,
652           final int startSeq, final int endSeq, final int yOffset)
653   {
654     updateViewport();
655     if (!av.hasHiddenColumns())
656     {
657       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
658     }
659     else
660     {
661       int screenY = 0;
662       final int screenYMax = endRes - startRes;
663       int blockStart = startRes;
664       int blockEnd = endRes;
665
666       for (int[] region : av.getAlignment().getHiddenColumns()
667               .getHiddenColumnsCopy())
668       {
669         int hideStart = region[0];
670         int hideEnd = region[1];
671
672         if (hideStart <= blockStart)
673         {
674           blockStart += (hideEnd - hideStart) + 1;
675           continue;
676         }
677
678         /*
679          * draw up to just before the next hidden region, or the end of
680          * the visible region, whichever comes first
681          */
682         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
683                 - screenY);
684
685         g1.translate(screenY * charWidth, 0);
686
687         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
688
689         /*
690          * draw the downline of the hidden column marker (ScalePanel draws the
691          * triangle on top) if we reached it
692          */
693         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
694         {
695           g1.setColor(Color.blue);
696
697           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
698                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
699                   (endSeq - startSeq + 1) * charHeight + yOffset);
700         }
701
702         g1.translate(-screenY * charWidth, 0);
703         screenY += blockEnd - blockStart + 1;
704         blockStart = hideEnd + 1;
705
706         if (screenY > screenYMax)
707         {
708           // already rendered last block
709           return;
710         }
711       }
712
713       if (screenY <= screenYMax)
714       {
715         // remaining visible region to render
716         blockEnd = blockStart + screenYMax - screenY;
717         g1.translate(screenY * charWidth, 0);
718         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
719
720         g1.translate(-screenY * charWidth, 0);
721       }
722     }
723
724   }
725
726   /**
727    * Draws a region of the visible alignment
728    * 
729    * @param g1
730    * @param startRes
731    *          offset of the first column in the visible region (0..)
732    * @param endRes
733    *          offset of the last column in the visible region (0..)
734    * @param startSeq
735    *          offset of the first sequence in the visible region (0..)
736    * @param endSeq
737    *          offset of the last sequence in the visible region (0..)
738    * @param yOffset
739    *          vertical offset at which to draw (for wrapped alignments)
740    */
741   private void draw(Graphics g, int startRes, int endRes, int startSeq,
742           int endSeq, int offset)
743   {
744     g.setFont(av.getFont());
745     sr.prepare(g, av.isRenderGaps());
746
747     SequenceI nextSeq;
748
749     // / First draw the sequences
750     // ///////////////////////////
751     for (int i = startSeq; i <= endSeq; i++)
752     {
753       nextSeq = av.getAlignment().getSequenceAt(i);
754       if (nextSeq == null)
755       {
756         // occasionally, a race condition occurs such that the alignment row is
757         // empty
758         continue;
759       }
760       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
761               startRes, endRes, offset + ((i - startSeq) * charHeight));
762
763       if (av.isShowSequenceFeatures())
764       {
765         fr.drawSequence(g, nextSeq, startRes, endRes, offset
766                 + ((i - startSeq) * charHeight), false);
767       }
768
769       /*
770        * highlight search Results once sequence has been drawn
771        */
772       if (av.hasSearchResults())
773       {
774         SearchResultsI searchResults = av.getSearchResults();
775         int[] visibleResults = searchResults.getResults(nextSeq,
776                 startRes, endRes);
777         if (visibleResults != null)
778         {
779           for (int r = 0; r < visibleResults.length; r += 2)
780           {
781             sr.drawHighlightedText(nextSeq, visibleResults[r],
782                     visibleResults[r + 1], (visibleResults[r] - startRes)
783                             * charWidth, offset
784                             + ((i - startSeq) * charHeight));
785           }
786         }
787       }
788
789       if (av.cursorMode && cursorY == i && cursorX >= startRes
790               && cursorX <= endRes)
791       {
792         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
793                 offset + ((i - startSeq) * charHeight));
794       }
795     }
796
797     if (av.getSelectionGroup() != null
798             || av.getAlignment().getGroups().size() > 0)
799     {
800       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
801     }
802
803   }
804
805   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
806           int startSeq, int endSeq, int offset)
807   {
808     Graphics2D g = (Graphics2D) g1;
809     //
810     // ///////////////////////////////////
811     // Now outline any areas if necessary
812     // ///////////////////////////////////
813     SequenceGroup group = av.getSelectionGroup();
814
815     int sx = -1;
816     int sy = -1;
817     int ex = -1;
818     int groupIndex = -1;
819     int visWidth = (endRes - startRes + 1) * charWidth;
820
821     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
822     {
823       group = av.getAlignment().getGroups().get(0);
824       groupIndex = 0;
825     }
826
827     if (group != null)
828     {
829       do
830       {
831         int oldY = -1;
832         int i = 0;
833         boolean inGroup = false;
834         int top = -1;
835         int bottom = -1;
836
837         for (i = startSeq; i <= endSeq; i++)
838         {
839           sx = (group.getStartRes() - startRes) * charWidth;
840           sy = offset + ((i - startSeq) * charHeight);
841           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
842
843           if (sx + ex < 0 || sx > visWidth)
844           {
845             continue;
846           }
847
848           if ((sx <= (endRes - startRes) * charWidth)
849                   && group.getSequences(null).contains(
850                           av.getAlignment().getSequenceAt(i)))
851           {
852             if ((bottom == -1)
853                     && !group.getSequences(null).contains(
854                             av.getAlignment().getSequenceAt(i + 1)))
855             {
856               bottom = sy + charHeight;
857             }
858
859             if (!inGroup)
860             {
861               if (((top == -1) && (i == 0))
862                       || !group.getSequences(null).contains(
863                               av.getAlignment().getSequenceAt(i - 1)))
864               {
865                 top = sy;
866               }
867
868               oldY = sy;
869               inGroup = true;
870
871               if (group == av.getSelectionGroup())
872               {
873                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
874                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
875                         0f));
876                 g.setColor(Color.RED);
877               }
878               else
879               {
880                 g.setStroke(new BasicStroke());
881                 g.setColor(group.getOutlineColour());
882               }
883             }
884           }
885           else
886           {
887             if (inGroup)
888             {
889               if (sx >= 0 && sx < visWidth)
890               {
891                 g.drawLine(sx, oldY, sx, sy);
892               }
893
894               if (sx + ex < visWidth)
895               {
896                 g.drawLine(sx + ex, oldY, sx + ex, sy);
897               }
898
899               if (sx < 0)
900               {
901                 ex += sx;
902                 sx = 0;
903               }
904
905               if (sx + ex > visWidth)
906               {
907                 ex = visWidth;
908               }
909
910               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
911               {
912                 ex = (endRes - startRes + 1) * charWidth;
913               }
914
915               if (top != -1)
916               {
917                 g.drawLine(sx, top, sx + ex, top);
918                 top = -1;
919               }
920
921               if (bottom != -1)
922               {
923                 g.drawLine(sx, bottom, sx + ex, bottom);
924                 bottom = -1;
925               }
926
927               inGroup = false;
928             }
929           }
930         }
931
932         if (inGroup)
933         {
934           sy = offset + ((i - startSeq) * charHeight);
935           if (sx >= 0 && sx < visWidth)
936           {
937             g.drawLine(sx, oldY, sx, sy);
938           }
939
940           if (sx + ex < visWidth)
941           {
942             g.drawLine(sx + ex, oldY, sx + ex, sy);
943           }
944
945           if (sx < 0)
946           {
947             ex += sx;
948             sx = 0;
949           }
950
951           if (sx + ex > visWidth)
952           {
953             ex = visWidth;
954           }
955           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
956           {
957             ex = (endRes - startRes + 1) * charWidth;
958           }
959
960           if (top != -1)
961           {
962             g.drawLine(sx, top, sx + ex, top);
963             top = -1;
964           }
965
966           if (bottom != -1)
967           {
968             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
969             bottom = -1;
970           }
971
972           inGroup = false;
973         }
974
975         groupIndex++;
976
977         g.setStroke(new BasicStroke());
978
979         if (groupIndex >= av.getAlignment().getGroups().size())
980         {
981           break;
982         }
983
984         group = av.getAlignment().getGroups().get(groupIndex);
985
986       } while (groupIndex < av.getAlignment().getGroups().size());
987
988     }
989
990   }
991
992   /**
993    * Highlights search results in the visible region by rendering as white text
994    * on a black background. Any previous highlighting is removed. Answers true
995    * if any highlight was left on the visible alignment (so status bar should be
996    * set to match), else false.
997    * 
998    * @param results
999    * @return
1000    */
1001   public boolean highlightSearchResults(SearchResultsI results)
1002   {
1003     updateViewport();
1004
1005     /*
1006      * for now, don't attempt fastpaint if wrapped format
1007      */
1008     boolean wrapped = av.getWrapAlignment();
1009     if (wrapped)
1010     {
1011       // drawWrappedMappedPositions(results);
1012       // img = null;
1013       // av.setSearchResults(results);
1014       // repaint();
1015       // return;
1016     }
1017     
1018     fastpainting = true;
1019     fastPaint = true;
1020
1021     try
1022     {
1023       /*
1024        * to avoid redrawing the whole visible region, we instead
1025        * redraw just the minimal regions to remove previous highlights
1026        * and add new ones
1027        */
1028       SearchResultsI previous = av.getSearchResults();
1029       av.setSearchResults(results);
1030       boolean redrawn = false;
1031       boolean drawn = false;
1032       if (wrapped)
1033       {
1034         redrawn = drawWrappedMappedPositions(previous);
1035         drawn = drawWrappedMappedPositions(results);
1036         redrawn |= drawn;
1037       }
1038       else
1039       {
1040         redrawn = drawMappedPositions(previous);
1041         drawn = drawMappedPositions(results);
1042         redrawn |= drawn;
1043       }
1044
1045       /*
1046        * if highlights were either removed or added, repaint
1047        */
1048       if (redrawn)
1049       {
1050         repaint();
1051       }
1052
1053       /*
1054        * return true only if highlights were added
1055        */
1056       return drawn;
1057
1058     } finally
1059     {
1060       fastpainting = false;
1061     }
1062
1063   }
1064
1065   /**
1066    * Redraws the minimal rectangle in the visible region (if any) that includes
1067    * mapped positions of the given search results. Whether or not positions are
1068    * highlighted depends on the SearchResults set on the Viewport. This allows
1069    * this method to be called to either clear or set highlighting. Answers true
1070    * if any positions were drawn (in which case a repaint is still required),
1071    * else false.
1072    * 
1073    * @param results
1074    * @return
1075    */
1076   protected boolean drawMappedPositions(SearchResultsI results)
1077   {
1078     if (results == null)
1079     {
1080       return false;
1081     }
1082
1083     /*
1084      * calculate the minimal rectangle to redraw that 
1085      * includes both new and existing search results
1086      */
1087     int firstSeq = Integer.MAX_VALUE;
1088     int lastSeq = -1;
1089     int firstCol = Integer.MAX_VALUE;
1090     int lastCol = -1;
1091     boolean matchFound = false;
1092
1093     ViewportRanges ranges = av.getRanges();
1094     int firstVisibleColumn = ranges.getStartRes();
1095     int lastVisibleColumn = ranges.getEndRes();
1096     AlignmentI alignment = av.getAlignment();
1097     if (av.hasHiddenColumns())
1098     {
1099       firstVisibleColumn = alignment.getHiddenColumns()
1100               .adjustForHiddenColumns(firstVisibleColumn);
1101       lastVisibleColumn = alignment.getHiddenColumns()
1102               .adjustForHiddenColumns(lastVisibleColumn);
1103     }
1104
1105     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1106             .getEndSeq(); seqNo++)
1107     {
1108       SequenceI seq = alignment.getSequenceAt(seqNo);
1109
1110       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1111               lastVisibleColumn);
1112       if (visibleResults != null)
1113       {
1114         for (int i = 0; i < visibleResults.length - 1; i += 2)
1115         {
1116           int firstMatchedColumn = visibleResults[i];
1117           int lastMatchedColumn = visibleResults[i + 1];
1118           if (firstMatchedColumn <= lastVisibleColumn
1119                   && lastMatchedColumn >= firstVisibleColumn)
1120           {
1121             /*
1122              * found a search results match in the visible region - 
1123              * remember the first and last sequence matched, and the first
1124              * and last visible columns in the matched positions
1125              */
1126             matchFound = true;
1127             firstSeq = Math.min(firstSeq, seqNo);
1128             lastSeq = Math.max(lastSeq, seqNo);
1129             firstMatchedColumn = Math.max(firstMatchedColumn,
1130                     firstVisibleColumn);
1131             lastMatchedColumn = Math.min(lastMatchedColumn,
1132                     lastVisibleColumn);
1133             firstCol = Math.min(firstCol, firstMatchedColumn);
1134             lastCol = Math.max(lastCol, lastMatchedColumn);
1135           }
1136         }
1137       }
1138     }
1139
1140     if (matchFound)
1141     {
1142       if (av.hasHiddenColumns())
1143       {
1144         firstCol = alignment.getHiddenColumns()
1145                 .findColumnPosition(firstCol);
1146         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1147       }
1148       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1149       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1150       gg.translate(transX, transY);
1151       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1152       gg.translate(-transX, -transY);
1153     }
1154
1155     return matchFound;
1156   }
1157
1158   @Override
1159   public void propertyChange(PropertyChangeEvent evt)
1160   {
1161     String eventName = evt.getPropertyName();
1162
1163     if (!av.getWrapAlignment())
1164     {
1165       int scrollX = 0;
1166       if (eventName.equals(ViewportRanges.STARTRES))
1167       {
1168         // Make sure we're not trying to draw a panel
1169         // larger than the visible window
1170         ViewportRanges vpRanges = av.getRanges();
1171         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1172         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1173         if (scrollX > range)
1174         {
1175           scrollX = range;
1176         }
1177         else if (scrollX < -range)
1178         {
1179           scrollX = -range;
1180         }
1181       }
1182
1183       // Both scrolling and resizing change viewport ranges: scrolling changes
1184       // both start and end points, but resize only changes end values.
1185       // Here we only want to fastpaint on a scroll, with resize using a normal
1186       // paint, so scroll events are identified as changes to the horizontal or
1187       // vertical start value.
1188       if (eventName.equals(ViewportRanges.STARTRES))
1189       {
1190         // scroll - startres and endres both change
1191         fastPaint(scrollX, 0);
1192       }
1193       else if (eventName.equals(ViewportRanges.STARTSEQ))
1194       {
1195         // scroll
1196         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1197       }
1198     }
1199   }
1200
1201   /**
1202    * Redraws any positions in the search results in the visible region. Any
1203    * highlights are drawn depending on the search results set on the Viewport,
1204    * not the results parameter. This allows this method to be called to either
1205    * clear highlighting (passing the previous search results), or set new
1206    * highlighting.
1207    * 
1208    * @param results
1209    * @return
1210    */
1211   protected boolean drawWrappedMappedPositions(SearchResultsI results)
1212   {
1213     if (results == null)
1214     {
1215       return false;
1216     }
1217   
1218     boolean matchFound = false;
1219   
1220     /*
1221      * Viewport ranges are set for the 'row' of the wrapped alignment
1222      * the cursor is in, not the whole visible region; really we want
1223      * the latter; +-6 a temporary fudge for codons wrapping across lines
1224      */
1225     ViewportRanges ranges = av.getRanges();
1226     int firstVisibleColumn = ranges.getStartRes() - 6;
1227     int lastVisibleColumn = ranges.getEndRes() + 6;
1228     AlignmentI alignment = av.getAlignment();
1229     if (av.hasHiddenColumns())
1230     {
1231       firstVisibleColumn = alignment.getHiddenColumns()
1232               .adjustForHiddenColumns(firstVisibleColumn);
1233       lastVisibleColumn = alignment.getHiddenColumns()
1234               .adjustForHiddenColumns(lastVisibleColumn);
1235     }
1236   
1237     /*
1238      * find width of alignment in residues, and height of alignment, 
1239      * so we can calculate where to render each matched position
1240      */
1241     int wrappedWidth = av.getWrappedWidth();
1242     int wrappedHeight = av.getAlignment().getHeight() * av.getCharHeight();
1243     int gapHeight = av.getCharHeight()
1244             * (av.getScaleAboveWrapped() ? 2 : 1);
1245     wrappedHeight += gapHeight;
1246     wrappedHeight += getAnnotationHeight();
1247
1248     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1249             .getEndSeq(); seqNo++)
1250     {
1251       SequenceI seq = alignment.getSequenceAt(seqNo);
1252
1253       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1254               lastVisibleColumn);
1255       if (visibleResults != null)
1256       {
1257         for (int i = 0; i < visibleResults.length - 1; i += 2)
1258         {
1259           int firstMatchedColumn = visibleResults[i];
1260           int lastMatchedColumn = visibleResults[i + 1];
1261           if (firstMatchedColumn <= lastVisibleColumn
1262                   && lastMatchedColumn >= firstVisibleColumn)
1263           {
1264             /*
1265              * found a search results match in the visible region
1266              */
1267             firstMatchedColumn = Math.max(firstMatchedColumn,
1268                     firstVisibleColumn);
1269             lastMatchedColumn = Math.min(lastMatchedColumn,
1270                     lastVisibleColumn);
1271
1272             /*
1273              * draw each mapped position separately (as contiguous positions may
1274              * wrap across lines)
1275              */
1276             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1277             {
1278               int displayColumn = mappedPos;
1279               if (av.hasHiddenColumns())
1280               {
1281                 displayColumn = alignment.getHiddenColumns()
1282                         .findColumnPosition(displayColumn);
1283               }
1284
1285               /*
1286                * transX: offset from left edge of canvas to residue position
1287                */
1288               int transX = labelWidthWest
1289                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1290                       * av.getCharWidth();
1291
1292               /*
1293                * transY: offset from top edge of canvas to residue position
1294                */
1295               int transY = gapHeight;
1296               transY += (displayColumn / wrappedWidth) * wrappedHeight;
1297               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1298
1299               /*
1300                * yOffset is from graphics origin to start of visible region
1301                */
1302               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1303               if (transY < getHeight())
1304               {
1305                 matchFound = true;
1306                 gg.translate(transX, transY);
1307                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1308                         yOffset);
1309                 gg.translate(-transX, -transY);
1310               }
1311             }
1312           }
1313         }
1314       }
1315     }
1316   
1317     return matchFound;
1318   }
1319 }