JAL-1858 highlights on wrapped panel - work in progress
[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.viewmodel.ViewportListenerI;
31 import jalview.viewmodel.ViewportRanges;
32
33 import java.awt.BasicStroke;
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.awt.RenderingHints;
40 import java.awt.Shape;
41 import java.awt.image.BufferedImage;
42 import java.beans.PropertyChangeEvent;
43 import java.util.List;
44
45 import javax.swing.JComponent;
46
47 /**
48  * DOCUMENT ME!
49  * 
50  * @author $author$
51  * @version $Revision$
52  */
53 public class SeqCanvas extends JComponent implements ViewportListenerI
54 {
55   private static String ZEROS = "0000000000";
56
57   final FeatureRenderer fr;
58
59   final SequenceRenderer sr;
60
61   BufferedImage img;
62
63   Graphics2D gg;
64
65   int imgWidth;
66
67   int imgHeight;
68
69   AlignViewport av;
70
71   boolean fastPaint = false;
72
73   int labelWidthWest;
74
75   int labelWidthEast;
76
77   int cursorX = 0;
78
79   int cursorY = 0;
80
81   /**
82    * Creates a new SeqCanvas object.
83    * 
84    * @param av
85    *          DOCUMENT ME!
86    */
87   public SeqCanvas(AlignmentPanel ap)
88   {
89     this.av = ap.av;
90     updateViewport();
91     fr = new FeatureRenderer(ap);
92     sr = new SequenceRenderer(av);
93     setLayout(new BorderLayout());
94     PaintRefresher.Register(this, av.getSequenceSetId());
95     setBackground(Color.white);
96
97     av.getRanges().addPropertyChangeListener(this);
98   }
99
100   public SequenceRenderer getSequenceRenderer()
101   {
102     return sr;
103   }
104
105   public FeatureRenderer getFeatureRenderer()
106   {
107     return fr;
108   }
109
110   int charHeight = 0, charWidth = 0;
111
112   private void updateViewport()
113   {
114     charHeight = av.getCharHeight();
115     charWidth = av.getCharWidth();
116   }
117
118   /**
119    * DOCUMENT ME!
120    * 
121    * @param g
122    *          DOCUMENT ME!
123    * @param startx
124    *          DOCUMENT ME!
125    * @param endx
126    *          DOCUMENT ME!
127    * @param ypos
128    *          DOCUMENT ME!
129    */
130   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
131   {
132     updateViewport();
133     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
134             endx))
135     {
136       int mpos = mark.column; // (i - startx - 1)
137       if (mpos < 0)
138       {
139         continue;
140       }
141       String mstring = mark.text;
142
143       if (mark.major)
144       {
145         if (mstring != null)
146         {
147           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
148         }
149         g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
150                 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
151                 ypos - 2);
152       }
153     }
154   }
155
156   /**
157    * DOCUMENT ME!
158    * 
159    * @param g
160    *          DOCUMENT ME!
161    * @param startx
162    *          DOCUMENT ME!
163    * @param endx
164    *          DOCUMENT ME!
165    * @param ypos
166    *          DOCUMENT ME!
167    */
168   void drawWestScale(Graphics g, int startx, int endx, int ypos)
169   {
170     FontMetrics fm = getFontMetrics(av.getFont());
171     ypos += charHeight;
172
173     if (av.hasHiddenColumns())
174     {
175       startx = av.getAlignment().getHiddenColumns()
176               .adjustForHiddenColumns(startx);
177       endx = av.getAlignment().getHiddenColumns()
178               .adjustForHiddenColumns(endx);
179     }
180
181     int maxwidth = av.getAlignment().getWidth();
182     if (av.hasHiddenColumns())
183     {
184       maxwidth = av.getAlignment().getHiddenColumns()
185               .findColumnPosition(maxwidth) - 1;
186     }
187
188     // WEST SCALE
189     for (int i = 0; i < av.getAlignment().getHeight(); i++)
190     {
191       SequenceI seq = av.getAlignment().getSequenceAt(i);
192       int index = startx;
193       int value = -1;
194
195       while (index < endx)
196       {
197         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
198         {
199           index++;
200
201           continue;
202         }
203
204         value = av.getAlignment().getSequenceAt(i).findPosition(index);
205
206         break;
207       }
208
209       if (value != -1)
210       {
211         int x = labelWidthWest - fm.stringWidth(String.valueOf(value))
212                 - charWidth / 2;
213         g.drawString(value + "", x, (ypos + (i * charHeight))
214                 - (charHeight / 5));
215       }
216     }
217   }
218
219   /**
220    * DOCUMENT ME!
221    * 
222    * @param g
223    *          DOCUMENT ME!
224    * @param startx
225    *          DOCUMENT ME!
226    * @param endx
227    *          DOCUMENT ME!
228    * @param ypos
229    *          DOCUMENT ME!
230    */
231   void drawEastScale(Graphics g, int startx, int endx, int ypos)
232   {
233     ypos += charHeight;
234
235     if (av.hasHiddenColumns())
236     {
237       endx = av.getAlignment().getHiddenColumns()
238               .adjustForHiddenColumns(endx);
239     }
240
241     SequenceI seq;
242     // EAST SCALE
243     for (int i = 0; i < av.getAlignment().getHeight(); i++)
244     {
245       seq = av.getAlignment().getSequenceAt(i);
246       int index = endx;
247       int value = -1;
248
249       while (index > startx)
250       {
251         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
252         {
253           index--;
254
255           continue;
256         }
257
258         value = seq.findPosition(index);
259
260         break;
261       }
262
263       if (value != -1)
264       {
265         g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
266                 - (charHeight / 5));
267       }
268     }
269   }
270
271   boolean fastpainting = false;
272
273   /**
274    * need to make this thread safe move alignment rendering in response to
275    * slider adjustment
276    * 
277    * @param horizontal
278    *          shift along
279    * @param vertical
280    *          shift up or down in repaint
281    */
282   public void fastPaint(int horizontal, int vertical)
283   {
284     if (fastpainting || gg == null)
285     {
286       return;
287     }
288     fastpainting = true;
289     fastPaint = true;
290     updateViewport();
291     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
292             imgHeight, -horizontal * charWidth, -vertical * charHeight);
293
294     ViewportRanges ranges = av.getRanges();
295     int startRes = ranges.getStartRes();
296     int endRes = ranges.getEndRes();
297     int startSeq = ranges.getStartSeq();
298     int endSeq = ranges.getEndSeq();
299     int transX = 0;
300     int transY = 0;
301
302     if (horizontal > 0) // scrollbar pulled right, image to the left
303     {
304       transX = (endRes - startRes - horizontal) * charWidth;
305       startRes = endRes - horizontal;
306     }
307     else if (horizontal < 0)
308     {
309       endRes = startRes - horizontal;
310     }
311     else if (vertical > 0) // scroll down
312     {
313       startSeq = endSeq - vertical;
314
315       if (startSeq < ranges.getStartSeq())
316       { // ie scrolling too fast, more than a page at a time
317         startSeq = ranges.getStartSeq();
318       }
319       else
320       {
321         transY = imgHeight - ((vertical + 1) * charHeight);
322       }
323     }
324     else if (vertical < 0)
325     {
326       endSeq = startSeq - vertical;
327
328       if (endSeq > ranges.getEndSeq())
329       {
330         endSeq = ranges.getEndSeq();
331       }
332     }
333
334     gg.translate(transX, transY);
335     drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
336     gg.translate(-transX, -transY);
337
338     repaint();
339     fastpainting = false;
340   }
341
342   @Override
343   public void paintComponent(Graphics g)
344   {
345     updateViewport();
346     BufferedImage lcimg = img; // take reference since other threads may null
347     // img and call later.
348     super.paintComponent(g);
349
350     if (lcimg != null
351             && (fastPaint
352                     || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
353                     .getClipBounds().height)))
354     {
355       g.drawImage(lcimg, 0, 0, this);
356       fastPaint = false;
357       return;
358     }
359
360     // this draws the whole of the alignment
361     imgWidth = getWidth();
362     imgHeight = getHeight();
363
364     imgWidth -= (imgWidth % charWidth);
365     imgHeight -= (imgHeight % charHeight);
366
367     if ((imgWidth < 1) || (imgHeight < 1))
368     {
369       return;
370     }
371
372     if (lcimg == null || imgWidth != lcimg.getWidth()
373             || imgHeight != lcimg.getHeight())
374     {
375       try
376       {
377         lcimg = img = new BufferedImage(imgWidth, imgHeight,
378                 BufferedImage.TYPE_INT_RGB);
379         gg = (Graphics2D) img.getGraphics();
380         gg.setFont(av.getFont());
381       } catch (OutOfMemoryError er)
382       {
383         System.gc();
384         System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
385         new OOMWarning("Creating alignment image for display", er);
386
387         return;
388       }
389     }
390
391     if (av.antiAlias)
392     {
393       gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
394               RenderingHints.VALUE_ANTIALIAS_ON);
395     }
396
397     gg.setColor(Color.white);
398     gg.fillRect(0, 0, imgWidth, imgHeight);
399
400     ViewportRanges ranges = av.getRanges();
401     if (av.getWrapAlignment())
402     {
403       drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
404     }
405     else
406     {
407       drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
408               ranges.getStartSeq(), ranges.getEndSeq(), 0);
409     }
410
411     g.drawImage(lcimg, 0, 0, this);
412
413   }
414
415   /**
416    * DOCUMENT ME!
417    * 
418    * @param cwidth
419    *          DOCUMENT ME!
420    * 
421    * @return DOCUMENT ME!
422    */
423   public int getWrappedCanvasWidth(int cwidth)
424   {
425     FontMetrics fm = getFontMetrics(av.getFont());
426
427     labelWidthEast = 0;
428     labelWidthWest = 0;
429
430     if (av.getScaleRightWrapped())
431     {
432       labelWidthEast = getLabelWidth(fm);
433     }
434
435     if (av.getScaleLeftWrapped())
436     {
437       labelWidthWest = getLabelWidth(fm);
438     }
439
440     return (cwidth - labelWidthEast - labelWidthWest) / charWidth;
441   }
442
443   /**
444    * Returns a pixel width suitable for showing the largest sequence coordinate
445    * (end position) in the alignment. Returns 2 plus the number of decimal
446    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
447    * 
448    * @param fm
449    * @return
450    */
451   protected int getLabelWidth(FontMetrics fm)
452   {
453     int maxWidth = 0;
454     for (int i = 0; i < av.getAlignment().getHeight(); i++)
455     {
456       maxWidth = Math.max(maxWidth, av.getAlignment().getSequenceAt(i)
457               .getEnd());
458     }
459
460     int length = 2;
461     for (int i = maxWidth; i > 0; i /= 10)
462     {
463       length++;
464     }
465
466     return fm.stringWidth(ZEROS.substring(0, length));
467   }
468
469   /**
470    * DOCUMENT ME!
471    * 
472    * @param g
473    *          DOCUMENT ME!
474    * @param canvasWidth
475    *          DOCUMENT ME!
476    * @param canvasHeight
477    *          DOCUMENT ME!
478    * @param startRes
479    *          DOCUMENT ME!
480    */
481   public void drawWrappedPanel(Graphics g, int canvasWidth,
482           int canvasHeight, int startRes)
483   {
484     updateViewport();
485     AlignmentI al = av.getAlignment();
486
487     int labelWidth = 0;
488     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
489     {
490       FontMetrics fm = getFontMetrics(av.getFont());
491       labelWidth = getLabelWidth(fm);
492     }
493
494     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
495     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
496
497     int hgap = charHeight;
498     if (av.getScaleAboveWrapped())
499     {
500       hgap += charHeight;
501     }
502
503     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
504     int cHeight = av.getAlignment().getHeight() * charHeight;
505
506     av.setWrappedWidth(cWidth);
507
508     av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
509
510     int endx;
511     int ypos = hgap;
512     int maxwidth = av.getAlignment().getWidth() - 1;
513
514     if (av.hasHiddenColumns())
515     {
516       maxwidth = av.getAlignment().getHiddenColumns()
517               .findColumnPosition(maxwidth) - 1;
518     }
519
520     int annotationHeight = getAnnotationHeight();
521
522     while ((ypos <= canvasHeight) && (startRes < maxwidth))
523     {
524       endx = startRes + cWidth - 1;
525
526       if (endx > maxwidth)
527       {
528         endx = maxwidth;
529       }
530
531       g.setFont(av.getFont());
532       g.setColor(Color.black);
533
534       if (av.getScaleLeftWrapped())
535       {
536         drawWestScale(g, startRes, endx, ypos);
537       }
538
539       if (av.getScaleRightWrapped())
540       {
541         g.translate(canvasWidth - labelWidthEast, 0);
542         drawEastScale(g, startRes, endx, ypos);
543         g.translate(-(canvasWidth - labelWidthEast), 0);
544       }
545
546       g.translate(labelWidthWest, 0);
547
548       if (av.getScaleAboveWrapped())
549       {
550         drawNorthScale(g, startRes, endx, ypos);
551       }
552
553       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
554       {
555         g.setColor(Color.blue);
556         int res;
557         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
558         for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
559         {
560           res = hidden.findHiddenRegionPosition(i) - startRes;
561
562           if (res < 0 || res > endx - startRes)
563           {
564             continue;
565           }
566
567           gg.fillPolygon(
568                   new int[] { res * charWidth - charHeight / 4,
569                       res * charWidth + charHeight / 4, res * charWidth },
570                   new int[] { ypos - (charHeight / 2),
571                       ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
572                   3);
573
574         }
575       }
576
577       // When printing we have an extra clipped region,
578       // the Printable page which we need to account for here
579       Shape clip = g.getClip();
580
581       if (clip == null)
582       {
583         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
584       }
585       else
586       {
587         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
588                 (int) clip.getBounds().getHeight());
589       }
590
591       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
592
593       if (av.isShowAnnotation())
594       {
595         g.translate(0, cHeight + ypos + 3);
596         if (annotations == null)
597         {
598           annotations = new AnnotationPanel(av);
599         }
600
601         annotations.renderer.drawComponent(annotations, av, g, -1,
602                 startRes, endx + 1);
603         g.translate(0, -cHeight - ypos - 3);
604       }
605       g.setClip(clip);
606       g.translate(-labelWidthWest, 0);
607
608       ypos += cHeight + annotationHeight + hgap;
609
610       startRes += cWidth;
611     }
612   }
613
614   AnnotationPanel annotations;
615
616   int getAnnotationHeight()
617   {
618     if (!av.isShowAnnotation())
619     {
620       return 0;
621     }
622
623     if (annotations == null)
624     {
625       annotations = new AnnotationPanel(av);
626     }
627
628     return annotations.adjustPanelHeight();
629   }
630
631   /**
632    * Draws the visible region of the alignment on the graphics context. If there
633    * are hidden column markers in the visible region, then each sub-region
634    * between the markers is drawn separately, followed by the hidden column
635    * marker.
636    * 
637    * @param g1
638    * @param startRes
639    *          offset of the first column in the visible region (0..)
640    * @param endRes
641    *          offset of the last column in the visible region (0..)
642    * @param startSeq
643    *          offset of the first sequence in the visible region (0..)
644    * @param endSeq
645    *          offset of the last sequence in the visible region (0..)
646    * @param yOffset
647    *          vertical offset at which to draw (for wrapped alignments)
648    */
649   public void drawPanel(Graphics g1, final int startRes, final int endRes,
650           final int startSeq, final int endSeq, final int yOffset)
651   {
652     updateViewport();
653     if (!av.hasHiddenColumns())
654     {
655       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
656     }
657     else
658     {
659       List<int[]> regions = av.getAlignment().getHiddenColumns()
660               .getHiddenRegions();
661
662       int screenY = 0;
663       final int screenYMax = endRes - startRes;
664       int blockStart = startRes;
665       int blockEnd = endRes;
666
667       for (int[] region : regions)
668       {
669         int hideStart = region[0];
670         int hideEnd = region[1];
671
672         if (hideStart <= blockStart)
673         {
674           blockStart += (hideEnd - hideStart) + 1;
675           continue;
676         }
677
678         /*
679          * draw up to just before the next hidden region, or the end of
680          * the visible region, whichever comes first
681          */
682         blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
683                 - screenY);
684
685         g1.translate(screenY * charWidth, 0);
686
687         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
688
689         /*
690          * draw the downline of the hidden column marker (ScalePanel draws the
691          * triangle on top) if we reached it
692          */
693         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
694         {
695           g1.setColor(Color.blue);
696
697           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
698                   0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
699                   (endSeq - startSeq + 1) * charHeight + yOffset);
700         }
701
702         g1.translate(-screenY * charWidth, 0);
703         screenY += blockEnd - blockStart + 1;
704         blockStart = hideEnd + 1;
705
706         if (screenY > screenYMax)
707         {
708           // already rendered last block
709           return;
710         }
711       }
712
713       if (screenY <= screenYMax)
714       {
715         // remaining visible region to render
716         blockEnd = blockStart + screenYMax - screenY;
717         g1.translate(screenY * charWidth, 0);
718         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
719
720         g1.translate(-screenY * charWidth, 0);
721       }
722     }
723
724   }
725
726   /**
727    * Draws a region of the visible alignment
728    * 
729    * @param g1
730    * @param startRes
731    *          offset of the first column in the visible region (0..)
732    * @param endRes
733    *          offset of the last column in the visible region (0..)
734    * @param startSeq
735    *          offset of the first sequence in the visible region (0..)
736    * @param endSeq
737    *          offset of the last sequence in the visible region (0..)
738    * @param yOffset
739    *          vertical offset at which to draw (for wrapped alignments)
740    */
741   private void draw(Graphics g, int startRes, int endRes, int startSeq,
742           int endSeq, int offset)
743   {
744     g.setFont(av.getFont());
745     sr.prepare(g, av.isRenderGaps());
746
747     SequenceI nextSeq;
748
749     // / First draw the sequences
750     // ///////////////////////////
751     for (int i = startSeq; i <= endSeq; i++)
752     {
753       nextSeq = av.getAlignment().getSequenceAt(i);
754       if (nextSeq == null)
755       {
756         // occasionally, a race condition occurs such that the alignment row is
757         // empty
758         continue;
759       }
760       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
761               startRes, endRes, offset + ((i - startSeq) * charHeight));
762
763       if (av.isShowSequenceFeatures())
764       {
765         fr.drawSequence(g, nextSeq, startRes, endRes, offset
766                 + ((i - startSeq) * charHeight), false);
767       }
768
769       /*
770        * highlight search Results once sequence has been drawn
771        */
772       if (av.hasSearchResults())
773       {
774         SearchResultsI searchResults = av.getSearchResults();
775         int[] visibleResults = searchResults.getResults(nextSeq,
776                 startRes, endRes);
777         if (visibleResults != null)
778         {
779           for (int r = 0; r < visibleResults.length; r += 2)
780           {
781             sr.drawHighlightedText(nextSeq, visibleResults[r],
782                     visibleResults[r + 1], (visibleResults[r] - startRes)
783                             * charWidth, offset
784                             + ((i - startSeq) * charHeight));
785           }
786         }
787       }
788
789       if (av.cursorMode && cursorY == i && cursorX >= startRes
790               && cursorX <= endRes)
791       {
792         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
793                 offset + ((i - startSeq) * charHeight));
794       }
795     }
796
797     if (av.getSelectionGroup() != null
798             || av.getAlignment().getGroups().size() > 0)
799     {
800       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
801     }
802
803   }
804
805   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
806           int startSeq, int endSeq, int offset)
807   {
808     Graphics2D g = (Graphics2D) g1;
809     //
810     // ///////////////////////////////////
811     // Now outline any areas if necessary
812     // ///////////////////////////////////
813     SequenceGroup group = av.getSelectionGroup();
814
815     int sx = -1;
816     int sy = -1;
817     int ex = -1;
818     int groupIndex = -1;
819     int visWidth = (endRes - startRes + 1) * charWidth;
820
821     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
822     {
823       group = av.getAlignment().getGroups().get(0);
824       groupIndex = 0;
825     }
826
827     if (group != null)
828     {
829       do
830       {
831         int oldY = -1;
832         int i = 0;
833         boolean inGroup = false;
834         int top = -1;
835         int bottom = -1;
836
837         for (i = startSeq; i <= endSeq; i++)
838         {
839           sx = (group.getStartRes() - startRes) * charWidth;
840           sy = offset + ((i - startSeq) * charHeight);
841           ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
842
843           if (sx + ex < 0 || sx > visWidth)
844           {
845             continue;
846           }
847
848           if ((sx <= (endRes - startRes) * charWidth)
849                   && group.getSequences(null).contains(
850                           av.getAlignment().getSequenceAt(i)))
851           {
852             if ((bottom == -1)
853                     && !group.getSequences(null).contains(
854                             av.getAlignment().getSequenceAt(i + 1)))
855             {
856               bottom = sy + charHeight;
857             }
858
859             if (!inGroup)
860             {
861               if (((top == -1) && (i == 0))
862                       || !group.getSequences(null).contains(
863                               av.getAlignment().getSequenceAt(i - 1)))
864               {
865                 top = sy;
866               }
867
868               oldY = sy;
869               inGroup = true;
870
871               if (group == av.getSelectionGroup())
872               {
873                 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
874                         BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
875                         0f));
876                 g.setColor(Color.RED);
877               }
878               else
879               {
880                 g.setStroke(new BasicStroke());
881                 g.setColor(group.getOutlineColour());
882               }
883             }
884           }
885           else
886           {
887             if (inGroup)
888             {
889               if (sx >= 0 && sx < visWidth)
890               {
891                 g.drawLine(sx, oldY, sx, sy);
892               }
893
894               if (sx + ex < visWidth)
895               {
896                 g.drawLine(sx + ex, oldY, sx + ex, sy);
897               }
898
899               if (sx < 0)
900               {
901                 ex += sx;
902                 sx = 0;
903               }
904
905               if (sx + ex > visWidth)
906               {
907                 ex = visWidth;
908               }
909
910               else if (sx + ex >= (endRes - startRes + 1) * charWidth)
911               {
912                 ex = (endRes - startRes + 1) * charWidth;
913               }
914
915               if (top != -1)
916               {
917                 g.drawLine(sx, top, sx + ex, top);
918                 top = -1;
919               }
920
921               if (bottom != -1)
922               {
923                 g.drawLine(sx, bottom, sx + ex, bottom);
924                 bottom = -1;
925               }
926
927               inGroup = false;
928             }
929           }
930         }
931
932         if (inGroup)
933         {
934           sy = offset + ((i - startSeq) * charHeight);
935           if (sx >= 0 && sx < visWidth)
936           {
937             g.drawLine(sx, oldY, sx, sy);
938           }
939
940           if (sx + ex < visWidth)
941           {
942             g.drawLine(sx + ex, oldY, sx + ex, sy);
943           }
944
945           if (sx < 0)
946           {
947             ex += sx;
948             sx = 0;
949           }
950
951           if (sx + ex > visWidth)
952           {
953             ex = visWidth;
954           }
955           else if (sx + ex >= (endRes - startRes + 1) * charWidth)
956           {
957             ex = (endRes - startRes + 1) * charWidth;
958           }
959
960           if (top != -1)
961           {
962             g.drawLine(sx, top, sx + ex, top);
963             top = -1;
964           }
965
966           if (bottom != -1)
967           {
968             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
969             bottom = -1;
970           }
971
972           inGroup = false;
973         }
974
975         groupIndex++;
976
977         g.setStroke(new BasicStroke());
978
979         if (groupIndex >= av.getAlignment().getGroups().size())
980         {
981           break;
982         }
983
984         group = av.getAlignment().getGroups().get(groupIndex);
985
986       } while (groupIndex < av.getAlignment().getGroups().size());
987
988     }
989
990   }
991
992   /**
993    * Highlights search results in the visible region by rendering as white text
994    * on a black background. Any previous highlighting is removed. Answers true
995    * if any highlight was left on the visible alignment (so status bar should be
996    * set to match), else false.
997    * 
998    * @param results
999    * @return
1000    */
1001   public boolean highlightSearchResults(SearchResultsI results)
1002   {
1003     updateViewport();
1004
1005     /*
1006      * for now, don't attempt fastpaint if wrapped format
1007      */
1008     boolean wrapped = av.getWrapAlignment();
1009     if (wrapped)
1010     {
1011       // drawWrappedMappedPositions(results);
1012       // img = null;
1013       // av.setSearchResults(results);
1014       // repaint();
1015       // return;
1016     }
1017     
1018     fastpainting = true;
1019     fastPaint = true;
1020
1021     try
1022     {
1023       /*
1024        * to avoid redrawing the whole visible region, we instead
1025        * redraw just the minimal regions to remove previous highlights
1026        * and add new ones
1027        */
1028       SearchResultsI previous = av.getSearchResults();
1029       av.setSearchResults(results);
1030       boolean redrawn = false;
1031       boolean drawn = false;
1032       if (wrapped)
1033       {
1034         redrawn = drawWrappedMappedPositions(previous);
1035         drawn = drawWrappedMappedPositions(results);
1036         redrawn |= drawn;
1037       }
1038       else
1039       {
1040         redrawn = drawMappedPositions(previous);
1041         drawn = drawMappedPositions(results);
1042         redrawn |= drawn;
1043       }
1044
1045       /*
1046        * if highlights were either removed or added, repaint
1047        */
1048       if (redrawn)
1049       {
1050         repaint();
1051       }
1052
1053       /*
1054        * return true only if highlights were added
1055        */
1056       return drawn;
1057
1058     } finally
1059     {
1060       fastpainting = false;
1061     }
1062
1063   }
1064
1065   /**
1066    * Redraws the minimal rectangle in the visible region (if any) that includes
1067    * mapped positions of the given search results. Whether or not positions are
1068    * highlighted depends on the SearchResults set on the Viewport. This allows
1069    * this method to be called to either clear or set highlighting. Answers true
1070    * if any positions were drawn (in which case a repaint is still required),
1071    * else false.
1072    * 
1073    * @param results
1074    * @return
1075    */
1076   protected boolean drawMappedPositions(SearchResultsI results)
1077   {
1078     if (results == null)
1079     {
1080       return false;
1081     }
1082
1083     /*
1084      * calculate the minimal rectangle to redraw that 
1085      * includes both new and existing search results
1086      */
1087     int firstSeq = Integer.MAX_VALUE;
1088     int lastSeq = -1;
1089     int firstCol = Integer.MAX_VALUE;
1090     int lastCol = -1;
1091     boolean matchFound = false;
1092
1093     ViewportRanges ranges = av.getRanges();
1094     int firstVisibleColumn = ranges.getStartRes();
1095     int lastVisibleColumn = ranges.getEndRes();
1096     AlignmentI alignment = av.getAlignment();
1097     if (av.hasHiddenColumns())
1098     {
1099       firstVisibleColumn = alignment.getHiddenColumns()
1100               .adjustForHiddenColumns(firstVisibleColumn);
1101       lastVisibleColumn = alignment.getHiddenColumns()
1102               .adjustForHiddenColumns(lastVisibleColumn);
1103     }
1104
1105     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1106             .getEndSeq(); seqNo++)
1107     {
1108       SequenceI seq = alignment.getSequenceAt(seqNo);
1109
1110       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1111               lastVisibleColumn);
1112       if (visibleResults != null)
1113       {
1114         for (int i = 0; i < visibleResults.length - 1; i += 2)
1115         {
1116           int firstMatchedColumn = visibleResults[i];
1117           int lastMatchedColumn = visibleResults[i + 1];
1118           if (firstMatchedColumn <= lastVisibleColumn
1119                   && lastMatchedColumn >= firstVisibleColumn)
1120           {
1121             /*
1122              * found a search results match in the visible region - 
1123              * remember the first and last sequence matched, and the first
1124              * and last visible columns in the matched positions
1125              */
1126             matchFound = true;
1127             firstSeq = Math.min(firstSeq, seqNo);
1128             lastSeq = Math.max(lastSeq, seqNo);
1129             firstMatchedColumn = Math.max(firstMatchedColumn,
1130                     firstVisibleColumn);
1131             lastMatchedColumn = Math.min(lastMatchedColumn,
1132                     lastVisibleColumn);
1133             firstCol = Math.min(firstCol, firstMatchedColumn);
1134             lastCol = Math.max(lastCol, lastMatchedColumn);
1135           }
1136         }
1137       }
1138     }
1139
1140     if (matchFound)
1141     {
1142       if (av.hasHiddenColumns())
1143       {
1144         firstCol = alignment.getHiddenColumns()
1145                 .findColumnPosition(firstCol);
1146         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1147       }
1148       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1149       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1150       gg.translate(transX, transY);
1151       drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1152       gg.translate(-transX, -transY);
1153     }
1154
1155     return matchFound;
1156   }
1157
1158   @Override
1159   public void propertyChange(PropertyChangeEvent evt)
1160   {
1161     if (!av.getWrapAlignment())
1162     {
1163       if (evt.getPropertyName().equals("startres")
1164               || evt.getPropertyName().equals("endres"))
1165       {
1166         // Make sure we're not trying to draw a panel
1167         // larger than the visible window
1168         ViewportRanges vpRanges = av.getRanges();
1169         int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1170         if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
1171         {
1172           scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
1173         }
1174         else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
1175         {
1176           scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
1177         }
1178         fastPaint(scrollX, 0);
1179       }
1180       else if (evt.getPropertyName().equals("startseq")
1181               || evt.getPropertyName().equals("endseq"))
1182       {
1183         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1184       }
1185     }
1186   }
1187
1188   /**
1189    * Redraws any positions in the search results in the visible region. Any
1190    * highlights are drawn depending on the search results set on the Viewport,
1191    * not the results parameter. This allows this method to be called to either
1192    * clear highlighting (passing the previous search results), or set new
1193    * highlighting.
1194    * 
1195    * @param results
1196    * @return
1197    */
1198   protected boolean drawWrappedMappedPositions(SearchResultsI results)
1199   {
1200     if (results == null)
1201     {
1202       return false;
1203     }
1204   
1205     boolean matchFound = false;
1206   
1207     /*
1208      * Viewport ranges are set for the 'row' of the wrapped alignment
1209      * the cursor is in, not the whole visible region; really we want
1210      * the latter; +-6 a temporary fudge for codons wrapping across lines
1211      */
1212     ViewportRanges ranges = av.getRanges();
1213     int firstVisibleColumn = ranges.getStartRes() - 6;
1214     int lastVisibleColumn = ranges.getEndRes() + 6;
1215     AlignmentI alignment = av.getAlignment();
1216     if (av.hasHiddenColumns())
1217     {
1218       firstVisibleColumn = alignment.getHiddenColumns()
1219               .adjustForHiddenColumns(firstVisibleColumn);
1220       lastVisibleColumn = alignment.getHiddenColumns()
1221               .adjustForHiddenColumns(lastVisibleColumn);
1222     }
1223   
1224     /*
1225      * find width of alignment in residues, and height of alignment, 
1226      * so we can calculate where to render each matched position
1227      */
1228     int wrappedWidth = av.getWrappedWidth();
1229     int wrappedHeight = av.getAlignment().getHeight() * av.getCharHeight();
1230     int gapHeight = av.getCharHeight()
1231             * (av.getScaleAboveWrapped() ? 2 : 1);
1232     wrappedHeight += gapHeight;
1233     wrappedHeight += getAnnotationHeight();
1234
1235     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1236             .getEndSeq(); seqNo++)
1237     {
1238       SequenceI seq = alignment.getSequenceAt(seqNo);
1239
1240       int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1241               lastVisibleColumn);
1242       if (visibleResults != null)
1243       {
1244         for (int i = 0; i < visibleResults.length - 1; i += 2)
1245         {
1246           int firstMatchedColumn = visibleResults[i];
1247           int lastMatchedColumn = visibleResults[i + 1];
1248           if (firstMatchedColumn <= lastVisibleColumn
1249                   && lastMatchedColumn >= firstVisibleColumn)
1250           {
1251             /*
1252              * found a search results match in the visible region
1253              */
1254             firstMatchedColumn = Math.max(firstMatchedColumn,
1255                     firstVisibleColumn);
1256             lastMatchedColumn = Math.min(lastMatchedColumn,
1257                     lastVisibleColumn);
1258
1259             /*
1260              * draw each mapped position separately (as contiguous positions may
1261              * wrap across lines)
1262              */
1263             for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1264             {
1265               int displayColumn = mappedPos;
1266               if (av.hasHiddenColumns())
1267               {
1268                 displayColumn = alignment.getHiddenColumns()
1269                         .findColumnPosition(displayColumn);
1270               }
1271
1272               /*
1273                * transX: offset from left edge of canvas to residue position
1274                */
1275               int transX = labelWidthWest
1276                       + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1277                       * av.getCharWidth();
1278
1279               /*
1280                * transY: offset from top edge of canvas to residue position
1281                */
1282               int transY = gapHeight;
1283               transY += (displayColumn / wrappedWidth) * wrappedHeight;
1284               transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1285
1286               /*
1287                * yOffset is from graphics origin to start of visible region
1288                */
1289               int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1290               if (transY < getHeight())
1291               {
1292                 matchFound = true;
1293                 gg.translate(transX, transY);
1294                 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1295                         yOffset);
1296                 gg.translate(-transX, -transY);
1297               }
1298             }
1299           }
1300         }
1301       }
1302     }
1303   
1304     return matchFound;
1305   }
1306 }