JAL-2690attemptedmerge partial merge
[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.util.Comparison;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.AlphaComposite; // ok
35 import java.awt.BasicStroke;
36 import java.awt.BorderLayout;
37 import java.awt.Color;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Graphics2D;
41 import java.awt.RenderingHints;
42 import java.awt.Shape;
43 import java.awt.image.BufferedImage;
44 import java.beans.PropertyChangeEvent;
45 import java.util.List;
46
47 import javax.swing.JComponent;
48
49 /**
50  * DOCUMENT ME!
51  * 
52  * @author $author$
53  * @version $Revision$
54  */
55 public class SeqCanvas extends JComponent implements ViewportListenerI
56 {
57   private static String ZEROS = "0000000000";
58
59   final FeatureRenderer fr;
60
61   final SequenceRenderer seqRdr; //ok
62
63   BufferedImage img;
64
65   Graphics2D gg;
66
67   int imgWidth;
68
69   int imgHeight;
70
71   AlignViewport av;
72
73   boolean fastPaint = false;
74
75   boolean fastpainting = false;
76
77   int cursorX = 0;
78
79   int cursorY = 0;
80
81   private AnnotationPanel annotations;
82
83   /*
84    * measurements for drawing a wrapped alignment
85    */
86   int labelWidthWest; // label left width in pixels if shown
87
88   private int labelWidthEast; // label right width in pixels if shown
89
90   private int wrappedSpaceAboveAlignment; // gap between widths
91
92   private int wrappedRepeatHeightPx; // height in pixels of wrapped width
93
94   private int wrappedVisibleWidths; // number of wrapped widths displayed
95
96   /**
97    * Creates a new SeqCanvas object.
98    * 
99    * @param av
100    *          DOCUMENT ME!
101    */
102   public SeqCanvas(AlignmentPanel ap)
103   {
104     this.av = ap.av;
105     fr = new FeatureRenderer(ap);
106     seqRdr = new SequenceRenderer(av); //ok
107     setLayout(new BorderLayout());
108     PaintRefresher.Register(this, av.getSequenceSetId());
109     setBackground(Color.white);
110
111     av.getRanges().addPropertyChangeListener(this);
112   }
113
114   public SequenceRenderer getSequenceRenderer()
115   {
116     return seqRdr; //ok
117   }
118
119   public FeatureRenderer getFeatureRenderer()
120   {
121     return fr;
122   }
123
124   /**
125    * Draws the scale above a region of a wrapped alignment, consisting of a
126    * column number every major interval (10 columns).
127    * 
128    * @param g
129    *          the graphics context to draw on, positioned at the start (bottom
130    *          left) of the line on which to draw any scale marks
131    * @param startx
132    *          start alignment column (0..)
133    * @param endx
134    *          end alignment column (0..)
135    * @param ypos
136    *          y offset to draw at
137    */
138   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
139   {
140     int charHeight = av.getCharHeight();
141     int charWidth = av.getCharWidth();
142
143     /*
144      * white fill the scale space (for the fastPaint case)
145      */
146     g.setColor(Color.white);
147     g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
148             charHeight * 3 / 2 + 2);
149     g.setColor(Color.black);
150
151     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
152             endx);
153     for (ScaleMark mark : marks)
154     {
155       int mpos = mark.column; // (i - startx - 1)
156       if (mpos < 0)
157       {
158         continue;
159       }
160       String mstring = mark.text;
161
162       if (mark.major)
163       {
164         if (mstring != null)
165         {
166           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
167         }
168
169         /*
170          * draw a tick mark below the column number, centred on the column;
171          * height of tick mark is 4 pixels less than half a character
172          */
173         int xpos = (mpos * charWidth) + (charWidth / 2);
174         g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
175       }
176     }
177   }
178
179   /**
180    * Draw the scale to the left or right of a wrapped alignment
181    * 
182    * @param g
183    *          graphics context, positioned at the start of the scale to be drawn
184    * @param startx
185    *          first column of wrapped width (0.. excluding any hidden columns)
186    * @param endx
187    *          last column of wrapped width (0.. excluding any hidden columns)
188    * @param ypos
189    *          vertical offset at which to begin the scale
190    * @param left
191    *          if true, scale is left of residues, if false, scale is right
192    */
193   void drawVerticalScale(Graphics g, int startx, int endx, int ypos,
194           boolean left)
195   {
196     int charHeight = av.getCharHeight();
197     int charWidth = av.getCharWidth();
198
199     ypos += charHeight;
200
201     if (av.hasHiddenColumns())
202     {
203       HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
204       startx = hiddenColumns.adjustForHiddenColumns(startx);
205       endx = hiddenColumns.adjustForHiddenColumns(endx);
206     }
207     FontMetrics fm = getFontMetrics(av.getFont());
208
209     for (int i = 0; i < av.getAlignment().getHeight(); i++)
210     {
211       SequenceI seq = av.getAlignment().getSequenceAt(i);
212
213       /*
214        * find sequence position of first non-gapped position -
215        * to the right if scale left, to the left if scale right
216        */
217       int index = left ? startx : endx;
218       int value = -1;
219       while (index >= startx && index <= endx)
220       {
221         if (!Comparison.isGap(seq.getCharAt(index)))
222         {
223           value = seq.findPosition(index);
224           break;
225         }
226         if (left)
227         {
228           index++;
229         }
230         else
231         {
232           index--;
233         }
234       }
235
236       /*
237        * white fill the space for the scale
238        */
239       g.setColor(Color.white);
240       int y = (ypos + (i * charHeight)) - (charHeight / 5);
241       // fillRect origin is top left of rectangle
242       g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast,
243               charHeight + 1);
244
245       if (value != -1)
246       {
247
248         /*
249          * draw scale value, right justified, with half a character width
250          * separation from the sequence data
251          */
252         String valueAsString = String.valueOf(value);
253         int justify = fm.stringWidth(valueAsString) + charWidth;
254         int xpos = left ? labelWidthWest - justify + charWidth / 2
255                 : labelWidthEast - justify + charWidth / 2;
256         g.setColor(Color.black);
257         g.drawString(valueAsString, xpos, y);
258       }
259     }
260   }
261
262   //ok
263   /**
264    * Does a fast paint of an alignment in response to a scroll. Most of the
265    * visible region is simply copied and shifted, and then any newly visible
266    * columns or rows are drawn. The scroll may be horizontal or vertical, but
267    * not both at once. Scrolling may be the result of
268    * <ul>
269    * <li>dragging a scroll bar</li>
270    * <li>clicking in the scroll bar</li>
271    * <li>scrolling by trackpad, middle mouse button, or other device</li>
272    * <li>by moving the box in the Overview window</li>
273    * <li>programmatically to make a highlighted position visible</li>
274    * </ul>
275    * 
276    * @param horizontal
277    *          columns to shift right (positive) or left (negative)
278    * @param vertical
279    *          rows to shift down (positive) or up (negative)
280    */
281   public void fastPaint(int horizontal, int vertical)
282   {
283     if (fastpainting || gg == null || img == null) //ok
284     {
285       return;
286     }
287     fastpainting = true;
288     fastPaint = true;
289
290     try
291     {
292       int charHeight = av.getCharHeight();
293       int charWidth = av.getCharWidth();
294     
295       ViewportRanges ranges = av.getRanges();
296       int startRes = ranges.getStartRes();
297       int endRes = ranges.getEndRes();
298       int startSeq = ranges.getStartSeq();
299       int endSeq = ranges.getEndSeq();
300       int transX = 0;
301       int transY = 0;
302
303     gg.copyArea(horizontal * charWidth, vertical * charHeight,
304             img.getWidth(), img.getHeight(), -horizontal * charWidth,
305             -vertical * charHeight);
306
307     if (horizontal > 0) // scrollbar pulled right, image to the left
308     {
309       transX = (endRes - startRes - horizontal) * charWidth;
310       startRes = endRes - horizontal;
311     }
312     else if (horizontal < 0)
313     {
314       endRes = startRes - horizontal;
315     }
316     else if (vertical > 0) // scroll down
317     {
318       startSeq = endSeq - vertical;
319
320       if (startSeq < ranges.getStartSeq())
321       { // ie scrolling too fast, more than a page at a time
322         startSeq = ranges.getStartSeq();
323       }
324       else
325       {
326         transY = img.getHeight() - ((vertical + 1) * charHeight);
327       }
328     }
329     else if (vertical < 0)
330     {
331       endSeq = startSeq - vertical;
332
333       if (endSeq > ranges.getEndSeq())
334       {
335         endSeq = ranges.getEndSeq();
336       }
337     }
338
339     gg.translate(transX, transY);
340     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
341     gg.translate(-transX, -transY);
342
343     repaint();
344     } finally
345     {
346       fastpainting = false;
347     }
348   }
349   //ok
350
351   //ok
352   @Override
353   public void paintComponent(Graphics g)
354   {
355     super.paintComponent(g);    
356     
357     int charHeight = av.getCharHeight();
358     int charWidth = av.getCharWidth();
359
360     ViewportRanges ranges = av.getRanges();
361
362     int width = getWidth();
363     int height = getHeight();
364
365     width -= (width % charWidth);
366     height -= (height % charHeight);
367
368     // selectImage is the selection group outline image
369     BufferedImage selectImage = drawSelectionGroup(
370             ranges.getStartRes(), ranges.getEndRes(),
371             ranges.getStartSeq(), ranges.getEndSeq());
372
373     if ((img != null) && (fastPaint
374             || (getVisibleRect().width != g.getClipBounds().width)
375             || (getVisibleRect().height != g.getClipBounds().height)))
376     {
377       BufferedImage lcimg = buildLocalImage(selectImage);
378       g.drawImage(lcimg, 0, 0, this);
379       fastPaint = false;
380     }
381     else if ((width > 0) && (height > 0))
382     {
383       // img is a cached version of the last view we drew, if any
384       // if we have no img or the size has changed, make a new one
385       if (img == null || width != img.getWidth()
386               || height != img.getHeight())
387       {
388         img = setupImage();
389         if (img == null)
390         {
391           return;
392         }
393         gg = (Graphics2D) img.getGraphics();
394         gg.setFont(av.getFont());
395       }
396
397       if (av.antiAlias)
398       {
399         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
400                 RenderingHints.VALUE_ANTIALIAS_ON);
401       }
402
403       gg.setColor(Color.white);
404       gg.fillRect(0, 0, img.getWidth(), img.getHeight());
405
406       if (av.getWrapAlignment())
407       {
408         drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
409       }
410       else
411       {
412         drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
413                 ranges.getStartSeq(), ranges.getEndSeq(), 0);
414       }
415
416       // lcimg is a local *copy* of img which we'll draw selectImage on top of
417       BufferedImage lcimg = buildLocalImage(selectImage);
418       g.drawImage(lcimg, 0, 0, this);
419     }
420   }
421 //ok
422   
423   /**
424    * Draw an alignment panel for printing
425    * 
426    * @param g1
427    *          Graphics object to draw with
428    * @param startRes
429    *          start residue of print area
430    * @param endRes
431    *          end residue of print area
432    * @param startSeq
433    *          start sequence of print area
434    * @param endSeq
435    *          end sequence of print area
436    */
437   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
438           int startSeq, int endSeq)
439   {
440     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
441
442     BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
443             startSeq, endSeq);
444     if (selectImage != null)
445     {
446       ((Graphics2D) g1).setComposite(AlphaComposite
447               .getInstance(AlphaComposite.SRC_OVER));
448       g1.drawImage(selectImage, 0, 0, this);
449     }
450   }
451
452   /**
453    * Draw a wrapped alignment panel for printing
454    * 
455    * @param g
456    *          Graphics object to draw with
457    * @param canvasWidth
458    *          width of drawing area
459    * @param canvasHeight
460    *          height of drawing area
461    * @param startRes
462    *          start residue of print area
463    */
464   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
465           int canvasHeight, int startRes)
466   {
467     SequenceGroup group = av.getSelectionGroup();
468
469     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
470
471     if (group != null)
472     {
473       BufferedImage selectImage = null;
474       try
475       {
476         selectImage = new BufferedImage(canvasWidth, canvasHeight,
477                 BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
478       } catch (OutOfMemoryError er)
479       {
480         System.gc();
481         System.err.println("Print image OutOfMemory Error.\n" + er);
482         new OOMWarning("Creating wrapped alignment image for printing", er);
483       }
484       if (selectImage != null)
485       {
486         Graphics2D g2 = selectImage.createGraphics();
487         setupSelectionGroup(g2, selectImage);
488         drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
489                 startRes);
490
491         g2.setComposite(
492                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
493         g.drawImage(selectImage, 0, 0, this);
494         g2.dispose();
495       }
496     }
497   }
498
499   /*
500    * Make a local image by combining the cached image img
501    * with any selection
502    */
503   private BufferedImage buildLocalImage(BufferedImage selectImage)
504   {
505     // clone the cached image
506     BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
507             img.getType());
508     Graphics2D g2d = lcimg.createGraphics();
509     g2d.drawImage(img, 0, 0, null);
510
511     // overlay selection group on lcimg
512     if (selectImage != null)
513     {
514       g2d.setComposite(
515               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
516       g2d.drawImage(selectImage, 0, 0, this);
517     }
518     g2d.dispose();
519
520     return lcimg;
521   }
522
523   /*
524    * Set up a buffered image of the correct height and size for the sequence canvas
525    */
526   private BufferedImage setupImage()
527   {
528     BufferedImage lcimg = null;
529
530     int width = getWidth();
531     int height = getHeight();
532
533     width -= (width % charWidth);
534     height -= (height % charHeight);
535
536     if ((width < 1) || (height < 1))
537     {
538       return null;
539     }
540
541     try
542     {
543       lcimg = new BufferedImage(width, height,
544               BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
545     } catch (OutOfMemoryError er)
546     {
547       System.gc();
548       System.err.println(
549               "Group image OutOfMemory Redraw Error.\n" + er);
550       new OOMWarning("Creating alignment image for display", er);
551
552       return null;
553     }
554
555     return lcimg;
556   }
557
558   /**
559    * Returns the visible width of the canvas in residues, after allowing for
560    * East or West scales (if shown)
561    * 
562    * @param canvasWidth
563    *          the width in pixels (possibly including scales)
564    * 
565    * @return
566    */
567   public int getWrappedCanvasWidth(int canvasWidth)
568   {
569     int charWidth = av.getCharWidth();
570
571     FontMetrics fm = getFontMetrics(av.getFont());
572
573     labelWidthEast = 0;
574     labelWidthWest = 0;
575
576     if (av.getScaleRightWrapped())
577     {
578       labelWidthEast = getLabelWidth(fm);
579     }
580
581     if (av.getScaleLeftWrapped())
582     {
583       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
584               : getLabelWidth(fm);
585     }
586
587     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
588   }
589
590   /**
591    * Returns a pixel width suitable for showing the largest sequence coordinate
592    * (end position) in the alignment. Returns 2 plus the number of decimal
593    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
594    * 
595    * @param fm
596    * @return
597    */
598   protected int getLabelWidth(FontMetrics fm)
599   {
600     /*
601      * find the biggest sequence end position we need to show
602      * (note this is not necessarily the sequence length)
603      */
604     int maxWidth = 0;
605     AlignmentI alignment = av.getAlignment();
606     for (int i = 0; i < alignment.getHeight(); i++)
607     {
608       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
609     }
610
611     int length = 2;
612     for (int i = maxWidth; i > 0; i /= 10)
613     {
614       length++;
615     }
616
617     return fm.stringWidth(ZEROS.substring(0, length));
618   }
619
620   /**
621    * Draws as many widths of a wrapped alignment as can fit in the visible
622    * window
623    * 
624    * @param g
625    * @param canvasWidth
626    *          available width in pixels
627    * @param canvasHeight
628    *          available height in pixels
629    * @param startColumn
630    *          the first column (0...) of the alignment to draw
631    */
632   public void drawWrappedPanel(Graphics g, int canvasWidth,
633           int canvasHeight, final int startColumn)
634   {
635     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
636             canvasHeight);
637
638     av.setWrappedWidth(wrappedWidthInResidues);
639
640     ViewportRanges ranges = av.getRanges();
641     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
642
643     /*
644      * draw one width at a time (including any scales or annotation shown),
645      * until we have run out of either alignment or vertical space available
646      */
647     int ypos = wrappedSpaceAboveAlignment;
648     int maxWidth = ranges.getVisibleAlignmentWidth();
649
650     int start = startColumn;
651     int currentWidth = 0;
652     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
653     {
654       int endColumn = Math
655               .min(maxWidth, start + wrappedWidthInResidues - 1);
656       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
657       ypos += wrappedRepeatHeightPx;
658       start += wrappedWidthInResidues;
659       currentWidth++;
660     }
661
662     drawWrappedDecorators(g, startColumn);
663   }
664
665   /**
666    * Calculates and saves values needed when rendering a wrapped alignment.
667    * These depend on many factors, including
668    * <ul>
669    * <li>canvas width and height</li>
670    * <li>number of visible sequences, and height of annotations if shown</li>
671    * <li>font and character width</li>
672    * <li>whether scales are shown left, right or above the alignment</li>
673    * </ul>
674    * 
675    * @param canvasWidth
676    * @param canvasHeight
677    * @return the number of residue columns in each width
678    */
679   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
680   {
681     int charHeight = av.getCharHeight();
682     int charWidth = av.getCharWidth();
683
684     /*
685      * width of labels in pixels left and right (if shown)
686      */
687     int labelWidth = 0;
688     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
689     {
690       FontMetrics fm = getFontMetrics(av.getFont());
691       labelWidth = getLabelWidth(fm);
692     }
693     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
694     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
695
696     /*
697      * vertical space in pixels between wrapped widths of alignment
698      * - one character height, or two if scale above is drawn
699      */
700     wrappedSpaceAboveAlignment = charHeight
701             * (av.getScaleAboveWrapped() ? 2 : 1);
702
703     /*
704      * height in pixels of the wrapped widths
705      */
706     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
707     // add sequences
708     wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
709             * charHeight;
710     // add annotations panel height if shown
711     wrappedRepeatHeightPx += getAnnotationHeight();
712
713     /*
714      * number of residue columns we can show in each row;
715      * this is just canvas width less scale left and right (if shown), 
716      * as a whole multiple of character widths 
717      */
718     int wrappedWidthInResidues = (canvasWidth - labelWidthEast
719             - labelWidthWest) / charWidth;
720
721     /*
722      * number of visible widths (the last one may be part height),
723      * ensuring a part height includes at least one sequence
724      */
725     ViewportRanges ranges = av.getRanges();
726     int xMax = ranges.getVisibleAlignmentWidth();
727     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
728     int remainder = canvasHeight % wrappedRepeatHeightPx;
729     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
730     {
731       wrappedVisibleWidths++;
732     }
733
734     /*
735      *  limit visibleWidths to not exceed width of alignment
736      */
737     int maxWidths = (xMax - ranges.getStartRes()) / wrappedWidthInResidues;
738     if (xMax % wrappedWidthInResidues > 0)
739     {
740       maxWidths++;
741     }
742     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
743
744     return wrappedWidthInResidues;
745   }
746
747   /**
748    * Draws one width of a wrapped alignment, including sequences and
749    * annnotations, if shown, but not scales or hidden column markers
750    * 
751    * @param g
752    * @param ypos
753    * @param startColumn
754    * @param endColumn
755    * @param canvasHeight
756    */
757   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
758           int endColumn, int canvasHeight)
759   {
760     int charHeight = av.getCharHeight();
761     int charWidth = av.getCharWidth();
762
763     ViewportRanges ranges = av.getRanges();
764     int viewportWidth = ranges.getViewportWidth();
765
766     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
767
768     /*
769      * move right before drawing by the width of the scale left (if any)
770      * plus column offset from left margin (usually zero, but may be non-zero
771      * when fast painting is drawing just a few columns)
772      */
773     int xOffset = labelWidthWest
774             + ((startColumn - ranges.getStartRes()) % viewportWidth)
775                     * charWidth;
776     g.translate(xOffset, 0);
777
778     // When printing we have an extra clipped region,
779     // the Printable page which we need to account for here
780     Shape clip = g.getClip();
781
782     if (clip == null)
783     {
784       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
785     }
786     else
787     {
788       g.setClip(0, (int) clip.getBounds().getY(), viewportWidth * charWidth,
789               (int) clip.getBounds().getHeight());
790     }
791
792     /*
793      * white fill the region to be drawn (so incremental fast paint doesn't
794      * scribble over an existing image)
795      */
796     gg.setColor(Color.white);
797     gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
798             wrappedRepeatHeightPx);
799
800     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
801             ypos);
802
803     int cHeight = av.getAlignment().getHeight() * charHeight;
804
805     if (av.isShowAnnotation())
806     {
807       g.translate(0, cHeight + ypos + 3);
808       if (annotations == null)
809       {
810         annotations = new AnnotationPanel(av);
811       }
812
813       annotations.renderer.drawComponent(annotations, av, g, -1,
814               startColumn, endx + 1);
815       g.translate(0, -cHeight - ypos - 3);
816     }
817     g.setClip(clip);
818     g.translate(-xOffset, 0);
819   }
820
821   /**
822    * Draws scales left, right and above (if shown), and any hidden column
823    * markers, on all widths of the wrapped alignment
824    * 
825    * @param g
826    * @param startColumn
827    */
828   protected void drawWrappedDecorators(Graphics g, int startColumn)
829   {
830     int charWidth = av.getCharWidth();
831
832     g.setFont(av.getFont());
833     g.setColor(Color.black);
834
835     int ypos = wrappedSpaceAboveAlignment;
836     ViewportRanges ranges = av.getRanges();
837     int viewportWidth = ranges.getViewportWidth();
838     int maxWidth = ranges.getVisibleAlignmentWidth();
839     int widthsDrawn = 0;
840     while (widthsDrawn < wrappedVisibleWidths)
841     {
842       int endColumn = Math.min(maxWidth, startColumn + viewportWidth - 1);
843
844       if (av.getScaleLeftWrapped())
845       {
846         drawVerticalScale(g, startColumn, endColumn - 1, ypos, true);
847       }
848
849       if (av.getScaleRightWrapped())
850       {
851         int x = labelWidthWest + viewportWidth * charWidth;
852         g.translate(x, 0);
853         drawVerticalScale(g, startColumn, endColumn, ypos, false);
854         g.translate(-x, 0);
855       }
856
857       /*
858        * white fill region of scale above and hidden column markers
859        * (to support incremental fast paint of image)
860        */
861       g.setColor(Color.white);
862       g.fillRect(0, ypos - wrappedSpaceAboveAlignment,
863               viewportWidth * charWidth + labelWidthWest,
864               wrappedSpaceAboveAlignment);
865       g.setColor(Color.black);
866
867       g.translate(labelWidthWest, 0);
868
869       if (av.getScaleAboveWrapped())
870       {
871         drawNorthScale(g, startColumn, endColumn, ypos);
872       }
873
874       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
875       {
876         drawHiddenColumnMarkers(g, ypos, startColumn, endColumn);
877       }
878
879       g.translate(-labelWidthWest, 0);
880
881       ypos += wrappedRepeatHeightPx;
882       startColumn += viewportWidth;
883       widthsDrawn++;
884     }
885   }
886
887   /**
888    * @param g
889    * @param ypos
890    * @param startColumn
891    * @param endColumn
892    */
893   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
894           int startColumn, int endColumn)
895   {
896     int charHeight = av.getCharHeight();
897     int charWidth = av.getCharWidth();
898
899     g.setColor(Color.blue);
900     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
901     List<Integer> positions = hidden.findHiddenRegionPositions();
902     for (int pos : positions)
903     {
904       int res = pos - startColumn;
905
906       if (res < 0 || res > endColumn - startColumn)
907       {
908         continue;
909       }
910
911       /*
912        * draw a downward-pointing triangle at the hidden columns location
913        * (before the following visible column)
914        */
915       int xMiddle = res * charWidth;
916       int[] xPoints = new int[] { xMiddle - charHeight / 4,
917           xMiddle + charHeight / 4, xMiddle };
918       int yTop = ypos - (charHeight / 2);
919       int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
920       g.fillPolygon(xPoints, yPoints, 3);
921     }
922   }
923
924   /*
925    * Draw a selection group over a wrapped alignment
926    */
927   private void drawWrappedSelection(Graphics2D g, SequenceGroup group,
928           int canvasWidth,
929           int canvasHeight, int startRes)
930   {
931     // height gap above each panel
932     int hgap = charHeight;
933     if (av.getScaleAboveWrapped())
934     {
935       hgap += charHeight;
936     }
937
938     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
939             / charWidth;
940     int cHeight = av.getAlignment().getHeight() * charHeight;
941
942     int startx = startRes;
943     int endx;
944     int ypos = hgap; // vertical offset
945     int maxwidth = av.getAlignment().getWidth();
946
947     if (av.hasHiddenColumns())
948     {
949       maxwidth = av.getAlignment().getHiddenColumns()
950               .findColumnPosition(maxwidth);
951     }
952
953     // chop the wrapped alignment extent up into panel-sized blocks and treat
954     // each block as if it were a block from an unwrapped alignment
955     while ((ypos <= canvasHeight) && (startx < maxwidth))
956     {
957       // set end value to be start + width, or maxwidth, whichever is smaller
958       endx = startx + cWidth - 1;
959
960       if (endx > maxwidth)
961       {
962         endx = maxwidth;
963       }
964
965       g.translate(labelWidthWest, 0);
966
967       drawUnwrappedSelection(g, group, startx, endx, 0,
968               av.getAlignment().getHeight() - 1,
969               ypos);
970
971       g.translate(-labelWidthWest, 0);
972
973       // update vertical offset
974       ypos += cHeight + getAnnotationHeight() + hgap;
975
976       // update horizontal offset
977       startx += cWidth;
978     }
979   }
980
981   int getAnnotationHeight()
982   {
983     if (!av.isShowAnnotation())
984     {
985       return 0;
986     }
987
988     if (annotations == null)
989     {
990       annotations = new AnnotationPanel(av);
991     }
992
993     return annotations.adjustPanelHeight();
994   }
995
996   /**
997    * Draws the visible region of the alignment on the graphics context. If there
998    * are hidden column markers in the visible region, then each sub-region
999    * between the markers is drawn separately, followed by the hidden column
1000    * marker.
1001    * 
1002    * @param g1
1003    *          the graphics context, positioned at the first residue to be drawn
1004    * @param startRes
1005    *          offset of the first column to draw (0..)
1006    * @param endRes
1007    *          offset of the last column to draw (0..)
1008    * @param startSeq
1009    *          offset of the first sequence to draw (0..)
1010    * @param endSeq
1011    *          offset of the last sequence to draw (0..)
1012    * @param yOffset
1013    *          vertical offset at which to draw (for wrapped alignments)
1014    */
1015   public void drawPanel(Graphics g1, final int startRes, final int endRes,
1016           final int startSeq, final int endSeq, final int yOffset)
1017   {
1018     int charHeight = av.getCharHeight();
1019     int charWidth = av.getCharWidth();
1020
1021     if (!av.hasHiddenColumns())
1022     {
1023       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
1024     }
1025     else
1026     {
1027       int screenY = 0;
1028       final int screenYMax = endRes - startRes;
1029       int blockStart = startRes;
1030       int blockEnd = endRes;
1031
1032       for (int[] region : av.getAlignment().getHiddenColumns()
1033               .getHiddenColumnsCopy())
1034       {
1035         int hideStart = region[0];
1036         int hideEnd = region[1];
1037
1038         if (hideStart <= blockStart)
1039         {
1040           blockStart += (hideEnd - hideStart) + 1;
1041           continue;
1042         }
1043
1044         /*
1045          * draw up to just before the next hidden region, or the end of
1046          * the visible region, whichever comes first
1047          */
1048         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
1049                 - screenY);
1050
1051         g1.translate(screenY * charWidth, 0);
1052
1053         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1054
1055         /*
1056          * draw the downline of the hidden column marker (ScalePanel draws the
1057          * triangle on top) if we reached it
1058          */
1059         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
1060         {
1061           g1.setColor(Color.blue);
1062
1063           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
1064                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
1065                   (endSeq - startSeq + 1) * charHeight + yOffset);
1066         }
1067
1068         g1.translate(-screenY * charWidth, 0);
1069         screenY += blockEnd - blockStart + 1;
1070         blockStart = hideEnd + 1;
1071
1072         if (screenY > screenYMax)
1073         {
1074           // already rendered last block
1075           return;
1076         }
1077       }
1078
1079       if (screenY <= screenYMax)
1080       {
1081         // remaining visible region to render
1082         blockEnd = blockStart + screenYMax - screenY;
1083         g1.translate(screenY * charWidth, 0);
1084         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
1085
1086         g1.translate(-screenY * charWidth, 0);
1087       }
1088     }
1089
1090   }
1091
1092   /**
1093    * Draws a region of the visible alignment
1094    * 
1095    * @param g1
1096    * @param startRes
1097    *          offset of the first column in the visible region (0..)
1098    * @param endRes
1099    *          offset of the last column in the visible region (0..)
1100    * @param startSeq
1101    *          offset of the first sequence in the visible region (0..)
1102    * @param endSeq
1103    *          offset of the last sequence in the visible region (0..)
1104    * @param yOffset
1105    *          vertical offset at which to draw (for wrapped alignments)
1106    */
1107   private void draw(Graphics g, int startRes, int endRes, int startSeq,
1108           int endSeq, int offset)
1109   {
1110     int charHeight = av.getCharHeight();
1111     int charWidth = av.getCharWidth();
1112
1113     g.setFont(av.getFont());
1114     seqRdr.prepare(g, av.isRenderGaps());
1115
1116     SequenceI nextSeq;
1117
1118     // / First draw the sequences
1119     // ///////////////////////////
1120     for (int i = startSeq; i <= endSeq; i++)
1121     {
1122       nextSeq = av.getAlignment().getSequenceAt(i);
1123       if (nextSeq == null)
1124       {
1125         // occasionally, a race condition occurs such that the alignment row is
1126         // empty
1127         continue;
1128       }
1129       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
1130               startRes, endRes, offset + ((i - startSeq) * charHeight));
1131
1132       if (av.isShowSequenceFeatures())
1133       {
1134         fr.drawSequence(g, nextSeq, startRes, endRes,
1135                 offset + ((i - startSeq) * charHeight), false);
1136       }
1137
1138       /*
1139        * highlight search Results once sequence has been drawn
1140        */
1141       if (av.hasSearchResults())
1142       {
1143         SearchResultsI searchResults = av.getSearchResults();
1144         int[] visibleResults = searchResults.getResults(nextSeq,
1145                 startRes, endRes);
1146         if (visibleResults != null)
1147         {
1148           for (int r = 0; r < visibleResults.length; r += 2)
1149           {
1150             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
1151                     visibleResults[r + 1], (visibleResults[r] - startRes)
1152                             * charWidth, offset
1153                             + ((i - startSeq) * charHeight));
1154           }
1155         }
1156       }
1157
1158       if (av.cursorMode && cursorY == i && cursorX >= startRes
1159               && cursorX <= endRes)
1160       {
1161         seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
1162                 offset + ((i - startSeq) * charHeight));
1163       }
1164     }
1165
1166     if (av.getSelectionGroup() != null
1167             || av.getAlignment().getGroups().size() > 0)
1168     {
1169       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1170     }
1171
1172   }
1173
1174   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1175           int startSeq, int endSeq, int offset)
1176   {
1177     Graphics2D g = (Graphics2D) g1;
1178     //
1179     // ///////////////////////////////////
1180     // Now outline any areas if necessary
1181     // ///////////////////////////////////
1182
1183     SequenceGroup group = null;
1184     int groupIndex = -1;
1185
1186     if (av.getAlignment().getGroups().size() > 0)
1187     {
1188       group = av.getAlignment().getGroups().get(0);
1189       groupIndex = 0;
1190     }
1191
1192     if (group != null)
1193     {
1194       g.setStroke(new BasicStroke());
1195       g.setColor(group.getOutlineColour());
1196       
1197       do
1198       {
1199         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1200                 endSeq, offset);
1201
1202         groupIndex++;
1203
1204         g.setStroke(new BasicStroke());
1205
1206         if (groupIndex >= av.getAlignment().getGroups().size())
1207         {
1208           break;
1209         }
1210
1211         group = av.getAlignment().getGroups().get(groupIndex);
1212
1213       } while (groupIndex < av.getAlignment().getGroups().size());
1214
1215     }
1216
1217   }
1218
1219
1220   /*
1221    * Draw the selection group as a separate image and overlay
1222    */
1223   private BufferedImage drawSelectionGroup(int startRes, int endRes,
1224           int startSeq, int endSeq)
1225   {
1226     // get a new image of the correct size
1227     BufferedImage selectionImage = setupImage();
1228
1229     if (selectionImage == null)
1230     {
1231       return null;
1232     }
1233
1234     SequenceGroup group = av.getSelectionGroup();
1235     if (group == null)
1236     {
1237       // nothing to draw
1238       return null;
1239     }
1240
1241     // set up drawing colour
1242     Graphics2D g = (Graphics2D) selectionImage.getGraphics();
1243
1244     setupSelectionGroup(g, selectionImage);
1245
1246     if (!av.getWrapAlignment())
1247     {
1248       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1249               0);
1250     }
1251     else
1252     {
1253       drawWrappedSelection(g, group, getWidth(), getHeight(),
1254               av.getRanges().getStartRes());
1255     }
1256
1257     g.dispose();
1258     return selectionImage;
1259   }
1260
1261   /*
1262    * Set up graphics for selection group
1263    */
1264   private void setupSelectionGroup(Graphics2D g,
1265           BufferedImage selectionImage)
1266   {
1267     // set background to transparent
1268     g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1269     g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
1270
1271     // set up foreground to draw red dashed line
1272     g.setComposite(AlphaComposite.Src);
1273     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1274             BasicStroke.JOIN_ROUND, 3f, new float[]
1275     { 5f, 3f }, 0f));
1276     g.setColor(Color.RED);
1277   }
1278
1279   /*
1280    * Draw a selection group over an unwrapped alignment
1281    * @param g graphics object to draw with
1282    * @param group selection group
1283    * @param startRes start residue of area to draw
1284    * @param endRes end residue of area to draw
1285    * @param startSeq start sequence of area to draw
1286    * @param endSeq end sequence of area to draw
1287    * @param offset vertical offset (used when called from wrapped alignment code)
1288    */
1289   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1290           int startRes, int endRes, int startSeq, int endSeq, int offset)
1291   {
1292         int charWidth = av.getCharWidth();
1293           
1294     if (!av.hasHiddenColumns())
1295     {
1296       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
1297               offset);
1298     }
1299     else
1300     {
1301       // package into blocks of visible columns
1302       int screenY = 0;
1303       int blockStart = startRes;
1304       int blockEnd = endRes;
1305
1306       for (int[] region : av.getAlignment().getHiddenColumns()
1307               .getHiddenColumnsCopy())
1308       {
1309         int hideStart = region[0];
1310         int hideEnd = region[1];
1311
1312         if (hideStart <= blockStart)
1313         {
1314           blockStart += (hideEnd - hideStart) + 1;
1315           continue;
1316         }
1317
1318         blockEnd = hideStart - 1;
1319
1320         g.translate(screenY * charWidth, 0);
1321         drawPartialGroupOutline(g, group,
1322                 blockStart, blockEnd, startSeq, endSeq, offset);
1323
1324         g.translate(-screenY * charWidth, 0);
1325         screenY += blockEnd - blockStart + 1;
1326         blockStart = hideEnd + 1;
1327
1328         if (screenY > (endRes - startRes))
1329         {
1330           // already rendered last block
1331           break;
1332         }
1333       }
1334
1335       if (screenY <= (endRes - startRes))
1336       {
1337         // remaining visible region to render
1338         blockEnd = blockStart + (endRes - startRes) - screenY;
1339         g.translate(screenY * charWidth, 0);
1340         drawPartialGroupOutline(g, group,
1341                 blockStart, blockEnd, startSeq, endSeq, offset);
1342         
1343         g.translate(-screenY * charWidth, 0);
1344       }
1345     }
1346   }
1347
1348   /*
1349    * Draw the selection group as a separate image and overlay
1350    */
1351   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
1352           int startRes, int endRes, int startSeq, int endSeq,
1353           int verticalOffset)
1354   {
1355         int charHeight = av.getCharHeight();
1356         int charWidth = av.getCharWidth();
1357           
1358     int visWidth = (endRes - startRes + 1) * charWidth;
1359
1360     int oldY = -1;
1361     int i = 0;
1362     boolean inGroup = false;
1363     int top = -1;
1364     int bottom = -1;
1365
1366     int sx = -1;
1367     int sy = -1;
1368     int xwidth = -1;
1369
1370     for (i = startSeq; i <= endSeq; i++)
1371     {
1372       // position of start residue of group relative to startRes, in pixels
1373       sx = (group.getStartRes() - startRes) * charWidth;
1374
1375       // width of group in pixels
1376       xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1377               - 1;
1378
1379       sy = verticalOffset + (i - startSeq) * charHeight;
1380
1381       if (sx + xwidth < 0 || sx > visWidth)
1382       {
1383         continue;
1384       }
1385
1386       if ((sx <= (endRes - startRes) * charWidth)
1387               && group.getSequences(null)
1388                       .contains(av.getAlignment().getSequenceAt(i)))
1389       {
1390         if ((bottom == -1) && !group.getSequences(null)
1391                 .contains(av.getAlignment().getSequenceAt(i + 1)))
1392         {
1393           bottom = sy + charHeight;
1394         }
1395
1396         if (!inGroup)
1397         {
1398           if (((top == -1) && (i == 0)) || !group.getSequences(null)
1399                   .contains(av.getAlignment().getSequenceAt(i - 1)))
1400           {
1401             top = sy;
1402           }
1403
1404           oldY = sy;
1405           inGroup = true;
1406         }
1407       }
1408       else
1409       {
1410         if (inGroup)
1411         {
1412           // if start position is visible, draw vertical line to left of
1413           // group
1414           if (sx >= 0 && sx < visWidth)
1415           {
1416             g.drawLine(sx, oldY, sx, sy);
1417           }
1418
1419           // if end position is visible, draw vertical line to right of
1420           // group
1421           if (sx + xwidth < visWidth)
1422           {
1423             g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1424           }
1425
1426           if (sx < 0)
1427           {
1428             xwidth += sx;
1429             sx = 0;
1430           }
1431
1432           // don't let width extend beyond current block, or group extent
1433           // fixes JAL-2672
1434           if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1435           {
1436             xwidth = (endRes - startRes + 1) * charWidth - sx;
1437           }
1438           
1439           // draw horizontal line at top of group
1440           if (top != -1)
1441           {
1442             g.drawLine(sx, top, sx + xwidth, top);
1443             top = -1;
1444           }
1445
1446           // draw horizontal line at bottom of group
1447           if (bottom != -1)
1448           {
1449             g.drawLine(sx, bottom, sx + xwidth, bottom);
1450             bottom = -1;
1451           }
1452
1453           inGroup = false;
1454         }
1455       }
1456     }
1457
1458     if (inGroup)
1459     {
1460       sy = verticalOffset + ((i - startSeq) * charHeight);
1461       if (sx >= 0 && sx < visWidth)
1462       {
1463         g.drawLine(sx, oldY, sx, sy);
1464       }
1465
1466       if (sx + xwidth < visWidth)
1467       {
1468         g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1469       }
1470
1471       if (sx < 0)
1472       {
1473         xwidth += sx;
1474         sx = 0;
1475       }
1476
1477       if (sx + xwidth > visWidth)
1478       {
1479         xwidth = visWidth;
1480       }
1481       else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
1482       {
1483         xwidth = (endRes - startRes + 1) * charWidth;
1484       }
1485
1486       if (top != -1)
1487       {
1488         g.drawLine(sx, top, sx + xwidth, top);
1489         top = -1;
1490       }
1491
1492       if (bottom != -1)
1493       {
1494         g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
1495         bottom = -1;
1496       }
1497
1498       inGroup = false;
1499     }
1500   }
1501   
1502   /**
1503    * Highlights search results in the visible region by rendering as white text
1504    * on a black background. Any previous highlighting is removed. Answers true
1505    * if any highlight was left on the visible alignment (so status bar should be
1506    * set to match), else false.
1507    * <p>
1508    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1509    * alignment had to be scrolled to show the highlighted region, then it should
1510    * be fully redrawn, otherwise a fast paint can be performed. This argument
1511    * could be removed if fast paint of scrolled wrapped alignment is coded in
1512    * future (JAL-2609).
1513    * 
1514    * @param results
1515    * @param noFastPaint
1516    * @return
1517    */
1518   public boolean highlightSearchResults(SearchResultsI results,
1519           boolean noFastPaint)
1520   {
1521     if (fastpainting)
1522     {
1523       return false;
1524     }
1525     boolean wrapped = av.getWrapAlignment();
1526     try
1527     {
1528       fastPaint = !noFastPaint;
1529       fastpainting = fastPaint;
1530
1531       /*
1532        * to avoid redrawing the whole visible region, we instead
1533        * redraw just the minimal regions to remove previous highlights
1534        * and add new ones
1535        */
1536       SearchResultsI previous = av.getSearchResults();
1537       av.setSearchResults(results);
1538       boolean redrawn = false;
1539       boolean drawn = false;
1540       if (wrapped)
1541       {
1542         redrawn = drawMappedPositionsWrapped(previous);
1543         drawn = drawMappedPositionsWrapped(results);
1544         redrawn |= drawn;
1545       }
1546       else
1547       {
1548         redrawn = drawMappedPositions(previous);
1549         drawn = drawMappedPositions(results);
1550         redrawn |= drawn;
1551       }
1552
1553       /*
1554        * if highlights were either removed or added, repaint
1555        */
1556       if (redrawn)
1557       {
1558         repaint();
1559       }
1560
1561       /*
1562        * return true only if highlights were added
1563        */
1564       return drawn;
1565
1566     } finally
1567     {
1568       fastpainting = false;
1569     }
1570   }
1571
1572   /**
1573    * Redraws the minimal rectangle in the visible region (if any) that includes
1574    * mapped positions of the given search results. Whether or not positions are
1575    * highlighted depends on the SearchResults set on the Viewport. This allows
1576    * this method to be called to either clear or set highlighting. Answers true
1577    * if any positions were drawn (in which case a repaint is still required),
1578    * else false.
1579    * 
1580    * @param results
1581    * @return
1582    */
1583   protected boolean drawMappedPositions(SearchResultsI results)
1584   {
1585     if (results == null)
1586     {
1587       return false;
1588     }
1589
1590     /*
1591      * calculate the minimal rectangle to redraw that 
1592      * includes both new and existing search results
1593      */
1594     int firstSeq = Integer.MAX_VALUE;
1595     int lastSeq = -1;
1596     int firstCol = Integer.MAX_VALUE;
1597     int lastCol = -1;
1598     boolean matchFound = false;
1599
1600     ViewportRanges ranges = av.getRanges();
1601     int firstVisibleColumn = ranges.getStartRes();
1602     int lastVisibleColumn = ranges.getEndRes();
1603     AlignmentI alignment = av.getAlignment();
1604     if (av.hasHiddenColumns())
1605     {
1606       firstVisibleColumn = alignment.getHiddenColumns()
1607               .adjustForHiddenColumns(firstVisibleColumn);
1608       lastVisibleColumn = alignment.getHiddenColumns()
1609               .adjustForHiddenColumns(lastVisibleColumn);
1610     }
1611
1612     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1613             .getEndSeq(); seqNo++)
1614     {
1615       SequenceI seq = alignment.getSequenceAt(seqNo);
1616
1617       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1618               lastVisibleColumn);
1619       if (visibleResults != null)
1620       {
1621         for (int i = 0; i < visibleResults.length - 1; i += 2)
1622         {
1623           int firstMatchedColumn = visibleResults[i];
1624           int lastMatchedColumn = visibleResults[i + 1];
1625           if (firstMatchedColumn <= lastVisibleColumn
1626                   && lastMatchedColumn >= firstVisibleColumn)
1627           {
1628             /*
1629              * found a search results match in the visible region - 
1630              * remember the first and last sequence matched, and the first
1631              * and last visible columns in the matched positions
1632              */
1633             matchFound = true;
1634             firstSeq = Math.min(firstSeq, seqNo);
1635             lastSeq = Math.max(lastSeq, seqNo);
1636             firstMatchedColumn = Math.max(firstMatchedColumn,
1637                     firstVisibleColumn);
1638             lastMatchedColumn = Math.min(lastMatchedColumn,
1639                     lastVisibleColumn);
1640             firstCol = Math.min(firstCol, firstMatchedColumn);
1641             lastCol = Math.max(lastCol, lastMatchedColumn);
1642           }
1643         }
1644       }
1645     }
1646
1647     if (matchFound)
1648     {
1649       if (av.hasHiddenColumns())
1650       {
1651         firstCol = alignment.getHiddenColumns()
1652                 .findColumnPosition(firstCol);
1653         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1654       }
1655       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1656       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1657       gg.translate(transX, transY);
1658       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1659       gg.translate(-transX, -transY);
1660     }
1661
1662     return matchFound;
1663   }
1664
1665   @Override
1666   public void propertyChange(PropertyChangeEvent evt)
1667   {
1668     String eventName = evt.getPropertyName();
1669
1670     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
1671     {
1672       fastPaint = true;
1673       repaint();
1674     }
1675     else if (eventName.equals(ViewportRanges.STARTRES))
1676     {
1677       int scrollX = 0;
1678       if (eventName.equals(ViewportRanges.STARTRES))
1679       {
1680         // Make sure we're not trying to draw a panel
1681         // larger than the visible window
1682         ViewportRanges vpRanges = av.getRanges();
1683         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1684         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1685         if (scrollX > range)
1686         {
1687           scrollX = range;
1688         }
1689         else if (scrollX < -range)
1690         {
1691           scrollX = -range;
1692         }
1693
1694         // Both scrolling and resizing change viewport ranges: scrolling changes
1695         // both start and end points, but resize only changes end values.
1696         // Here we only want to fastpaint on a scroll, with resize using a normal
1697         // paint, so scroll events are identified as changes to the horizontal or
1698         // vertical start value.
1699         
1700         // scroll - startres and endres both change
1701           if (av.getWrapAlignment())
1702         {
1703           fastPaintWrapped(scrollX);
1704         }
1705         else
1706         {
1707           fastPaint(scrollX, 0);
1708         }
1709       }
1710       else if (eventName.equals(ViewportRanges.STARTSEQ))
1711       {
1712         // scroll
1713         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1714       }
1715     }
1716   }
1717
1718   /**
1719    * Does a minimal update of the image for a scroll movement. This method
1720    * handles scroll movements of up to one width of the wrapped alignment (one
1721    * click in the vertical scrollbar). Larger movements (for example after a
1722    * scroll to highlight a mapped position) trigger a full redraw instead.
1723    * 
1724    * @param scrollX
1725    *          number of positions scrolled (right if positive, left if negative)
1726    */
1727   protected void fastPaintWrapped(int scrollX)
1728   {
1729     ViewportRanges ranges = av.getRanges();
1730
1731     if (Math.abs(scrollX) > ranges.getViewportWidth())
1732     {
1733       /*
1734        * shift of more than one view width is 
1735        * overcomplicated to handle in this method
1736        */
1737       fastPaint = false;
1738       repaint();
1739       return;
1740     }
1741
1742     if (fastpainting || gg == null)
1743     {
1744       return;
1745     }
1746
1747     fastPaint = true;
1748     fastpainting = true;
1749
1750     try
1751     {
1752       calculateWrappedGeometry(getWidth(), getHeight());
1753
1754       /*
1755        * relocate the regions of the alignment that are still visible
1756        */
1757       shiftWrappedAlignment(-scrollX);
1758
1759       /*
1760        * add new columns (sequence, annotation)
1761        * - at top left if scrollX < 0 
1762        * - at right of last two widths if scrollX > 0
1763        */
1764       if (scrollX < 0)
1765       {
1766         int startRes = ranges.getStartRes();
1767         drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
1768                 - scrollX - 1, getHeight());
1769       }
1770       else
1771       {
1772         fastPaintWrappedAddRight(scrollX);
1773       }
1774
1775       /*
1776        * draw all scales (if  shown) and hidden column markers
1777        */
1778       drawWrappedDecorators(gg, ranges.getStartRes());
1779
1780       repaint();
1781     } finally
1782     {
1783       fastpainting = false;
1784     }
1785   }
1786
1787   /**
1788    * Draws the specified number of columns at the 'end' (bottom right) of a
1789    * wrapped alignment view, including sequences and annotations if shown, but
1790    * not scales. Also draws the same number of columns at the right hand end of
1791    * the second last width shown, if the last width is not full height (so
1792    * cannot simply be copied from the graphics image).
1793    * 
1794    * @param columns
1795    */
1796   protected void fastPaintWrappedAddRight(int columns)
1797   {
1798     if (columns == 0)
1799     {
1800       return;
1801     }
1802
1803     ViewportRanges ranges = av.getRanges();
1804     int viewportWidth = ranges.getViewportWidth();
1805     int charWidth = av.getCharWidth();
1806
1807     /**
1808      * draw full height alignment in the second last row, last columns, if the
1809      * last row was not full height
1810      */
1811     int visibleWidths = wrappedVisibleWidths;
1812     int canvasHeight = getHeight();
1813     boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
1814
1815     if (lastWidthPartHeight)
1816     {
1817       int widthsAbove = Math.max(0, visibleWidths - 2);
1818       int ypos = wrappedRepeatHeightPx * widthsAbove
1819               + wrappedSpaceAboveAlignment;
1820       int endRes = ranges.getEndRes();
1821       endRes += widthsAbove * viewportWidth;
1822       int startRes = endRes - columns;
1823       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1824               * charWidth;
1825
1826       /*
1827        * white fill first to erase annotations
1828        */
1829       gg.translate(xOffset, 0);
1830       gg.setColor(Color.white);
1831       gg.fillRect(labelWidthWest, ypos,
1832               (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
1833       gg.translate(-xOffset, 0);
1834
1835       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1836     }
1837
1838     /*
1839      * draw newly visible columns in last wrapped width (none if we
1840      * have reached the end of the alignment)
1841      * y-offset for drawing last width is height of widths above,
1842      * plus one gap row
1843      */
1844     int widthsAbove = visibleWidths - 1;
1845     int ypos = wrappedRepeatHeightPx * widthsAbove
1846             + wrappedSpaceAboveAlignment;
1847     int endRes = ranges.getEndRes();
1848     endRes += widthsAbove * viewportWidth;
1849     int startRes = endRes - columns + 1;
1850
1851     /*
1852      * white fill first to erase annotations
1853      */
1854     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1855             * charWidth;
1856     gg.translate(xOffset, 0);
1857     gg.setColor(Color.white);
1858     int width = viewportWidth * charWidth - xOffset;
1859     gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
1860     gg.translate(-xOffset, 0);
1861
1862     gg.setFont(av.getFont());
1863     gg.setColor(Color.black);
1864
1865     if (startRes < ranges.getVisibleAlignmentWidth())
1866     {
1867       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1868     }
1869
1870     /*
1871      * and finally, white fill any space below the visible alignment
1872      */
1873     int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
1874     if (heightBelow > 0)
1875     {
1876       gg.setColor(Color.white);
1877       gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
1878     }
1879   }
1880
1881   /**
1882    * Shifts the visible alignment by the specified number of columns - left if
1883    * negative, right if positive. Copies and moves sequences and annotations (if
1884    * shown). Scales, hidden column markers and any newly visible columns must be
1885    * drawn separately.
1886    * 
1887    * @param positions
1888    */
1889   protected void shiftWrappedAlignment(int positions)
1890   {
1891     if (positions == 0)
1892     {
1893       return;
1894     }
1895     int charWidth = av.getCharWidth();
1896
1897     int canvasHeight = getHeight();
1898     ViewportRanges ranges = av.getRanges();
1899     int viewportWidth = ranges.getViewportWidth();
1900     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
1901             * charWidth;
1902     int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
1903     int xMax = ranges.getVisibleAlignmentWidth();
1904
1905     if (positions > 0)
1906     {
1907       /*
1908        * shift right (after scroll left)
1909        * for each wrapped width (starting with the last), copy (width-positions) 
1910        * columns from the left margin to the right margin, and copy positions 
1911        * columns from the right margin of the row above (if any) to the 
1912        * left margin of the current row
1913        */
1914
1915       /*
1916        * get y-offset of last wrapped width, first row of sequences
1917        */
1918       int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
1919       y += wrappedSpaceAboveAlignment;
1920       int copyFromLeftStart = labelWidthWest;
1921       int copyFromRightStart = copyFromLeftStart + widthToCopy;
1922
1923       while (y >= 0)
1924       {
1925         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
1926                 positions * charWidth, 0);
1927         if (y > 0)
1928         {
1929           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
1930                   positions * charWidth, heightToCopy, -widthToCopy,
1931                   wrappedRepeatHeightPx);
1932         }
1933
1934         y -= wrappedRepeatHeightPx;
1935       }
1936     }
1937     else
1938     {
1939       /*
1940        * shift left (after scroll right)
1941        * for each wrapped width (starting with the first), copy (width-positions) 
1942        * columns from the right margin to the left margin, and copy positions 
1943        * columns from the left margin of the row below (if any) to the 
1944        * right margin of the current row
1945        */
1946       int xpos = av.getRanges().getStartRes();
1947       int y = wrappedSpaceAboveAlignment;
1948       int copyFromRightStart = labelWidthWest - positions * charWidth;
1949
1950       while (y < canvasHeight)
1951       {
1952         gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
1953                 positions * charWidth, 0);
1954         if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
1955                 && (xpos + viewportWidth <= xMax))
1956         {
1957           gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
1958                   * charWidth, heightToCopy, widthToCopy,
1959                   -wrappedRepeatHeightPx);
1960         }
1961
1962         y += wrappedRepeatHeightPx;
1963         xpos += viewportWidth;
1964       }
1965     }
1966   }
1967
1968   
1969   /**
1970    * Redraws any positions in the search results in the visible region of a
1971    * wrapped alignment. Any highlights are drawn depending on the search results
1972    * set on the Viewport, not the <code>results</code> argument. This allows
1973    * this method to be called either to clear highlights (passing the previous
1974    * search results), or to draw new highlights.
1975    * 
1976    * @param results
1977    * @return
1978    */
1979   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1980   {
1981     if (results == null)
1982     {
1983       return false;
1984     }
1985     int charHeight = av.getCharHeight();
1986
1987     boolean matchFound = false;
1988
1989     calculateWrappedGeometry(getWidth(), getHeight());
1990     int wrappedWidth = av.getWrappedWidth();
1991     int wrappedHeight = wrappedRepeatHeightPx;
1992
1993     ViewportRanges ranges = av.getRanges();
1994     int canvasHeight = getHeight();
1995     int repeats = canvasHeight / wrappedHeight;
1996     if (canvasHeight / wrappedHeight > 0)
1997     {
1998       repeats++;
1999     }
2000
2001     int firstVisibleColumn = ranges.getStartRes();
2002     int lastVisibleColumn = ranges.getStartRes() + repeats
2003             * ranges.getViewportWidth() - 1;
2004
2005     AlignmentI alignment = av.getAlignment();
2006     if (av.hasHiddenColumns())
2007     {
2008       firstVisibleColumn = alignment.getHiddenColumns()
2009               .adjustForHiddenColumns(firstVisibleColumn);
2010       lastVisibleColumn = alignment.getHiddenColumns()
2011               .adjustForHiddenColumns(lastVisibleColumn);
2012     }
2013
2014     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
2015
2016     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
2017             .getEndSeq(); seqNo++)
2018     {
2019       SequenceI seq = alignment.getSequenceAt(seqNo);
2020
2021       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
2022               lastVisibleColumn);
2023       if (visibleResults != null)
2024       {
2025         for (int i = 0; i < visibleResults.length - 1; i += 2)
2026         {
2027           int firstMatchedColumn = visibleResults[i];
2028           int lastMatchedColumn = visibleResults[i + 1];
2029           if (firstMatchedColumn <= lastVisibleColumn
2030                   && lastMatchedColumn >= firstVisibleColumn)
2031           {
2032             /*
2033              * found a search results match in the visible region
2034              */
2035             firstMatchedColumn = Math.max(firstMatchedColumn,
2036                     firstVisibleColumn);
2037             lastMatchedColumn = Math.min(lastMatchedColumn,
2038                     lastVisibleColumn);
2039
2040             /*
2041              * draw each mapped position separately (as contiguous positions may
2042              * wrap across lines)
2043              */
2044             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
2045             {
2046               int displayColumn = mappedPos;
2047               if (av.hasHiddenColumns())
2048               {
2049                 displayColumn = alignment.getHiddenColumns()
2050                         .findColumnPosition(displayColumn);
2051               }
2052
2053               /*
2054                * transX: offset from left edge of canvas to residue position
2055                */
2056               int transX = labelWidthWest
2057                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
2058                       * av.getCharWidth();
2059
2060               /*
2061                * transY: offset from top edge of canvas to residue position
2062                */
2063               int transY = gapHeight;
2064               transY += (displayColumn - ranges.getStartRes())
2065                       / wrappedWidth * wrappedHeight;
2066               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
2067
2068               /*
2069                * yOffset is from graphics origin to start of visible region
2070                */
2071               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
2072               if (transY < getHeight())
2073               {
2074                 matchFound = true;
2075                 gg.translate(transX, transY);
2076                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
2077                         yOffset);
2078                 gg.translate(-transX, -transY);
2079               }
2080             }
2081           }
2082         }
2083       }
2084     }
2085   
2086     return matchFound;
2087   }
2088 }