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