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
{
- private FeatureColourFinder finder;
+ // transparency of hidden cols/seqs overlay
+ private final float TRANSPARENCY = 0.5f;
+
+ public static final String UPDATE = "OverviewUpdate";
- private jalview.api.SequenceRenderer sr;
+ private static final int MAX_PROGRESS = 100;
+
+ 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;
+ /**
+ * Number of visible columns per pixel.
+ *
+ */
+ private float colsPerPixel;
+
// raw number of pixels to allocate to each row
private float pixelsPerSeq;
- public OverviewRenderer(jalview.api.SequenceRenderer seqRenderer,
- FeatureColourFinder colfinder, OverviewDimensions od)
+ // 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 sequencesHeight;
+
+ public OverviewRenderer(AlignmentViewPanel panel,
+ jalview.api.FeatureRenderer fr, OverviewDimensions od,
+ AlignmentI alignment, ResidueShaderI resshader,
+ OverviewResColourFinder colFinder)
{
- sr = seqRenderer;
- finder = colfinder;
+ this(panel, fr, od, alignment, resshader, colFinder, true);
+ }
+
+ /**
+ * @param panel
+ * @param fr
+ * @param od
+ * @param alignment
+ * @param resshader
+ * @param colFinder
+ * @param shwoProgress
+ * 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);
+ }
}
+ final static int STATE_INIT = 0;
+ final static int STATE_NEXT = 1;
+ final static int STATE_DONE = 2;
+
+ int state;
+
+ boolean isJS = Platform.isJS();
+
+ Timer timer;
+ int delay = (isJS ? 1 : 0);
+
+ int seqIndex;
+
+ int pixelRow;
+
+ private Integer row;
+
/**
- * 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()
+ {
+ 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()
{
- int rgbcolor = Color.white.getRGB();
- int seqIndex = 0;
- int pixelRow = 0;
- for (int alignmentRow : rows)
+ rowIterator = rows.iterator();
+ seqIndex = 0;
+ pixelRow = 0;
+ lastRowUpdate = 0;
+ lastUpdate = 0;
+ totalPixels = w * alignmentHeight;
+
+ if (showProgress)
{
- // get details of this alignment row
- boolean hidden = rows.isHidden(alignmentRow);
- SequenceI seq = rows.getSequence(alignmentRow);
+ 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.getOverviewBitSet();
+ Platform.timeCheck(null, Platform.TIME_MARK);
+ }
- // calculate where this row extends to in pixels
- int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
- miniMe.getHeight() - 1);
+ private void nextRow()
+ {
+ row = rowIterator.next();
+ // get details of this alignment row
+ SequenceI seq = rows.getSequence(row);
+
+ // rate limiting step when rendering overview for lots of groups
+ SequenceGroup[] allGroups = al.findAllGroups(seq);
- int colIndex = 0;
- int pixelCol = 0;
- for (int alignmentCol : cols)
+ // calculate where this row extends to in pixels
+ int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
+ for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
+ .nextSetBit(0); icol >= 0; icol = getNextCol(icol, pixelEnd))
+ {
+ if (redraw)
{
- // calculate where this column extends to in pixels
- int endCol = Math.min(
- Math.round((colIndex + 1) * pixelsPerCol) - 1,
- miniMe.getWidth() - 1);
+ break;
+ }
- // determine the colour based on the sequence and column position
- rgbcolor = getColumnColourFromSequence(seq,
- hidden || cols.isHidden(alignmentCol), alignmentCol, finder);
+ ++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)
+ // System.out.println(
+ // "OR colNext=" + colNext + " " + pixelCol
+ // + "-" + pixelEnd + " icol=" + icol + " " + rgb + " "
+ // + pixelsPerCol);
+ for (int row = pixelRow; row < endRow; ++row)
{
- for (int col = pixelCol; col <= endCol; ++col)
+ for (int col = pixelCol; col < pixelEnd; ++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 = 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;
+ }
+ }
+ }
+
+ /**
+ * The next column is either the next set bit (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).
+ *
+ * @param icol
+ * @param pixel
+ * @return
+ */
+ private int getNextCol(int icol, int pixel)
+ {
+ return bscol.nextSetBit(
+ pixelsPerCol >= 1 ? icol + 1 : (int) (pixel * colsPerPixel));
+ }
+
+ private int getNextPixel(int icol, int pixel)
+ {
+ return Math.min(
+ pixelsPerCol >= 1 || pixel == 0
+ ? Math.round(icol * pixelsPerCol)
+ : pixel,
+ w);
+ }
+
+ private ActionListener listener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mainLoop();
+ }
+
+ };
+
+ private boolean loop()
+ {
+ if (delay <= 0)
+ {
+ return false;
+ }
+ if (timer == null)
+ {
+ timer = new Timer(delay, listener);
+ 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);
+ }
- pixelCol = endCol + 1;
- colIndex++;
+ 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);
}
- pixelRow = endRow + 1;
- seqIndex++;
}
- return miniMe;
+ panel.overviewDone(miniMe);
+ }
+
+ int ndone = 0;
+
+ private AlignmentRowsCollectionI rows;
+
+ private AlignmentColsCollectionI cols;
+
+ Iterator<Integer> rowIterator;
+
+ int alignmentHeight;
+
+ int totalPixels;
+
+ int lastRowUpdate;
+
+ int lastUpdate;
+
+ int[] pixels;
+
+ BitSet bscol;
+
+ int w, h;
+
+ /*
+ * 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 colour of a sequence at a specified column position
+ * 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
*/
- private int getColumnColourFromSequence(jalview.datamodel.SequenceI seq,
- boolean isHidden, int lastcol, FeatureColourFinder fcfinder)
+ int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
+ int icol)
{
- Color color = Color.white;
+ return (seq == null || icol >= seq.getLength()
+ ? resColFinder.GAP_COLOUR
+ : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
+ icol, finder));
+ }
- if ((seq != null) && (seq.getLength() > lastcol))
+ /**
+ * Overlay the hidden regions on the overview image
+ *
+ */
+ private void overlayHiddenRegions()
+ {
+ if (cols.hasHidden() || rows.hasHidden())
{
- color = sr.getResidueColour(seq, lastcol, fcfinder);
+ 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);
- if (isHidden)
+ // set next colour to opaque
+ g2d.setComposite(AlphaComposite.Src);
+
+ // System.out.println(cols.getClass().getName());
+ if (cols.hasHidden())
{
- color = color.darker().darker();
+ // 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);
- return color.getRGB();
+ // get details of this alignment row
+ if (rows.isHidden(alignmentRow))
+ {
+ g2d.fillRect(0, pixelRow, w, endRow - 1 - 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, pixelEnd))
{
- if (alignmentCol >= annotations.length)
+ if (redraw)
{
- break; // no more annotations to draw here
+ if (showProgress)
+ {
+ changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
+ }
+ break;
}
- else
+
+ ++colNext;
+ pixelEnd = getNextPixel(colNext, colNext);
+ Annotation ann = annotations[icol];
+ if (ann != null)
{
- int endCol = Math.min(
- Math.round((colIndex + 1) * pixelsPerCol) - 1,
- miniMe.getWidth() - 1);
+ 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;
+ }
- if (annotations[alignmentCol] != null)
- {
- if (annotations[alignmentCol].colour == null)
- {
- g.setColor(Color.black);
- }
- else
- {
- g.setColor(annotations[alignmentCol].colour);
- }
+ g.translate(0, -alignmentHeight);
+ g.dispose();
- height = (int) ((annotations[alignmentCol].value / anno.graphMax) * y);
- if (height > y)
- {
- height = y;
- }
+ if (showProgress)
+ {
+ changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
+ MAX_PROGRESS);
+ }
- g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
- }
- pixelCol = endCol + 1;
- colIndex++;
- }
+ }
+
+ /**
+ * 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);
+ }
}