2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
21 package jalview.renderer;
23 import jalview.api.AlignmentColsCollectionI;
24 import jalview.api.AlignmentRowsCollectionI;
25 import jalview.api.AlignmentViewPanel;
26 import jalview.api.RendererListenerI;
27 import jalview.datamodel.AlignmentAnnotation;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.Annotation;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.datamodel.SequenceI;
32 import jalview.renderer.seqfeatures.FeatureColourFinder;
33 import jalview.util.Platform;
34 import jalview.viewmodel.OverviewDimensions;
36 import java.awt.AlphaComposite;
37 import java.awt.Color;
38 import java.awt.Graphics;
39 import java.awt.Graphics2D;
40 import java.awt.event.ActionEvent;
41 import java.awt.event.ActionListener;
42 import java.awt.image.BufferedImage;
43 import java.awt.image.DataBufferInt;
44 import java.awt.image.WritableRaster;
45 import java.beans.PropertyChangeSupport;
46 import java.util.BitSet;
47 import java.util.Iterator;
49 import javax.swing.Timer;
51 public class OverviewRenderer
53 public static final String UPDATE = "OverviewUpdate";
55 // transparency of hidden cols/seqs overlay
56 private static final float TRANSPARENCY = 0.5f;
58 private static final int MAX_PROGRESS = 100;
60 private static final int STATE_INIT = 0;
62 private static final int STATE_NEXT = 1;
64 private static final int STATE_DONE = 2;
70 private int delay = (Platform.isJS() ? 1 : 0);
78 private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
81 private FeatureColourFinder finder;
84 private BufferedImage miniMe;
87 * Number of pixelsPerCol;
89 private float pixelsPerCol;
92 * Number of visible columns per pixel.
95 private float colsPerPixel;
98 * raw number of pixels to allocate to each row
100 private float pixelsPerSeq;
103 * true when colsPerPixel > 1
105 private boolean skippingColumns;
108 * pre-calculated list of columns needed for a "dense" overview, where there
109 * are more columns than pixels
112 private int[] columnsToShow;
114 // height in pixels of graph
115 private int graphHeight;
117 // flag to indicate whether to halt drawing
118 private volatile boolean redraw = false;
120 // reference to alignment, needed to get sequence groups
121 private AlignmentI al;
123 private ResidueShaderI shader;
125 private OverviewResColourFinder resColFinder;
127 private boolean showProgress;
129 private AlignmentViewPanel panel;
131 private int ndone = 0;
133 private AlignmentRowsCollectionI rows;
135 private AlignmentColsCollectionI cols;
137 private Iterator<Integer> rowIterator;
139 private int alignmentHeight;
141 private int totalPixels;
143 private int lastRowUpdate;
145 private int lastUpdate;
147 private int[] pixels;
149 private BitSet bscol;
152 * Overview width in pixels
157 * Overview height in pixels
161 public OverviewRenderer(AlignmentViewPanel panel,
162 jalview.api.FeatureRenderer fr, OverviewDimensions od,
163 AlignmentI alignment, ResidueShaderI resshader,
164 OverviewResColourFinder colFinder)
166 this(panel, fr, od, alignment, resshader, colFinder, true);
176 * @param showProgress
177 * possibly not, in JavaScript and for testng
179 public OverviewRenderer(AlignmentViewPanel panel,
180 jalview.api.FeatureRenderer fr, OverviewDimensions od,
181 AlignmentI alignment, ResidueShaderI resshader,
182 OverviewResColourFinder colFinder, boolean showProgress)
185 finder = new FeatureColourFinder(fr);
188 resColFinder = colFinder;
189 this.showProgress = showProgress;
193 rows = od.getRows(alignment);
194 cols = od.getColumns(alignment);
195 graphHeight = od.getGraphHeight();
196 alignmentHeight = od.getSequencesHeight();
198 pixelsPerSeq = od.getPixelsPerSeq();
199 pixelsPerCol = od.getPixelsPerCol();
200 colsPerPixel = Math.max(1, 1f / pixelsPerCol);
202 skippingColumns = (pixelsPerCol < 1);
206 * Draw alignment rows and columns onto an image. This method is asynchronous
207 * in JavaScript and interruptible in Java.
209 * Whether hidden rows or columns are drawn depends upon the type of
212 * Updated to skip through high-density sequences, where columns/pixels > 1.
214 * When the process is complete, the image is passed to the AlignmentViewPanel
215 * provided by the constructor.
218 * collection of rows to be drawn
220 * collection of columns to be drawn
221 * @return image containing the drawing
223 * @author Bob Hanson 2019.07.30
225 public void drawMiniMe()
231 protected void mainLoop()
242 if (!rowIterator.hasNext())
258 // Java will continue without a timeout
265 rowIterator = rows.iterator();
270 totalPixels = w * alignmentHeight;
273 changeSupport.firePropertyChange(UPDATE, -1, 0);
276 miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
277 WritableRaster raster = miniMe.getRaster();
278 DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
279 pixels = db.getBankData()[0];
280 bscol = cols.getShownBitSet();
283 columnsToShow = calcColumnsToShow();
286 // Platform.timeCheck(null, Platform.TIME_MARK);
289 private void nextRow()
291 row = rowIterator.next();
292 SequenceI seq = rows.getSequence(row);
294 // rate limiting step when rendering overview for lots of groups
295 SequenceGroup[] allGroups = al.findAllGroups(seq);
297 // calculate where this row extends to in pixels
298 int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
299 // this is the key modification -- we use bscol to jump to the next column
300 // when there are more columns than pixels.
302 for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
303 .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
305 // asynchronous exit flag
312 pixelEnd = getNextPixel(colNext, colNext);
314 if (pixelCol == pixelEnd)
318 else if (pixelCol < pixelEnd)
320 int rgb = getColumnColourFromSequence(allGroups, seq, icol);
321 // fill in the appropriate number of pixels
322 for (int row = pixelRow; row < endRow; ++row)
324 for (int col = pixelCol; col < pixelEnd; ++col)
326 // BH 2019.07.27 was:
328 // miniMe.setRGB(col, row, rgbcolor);
330 // but just directly writing to the int[] pixel buffer
331 // is three times faster by my experimentation
332 pixels[row * w + col] = rgb;
337 // store last update value
340 lastUpdate = sendProgressUpdate(
341 pixelEnd * (endRow - 1 - pixelRow), totalPixels,
342 lastRowUpdate, lastUpdate);
347 if (pixelRow < endRow)
350 // store row offset and last update value
353 // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
354 // pixelRow + 1, surely
355 lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
357 lastUpdate = lastRowUpdate;
363 * Precalculate the columns that will be used for each pixel in a dense
364 * overview. So we have to skip through the bscol BitSet to pick up one
365 * (representative?) column for each pixel.
367 * Note that there is no easy solution if we want to do color averaging, but
368 * this method might be adapted to do that. Or it could be adapted to pick the
369 * "most representative color" for a group of columns.
371 * @author Bob Hanson 2019.09.03
372 * @return a -1 terminated int[]
374 private int[] calcColumnsToShow()
376 int[] a = new int[w + 1];
378 float offset = bscol.nextSetBit(0);
381 return new int[] { -1 };
384 a[pixel++] = (int) offset;
385 // for example, say we have 10 pixels per column:
386 // ...............xxxxxxxx....xxxxxx.........xxxxxx......
387 // nextSet(i).....^...........^..............^...........
388 // nextClear..............^.........^..............^.....
389 // run lengths....|--n1--|....|-n2-|.........|-n3-|......
390 // 10 pixel/col...|---pixel1---||-----pixel2------|......
391 // pixel..........^0............^1.......................
392 for (int i, iClear = -1; pixel < w
393 && (i = bscol.nextSetBit(iClear + 1)) >= 0;)
395 // find the next clear bit
396 iClear = bscol.nextClearBit(i + 1);
397 // add the run length n1, n2, n3 to grow the column buffer
398 colBuffer += iClear - i; // n1, n2, etc.
399 // add columns if we have accumulated enough pixels
401 while (colBuffer > colsPerPixel && pixel < w)
403 colBuffer -= colsPerPixel;
404 offset += colsPerPixel;
405 a[pixel++] = i + (int) offset;
407 // set back column pointer relative to the next run
416 * The next column is either a precalculated pixel (when there are multiple
417 * pixels per column) or the next set bit for the column that aligns with the
418 * next pixel (when there are more columns than pixels).
420 * When columns are hidden, this value is precalculated; otherwise it is
425 * pixel pointer into columnsToShow
428 private int getNextCol(int icol, int pixel)
430 return (skippingColumns ? columnsToShow[pixel]
431 : bscol.nextSetBit(icol + 1));
435 * Derive the next pixel from either as the given pixel (when we are skipping
436 * columns because this is a dense overview and the pixel known), or from the
437 * current column based on pixels/column. The latter is used for drawing the
438 * hidden-column mask or for overviews that have more pixels across than
445 private int getNextPixel(int icol, int pixel)
447 return Math.min(skippingColumns && pixel > 0 ? pixel
448 : Math.round(icol * pixelsPerCol), w);
451 private boolean loop()
459 timer = new Timer(delay, new ActionListener()
462 public void actionPerformed(ActionEvent e)
467 timer.setRepeats(false);
481 // Platform.timeCheck(
482 // "overviewrender " + ndone + " pixels row:" + row + " redraw:"
484 // Platform.TIME_MARK);
487 overlayHiddenRegions();
490 // final update to progress bar if present
494 // BH was pixelRow - 1, but that could go negative
495 sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
499 // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
500 sendProgressUpdate(1, 1, 0, 0);
504 panel.overviewDone(miniMe);
508 * Calculate progress update value and fire event
509 * @param rowOffset number of rows to offset calculation by
510 * @return new rowOffset - return value only to be used when at end of a row
512 private int sendProgressUpdate(int position, int maximum, int rowOffset,
515 int newUpdate = rowOffset
516 + Math.round(MAX_PROGRESS * ((float) position / maximum));
517 if (newUpdate > lastUpdate)
519 changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
526 * Find the RGB value of the colour of a sequence at a specified column position
529 * sequence to get colour for
531 * column position to get colour for
532 * @return colour of sequence at this position, as RGB
534 int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
537 return (seq == null || icol >= seq.getLength()
538 ? resColFinder.gapColourInt
539 : resColFinder.getResidueColour(true, shader, allGroups, seq,
540 icol, finder).getRGB());
544 * Overlay the hidden regions on the overview image
547 private void overlayHiddenRegions()
549 if (cols.hasHidden() || rows.hasHidden())
551 BufferedImage mask = buildHiddenImage();
553 Graphics2D g = (Graphics2D) miniMe.getGraphics();
554 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
556 g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
562 * Build a masking image of hidden columns and rows to be applied on top of
563 * the main overview image.
566 * collection of rows the overview is built over
568 * collection of columns the overview is built over
570 * width of overview in pixels
572 * height of overview in pixels
573 * @return BufferedImage containing mask of hidden regions
575 private BufferedImage buildHiddenImage()
578 BufferedImage hiddenImage = new BufferedImage(w, h,
579 BufferedImage.TYPE_INT_ARGB);
581 Color hidden = resColFinder.getHiddenColour();
583 Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
585 g2d.setColor(hidden);
586 // set background to transparent
587 // g2d.setComposite(AlphaComposite.Clear);
588 // g2d.fillRect(0, 0, width, height);
590 // set next colour to opaque
591 g2d.setComposite(AlphaComposite.Src);
593 // System.out.println(cols.getClass().getName());
594 if (cols.hasHidden())
596 // AllColsCollection only
597 BitSet bs = cols.getHiddenBitSet();
598 for (int pixelCol = -1, icol2 = 0, icol = bs
599 .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2))
605 icol2 = bs.nextClearBit(icol + 1);
606 int pixelEnd = getNextPixel(icol2, 0);
607 if (pixelEnd > pixelCol)
609 pixelCol = getNextPixel(icol, 0);
610 g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
616 if (rows.hasHidden())
620 for (int alignmentRow : rows)
627 // calculate where this row extends to in pixels
628 int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
631 // get details of this alignment row
632 if (rows.isHidden(alignmentRow))
634 g2d.fillRect(0, pixelRow, w, endRow - pixelRow);
644 * Draw the alignment annotation in the overview panel
647 * alignment annotation information
649 public void drawGraph(AlignmentAnnotation anno)
652 Graphics g = miniMe.getGraphics();
653 g.translate(0, alignmentHeight);
655 Annotation[] annotations = anno.annotations;
656 float max = anno.graphMax;
657 g.setColor(Color.white);
658 g.fillRect(0, 0, w, y);
660 for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
661 .nextSetBit(0); icol >= 0
662 && icol < len; icol = getNextCol(icol, colNext))
668 changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
674 pixelEnd = getNextPixel(colNext, colNext);
675 Annotation ann = annotations[icol];
678 Color color = ann.colour;
679 g.setColor(color == null ? Color.black : color);
680 int height = Math.min(y, (int) ((ann.value / max) * y));
681 g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
687 g.translate(0, -alignmentHeight);
692 changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
699 * Allows redraw flag to be set
702 * value to set redraw to: true = redraw is occurring, false = no
705 public void setRedraw(boolean b)
713 public void addPropertyChangeListener(RendererListenerI listener)
715 changeSupport.addPropertyChangeListener(listener);
718 public void removePropertyChangeListener(RendererListenerI listener)
720 changeSupport.removePropertyChangeListener(listener);