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    * Returns the visible width of the canvas in residues, after allowing for
418    * East or West scales (if shown)
419    * 
420    * @param canvasWidth
421    *          the width in pixels (possibly including scales)
422    * 
423    * @return
424    */
425   public int getWrappedCanvasWidth(int canvasWidth)
426   {
427     FontMetrics fm = getFontMetrics(av.getFont());
428
429     labelWidthEast = 0;
430     labelWidthWest = 0;
431
432     if (av.getScaleRightWrapped())
433     {
434       labelWidthEast = getLabelWidth(fm);
435     }
436
437     if (av.getScaleLeftWrapped())
438     {
439       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
440               : getLabelWidth(fm);
441     }
442
443     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
444   }
445
446   /**
447    * Returns a pixel width suitable for showing the largest sequence coordinate
448    * (end position) in the alignment. Returns 2 plus the number of decimal
449    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
450    * 
451    * @param fm
452    * @return
453    */
454   protected int getLabelWidth(FontMetrics fm)
455   {
456     /*
457      * find the biggest sequence end position we need to show
458      * (note this is not necessarily the sequence length)
459      */
460     int maxWidth = 0;
461     AlignmentI alignment = av.getAlignment();
462     for (int i = 0; i < alignment.getHeight(); i++)
463     {
464       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
465     }
466
467     int length = 2;
468     for (int i = maxWidth; i > 0; i /= 10)
469     {
470       length++;
471     }
472
473     return fm.stringWidth(ZEROS.substring(0, length));
474   }
475
476   /**
477    * DOCUMENT ME!
478    * 
479    * @param g
480    *          DOCUMENT ME!
481    * @param canvasWidth
482    *          DOCUMENT ME!
483    * @param canvasHeight
484    *          DOCUMENT ME!
485    * @param startRes
486    *          DOCUMENT ME!
487    */
488   public void drawWrappedPanel(Graphics g, int canvasWidth,
489           int canvasHeight, int startRes)
490   {
491     updateViewport();
492     AlignmentI al = av.getAlignment();
493
494     int labelWidth = 0;
495     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
496     {
497       FontMetrics fm = getFontMetrics(av.getFont());
498       labelWidth = getLabelWidth(fm);
499     }
500
501     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
502     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
503
504     int hgap = charHeight;
505     if (av.getScaleAboveWrapped())
506     {
507       hgap += charHeight;
508     }
509
510     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
511     int cHeight = av.getAlignment().getHeight() * charHeight;
512
513     av.setWrappedWidth(cWidth);
514
515     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
516
517     int endx;
518     int ypos = hgap;
519     int maxwidth = av.getAlignment().getWidth();
520
521     if (av.hasHiddenColumns())
522     {
523       maxwidth = av.getAlignment().getHiddenColumns()
524               .findColumnPosition(maxwidth);
525     }
526
527     int annotationHeight = getAnnotationHeight();
528
529     while ((ypos <= canvasHeight) && (startRes < maxwidth))
530     {
531       endx = startRes + cWidth - 1;
532
533       if (endx > maxwidth)
534       {
535         endx = maxwidth;
536       }
537
538       g.setFont(av.getFont());
539       g.setColor(Color.black);
540
541       if (av.getScaleLeftWrapped())
542       {
543         drawWestScale(g, startRes, endx, ypos);
544       }
545
546       if (av.getScaleRightWrapped())
547       {
548         g.translate(canvasWidth - labelWidthEast, 0);
549         drawEastScale(g, startRes, endx, ypos);
550         g.translate(-(canvasWidth - labelWidthEast), 0);
551       }
552
553       g.translate(labelWidthWest, 0);
554
555       if (av.getScaleAboveWrapped())
556       {
557         drawNorthScale(g, startRes, endx, ypos);
558       }
559
560       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
561       {
562         g.setColor(Color.blue);
563         int res;
564         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
565         List<Integer> positions = hidden.findHiddenRegionPositions();
566         for (int pos : positions)
567         {
568           res = pos - startRes;
569
570           if (res < 0 || res > endx - startRes)
571           {
572             continue;
573           }
574
575           gg.fillPolygon(
576                   new int[] { res * charWidth - charHeight / 4,
577                       res * charWidth + charHeight / 4, res * charWidth },
578                   new int[] { ypos - (charHeight / 2),
579                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
580                   3);
581
582         }
583       }
584
585       // When printing we have an extra clipped region,
586       // the Printable page which we need to account for here
587       Shape clip = g.getClip();
588
589       if (clip == null)
590       {
591         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
592       }
593       else
594       {
595         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
596                 (int) clip.getBounds().getHeight());
597       }
598
599       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
600
601       if (av.isShowAnnotation())
602       {
603         g.translate(0, cHeight + ypos + 3);
604         if (annotations == null)
605         {
606           annotations = new AnnotationPanel(av);
607         }
608
609         annotations.renderer.drawComponent(annotations, av, g, -1,
610                 startRes, endx + 1);
611         g.translate(0, -cHeight - ypos - 3);
612       }
613       g.setClip(clip);
614       g.translate(-labelWidthWest, 0);
615
616       ypos += cHeight + annotationHeight + hgap;
617
618       startRes += cWidth;
619     }
620   }
621
622   AnnotationPanel annotations;
623
624   int getAnnotationHeight()
625   {
626     if (!av.isShowAnnotation())
627     {
628       return 0;
629     }
630
631     if (annotations == null)
632     {
633       annotations = new AnnotationPanel(av);
634     }
635
636     return annotations.adjustPanelHeight();
637   }
638
639   /**
640    * Draws the visible region of the alignment on the graphics context. If there
641    * are hidden column markers in the visible region, then each sub-region
642    * between the markers is drawn separately, followed by the hidden column
643    * marker.
644    * 
645    * @param g1
646    * @param startRes
647    *          offset of the first column in the visible region (0..)
648    * @param endRes
649    *          offset of the last column in the visible region (0..)
650    * @param startSeq
651    *          offset of the first sequence in the visible region (0..)
652    * @param endSeq
653    *          offset of the last sequence in the visible region (0..)
654    * @param yOffset
655    *          vertical offset at which to draw (for wrapped alignments)
656    */
657   public void drawPanel(Graphics g1, final int startRes, final int endRes,
658           final int startSeq, final int endSeq, final int yOffset)
659   {
660     updateViewport();
661     if (!av.hasHiddenColumns())
662     {
663       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
664     }
665     else
666     {
667       int screenY = 0;
668       final int screenYMax = endRes - startRes;
669       int blockStart = startRes;
670       int blockEnd = endRes;
671
672       for (int[] region : av.getAlignment().getHiddenColumns()
673               .getHiddenColumnsCopy())
674       {
675         int hideStart = region[0];
676         int hideEnd = region[1];
677
678         if (hideStart <= blockStart)
679         {
680           blockStart += (hideEnd - hideStart) + 1;
681           continue;
682         }
683
684         /*
685          * draw up to just before the next hidden region, or the end of
686          * the visible region, whichever comes first
687          */
688         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
689                 - screenY);
690
691         g1.translate(screenY * charWidth, 0);
692
693         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
694
695         /*
696          * draw the downline of the hidden column marker (ScalePanel draws the
697          * triangle on top) if we reached it
698          */
699         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
700         {
701           g1.setColor(Color.blue);
702
703           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
704                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
705                   (endSeq - startSeq + 1) * charHeight + yOffset);
706         }
707
708         g1.translate(-screenY * charWidth, 0);
709         screenY += blockEnd - blockStart + 1;
710         blockStart = hideEnd + 1;
711
712         if (screenY > screenYMax)
713         {
714           // already rendered last block
715           return;
716         }
717       }
718
719       if (screenY <= screenYMax)
720       {
721         // remaining visible region to render
722         blockEnd = blockStart + screenYMax - screenY;
723         g1.translate(screenY * charWidth, 0);
724         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
725
726         g1.translate(-screenY * charWidth, 0);
727       }
728     }
729
730   }
731
732   /**
733    * Draws a region of the visible alignment
734    * 
735    * @param g1
736    * @param startRes
737    *          offset of the first column in the visible region (0..)
738    * @param endRes
739    *          offset of the last column in the visible region (0..)
740    * @param startSeq
741    *          offset of the first sequence in the visible region (0..)
742    * @param endSeq
743    *          offset of the last sequence in the visible region (0..)
744    * @param yOffset
745    *          vertical offset at which to draw (for wrapped alignments)
746    */
747   private void draw(Graphics g, int startRes, int endRes, int startSeq,
748           int endSeq, int offset)
749   {
750     g.setFont(av.getFont());
751     sr.prepare(g, av.isRenderGaps());
752
753     SequenceI nextSeq;
754
755     // / First draw the sequences
756     // ///////////////////////////
757     for (int i = startSeq; i <= endSeq; i++)
758     {
759       nextSeq = av.getAlignment().getSequenceAt(i);
760       if (nextSeq == null)
761       {
762         // occasionally, a race condition occurs such that the alignment row is
763         // empty
764         continue;
765       }
766       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
767               startRes, endRes, offset + ((i - startSeq) * charHeight));
768
769       if (av.isShowSequenceFeatures())
770       {
771         fr.drawSequence(g, nextSeq, startRes, endRes, offset
772                 + ((i - startSeq) * charHeight), false);
773       }
774
775       /*
776        * highlight search Results once sequence has been drawn
777        */
778       if (av.hasSearchResults())
779       {
780         SearchResultsI searchResults = av.getSearchResults();
781         int[] visibleResults = searchResults.getResults(nextSeq,
782                 startRes, endRes);
783         if (visibleResults != null)
784         {
785           for (int r = 0; r < visibleResults.length; r += 2)
786           {
787             sr.drawHighlightedText(nextSeq, visibleResults[r],
788                     visibleResults[r + 1], (visibleResults[r] - startRes)
789                             * charWidth, offset
790                             + ((i - startSeq) * charHeight));
791           }
792         }
793       }
794
795       if (av.cursorMode && cursorY == i && cursorX >= startRes
796               && cursorX <= endRes)
797       {
798         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
799                 offset + ((i - startSeq) * charHeight));
800       }
801     }
802
803     if (av.getSelectionGroup() != null
804             || av.getAlignment().getGroups().size() > 0)
805     {
806       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
807     }
808
809   }
810
811   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
812           int startSeq, int endSeq, int offset)
813   {
814     Graphics2D g = (Graphics2D) g1;
815     //
816     // ///////////////////////////////////
817     // Now outline any areas if necessary
818     // ///////////////////////////////////
819     SequenceGroup group = av.getSelectionGroup();
820
821     int sx = -1;
822     int sy = -1;
823     int ex = -1;
824     int groupIndex = -1;
825     int visWidth = (endRes - startRes + 1) * charWidth;
826
827     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
828     {
829       group = av.getAlignment().getGroups().get(0);
830       groupIndex = 0;
831     }
832
833     if (group != null)
834     {
835       do
836       {
837         int oldY = -1;
838         int i = 0;
839         boolean inGroup = false;
840         int top = -1;
841         int bottom = -1;
842
843         for (i = startSeq; i <= endSeq; i++)
844         {
845           sx = (group.getStartRes() - startRes) * charWidth;
846           sy = offset + ((i - startSeq) * charHeight);
847           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
848
849           if (sx + ex < 0 || sx > visWidth)
850           {
851             continue;
852           }
853
854           if ((sx <= (endRes - startRes) * charWidth)
855                   && group.getSequences(null).contains(
856                           av.getAlignment().getSequenceAt(i)))
857           {
858             if ((bottom == -1)
859                     && !group.getSequences(null).contains(
860                             av.getAlignment().getSequenceAt(i + 1)))
861             {
862               bottom = sy + charHeight;
863             }
864
865             if (!inGroup)
866             {
867               if (((top == -1) && (i == 0))
868                       || !group.getSequences(null).contains(
869                               av.getAlignment().getSequenceAt(i - 1)))
870               {
871                 top = sy;
872               }
873
874               oldY = sy;
875               inGroup = true;
876
877               if (group == av.getSelectionGroup())
878               {
879                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
880                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
881                         0f));
882                 g.setColor(Color.RED);
883               }
884               else
885               {
886                 g.setStroke(new BasicStroke());
887                 g.setColor(group.getOutlineColour());
888               }
889             }
890           }
891           else
892           {
893             if (inGroup)
894             {
895               if (sx >= 0 && sx < visWidth)
896               {
897                 g.drawLine(sx, oldY, sx, sy);
898               }
899
900               if (sx + ex < visWidth)
901               {
902                 g.drawLine(sx + ex, oldY, sx + ex, sy);
903               }
904
905               if (sx < 0)
906               {
907                 ex += sx;
908                 sx = 0;
909               }
910
911               if (sx + ex > visWidth)
912               {
913                 ex = visWidth;
914               }
915
916               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
917               {
918                 ex = (endRes - startRes + 1) * charWidth;
919               }
920
921               if (top != -1)
922               {
923                 g.drawLine(sx, top, sx + ex, top);
924                 top = -1;
925               }
926
927               if (bottom != -1)
928               {
929                 g.drawLine(sx, bottom, sx + ex, bottom);
930                 bottom = -1;
931               }
932
933               inGroup = false;
934             }
935           }
936         }
937
938         if (inGroup)
939         {
940           sy = offset + ((i - startSeq) * charHeight);
941           if (sx >= 0 && sx < visWidth)
942           {
943             g.drawLine(sx, oldY, sx, sy);
944           }
945
946           if (sx + ex < visWidth)
947           {
948             g.drawLine(sx + ex, oldY, sx + ex, sy);
949           }
950
951           if (sx < 0)
952           {
953             ex += sx;
954             sx = 0;
955           }
956
957           if (sx + ex > visWidth)
958           {
959             ex = visWidth;
960           }
961           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
962           {
963             ex = (endRes - startRes + 1) * charWidth;
964           }
965
966           if (top != -1)
967           {
968             g.drawLine(sx, top, sx + ex, top);
969             top = -1;
970           }
971
972           if (bottom != -1)
973           {
974             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
975             bottom = -1;
976           }
977
978           inGroup = false;
979         }
980
981         groupIndex++;
982
983         g.setStroke(new BasicStroke());
984
985         if (groupIndex >= av.getAlignment().getGroups().size())
986         {
987           break;
988         }
989
990         group = av.getAlignment().getGroups().get(groupIndex);
991
992       } while (groupIndex < av.getAlignment().getGroups().size());
993
994     }
995
996   }
997
998   /**
999    * Highlights search results in the visible region by rendering as white text
1000    * on a black background. Any previous highlighting is removed. Answers true
1001    * if any highlight was left on the visible alignment (so status bar should be
1002    * set to match), else false.
1003    * <p>
1004    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1005    * alignment had to be scrolled to show the highlighted region, then it should
1006    * be fully redrawn, otherwise a fast paint can be performed. This argument
1007    * could be removed if fast paint of scrolled wrapped alignment is coded in
1008    * future (JAL-2609).
1009    * 
1010    * @param results
1011    * @param noFastPaint
1012    * @return
1013    */
1014   public boolean highlightSearchResults(SearchResultsI results,
1015           boolean noFastPaint)
1016   {
1017     if (fastpainting)
1018     {
1019       return false;
1020     }
1021     boolean wrapped = av.getWrapAlignment();
1022
1023     try
1024     {
1025       fastPaint = !noFastPaint;
1026       fastpainting = fastPaint;
1027
1028       updateViewport();
1029
1030       /*
1031        * to avoid redrawing the whole visible region, we instead
1032        * redraw just the minimal regions to remove previous highlights
1033        * and add new ones
1034        */
1035       SearchResultsI previous = av.getSearchResults();
1036       av.setSearchResults(results);
1037       boolean redrawn = false;
1038       boolean drawn = false;
1039       if (wrapped)
1040       {
1041         redrawn = drawMappedPositionsWrapped(previous);
1042         drawn = drawMappedPositionsWrapped(results);
1043         redrawn |= drawn;
1044       }
1045       else
1046       {
1047         redrawn = drawMappedPositions(previous);
1048         drawn = drawMappedPositions(results);
1049         redrawn |= drawn;
1050       }
1051
1052       /*
1053        * if highlights were either removed or added, repaint
1054        */
1055       if (redrawn)
1056       {
1057         repaint();
1058       }
1059
1060       /*
1061        * return true only if highlights were added
1062        */
1063       return drawn;
1064
1065     } finally
1066     {
1067       fastpainting = false;
1068     }
1069   }
1070
1071   /**
1072    * Redraws the minimal rectangle in the visible region (if any) that includes
1073    * mapped positions of the given search results. Whether or not positions are
1074    * highlighted depends on the SearchResults set on the Viewport. This allows
1075    * this method to be called to either clear or set highlighting. Answers true
1076    * if any positions were drawn (in which case a repaint is still required),
1077    * else false.
1078    * 
1079    * @param results
1080    * @return
1081    */
1082   protected boolean drawMappedPositions(SearchResultsI results)
1083   {
1084     if (results == null)
1085     {
1086       return false;
1087     }
1088
1089     /*
1090      * calculate the minimal rectangle to redraw that 
1091      * includes both new and existing search results
1092      */
1093     int firstSeq = Integer.MAX_VALUE;
1094     int lastSeq = -1;
1095     int firstCol = Integer.MAX_VALUE;
1096     int lastCol = -1;
1097     boolean matchFound = false;
1098
1099     ViewportRanges ranges = av.getRanges();
1100     int firstVisibleColumn = ranges.getStartRes();
1101     int lastVisibleColumn = ranges.getEndRes();
1102     AlignmentI alignment = av.getAlignment();
1103     if (av.hasHiddenColumns())
1104     {
1105       firstVisibleColumn = alignment.getHiddenColumns()
1106               .adjustForHiddenColumns(firstVisibleColumn);
1107       lastVisibleColumn = alignment.getHiddenColumns()
1108               .adjustForHiddenColumns(lastVisibleColumn);
1109     }
1110
1111     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1112             .getEndSeq(); seqNo++)
1113     {
1114       SequenceI seq = alignment.getSequenceAt(seqNo);
1115
1116       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1117               lastVisibleColumn);
1118       if (visibleResults != null)
1119       {
1120         for (int i = 0; i < visibleResults.length - 1; i += 2)
1121         {
1122           int firstMatchedColumn = visibleResults[i];
1123           int lastMatchedColumn = visibleResults[i + 1];
1124           if (firstMatchedColumn <= lastVisibleColumn
1125                   && lastMatchedColumn >= firstVisibleColumn)
1126           {
1127             /*
1128              * found a search results match in the visible region - 
1129              * remember the first and last sequence matched, and the first
1130              * and last visible columns in the matched positions
1131              */
1132             matchFound = true;
1133             firstSeq = Math.min(firstSeq, seqNo);
1134             lastSeq = Math.max(lastSeq, seqNo);
1135             firstMatchedColumn = Math.max(firstMatchedColumn,
1136                     firstVisibleColumn);
1137             lastMatchedColumn = Math.min(lastMatchedColumn,
1138                     lastVisibleColumn);
1139             firstCol = Math.min(firstCol, firstMatchedColumn);
1140             lastCol = Math.max(lastCol, lastMatchedColumn);
1141           }
1142         }
1143       }
1144     }
1145
1146     if (matchFound)
1147     {
1148       if (av.hasHiddenColumns())
1149       {
1150         firstCol = alignment.getHiddenColumns()
1151                 .findColumnPosition(firstCol);
1152         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1153       }
1154       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1155       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1156       gg.translate(transX, transY);
1157       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1158       gg.translate(-transX, -transY);
1159     }
1160
1161     return matchFound;
1162   }
1163
1164   @Override
1165   public void propertyChange(PropertyChangeEvent evt)
1166   {
1167     String eventName = evt.getPropertyName();
1168
1169     if (av.getWrapAlignment())
1170     {
1171       if (eventName.equals(ViewportRanges.STARTRES))
1172       {
1173         repaint();
1174       }
1175     }
1176     else
1177     {
1178       int scrollX = 0;
1179       if (eventName.equals(ViewportRanges.STARTRES))
1180       {
1181         // Make sure we're not trying to draw a panel
1182         // larger than the visible window
1183         ViewportRanges vpRanges = av.getRanges();
1184         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1185         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1186         if (scrollX > range)
1187         {
1188           scrollX = range;
1189         }
1190         else if (scrollX < -range)
1191         {
1192           scrollX = -range;
1193         }
1194       }
1195
1196       // Both scrolling and resizing change viewport ranges: scrolling changes
1197       // both start and end points, but resize only changes end values.
1198       // Here we only want to fastpaint on a scroll, with resize using a normal
1199       // paint, so scroll events are identified as changes to the horizontal or
1200       // vertical start value.
1201       if (eventName.equals(ViewportRanges.STARTRES))
1202       {
1203         // scroll - startres and endres both change
1204         fastPaint(scrollX, 0);
1205       }
1206       else if (eventName.equals(ViewportRanges.STARTSEQ))
1207       {
1208         // scroll
1209         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1210       }
1211     }
1212   }
1213
1214   /**
1215    * Redraws any positions in the search results in the visible region of a
1216    * wrapped alignment. Any highlights are drawn depending on the search results
1217    * set on the Viewport, not the <code>results</code> argument. This allows
1218    * this method to be called either to clear highlights (passing the previous
1219    * search results), or to draw new highlights.
1220    * 
1221    * @param results
1222    * @return
1223    */
1224   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1225   {
1226     if (results == null)
1227     {
1228       return false;
1229     }
1230   
1231     boolean matchFound = false;
1232
1233     int wrappedWidth = av.getWrappedWidth();
1234     int wrappedHeight = getRepeatHeightWrapped();
1235
1236     ViewportRanges ranges = av.getRanges();
1237     int canvasHeight = getHeight();
1238     int repeats = canvasHeight / wrappedHeight;
1239     if (canvasHeight / wrappedHeight > 0)
1240     {
1241       repeats++;
1242     }
1243
1244     int firstVisibleColumn = ranges.getStartRes();
1245     int lastVisibleColumn = ranges.getStartRes() + repeats
1246             * ranges.getViewportWidth() - 1;
1247
1248     AlignmentI alignment = av.getAlignment();
1249     if (av.hasHiddenColumns())
1250     {
1251       firstVisibleColumn = alignment.getHiddenColumns()
1252               .adjustForHiddenColumns(firstVisibleColumn);
1253       lastVisibleColumn = alignment.getHiddenColumns()
1254               .adjustForHiddenColumns(lastVisibleColumn);
1255     }
1256
1257     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1258
1259     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1260             .getEndSeq(); seqNo++)
1261     {
1262       SequenceI seq = alignment.getSequenceAt(seqNo);
1263
1264       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1265               lastVisibleColumn);
1266       if (visibleResults != null)
1267       {
1268         for (int i = 0; i < visibleResults.length - 1; i += 2)
1269         {
1270           int firstMatchedColumn = visibleResults[i];
1271           int lastMatchedColumn = visibleResults[i + 1];
1272           if (firstMatchedColumn <= lastVisibleColumn
1273                   && lastMatchedColumn >= firstVisibleColumn)
1274           {
1275             /*
1276              * found a search results match in the visible region
1277              */
1278             firstMatchedColumn = Math.max(firstMatchedColumn,
1279                     firstVisibleColumn);
1280             lastMatchedColumn = Math.min(lastMatchedColumn,
1281                     lastVisibleColumn);
1282
1283             /*
1284              * draw each mapped position separately (as contiguous positions may
1285              * wrap across lines)
1286              */
1287             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1288             {
1289               int displayColumn = mappedPos;
1290               if (av.hasHiddenColumns())
1291               {
1292                 displayColumn = alignment.getHiddenColumns()
1293                         .findColumnPosition(displayColumn);
1294               }
1295
1296               /*
1297                * transX: offset from left edge of canvas to residue position
1298                */
1299               int transX = labelWidthWest
1300                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1301                       * av.getCharWidth();
1302
1303               /*
1304                * transY: offset from top edge of canvas to residue position
1305                */
1306               int transY = gapHeight;
1307               transY += (displayColumn - ranges.getStartRes())
1308                       / wrappedWidth * wrappedHeight;
1309               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1310
1311               /*
1312                * yOffset is from graphics origin to start of visible region
1313                */
1314               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1315               if (transY < getHeight())
1316               {
1317                 matchFound = true;
1318                 gg.translate(transX, transY);
1319                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1320                         yOffset);
1321                 gg.translate(-transX, -transY);
1322               }
1323             }
1324           }
1325         }
1326       }
1327     }
1328   
1329     return matchFound;
1330   }
1331
1332   /**
1333    * Answers the height in pixels of a repeating section of the wrapped
1334    * alignment, including space above, scale above if shown, sequences, and
1335    * annotation panel if shown
1336    * 
1337    * @return
1338    */
1339   protected int getRepeatHeightWrapped()
1340   {
1341     // gap (and maybe scale) above
1342     int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1343
1344     // add sequences
1345     repeatHeight += av.getRanges().getViewportHeight() * charHeight;
1346
1347     // add annotations panel height if shown
1348     repeatHeight += getAnnotationHeight();
1349
1350     return repeatHeight;
1351   }
1352 }