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