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