JAL-2609 further progress towards scroll right fast paint...
[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.renderer.ScaleRenderer;
29 import jalview.renderer.ScaleRenderer.ScaleMark;
30 import jalview.util.Comparison;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.BasicStroke;
35 import java.awt.BorderLayout;
36 import java.awt.Color;
37 import java.awt.FontMetrics;
38 import java.awt.Graphics;
39 import java.awt.Graphics2D;
40 import java.awt.RenderingHints;
41 import java.awt.Shape;
42 import java.awt.image.BufferedImage;
43 import java.beans.PropertyChangeEvent;
44 import java.util.List;
45
46 import javax.swing.JComponent;
47
48 /**
49  * DOCUMENT ME!
50  * 
51  * @author $author$
52  * @version $Revision$
53  */
54 public class SeqCanvas extends JComponent implements ViewportListenerI
55 {
56   private static String ZEROS = "0000000000";
57
58   final FeatureRenderer fr;
59
60   final SequenceRenderer sr;
61
62   BufferedImage img;
63
64   Graphics2D gg;
65
66   int imgWidth;
67
68   int imgHeight;
69
70   AlignViewport av;
71
72   boolean fastPaint = false;
73
74   boolean fastpainting = false;
75
76   int labelWidthWest;
77
78   int labelWidthEast;
79
80   int cursorX = 0;
81
82   int cursorY = 0;
83
84   /**
85    * Creates a new SeqCanvas object.
86    * 
87    * @param av
88    *          DOCUMENT ME!
89    */
90   public SeqCanvas(AlignmentPanel ap)
91   {
92     this.av = ap.av;
93     updateViewport();
94     fr = new FeatureRenderer(ap);
95     sr = new SequenceRenderer(av);
96     setLayout(new BorderLayout());
97     PaintRefresher.Register(this, av.getSequenceSetId());
98     setBackground(Color.white);
99
100     av.getRanges().addPropertyChangeListener(this);
101   }
102
103   public SequenceRenderer getSequenceRenderer()
104   {
105     return sr;
106   }
107
108   public FeatureRenderer getFeatureRenderer()
109   {
110     return fr;
111   }
112
113   int charHeight = 0, charWidth = 0;
114
115   private void updateViewport()
116   {
117     charHeight = av.getCharHeight();
118     charWidth = av.getCharWidth();
119   }
120
121   /**
122    * Draws the scale above a region of a wrapped alignment, consisting of a
123    * column number every major interval (10 columns).
124    * 
125    * @param g
126    *          the graphics context to draw on, positioned at the start (bottom
127    *          left) of the line on which to draw any scale marks
128    * @param startx
129    *          start alignment column (0..)
130    * @param endx
131    *          end alignment column (0..)
132    * @param ypos
133    *          y offset to draw at
134    */
135   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
136   {
137     updateViewport();
138     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
139             endx);
140     for (ScaleMark mark : marks)
141     {
142       int mpos = mark.column; // (i - startx - 1)
143       if (mpos < 0)
144       {
145         continue;
146       }
147       String mstring = mark.text;
148
149       if (mark.major)
150       {
151         if (mstring != null)
152         {
153           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
154         }
155
156         /*
157          * draw a tick mark below the column number, centred on the column;
158          * height of tick mark is 4 pixels less than half a character
159          */
160         int xpos = (mpos * charWidth) + (charWidth / 2);
161         g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
162       }
163     }
164   }
165
166   /**
167    * Draw the scale to the left or right of a wrapped alignment
168    * 
169    * @param g
170    * @param startx
171    *          first column of wrapped width (0.. excluding any hidden columns)
172    * @param endx
173    *          last column of wrapped width (0.. excluding any hidden columns)
174    * @param ypos
175    *          vertical offset at which to begin the scale
176    * @param left
177    *          if true, scale is left of residues, if false, scale is right
178    */
179   void drawVerticalScale(Graphics g, int startx, int endx, int ypos,
180           boolean left)
181   {
182     ypos += charHeight;
183
184     if (av.hasHiddenColumns())
185     {
186       HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
187       startx = hiddenColumns.adjustForHiddenColumns(startx);
188       endx = hiddenColumns.adjustForHiddenColumns(endx);
189     }
190     FontMetrics fm = getFontMetrics(av.getFont());
191
192     for (int i = 0; i < av.getAlignment().getHeight(); i++)
193     {
194       SequenceI seq = av.getAlignment().getSequenceAt(i);
195
196       /*
197        * find sequence position of first non-gapped position -
198        * to the right if scale left, to the left if scale right
199        */
200       int index = left ? startx : endx;
201       int value = -1;
202       while (index >= startx && index <= endx)
203       {
204         if (!Comparison.isGap(seq.getCharAt(index)))
205         {
206           value = seq.findPosition(index);
207           break;
208         }
209         if (left)
210         {
211           index++;
212         }
213         else
214         {
215           index--;
216         }
217       }
218
219       if (value != -1)
220       {
221         /*
222          * white fill the space for the scale
223          */
224         g.setColor(Color.white);
225         int y = (ypos + (i * charHeight)) - (charHeight / 5);
226         y -= charHeight; // fillRect: origin is top left of rectangle
227         int xpos = left ? 0 : getWidth() - labelWidthEast;
228         g.fillRect(xpos, y, left ? labelWidthWest : labelWidthEast,
229                 charHeight + 1);
230         y += charHeight; // drawString: origin is bottom left of text
231
232         /*
233          * draw scale value, right justified, with half a character width
234          * separation from the sequence data
235          */
236         String valueAsString = String.valueOf(value);
237         int justify = fm.stringWidth(valueAsString) + charWidth;
238         xpos = left ? labelWidthWest - justify + charWidth / 2
239                 : getWidth() - justify - charWidth / 2;
240
241         g.setColor(Color.black);
242         g.drawString(valueAsString, xpos, y);
243       }
244     }
245   }
246
247   /**
248    * Does a fast paint of an alignment in response to a scroll. Most of the
249    * visible region is simply copied and shifted, and then any newly visible
250    * columns or rows are drawn. The scroll may be horizontal or vertical, but
251    * not both at once. Scrolling may be the result of
252    * <ul>
253    * <li>dragging a scroll bar</li>
254    * <li>clicking in the scroll bar</li>
255    * <li>scrolling by trackpad, middle mouse button, or other device</li>
256    * <li>by moving the box in the Overview window</li>
257    * <li>programmatically to make a highlighted position visible</li>
258    * </ul>
259    * 
260    * @param horizontal
261    *          columns to shift right (positive) or left (negative)
262    * @param vertical
263    *          rows to shift down (positive) or up (negative)
264    */
265   public void fastPaint(int horizontal, int vertical)
266   {
267     if (fastpainting || gg == null)
268     {
269       return;
270     }
271     fastpainting = true;
272     fastPaint = true;
273     updateViewport();
274
275     ViewportRanges ranges = av.getRanges();
276     int startRes = ranges.getStartRes();
277     int endRes = ranges.getEndRes();
278     int startSeq = ranges.getStartSeq();
279     int endSeq = ranges.getEndSeq();
280     int transX = 0;
281     int transY = 0;
282
283     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
284             imgHeight, -horizontal * charWidth, -vertical * charHeight);
285
286     if (horizontal > 0) // scrollbar pulled right, image to the left
287     {
288       transX = (endRes - startRes - horizontal) * charWidth;
289       startRes = endRes - horizontal;
290     }
291     else if (horizontal < 0)
292     {
293       endRes = startRes - horizontal;
294     }
295     else if (vertical > 0) // scroll down
296     {
297       startSeq = endSeq - vertical;
298
299       if (startSeq < ranges.getStartSeq())
300       { // ie scrolling too fast, more than a page at a time
301         startSeq = ranges.getStartSeq();
302       }
303       else
304       {
305         transY = imgHeight - ((vertical + 1) * charHeight);
306       }
307     }
308     else if (vertical < 0)
309     {
310       endSeq = startSeq - vertical;
311
312       if (endSeq > ranges.getEndSeq())
313       {
314         endSeq = ranges.getEndSeq();
315       }
316     }
317
318     gg.translate(transX, transY);
319     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
320     gg.translate(-transX, -transY);
321
322     repaint();
323     fastpainting = false;
324   }
325
326   @Override
327   public void paintComponent(Graphics g)
328   {
329     updateViewport();
330     BufferedImage lcimg = img; // take reference since other threads may null
331     // img and call later.
332     super.paintComponent(g);
333
334     if (lcimg != null
335             && (fastPaint
336                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
337                     .getClipBounds().height)))
338     {
339       g.drawImage(lcimg, 0, 0, this);
340       fastPaint = false;
341       return;
342     }
343
344     // this draws the whole of the alignment
345     imgWidth = getWidth();
346     imgHeight = getHeight();
347
348     imgWidth -= (imgWidth % charWidth);
349     imgHeight -= (imgHeight % charHeight);
350
351     if ((imgWidth < 1) || (imgHeight < 1))
352     {
353       return;
354     }
355
356     if (lcimg == null || imgWidth != lcimg.getWidth()
357             || imgHeight != lcimg.getHeight())
358     {
359       try
360       {
361         lcimg = img = new BufferedImage(imgWidth, imgHeight,
362                 BufferedImage.TYPE_INT_RGB);
363         gg = (Graphics2D) img.getGraphics();
364         gg.setFont(av.getFont());
365       } catch (OutOfMemoryError er)
366       {
367         System.gc();
368         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
369         new OOMWarning("Creating alignment image for display", er);
370
371         return;
372       }
373     }
374
375     if (av.antiAlias)
376     {
377       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
378               RenderingHints.VALUE_ANTIALIAS_ON);
379     }
380
381     gg.setColor(Color.white);
382     gg.fillRect(0, 0, imgWidth, imgHeight);
383
384     ViewportRanges ranges = av.getRanges();
385     if (av.getWrapAlignment())
386     {
387       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
388     }
389     else
390     {
391       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
392               ranges.getStartSeq(), ranges.getEndSeq(), 0);
393     }
394
395     g.drawImage(lcimg, 0, 0, this);
396
397   }
398
399   /**
400    * Returns the visible width of the canvas in residues, after allowing for
401    * East or West scales (if shown)
402    * 
403    * @param canvasWidth
404    *          the width in pixels (possibly including scales)
405    * 
406    * @return
407    */
408   public int getWrappedCanvasWidth(int canvasWidth)
409   {
410     FontMetrics fm = getFontMetrics(av.getFont());
411
412     labelWidthEast = 0;
413     labelWidthWest = 0;
414
415     if (av.getScaleRightWrapped())
416     {
417       labelWidthEast = getLabelWidth(fm);
418     }
419
420     if (av.getScaleLeftWrapped())
421     {
422       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
423               : getLabelWidth(fm);
424     }
425
426     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
427   }
428
429   /**
430    * Returns a pixel width suitable for showing the largest sequence coordinate
431    * (end position) in the alignment. Returns 2 plus the number of decimal
432    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
433    * 
434    * @param fm
435    * @return
436    */
437   protected int getLabelWidth(FontMetrics fm)
438   {
439     /*
440      * find the biggest sequence end position we need to show
441      * (note this is not necessarily the sequence length)
442      */
443     int maxWidth = 0;
444     AlignmentI alignment = av.getAlignment();
445     for (int i = 0; i < alignment.getHeight(); i++)
446     {
447       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
448     }
449
450     int length = 2;
451     for (int i = maxWidth; i > 0; i /= 10)
452     {
453       length++;
454     }
455
456     return fm.stringWidth(ZEROS.substring(0, length));
457   }
458
459   /**
460    * Draws as many widths of a wrapped alignment as can fit in the visible
461    * window
462    * 
463    * @param g
464    * @param canvasWidth
465    *          available width in pixels
466    * @param canvasHeight
467    *          available height in pixels
468    * @param startRes
469    *          the first visible column (0...) of the alignment to draw
470    */
471   public void drawWrappedPanel(Graphics g, int canvasWidth,
472           int canvasHeight, int startRes)
473   {
474     updateViewport();
475     int labelWidth = 0;
476     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
477     {
478       FontMetrics fm = getFontMetrics(av.getFont());
479       labelWidth = getLabelWidth(fm);
480     }
481
482     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
483     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
484
485     int hgap = charHeight;
486     if (av.getScaleAboveWrapped())
487     {
488       hgap += charHeight;
489     }
490
491     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
492
493     av.setWrappedWidth(cWidth);
494
495     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
496
497     int ypos = hgap;
498     int maxwidth = av.getAlignment().getWidth();
499
500     if (av.hasHiddenColumns())
501     {
502       maxwidth = av.getAlignment().getHiddenColumns()
503               .findColumnPosition(maxwidth);
504     }
505
506     int annotationHeight = getAnnotationHeight();
507     int sequencesHeight = av.getAlignment().getHeight() * charHeight;
508
509     /*
510      * draw one width at a time (including any scales or annotation shown),
511      * until we have run out of alignment or vertical space available
512      * (stop if not enough room left for at least one sequence)
513      */
514     int yposMax = canvasHeight;// - hgap - charHeight + 1;
515     while ((ypos <= yposMax) && (startRes < maxwidth))
516     {
517       drawWrappedWidth(g, startRes, canvasHeight, cWidth, maxwidth, ypos);
518
519       ypos += sequencesHeight + annotationHeight + hgap;
520
521       startRes += cWidth;
522     }
523   }
524
525   /**
526    * Draws one width of a wrapped alignment, including scales left, right or
527    * above, and annnotations, if shown
528    * 
529    * @param g
530    * @param startRes
531    * @param canvasHeight
532    * @param canvasWidth
533    * @param maxWidth
534    * @param ypos
535    */
536   protected void drawWrappedWidth(Graphics g, int startRes,
537           int canvasHeight, int canvasWidth, int maxWidth, int ypos)
538   {
539     int endx;
540     endx = startRes + canvasWidth - 1;
541
542     if (endx > maxWidth)
543     {
544       endx = maxWidth;
545     }
546
547     g.setFont(av.getFont());
548     g.setColor(Color.black);
549
550     if (av.getScaleLeftWrapped())
551     {
552       drawVerticalScale(g, startRes, endx, ypos, true);
553     }
554
555     if (av.getScaleRightWrapped())
556     {
557       drawVerticalScale(g, startRes, endx, ypos, false);
558     }
559
560     drawWrappedRegion(g, startRes, endx, canvasHeight, canvasWidth, ypos);
561   }
562
563   /**
564    * Draws columns of a wrapped alignment from startRes to endRes, including
565    * scale above and annotations if shown, but not scale left or right.
566    * 
567    * @param g
568    * @param startRes
569    * @param endRes
570    * @param canvasHeight
571    * @param canvasWidth
572    * @param ypos
573    */
574   protected void drawWrappedRegion(Graphics g, int startRes, int endRes,
575           int canvasHeight, int canvasWidth, int ypos)
576   {
577     g.translate(labelWidthWest, 0);
578
579     if (av.getScaleAboveWrapped())
580     {
581       drawNorthScale(g, startRes, endRes, ypos);
582     }
583
584     // todo can we let drawPanel() handle this?
585     if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
586     {
587       g.setColor(Color.blue);
588       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
589       List<Integer> positions = hidden.findHiddenRegionPositions();
590       for (int pos : positions)
591       {
592         int res = pos - startRes;
593
594         if (res < 0 || res > endRes - startRes)
595         {
596           continue;
597         }
598
599         gg.fillPolygon(new int[] { res * charWidth - charHeight / 4,
600             res * charWidth + charHeight / 4, res * charWidth }, new int[] {
601             ypos - (charHeight / 2), ypos - (charHeight / 2),
602             ypos - (charHeight / 2) + 8 }, 3);
603       }
604     }
605
606     // When printing we have an extra clipped region,
607     // the Printable page which we need to account for here
608     Shape clip = g.getClip();
609
610     if (clip == null)
611     {
612       g.setClip(0, 0, canvasWidth * charWidth, canvasHeight);
613     }
614     else
615     {
616       g.setClip(0, (int) clip.getBounds().getY(), canvasWidth * charWidth,
617               (int) clip.getBounds().getHeight());
618     }
619
620     drawPanel(g, startRes, endRes, 0, av.getAlignment().getHeight() - 1, ypos);
621
622     int cHeight = av.getAlignment().getHeight() * charHeight;
623
624     if (av.isShowAnnotation())
625     {
626       g.translate(0, cHeight + ypos + 3);
627       if (annotations == null)
628       {
629         annotations = new AnnotationPanel(av);
630       }
631
632       annotations.renderer.drawComponent(annotations, av, g, -1, startRes,
633               endRes + 1);
634       g.translate(0, -cHeight - ypos - 3);
635     }
636     g.setClip(clip);
637     g.translate(-labelWidthWest, 0);
638   }
639
640   AnnotationPanel annotations;
641
642   int getAnnotationHeight()
643   {
644     if (!av.isShowAnnotation())
645     {
646       return 0;
647     }
648
649     if (annotations == null)
650     {
651       annotations = new AnnotationPanel(av);
652     }
653
654     return annotations.adjustPanelHeight();
655   }
656
657   /**
658    * Draws the visible region of the alignment on the graphics context. If there
659    * are hidden column markers in the visible region, then each sub-region
660    * between the markers is drawn separately, followed by the hidden column
661    * marker.
662    * 
663    * @param g1
664    *          the graphics context, positioned at the first residue to be drawn
665    * @param startRes
666    *          offset of the first column to draw (0..)
667    * @param endRes
668    *          offset of the last column to draw (0..)
669    * @param startSeq
670    *          offset of the first sequence to draw (0..)
671    * @param endSeq
672    *          offset of the last sequence to draw (0..)
673    * @param yOffset
674    *          vertical offset at which to draw (for wrapped alignments)
675    */
676   public void drawPanel(Graphics g1, final int startRes, final int endRes,
677           final int startSeq, final int endSeq, final int yOffset)
678   {
679     updateViewport();
680     if (!av.hasHiddenColumns())
681     {
682       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
683     }
684     else
685     {
686       int screenY = 0;
687       final int screenYMax = endRes - startRes;
688       int blockStart = startRes;
689       int blockEnd = endRes;
690
691       for (int[] region : av.getAlignment().getHiddenColumns()
692               .getHiddenColumnsCopy())
693       {
694         int hideStart = region[0];
695         int hideEnd = region[1];
696
697         if (hideStart <= blockStart)
698         {
699           blockStart += (hideEnd - hideStart) + 1;
700           continue;
701         }
702
703         /*
704          * draw up to just before the next hidden region, or the end of
705          * the visible region, whichever comes first
706          */
707         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
708                 - screenY);
709
710         g1.translate(screenY * charWidth, 0);
711
712         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
713
714         /*
715          * draw the downline of the hidden column marker (ScalePanel draws the
716          * triangle on top) if we reached it
717          */
718         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
719         {
720           g1.setColor(Color.blue);
721
722           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
723                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
724                   (endSeq - startSeq + 1) * charHeight + yOffset);
725         }
726
727         g1.translate(-screenY * charWidth, 0);
728         screenY += blockEnd - blockStart + 1;
729         blockStart = hideEnd + 1;
730
731         if (screenY > screenYMax)
732         {
733           // already rendered last block
734           return;
735         }
736       }
737
738       if (screenY <= screenYMax)
739       {
740         // remaining visible region to render
741         blockEnd = blockStart + screenYMax - screenY;
742         g1.translate(screenY * charWidth, 0);
743         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
744
745         g1.translate(-screenY * charWidth, 0);
746       }
747     }
748
749   }
750
751   /**
752    * Draws a region of the visible alignment
753    * 
754    * @param g1
755    * @param startRes
756    *          offset of the first column in the visible region (0..)
757    * @param endRes
758    *          offset of the last column in the visible region (0..)
759    * @param startSeq
760    *          offset of the first sequence in the visible region (0..)
761    * @param endSeq
762    *          offset of the last sequence in the visible region (0..)
763    * @param yOffset
764    *          vertical offset at which to draw (for wrapped alignments)
765    */
766   private void draw(Graphics g, int startRes, int endRes, int startSeq,
767           int endSeq, int offset)
768   {
769     g.setFont(av.getFont());
770     sr.prepare(g, av.isRenderGaps());
771
772     SequenceI nextSeq;
773
774     // / First draw the sequences
775     // ///////////////////////////
776     for (int i = startSeq; i <= endSeq; i++)
777     {
778       nextSeq = av.getAlignment().getSequenceAt(i);
779       if (nextSeq == null)
780       {
781         // occasionally, a race condition occurs such that the alignment row is
782         // empty
783         continue;
784       }
785       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
786               startRes, endRes, offset + ((i - startSeq) * charHeight));
787
788       if (av.isShowSequenceFeatures())
789       {
790         fr.drawSequence(g, nextSeq, startRes, endRes, offset
791                 + ((i - startSeq) * charHeight), false);
792       }
793
794       /*
795        * highlight search Results once sequence has been drawn
796        */
797       if (av.hasSearchResults())
798       {
799         SearchResultsI searchResults = av.getSearchResults();
800         int[] visibleResults = searchResults.getResults(nextSeq,
801                 startRes, endRes);
802         if (visibleResults != null)
803         {
804           for (int r = 0; r < visibleResults.length; r += 2)
805           {
806             sr.drawHighlightedText(nextSeq, visibleResults[r],
807                     visibleResults[r + 1], (visibleResults[r] - startRes)
808                             * charWidth, offset
809                             + ((i - startSeq) * charHeight));
810           }
811         }
812       }
813
814       if (av.cursorMode && cursorY == i && cursorX >= startRes
815               && cursorX <= endRes)
816       {
817         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
818                 offset + ((i - startSeq) * charHeight));
819       }
820     }
821
822     if (av.getSelectionGroup() != null
823             || av.getAlignment().getGroups().size() > 0)
824     {
825       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
826     }
827
828   }
829
830   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
831           int startSeq, int endSeq, int offset)
832   {
833     Graphics2D g = (Graphics2D) g1;
834     //
835     // ///////////////////////////////////
836     // Now outline any areas if necessary
837     // ///////////////////////////////////
838     SequenceGroup group = av.getSelectionGroup();
839
840     int sx = -1;
841     int sy = -1;
842     int ex = -1;
843     int groupIndex = -1;
844     int visWidth = (endRes - startRes + 1) * charWidth;
845
846     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
847     {
848       group = av.getAlignment().getGroups().get(0);
849       groupIndex = 0;
850     }
851
852     if (group != null)
853     {
854       do
855       {
856         int oldY = -1;
857         int i = 0;
858         boolean inGroup = false;
859         int top = -1;
860         int bottom = -1;
861
862         for (i = startSeq; i <= endSeq; i++)
863         {
864           sx = (group.getStartRes() - startRes) * charWidth;
865           sy = offset + ((i - startSeq) * charHeight);
866           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
867
868           if (sx + ex < 0 || sx > visWidth)
869           {
870             continue;
871           }
872
873           if ((sx <= (endRes - startRes) * charWidth)
874                   && group.getSequences(null).contains(
875                           av.getAlignment().getSequenceAt(i)))
876           {
877             if ((bottom == -1)
878                     && !group.getSequences(null).contains(
879                             av.getAlignment().getSequenceAt(i + 1)))
880             {
881               bottom = sy + charHeight;
882             }
883
884             if (!inGroup)
885             {
886               if (((top == -1) && (i == 0))
887                       || !group.getSequences(null).contains(
888                               av.getAlignment().getSequenceAt(i - 1)))
889               {
890                 top = sy;
891               }
892
893               oldY = sy;
894               inGroup = true;
895
896               if (group == av.getSelectionGroup())
897               {
898                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
899                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
900                         0f));
901                 g.setColor(Color.RED);
902               }
903               else
904               {
905                 g.setStroke(new BasicStroke());
906                 g.setColor(group.getOutlineColour());
907               }
908             }
909           }
910           else
911           {
912             if (inGroup)
913             {
914               if (sx >= 0 && sx < visWidth)
915               {
916                 g.drawLine(sx, oldY, sx, sy);
917               }
918
919               if (sx + ex < visWidth)
920               {
921                 g.drawLine(sx + ex, oldY, sx + ex, sy);
922               }
923
924               if (sx < 0)
925               {
926                 ex += sx;
927                 sx = 0;
928               }
929
930               if (sx + ex > visWidth)
931               {
932                 ex = visWidth;
933               }
934
935               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
936               {
937                 ex = (endRes - startRes + 1) * charWidth;
938               }
939
940               if (top != -1)
941               {
942                 g.drawLine(sx, top, sx + ex, top);
943                 top = -1;
944               }
945
946               if (bottom != -1)
947               {
948                 g.drawLine(sx, bottom, sx + ex, bottom);
949                 bottom = -1;
950               }
951
952               inGroup = false;
953             }
954           }
955         }
956
957         if (inGroup)
958         {
959           sy = offset + ((i - startSeq) * charHeight);
960           if (sx >= 0 && sx < visWidth)
961           {
962             g.drawLine(sx, oldY, sx, sy);
963           }
964
965           if (sx + ex < visWidth)
966           {
967             g.drawLine(sx + ex, oldY, sx + ex, sy);
968           }
969
970           if (sx < 0)
971           {
972             ex += sx;
973             sx = 0;
974           }
975
976           if (sx + ex > visWidth)
977           {
978             ex = visWidth;
979           }
980           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
981           {
982             ex = (endRes - startRes + 1) * charWidth;
983           }
984
985           if (top != -1)
986           {
987             g.drawLine(sx, top, sx + ex, top);
988             top = -1;
989           }
990
991           if (bottom != -1)
992           {
993             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
994             bottom = -1;
995           }
996
997           inGroup = false;
998         }
999
1000         groupIndex++;
1001
1002         g.setStroke(new BasicStroke());
1003
1004         if (groupIndex >= av.getAlignment().getGroups().size())
1005         {
1006           break;
1007         }
1008
1009         group = av.getAlignment().getGroups().get(groupIndex);
1010
1011       } while (groupIndex < av.getAlignment().getGroups().size());
1012
1013     }
1014
1015   }
1016
1017   /**
1018    * Highlights search results in the visible region by rendering as white text
1019    * on a black background. Any previous highlighting is removed. Answers true
1020    * if any highlight was left on the visible alignment (so status bar should be
1021    * set to match), else false.
1022    * <p>
1023    * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1024    * alignment had to be scrolled to show the highlighted region, then it should
1025    * be fully redrawn, otherwise a fast paint can be performed. This argument
1026    * could be removed if fast paint of scrolled wrapped alignment is coded in
1027    * future (JAL-2609).
1028    * 
1029    * @param results
1030    * @param noFastPaint
1031    * @return
1032    */
1033   public boolean highlightSearchResults(SearchResultsI results,
1034           boolean noFastPaint)
1035   {
1036     if (fastpainting)
1037     {
1038       return false;
1039     }
1040     boolean wrapped = av.getWrapAlignment();
1041
1042     try
1043     {
1044       fastPaint = !noFastPaint;
1045       fastpainting = fastPaint;
1046
1047       updateViewport();
1048
1049       /*
1050        * to avoid redrawing the whole visible region, we instead
1051        * redraw just the minimal regions to remove previous highlights
1052        * and add new ones
1053        */
1054       SearchResultsI previous = av.getSearchResults();
1055       av.setSearchResults(results);
1056       boolean redrawn = false;
1057       boolean drawn = false;
1058       if (wrapped)
1059       {
1060         redrawn = drawMappedPositionsWrapped(previous);
1061         drawn = drawMappedPositionsWrapped(results);
1062         redrawn |= drawn;
1063       }
1064       else
1065       {
1066         redrawn = drawMappedPositions(previous);
1067         drawn = drawMappedPositions(results);
1068         redrawn |= drawn;
1069       }
1070
1071       /*
1072        * if highlights were either removed or added, repaint
1073        */
1074       if (redrawn)
1075       {
1076         repaint();
1077       }
1078
1079       /*
1080        * return true only if highlights were added
1081        */
1082       return drawn;
1083
1084     } finally
1085     {
1086       fastpainting = false;
1087     }
1088   }
1089
1090   /**
1091    * Redraws the minimal rectangle in the visible region (if any) that includes
1092    * mapped positions of the given search results. Whether or not positions are
1093    * highlighted depends on the SearchResults set on the Viewport. This allows
1094    * this method to be called to either clear or set highlighting. Answers true
1095    * if any positions were drawn (in which case a repaint is still required),
1096    * else false.
1097    * 
1098    * @param results
1099    * @return
1100    */
1101   protected boolean drawMappedPositions(SearchResultsI results)
1102   {
1103     if (results == null)
1104     {
1105       return false;
1106     }
1107
1108     /*
1109      * calculate the minimal rectangle to redraw that 
1110      * includes both new and existing search results
1111      */
1112     int firstSeq = Integer.MAX_VALUE;
1113     int lastSeq = -1;
1114     int firstCol = Integer.MAX_VALUE;
1115     int lastCol = -1;
1116     boolean matchFound = false;
1117
1118     ViewportRanges ranges = av.getRanges();
1119     int firstVisibleColumn = ranges.getStartRes();
1120     int lastVisibleColumn = ranges.getEndRes();
1121     AlignmentI alignment = av.getAlignment();
1122     if (av.hasHiddenColumns())
1123     {
1124       firstVisibleColumn = alignment.getHiddenColumns()
1125               .adjustForHiddenColumns(firstVisibleColumn);
1126       lastVisibleColumn = alignment.getHiddenColumns()
1127               .adjustForHiddenColumns(lastVisibleColumn);
1128     }
1129
1130     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1131             .getEndSeq(); seqNo++)
1132     {
1133       SequenceI seq = alignment.getSequenceAt(seqNo);
1134
1135       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1136               lastVisibleColumn);
1137       if (visibleResults != null)
1138       {
1139         for (int i = 0; i < visibleResults.length - 1; i += 2)
1140         {
1141           int firstMatchedColumn = visibleResults[i];
1142           int lastMatchedColumn = visibleResults[i + 1];
1143           if (firstMatchedColumn <= lastVisibleColumn
1144                   && lastMatchedColumn >= firstVisibleColumn)
1145           {
1146             /*
1147              * found a search results match in the visible region - 
1148              * remember the first and last sequence matched, and the first
1149              * and last visible columns in the matched positions
1150              */
1151             matchFound = true;
1152             firstSeq = Math.min(firstSeq, seqNo);
1153             lastSeq = Math.max(lastSeq, seqNo);
1154             firstMatchedColumn = Math.max(firstMatchedColumn,
1155                     firstVisibleColumn);
1156             lastMatchedColumn = Math.min(lastMatchedColumn,
1157                     lastVisibleColumn);
1158             firstCol = Math.min(firstCol, firstMatchedColumn);
1159             lastCol = Math.max(lastCol, lastMatchedColumn);
1160           }
1161         }
1162       }
1163     }
1164
1165     if (matchFound)
1166     {
1167       if (av.hasHiddenColumns())
1168       {
1169         firstCol = alignment.getHiddenColumns()
1170                 .findColumnPosition(firstCol);
1171         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1172       }
1173       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1174       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1175       gg.translate(transX, transY);
1176       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1177       gg.translate(-transX, -transY);
1178     }
1179
1180     return matchFound;
1181   }
1182
1183   @Override
1184   public void propertyChange(PropertyChangeEvent evt)
1185   {
1186     String eventName = evt.getPropertyName();
1187
1188     // if (av.getWrapAlignment())
1189     // {
1190     // if (eventName.equals(ViewportRanges.STARTRES))
1191     // {
1192     // repaint();
1193     // }
1194     // }
1195     // else
1196     {
1197       int scrollX = 0;
1198       if (eventName.equals(ViewportRanges.STARTRES))
1199       {
1200         // Make sure we're not trying to draw a panel
1201         // larger than the visible window
1202         ViewportRanges vpRanges = av.getRanges();
1203         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1204         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1205         if (scrollX > range)
1206         {
1207           scrollX = range;
1208         }
1209         else if (scrollX < -range)
1210         {
1211           scrollX = -range;
1212         }
1213       }
1214
1215       // Both scrolling and resizing change viewport ranges: scrolling changes
1216       // both start and end points, but resize only changes end values.
1217       // Here we only want to fastpaint on a scroll, with resize using a normal
1218       // paint, so scroll events are identified as changes to the horizontal or
1219       // vertical start value.
1220       if (eventName.equals(ViewportRanges.STARTRES))
1221       {
1222         // scroll - startres and endres both change
1223         if (av.getWrapAlignment())
1224         {
1225           fastPaintWrapped(scrollX);
1226           // fastPaintWrapped(scrollX > 0 ? 1 : -1); // to debug: 1 at a time
1227         }
1228         else
1229         {
1230           fastPaint(scrollX, 0);
1231         }
1232       }
1233       else if (eventName.equals(ViewportRanges.STARTSEQ))
1234       {
1235         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1236       }
1237     }
1238   }
1239
1240   /**
1241    * Does a minimal update of the image for a scroll movement. This method
1242    * handles scroll movements of up to one width of the wrapped alignment (one
1243    * click in the vertical scrollbar). Larger movements (for example after a
1244    * scroll to highlight a mapped position) trigger a full redraw instead.
1245    * 
1246    * @param scrollX
1247    *          number of positions scrolled (right if positive, left if negative)
1248    */
1249   protected void fastPaintWrapped(int scrollX)
1250   {
1251     if (Math.abs(scrollX) > av.getRanges().getViewportWidth())
1252     {
1253       /*
1254        * shift of more than one view width is 
1255        * too complicated to handle in this method
1256        */
1257       fastPaint = false;
1258       repaint();
1259       return;
1260     }
1261
1262     fastPaint = true;
1263
1264     /*
1265      * relocate the regions of the alignment that are still visible
1266      */
1267     shiftWrappedAlignment(-scrollX);
1268
1269     /*
1270      * add new columns (scale above, sequence, annotation)
1271      * - at top left if scrollX < 0 
1272      * - at right of last two widths if scrollX > 0
1273      * also West scale top left or East scale bottom right if shown
1274      */
1275     if (scrollX < 0)
1276     {
1277       fastPaintWrappedAddLeft(-scrollX);
1278     }
1279     else
1280     {
1281       fastPaintWrappedAddRight(scrollX);
1282     }
1283
1284     repaint();
1285   }
1286
1287   /**
1288    * Draws the specified number of columns at the 'end' (bottom right) of a
1289    * wrapped alignment view, including scale above and right and annotations if
1290    * shown. Also draws the same number of columns at the right hand end of the
1291    * second last width shown, if the last width is not full height (so cannot
1292    * simply be copied from the graphics image).
1293    * 
1294    * @param columns
1295    */
1296   protected void fastPaintWrappedAddRight(int columns)
1297   {
1298     if (columns == 0)
1299     {
1300       return;
1301     }
1302
1303     /*
1304      * how many widths are visible? we will be adding
1305      * columns to the last visible width, right hand end
1306      */
1307     int repeatHeight = getRepeatHeightWrapped();
1308     int canvasHeight = getHeight();
1309     int visibleWidths = canvasHeight / repeatHeight;
1310     int remainder = canvasHeight % repeatHeight;
1311     int hgap = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1312     boolean lastWidthPartHeight = false;
1313     if (remainder >= (hgap + charHeight))
1314     {
1315       visibleWidths++;
1316       lastWidthPartHeight = true;
1317     }
1318
1319     /*
1320      * limit visible widths to max widths of alignment
1321      */
1322     ViewportRanges ranges = av.getRanges();
1323     int visibleAlignmentWidth = ranges.getVisibleAlignmentWidth();
1324     int viewportWidth = ranges.getViewportWidth();
1325     int maxWidths = visibleAlignmentWidth / viewportWidth;
1326     if (visibleAlignmentWidth % viewportWidth > 0)
1327     {
1328       maxWidths++;
1329     }
1330     visibleWidths = Math.min(visibleWidths, maxWidths);
1331     int widthInColumns = (getWidth() - labelWidthEast - labelWidthWest)
1332             / charWidth;
1333
1334     /**
1335      * draw full height alignment in the second last row, last columns, if the
1336      * last row was not full height
1337      */
1338     if (lastWidthPartHeight)
1339     {
1340       int widthsAbove = visibleWidths - 2;
1341       int ypos = repeatHeight * widthsAbove + hgap;
1342       int endRes = ranges.getEndRes();
1343       endRes += widthsAbove * viewportWidth;
1344       int startRes = endRes - columns;
1345       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1346               * charWidth;
1347       gg.translate(xOffset, 0);
1348
1349       /*
1350        * white fill first to erase annotations
1351        */
1352       gg.setColor(Color.white);
1353       gg.fillRect(labelWidthWest, ypos,
1354               (endRes - startRes + 1) * charWidth, repeatHeight);
1355
1356       drawWrappedRegion(gg, startRes, endRes, canvasHeight, widthInColumns,
1357               ypos);
1358       gg.translate(-xOffset, 0);
1359     }
1360
1361     /*
1362      * y-offset for drawing is height of widths above,
1363      * plus one gap row
1364      */
1365     int widthsAbove = visibleWidths - 1;
1366     int ypos = repeatHeight * widthsAbove + hgap;
1367     int endRes = ranges.getEndRes();
1368     endRes += widthsAbove * viewportWidth;
1369     endRes = Math.min(endRes, ranges.getVisibleAlignmentWidth());
1370
1371     /*
1372      * draw one extra column than strictly needed - this is a (harmless)
1373      * fudge to ensure scale marks get drawn (JAL-2636)
1374      */
1375     int startRes = endRes - columns;
1376
1377     /*
1378      * x-offset is x-start modulo viewport start residue;
1379      * doesn't include label West (offset is applied in drawWrappedRegion)
1380      */
1381
1382     int leftEndColumn = ranges.getStartRes() + widthsAbove
1383             * ranges.getViewportWidth();
1384     // startRes = Math.max(startRes - 0, leftEndColumn);
1385     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1386             * charWidth;
1387     gg.translate(xOffset, 0);
1388
1389     /*
1390      * white fill the region to be drawn including scale left or above;
1391      * extend to right hand margin so as to erase scale above when
1392      * scrolling right beyond end of alignment
1393      */
1394     gg.setColor(Color.white);
1395     int width = getWidth() - labelWidthWest - xOffset;
1396     gg.fillRect(labelWidthWest, ypos - hgap, width, repeatHeight);
1397
1398     gg.setFont(av.getFont());
1399     gg.setColor(Color.black);
1400
1401     drawWrappedRegion(gg, startRes, endRes, canvasHeight, widthInColumns,
1402             ypos);
1403     gg.translate(-xOffset, 0);
1404
1405     /*
1406      * draw scale right if shown, passing in the start/end columns
1407      * for the whole line, not just the last few columns
1408      */
1409     if (av.getScaleRightWrapped())
1410     {
1411       drawVerticalScale(gg, leftEndColumn, endRes, ypos, false);
1412     }
1413   }
1414
1415   /**
1416    * Draws the specified number of columns at the 'start' (top left) of a
1417    * wrapped alignment view, including scale above and left and annotations if
1418    * shown
1419    * 
1420    * @param columns
1421    */
1422   protected void fastPaintWrappedAddLeft(int columns)
1423   {
1424     int startRes = av.getRanges().getStartRes();
1425
1426     /*
1427      * draw one extra column than strictly needed - this is a (harmless)
1428      * fudge to ensure scale marks get drawn (JAL-2636)
1429      */
1430     int endx = startRes + columns;
1431     int ypos = 0;
1432
1433     /*
1434      * white fill the region to be drawn including scale left or above
1435      */
1436     gg.setColor(Color.white);
1437     int height = getRepeatHeightWrapped();
1438     gg.fillRect(0, ypos, labelWidthWest + columns * charWidth, height);
1439     ypos += charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1440
1441     gg.setFont(av.getFont());
1442     gg.setColor(Color.black);
1443
1444     if (av.getScaleLeftWrapped())
1445     {
1446       drawVerticalScale(gg, startRes, endx, ypos, true);
1447     }
1448
1449     int cWidth = (getWidth() - labelWidthEast - labelWidthWest) / charWidth;
1450
1451     drawWrappedRegion(gg, startRes, endx, getHeight(), cWidth, ypos);
1452   }
1453
1454   /**
1455    * Shifts the visible alignment by the specified number of columns - left if
1456    * negative, right if positive. Includes scale above, left or right and
1457    * annotations (if shown). Does not draw newly visible columns.
1458    * 
1459    * @param positions
1460    */
1461   protected void shiftWrappedAlignment(int positions)
1462   {
1463     if (positions == 0)
1464     {
1465       return;
1466     }
1467
1468     int repeatHeight = getRepeatHeightWrapped();
1469     ViewportRanges ranges = av.getRanges();
1470     int xMax = ranges.getVisibleAlignmentWidth();
1471     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
1472             * charWidth;
1473     int canvasHeight = getHeight();
1474     int visibleWidths = canvasHeight / repeatHeight;
1475     if (canvasHeight % repeatHeight > 0)
1476     {
1477       visibleWidths++;
1478     }
1479     int viewportWidth = ranges.getViewportWidth();
1480     int hgap = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1481
1482     int remainder = canvasHeight % repeatHeight;
1483     if (remainder >= (hgap + charHeight))
1484     {
1485       visibleWidths++;
1486     }
1487
1488     if (positions > 0)
1489     {
1490       /*
1491        * shift right (after scroll left)
1492        * for each wrapped width (starting with the last), copy (width-positions) 
1493        * columns from the left margin to the right margin, and copy positions 
1494        * columns from the right margin of the row above (if any) to the 
1495        * left margin of the current row
1496        */
1497       int xpos = ranges.getStartRes() + (visibleWidths - 1) * viewportWidth;
1498
1499       /*
1500        * get y-offset of last wrapped width
1501        */
1502       int y = canvasHeight / repeatHeight * repeatHeight;
1503       int copyFromLeftStart = labelWidthWest;
1504       int copyFromRightStart = copyFromLeftStart + widthToCopy;
1505
1506       while (y >= 0)
1507       {
1508         gg.copyArea(copyFromLeftStart, y, widthToCopy, repeatHeight,
1509                 positions * charWidth, 0);
1510         if (y > 0)
1511         {
1512           gg.copyArea(copyFromRightStart, y - repeatHeight, positions
1513                   * charWidth, repeatHeight, -widthToCopy, repeatHeight);
1514         }
1515
1516         if (av.getScaleLeftWrapped())
1517         {
1518           drawVerticalScale(gg, xpos, xpos + viewportWidth - 1, y + hgap,
1519                   true);
1520         }
1521         if (av.getScaleRightWrapped())
1522         {
1523           drawVerticalScale(gg, xpos, xpos + viewportWidth - 1, y + hgap,
1524                   false);
1525         }
1526
1527         y -= repeatHeight;
1528         xpos -= viewportWidth;
1529       }
1530     }
1531     else
1532     {
1533       /*
1534        * shift left (after scroll right)
1535        * for each wrapped width (starting with the first), copy (width-positions) 
1536        * columns from the right margin to the left margin, and copy positions 
1537        * columns from the left margin of the row below (if any) to the 
1538        * right margin of the current row
1539        */
1540       int xpos = ranges.getStartRes();
1541       int y = 0;
1542       int copyFromRightStart = labelWidthWest - positions * charWidth;
1543
1544       while (y < canvasHeight)
1545       {
1546         gg.copyArea(copyFromRightStart, y, widthToCopy, repeatHeight,
1547                 positions * charWidth, 0);
1548         if (y + repeatHeight < canvasHeight - repeatHeight
1549                 && (xpos + viewportWidth <= xMax))
1550         {
1551           gg.copyArea(labelWidthWest, y + repeatHeight, -positions
1552                   * charWidth, repeatHeight, widthToCopy, -repeatHeight);
1553         }
1554
1555         if (av.getScaleLeftWrapped())
1556         {
1557           drawVerticalScale(gg, xpos, xpos + viewportWidth, y + hgap, true);
1558         }
1559         if (av.getScaleRightWrapped())
1560         {
1561           drawVerticalScale(gg, xpos, xpos + viewportWidth, y + hgap, false);
1562         }
1563
1564         y += repeatHeight;
1565         xpos += viewportWidth;
1566       }
1567     }
1568   }
1569
1570   /**
1571    * Redraws any positions in the search results in the visible region of a
1572    * wrapped alignment. Any highlights are drawn depending on the search results
1573    * set on the Viewport, not the <code>results</code> argument. This allows
1574    * this method to be called either to clear highlights (passing the previous
1575    * search results), or to draw new highlights.
1576    * 
1577    * @param results
1578    * @return
1579    */
1580   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1581   {
1582     if (results == null)
1583     {
1584       return false;
1585     }
1586   
1587     boolean matchFound = false;
1588
1589     int wrappedWidth = av.getWrappedWidth();
1590     int wrappedHeight = getRepeatHeightWrapped();
1591
1592     ViewportRanges ranges = av.getRanges();
1593     int canvasHeight = getHeight();
1594     int repeats = canvasHeight / wrappedHeight;
1595     if (canvasHeight / wrappedHeight > 0)
1596     {
1597       repeats++;
1598     }
1599
1600     int firstVisibleColumn = ranges.getStartRes();
1601     int lastVisibleColumn = ranges.getStartRes() + repeats
1602             * ranges.getViewportWidth() - 1;
1603
1604     AlignmentI alignment = av.getAlignment();
1605     if (av.hasHiddenColumns())
1606     {
1607       firstVisibleColumn = alignment.getHiddenColumns()
1608               .adjustForHiddenColumns(firstVisibleColumn);
1609       lastVisibleColumn = alignment.getHiddenColumns()
1610               .adjustForHiddenColumns(lastVisibleColumn);
1611     }
1612
1613     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1614
1615     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1616             .getEndSeq(); seqNo++)
1617     {
1618       SequenceI seq = alignment.getSequenceAt(seqNo);
1619
1620       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1621               lastVisibleColumn);
1622       if (visibleResults != null)
1623       {
1624         for (int i = 0; i < visibleResults.length - 1; i += 2)
1625         {
1626           int firstMatchedColumn = visibleResults[i];
1627           int lastMatchedColumn = visibleResults[i + 1];
1628           if (firstMatchedColumn <= lastVisibleColumn
1629                   && lastMatchedColumn >= firstVisibleColumn)
1630           {
1631             /*
1632              * found a search results match in the visible region
1633              */
1634             firstMatchedColumn = Math.max(firstMatchedColumn,
1635                     firstVisibleColumn);
1636             lastMatchedColumn = Math.min(lastMatchedColumn,
1637                     lastVisibleColumn);
1638
1639             /*
1640              * draw each mapped position separately (as contiguous positions may
1641              * wrap across lines)
1642              */
1643             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1644             {
1645               int displayColumn = mappedPos;
1646               if (av.hasHiddenColumns())
1647               {
1648                 displayColumn = alignment.getHiddenColumns()
1649                         .findColumnPosition(displayColumn);
1650               }
1651
1652               /*
1653                * transX: offset from left edge of canvas to residue position
1654                */
1655               int transX = labelWidthWest
1656                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1657                       * av.getCharWidth();
1658
1659               /*
1660                * transY: offset from top edge of canvas to residue position
1661                */
1662               int transY = gapHeight;
1663               transY += (displayColumn - ranges.getStartRes())
1664                       / wrappedWidth * wrappedHeight;
1665               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1666
1667               /*
1668                * yOffset is from graphics origin to start of visible region
1669                */
1670               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1671               if (transY < getHeight())
1672               {
1673                 matchFound = true;
1674                 gg.translate(transX, transY);
1675                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1676                         yOffset);
1677                 gg.translate(-transX, -transY);
1678               }
1679             }
1680           }
1681         }
1682       }
1683     }
1684   
1685     return matchFound;
1686   }
1687
1688   /**
1689    * Answers the height in pixels of a repeating section of the wrapped
1690    * alignment, including space above, scale above if shown, sequences, and
1691    * annotation panel if shown
1692    * 
1693    * @return
1694    */
1695   protected int getRepeatHeightWrapped()
1696   {
1697     // gap (and maybe scale) above
1698     int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1699
1700     // add sequences
1701     repeatHeight += av.getRanges().getViewportHeight() * charHeight;
1702
1703     // add annotations panel height if shown
1704     repeatHeight += getAnnotationHeight();
1705
1706     return repeatHeight;
1707   }
1708 }