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