+ * Optionally, set the 'fastPaint' flag for a faster redraw if only the + * highlighted regions are modified. This speeds up highlighting across linked + * alignments. + *
+ * Currently fastPaint is not implemented for scrolled wrapped alignments. If
+ * a wrapped alignment had to be scrolled to show the highlighted region, then
+ * it should be fully redrawn, otherwise a fast paint can be performed. This
+ * argument could be removed if fast paint of scrolled wrapped alignment is
+ * coded in future (JAL-2609).
+ *
+ * @param results
+ * @param doFastPaint
+ * if true, sets a flag so the next repaint only redraws the modified
+ * image
+ * @return
+ */
+ public boolean highlightSearchResults(SearchResultsI results,
+ boolean doFastPaint)
+ {
+ if (fastpainting)
+ {
+ return false;
+ }
+ boolean wrapped = av.getWrapAlignment();
+ try
+ {
+ fastPaint = doFastPaint;
+ fastpainting = fastPaint;
+
+ /*
+ * to avoid redrawing the whole visible region, we instead
+ * redraw just the minimal regions to remove previous highlights
+ * and add new ones
+ */
+ SearchResultsI previous = av.getSearchResults();
+ av.setSearchResults(results);
+ boolean redrawn = false;
+ boolean drawn = false;
+ if (wrapped)
+ {
+ redrawn = drawMappedPositionsWrapped(previous);
+ drawn = drawMappedPositionsWrapped(results);
+ redrawn |= drawn;
+ }
+ else
+ {
+ redrawn = drawMappedPositions(previous);
+ drawn = drawMappedPositions(results);
+ redrawn |= drawn;
+ }
+
+ /*
+ * if highlights were either removed or added, repaint
+ */
+ if (redrawn)
+ {
+ repaint();
+ }
+
+ /*
+ * return true only if highlights were added
+ */
+ return drawn;
+
+ } finally
+ {
+ fastpainting = false;
+ }
+ }
+
+ /**
+ * Redraws the minimal rectangle in the visible region (if any) that includes
+ * mapped positions of the given search results. Whether or not positions are
+ * highlighted depends on the SearchResults set on the Viewport. This allows
+ * this method to be called to either clear or set highlighting. Answers true
+ * if any positions were drawn (in which case a repaint is still required),
+ * else false.
+ *
+ * @param results
+ * @return
+ */
+ protected boolean drawMappedPositions(SearchResultsI results)
+ {
+ if ((results == null) || (img == null)) // JAL-2784 check gg is not null
+ {
+ return false;
+ }
+
+ /*
+ * calculate the minimal rectangle to redraw that
+ * includes both new and existing search results
+ */
+ int firstSeq = Integer.MAX_VALUE;
+ int lastSeq = -1;
+ int firstCol = Integer.MAX_VALUE;
+ int lastCol = -1;
+ boolean matchFound = false;
+
+ ViewportRanges ranges = av.getRanges();
+ int firstVisibleColumn = ranges.getStartRes();
+ int lastVisibleColumn = ranges.getEndRes();
+ AlignmentI alignment = av.getAlignment();
+ if (av.hasHiddenColumns())
+ {
+ firstVisibleColumn = alignment.getHiddenColumns()
+ .visibleToAbsoluteColumn(firstVisibleColumn);
+ lastVisibleColumn = alignment.getHiddenColumns()
+ .visibleToAbsoluteColumn(lastVisibleColumn);
+ }
+
+ for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+ .getEndSeq(); seqNo++)
+ {
+ SequenceI seq = alignment.getSequenceAt(seqNo);
+
+ int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+ lastVisibleColumn);
+ if (visibleResults != null)
+ {
+ for (int i = 0; i < visibleResults.length - 1; i += 2)
+ {
+ int firstMatchedColumn = visibleResults[i];
+ int lastMatchedColumn = visibleResults[i + 1];
+ if (firstMatchedColumn <= lastVisibleColumn
+ && lastMatchedColumn >= firstVisibleColumn)
+ {
+ /*
+ * found a search results match in the visible region -
+ * remember the first and last sequence matched, and the first
+ * and last visible columns in the matched positions
+ */
+ matchFound = true;
+ firstSeq = Math.min(firstSeq, seqNo);
+ lastSeq = Math.max(lastSeq, seqNo);
+ firstMatchedColumn = Math.max(firstMatchedColumn,
+ firstVisibleColumn);
+ lastMatchedColumn = Math.min(lastMatchedColumn,
+ lastVisibleColumn);
+ firstCol = Math.min(firstCol, firstMatchedColumn);
+ lastCol = Math.max(lastCol, lastMatchedColumn);
+ }
+ }
+ }
+ }
+
+ if (matchFound)
+ {
+ if (av.hasHiddenColumns())
+ {
+ firstCol = alignment.getHiddenColumns()
+ .absoluteToVisibleColumn(firstCol);
+ lastCol = alignment.getHiddenColumns().absoluteToVisibleColumn(lastCol);
+ }
+ int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
+ int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
+ Graphics gg = img.getGraphics();
+ gg.translate(transX, transY);
+ drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
+ gg.translate(-transX, -transY);
+ gg.dispose();
+ }
+
+ return matchFound;
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt)
+ {
+ String eventName = evt.getPropertyName();
+ // System.err.println(">>SeqCanvas propertyChange " + eventName);
+ if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
+ {
+ fastPaint = true;
+ repaint();
+ return;
+ }
+ else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT))
+ {
+ fastPaint = false;
+ // System.err.println("!!!! fastPaint false from MOVE_VIEWPORT");
+ repaint();
+ return;
+ }
+
+ int scrollX = 0;
+ if (eventName.equals(ViewportRanges.STARTRES)
+ || eventName.equals(ViewportRanges.STARTRESANDSEQ))
+ {
+ // Make sure we're not trying to draw a panel
+ // larger than the visible window
+ if (eventName.equals(ViewportRanges.STARTRES))
+ {
+ scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
+ }
+ else
+ {
+ scrollX = ((int[]) evt.getNewValue())[0]
+ - ((int[]) evt.getOldValue())[0];
+ }
+ ViewportRanges vpRanges = av.getRanges();
+
+ int range = vpRanges.getEndRes() - vpRanges.getStartRes() + 1;
+ if (scrollX > range)
+ {
+ scrollX = range;
+ }
+ else if (scrollX < -range)
+ {
+ scrollX = -range;
+ }
+ }
+ // Both scrolling and resizing change viewport ranges: scrolling changes
+ // both start and end points, but resize only changes end values.
+ // Here we only want to fastpaint on a scroll, with resize using a normal
+ // paint, so scroll events are identified as changes to the horizontal or
+ // vertical start value.
+ if (eventName.equals(ViewportRanges.STARTRES))
+ {
+ if (av.getWrapAlignment())
+ {
+ fastPaintWrapped(scrollX);
+ }
+ else
+ {
+ fastPaint(scrollX, 0);
+ }
+ }
+ else if (eventName.equals(ViewportRanges.STARTSEQ))
+ {
+ // scroll
+ fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+ }
+ else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+ {
+ if (av.getWrapAlignment())
+ {
+ fastPaintWrapped(scrollX);
+ }
+ else
+ {
+ fastPaint(scrollX, 0);
+ }
+ }
+ else if (eventName.equals(ViewportRanges.STARTSEQ))
+ {
+ // scroll
+ fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+ }
+ else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+ {
+ if (av.getWrapAlignment())
+ {
+ fastPaintWrapped(scrollX);
+ }
+ }
+ }
+
+ /**
+ * Does a minimal update of the image for a scroll movement. This method
+ * handles scroll movements of up to one width of the wrapped alignment (one
+ * click in the vertical scrollbar). Larger movements (for example after a
+ * scroll to highlight a mapped position) trigger a full redraw instead.
+ *
+ * @param scrollX
+ * number of positions scrolled (right if positive, left if negative)
+ */
+ protected void fastPaintWrapped(int scrollX)
+ {
+ ViewportRanges ranges = av.getRanges();
+
+ if (Math.abs(scrollX) >= ranges.getViewportWidth())
+ {
+ /*
+ * shift of one view width or more is
+ * overcomplicated to handle in this method
+ */
+ fastPaint = false;
+ repaint();
+ return;
+ }
+
+ if (fastpainting || img == null)
+ {
+ return;
+ }
+
+ fastPaint = true;
+ fastpainting = true;
+
+ try
+ {
+
+ Graphics gg = img.getGraphics();
+
+ calculateWrappedGeometry(getWidth(), getHeight());
+
+ /*
+ * relocate the regions of the alignment that are still visible
+ */
+ shiftWrappedAlignment(-scrollX);
+
+ /*
+ * add new columns (sequence, annotation)
+ * - at top left if scrollX < 0
+ * - at right of last two widths if scrollX > 0
+ */
+ if (scrollX < 0)
+ {
+ int startRes = ranges.getStartRes();
+ drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
+ - scrollX - 1, getHeight());
+ }
+ else
+ {
+ fastPaintWrappedAddRight(scrollX);
+ }
+
+ /*
+ * draw all scales (if shown) and hidden column markers
+ */
+ drawWrappedDecorators(gg, ranges.getStartRes());
+
+ gg.dispose();
+
+ repaint();
+ } finally
+ {
+ fastpainting = false;
+ }
+ }
+
+ /**
+ * Draws the specified number of columns at the 'end' (bottom right) of a
+ * wrapped alignment view, including sequences and annotations if shown, but
+ * not scales. Also draws the same number of columns at the right hand end of
+ * the second last width shown, if the last width is not full height (so
+ * cannot simply be copied from the graphics image).
+ *
+ * @param columns
+ */
+ protected void fastPaintWrappedAddRight(int columns)
+ {
+ if (columns == 0)
+ {
+ return;
+ }
+
+ Graphics gg = img.getGraphics();
+
+ ViewportRanges ranges = av.getRanges();
+ int viewportWidth = ranges.getViewportWidth();
+ int charWidth = av.getCharWidth();
+
+ /**
+ * draw full height alignment in the second last row, last columns, if the
+ * last row was not full height
+ */
+ int visibleWidths = wrappedVisibleWidths;
+ int canvasHeight = getHeight();
+ boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
+
+ if (lastWidthPartHeight)
+ {
+ int widthsAbove = Math.max(0, visibleWidths - 2);
+ int ypos = wrappedRepeatHeightPx * widthsAbove
+ + wrappedSpaceAboveAlignment;
+ int endRes = ranges.getEndRes();
+ endRes += widthsAbove * viewportWidth;
+ int startRes = endRes - columns;
+ int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+ * charWidth;
+
+ /*
+ * white fill first to erase annotations
+ */
+
+
+ gg.translate(xOffset, 0);
+ gg.setColor(Color.white);
+ gg.fillRect(labelWidthWest, ypos,
+ (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
+ gg.translate(-xOffset, 0);
+
+ drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+
+ }
+
+ /*
+ * draw newly visible columns in last wrapped width (none if we
+ * have reached the end of the alignment)
+ * y-offset for drawing last width is height of widths above,
+ * plus one gap row
+ */
+ int widthsAbove = visibleWidths - 1;
+ int ypos = wrappedRepeatHeightPx * widthsAbove
+ + wrappedSpaceAboveAlignment;
+ int endRes = ranges.getEndRes();
+ endRes += widthsAbove * viewportWidth;
+ int startRes = endRes - columns + 1;
+
+ /*
+ * white fill first to erase annotations
+ */
+ int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+ * charWidth;
+ gg.translate(xOffset, 0);
+ gg.setColor(Color.white);
+ int width = viewportWidth * charWidth - xOffset;
+ gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
+ gg.translate(-xOffset, 0);
+
+ gg.setFont(av.getFont());
+ gg.setColor(Color.black);
+
+ if (startRes < ranges.getVisibleAlignmentWidth())
+ {
+ drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+ }
+
+ /*
+ * and finally, white fill any space below the visible alignment
+ */
+ int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
+ if (heightBelow > 0)
+ {
+ gg.setColor(Color.white);
+ gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
+ }
+ gg.dispose();
+ }
+
+ /**
+ * Shifts the visible alignment by the specified number of columns - left if
+ * negative, right if positive. Copies and moves sequences and annotations (if
+ * shown). Scales, hidden column markers and any newly visible columns must be
+ * drawn separately.
+ *
+ * @param positions
+ */
+ protected void shiftWrappedAlignment(int positions)
+ {
+ if (positions == 0)
+ {
+ return;
+ }
+
+ Graphics gg = img.getGraphics();
+
+ int charWidth = av.getCharWidth();
+
+ int canvasHeight = getHeight();
+ ViewportRanges ranges = av.getRanges();
+ int viewportWidth = ranges.getViewportWidth();
+ int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
+ * charWidth;
+ int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
+ int xMax = ranges.getVisibleAlignmentWidth();
+
+ if (positions > 0)
+ {
+ /*
+ * shift right (after scroll left)
+ * for each wrapped width (starting with the last), copy (width-positions)
+ * columns from the left margin to the right margin, and copy positions
+ * columns from the right margin of the row above (if any) to the
+ * left margin of the current row
+ */
+
+ /*
+ * get y-offset of last wrapped width, first row of sequences
+ */
+ int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
+ y += wrappedSpaceAboveAlignment;
+ int copyFromLeftStart = labelWidthWest;
+ int copyFromRightStart = copyFromLeftStart + widthToCopy;
+
+ while (y >= 0)
+ {
+ /*
+ * shift 'widthToCopy' residues by 'positions' places to the right
+ */
+ gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
+ positions * charWidth, 0);
+ if (y > 0)
+ {
+ /*
+ * copy 'positions' residue from the row above (right hand end)
+ * to this row's left hand end
+ */
+ gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
+ positions * charWidth, heightToCopy, -widthToCopy,
+ wrappedRepeatHeightPx);
+ }
+
+ y -= wrappedRepeatHeightPx;
+ }
+ }
+ else
+ {
+ /*
+ * shift left (after scroll right)
+ * for each wrapped width (starting with the first), copy (width-positions)
+ * columns from the right margin to the left margin, and copy positions
+ * columns from the left margin of the row below (if any) to the
+ * right margin of the current row
+ */
+ int xpos = av.getRanges().getStartRes();
+ int y = wrappedSpaceAboveAlignment;
+ int copyFromRightStart = labelWidthWest - positions * charWidth;
+
+ while (y < canvasHeight)
+ {
+ gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
+ positions * charWidth, 0);
+ if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
+ && (xpos + viewportWidth <= xMax))
+ {
+ gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
+ * charWidth, heightToCopy, widthToCopy,
+ -wrappedRepeatHeightPx);
+ }
+ y += wrappedRepeatHeightPx;
+ xpos += viewportWidth;
+ }
+ }
+ gg.dispose();
+ }
+
+
+ /**
+ * Redraws any positions in the search results in the visible region of a
+ * wrapped alignment. Any highlights are drawn depending on the search results
+ * set on the Viewport, not the results
argument. This allows
+ * this method to be called either to clear highlights (passing the previous
+ * search results), or to draw new highlights.
+ *
+ * @param results
+ * @return
+ */
+ protected boolean drawMappedPositionsWrapped(SearchResultsI results)
+ {
+ if ((results == null) || (img == null)) // JAL-2784 check gg is not null
+ {
+ return false;
+ }
+ int charHeight = av.getCharHeight();
+
+ boolean matchFound = false;
+
+ calculateWrappedGeometry(getWidth(), getHeight());
+ int wrappedWidth = av.getWrappedWidth();
+ int wrappedHeight = wrappedRepeatHeightPx;
+
+ ViewportRanges ranges = av.getRanges();
+ int canvasHeight = getHeight();
+ int repeats = canvasHeight / wrappedHeight;
+ if (canvasHeight / wrappedHeight > 0)
+ {
+ repeats++;
+ }
+
+ int firstVisibleColumn = ranges.getStartRes();
+ int lastVisibleColumn = ranges.getStartRes() + repeats
+ * ranges.getViewportWidth() - 1;
+
+ AlignmentI alignment = av.getAlignment();
+ if (av.hasHiddenColumns())
+ {
+ firstVisibleColumn = alignment.getHiddenColumns()
+ .visibleToAbsoluteColumn(firstVisibleColumn);
+ lastVisibleColumn = alignment.getHiddenColumns()
+ .visibleToAbsoluteColumn(lastVisibleColumn);
+ }
+
+ int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
+
+
+ Graphics gg = img.getGraphics();
+
+ for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+ .getEndSeq(); seqNo++)
+ {
+ SequenceI seq = alignment.getSequenceAt(seqNo);
+
+ int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+ lastVisibleColumn);
+ if (visibleResults != null)
+ {
+ for (int i = 0; i < visibleResults.length - 1; i += 2)
+ {
+ int firstMatchedColumn = visibleResults[i];
+ int lastMatchedColumn = visibleResults[i + 1];
+ if (firstMatchedColumn <= lastVisibleColumn
+ && lastMatchedColumn >= firstVisibleColumn)
+ {
+ /*
+ * found a search results match in the visible region
+ */
+ firstMatchedColumn = Math.max(firstMatchedColumn,
+ firstVisibleColumn);
+ lastMatchedColumn = Math.min(lastMatchedColumn,
+ lastVisibleColumn);
+
+ /*
+ * draw each mapped position separately (as contiguous positions may
+ * wrap across lines)
+ */
+ for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
+ {
+ int displayColumn = mappedPos;
+ if (av.hasHiddenColumns())
+ {
+ displayColumn = alignment.getHiddenColumns()
+ .absoluteToVisibleColumn(displayColumn);
+ }
+
+ /*
+ * transX: offset from left edge of canvas to residue position
+ */
+ int transX = labelWidthWest
+ + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
+ * av.getCharWidth();
+
+ /*
+ * transY: offset from top edge of canvas to residue position
+ */
+ int transY = gapHeight;
+ transY += (displayColumn - ranges.getStartRes())
+ / wrappedWidth * wrappedHeight;
+ transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
+
+ /*
+ * yOffset is from graphics origin to start of visible region
+ */
+ int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
+ if (transY < getHeight())
+ {
+ matchFound = true;
+ gg.translate(transX, transY);
+ drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
+ yOffset);
+ gg.translate(-transX, -transY);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ gg.dispose();
+
+ return matchFound;
+ }
+
+ /**
+ * Answers the width in pixels of the left scale labels (0 if not shown)
+ *
+ * @return
+ */
+ int getLabelWidthWest()
+ {
+ return labelWidthWest;
+ }
+
+}