X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Fjalview%2Frenderer%2FOverviewRenderer.java;h=82e89e5d3bc8e751e8ebe932ecede33c82238f7f;hb=732b23aab5c88efbd8ca24db9e432345492ed9b1;hp=77c3700b01b5049ce949ccae84113b8123eb7ae0;hpb=060557f7086fd2aab4fea43ecbf21e2ef4191dd1;p=jalview.git diff --git a/src/jalview/renderer/OverviewRenderer.java b/src/jalview/renderer/OverviewRenderer.java index 77c3700..82e89e5 100644 --- a/src/jalview/renderer/OverviewRenderer.java +++ b/src/jalview/renderer/OverviewRenderer.java @@ -22,35 +22,98 @@ package jalview.renderer; import jalview.api.AlignmentColsCollectionI; import jalview.api.AlignmentRowsCollectionI; +import jalview.api.AlignmentViewPanel; +import jalview.api.RendererListenerI; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.Annotation; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.renderer.seqfeatures.FeatureColourFinder; -import jalview.renderer.seqfeatures.FeatureRenderer; -import jalview.util.Comparison; +import jalview.util.Platform; import jalview.viewmodel.OverviewDimensions; +import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.WritableRaster; +import java.beans.PropertyChangeSupport; +import java.util.BitSet; +import java.util.Iterator; + +import javax.swing.Timer; public class OverviewRenderer { - private FeatureColourFinder finder; + public static final String UPDATE = "OverviewUpdate"; + + // transparency of hidden cols/seqs overlay + private static final float TRANSPARENCY = 0.5f; + + private static final int MAX_PROGRESS = 100; + + private static final int STATE_INIT = 0; + + private static final int STATE_NEXT = 1; + + private static final int STATE_DONE = 2; - private jalview.api.SequenceRenderer sr; + private int state; + + private Timer timer; + + private int delay = (Platform.isJS() ? 1 : 0); + + private int seqIndex; + + private int pixelRow; + + private Integer row; + + private PropertyChangeSupport changeSupport = new PropertyChangeSupport( + this); + + private FeatureColourFinder finder; // image to render on private BufferedImage miniMe; - // raw number of pixels to allocate to each column + /** + * Number of pixelsPerCol; + */ private float pixelsPerCol; - // raw number of pixels to allocate to each row + /** + * Number of visible columns per pixel. + * + */ + private float colsPerPixel; + + /** + * raw number of pixels to allocate to each row + */ private float pixelsPerSeq; + /** + * true when colsPerPixel > 1 + */ + private boolean skippingColumns; + + /** + * pre-calculated list of columns needed for a "dense" overview, where there + * are more columns than pixels + */ + + private int[] columnsToShow; + + // height in pixels of graph + private int graphHeight; + // flag to indicate whether to halt drawing private volatile boolean redraw = false; @@ -59,262 +122,582 @@ public class OverviewRenderer private ResidueShaderI shader; - public OverviewRenderer(jalview.api.SequenceRenderer seqRenderer, - FeatureRenderer fr, OverviewDimensions od, AlignmentI alignment, - ResidueShaderI resshader) + private OverviewResColourFinder resColFinder; + + private boolean showProgress; + + private AlignmentViewPanel panel; + + private int ndone = 0; + + private AlignmentRowsCollectionI rows; + + private AlignmentColsCollectionI cols; + + private Iterator rowIterator; + + private int alignmentHeight; + + private int totalPixels; + + private int lastRowUpdate; + + private int lastUpdate; + + private int[] pixels; + + private BitSet bscol; + + private final int w; + + private final int h; + + public OverviewRenderer(AlignmentViewPanel panel, + jalview.api.FeatureRenderer fr, OverviewDimensions od, + AlignmentI alignment, ResidueShaderI resshader, + OverviewResColourFinder colFinder) { - sr = seqRenderer; - finder = new FeatureColourFinder(fr); + this(panel, fr, od, alignment, resshader, colFinder, true); + } + /** + * @param panel + * @param fr + * @param od + * @param alignment + * @param resshader + * @param colFinder + * @param showProgress + * possibly not, in JavaScript and for testng + */ + public OverviewRenderer(AlignmentViewPanel panel, + jalview.api.FeatureRenderer fr, OverviewDimensions od, + AlignmentI alignment, ResidueShaderI resshader, + OverviewResColourFinder colFinder, boolean showProgress) + { + this.panel = panel; + finder = new FeatureColourFinder(fr); al = alignment; shader = resshader; + resColFinder = colFinder; + this.showProgress = showProgress; + + w = od.getWidth(); + h = od.getHeight(); + rows = od.getRows(alignment); + cols = od.getColumns(alignment); + graphHeight = od.getGraphHeight(); + alignmentHeight = od.getSequencesHeight(); - pixelsPerCol = od.getPixelsPerCol(); pixelsPerSeq = od.getPixelsPerSeq(); - miniMe = new BufferedImage(od.getWidth(), od.getHeight(), - BufferedImage.TYPE_INT_RGB); + pixelsPerCol = od.getPixelsPerCol(); + colsPerPixel = Math.max(1, 1f / pixelsPerCol); + + skippingColumns = (pixelsPerCol < 1); } /** - * Draw alignment rows and columns onto an image + * Draw alignment rows and columns onto an image. This method is asynchronous + * in JavaScript and interruptible in Java. + * + * Whether hidden rows or columns are drawn depends upon the type of + * collection. + * + * Updated to skip through high-density sequences, where columns/pixels > 1. * - * @param rit - * Iterator over rows to be drawn - * @param cit - * Iterator over columns to be drawn + * When the process is complete, the image is passed to the AlignmentViewPanel + * provided by the constructor. + * + * @param rows + * collection of rows to be drawn + * @param cols + * collection of columns to be drawn * @return image containing the drawing + * + * @author Bob Hanson 2019.07.30 */ - public BufferedImage draw(AlignmentRowsCollectionI rows, - AlignmentColsCollectionI cols) + public void drawMiniMe() { - int rgbcolor = Color.white.getRGB(); - int seqIndex = 0; - int pixelRow = 0; + state = STATE_INIT; + mainLoop(); + } - for (int alignmentRow : rows) + protected void mainLoop() + { + out: while (!redraw) { - if (redraw) + switch (state) { - break; + case STATE_INIT: + init(); + state = STATE_NEXT; + continue; + case STATE_NEXT: + if (!rowIterator.hasNext()) + { + state = STATE_DONE; + continue; + } + nextRow(); + if (!loop()) + { + // Java + continue; + } + // JavaScript + return; + case STATE_DONE: + break out; } + // Java will continue without a timeout + } + done(); + } + + private void init() + { + rowIterator = rows.iterator(); + seqIndex = 0; + pixelRow = 0; + lastRowUpdate = 0; + lastUpdate = 0; + totalPixels = w * alignmentHeight; + if (showProgress) + { + changeSupport.firePropertyChange(UPDATE, -1, 0); + } + + miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + WritableRaster raster = miniMe.getRaster(); + DataBufferInt db = (DataBufferInt) raster.getDataBuffer(); + pixels = db.getBankData()[0]; + bscol = cols.getShownBitSet(); + if (skippingColumns) + { + columnsToShow = calcColumnsToShow(); + } + + Platform.timeCheck(null, Platform.TIME_MARK); + } + + private void nextRow() + { + row = rowIterator.next(); + SequenceI seq = rows.getSequence(row); - // get details of this alignment row - boolean hidden = rows.isHidden(alignmentRow); - SequenceI seq = rows.getSequence(alignmentRow); - // rate limiting step when rendering overview for lots of groups - SequenceGroup[] allGroups = al.findAllGroups(seq); + // rate limiting step when rendering overview for lots of groups + SequenceGroup[] allGroups = al.findAllGroups(seq); - // calculate where this row extends to in pixels - int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1, - miniMe.getHeight() - 1); + // calculate where this row extends to in pixels + int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h); + // this is the key modification -- we use bscol to jump to the next column + // when there are more columns than pixels. - int colIndex = 0; - int pixelCol = 0; - for (int alignmentCol : cols) + for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol + .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext)) + { + // asynchronous exit flag + if (redraw) { - if (redraw) - { - break; - } + break; + } - // calculate where this column extends to in pixels - int endCol = Math.min( - Math.round((colIndex + 1) * pixelsPerCol) - 1, - miniMe.getWidth() - 1); + ++colNext; + pixelEnd = getNextPixel(colNext, colNext); - // don't do expensive colour determination if we're not going to use it - // NB this is important to avoid performance issues in the overview - // panel - if (pixelCol <= endCol) + if (pixelCol == pixelEnd) + { + break; + } + else if (pixelCol < pixelEnd) + { + int rgb = getColumnColourFromSequence(allGroups, seq, icol); + // fill in the appropriate number of pixels + for (int row = pixelRow; row < endRow; ++row) { - // determine the colour based on the sequence and column position - rgbcolor = getColumnColourFromSequence(allGroups, seq, - hidden || cols.isHidden(alignmentCol), alignmentCol, - finder); - - // fill in the appropriate number of pixels - for (int row = pixelRow; row <= endRow; ++row) + for (int col = pixelCol; col < pixelEnd; ++col) { - for (int col = pixelCol; col <= endCol; ++col) - { - miniMe.setRGB(col, row, rgbcolor); - } + // BH 2019.07.27 was: + // + // miniMe.setRGB(col, row, rgbcolor); + // + // but just directly writing to the int[] pixel buffer + // is three times faster by my experimentation + pixels[row * w + col] = rgb; + ndone++; } - - pixelCol = endCol + 1; } - colIndex++; + pixelCol = pixelEnd; + // store last update value + if (showProgress) + { + lastUpdate = sendProgressUpdate( + pixelEnd * (endRow - 1 - pixelRow), totalPixels, + lastRowUpdate, lastUpdate); + } + } + + } + if (pixelRow < endRow) + { + pixelRow = endRow; + // store row offset and last update value + if (showProgress) + { + // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be + // pixelRow + 1, surely + lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0, + lastUpdate); + lastUpdate = lastRowUpdate; } - pixelRow = endRow + 1; - seqIndex++; } - return miniMe; } - /* - * Find the colour of a sequence at a specified column position + /** + * Precalculate the columns that will be used for each pixel in a dense + * overview. So we have to skip through the bscol BitSet to pick up one + * (representative?) column for each pixel. + * + * Note that there is no easy solution if we want to do color averaging, but + * this method might be adapted to do that. Or it could be adapted to pick the + * "most representative color" for a group of columns. + * + * @author Bob Hanson 2019.09.03 + * @return a -1 terminated int[] */ - private int getColumnColourFromSequence(SequenceGroup[] allGroups, - jalview.datamodel.SequenceI seq, - boolean isHidden, int lastcol, FeatureColourFinder fcfinder) + private int[] calcColumnsToShow() { - Color color = Color.white; - - if ((seq != null) && (seq.getLength() > lastcol)) + int[] a = new int[w + 1]; + float colBuffer = 0; + float offset = bscol.nextSetBit(0); + if (offset < 0) { - color = getResidueColour(allGroups, seq, lastcol, fcfinder); + return new int[] { -1 }; } - - if (isHidden) + int pixel = 0; + a[pixel++] = (int) offset; + // for example, say we have 10 pixels per column: + // ...............xxxxxxxx....xxxxxx.........xxxxxx...... + // nextSet(i).....^...........^..............^........... + // nextClear..............^.........^..............^..... + // run lengths....|--n1--|....|-n2-|.........|-n3-|...... + // 10 pixel/col...|---pixel1---||-----pixel2------|...... + // pixel..........^0............^1....................... + for (int i, iClear = -1; pixel < w + && (i = bscol.nextSetBit(iClear + 1)) >= 0;) { - color = color.darker().darker(); - } + // find the next clear bit + iClear = bscol.nextClearBit(i + 1); + // add the run length n1, n2, n3 to grow the column buffer + colBuffer += iClear - i; // n1, n2, etc. + // add columns if we have accumulated enough pixels - return color.getRGB(); + while (colBuffer > colsPerPixel && pixel < w) + { + colBuffer -= colsPerPixel; + offset += colsPerPixel; + a[pixel++] = i + (int) offset; + } + // set back column pointer relative to the next run + offset = -colBuffer; + } + // add a terminator + a[pixel] = -1; + return a; } - private Color getResidueColour(SequenceGroup[] allGroups, - final SequenceI seq, int position, - FeatureColourFinder finder) + /** + * The next column is either a precalculated pixel (when there are multiple + * pixels per column) or the next set bit for the column that aligns with the + * next pixel (when there are more columns than pixels). + * + * When columns are hidden, this value is precalculated; otherwise it is + * calculated here. + * + * @param icol + * @param pixel + * pixel pointer into columnsToShow + * @return + */ + private int getNextCol(int icol, int pixel) { - Color col = getResidueBoxColour(allGroups, seq, position); - - if (finder != null) - { - col = finder.findFeatureColour(col, seq, position); - } - return col; + return (skippingColumns ? columnsToShow[pixel] + : bscol.nextSetBit(icol + 1)); } - protected Color getResidueBoxColour(SequenceGroup[] allGroups, - SequenceI seq, int i) + /** + * Derive the next pixel from either as the given pixel (when we are skipping + * columns because this is a dense overview and the pixel known), or from the + * current column based on pixels/column. The latter is used for drawing the + * hidden-column mask or for overviews that have more pixels across than + * columns. + * + * @param icol + * @param pixel + * @return + */ + private int getNextPixel(int icol, int pixel) { + return Math.min(skippingColumns && pixel > 0 ? pixel + : Math.round(icol * pixelsPerCol), w); + } - ResidueShaderI currentShader; - - SequenceGroup currentSequenceGroup = inCurrentSequenceGroup(allGroups, - i); - if (currentSequenceGroup != null) + private boolean loop() + { + if (delay <= 0) { - currentShader = currentSequenceGroup.getGroupColourScheme(); + return false; + } + if (timer == null) + { + timer = new Timer(delay, new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + mainLoop(); + } + }); + timer.setRepeats(false); + timer.start(); } else { - currentShader = shader; + timer.restart(); } - - return getBoxColour(currentShader, seq, i); + return true; } - SequenceGroup inCurrentSequenceGroup(SequenceGroup[] allGroups, int res) + private void done() { - if (allGroups == null) + if (!redraw) { - return null; + Platform.timeCheck( + "overviewrender " + ndone + " pixels row:" + row + " redraw:" + + redraw, + Platform.TIME_MARK); } - for (int i = 0; i < allGroups.length; i++) + overlayHiddenRegions(); + if (showProgress) { - if ((allGroups[i].getStartRes() <= res) - && (allGroups[i].getEndRes() >= res)) + // final update to progress bar if present + if (redraw) + { + // aborted in Java + // BH was pixelRow - 1, but that could go negative + sendProgressUpdate(pixelRow, alignmentHeight, 0, 0); + } + else { - return (allGroups[i]); + // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0); + sendProgressUpdate(1, 1, 0, 0); } } - return null; + panel.overviewDone(miniMe); + } + + /* + * Calculate progress update value and fire event + * @param rowOffset number of rows to offset calculation by + * @return new rowOffset - return value only to be used when at end of a row + */ + private int sendProgressUpdate(int position, int maximum, int rowOffset, + int lastUpdate) + { + int newUpdate = rowOffset + + Math.round(MAX_PROGRESS * ((float) position / maximum)); + if (newUpdate > lastUpdate) + { + changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate); + return newUpdate; + } + return newUpdate; } - Color getBoxColour(ResidueShaderI shader, SequenceI seq, int i) + /* + * Find the RGB value of the colour of a sequence at a specified column position + * + * @param seq + * sequence to get colour for + * @param lastcol + * column position to get colour for + * @return colour of sequence at this position, as RGB + */ + int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq, + int icol) { - Color resBoxColour = Color.white; - char currentChar = seq.getCharAt(i); + return (seq == null || icol >= seq.getLength() + ? resColFinder.gapColourInt + : resColFinder.getResidueColour(true, shader, allGroups, seq, + icol, finder).getRGB()); + } - if (shader.getColourScheme() != null) + /** + * Overlay the hidden regions on the overview image + * + */ + private void overlayHiddenRegions() + { + if (cols.hasHidden() || rows.hasHidden()) { - if (Comparison.isGap(currentChar) - && !shader.getColourScheme().hasGapColour()) - { - resBoxColour = Color.lightGray; - } - else + BufferedImage mask = buildHiddenImage(); + + Graphics2D g = (Graphics2D) miniMe.getGraphics(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + TRANSPARENCY)); + g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null); + g.dispose(); + } + } + + /** + * Build a masking image of hidden columns and rows to be applied on top of + * the main overview image. + * + * @param rows + * collection of rows the overview is built over + * @param cols + * collection of columns the overview is built over + * @param width + * width of overview in pixels + * @param height + * height of overview in pixels + * @return BufferedImage containing mask of hidden regions + */ + private BufferedImage buildHiddenImage() + { + // new masking image + BufferedImage hiddenImage = new BufferedImage(w, h, + BufferedImage.TYPE_INT_ARGB); + + Color hidden = resColFinder.getHiddenColour(); + + Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics(); + + g2d.setColor(hidden); + // set background to transparent + // g2d.setComposite(AlphaComposite.Clear); + // g2d.fillRect(0, 0, width, height); + + // set next colour to opaque + g2d.setComposite(AlphaComposite.Src); + + // System.out.println(cols.getClass().getName()); + if (cols.hasHidden()) + { + // AllColsCollection only + BitSet bs = cols.getHiddenBitSet(); + for (int pixelCol = -1, icol2 = 0, icol = bs + .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2)) { - resBoxColour = shader.findColour(currentChar, i, seq); + if (redraw) + { + break; + } + icol2 = bs.nextClearBit(icol + 1); + int pixelEnd = getNextPixel(icol2, 0); + if (pixelEnd > pixelCol) + { + pixelCol = getNextPixel(icol, 0); + g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol), + h); + pixelCol = pixelEnd; + } } } - else if (Comparison.isGap(currentChar)) + if (rows.hasHidden()) { - resBoxColour = Color.lightGray; - } + int seqIndex = 0; + int pixelRow = 0; + for (int alignmentRow : rows) + { + if (redraw) + { + break; + } + + // calculate where this row extends to in pixels + int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), + h); - return resBoxColour; + // get details of this alignment row + if (rows.isHidden(alignmentRow)) + { + // BH 2019.09.24 fixes JAL-3440 Java+JavaScript off by one row in + // height + g2d.fillRect(0, pixelRow, w, endRow - pixelRow); + } + pixelRow = endRow; + } + } + g2d.dispose(); + return hiddenImage; } /** * Draw the alignment annotation in the overview panel * - * @param g - * the graphics object to draw on * @param anno * alignment annotation information - * @param charWidth - * alignment character width value - * @param y - * y-position for the annotation graph - * @param cols - * the collection of columns used in the overview panel */ - public void drawGraph(Graphics g, AlignmentAnnotation anno, int charWidth, - int y, AlignmentColsCollectionI cols) + public void drawGraph(AlignmentAnnotation anno) { + int y = graphHeight; + Graphics g = miniMe.getGraphics(); + g.translate(0, alignmentHeight); + Annotation[] annotations = anno.annotations; + float max = anno.graphMax; g.setColor(Color.white); - g.fillRect(0, 0, miniMe.getWidth(), y); + g.fillRect(0, 0, w, y); - int height; - int colIndex = 0; - int pixelCol = 0; - for (int alignmentCol : cols) + for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol + .nextSetBit(0); icol >= 0 + && icol < len; icol = getNextCol(icol, colNext)) { if (redraw) { + if (showProgress) + { + changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0); + } break; } - if (alignmentCol >= annotations.length) + + ++colNext; + pixelEnd = getNextPixel(colNext, colNext); + Annotation ann = annotations[icol]; + if (ann != null) { - break; // no more annotations to draw here + Color color = ann.colour; + g.setColor(color == null ? Color.black : color); + int height = Math.min(y, (int) ((ann.value / max) * y)); + g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol), + height); } - else - { - int endCol = Math.min( - Math.round((colIndex + 1) * pixelsPerCol) - 1, - miniMe.getWidth() - 1); - - if (annotations[alignmentCol] != null) - { - if (annotations[alignmentCol].colour == null) - { - g.setColor(Color.black); - } - else - { - g.setColor(annotations[alignmentCol].colour); - } + pixelCol = pixelEnd; + } - height = (int) ((annotations[alignmentCol].value / anno.graphMax) * y); - if (height > y) - { - height = y; - } + g.translate(0, -alignmentHeight); + g.dispose(); - g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height); - } - pixelCol = endCol + 1; - colIndex++; - } + if (showProgress) + { + changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, + MAX_PROGRESS); } + } + /** + * Allows redraw flag to be set + * + * @param b + * value to set redraw to: true = redraw is occurring, false = no + * redraw + */ public void setRedraw(boolean b) { synchronized (this) @@ -322,4 +705,14 @@ public class OverviewRenderer redraw = b; } } + + public void addPropertyChangeListener(RendererListenerI listener) + { + changeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(RendererListenerI listener) + { + changeSupport.removePropertyChangeListener(listener); + } }