import jalview.datamodel.SequenceGroup;
import jalview.datamodel.SequenceI;
import jalview.renderer.seqfeatures.FeatureColourFinder;
-import jalview.renderer.seqfeatures.FeatureRenderer;
import jalview.util.Platform;
import jalview.viewmodel.OverviewDimensions;
private static final int MAX_PROGRESS = 100;
+ final static int STATE_INIT = 0;
+
+ final static int STATE_NEXT = 1;
+
+ final static int STATE_DONE = 2;
+
+ private int state;
+
+ private Timer timer;
+
+ private boolean isJS = Platform.isJS();
+
+ private int delay = (isJS ? 1 : 0);
+
+ private int seqIndex;
+
+ private int pixelRow;
+
+ private Integer row;
+
private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
this);
// 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;
private AlignmentViewPanel panel;
- public OverviewRenderer(AlignmentViewPanel panel, FeatureRenderer fr,
- OverviewDimensions od,
- AlignmentI alignment,
- ResidueShaderI resshader, OverviewResColourFinder colFinder)
+ // private int sequencesHeight;
+
+ 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 shwoProgress
+ * possibly not, in JavaScript and for testng
+ */
public OverviewRenderer(AlignmentViewPanel panel,
- jalview.api.FeatureRenderer fr,
- OverviewDimensions od,
+ jalview.api.FeatureRenderer fr, OverviewDimensions od,
AlignmentI alignment, ResidueShaderI resshader,
OverviewResColourFinder colFinder, boolean showProgress)
{
- this.panel = panel;
- finder = new FeatureColourFinder(fr);
- resColFinder = colFinder;
-
- al = alignment;
- shader = resshader;
-
- pixelsPerCol = od.getPixelsPerCol();
- pixelsPerSeq = od.getPixelsPerSeq();
- graphHeight = od.getGraphHeight();
- miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
- BufferedImage.TYPE_INT_RGB);
- this.showProgress = showProgress;
- }
-
- 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;
+ {
+ 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);
- int pixelRow;
+ }
+ }
- private Integer row;
+ /**
+ * 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();
+ }
- void mainLoop()
+ protected void mainLoop()
{
- while (!redraw)
+ out: while (!redraw)
{
switch (state)
{
case STATE_INIT:
- seqIndex = 0;
- pixelRow = 0;
+ init();
state = STATE_NEXT;
continue;
case STATE_NEXT:
- if (iter.hasNext())
+ if (!rowIterator.hasNext())
{
- nextRow();
+ state = STATE_DONE;
+ continue;
}
- else
+ nextRow();
+ if (!loop())
{
- state = STATE_DONE;
+ // Java
+ continue;
}
- break;
- case STATE_DONE:
- done();
- return;
- }
- if (delay > 0)
- {
- jsloop();
+ // JavaScript
return;
+ case STATE_DONE:
+ break out;
}
+ // Java will continue without a timeout
}
done();
}
- private void jsloop()
+ private void init()
{
- if (timer == null)
+ rowIterator = rows.iterator();
+ seqIndex = 0;
+ pixelRow = 0;
+ lastRowUpdate = 0;
+ lastUpdate = 0;
+ totalPixels = w * alignmentHeight;
+ if (showProgress)
{
- timer = new Timer(delay, new ActionListener()
- {
- @Override
- public void actionPerformed(ActionEvent e)
- {
- mainLoop();
- }
-
- });
- timer.setRepeats(false);
- timer.start();
+ changeSupport.firePropertyChange(UPDATE, -1, 0);
}
- else
+
+ 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();
+ if (skippingColumns)
{
- timer.restart();
+ columnsToShow = calcColumnsToShow();
}
+
+ Platform.timeCheck(null, Platform.TIME_MARK);
}
private void nextRow()
{
- row = iter.next();
- // System.out.println("OR row " + r);
- // get details of this alignment row
+ row = rowIterator.next();
SequenceI seq = rows.getSequence(row);
// rate limiting step when rendering overview for lots of groups
// 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, colIndex = 0, c = bscol
- .nextSetBit(0); c >= 0; c = bscol.nextSetBit(c + 1))
+ for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
+ .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
{
+ // asynchronous exit flag
if (redraw)
{
break;
}
- // calculate where this column extends to in pixels
- int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol), w);
+ ++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)
{
- // System.out.println("OR pc ec " + pixelCol + " " + endCol);
- int rgb = getColumnColourFromSequence(allGroups, seq, c);
+ 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 < endCol; ++col)
+ for (int col = pixelCol; col < pixelEnd; ++col)
{
// BH 2019.07.27 was:
//
ndone++;
}
}
- // }
-
- pixelCol = endCol;
+ pixelCol = pixelEnd;
// store last update value
if (showProgress)
{
- lastUpdate = sendProgressUpdate(endCol * (endRow - 1 - pixelRow),
- totalPixels, lastRowUpdate, lastUpdate);
+ lastUpdate = sendProgressUpdate(
+ pixelEnd * (endRow - 1 - pixelRow), totalPixels,
+ lastRowUpdate, lastUpdate);
}
}
+
}
if (pixelRow < endRow)
{
}
}
+ /**
+ * 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 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()
{
- Platform.timeCheck(
- "overviewrender " + ndone + " pixels row:" + row + " redraw:"
- + redraw,
- Platform.TIME_MARK);
+ if (!redraw)
+ {
+ Platform.timeCheck(
+ "overviewrender " + ndone + " pixels row:" + row + " redraw:"
+ + redraw,
+ Platform.TIME_MARK);
+ }
- overlayHiddenRegions(rows, cols);
+ overlayHiddenRegions();
if (showProgress)
{
// final update to progress bar if present
private AlignmentColsCollectionI cols;
- Iterator<Integer> iter;
+ Iterator<Integer> rowIterator;
int alignmentHeight;
int[] pixels;
- BitSet bscol = new BitSet();
+ BitSet bscol;
int w, h;
- /**
- * Draw alignment rows and columns onto an image
- *
- * @param rit
- * Iterator over rows to be drawn
- * @param cit
- * Iterator over columns to be drawn
- * @return image containing the drawing
- */
- public BufferedImage draw(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols)
- {
- this.rows = rows;
- this.cols = cols;
- iter = rows.iterator();
-
- w = miniMe.getWidth();
- h = miniMe.getHeight();
- alignmentHeight = h - graphHeight;
- totalPixels = w * alignmentHeight;
- lastRowUpdate = 0;
- lastUpdate = 0;
-
- if (showProgress)
- {
- changeSupport.firePropertyChange(UPDATE, -1, 0);
- }
-
- WritableRaster raster = miniMe.getRaster();
- DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
- Platform.timeCheck(null, Platform.TIME_MARK);
- pixels = db.getBankData()[0];
- bscol.clear();
- for (int c : cols)
- {
- bscol.set(c);
- }
- state = STATE_INIT;
- mainLoop();
-
- return miniMe;
- }
-
/*
* Calculate progress update value and fire event
* @param rowOffset number of rows to offset calculation by
{
return (seq == null || icol >= seq.getLength()
? resColFinder.GAP_COLOUR
- : resColFinder.getResidueColour(true, shader, allGroups, seq,
- icol, finder)).getRGB();
+ : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
+ icol, finder));
}
/**
* Overlay the hidden regions on the overview image
*
- * @param rows
- * collection of rows the overview is built over
- * @param cols
- * collection of columns the overview is built over
*/
- private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols)
+ private void overlayHiddenRegions()
{
if (cols.hasHidden() || rows.hasHidden())
{
- BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
- miniMe.getHeight());
+ BufferedImage mask = buildHiddenImage();
Graphics2D g = (Graphics2D) miniMe.getGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
* height of overview in pixels
* @return BufferedImage containing mask of hidden regions
*/
- private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols, int width, int height)
+ private BufferedImage buildHiddenImage()
{
// new masking image
- BufferedImage hiddenImage = new BufferedImage(width, height,
+ 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())
{
- int colIndex = 0;
- int pixelCol = 0;
- for (int alignmentCol : cols)
+ // 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;
}
-
- // calculate where this column extends to in pixels
- int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol),
- width);
-
- // endCol is one more than old endCol
- if (pixelCol < endCol)
+ icol2 = bs.nextClearBit(icol + 1);
+ int pixelEnd = getNextPixel(icol2, 0);
+ if (pixelEnd > pixelCol)
{
- // determine the colour based on the sequence and column position
- if (cols.isHidden(alignmentCol))
- {
- g2d.setColor(hidden);
- g2d.fillRect(pixelCol, 0, endCol - pixelCol, height);
- }
- pixelCol = endCol;
+ pixelCol = getNextPixel(icol, 0);
+ g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
+ h);
+ pixelCol = pixelEnd;
}
}
}
// calculate where this row extends to in pixels
int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
- height);
+ h);
// get details of this alignment row
if (rows.isHidden(alignmentRow))
{
- g2d.setColor(hidden);
- g2d.fillRect(0, pixelRow, width, endRow - 1 - pixelRow);
+ g2d.fillRect(0, pixelRow, w, endRow - 1 - pixelRow);
}
pixelRow = endRow;
}
/**
* Draw the alignment annotation in the overview panel
*
- * @param g
- * the graphics object to draw on
* @param anno
* alignment annotation information
- * @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 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);
- int width = miniMe.getWidth();
- g.fillRect(0, 0, width, y);
+ g.fillRect(0, 0, w, y);
- int colIndex = 0;
- int pixelCol = 0;
- for (int icol : 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)
{
break;
}
- if (icol >= annotations.length)
- {
- break; // no more annotations to draw here
- }
- int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol), width);
+ ++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, endCol - pixelCol, height);
+ g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
+ height);
}
- pixelCol = endCol;
+ pixelCol = pixelEnd;
}
+
+ g.translate(0, -alignmentHeight);
+ g.dispose();
+
if (showProgress)
{
changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
MAX_PROGRESS);
}
+
}
/**