2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.SearchResults;
26 import jalview.datamodel.SearchResultsI;
27 import jalview.datamodel.SequenceGroup;
28 import jalview.datamodel.SequenceI;
29 import jalview.renderer.ScaleRenderer;
30 import jalview.renderer.ScaleRenderer.ScaleMark;
31 import jalview.viewmodel.ViewportListenerI;
32 import jalview.viewmodel.ViewportRanges;
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;
46 import javax.swing.JComponent;
54 public class SeqCanvas extends JComponent implements ViewportListenerI
56 final FeatureRenderer fr;
58 final SequenceRenderer sr;
70 boolean fastPaint = false;
81 * Creates a new SeqCanvas object.
86 public SeqCanvas(AlignmentPanel ap)
90 fr = new FeatureRenderer(ap);
91 sr = new SequenceRenderer(av);
92 setLayout(new BorderLayout());
93 PaintRefresher.Register(this, av.getSequenceSetId());
94 setBackground(Color.white);
96 av.getRanges().addPropertyChangeListener(this);
99 public SequenceRenderer getSequenceRenderer()
104 public FeatureRenderer getFeatureRenderer()
109 int charHeight = 0, charWidth = 0;
111 private void updateViewport()
113 charHeight = av.getCharHeight();
114 charWidth = av.getCharWidth();
129 private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
132 for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
135 int mpos = mark.column; // (i - startx - 1)
140 String mstring = mark.text;
146 g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
148 g.drawLine((mpos * charWidth) + (charWidth / 2), (ypos + 2)
149 - (charHeight / 2), (mpos * charWidth) + (charWidth / 2),
167 void drawWestScale(Graphics g, int startx, int endx, int ypos)
169 FontMetrics fm = getFontMetrics(av.getFont());
172 if (av.hasHiddenColumns())
174 startx = av.getAlignment().getHiddenColumns()
175 .adjustForHiddenColumns(startx);
176 endx = av.getAlignment().getHiddenColumns()
177 .adjustForHiddenColumns(endx);
180 int maxwidth = av.getAlignment().getWidth();
181 if (av.hasHiddenColumns())
183 maxwidth = av.getAlignment().getHiddenColumns()
184 .findColumnPosition(maxwidth) - 1;
188 for (int i = 0; i < av.getAlignment().getHeight(); i++)
190 SequenceI seq = av.getAlignment().getSequenceAt(i);
196 if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
203 value = av.getAlignment().getSequenceAt(i).findPosition(index);
210 int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
212 g.drawString(value + "", x, (ypos + (i * charHeight))
230 void drawEastScale(Graphics g, int startx, int endx, int ypos)
234 if (av.hasHiddenColumns())
236 endx = av.getAlignment().getHiddenColumns()
237 .adjustForHiddenColumns(endx);
242 for (int i = 0; i < av.getAlignment().getHeight(); i++)
244 seq = av.getAlignment().getSequenceAt(i);
248 while (index > startx)
250 if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
257 value = seq.findPosition(index);
264 g.drawString(String.valueOf(value), 0, (ypos + (i * charHeight))
270 boolean fastpainting = false;
273 * need to make this thread safe move alignment rendering in response to
279 * shift up or down in repaint
281 public void fastPaint(int horizontal, int vertical)
283 if (fastpainting || gg == null)
290 gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
291 imgHeight, -horizontal * charWidth, -vertical * charHeight);
293 ViewportRanges ranges = av.getRanges();
294 int startRes = ranges.getStartRes();
295 int endRes = ranges.getEndRes();
296 int startSeq = ranges.getStartSeq();
297 int endSeq = ranges.getEndSeq();
301 if (horizontal > 0) // scrollbar pulled right, image to the left
303 transX = (endRes - startRes - horizontal) * charWidth;
304 startRes = endRes - horizontal;
306 else if (horizontal < 0)
308 endRes = startRes - horizontal;
310 else if (vertical > 0) // scroll down
312 startSeq = endSeq - vertical;
314 if (startSeq < ranges.getStartSeq())
315 { // ie scrolling too fast, more than a page at a time
316 startSeq = ranges.getStartSeq();
320 transY = imgHeight - ((vertical + 1) * charHeight);
323 else if (vertical < 0)
325 endSeq = startSeq - vertical;
327 if (endSeq > ranges.getEndSeq())
329 endSeq = ranges.getEndSeq();
333 gg.translate(transX, transY);
334 drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
335 gg.translate(-transX, -transY);
338 fastpainting = false;
342 public void paintComponent(Graphics g)
345 BufferedImage lcimg = img; // take reference since other threads may null
346 // img and call later.
347 super.paintComponent(g);
351 || (getVisibleRect().width != g.getClipBounds().width) || (getVisibleRect().height != g
352 .getClipBounds().height)))
354 g.drawImage(lcimg, 0, 0, this);
359 // this draws the whole of the alignment
360 imgWidth = getWidth();
361 imgHeight = getHeight();
363 imgWidth -= (imgWidth % charWidth);
364 imgHeight -= (imgHeight % charHeight);
366 if ((imgWidth < 1) || (imgHeight < 1))
371 if (lcimg == null || imgWidth != lcimg.getWidth()
372 || imgHeight != lcimg.getHeight())
376 lcimg = img = new BufferedImage(imgWidth, imgHeight,
377 BufferedImage.TYPE_INT_RGB);
378 gg = (Graphics2D) img.getGraphics();
379 gg.setFont(av.getFont());
380 } catch (OutOfMemoryError er)
383 System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
384 new OOMWarning("Creating alignment image for display", er);
392 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
393 RenderingHints.VALUE_ANTIALIAS_ON);
396 gg.setColor(Color.white);
397 gg.fillRect(0, 0, imgWidth, imgHeight);
399 ViewportRanges ranges = av.getRanges();
400 if (av.getWrapAlignment())
402 drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
406 drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
407 ranges.getStartSeq(), ranges.getEndSeq(), 0);
410 g.drawImage(lcimg, 0, 0, this);
420 * @return DOCUMENT ME!
422 public int getWrappedCanvasWidth(int cwidth)
424 FontMetrics fm = getFontMetrics(av.getFont());
429 if (av.getScaleRightWrapped())
431 LABEL_EAST = fm.stringWidth(getMask());
434 if (av.getScaleLeftWrapped())
436 LABEL_WEST = fm.stringWidth(getMask());
439 return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
443 * Generates a string of zeroes.
452 for (int i = 0; i < av.getAlignment().getHeight(); i++)
454 tmp = av.getAlignment().getSequenceAt(i).getEnd();
461 for (int i = maxWidth; i > 0; i /= 10)
475 * @param canvasHeight
480 public void drawWrappedPanel(Graphics g, int canvasWidth,
481 int canvasHeight, int startRes)
484 AlignmentI al = av.getAlignment();
486 FontMetrics fm = getFontMetrics(av.getFont());
488 if (av.getScaleRightWrapped())
490 LABEL_EAST = fm.stringWidth(getMask());
493 if (av.getScaleLeftWrapped())
495 LABEL_WEST = fm.stringWidth(getMask());
498 int hgap = charHeight;
499 if (av.getScaleAboveWrapped())
504 int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
505 int cHeight = av.getAlignment().getHeight() * charHeight;
507 av.setWrappedWidth(cWidth);
509 av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
513 int maxwidth = av.getAlignment().getWidth() - 1;
515 if (av.hasHiddenColumns())
517 maxwidth = av.getAlignment().getHiddenColumns()
518 .findColumnPosition(maxwidth) - 1;
521 while ((ypos <= canvasHeight) && (startRes < maxwidth))
523 endx = startRes + cWidth - 1;
530 g.setFont(av.getFont());
531 g.setColor(Color.black);
533 if (av.getScaleLeftWrapped())
535 drawWestScale(g, startRes, endx, ypos);
538 if (av.getScaleRightWrapped())
540 g.translate(canvasWidth - LABEL_EAST, 0);
541 drawEastScale(g, startRes, endx, ypos);
542 g.translate(-(canvasWidth - LABEL_EAST), 0);
545 g.translate(LABEL_WEST, 0);
547 if (av.getScaleAboveWrapped())
549 drawNorthScale(g, startRes, endx, ypos);
552 if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
554 g.setColor(Color.blue);
556 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
557 for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
559 res = hidden.findHiddenRegionPosition(i) - startRes;
561 if (res < 0 || res > endx - startRes)
567 new int[] { res * charWidth - charHeight / 4,
568 res * charWidth + charHeight / 4, res * charWidth },
569 new int[] { ypos - (charHeight / 2),
570 ypos - (charHeight / 2), ypos - (charHeight / 2) + 8 },
576 // When printing we have an extra clipped region,
577 // the Printable page which we need to account for here
578 Shape clip = g.getClip();
582 g.setClip(0, 0, cWidth * charWidth, canvasHeight);
586 g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
587 (int) clip.getBounds().getHeight());
590 drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
592 if (av.isShowAnnotation())
594 g.translate(0, cHeight + ypos + 3);
595 if (annotations == null)
597 annotations = new AnnotationPanel(av);
600 annotations.renderer.drawComponent(annotations, av, g, -1,
602 g.translate(0, -cHeight - ypos - 3);
605 g.translate(-LABEL_WEST, 0);
607 ypos += cHeight + getAnnotationHeight() + hgap;
613 AnnotationPanel annotations;
615 int getAnnotationHeight()
617 if (!av.isShowAnnotation())
622 if (annotations == null)
624 annotations = new AnnotationPanel(av);
627 return annotations.adjustPanelHeight();
631 * Draws the visible region of the alignment on the graphics context. If there
632 * are hidden column markers in the visible region, then each sub-region
633 * between the markers is drawn separately, followed by the hidden column
638 * offset of the first column in the visible region (0..)
640 * offset of the last column in the visible region (0..)
642 * offset of the first sequence in the visible region (0..)
644 * offset of the last sequence in the visible region (0..)
646 * vertical offset at which to draw (for wrapped alignments)
648 public void drawPanel(Graphics g1, final int startRes, final int endRes,
649 final int startSeq, final int endSeq, final int yOffset)
652 if (!av.hasHiddenColumns())
654 draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
658 List<int[]> regions = av.getAlignment().getHiddenColumns()
662 final int screenYMax = endRes - startRes;
663 int blockStart = startRes;
664 int blockEnd = endRes;
666 for (int[] region : regions)
668 int hideStart = region[0];
669 int hideEnd = region[1];
671 if (hideStart <= blockStart)
673 blockStart += (hideEnd - hideStart) + 1;
678 * draw up to just before the next hidden region, or the end of
679 * the visible region, whichever comes first
681 blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
684 g1.translate(screenY * charWidth, 0);
686 draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
689 * draw the downline of the hidden column marker (ScalePanel draws the
690 * triangle on top) if we reached it
692 if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
694 g1.setColor(Color.blue);
696 g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
697 0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
698 (endSeq - startSeq + 1) * charHeight + yOffset);
701 g1.translate(-screenY * charWidth, 0);
702 screenY += blockEnd - blockStart + 1;
703 blockStart = hideEnd + 1;
705 if (screenY > screenYMax)
707 // already rendered last block
712 if (screenY <= screenYMax)
714 // remaining visible region to render
715 blockEnd = blockStart + screenYMax - screenY;
716 g1.translate(screenY * charWidth, 0);
717 draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
719 g1.translate(-screenY * charWidth, 0);
726 * Draws a region of the visible alignment
730 * offset of the first column in the visible region (0..)
732 * offset of the last column in the visible region (0..)
734 * offset of the first sequence in the visible region (0..)
736 * offset of the last sequence in the visible region (0..)
738 * vertical offset at which to draw (for wrapped alignments)
740 private void draw(Graphics g, int startRes, int endRes, int startSeq,
741 int endSeq, int offset)
743 g.setFont(av.getFont());
744 sr.prepare(g, av.isRenderGaps());
748 // / First draw the sequences
749 // ///////////////////////////
750 for (int i = startSeq; i <= endSeq; i++)
752 nextSeq = av.getAlignment().getSequenceAt(i);
755 // occasionally, a race condition occurs such that the alignment row is
759 sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
760 startRes, endRes, offset + ((i - startSeq) * charHeight));
762 if (av.isShowSequenceFeatures())
764 fr.drawSequence(g, nextSeq, startRes, endRes, offset
765 + ((i - startSeq) * charHeight), false);
769 * highlight search Results once sequence has been drawn
771 if (av.hasSearchResults())
773 SearchResultsI searchResults = av.getSearchResults();
774 int[] visibleResults = searchResults.getResults(nextSeq,
776 if (visibleResults != null)
778 for (int r = 0; r < visibleResults.length; r += 2)
780 sr.drawHighlightedText(nextSeq, visibleResults[r],
781 visibleResults[r + 1], (visibleResults[r] - startRes)
783 + ((i - startSeq) * charHeight));
788 if (av.cursorMode && cursorY == i && cursorX >= startRes
789 && cursorX <= endRes)
791 sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
792 offset + ((i - startSeq) * charHeight));
796 if (av.getSelectionGroup() != null
797 || av.getAlignment().getGroups().size() > 0)
799 drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
804 void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
805 int startSeq, int endSeq, int offset)
807 Graphics2D g = (Graphics2D) g1;
809 // ///////////////////////////////////
810 // Now outline any areas if necessary
811 // ///////////////////////////////////
812 SequenceGroup group = av.getSelectionGroup();
818 int visWidth = (endRes - startRes + 1) * charWidth;
820 if ((group == null) && (av.getAlignment().getGroups().size() > 0))
822 group = av.getAlignment().getGroups().get(0);
832 boolean inGroup = false;
836 for (i = startSeq; i <= endSeq; i++)
838 sx = (group.getStartRes() - startRes) * charWidth;
839 sy = offset + ((i - startSeq) * charHeight);
840 ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth) - 1;
842 if (sx + ex < 0 || sx > visWidth)
847 if ((sx <= (endRes - startRes) * charWidth)
848 && group.getSequences(null).contains(
849 av.getAlignment().getSequenceAt(i)))
852 && !group.getSequences(null).contains(
853 av.getAlignment().getSequenceAt(i + 1)))
855 bottom = sy + charHeight;
860 if (((top == -1) && (i == 0))
861 || !group.getSequences(null).contains(
862 av.getAlignment().getSequenceAt(i - 1)))
870 if (group == av.getSelectionGroup())
872 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
873 BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f },
875 g.setColor(Color.RED);
879 g.setStroke(new BasicStroke());
880 g.setColor(group.getOutlineColour());
888 if (sx >= 0 && sx < visWidth)
890 g.drawLine(sx, oldY, sx, sy);
893 if (sx + ex < visWidth)
895 g.drawLine(sx + ex, oldY, sx + ex, sy);
904 if (sx + ex > visWidth)
909 else if (sx + ex >= (endRes - startRes + 1) * charWidth)
911 ex = (endRes - startRes + 1) * charWidth;
916 g.drawLine(sx, top, sx + ex, top);
922 g.drawLine(sx, bottom, sx + ex, bottom);
933 sy = offset + ((i - startSeq) * charHeight);
934 if (sx >= 0 && sx < visWidth)
936 g.drawLine(sx, oldY, sx, sy);
939 if (sx + ex < visWidth)
941 g.drawLine(sx + ex, oldY, sx + ex, sy);
950 if (sx + ex > visWidth)
954 else if (sx + ex >= (endRes - startRes + 1) * charWidth)
956 ex = (endRes - startRes + 1) * charWidth;
961 g.drawLine(sx, top, sx + ex, top);
967 g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
976 g.setStroke(new BasicStroke());
978 if (groupIndex >= av.getAlignment().getGroups().size())
983 group = av.getAlignment().getGroups().get(groupIndex);
985 } while (groupIndex < av.getAlignment().getGroups().size());
992 * Highlights search results in the visible region by rendering as white text
993 * on a black background. Any previous highlighting is removed.
997 public void highlightSearchResults(SearchResultsI results)
1000 fastpainting = true;
1004 * to avoid redrawing the whole visible region, we instead
1005 * calculate the minimal redraw to remove previous highlights
1007 * first make a temporary SearchResults that includes
1008 * both the new and any existing search results
1010 SearchResultsI combined = results;
1011 SearchResultsI previous = av.getSearchResults();
1012 if (previous != null)
1014 combined = new SearchResults();
1015 combined.addSearchResults(results);
1016 combined.addSearchResults(previous);
1020 * calculate the minimal rectangle to redraw that
1021 * includes both new and existing search results
1023 int firstSeq = Integer.MAX_VALUE;
1025 int firstCol = Integer.MAX_VALUE;
1027 boolean matchFound = false;
1029 ViewportRanges ranges = av.getRanges();
1030 int firstVisibleColumn = ranges.getStartRes();
1031 int lastVisibleColumn = ranges.getEndRes();
1032 if (av.hasHiddenColumns())
1034 firstVisibleColumn = av.getAlignment().getHiddenColumns()
1035 .adjustForHiddenColumns(firstVisibleColumn);
1036 lastVisibleColumn = av.getAlignment().getHiddenColumns()
1037 .adjustForHiddenColumns(lastVisibleColumn);
1040 for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1041 .getEndSeq(); seqNo++)
1043 SequenceI seq = av.getAlignment().getSequenceAt(seqNo);
1045 int[] visibleResults = combined.getResults(seq, firstVisibleColumn,
1047 if (visibleResults != null)
1049 for (int i = 0; i < visibleResults.length - 1; i += 2)
1051 int firstMatchedColumn = visibleResults[i];
1052 int lastMatchedColumn = visibleResults[i + 1];
1053 if (firstMatchedColumn <= lastVisibleColumn
1054 && lastMatchedColumn >= firstVisibleColumn)
1057 * found a search results match in the visible region -
1058 * remember the first and last sequence matched, and the first
1059 * and last visible columns in the matched positions
1062 firstSeq = Math.min(firstSeq, seqNo);
1063 lastSeq = Math.max(lastSeq, seqNo);
1064 firstMatchedColumn = Math.max(firstMatchedColumn,
1065 firstVisibleColumn);
1066 lastMatchedColumn = Math.min(lastMatchedColumn,
1068 firstCol = Math.min(firstCol, firstMatchedColumn);
1069 lastCol = Math.max(lastCol, lastMatchedColumn);
1075 av.setSearchResults(results);
1079 if (av.hasHiddenColumns())
1081 firstCol = av.getAlignment().getHiddenColumns()
1082 .findColumnPosition(firstCol);
1083 lastCol = av.getAlignment().getHiddenColumns()
1084 .findColumnPosition(lastCol);
1086 int transX = (firstCol - firstVisibleColumn) * av.getCharWidth();
1087 int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1088 gg.translate(transX, transY);
1089 drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1090 gg.translate(-transX, -transY);
1095 fastpainting = false; // todo in finally block?
1099 public void propertyChange(PropertyChangeEvent evt)
1101 if (!av.getWrapAlignment())
1103 if (evt.getPropertyName().equals("startres")
1104 || evt.getPropertyName().equals("endres"))
1106 // Make sure we're not trying to draw a panel
1107 // larger than the visible window
1108 ViewportRanges vpRanges = av.getRanges();
1109 int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1110 if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
1112 scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
1114 else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
1116 scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
1118 fastPaint(scrollX, 0);
1120 else if (evt.getPropertyName().equals("startseq")
1121 || evt.getPropertyName().equals("endseq"))
1123 fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());