pushing again, just comments
[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.Rectangle;
42 import java.awt.RenderingHints;
43 import java.awt.image.BufferedImage;
44 import java.beans.PropertyChangeEvent;
45 import java.util.Iterator;
46 import java.util.List;
47
48 import javax.swing.JPanel;
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    * pixels gap between sequences and annotations when in wrapped mode
61    */
62   static final int SEQS_ANNOTATION_GAP = 3;
63
64   private static final String ZEROS = "0000000000";
65
66   final FeatureRenderer fr;
67
68   BufferedImage img;
69
70   AlignViewport av;
71
72   int cursorX = 0;
73
74   int cursorY = 0;
75
76   private final SequenceRenderer seqRdr;
77
78   private boolean fastPaint = false;
79
80   private boolean fastpainting = false;
81
82   private AnnotationPanel annotations;
83
84   /*
85    * measurements for drawing a wrapped alignment
86    */
87   private int labelWidthEast; // label right width in pixels if shown
88
89   private int labelWidthWest; // label left width in pixels if shown
90
91   int wrappedSpaceAboveAlignment; // gap between widths
92
93   int wrappedRepeatHeightPx; // height in pixels of wrapped width
94
95   private int wrappedVisibleWidths; // number of wrapped widths displayed
96
97   // Don't do this! Graphics handles are supposed to be transient
98   //private Graphics2D gg;
99
100   /**
101    * Creates a new SeqCanvas object.
102    * 
103    * @param ap
104    */
105   public SeqCanvas(AlignmentPanel ap)
106   {
107     this.av = ap.av;
108     fr = new FeatureRenderer(ap);
109     seqRdr = new SequenceRenderer(av);
110     setLayout(new BorderLayout());
111     PaintRefresher.Register(this, av.getSequenceSetId());
112     setBackground(Color.white);
113
114     av.getRanges().addPropertyChangeListener(this);
115   }
116
117   public SequenceRenderer getSequenceRenderer()
118   {
119     return seqRdr; 
120   }
121
122   public FeatureRenderer getFeatureRenderer()
123   {
124     return fr;
125   }
126
127   /**
128    * Draws the scale above a region of a wrapped alignment, consisting of a
129    * column number every major interval (10 columns).
130    * 
131    * @param g
132    *          the graphics context to draw on, positioned at the start (bottom
133    *          left) of the line on which to draw any scale marks
134    * @param startx
135    *          start alignment column (0..)
136    * @param endx
137    *          end alignment column (0..)
138    * @param ypos
139    *          y offset to draw at
140    */
141   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
142   {
143     int charHeight = av.getCharHeight();
144     int charWidth = av.getCharWidth();
145
146     /*
147      * white fill the scale space (for the fastPaint case)
148      */
149     g.setColor(Color.white);
150     g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
151             charHeight * 3 / 2 + 2);
152     g.setColor(Color.black);
153
154     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
155             endx);
156     for (ScaleMark mark : marks)
157     {
158       int mpos = mark.column; // (i - startx - 1)
159       if (mpos < 0)
160       {
161         continue;
162       }
163       String mstring = mark.text;
164
165       if (mark.major)
166       {
167         if (mstring != null)
168         {
169           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
170         }
171
172         /*
173          * draw a tick mark below the column number, centred on the column;
174          * height of tick mark is 4 pixels less than half a character
175          */
176         int xpos = (mpos * charWidth) + (charWidth / 2);
177         g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
178       }
179     }
180   }
181
182   /**
183    * Draw the scale to the left or right of a wrapped alignment
184    * 
185    * @param g
186    *          graphics context, positioned at the start of the scale to be drawn
187    * @param startx
188    *          first column of wrapped width (0.. excluding any hidden columns)
189    * @param endx
190    *          last column of wrapped width (0.. excluding any hidden columns)
191    * @param ypos
192    *          vertical offset at which to begin the scale
193    * @param left
194    *          if true, scale is left of residues, if false, scale is right
195    */
196   void drawVerticalScale(Graphics g, final int startx, final int endx,
197           final int ypos, final boolean left)
198   {
199     int charHeight = av.getCharHeight();
200     int charWidth = av.getCharWidth();
201
202     int yPos = ypos + charHeight;
203     int startX = startx;
204     int endX = endx;
205     
206     if (av.hasHiddenColumns())
207     {
208       HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
209       startX = hiddenColumns.visibleToAbsoluteColumn(startx);
210       endX = hiddenColumns.visibleToAbsoluteColumn(endx);
211     }
212     FontMetrics fm = getFontMetrics(av.getFont());
213
214     for (int i = 0; i < av.getAlignment().getHeight(); i++)
215     {
216       SequenceI seq = av.getAlignment().getSequenceAt(i);
217
218       /*
219        * find sequence position of first non-gapped position -
220        * to the right if scale left, to the left if scale right
221        */
222       int index = left ? startX : endX;
223       int value = -1;
224       while (index >= startX && index <= endX)
225       {
226         if (!Comparison.isGap(seq.getCharAt(index)))
227         {
228           value = seq.findPosition(index);
229           break;
230         }
231         if (left)
232         {
233           index++;
234         }
235         else
236         {
237           index--;
238         }
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     // System.err.println("<<SeqCanvas fastPaint " + fastpainting + " "
291     // + horizontal + " " + vertical);
292     // effectively:
293     // if (horizontal != 0 && vertical != 0)
294     // throw new InvalidArgumentException();
295     if (fastpainting || img == null)
296     {
297       return;
298     }
299     fastpainting = true;
300     fastPaint = true;
301     try
302     {
303       int charHeight = av.getCharHeight();
304       int charWidth = av.getCharWidth();
305
306       ViewportRanges ranges = av.getRanges();
307       int startRes = ranges.getStartRes();
308       int endRes = ranges.getEndRes();
309       int startSeq = ranges.getStartSeq();
310       int endSeq = ranges.getEndSeq();
311       int transX = 0;
312       int transY = 0;
313
314       if (horizontal > 0) // scrollbar pulled right, image to the left
315       {
316         transX = (endRes - startRes - horizontal) * charWidth;
317         startRes = endRes - horizontal;
318       }
319       else if (horizontal < 0)
320       {
321         endRes = startRes - horizontal;
322       }
323
324       if (vertical > 0) // scroll down
325       {
326         startSeq = endSeq - vertical;
327
328         if (startSeq < ranges.getStartSeq())
329         { // ie scrolling too fast, more than a page at a time
330           startSeq = ranges.getStartSeq();
331         }
332         else
333         {
334           transY = img.getHeight() - ((vertical + 1) * charHeight);
335         }
336       }
337       else if (vertical < 0)
338       {
339         endSeq = startSeq - vertical;
340
341         if (endSeq > ranges.getEndSeq())
342         {
343           endSeq = ranges.getEndSeq();
344         }
345       }
346
347
348       // System.err.println(">>> FastPaint to " + transX + " " + transY + " "
349       // + horizontal + " " + vertical + " " + startRes + " " + endRes
350       // + " " + startSeq + " " + endSeq);
351
352       Graphics gg = img.getGraphics();
353       gg.copyArea(horizontal * charWidth, vertical * charHeight,
354               img.getWidth(), img.getHeight(), -horizontal * charWidth,
355               -vertical * charHeight);
356
357       /** @j2sNative xxi = this.img */
358
359       gg.translate(transX, transY);
360       drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
361       gg.translate(-transX, -transY);
362       gg.dispose();
363
364       // Call repaint on alignment panel so that repaints from other alignment
365       // panel components can be aggregated. Otherwise performance of the
366       // overview window and others may be adversely affected.
367       // System.out.println("SeqCanvas fastPaint() repaint() request...");
368       av.getAlignPanel().repaint();
369     } finally
370     {
371       fastpainting = false;
372     }
373   }
374
375   @Override
376   public void paintComponent(Graphics g)
377   {
378     int charHeight = av.getCharHeight();
379     int charWidth = av.getCharWidth();
380
381     int width = getWidth();
382     int height = getHeight();
383
384     width -= (width % charWidth);
385     height -= (height % charHeight);
386
387     // BH 2019 can't possibly fastPaint if width and height are 0
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     // System.err.println(">>SeqCanvas paintComponent " + fastPaint + "\n"
400     // + getVisibleRect() + "\n" + g.getClipBounds());
401     // System.err.println(">>>>>>>>>>>>>>>>SeqCanvas paintComponent "
402     // + startRes + " " + endRes + " " + startSeq + " " + endSeq);
403     Rectangle vis, clip;
404     if (img != null
405             && (fastPaint
406                     || (vis = getVisibleRect()).width != (clip = g
407                             .getClipBounds()).width
408                     || vis.height != clip.height))
409     {
410       g.drawImage(img, 0, 0, this);
411       // System.err.println(">>>>>>>>>>>>>>>>SeqCanvas paintComponent FAST");
412       drawSelectionGroup((Graphics2D) g, startRes, endRes, startSeq,
413               endSeq);
414       fastPaint = false;
415       //System.out.println("SeqCanvas fast paint");
416     }
417     else
418     {
419
420       System.out.println("SeqCanvas full paint");
421       /*
422        * img is a cached version of the last view we drew.
423        * If we have no img or the size has changed, make a new one
424        */
425       if (img == null || width != img.getWidth()
426               || height != img.getHeight())
427       {
428         img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
429       }
430       
431       Graphics2D gg = (Graphics2D) img.getGraphics();
432       gg.setFont(av.getFont());
433
434       if (av.antiAlias)
435       {
436         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
437                 RenderingHints.VALUE_ANTIALIAS_ON);
438       }
439
440       gg.setColor(Color.white);
441       gg.fillRect(0, 0, img.getWidth(), img.getHeight());
442
443       if (av.getWrapAlignment())
444       {
445         drawWrappedPanel(gg, width, height, ranges.getStartRes());
446       }
447       else
448       {
449         drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
450       }
451
452       drawSelectionGroup(gg, startRes, endRes, startSeq, endSeq);
453
454       g.drawImage(img, 0, 0, this);
455       gg.dispose();
456     }
457
458     if (av.cursorMode)
459     {
460       drawCursor(g, startRes, endRes, startSeq, endSeq);
461     }
462   }
463   
464   /**
465    * Draw an alignment panel for printing
466    * 
467    * @param g1
468    *          Graphics object to draw with
469    * @param startRes
470    *          start residue of print area
471    * @param endRes
472    *          end residue of print area
473    * @param startSeq
474    *          start sequence of print area
475    * @param endSeq
476    *          end sequence of print area
477    */
478   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
479           int startSeq, int endSeq)
480   {
481     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
482
483     drawSelectionGroup((Graphics2D) g1, startRes, endRes,
484             startSeq, endSeq);
485   }
486
487   /**
488    * Draw a wrapped alignment panel for printing
489    * 
490    * @param g
491    *          Graphics object to draw with
492    * @param canvasWidth
493    *          width of drawing area
494    * @param canvasHeight
495    *          height of drawing area
496    * @param startRes
497    *          start residue of print area
498    */
499   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
500           int canvasHeight, int startRes)
501   {
502     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
503
504     SequenceGroup group = av.getSelectionGroup();
505     if (group != null)
506     {
507       drawWrappedSelection((Graphics2D) g, group, canvasWidth, canvasHeight,
508                 startRes);
509     }
510   }
511
512   /**
513    * Returns the visible width of the canvas in residues, after allowing for
514    * East or West scales (if shown)
515    * 
516    * @param canvasWidth
517    *          the width in pixels (possibly including scales)
518    * 
519    * @return
520    */
521   public int getWrappedCanvasWidth(int canvasWidth)
522   {
523     int charWidth = av.getCharWidth();
524
525     FontMetrics fm = getFontMetrics(av.getFont());
526
527     int labelWidth = 0;
528     
529     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
530     {
531       labelWidth = getLabelWidth(fm);
532     }
533
534     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
535
536     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
537
538     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
539   }
540
541   /**
542    * Returns a pixel width sufficient to show the largest sequence coordinate
543    * (end position) in the alignment, calculated as the FontMetrics width of
544    * zeroes "0000000" limited to the number of decimal digits to be shown (3 for
545    * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for
546    * half a character width space on either side.
547    * 
548    * @param fm
549    * @return
550    */
551   protected int getLabelWidth(FontMetrics fm)
552   {
553     /*
554      * find the biggest sequence end position we need to show
555      * (note this is not necessarily the sequence length)
556      */
557     int maxWidth = 0;
558     AlignmentI alignment = av.getAlignment();
559     for (int i = 0; i < alignment.getHeight(); i++)
560     {
561       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
562     }
563
564     int length = 0;
565     for (int i = maxWidth; i > 0; i /= 10)
566     {
567       length++;
568     }
569
570     return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth();
571   }
572
573   /**
574    * Draws as many widths of a wrapped alignment as can fit in the visible
575    * window
576    * 
577    * @param g
578    * @param canvasWidth
579    *          available width in pixels
580    * @param canvasHeight
581    *          available height in pixels
582    * @param startColumn
583    *          the first column (0...) of the alignment to draw
584    */
585   public void drawWrappedPanel(Graphics g, int canvasWidth,
586           int canvasHeight, final int startColumn)
587   {
588     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
589             canvasHeight);
590
591     av.setWrappedWidth(wrappedWidthInResidues);
592
593     ViewportRanges ranges = av.getRanges();
594     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
595
596     // we need to call this again to make sure the startColumn +
597     // wrappedWidthInResidues values are used to calculate wrappedVisibleWidths
598     // correctly.
599     calculateWrappedGeometry(canvasWidth, canvasHeight);
600
601     /*
602      * draw one width at a time (excluding any scales shown),
603      * until we have run out of either alignment or vertical space available
604      */
605     int ypos = wrappedSpaceAboveAlignment;
606     int maxWidth = ranges.getVisibleAlignmentWidth();
607
608     int start = startColumn;
609     int currentWidth = 0;
610     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
611     {
612       int endColumn = Math
613               .min(maxWidth, start + wrappedWidthInResidues - 1);
614       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
615       ypos += wrappedRepeatHeightPx;
616       start += wrappedWidthInResidues;
617       currentWidth++;
618     }
619
620     drawWrappedDecorators(g, startColumn);
621   }
622
623   /**
624    * Calculates and saves values needed when rendering a wrapped alignment.
625    * These depend on many factors, including
626    * <ul>
627    * <li>canvas width and height</li>
628    * <li>number of visible sequences, and height of annotations if shown</li>
629    * <li>font and character width</li>
630    * <li>whether scales are shown left, right or above the alignment</li>
631    * </ul>
632    * 
633    * @param canvasWidth
634    * @param canvasHeight
635    * @return the number of residue columns in each width
636    */
637   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
638   {
639     int charHeight = av.getCharHeight();
640
641     /*
642      * vertical space in pixels between wrapped widths of alignment
643      * - one character height, or two if scale above is drawn
644      */
645     wrappedSpaceAboveAlignment = charHeight
646             * (av.getScaleAboveWrapped() ? 2 : 1);
647
648     /*
649      * compute height in pixels of the wrapped widths
650      * - start with space above plus sequences
651      */
652     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
653     wrappedRepeatHeightPx += av.getAlignment().getHeight()
654             * charHeight;
655
656     /*
657      * add annotations panel height if shown
658      * also gap between sequences and annotations
659      */
660     if (av.isShowAnnotation())
661     {
662       wrappedRepeatHeightPx += getAnnotationHeight();
663       wrappedRepeatHeightPx += SEQS_ANNOTATION_GAP; // 3px
664     }
665
666     /*
667      * number of visible widths (the last one may be part height),
668      * ensuring a part height includes at least one sequence
669      */
670     ViewportRanges ranges = av.getRanges();
671     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
672     int remainder = canvasHeight % wrappedRepeatHeightPx;
673     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
674     {
675       wrappedVisibleWidths++;
676     }
677
678     /*
679      * compute width in residues; this also sets East and West label widths
680      */
681     int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
682
683     /*
684      *  limit visibleWidths to not exceed width of alignment
685      */
686     int xMax = ranges.getVisibleAlignmentWidth();
687     int startToEnd = xMax - ranges.getStartRes();
688     int maxWidths = startToEnd / wrappedWidthInResidues;
689     if (startToEnd % wrappedWidthInResidues > 0)
690     {
691       maxWidths++;
692     }
693     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
694
695     return wrappedWidthInResidues;
696   }
697
698   /**
699    * Draws one width of a wrapped alignment, including sequences and
700    * annnotations, if shown, but not scales or hidden column markers
701    * 
702    * @param g
703    * @param ypos
704    * @param startColumn
705    * @param endColumn
706    * @param canvasHeight
707    */
708   protected void drawWrappedWidth(Graphics g, final int ypos,
709           final int startColumn, final int endColumn,
710           final int canvasHeight)
711   {
712     ViewportRanges ranges = av.getRanges();
713     int viewportWidth = ranges.getViewportWidth();
714
715     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
716
717     /*
718      * move right before drawing by the width of the scale left (if any)
719      * plus column offset from left margin (usually zero, but may be non-zero
720      * when fast painting is drawing just a few columns)
721      */
722     int charWidth = av.getCharWidth();
723     int xOffset = labelWidthWest
724             + ((startColumn - ranges.getStartRes()) % viewportWidth)
725             * charWidth;
726
727     g.translate(xOffset, 0);
728
729     /*
730      * white fill the region to be drawn (so incremental fast paint doesn't
731      * scribble over an existing image)
732      */
733     g.setColor(Color.white);
734     g.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
735             wrappedRepeatHeightPx);
736
737     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
738             ypos);
739
740     int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
741
742     if (av.isShowAnnotation())
743     {
744       final int yShift = cHeight + ypos + SEQS_ANNOTATION_GAP;
745       g.translate(0, yShift);
746       if (annotations == null)
747       {
748         annotations = new AnnotationPanel(av);
749       }
750
751       annotations.renderer.drawComponent(annotations, av, g, -1,
752               startColumn, endx + 1);
753       g.translate(0, -yShift);
754     }
755     g.translate(-xOffset, 0);
756   }
757
758   /**
759    * Draws scales left, right and above (if shown), and any hidden column
760    * markers, on all widths of the wrapped alignment
761    * 
762    * @param g
763    * @param startColumn
764    */
765   protected void drawWrappedDecorators(Graphics g, final int startColumn)
766   {
767     int charWidth = av.getCharWidth();
768
769     g.setFont(av.getFont());
770
771     g.setColor(Color.black);
772
773     int ypos = wrappedSpaceAboveAlignment;
774     ViewportRanges ranges = av.getRanges();
775     int viewportWidth = ranges.getViewportWidth();
776     int maxWidth = ranges.getVisibleAlignmentWidth();
777     int widthsDrawn = 0;
778     int startCol = startColumn;
779
780     while (widthsDrawn < wrappedVisibleWidths)
781     {
782       int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1);
783
784       if (av.getScaleLeftWrapped())
785       {
786         drawVerticalScale(g, startCol, endColumn - 1, ypos, true);
787       }
788
789       if (av.getScaleRightWrapped())
790       {
791         int x = labelWidthWest + viewportWidth * charWidth;
792         
793         g.translate(x, 0);
794         drawVerticalScale(g, startCol, endColumn, ypos, false);
795         g.translate(-x, 0);
796       }
797
798       /*
799        * white fill region of scale above and hidden column markers
800        * (to support incremental fast paint of image)
801        */
802       g.translate(labelWidthWest, 0);
803       g.setColor(Color.white);
804       g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
805               * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
806       g.setColor(Color.black);
807       g.translate(-labelWidthWest, 0);
808
809       g.translate(labelWidthWest, 0);
810
811       if (av.getScaleAboveWrapped())
812       {
813         drawNorthScale(g, startCol, endColumn, ypos);
814       }
815
816       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
817       {
818         drawHiddenColumnMarkers(g, ypos, startCol, endColumn);
819       }
820
821       g.translate(-labelWidthWest, 0);
822
823       ypos += wrappedRepeatHeightPx;
824       startCol += viewportWidth;
825       widthsDrawn++;
826     }
827   }
828
829   /**
830    * Draws markers (triangles) above hidden column positions between startColumn
831    * and endColumn.
832    * 
833    * @param g
834    * @param ypos
835    * @param startColumn
836    * @param endColumn
837    */
838   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
839           int startColumn, int endColumn)
840   {
841     int charHeight = av.getCharHeight();
842     int charWidth = av.getCharWidth();
843
844     g.setColor(Color.blue);
845     int res;
846     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
847
848     Iterator<Integer> it = hidden.getStartRegionIterator(startColumn,
849             endColumn);
850     while (it.hasNext())
851     {
852       res = it.next() - startColumn;
853
854       if (res < 0 || res > endColumn - startColumn + 1)
855       {
856         continue;
857       }
858
859       /*
860        * draw a downward-pointing triangle at the hidden columns location
861        * (before the following visible column)
862        */
863       int xMiddle = res * charWidth;
864       int[] xPoints = new int[] { xMiddle - charHeight / 4,
865           xMiddle + charHeight / 4, xMiddle };
866       int yTop = ypos - (charHeight / 2);
867       int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
868       g.fillPolygon(xPoints, yPoints, 3);
869     }
870   }
871
872   /*
873    * Draw a selection group over a wrapped alignment
874    */
875   private void drawWrappedSelection(Graphics2D g, SequenceGroup group,
876           int canvasWidth,
877           int canvasHeight, int startRes)
878   {
879     int charHeight = av.getCharHeight();
880     int charWidth = av.getCharWidth();
881       
882     // height gap above each panel
883     int hgap = charHeight;
884     if (av.getScaleAboveWrapped())
885     {
886       hgap += charHeight;
887     }
888
889     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
890             / charWidth;
891     int cHeight = av.getAlignment().getHeight() * charHeight;
892
893     int startx = startRes;
894     int endx;
895     int ypos = hgap; // vertical offset
896     int maxwidth = av.getAlignment().getVisibleWidth();
897
898     // chop the wrapped alignment extent up into panel-sized blocks and treat
899     // each block as if it were a block from an unwrapped alignment
900     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
901             BasicStroke.JOIN_ROUND, 3f, new float[]
902             { 5f, 3f }, 0f));
903     g.setColor(Color.RED);
904     while ((ypos <= canvasHeight) && (startx < maxwidth))
905     {
906       // set end value to be start + width, or maxwidth, whichever is smaller
907       endx = startx + cWidth - 1;
908
909       if (endx > maxwidth)
910       {
911         endx = maxwidth;
912       }
913
914       g.translate(labelWidthWest, 0);
915
916       drawUnwrappedSelection(g, group, startx, endx, 0,
917               av.getAlignment().getHeight() - 1,
918               ypos);
919
920       g.translate(-labelWidthWest, 0);
921
922       // update vertical offset
923       ypos += cHeight + getAnnotationHeight() + hgap;
924
925       // update horizontal offset
926       startx += cWidth;
927     }
928     g.setStroke(new BasicStroke());
929   }
930
931   int getAnnotationHeight()
932   {
933     if (!av.isShowAnnotation())
934     {
935       return 0;
936     }
937
938     if (annotations == null)
939     {
940       annotations = new AnnotationPanel(av);
941     }
942
943     return annotations.adjustPanelHeight();
944   }
945
946   /**
947    * Draws the visible region of the alignment on the graphics context. If there
948    * are hidden column markers in the visible region, then each sub-region
949    * between the markers is drawn separately, followed by the hidden column
950    * marker.
951    * 
952    * @param g1
953    *          the graphics context, positioned at the first residue to be drawn
954    * @param startRes
955    *          offset of the first column to draw (0..)
956    * @param endRes
957    *          offset of the last column to draw (0..)
958    * @param startSeq
959    *          offset of the first sequence to draw (0..)
960    * @param endSeq
961    *          offset of the last sequence to draw (0..)
962    * @param yOffset
963    *          vertical offset at which to draw (for wrapped alignments)
964    */
965   public void drawPanel(Graphics g1, final int startRes, final int endRes,
966           final int startSeq, final int endSeq, final int yOffset)
967   {
968     int charHeight = av.getCharHeight();
969     int charWidth = av.getCharWidth();
970
971     if (!av.hasHiddenColumns())
972     {
973       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
974     }
975     else
976     {
977       int screenY = 0;
978       int blockStart;
979       int blockEnd;
980
981       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
982       VisibleContigsIterator regions = hidden
983               .getVisContigsIterator(startRes, endRes + 1, true);
984
985       while (regions.hasNext())
986       {
987         int[] region = regions.next();
988         blockEnd = region[1];
989         blockStart = region[0];
990
991         /*
992          * draw up to just before the next hidden region, or the end of
993          * the visible region, whichever comes first
994          */
995         g1.translate(screenY * charWidth, 0);
996
997         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
998
999         /*
1000          * draw the downline of the hidden column marker (ScalePanel draws the
1001          * triangle on top) if we reached it
1002          */
1003         if (av.getShowHiddenMarkers()
1004                 && (regions.hasNext() || regions.endsAtHidden()))
1005         {
1006           g1.setColor(Color.blue);
1007
1008           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
1009                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
1010                   (endSeq - startSeq + 1) * charHeight + yOffset);
1011         }
1012
1013         g1.translate(-screenY * charWidth, 0);
1014         screenY += blockEnd - blockStart + 1;
1015       }
1016     }
1017
1018   }
1019
1020   /**
1021    * Draws a region of the visible alignment
1022    * 
1023    * @param g1
1024    * @param startRes
1025    *          offset of the first column in the visible region (0..)
1026    * @param endRes
1027    *          offset of the last column in the visible region (0..)
1028    * @param startSeq
1029    *          offset of the first sequence in the visible region (0..)
1030    * @param endSeq
1031    *          offset of the last sequence in the visible region (0..)
1032    * @param yOffset
1033    *          vertical offset at which to draw (for wrapped alignments)
1034    */
1035   private void draw(Graphics g, int startRes, int endRes, int startSeq,
1036           int endSeq, int offset)
1037   {
1038     int charHeight = av.getCharHeight();
1039     int charWidth = av.getCharWidth();
1040
1041     g.setFont(av.getFont());
1042     seqRdr.prepare(g, av.isRenderGaps());
1043
1044     SequenceI nextSeq;
1045
1046     // / First draw the sequences
1047     // ///////////////////////////
1048     for (int i = startSeq; i <= endSeq; i++)
1049     {
1050       nextSeq = av.getAlignment().getSequenceAt(i);
1051       if (nextSeq == null)
1052       {
1053         // occasionally, a race condition occurs such that the alignment row is
1054         // empty
1055         continue;
1056       }
1057       seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
1058               startRes, endRes, offset + ((i - startSeq) * charHeight));
1059
1060       if (av.isShowSequenceFeatures())
1061       {
1062         fr.drawSequence(g, nextSeq, startRes, endRes,
1063                 offset + ((i - startSeq) * charHeight), false);
1064       }
1065
1066       /*
1067        * highlight search Results once sequence has been drawn
1068        */
1069       if (av.hasSearchResults())
1070       {
1071         SearchResultsI searchResults = av.getSearchResults();
1072         int[] visibleResults = searchResults.getResults(nextSeq, startRes,
1073                 endRes);
1074         if (visibleResults != null)
1075         {
1076           for (int r = 0; r < visibleResults.length; r += 2)
1077           {
1078             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
1079                     visibleResults[r + 1],
1080                     (visibleResults[r] - startRes) * charWidth,
1081                     offset + ((i - startSeq) * charHeight));
1082           }
1083         }
1084       }
1085     }
1086
1087     if (av.getSelectionGroup() != null
1088             || av.getAlignment().getGroups().size() > 0)
1089     {
1090       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
1091     }
1092
1093   }
1094
1095   /**
1096    * Draws the outlines of any groups defined on the alignment (excluding the
1097    * current selection group, if any)
1098    * 
1099    * @param g1
1100    * @param startRes
1101    * @param endRes
1102    * @param startSeq
1103    * @param endSeq
1104    * @param offset
1105    */
1106   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
1107           int startSeq, int endSeq, int offset)
1108   {
1109     Graphics2D g = (Graphics2D) g1;
1110
1111     SequenceGroup group = null;
1112     int groupIndex = -1;
1113
1114     if (av.getAlignment().getGroups().size() > 0)
1115     {
1116       group = av.getAlignment().getGroups().get(0);
1117       groupIndex = 0;
1118     }
1119
1120     if (group != null)
1121     {
1122       do
1123       {
1124         g.setColor(group.getOutlineColour());
1125         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
1126                 endSeq, offset);
1127
1128         groupIndex++;
1129         if (groupIndex >= av.getAlignment().getGroups().size())
1130         {
1131           break;
1132         }
1133         group = av.getAlignment().getGroups().get(groupIndex);
1134       } while (groupIndex < av.getAlignment().getGroups().size());
1135     }
1136   }
1137
1138   /**
1139    * Draws the outline of the current selection group (if any)
1140    * 
1141    * @param g
1142    * @param startRes
1143    * @param endRes
1144    * @param startSeq
1145    * @param endSeq
1146    */
1147   private void drawSelectionGroup(Graphics2D g, int startRes, int endRes,
1148           int startSeq, int endSeq)
1149   {
1150     SequenceGroup group = av.getSelectionGroup();
1151     if (group == null)
1152     {
1153       return;
1154     }
1155
1156     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1157             BasicStroke.JOIN_ROUND, 3f, new float[]
1158             { 5f, 3f }, 0f));
1159     g.setColor(Color.RED);
1160     if (!av.getWrapAlignment())
1161     {
1162       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
1163               0);
1164     }
1165     else
1166     {
1167       drawWrappedSelection(g, group, getWidth(), getHeight(),
1168               av.getRanges().getStartRes());
1169     }
1170     g.setStroke(new BasicStroke());
1171   }
1172
1173   /**
1174    * Draw the cursor as a separate image and overlay
1175    * 
1176    * @param startRes
1177    *          start residue of area to draw cursor in
1178    * @param endRes
1179    *          end residue of area to draw cursor in
1180    * @param startSeq
1181    *          start sequence of area to draw cursor in
1182    * @param endSeq
1183    *          end sequence of are to draw cursor in
1184    * @return a transparent image of the same size as the sequence canvas, with
1185    *         the cursor drawn on it, if any
1186    */
1187   private void drawCursor(Graphics g, int startRes, int endRes,
1188           int startSeq,
1189           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   /**
1262    * Draw a selection group over an unwrapped alignment
1263    * 
1264    * @param g
1265    *          graphics object to draw with
1266    * @param group
1267    *          selection group
1268    * @param startRes
1269    *          start residue of area to draw
1270    * @param endRes
1271    *          end residue of area to draw
1272    * @param startSeq
1273    *          start sequence of area to draw
1274    * @param endSeq
1275    *          end sequence of area to draw
1276    * @param offset
1277    *          vertical offset (used when called from wrapped alignment code)
1278    */
1279   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
1280           int startRes, int endRes, int startSeq, int endSeq, int offset)
1281   {
1282     int charWidth = av.getCharWidth();
1283           
1284     if (!av.hasHiddenColumns())
1285     {
1286       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
1287               offset);
1288     }
1289     else
1290     {
1291       // package into blocks of visible columns
1292       int screenY = 0;
1293       int blockStart;
1294       int blockEnd;
1295
1296       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
1297       VisibleContigsIterator regions = hidden
1298               .getVisContigsIterator(startRes, endRes + 1, true);
1299       while (regions.hasNext())
1300       {
1301         int[] region = regions.next();
1302         blockEnd = region[1];
1303         blockStart = region[0];
1304
1305         g.translate(screenY * charWidth, 0);
1306         drawPartialGroupOutline(g, group,
1307                 blockStart, blockEnd, startSeq, endSeq, offset);
1308
1309         g.translate(-screenY * charWidth, 0);
1310         screenY += blockEnd - blockStart + 1;
1311       }
1312     }
1313   }
1314
1315   /**
1316    * Draws part of a selection group outline
1317    * 
1318    * @param g
1319    * @param group
1320    * @param startRes
1321    * @param endRes
1322    * @param startSeq
1323    * @param endSeq
1324    * @param verticalOffset
1325    */
1326   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
1327           int startRes, int endRes, int startSeq, int endSeq,
1328           int verticalOffset)
1329   {
1330     int charHeight = av.getCharHeight();
1331     int charWidth = av.getCharWidth();
1332     int visWidth = (endRes - startRes + 1) * charWidth;
1333
1334     int oldY = -1;
1335     int i = 0;
1336     boolean inGroup = false;
1337     int top = -1;
1338     int bottom = -1;
1339     int sy = -1;
1340
1341     List<SequenceI> seqs = group.getSequences(null);
1342
1343     // position of start residue of group relative to startRes, in pixels
1344     int sx = (group.getStartRes() - startRes) * charWidth;
1345
1346     // width of group in pixels
1347     int xwidth = (((group.getEndRes() + 1) - group.getStartRes())
1348             * charWidth) - 1;
1349
1350     if (!(sx + xwidth < 0 || sx > visWidth))
1351     {
1352       for (i = startSeq; i <= endSeq; i++)
1353       {
1354         sy = verticalOffset + (i - startSeq) * charHeight;
1355
1356         if ((sx <= (endRes - startRes) * charWidth)
1357                 && seqs.contains(av.getAlignment().getSequenceAt(i)))
1358         {
1359           if ((bottom == -1)
1360                   && !seqs.contains(av.getAlignment().getSequenceAt(i + 1)))
1361           {
1362             bottom = sy + charHeight;
1363           }
1364
1365           if (!inGroup)
1366           {
1367             if (((top == -1) && (i == 0)) || !seqs
1368                     .contains(av.getAlignment().getSequenceAt(i - 1)))
1369             {
1370               top = sy;
1371             }
1372
1373             oldY = sy;
1374             inGroup = true;
1375           }
1376         }
1377         else if (inGroup)
1378         {
1379           drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
1380           drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
1381
1382           // reset top and bottom
1383           top = -1;
1384           bottom = -1;
1385           inGroup = false;
1386         }
1387       }
1388       if (inGroup)
1389       {
1390         sy = verticalOffset + ((i - startSeq) * charHeight);
1391         drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
1392         drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
1393       }
1394     }
1395   }
1396
1397   /**
1398    * Draw horizontal selection group boundaries at top and bottom positions
1399    * 
1400    * @param g
1401    *          graphics object to draw on
1402    * @param sx
1403    *          start x position
1404    * @param xwidth
1405    *          width of gap
1406    * @param visWidth
1407    *          visWidth maximum available width
1408    * @param top
1409    *          position to draw top of group at
1410    * @param bottom
1411    *          position to draw bottom of group at
1412    */
1413   private void drawHorizontals(Graphics2D g, int sx, int xwidth,
1414           int visWidth, int top, int bottom)
1415   {
1416     int width = xwidth;
1417     int startx = sx;
1418     if (startx < 0)
1419     {
1420       width += startx;
1421       startx = 0;
1422     }
1423
1424     // don't let width extend beyond current block, or group extent
1425     // fixes JAL-2672
1426     if (startx + width >= visWidth)
1427     {
1428       width = visWidth - startx;
1429     }
1430
1431     if (top != -1)
1432     {
1433       g.drawLine(startx, top, startx + width, top);
1434     }
1435
1436     if (bottom != -1)
1437     {
1438       g.drawLine(startx, bottom - 1, startx + width, bottom - 1);
1439     }
1440   }
1441
1442   /**
1443    * Draw vertical lines at sx and sx+xwidth providing they lie within
1444    * [0,visWidth)
1445    * 
1446    * @param g
1447    *          graphics object to draw on
1448    * @param sx
1449    *          start x position
1450    * @param xwidth
1451    *          width of gap
1452    * @param visWidth
1453    *          visWidth maximum available width
1454    * @param oldY
1455    *          top y value
1456    * @param sy
1457    *          bottom y value
1458    */
1459   private void drawVerticals(Graphics2D g, int sx, int xwidth, int visWidth,
1460           int oldY, int sy)
1461   {
1462     // if start position is visible, draw vertical line to left of
1463     // group
1464     if (sx >= 0 && sx < visWidth)
1465     {
1466       g.drawLine(sx, oldY, sx, sy);
1467     }
1468
1469     // if end position is visible, draw vertical line to right of
1470     // group
1471     if (sx + xwidth < visWidth)
1472     {
1473       g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
1474     }
1475   }
1476   
1477   /**
1478    * Highlights search results in the visible region by rendering as white text
1479    * on a black background. Any previous highlighting is removed. Answers true
1480    * if any highlight was left on the visible alignment (so status bar should be
1481    * set to match), else false. This method does _not_ set the 'fastPaint' flag,
1482    * so allows the next repaint to update the whole display.
1483    * 
1484    * @param results
1485    * @return
1486    */
1487   public boolean highlightSearchResults(SearchResultsI results)
1488   {
1489     return highlightSearchResults(results, false);
1490
1491   }
1492   
1493   /**
1494    * Highlights search results in the visible region by rendering as white text
1495    * on a black background. Any previous highlighting is removed. Answers true
1496    * if any highlight was left on the visible alignment (so status bar should be
1497    * set to match), else false.
1498    * <p>
1499    * Optionally, set the 'fastPaint' flag for a faster redraw if only the
1500    * highlighted regions are modified. This speeds up highlighting across linked
1501    * alignments.
1502    * <p>
1503    * Currently fastPaint is not implemented for scrolled wrapped alignments. If
1504    * a wrapped alignment had to be scrolled to show the highlighted region, then
1505    * it should be fully redrawn, otherwise a fast paint can be performed. This
1506    * argument could be removed if fast paint of scrolled wrapped alignment is
1507    * coded in future (JAL-2609).
1508    * 
1509    * @param results
1510    * @param doFastPaint
1511    *          if true, sets a flag so the next repaint only redraws the modified
1512    *          image
1513    * @return
1514    */
1515   public boolean highlightSearchResults(SearchResultsI results,
1516           boolean doFastPaint)
1517   {
1518     if (fastpainting)
1519     {
1520       return false;
1521     }
1522     boolean wrapped = av.getWrapAlignment();
1523     try
1524     {
1525       fastPaint = doFastPaint;
1526       fastpainting = fastPaint;
1527
1528       /*
1529        * to avoid redrawing the whole visible region, we instead
1530        * redraw just the minimal regions to remove previous highlights
1531        * and add new ones
1532        */
1533       SearchResultsI previous = av.getSearchResults();
1534       av.setSearchResults(results);
1535       boolean redrawn = false;
1536       boolean drawn = false;
1537       if (wrapped)
1538       {
1539         redrawn = drawMappedPositionsWrapped(previous);
1540         drawn = drawMappedPositionsWrapped(results);
1541         redrawn |= drawn;
1542       }
1543       else
1544       {
1545         redrawn = drawMappedPositions(previous);
1546         drawn = drawMappedPositions(results);
1547         redrawn |= drawn;
1548       }
1549
1550       /*
1551        * if highlights were either removed or added, repaint
1552        */
1553       if (redrawn)
1554       {
1555         repaint();
1556       }
1557
1558       /*
1559        * return true only if highlights were added
1560        */
1561       return drawn;
1562
1563     } finally
1564     {
1565       fastpainting = false;
1566     }
1567   }
1568
1569   /**
1570    * Redraws the minimal rectangle in the visible region (if any) that includes
1571    * mapped positions of the given search results. Whether or not positions are
1572    * highlighted depends on the SearchResults set on the Viewport. This allows
1573    * this method to be called to either clear or set highlighting. Answers true
1574    * if any positions were drawn (in which case a repaint is still required),
1575    * else false.
1576    * 
1577    * @param results
1578    * @return
1579    */
1580   protected boolean drawMappedPositions(SearchResultsI results)
1581   {
1582     if ((results == null) || (img == null)) // JAL-2784 check gg is not null
1583     {
1584       return false;
1585     }
1586
1587     /*
1588      * calculate the minimal rectangle to redraw that 
1589      * includes both new and existing search results
1590      */
1591     int firstSeq = Integer.MAX_VALUE;
1592     int lastSeq = -1;
1593     int firstCol = Integer.MAX_VALUE;
1594     int lastCol = -1;
1595     boolean matchFound = false;
1596
1597     ViewportRanges ranges = av.getRanges();
1598     int firstVisibleColumn = ranges.getStartRes();
1599     int lastVisibleColumn = ranges.getEndRes();
1600     AlignmentI alignment = av.getAlignment();
1601     if (av.hasHiddenColumns())
1602     {
1603       firstVisibleColumn = alignment.getHiddenColumns()
1604               .visibleToAbsoluteColumn(firstVisibleColumn);
1605       lastVisibleColumn = alignment.getHiddenColumns()
1606               .visibleToAbsoluteColumn(lastVisibleColumn);
1607     }
1608
1609     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1610             .getEndSeq(); seqNo++)
1611     {
1612       SequenceI seq = alignment.getSequenceAt(seqNo);
1613
1614       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1615               lastVisibleColumn);
1616       if (visibleResults != null)
1617       {
1618         for (int i = 0; i < visibleResults.length - 1; i += 2)
1619         {
1620           int firstMatchedColumn = visibleResults[i];
1621           int lastMatchedColumn = visibleResults[i + 1];
1622           if (firstMatchedColumn <= lastVisibleColumn
1623                   && lastMatchedColumn >= firstVisibleColumn)
1624           {
1625             /*
1626              * found a search results match in the visible region - 
1627              * remember the first and last sequence matched, and the first
1628              * and last visible columns in the matched positions
1629              */
1630             matchFound = true;
1631             firstSeq = Math.min(firstSeq, seqNo);
1632             lastSeq = Math.max(lastSeq, seqNo);
1633             firstMatchedColumn = Math.max(firstMatchedColumn,
1634                     firstVisibleColumn);
1635             lastMatchedColumn = Math.min(lastMatchedColumn,
1636                     lastVisibleColumn);
1637             firstCol = Math.min(firstCol, firstMatchedColumn);
1638             lastCol = Math.max(lastCol, lastMatchedColumn);
1639           }
1640         }
1641       }
1642     }
1643
1644     if (matchFound)
1645     {
1646       if (av.hasHiddenColumns())
1647       {
1648         firstCol = alignment.getHiddenColumns()
1649                 .absoluteToVisibleColumn(firstCol);
1650         lastCol = alignment.getHiddenColumns().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, startRes
1809                 - 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 * wrappedRepeatHeightPx) > canvasHeight;
1859
1860     if (lastWidthPartHeight)
1861     {
1862       int widthsAbove = Math.max(0, visibleWidths - 2);
1863       int ypos = wrappedRepeatHeightPx * widthsAbove
1864               + wrappedSpaceAboveAlignment;
1865       int endRes = ranges.getEndRes();
1866       endRes += widthsAbove * viewportWidth;
1867       int startRes = endRes - columns;
1868       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1869               * charWidth;
1870
1871       /*
1872        * white fill first to erase annotations
1873        */
1874       
1875       
1876       gg.translate(xOffset, 0);
1877       gg.setColor(Color.white);
1878       gg.fillRect(labelWidthWest, ypos,
1879               (endRes - startRes + 1) * charWidth, 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, -positions
2017                   * charWidth, heightToCopy, widthToCopy,
2018                   -wrappedRepeatHeightPx);
2019         }
2020         y += wrappedRepeatHeightPx;
2021         xpos += viewportWidth;
2022       }
2023     }
2024     gg.dispose();
2025   }
2026
2027   
2028   /**
2029    * Redraws any positions in the search results in the visible region of a
2030    * wrapped alignment. Any highlights are drawn depending on the search results
2031    * set on the Viewport, not the <code>results</code> argument. This allows
2032    * this method to be called either to clear highlights (passing the previous
2033    * search results), or to draw new highlights.
2034    * 
2035    * @param results
2036    * @return
2037    */
2038   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
2039   {
2040     if ((results == null) || (img == null)) // JAL-2784 check gg is not null
2041     {
2042       return false;
2043     }
2044     int charHeight = av.getCharHeight();
2045
2046     boolean matchFound = false;
2047
2048     calculateWrappedGeometry(getWidth(), getHeight());
2049     int wrappedWidth = av.getWrappedWidth();
2050     int wrappedHeight = wrappedRepeatHeightPx;
2051
2052     ViewportRanges ranges = av.getRanges();
2053     int canvasHeight = getHeight();
2054     int repeats = canvasHeight / wrappedHeight;
2055     if (canvasHeight / wrappedHeight > 0)
2056     {
2057       repeats++;
2058     }
2059
2060     int firstVisibleColumn = ranges.getStartRes();
2061     int lastVisibleColumn = ranges.getStartRes() + repeats
2062             * ranges.getViewportWidth() - 1;
2063
2064     AlignmentI alignment = av.getAlignment();
2065     if (av.hasHiddenColumns())
2066     {
2067       firstVisibleColumn = alignment.getHiddenColumns()
2068               .visibleToAbsoluteColumn(firstVisibleColumn);
2069       lastVisibleColumn = alignment.getHiddenColumns()
2070               .visibleToAbsoluteColumn(lastVisibleColumn);
2071     }
2072
2073     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
2074
2075     
2076     Graphics gg = img.getGraphics();
2077
2078     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
2079             .getEndSeq(); seqNo++)
2080     {
2081       SequenceI seq = alignment.getSequenceAt(seqNo);
2082
2083       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
2084               lastVisibleColumn);
2085       if (visibleResults != null)
2086       {
2087         for (int i = 0; i < visibleResults.length - 1; i += 2)
2088         {
2089           int firstMatchedColumn = visibleResults[i];
2090           int lastMatchedColumn = visibleResults[i + 1];
2091           if (firstMatchedColumn <= lastVisibleColumn
2092                   && lastMatchedColumn >= firstVisibleColumn)
2093           {
2094             /*
2095              * found a search results match in the visible region
2096              */
2097             firstMatchedColumn = Math.max(firstMatchedColumn,
2098                     firstVisibleColumn);
2099             lastMatchedColumn = Math.min(lastMatchedColumn,
2100                     lastVisibleColumn);
2101
2102             /*
2103              * draw each mapped position separately (as contiguous positions may
2104              * wrap across lines)
2105              */
2106             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
2107             {
2108               int displayColumn = mappedPos;
2109               if (av.hasHiddenColumns())
2110               {
2111                 displayColumn = alignment.getHiddenColumns()
2112                         .absoluteToVisibleColumn(displayColumn);
2113               }
2114
2115               /*
2116                * transX: offset from left edge of canvas to residue position
2117                */
2118               int transX = labelWidthWest
2119                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
2120                       * av.getCharWidth();
2121
2122               /*
2123                * transY: offset from top edge of canvas to residue position
2124                */
2125               int transY = gapHeight;
2126               transY += (displayColumn - ranges.getStartRes())
2127                       / wrappedWidth * wrappedHeight;
2128               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
2129
2130               /*
2131                * yOffset is from graphics origin to start of visible region
2132                */
2133               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
2134               if (transY < getHeight())
2135               {
2136                 matchFound = true;
2137                 gg.translate(transX, transY);
2138                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
2139                         yOffset);
2140                 gg.translate(-transX, -transY);
2141               }
2142             }
2143           }
2144         }
2145       }
2146     }
2147   
2148     gg.dispose();
2149
2150     return matchFound;
2151   }
2152
2153   /**
2154    * Answers the width in pixels of the left scale labels (0 if not shown)
2155    * 
2156    * @return
2157    */
2158   int getLabelWidthWest()
2159   {
2160     return labelWidthWest;
2161   }
2162
2163   /**
2164    * Ensure that a full paint is done next, for whatever reason. This was
2165    * necessary for JavaScript; apparently in Java the timing is just right on
2166    * multiple threads (EventQueue-0, Consensus, Conservation) that we can get
2167    * away with one fast paint before the others, but this ensures that in the
2168    * end we get a full paint. Problem arose in relation to copy/paste, where the
2169    * paste was not finalized with a full paint.
2170    * 
2171    * @author hansonr 2019.04.17
2172    */
2173   public void clearFastPaint()
2174   {
2175     fastPaint = false;
2176   }
2177 }