/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ 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.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 { 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 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; /** * Number of pixelsPerCol; */ private float pixelsPerCol; /** * 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; // reference to alignment, needed to get sequence groups private AlignmentI al; private ResidueShaderI shader; 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; /* * Overview width in pixels */ private final int w; /* * Overview height in pixels */ private final int h; public OverviewRenderer(AlignmentViewPanel panel, jalview.api.FeatureRenderer fr, OverviewDimensions od, AlignmentI alignment, ResidueShaderI resshader, OverviewResColourFinder colFinder) { 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(); pixelsPerSeq = od.getPixelsPerSeq(); pixelsPerCol = od.getPixelsPerCol(); colsPerPixel = Math.max(1, 1f / pixelsPerCol); skippingColumns = (pixelsPerCol < 1); } /** * 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. * * 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 void drawMiniMe() { state = STATE_INIT; mainLoop(); } protected void mainLoop() { out: while (!redraw) { switch (state) { 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); // 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) * pixelsPerSeq), h); // this is the key modification -- we use bscol to jump to the next column // when there are more columns than pixels. for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext)) { // asynchronous exit flag if (redraw) { break; } ++colNext; pixelEnd = getNextPixel(colNext, colNext); 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) { for (int col = pixelCol; col < pixelEnd; ++col) { // 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 = 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; } } } /** * 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[] calcColumnsToShow() { int[] a = new int[w + 1]; float colBuffer = 0; float offset = bscol.nextSetBit(0); if (offset < 0) { return new int[] { -1 }; } 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;) { // 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 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; } /** * 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) { return (skippingColumns ? columnsToShow[pixel] : bscol.nextSetBit(icol + 1)); } /** * 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); } private boolean loop() { if (delay <= 0) { return false; } if (timer == null) { timer = new Timer(delay, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { mainLoop(); } }); timer.setRepeats(false); timer.start(); } else { timer.restart(); } return true; } private void done() { // if (!redraw) // { // Platform.timeCheck( // "overviewrender " + ndone + " pixels row:" + row + " redraw:" // + redraw, // Platform.TIME_MARK); // } overlayHiddenRegions(); if (showProgress) { // 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 { // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0); sendProgressUpdate(1, 1, 0, 0); } } 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; } /* * 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) { return (seq == null || icol >= seq.getLength() ? resColFinder.gapColourInt : resColFinder.getResidueColour(true, shader, allGroups, seq, icol, finder).getRGB()); } /** * Overlay the hidden regions on the overview image * */ private void overlayHiddenRegions() { if (cols.hasHidden() || rows.hasHidden()) { 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)) { 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; } } } if (rows.hasHidden()) { 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); // get details of this alignment row if (rows.isHidden(alignmentRow)) { g2d.fillRect(0, pixelRow, w, endRow - pixelRow); } pixelRow = endRow; } } g2d.dispose(); return hiddenImage; } /** * Draw the alignment annotation in the overview panel * * @param anno * alignment annotation information */ 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, w, y); 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; } ++colNext; pixelEnd = getNextPixel(colNext, colNext); Annotation ann = annotations[icol]; if (ann != null) { 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); } pixelCol = pixelEnd; } g.translate(0, -alignmentHeight); g.dispose(); 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) { redraw = b; } } public void addPropertyChangeListener(RendererListenerI listener) { changeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(RendererListenerI listener) { changeSupport.removePropertyChangeListener(listener); } }