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.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;
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 private static String ZEROS = "0000000000";
58 final FeatureRenderer fr;
60 final SequenceRenderer sr;
72 boolean fastPaint = false;
74 boolean fastpainting = false;
80 private AnnotationPanel annotations;
83 * measurements for drawing a wrapped alignment
85 int labelWidthWest; // label left width in pixels if shown
87 private int labelWidthEast; // label right width in pixels if shown
89 private int wrappedSpaceAboveAlignment; // gap between widths
91 private int wrappedRepeatHeightPx; // height in pixels of wrapped width
93 private int wrappedVisibleWidths; // number of wrapped widths displayed
96 * Creates a new SeqCanvas object.
101 public SeqCanvas(AlignmentPanel ap)
104 fr = new FeatureRenderer(ap);
105 sr = new SequenceRenderer(av);
106 setLayout(new BorderLayout());
107 PaintRefresher.Register(this, av.getSequenceSetId());
108 setBackground(Color.white);
110 av.getRanges().addPropertyChangeListener(this);
113 public SequenceRenderer getSequenceRenderer()
118 public FeatureRenderer getFeatureRenderer()
124 * Draws the scale above a region of a wrapped alignment, consisting of a
125 * column number every major interval (10 columns).
128 * the graphics context to draw on, positioned at the start (bottom
129 * left) of the line on which to draw any scale marks
131 * start alignment column (0..)
133 * end alignment column (0..)
135 * y offset to draw at
137 private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
139 int charHeight = av.getCharHeight();
140 int charWidth = av.getCharWidth();
143 * white fill the scale space (for the fastPaint case)
145 g.setColor(Color.white);
146 g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
147 charHeight * 3 / 2 + 2);
148 g.setColor(Color.black);
150 List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
152 for (ScaleMark mark : marks)
154 int mpos = mark.column; // (i - startx - 1)
159 String mstring = mark.text;
165 g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
169 * draw a tick mark below the column number, centred on the column;
170 * height of tick mark is 4 pixels less than half a character
172 int xpos = (mpos * charWidth) + (charWidth / 2);
173 g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
179 * Draw the scale to the left or right of a wrapped alignment
182 * graphics context, positioned at the start of the scale to be drawn
184 * first column of wrapped width (0.. excluding any hidden columns)
186 * last column of wrapped width (0.. excluding any hidden columns)
188 * vertical offset at which to begin the scale
190 * if true, scale is left of residues, if false, scale is right
192 void drawVerticalScale(Graphics g, int startx, int endx, int ypos,
195 int charHeight = av.getCharHeight();
196 int charWidth = av.getCharWidth();
200 if (av.hasHiddenColumns())
202 HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
203 startx = hiddenColumns.adjustForHiddenColumns(startx);
204 endx = hiddenColumns.adjustForHiddenColumns(endx);
206 FontMetrics fm = getFontMetrics(av.getFont());
208 for (int i = 0; i < av.getAlignment().getHeight(); i++)
210 SequenceI seq = av.getAlignment().getSequenceAt(i);
213 * find sequence position of first non-gapped position -
214 * to the right if scale left, to the left if scale right
216 int index = left ? startx : endx;
218 while (index >= startx && index <= endx)
220 if (!Comparison.isGap(seq.getCharAt(index)))
222 value = seq.findPosition(index);
236 * white fill the space for the scale
238 g.setColor(Color.white);
239 int y = (ypos + (i * charHeight)) - (charHeight / 5);
240 y -= charHeight; // fillRect: origin is top left of rectangle
241 g.fillRect(0, y, left ? labelWidthWest : labelWidthEast,
243 y += charHeight; // drawString: origin is bottom left of text
249 * draw scale value, right justified, with half a character width
250 * separation from the sequence data
252 String valueAsString = String.valueOf(value);
253 int justify = fm.stringWidth(valueAsString) + charWidth;
254 int xpos = left ? labelWidthWest - justify + charWidth / 2
255 : labelWidthEast - justify + charWidth / 2;
256 g.setColor(Color.black);
257 g.drawString(valueAsString, xpos, y);
263 * Does a fast paint of an alignment in response to a scroll. Most of the
264 * visible region is simply copied and shifted, and then any newly visible
265 * columns or rows are drawn. The scroll may be horizontal or vertical, but
266 * not both at once. Scrolling may be the result of
268 * <li>dragging a scroll bar</li>
269 * <li>clicking in the scroll bar</li>
270 * <li>scrolling by trackpad, middle mouse button, or other device</li>
271 * <li>by moving the box in the Overview window</li>
272 * <li>programmatically to make a highlighted position visible</li>
276 * columns to shift right (positive) or left (negative)
278 * rows to shift down (positive) or up (negative)
280 public void fastPaint(int horizontal, int vertical)
282 if (fastpainting || gg == null)
291 int charHeight = av.getCharHeight();
292 int charWidth = av.getCharWidth();
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();
302 gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
303 imgHeight, -horizontal * charWidth, -vertical * charHeight);
305 if (horizontal > 0) // scrollbar pulled right, image to the left
307 transX = (endRes - startRes - horizontal) * charWidth;
308 startRes = endRes - horizontal;
310 else if (horizontal < 0)
312 endRes = startRes - horizontal;
314 else if (vertical > 0) // scroll down
316 startSeq = endSeq - vertical;
318 if (startSeq < ranges.getStartSeq())
319 { // ie scrolling too fast, more than a page at a time
320 startSeq = ranges.getStartSeq();
324 transY = imgHeight - ((vertical + 1) * charHeight);
327 else if (vertical < 0)
329 endSeq = startSeq - vertical;
331 if (endSeq > ranges.getEndSeq())
333 endSeq = ranges.getEndSeq();
337 gg.translate(transX, transY);
338 drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
339 gg.translate(-transX, -transY);
344 fastpainting = false;
349 public void paintComponent(Graphics g)
351 int charHeight = av.getCharHeight();
352 int charWidth = av.getCharWidth();
353 BufferedImage lcimg = img; // take reference since other threads may null
354 // img and call later.
355 super.paintComponent(g);
357 if (lcimg != null && (fastPaint
358 || (getVisibleRect().width != g.getClipBounds().width)
359 || (getVisibleRect().height != g.getClipBounds().height)))
361 g.drawImage(lcimg, 0, 0, this);
366 // this draws the whole of the alignment
367 imgWidth = getWidth();
368 imgHeight = getHeight();
370 imgWidth -= (imgWidth % charWidth);
371 imgHeight -= (imgHeight % charHeight);
373 if ((imgWidth < 1) || (imgHeight < 1))
378 if (lcimg == null || imgWidth != lcimg.getWidth()
379 || imgHeight != lcimg.getHeight())
383 lcimg = img = new BufferedImage(imgWidth, imgHeight,
384 BufferedImage.TYPE_INT_RGB);
385 gg = (Graphics2D) img.getGraphics();
386 gg.setFont(av.getFont());
387 } catch (OutOfMemoryError er)
390 System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
391 new OOMWarning("Creating alignment image for display", er);
399 gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
400 RenderingHints.VALUE_ANTIALIAS_ON);
403 gg.setColor(Color.white);
404 gg.fillRect(0, 0, imgWidth, imgHeight);
406 ViewportRanges ranges = av.getRanges();
407 if (av.getWrapAlignment())
409 drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
413 drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
414 ranges.getStartSeq(), ranges.getEndSeq(), 0);
417 g.drawImage(lcimg, 0, 0, this);
422 * Returns the visible width of the canvas in residues, after allowing for
423 * East or West scales (if shown)
426 * the width in pixels (possibly including scales)
430 public int getWrappedCanvasWidth(int canvasWidth)
432 int charWidth = av.getCharWidth();
434 FontMetrics fm = getFontMetrics(av.getFont());
439 if (av.getScaleRightWrapped())
441 labelWidthEast = getLabelWidth(fm);
444 if (av.getScaleLeftWrapped())
446 labelWidthWest = labelWidthEast > 0 ? labelWidthEast
450 return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
454 * Returns a pixel width suitable for showing the largest sequence coordinate
455 * (end position) in the alignment. Returns 2 plus the number of decimal
456 * digits to be shown (3 for 1-10, 4 for 11-99 etc).
461 protected int getLabelWidth(FontMetrics fm)
464 * find the biggest sequence end position we need to show
465 * (note this is not necessarily the sequence length)
468 AlignmentI alignment = av.getAlignment();
469 for (int i = 0; i < alignment.getHeight(); i++)
471 maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
475 for (int i = maxWidth; i > 0; i /= 10)
480 return fm.stringWidth(ZEROS.substring(0, length));
484 * Draws as many widths of a wrapped alignment as can fit in the visible
489 * available width in pixels
490 * @param canvasHeight
491 * available height in pixels
493 * the first column (0...) of the alignment to draw
495 public void drawWrappedPanel(Graphics g, int canvasWidth,
496 int canvasHeight, final int startColumn)
498 int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
501 av.setWrappedWidth(wrappedWidthInResidues);
503 ViewportRanges ranges = av.getRanges();
504 ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
507 * draw one width at a time (including any scales or annotation shown),
508 * until we have run out of either alignment or vertical space available
510 int ypos = wrappedSpaceAboveAlignment;
511 int maxWidth = ranges.getVisibleAlignmentWidth();
513 int start = startColumn;
514 int currentWidth = 0;
515 while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
518 .min(maxWidth, start + wrappedWidthInResidues - 1);
519 drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
520 ypos += wrappedRepeatHeightPx;
521 start += wrappedWidthInResidues;
525 drawWrappedDecorators(g, startColumn);
529 * Calculates and saves values needed when rendering a wrapped alignment.
530 * These depend on many factors, including
532 * <li>canvas width and height</li>
533 * <li>number of visible sequences, and height of annotations if shown</li>
534 * <li>font and character width</li>
535 * <li>whether scales are shown left, right or above the alignment</li>
539 * @param canvasHeight
540 * @return the number of residue columns in each width
542 protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
544 int charHeight = av.getCharHeight();
545 int charWidth = av.getCharWidth();
548 * width of labels in pixels left and right (if shown)
551 if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
553 FontMetrics fm = getFontMetrics(av.getFont());
554 labelWidth = getLabelWidth(fm);
556 labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
557 labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
560 * vertical space in pixels between wrapped widths of alignment
561 * - one character height, or two if scale above is drawn
563 wrappedSpaceAboveAlignment = charHeight
564 * (av.getScaleAboveWrapped() ? 2 : 1);
567 * height in pixels of the wrapped widths
569 wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
571 wrappedRepeatHeightPx += av.getRanges().getViewportHeight() * charHeight;
572 // add annotations panel height if shown
573 wrappedRepeatHeightPx += getAnnotationHeight();
576 * number of residue columns we can show in each row;
577 * this is just canvas width less scale left and right (if shown),
578 * as a whole multiple of character widths
580 int wrappedWidthInResidues = (canvasWidth - labelWidthEast - labelWidthWest)
584 * number of visible widths (the last one may be part height),
585 * ensuring a part height includes at least one sequence
587 ViewportRanges ranges = av.getRanges();
588 int xMax = ranges.getVisibleAlignmentWidth();
589 wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
590 int remainder = canvasHeight % wrappedRepeatHeightPx;
591 if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
593 wrappedVisibleWidths++;
597 * limit visibleWidths to not exceed width of alignment
599 int maxWidths = (xMax - ranges.getStartRes()) / wrappedWidthInResidues;
600 if (xMax % wrappedWidthInResidues > 0)
604 wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
606 return wrappedWidthInResidues;
610 * Draws one width of a wrapped alignment, including sequences and
611 * annnotations, if shown, but not scales or hidden column markers
617 * @param canvasHeight
619 protected void drawWrappedWidth(Graphics g, int ypos,
620 int startColumn, int endColumn, int canvasHeight)
622 int charHeight = av.getCharHeight();
623 int charWidth = av.getCharWidth();
625 ViewportRanges ranges = av.getRanges();
626 int viewportWidth = ranges.getViewportWidth();
628 int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
631 * move right before drawing by the width of the scale left (if any)
632 * plus column offset from left margin (usually zero, but may be non-zero
633 * when fast painting is drawing just a few columns)
635 int xOffset = labelWidthWest
636 + ((startColumn - ranges.getStartRes()) % viewportWidth)
638 g.translate(xOffset, 0);
640 // When printing we have an extra clipped region,
641 // the Printable page which we need to account for here
642 Shape clip = g.getClip();
646 g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
650 g.setClip(0, (int) clip.getBounds().getY(),
651 viewportWidth * charWidth, (int) clip.getBounds().getHeight());
655 * white fill the region to be drawn (so incremental fast paint doesn't
656 * scribble over an existing image)
658 gg.setColor(Color.white);
659 gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
660 wrappedRepeatHeightPx);
662 drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
665 int cHeight = av.getAlignment().getHeight() * charHeight;
667 if (av.isShowAnnotation())
669 g.translate(0, cHeight + ypos + 3);
670 if (annotations == null)
672 annotations = new AnnotationPanel(av);
675 annotations.renderer.drawComponent(annotations, av, g, -1,
676 startColumn, endx + 1);
677 g.translate(0, -cHeight - ypos - 3);
680 g.translate(-xOffset, 0);
684 * Draws scales left, right and above (if shown), and any hidden column
685 * markers, on all widths of the wrapped alignment
690 protected void drawWrappedDecorators(Graphics g, int startColumn)
692 int charWidth = av.getCharWidth();
694 g.setFont(av.getFont());
695 g.setColor(Color.black);
697 int ypos = wrappedSpaceAboveAlignment;
698 ViewportRanges ranges = av.getRanges();
699 int viewportWidth = ranges.getViewportWidth();
700 int maxWidth = ranges.getVisibleAlignmentWidth();
702 while (widthsDrawn < wrappedVisibleWidths)
704 int endColumn = Math.min(maxWidth, startColumn + viewportWidth - 1);
706 if (av.getScaleLeftWrapped())
708 drawVerticalScale(g, startColumn, endColumn - 1, ypos, true);
711 if (av.getScaleRightWrapped())
713 int x = labelWidthWest + viewportWidth * charWidth;
715 drawVerticalScale(g, startColumn, endColumn, ypos, false);
720 * white fill region of scale above and hidden column markers
721 * (to support incremental fast paint of image)
723 g.setColor(Color.white);
724 g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
725 * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
726 g.setColor(Color.black);
728 g.translate(labelWidthWest, 0);
730 if (av.getScaleAboveWrapped())
732 drawNorthScale(g, startColumn, endColumn, ypos);
735 if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
737 drawHiddenColumnMarkers(g, ypos, startColumn, endColumn);
740 g.translate(-labelWidthWest, 0);
742 ypos += wrappedRepeatHeightPx;
743 startColumn += viewportWidth;
754 protected void drawHiddenColumnMarkers(Graphics g, int ypos,
755 int startColumn, int endColumn)
757 int charHeight = av.getCharHeight();
758 int charWidth = av.getCharWidth();
760 g.setColor(Color.blue);
761 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
762 List<Integer> positions = hidden.findHiddenRegionPositions();
763 for (int pos : positions)
765 int res = pos - startColumn;
767 if (res < 0 || res > endColumn - startColumn)
773 * draw a downward-pointing triangle at the hidden columns location
774 * (before the following visible column)
776 int xMiddle = res * charWidth;
777 int[] xPoints = new int[] { xMiddle - charHeight / 4,
778 xMiddle + charHeight / 4, xMiddle };
779 int yTop = ypos - (charHeight / 2);
780 int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
781 g.fillPolygon(xPoints, yPoints, 3);
785 int getAnnotationHeight()
787 if (!av.isShowAnnotation())
792 if (annotations == null)
794 annotations = new AnnotationPanel(av);
797 return annotations.adjustPanelHeight();
801 * Draws the visible region of the alignment on the graphics context. If there
802 * are hidden column markers in the visible region, then each sub-region
803 * between the markers is drawn separately, followed by the hidden column
807 * the graphics context, positioned at the first residue to be drawn
809 * offset of the first column to draw (0..)
811 * offset of the last column to draw (0..)
813 * offset of the first sequence to draw (0..)
815 * offset of the last sequence to draw (0..)
817 * vertical offset at which to draw (for wrapped alignments)
819 public void drawPanel(Graphics g1, final int startRes, final int endRes,
820 final int startSeq, final int endSeq, final int yOffset)
822 int charHeight = av.getCharHeight();
823 int charWidth = av.getCharWidth();
825 if (!av.hasHiddenColumns())
827 draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
832 final int screenYMax = endRes - startRes;
833 int blockStart = startRes;
834 int blockEnd = endRes;
836 for (int[] region : av.getAlignment().getHiddenColumns()
837 .getHiddenColumnsCopy())
839 int hideStart = region[0];
840 int hideEnd = region[1];
842 if (hideStart <= blockStart)
844 blockStart += (hideEnd - hideStart) + 1;
849 * draw up to just before the next hidden region, or the end of
850 * the visible region, whichever comes first
852 blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
855 g1.translate(screenY * charWidth, 0);
857 draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
860 * draw the downline of the hidden column marker (ScalePanel draws the
861 * triangle on top) if we reached it
863 if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
865 g1.setColor(Color.blue);
867 g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
868 0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
869 (endSeq - startSeq + 1) * charHeight + yOffset);
872 g1.translate(-screenY * charWidth, 0);
873 screenY += blockEnd - blockStart + 1;
874 blockStart = hideEnd + 1;
876 if (screenY > screenYMax)
878 // already rendered last block
883 if (screenY <= screenYMax)
885 // remaining visible region to render
886 blockEnd = blockStart + screenYMax - screenY;
887 g1.translate(screenY * charWidth, 0);
888 draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
890 g1.translate(-screenY * charWidth, 0);
897 * Draws a region of the visible alignment
901 * offset of the first column in the visible region (0..)
903 * offset of the last column in the visible region (0..)
905 * offset of the first sequence in the visible region (0..)
907 * offset of the last sequence in the visible region (0..)
909 * vertical offset at which to draw (for wrapped alignments)
911 private void draw(Graphics g, int startRes, int endRes, int startSeq,
912 int endSeq, int offset)
914 int charHeight = av.getCharHeight();
915 int charWidth = av.getCharWidth();
917 g.setFont(av.getFont());
918 sr.prepare(g, av.isRenderGaps());
922 // / First draw the sequences
923 // ///////////////////////////
924 for (int i = startSeq; i <= endSeq; i++)
926 nextSeq = av.getAlignment().getSequenceAt(i);
929 // occasionally, a race condition occurs such that the alignment row is
933 sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
934 startRes, endRes, offset + ((i - startSeq) * charHeight));
936 if (av.isShowSequenceFeatures())
938 fr.drawSequence(g, nextSeq, startRes, endRes,
939 offset + ((i - startSeq) * charHeight), false);
943 * highlight search Results once sequence has been drawn
945 if (av.hasSearchResults())
947 SearchResultsI searchResults = av.getSearchResults();
948 int[] visibleResults = searchResults.getResults(nextSeq,
950 if (visibleResults != null)
952 for (int r = 0; r < visibleResults.length; r += 2)
954 sr.drawHighlightedText(nextSeq, visibleResults[r],
955 visibleResults[r + 1],
956 (visibleResults[r] - startRes) * charWidth,
957 offset + ((i - startSeq) * charHeight));
962 if (av.cursorMode && cursorY == i && cursorX >= startRes
963 && cursorX <= endRes)
965 sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
966 offset + ((i - startSeq) * charHeight));
970 if (av.getSelectionGroup() != null
971 || av.getAlignment().getGroups().size() > 0)
973 drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
978 void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
979 int startSeq, int endSeq, int offset)
981 int charHeight = av.getCharHeight();
982 int charWidth = av.getCharWidth();
984 Graphics2D g = (Graphics2D) g1;
986 // ///////////////////////////////////
987 // Now outline any areas if necessary
988 // ///////////////////////////////////
989 SequenceGroup group = av.getSelectionGroup();
995 int visWidth = (endRes - startRes + 1) * charWidth;
997 if ((group == null) && (av.getAlignment().getGroups().size() > 0))
999 group = av.getAlignment().getGroups().get(0);
1009 boolean inGroup = false;
1013 for (i = startSeq; i <= endSeq; i++)
1015 sx = (group.getStartRes() - startRes) * charWidth;
1016 sy = offset + ((i - startSeq) * charHeight);
1017 ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
1020 if (sx + ex < 0 || sx > visWidth)
1025 if ((sx <= (endRes - startRes) * charWidth)
1026 && group.getSequences(null)
1027 .contains(av.getAlignment().getSequenceAt(i)))
1029 if ((bottom == -1) && !group.getSequences(null)
1030 .contains(av.getAlignment().getSequenceAt(i + 1)))
1032 bottom = sy + charHeight;
1037 if (((top == -1) && (i == 0)) || !group.getSequences(null)
1038 .contains(av.getAlignment().getSequenceAt(i - 1)))
1046 if (group == av.getSelectionGroup())
1048 g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
1049 BasicStroke.JOIN_ROUND, 3f, new float[]
1051 g.setColor(Color.RED);
1055 g.setStroke(new BasicStroke());
1056 g.setColor(group.getOutlineColour());
1064 if (sx >= 0 && sx < visWidth)
1066 g.drawLine(sx, oldY, sx, sy);
1069 if (sx + ex < visWidth)
1071 g.drawLine(sx + ex, oldY, sx + ex, sy);
1080 if (sx + ex > visWidth)
1085 else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1087 ex = (endRes - startRes + 1) * charWidth;
1092 g.drawLine(sx, top, sx + ex, top);
1098 g.drawLine(sx, bottom, sx + ex, bottom);
1109 sy = offset + ((i - startSeq) * charHeight);
1110 if (sx >= 0 && sx < visWidth)
1112 g.drawLine(sx, oldY, sx, sy);
1115 if (sx + ex < visWidth)
1117 g.drawLine(sx + ex, oldY, sx + ex, sy);
1126 if (sx + ex > visWidth)
1130 else if (sx + ex >= (endRes - startRes + 1) * charWidth)
1132 ex = (endRes - startRes + 1) * charWidth;
1137 g.drawLine(sx, top, sx + ex, top);
1143 g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
1152 g.setStroke(new BasicStroke());
1154 if (groupIndex >= av.getAlignment().getGroups().size())
1159 group = av.getAlignment().getGroups().get(groupIndex);
1161 } while (groupIndex < av.getAlignment().getGroups().size());
1168 * Highlights search results in the visible region by rendering as white text
1169 * on a black background. Any previous highlighting is removed. Answers true
1170 * if any highlight was left on the visible alignment (so status bar should be
1171 * set to match), else false.
1173 * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
1174 * alignment had to be scrolled to show the highlighted region, then it should
1175 * be fully redrawn, otherwise a fast paint can be performed. This argument
1176 * could be removed if fast paint of scrolled wrapped alignment is coded in
1177 * future (JAL-2609).
1180 * @param noFastPaint
1183 public boolean highlightSearchResults(SearchResultsI results,
1184 boolean noFastPaint)
1190 boolean wrapped = av.getWrapAlignment();
1193 fastPaint = !noFastPaint;
1194 fastpainting = fastPaint;
1197 * to avoid redrawing the whole visible region, we instead
1198 * redraw just the minimal regions to remove previous highlights
1201 SearchResultsI previous = av.getSearchResults();
1202 av.setSearchResults(results);
1203 boolean redrawn = false;
1204 boolean drawn = false;
1207 redrawn = drawMappedPositionsWrapped(previous);
1208 drawn = drawMappedPositionsWrapped(results);
1213 redrawn = drawMappedPositions(previous);
1214 drawn = drawMappedPositions(results);
1219 * if highlights were either removed or added, repaint
1227 * return true only if highlights were added
1233 fastpainting = false;
1238 * Redraws the minimal rectangle in the visible region (if any) that includes
1239 * mapped positions of the given search results. Whether or not positions are
1240 * highlighted depends on the SearchResults set on the Viewport. This allows
1241 * this method to be called to either clear or set highlighting. Answers true
1242 * if any positions were drawn (in which case a repaint is still required),
1248 protected boolean drawMappedPositions(SearchResultsI results)
1250 if (results == null)
1256 * calculate the minimal rectangle to redraw that
1257 * includes both new and existing search results
1259 int firstSeq = Integer.MAX_VALUE;
1261 int firstCol = Integer.MAX_VALUE;
1263 boolean matchFound = false;
1265 ViewportRanges ranges = av.getRanges();
1266 int firstVisibleColumn = ranges.getStartRes();
1267 int lastVisibleColumn = ranges.getEndRes();
1268 AlignmentI alignment = av.getAlignment();
1269 if (av.hasHiddenColumns())
1271 firstVisibleColumn = alignment.getHiddenColumns()
1272 .adjustForHiddenColumns(firstVisibleColumn);
1273 lastVisibleColumn = alignment.getHiddenColumns()
1274 .adjustForHiddenColumns(lastVisibleColumn);
1277 for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1278 .getEndSeq(); seqNo++)
1280 SequenceI seq = alignment.getSequenceAt(seqNo);
1282 int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1284 if (visibleResults != null)
1286 for (int i = 0; i < visibleResults.length - 1; i += 2)
1288 int firstMatchedColumn = visibleResults[i];
1289 int lastMatchedColumn = visibleResults[i + 1];
1290 if (firstMatchedColumn <= lastVisibleColumn
1291 && lastMatchedColumn >= firstVisibleColumn)
1294 * found a search results match in the visible region -
1295 * remember the first and last sequence matched, and the first
1296 * and last visible columns in the matched positions
1299 firstSeq = Math.min(firstSeq, seqNo);
1300 lastSeq = Math.max(lastSeq, seqNo);
1301 firstMatchedColumn = Math.max(firstMatchedColumn,
1302 firstVisibleColumn);
1303 lastMatchedColumn = Math.min(lastMatchedColumn,
1305 firstCol = Math.min(firstCol, firstMatchedColumn);
1306 lastCol = Math.max(lastCol, lastMatchedColumn);
1314 if (av.hasHiddenColumns())
1316 firstCol = alignment.getHiddenColumns()
1317 .findColumnPosition(firstCol);
1318 lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
1320 int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
1321 int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
1322 gg.translate(transX, transY);
1323 drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
1324 gg.translate(-transX, -transY);
1331 public void propertyChange(PropertyChangeEvent evt)
1333 String eventName = evt.getPropertyName();
1336 if (eventName.equals(ViewportRanges.STARTRES))
1338 // Make sure we're not trying to draw a panel
1339 // larger than the visible window
1340 ViewportRanges vpRanges = av.getRanges();
1341 scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
1342 int range = vpRanges.getEndRes() - vpRanges.getStartRes();
1343 if (scrollX > range)
1347 else if (scrollX < -range)
1353 // Both scrolling and resizing change viewport ranges: scrolling changes
1354 // both start and end points, but resize only changes end values.
1355 // Here we only want to fastpaint on a scroll, with resize using a normal
1356 // paint, so scroll events are identified as changes to the horizontal or
1357 // vertical start value.
1358 if (eventName.equals(ViewportRanges.STARTRES))
1360 // scroll - startres and endres both change
1361 if (av.getWrapAlignment())
1363 fastPaintWrapped(scrollX);
1367 fastPaint(scrollX, 0);
1370 else if (eventName.equals(ViewportRanges.STARTSEQ))
1372 fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
1377 * Does a minimal update of the image for a scroll movement. This method
1378 * handles scroll movements of up to one width of the wrapped alignment (one
1379 * click in the vertical scrollbar). Larger movements (for example after a
1380 * scroll to highlight a mapped position) trigger a full redraw instead.
1383 * number of positions scrolled (right if positive, left if negative)
1385 protected void fastPaintWrapped(int scrollX)
1387 ViewportRanges ranges = av.getRanges();
1389 if (Math.abs(scrollX) > ranges.getViewportWidth())
1392 * shift of more than one view width is
1393 * overcomplicated to handle in this method
1400 if (fastpainting || gg == null)
1406 fastpainting = true;
1410 calculateWrappedGeometry(getWidth(), getHeight());
1413 * relocate the regions of the alignment that are still visible
1415 shiftWrappedAlignment(-scrollX);
1418 * add new columns (sequence, annotation)
1419 * - at top left if scrollX < 0
1420 * - at right of last two widths if scrollX > 0
1424 int startRes = ranges.getStartRes();
1425 drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
1426 - scrollX - 1, getHeight());
1430 fastPaintWrappedAddRight(scrollX);
1434 * draw all scales (if shown) and hidden column markers
1436 drawWrappedDecorators(gg, ranges.getStartRes());
1441 fastpainting = false;
1446 * Draws the specified number of columns at the 'end' (bottom right) of a
1447 * wrapped alignment view, including sequences and annotations if shown, but
1448 * not scales. Also draws the same number of columns at the right hand end of
1449 * the second last width shown, if the last width is not full height (so
1450 * cannot simply be copied from the graphics image).
1454 protected void fastPaintWrappedAddRight(int columns)
1461 ViewportRanges ranges = av.getRanges();
1462 int viewportWidth = ranges.getViewportWidth();
1463 int charWidth = av.getCharWidth();
1466 * draw full height alignment in the second last row, last columns, if the
1467 * last row was not full height
1469 int visibleWidths = wrappedVisibleWidths;
1470 int canvasHeight = getHeight();
1471 boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
1473 if (lastWidthPartHeight)
1475 int widthsAbove = Math.max(0, visibleWidths - 2);
1476 int ypos = wrappedRepeatHeightPx * widthsAbove
1477 + wrappedSpaceAboveAlignment;
1478 int endRes = ranges.getEndRes();
1479 endRes += widthsAbove * viewportWidth;
1480 int startRes = endRes - columns;
1481 int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1485 * white fill first to erase annotations
1487 gg.translate(xOffset, 0);
1488 gg.setColor(Color.white);
1489 gg.fillRect(labelWidthWest, ypos,
1490 (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
1491 gg.translate(-xOffset, 0);
1493 drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1497 * draw newly visible columns in last wrapped width (none if we
1498 * have reached the end of the alignment)
1499 * y-offset for drawing last width is height of widths above,
1502 int widthsAbove = visibleWidths - 1;
1503 int ypos = wrappedRepeatHeightPx * widthsAbove
1504 + wrappedSpaceAboveAlignment;
1505 int endRes = ranges.getEndRes();
1506 endRes += widthsAbove * viewportWidth;
1507 int startRes = endRes - columns + 1;
1510 * white fill first to erase annotations
1512 int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
1514 gg.translate(xOffset, 0);
1515 gg.setColor(Color.white);
1516 int width = viewportWidth * charWidth - xOffset;
1517 gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
1518 gg.translate(-xOffset, 0);
1520 gg.setFont(av.getFont());
1521 gg.setColor(Color.black);
1523 if (startRes < ranges.getVisibleAlignmentWidth())
1525 drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
1529 * and finally, white fill any space below the visible alignment
1531 int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
1532 if (heightBelow > 0)
1534 gg.setColor(Color.white);
1535 gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
1540 * Shifts the visible alignment by the specified number of columns - left if
1541 * negative, right if positive. Copies and moves sequences and annotations (if
1542 * shown). Scales, hidden column markers and any newly visible columns must be
1547 protected void shiftWrappedAlignment(int positions)
1553 int charWidth = av.getCharWidth();
1555 int canvasHeight = getHeight();
1556 ViewportRanges ranges = av.getRanges();
1557 int viewportWidth = ranges.getViewportWidth();
1558 int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
1560 int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
1561 int xMax = ranges.getVisibleAlignmentWidth();
1566 * shift right (after scroll left)
1567 * for each wrapped width (starting with the last), copy (width-positions)
1568 * columns from the left margin to the right margin, and copy positions
1569 * columns from the right margin of the row above (if any) to the
1570 * left margin of the current row
1574 * get y-offset of last wrapped width, first row of sequences
1576 int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
1577 y += wrappedSpaceAboveAlignment;
1578 int copyFromLeftStart = labelWidthWest;
1579 int copyFromRightStart = copyFromLeftStart + widthToCopy;
1583 gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
1584 positions * charWidth, 0);
1587 gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
1588 positions * charWidth, heightToCopy, -widthToCopy,
1589 wrappedRepeatHeightPx);
1592 y -= wrappedRepeatHeightPx;
1598 * shift left (after scroll right)
1599 * for each wrapped width (starting with the first), copy (width-positions)
1600 * columns from the right margin to the left margin, and copy positions
1601 * columns from the left margin of the row below (if any) to the
1602 * right margin of the current row
1604 int xpos = av.getRanges().getStartRes();
1605 int y = wrappedSpaceAboveAlignment;
1606 int copyFromRightStart = labelWidthWest - positions * charWidth;
1608 while (y < canvasHeight)
1610 gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
1611 positions * charWidth, 0);
1612 if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
1613 && (xpos + viewportWidth <= xMax))
1615 gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
1616 * charWidth, heightToCopy, widthToCopy,
1617 -wrappedRepeatHeightPx);
1620 y += wrappedRepeatHeightPx;
1621 xpos += viewportWidth;
1627 * Redraws any positions in the search results in the visible region of a
1628 * wrapped alignment. Any highlights are drawn depending on the search results
1629 * set on the Viewport, not the <code>results</code> argument. This allows
1630 * this method to be called either to clear highlights (passing the previous
1631 * search results), or to draw new highlights.
1636 protected boolean drawMappedPositionsWrapped(SearchResultsI results)
1638 if (results == null)
1642 int charHeight = av.getCharHeight();
1644 boolean matchFound = false;
1646 calculateWrappedGeometry(getWidth(), getHeight());
1647 int wrappedWidth = av.getWrappedWidth();
1648 int wrappedHeight = wrappedRepeatHeightPx;
1650 ViewportRanges ranges = av.getRanges();
1651 int canvasHeight = getHeight();
1652 int repeats = canvasHeight / wrappedHeight;
1653 if (canvasHeight / wrappedHeight > 0)
1658 int firstVisibleColumn = ranges.getStartRes();
1659 int lastVisibleColumn = ranges.getStartRes() + repeats
1660 * ranges.getViewportWidth() - 1;
1662 AlignmentI alignment = av.getAlignment();
1663 if (av.hasHiddenColumns())
1665 firstVisibleColumn = alignment.getHiddenColumns()
1666 .adjustForHiddenColumns(firstVisibleColumn);
1667 lastVisibleColumn = alignment.getHiddenColumns()
1668 .adjustForHiddenColumns(lastVisibleColumn);
1671 int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
1673 for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
1674 .getEndSeq(); seqNo++)
1676 SequenceI seq = alignment.getSequenceAt(seqNo);
1678 int[] visibleResults = results.getResults(seq, firstVisibleColumn,
1680 if (visibleResults != null)
1682 for (int i = 0; i < visibleResults.length - 1; i += 2)
1684 int firstMatchedColumn = visibleResults[i];
1685 int lastMatchedColumn = visibleResults[i + 1];
1686 if (firstMatchedColumn <= lastVisibleColumn
1687 && lastMatchedColumn >= firstVisibleColumn)
1690 * found a search results match in the visible region
1692 firstMatchedColumn = Math.max(firstMatchedColumn,
1693 firstVisibleColumn);
1694 lastMatchedColumn = Math.min(lastMatchedColumn,
1698 * draw each mapped position separately (as contiguous positions may
1699 * wrap across lines)
1701 for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
1703 int displayColumn = mappedPos;
1704 if (av.hasHiddenColumns())
1706 displayColumn = alignment.getHiddenColumns()
1707 .findColumnPosition(displayColumn);
1711 * transX: offset from left edge of canvas to residue position
1713 int transX = labelWidthWest
1714 + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
1715 * av.getCharWidth();
1718 * transY: offset from top edge of canvas to residue position
1720 int transY = gapHeight;
1721 transY += (displayColumn - ranges.getStartRes())
1722 / wrappedWidth * wrappedHeight;
1723 transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
1726 * yOffset is from graphics origin to start of visible region
1728 int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
1729 if (transY < getHeight())
1732 gg.translate(transX, transY);
1733 drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
1735 gg.translate(-transX, -transY);