d4b3e739961e56109be1708d47b9e7692d2e3b19
[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),
150                 (ypos + 2) - (charHeight / 2),
151                 (mpos * charWidth) + (charWidth / 2), 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,
214                 (ypos + (i * charHeight)) - (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,
266                 (ypos + (i * charHeight)) - (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 && (fastPaint
352             || (getVisibleRect().width != g.getClipBounds().width)
353             || (getVisibleRect().height != g.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    * Returns the visible width of the canvas in residues, after allowing for
417    * East or West scales (if shown)
418    * 
419    * @param canvasWidth
420    *          the width in pixels (possibly including scales)
421    * 
422    * @return
423    */
424   public int getWrappedCanvasWidth(int canvasWidth)
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 = labelWidthEast > 0 ? labelWidthEast
439               : getLabelWidth(fm);
440     }
441
442     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
443   }
444
445   /**
446    * Returns a pixel width suitable for showing the largest sequence coordinate
447    * (end position) in the alignment. Returns 2 plus the number of decimal
448    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
449    * 
450    * @param fm
451    * @return
452    */
453   protected int getLabelWidth(FontMetrics fm)
454   {
455     /*
456      * find the biggest sequence end position we need to show
457      * (note this is not necessarily the sequence length)
458      */
459     int maxWidth = 0;
460     AlignmentI alignment = av.getAlignment();
461     for (int i = 0; i < alignment.getHeight(); i++)
462     {
463       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
464     }
465
466     int length = 2;
467     for (int i = maxWidth; i > 0; i /= 10)
468     {
469       length++;
470     }
471
472     return fm.stringWidth(ZEROS.substring(0, length));
473   }
474
475   /**
476    * DOCUMENT ME!
477    * 
478    * @param g
479    *          DOCUMENT ME!
480    * @param canvasWidth
481    *          DOCUMENT ME!
482    * @param canvasHeight
483    *          DOCUMENT ME!
484    * @param startRes
485    *          DOCUMENT ME!
486    */
487   public void drawWrappedPanel(Graphics g, int canvasWidth,
488           int canvasHeight, int startRes)
489   {
490     updateViewport();
491     AlignmentI al = av.getAlignment();
492
493     int labelWidth = 0;
494     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
495     {
496       FontMetrics fm = getFontMetrics(av.getFont());
497       labelWidth = getLabelWidth(fm);
498     }
499
500     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
501     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
502
503     int hgap = charHeight;
504     if (av.getScaleAboveWrapped())
505     {
506       hgap += charHeight;
507     }
508
509     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
510     int cHeight = av.getAlignment().getHeight() * charHeight;
511
512     av.setWrappedWidth(cWidth);
513
514     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
515
516     int endx;
517     int ypos = hgap;
518     int maxwidth = av.getAlignment().getWidth();
519
520     if (av.hasHiddenColumns())
521     {
522       maxwidth = av.getAlignment().getHiddenColumns()
523               .findColumnPosition(maxwidth);
524     }
525
526     int annotationHeight = getAnnotationHeight();
527
528     while ((ypos <= canvasHeight) && (startRes < maxwidth))
529     {
530       endx = startRes + cWidth - 1;
531
532       if (endx > maxwidth)
533       {
534         endx = maxwidth;
535       }
536
537       g.setFont(av.getFont());
538       g.setColor(Color.black);
539
540       if (av.getScaleLeftWrapped())
541       {
542         drawWestScale(g, startRes, endx, ypos);
543       }
544
545       if (av.getScaleRightWrapped())
546       {
547         g.translate(canvasWidth - labelWidthEast, 0);
548         drawEastScale(g, startRes, endx, ypos);
549         g.translate(-(canvasWidth - labelWidthEast), 0);
550       }
551
552       g.translate(labelWidthWest, 0);
553
554       if (av.getScaleAboveWrapped())
555       {
556         drawNorthScale(g, startRes, endx, ypos);
557       }
558
559       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
560       {
561         g.setColor(Color.blue);
562         int res;
563         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
564         List<Integer> positions = hidden.findHiddenRegionPositions();
565         for (int pos : positions)
566         {
567           res = pos - startRes;
568
569           if (res < 0 || res > endx - startRes)
570           {
571             continue;
572           }
573
574           gg.fillPolygon(
575                   new int[]
576                   { res * charWidth - charHeight / 4,
577                       res * charWidth + charHeight / 4, res * charWidth },
578                   new int[]
579                   { ypos - (charHeight / 2), ypos - (charHeight / 2),
580                       ypos - (charHeight / 2) + 8 },
581                   3);
582
583         }
584       }
585
586       // When printing we have an extra clipped region,
587       // the Printable page which we need to account for here
588       Shape clip = g.getClip();
589
590       if (clip == null)
591       {
592         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
593       }
594       else
595       {
596         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
597                 (int) clip.getBounds().getHeight());
598       }
599
600       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
601
602       if (av.isShowAnnotation())
603       {
604         g.translate(0, cHeight + ypos + 3);
605         if (annotations == null)
606         {
607           annotations = new AnnotationPanel(av);
608         }
609
610         annotations.renderer.drawComponent(annotations, av, g, -1, startRes,
611                 endx + 1);
612         g.translate(0, -cHeight - ypos - 3);
613       }
614       g.setClip(clip);
615       g.translate(-labelWidthWest, 0);
616
617       ypos += cHeight + annotationHeight + hgap;
618
619       startRes += cWidth;
620     }
621   }
622
623   AnnotationPanel annotations;
624
625   int getAnnotationHeight()
626   {
627     if (!av.isShowAnnotation())
628     {
629       return 0;
630     }
631
632     if (annotations == null)
633     {
634       annotations = new AnnotationPanel(av);
635     }
636
637     return annotations.adjustPanelHeight();
638   }
639
640   /**
641    * Draws the visible region of the alignment on the graphics context. If there
642    * are hidden column markers in the visible region, then each sub-region
643    * between the markers is drawn separately, followed by the hidden column
644    * marker.
645    * 
646    * @param g1
647    * @param startRes
648    *          offset of the first column in the visible region (0..)
649    * @param endRes
650    *          offset of the last column in the visible region (0..)
651    * @param startSeq
652    *          offset of the first sequence in the visible region (0..)
653    * @param endSeq
654    *          offset of the last sequence in the visible region (0..)
655    * @param yOffset
656    *          vertical offset at which to draw (for wrapped alignments)
657    */
658   public void drawPanel(Graphics g1, final int startRes, final int endRes,
659           final int startSeq, final int endSeq, final int yOffset)
660   {
661     updateViewport();
662     if (!av.hasHiddenColumns())
663     {
664       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
665     }
666     else
667     {
668       int screenY = 0;
669       final int screenYMax = endRes - startRes;
670       int blockStart = startRes;
671       int blockEnd = endRes;
672
673       for (int[] region : av.getAlignment().getHiddenColumns()
674               .getHiddenColumnsCopy())
675       {
676         int hideStart = region[0];
677         int hideEnd = region[1];
678
679         if (hideStart <= blockStart)
680         {
681           blockStart += (hideEnd - hideStart) + 1;
682           continue;
683         }
684
685         /*
686          * draw up to just before the next hidden region, or the end of
687          * the visible region, whichever comes first
688          */
689         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
690                 - screenY);
691
692         g1.translate(screenY * charWidth, 0);
693
694         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
695
696         /*
697          * draw the downline of the hidden column marker (ScalePanel draws the
698          * triangle on top) if we reached it
699          */
700         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
701         {
702           g1.setColor(Color.blue);
703
704           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
705                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
706                   (endSeq - startSeq + 1) * charHeight + yOffset);
707         }
708
709         g1.translate(-screenY * charWidth, 0);
710         screenY += blockEnd - blockStart + 1;
711         blockStart = hideEnd + 1;
712
713         if (screenY > screenYMax)
714         {
715           // already rendered last block
716           return;
717         }
718       }
719
720       if (screenY <= screenYMax)
721       {
722         // remaining visible region to render
723         blockEnd = blockStart + screenYMax - screenY;
724         g1.translate(screenY * charWidth, 0);
725         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
726
727         g1.translate(-screenY * charWidth, 0);
728       }
729     }
730
731   }
732
733   /**
734    * Draws a region of the visible alignment
735    * 
736    * @param g1
737    * @param startRes
738    *          offset of the first column in the visible region (0..)
739    * @param endRes
740    *          offset of the last column in the visible region (0..)
741    * @param startSeq
742    *          offset of the first sequence in the visible region (0..)
743    * @param endSeq
744    *          offset of the last sequence in the visible region (0..)
745    * @param yOffset
746    *          vertical offset at which to draw (for wrapped alignments)
747    */
748   private void draw(Graphics g, int startRes, int endRes, int startSeq,
749           int endSeq, int offset)
750   {
751     g.setFont(av.getFont());
752     sr.prepare(g, av.isRenderGaps());
753
754     SequenceI nextSeq;
755
756     // / First draw the sequences
757     // ///////////////////////////
758     for (int i = startSeq; i <= endSeq; i++)
759     {
760       nextSeq = av.getAlignment().getSequenceAt(i);
761       if (nextSeq == null)
762       {
763         // occasionally, a race condition occurs such that the alignment row is
764         // empty
765         continue;
766       }
767       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
768               startRes, endRes, offset + ((i - startSeq) * charHeight));
769
770       if (av.isShowSequenceFeatures())
771       {
772         fr.drawSequence(g, nextSeq, startRes, endRes,
773                 offset + ((i - startSeq) * charHeight), false);
774       }
775
776       /*
777        * highlight search Results once sequence has been drawn
778        */
779       if (av.hasSearchResults())
780       {
781         SearchResultsI searchResults = av.getSearchResults();
782         int[] visibleResults = searchResults.getResults(nextSeq,
783                 startRes, endRes);
784         if (visibleResults != null)
785         {
786           for (int r = 0; r < visibleResults.length; r += 2)
787           {
788             sr.drawHighlightedText(nextSeq, visibleResults[r],
789                     visibleResults[r + 1],
790                     (visibleResults[r] - startRes) * charWidth,
791                     offset + ((i - startSeq) * charHeight));
792           }
793         }
794       }
795
796       if (av.cursorMode && cursorY == i && cursorX >= startRes
797               && cursorX <= endRes)
798       {
799         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
800                 offset + ((i - startSeq) * charHeight));
801       }
802     }
803
804     if (av.getSelectionGroup() != null
805             || av.getAlignment().getGroups().size() > 0)
806     {
807       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
808     }
809
810   }
811
812   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
813           int startSeq, int endSeq, int offset)
814   {
815     Graphics2D g = (Graphics2D) g1;
816     //
817     // ///////////////////////////////////
818     // Now outline any areas if necessary
819     // ///////////////////////////////////
820     SequenceGroup group = av.getSelectionGroup();
821
822     int sx = -1;
823     int sy = -1;
824     int ex = -1;
825     int groupIndex = -1;
826     int visWidth = (endRes - startRes + 1) * charWidth;
827
828     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
829     {
830       group = av.getAlignment().getGroups().get(0);
831       groupIndex = 0;
832     }
833
834     if (group != null)
835     {
836       do
837       {
838         int oldY = -1;
839         int i = 0;
840         boolean inGroup = false;
841         int top = -1;
842         int bottom = -1;
843
844         for (i = startSeq; i <= endSeq; i++)
845         {
846           sx = (group.getStartRes() - startRes) * charWidth;
847           sy = offset + ((i - startSeq) * charHeight);
848           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
849                   - 1;
850
851           if (sx + ex < 0 || sx > visWidth)
852           {
853             continue;
854           }
855
856           if ((sx <= (endRes - startRes) * charWidth)
857                   && group.getSequences(null)
858                           .contains(av.getAlignment().getSequenceAt(i)))
859           {
860             if ((bottom == -1) && !group.getSequences(null)
861                     .contains(av.getAlignment().getSequenceAt(i + 1)))
862             {
863               bottom = sy + charHeight;
864             }
865
866             if (!inGroup)
867             {
868               if (((top == -1) && (i == 0)) || !group.getSequences(null)
869                       .contains(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[]
881                         { 5f, 3f }, 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 }