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     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
292             imgHeight, -horizontal * charWidth, -vertical * charHeight);
293
294     ViewportRanges ranges = av.getRanges();
295     int startRes = ranges.getStartRes();
296     int endRes = ranges.getEndRes();
297     int startSeq = ranges.getStartSeq();
298     int endSeq = ranges.getEndSeq();
299     int transX = 0;
300     int transY = 0;
301
302     if (horizontal > 0) // scrollbar pulled right, image to the left
303     {
304       transX = (endRes - startRes - horizontal) * charWidth;
305       startRes = endRes - horizontal;
306     }
307     else if (horizontal < 0)
308     {
309       endRes = startRes - horizontal;
310     }
311     else if (vertical > 0) // scroll down
312     {
313       startSeq = endSeq - vertical;
314
315       if (startSeq < ranges.getStartSeq())
316       { // ie scrolling too fast, more than a page at a time
317         startSeq = ranges.getStartSeq();
318       }
319       else
320       {
321         transY = imgHeight - ((vertical + 1) * charHeight);
322       }
323     }
324     else if (vertical < 0)
325     {
326       endSeq = startSeq - vertical;
327
328       if (endSeq > ranges.getEndSeq())
329       {
330         endSeq = ranges.getEndSeq();
331       }
332     }
333
334     gg.translate(transX, transY);
335     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
336     gg.translate(-transX, -transY);
337
338     repaint();
339     fastpainting = false;
340   }
341
342   @Override
343   public void paintComponent(Graphics g)
344   {
345     updateViewport();
346     BufferedImage lcimg = img; // take reference since other threads may null
347     // img and call later.
348     super.paintComponent(g);
349
350     if (lcimg != null
351             && (fastPaint
352                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
353                     .getClipBounds().height)))
354     {
355       g.drawImage(lcimg, 0, 0, this);
356       fastPaint = false;
357       return;
358     }
359
360     // this draws the whole of the alignment
361     imgWidth = getWidth();
362     imgHeight = getHeight();
363
364     imgWidth -= (imgWidth % charWidth);
365     imgHeight -= (imgHeight % charHeight);
366
367     if ((imgWidth < 1) || (imgHeight < 1))
368     {
369       return;
370     }
371
372     if (lcimg == null || imgWidth != lcimg.getWidth()
373             || imgHeight != lcimg.getHeight())
374     {
375       try
376       {
377         lcimg = img = new BufferedImage(imgWidth, imgHeight,
378                 BufferedImage.TYPE_INT_RGB);
379         gg = (Graphics2D) img.getGraphics();
380         gg.setFont(av.getFont());
381       } catch (OutOfMemoryError er)
382       {
383         System.gc();
384         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
385         new OOMWarning("Creating alignment image for display", er);
386
387         return;
388       }
389     }
390
391     if (av.antiAlias)
392     {
393       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
394               RenderingHints.VALUE_ANTIALIAS_ON);
395     }
396
397     gg.setColor(Color.white);
398     gg.fillRect(0, 0, imgWidth, imgHeight);
399
400     ViewportRanges ranges = av.getRanges();
401     if (av.getWrapAlignment())
402     {
403       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
404     }
405     else
406     {
407       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
408               ranges.getStartSeq(), ranges.getEndSeq(), 0);
409     }
410
411     g.drawImage(lcimg, 0, 0, this);
412
413   }
414
415   /**
416    * DOCUMENT ME!
417    * 
418    * @param cwidth
419    *          DOCUMENT ME!
420    * 
421    * @return DOCUMENT ME!
422    */
423   public int getWrappedCanvasWidth(int cwidth)
424   {
425     FontMetrics fm = getFontMetrics(av.getFont());
426
427     labelWidthEast = 0;
428     labelWidthWest = 0;
429
430     if (av.getScaleRightWrapped())
431     {
432       labelWidthEast = getLabelWidth(fm);
433     }
434
435     if (av.getScaleLeftWrapped())
436     {
437       labelWidthWest = getLabelWidth(fm);
438     }
439
440     return (cwidth - labelWidthEast - labelWidthWest) / charWidth;
441   }
442
443   /**
444    * Returns a pixel width suitable for showing the largest sequence coordinate
445    * (end position) in the alignment. Returns 2 plus the number of decimal
446    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
447    * 
448    * @param fm
449    * @return
450    */
451   protected int getLabelWidth(FontMetrics fm)
452   {
453     int maxWidth = 0;
454     for (int i = 0; i < av.getAlignment().getHeight(); i++)
455     {
456       maxWidth = Math.max(maxWidth, av.getAlignment().getSequenceAt(i)
457               .getEnd());
458     }
459
460     int length = 2;
461     for (int i = maxWidth; i > 0; i /= 10)
462     {
463       length++;
464     }
465
466     return fm.stringWidth(ZEROS.substring(0, length));
467   }
468
469   /**
470    * DOCUMENT ME!
471    * 
472    * @param g
473    *          DOCUMENT ME!
474    * @param canvasWidth
475    *          DOCUMENT ME!
476    * @param canvasHeight
477    *          DOCUMENT ME!
478    * @param startRes
479    *          DOCUMENT ME!
480    */
481   public void drawWrappedPanel(Graphics g, int canvasWidth,
482           int canvasHeight, int startRes)
483   {
484     updateViewport();
485     AlignmentI al = av.getAlignment();
486
487     int labelWidth = 0;
488     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
489     {
490       FontMetrics fm = getFontMetrics(av.getFont());
491       labelWidth = getLabelWidth(fm);
492     }
493
494     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
495     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
496
497     int hgap = charHeight;
498     if (av.getScaleAboveWrapped())
499     {
500       hgap += charHeight;
501     }
502
503     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
504     int cHeight = av.getAlignment().getHeight() * charHeight;
505
506     av.setWrappedWidth(cWidth);
507
508     av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
509
510     int endx;
511     int ypos = hgap;
512     int maxwidth = av.getAlignment().getWidth() - 1;
513
514     if (av.hasHiddenColumns())
515     {
516       maxwidth = av.getAlignment().getHiddenColumns()
517               .findColumnPosition(maxwidth) - 1;
518     }
519
520     int annotationHeight = getAnnotationHeight();
521
522     while ((ypos <= canvasHeight) && (startRes < maxwidth))
523     {
524       endx = startRes + cWidth - 1;
525
526       if (endx > maxwidth)
527       {
528         endx = maxwidth;
529       }
530
531       g.setFont(av.getFont());
532       g.setColor(Color.black);
533
534       if (av.getScaleLeftWrapped())
535       {
536         drawWestScale(g, startRes, endx, ypos);
537       }
538
539       if (av.getScaleRightWrapped())
540       {
541         g.translate(canvasWidth - labelWidthEast, 0);
542         drawEastScale(g, startRes, endx, ypos);
543         g.translate(-(canvasWidth - labelWidthEast), 0);
544       }
545
546       g.translate(labelWidthWest, 0);
547
548       if (av.getScaleAboveWrapped())
549       {
550         drawNorthScale(g, startRes, endx, ypos);
551       }
552
553       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
554       {
555         g.setColor(Color.blue);
556         int res;
557         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
558         List<Integer> positions = hidden.findHiddenRegionPositions();
559         for (int pos : positions)
560         {
561           res = pos - startRes;
562
563           if (res < 0 || res > endx - startRes)
564           {
565             continue;
566           }
567
568           gg.fillPolygon(
569                   new int[] { res * charWidth - charHeight / 4,
570                       res * charWidth + charHeight / 4, res * charWidth },
571                   new int[] { ypos - (charHeight / 2),
572                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
573                   3);
574
575         }
576       }
577
578       // When printing we have an extra clipped region,
579       // the Printable page which we need to account for here
580       Shape clip = g.getClip();
581
582       if (clip == null)
583       {
584         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
585       }
586       else
587       {
588         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
589                 (int) clip.getBounds().getHeight());
590       }
591
592       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
593
594       if (av.isShowAnnotation())
595       {
596         g.translate(0, cHeight + ypos + 3);
597         if (annotations == null)
598         {
599           annotations = new AnnotationPanel(av);
600         }
601
602         annotations.renderer.drawComponent(annotations, av, g, -1,
603                 startRes, endx + 1);
604         g.translate(0, -cHeight - ypos - 3);
605       }
606       g.setClip(clip);
607       g.translate(-labelWidthWest, 0);
608
609       ypos += cHeight + annotationHeight + hgap;
610
611       startRes += cWidth;
612     }
613   }
614
615   AnnotationPanel annotations;
616
617   int getAnnotationHeight()
618   {
619     if (!av.isShowAnnotation())
620     {
621       return 0;
622     }
623
624     if (annotations == null)
625     {
626       annotations = new AnnotationPanel(av);
627     }
628
629     return annotations.adjustPanelHeight();
630   }
631
632   /**
633    * Draws the visible region of the alignment on the graphics context. If there
634    * are hidden column markers in the visible region, then each sub-region
635    * between the markers is drawn separately, followed by the hidden column
636    * marker.
637    * 
638    * @param g1
639    * @param startRes
640    *          offset of the first column in the visible region (0..)
641    * @param endRes
642    *          offset of the last column in the visible region (0..)
643    * @param startSeq
644    *          offset of the first sequence in the visible region (0..)
645    * @param endSeq
646    *          offset of the last sequence in the visible region (0..)
647    * @param yOffset
648    *          vertical offset at which to draw (for wrapped alignments)
649    */
650   public void drawPanel(Graphics g1, final int startRes, final int endRes,
651           final int startSeq, final int endSeq, final int yOffset)
652   {
653     updateViewport();
654     if (!av.hasHiddenColumns())
655     {
656       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
657     }
658     else
659     {
660       int screenY = 0;
661       final int screenYMax = endRes - startRes;
662       int blockStart = startRes;
663       int blockEnd = endRes;
664
665       for (int[] region : av.getAlignment().getHiddenColumns()
666               .getHiddenColumnsCopy())
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. Answers true
994    * if any highlight was left on the visible alignment (so status bar should be
995    * set to match), else false.
996    * 
997    * @param results
998    * @return
999    */
1000   public boolean highlightSearchResults(SearchResultsI results)
1001   {
1002     updateViewport();
1003
1004     /*
1005      * for now, don't attempt fastpaint if wrapped format
1006      */
1007     boolean wrapped = av.getWrapAlignment();
1008     if (wrapped)
1009     {
1010       // drawWrappedMappedPositions(results);
1011       // img = null;
1012       // av.setSearchResults(results);
1013       // repaint();
1014       // return;
1015     }
1016     
1017     fastpainting = true;
1018     fastPaint = true;
1019
1020     try
1021     {
1022       /*
1023        * to avoid redrawing the whole visible region, we instead
1024        * redraw just the minimal regions to remove previous highlights
1025        * and add new ones
1026        */
1027       SearchResultsI previous = av.getSearchResults();
1028       av.setSearchResults(results);
1029       boolean redrawn = false;
1030       boolean drawn = false;
1031       if (wrapped)
1032       {
1033         redrawn = drawWrappedMappedPositions(previous);
1034         drawn = drawWrappedMappedPositions(results);
1035         redrawn |= drawn;
1036       }
1037       else
1038       {
1039         redrawn = drawMappedPositions(previous);
1040         drawn = drawMappedPositions(results);
1041         redrawn |= drawn;
1042       }
1043
1044       /*
1045        * if highlights were either removed or added, repaint
1046        */
1047       if (redrawn)
1048       {
1049         repaint();
1050       }
1051
1052       /*
1053        * return true only if highlights were added
1054        */
1055       return drawn;
1056
1057     } finally
1058     {
1059       fastpainting = false;
1060     }
1061
1062   }
1063
1064   /**
1065    * Redraws the minimal rectangle in the visible region (if any) that includes
1066    * mapped positions of the given search results. Whether or not positions are
1067    * highlighted depends on the SearchResults set on the Viewport. This allows
1068    * this method to be called to either clear or set highlighting. Answers true
1069    * if any positions were drawn (in which case a repaint is still required),
1070    * else false.
1071    * 
1072    * @param results
1073    * @return
1074    */
1075   protected boolean drawMappedPositions(SearchResultsI results)
1076   {
1077     if (results == null)
1078     {
1079       return false;
1080     }
1081
1082     /*
1083      * calculate the minimal rectangle to redraw that 
1084      * includes both new and existing search results
1085      */
1086     int firstSeq = Integer.MAX_VALUE;
1087     int lastSeq = -1;
1088     int firstCol = Integer.MAX_VALUE;
1089     int lastCol = -1;
1090     boolean matchFound = false;
1091
1092     ViewportRanges ranges = av.getRanges();
1093     int firstVisibleColumn = ranges.getStartRes();
1094     int lastVisibleColumn = ranges.getEndRes();
1095     AlignmentI alignment = av.getAlignment();
1096     if (av.hasHiddenColumns())
1097     {
1098       firstVisibleColumn = alignment.getHiddenColumns()
1099               .adjustForHiddenColumns(firstVisibleColumn);
1100       lastVisibleColumn = alignment.getHiddenColumns()
1101               .adjustForHiddenColumns(lastVisibleColumn);
1102     }
1103
1104     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1105             .getEndSeq(); seqNo++)
1106     {
1107       SequenceI seq = alignment.getSequenceAt(seqNo);
1108
1109       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1110               lastVisibleColumn);
1111       if (visibleResults != null)
1112       {
1113         for (int i = 0; i < visibleResults.length - 1; i += 2)
1114         {
1115           int firstMatchedColumn = visibleResults[i];
1116           int lastMatchedColumn = visibleResults[i + 1];
1117           if (firstMatchedColumn <= lastVisibleColumn
1118                   && lastMatchedColumn >= firstVisibleColumn)
1119           {
1120             /*
1121              * found a search results match in the visible region - 
1122              * remember the first and last sequence matched, and the first
1123              * and last visible columns in the matched positions
1124              */
1125             matchFound = true;
1126             firstSeq = Math.min(firstSeq, seqNo);
1127             lastSeq = Math.max(lastSeq, seqNo);
1128             firstMatchedColumn = Math.max(firstMatchedColumn,
1129                     firstVisibleColumn);
1130             lastMatchedColumn = Math.min(lastMatchedColumn,
1131                     lastVisibleColumn);
1132             firstCol = Math.min(firstCol, firstMatchedColumn);
1133             lastCol = Math.max(lastCol, lastMatchedColumn);
1134           }
1135         }
1136       }
1137     }
1138
1139     if (matchFound)
1140     {
1141       if (av.hasHiddenColumns())
1142       {
1143         firstCol = alignment.getHiddenColumns()
1144                 .findColumnPosition(firstCol);
1145         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1146       }
1147       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1148       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1149       gg.translate(transX, transY);
1150       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1151       gg.translate(-transX, -transY);
1152     }
1153
1154     return matchFound;
1155   }
1156
1157   @Override
1158   public void propertyChange(PropertyChangeEvent evt)
1159   {
1160     if (!av.getWrapAlignment())
1161     {
1162       if (evt.getPropertyName().equals("startres")
1163               || evt.getPropertyName().equals("endres"))
1164       {
1165         // Make sure we're not trying to draw a panel
1166         // larger than the visible window
1167         ViewportRanges vpRanges = av.getRanges();
1168         int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1169         if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
1170         {
1171           scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
1172         }
1173         else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
1174         {
1175           scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
1176         }
1177         fastPaint(scrollX, 0);
1178       }
1179       else if (evt.getPropertyName().equals("startseq")
1180               || evt.getPropertyName().equals("endseq"))
1181       {
1182         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1183       }
1184     }
1185   }
1186
1187   /**
1188    * Redraws any positions in the search results in the visible region. Any
1189    * highlights are drawn depending on the search results set on the Viewport,
1190    * not the results parameter. This allows this method to be called to either
1191    * clear highlighting (passing the previous search results), or set new
1192    * highlighting.
1193    * 
1194    * @param results
1195    * @return
1196    */
1197   protected boolean drawWrappedMappedPositions(SearchResultsI results)
1198   {
1199     if (results == null)
1200     {
1201       return false;
1202     }
1203   
1204     boolean matchFound = false;
1205   
1206     /*
1207      * Viewport ranges are set for the 'row' of the wrapped alignment
1208      * the cursor is in, not the whole visible region; really we want
1209      * the latter; +-6 a temporary fudge for codons wrapping across lines
1210      */
1211     ViewportRanges ranges = av.getRanges();
1212     int firstVisibleColumn = ranges.getStartRes() - 6;
1213     int lastVisibleColumn = ranges.getEndRes() + 6;
1214     AlignmentI alignment = av.getAlignment();
1215     if (av.hasHiddenColumns())
1216     {
1217       firstVisibleColumn = alignment.getHiddenColumns()
1218               .adjustForHiddenColumns(firstVisibleColumn);
1219       lastVisibleColumn = alignment.getHiddenColumns()
1220               .adjustForHiddenColumns(lastVisibleColumn);
1221     }
1222   
1223     /*
1224      * find width of alignment in residues, and height of alignment, 
1225      * so we can calculate where to render each matched position
1226      */
1227     int wrappedWidth = av.getWrappedWidth();
1228     int wrappedHeight = av.getAlignment().getHeight() * av.getCharHeight();
1229     int gapHeight = av.getCharHeight()
1230             * (av.getScaleAboveWrapped() ? 2 : 1);
1231     wrappedHeight += gapHeight;
1232     wrappedHeight += getAnnotationHeight();
1233
1234     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1235             .getEndSeq(); seqNo++)
1236     {
1237       SequenceI seq = alignment.getSequenceAt(seqNo);
1238
1239       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1240               lastVisibleColumn);
1241       if (visibleResults != null)
1242       {
1243         for (int i = 0; i < visibleResults.length - 1; i += 2)
1244         {
1245           int firstMatchedColumn = visibleResults[i];
1246           int lastMatchedColumn = visibleResults[i + 1];
1247           if (firstMatchedColumn <= lastVisibleColumn
1248                   && lastMatchedColumn >= firstVisibleColumn)
1249           {
1250             /*
1251              * found a search results match in the visible region
1252              */
1253             firstMatchedColumn = Math.max(firstMatchedColumn,
1254                     firstVisibleColumn);
1255             lastMatchedColumn = Math.min(lastMatchedColumn,
1256                     lastVisibleColumn);
1257
1258             /*
1259              * draw each mapped position separately (as contiguous positions may
1260              * wrap across lines)
1261              */
1262             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1263             {
1264               int displayColumn = mappedPos;
1265               if (av.hasHiddenColumns())
1266               {
1267                 displayColumn = alignment.getHiddenColumns()
1268                         .findColumnPosition(displayColumn);
1269               }
1270
1271               /*
1272                * transX: offset from left edge of canvas to residue position
1273                */
1274               int transX = labelWidthWest
1275                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1276                       * av.getCharWidth();
1277
1278               /*
1279                * transY: offset from top edge of canvas to residue position
1280                */
1281               int transY = gapHeight;
1282               transY += (displayColumn / wrappedWidth) * wrappedHeight;
1283               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1284
1285               /*
1286                * yOffset is from graphics origin to start of visible region
1287                */
1288               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1289               if (transY < getHeight())
1290               {
1291                 matchFound = true;
1292                 gg.translate(transX, transY);
1293                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1294                         yOffset);
1295                 gg.translate(-transX, -transY);
1296               }
1297             }
1298           }
1299         }
1300       }
1301     }
1302   
1303     return matchFound;
1304   }
1305 }