/*
* 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);
}
}