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