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