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