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