82e89e5d3bc8e751e8ebe932ecede33c82238f7f
[jalview.git] / src / jalview / renderer / OverviewRenderer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
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.
11  *  
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.
16  * 
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.
20  */
21 package jalview.renderer;
22
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;
35
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;
48
49 import javax.swing.Timer;
50
51 public class OverviewRenderer
52 {
53   public static final String UPDATE = "OverviewUpdate";
54
55   // transparency of hidden cols/seqs overlay
56   private static final float TRANSPARENCY = 0.5f;
57
58   private static final int MAX_PROGRESS = 100;
59
60   private static final int STATE_INIT = 0;
61
62   private static final int STATE_NEXT = 1;
63
64   private static final int STATE_DONE = 2;
65
66   private int state;
67
68   private Timer timer;
69
70   private int delay = (Platform.isJS() ? 1 : 0);
71
72   private int seqIndex;
73
74   private int pixelRow;
75
76   private Integer row;
77
78   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
79           this);
80
81   private FeatureColourFinder finder;
82
83   // image to render on
84   private BufferedImage miniMe;
85
86   /**
87    * Number of pixelsPerCol;
88    */
89   private float pixelsPerCol;
90
91   /**
92    * Number of visible columns per pixel.
93    * 
94    */
95   private float colsPerPixel;
96
97   /**
98    * raw number of pixels to allocate to each row
99    */
100   private float pixelsPerSeq;
101
102   /**
103    * true when colsPerPixel > 1
104    */
105   private boolean skippingColumns;
106
107   /**
108    * pre-calculated list of columns needed for a "dense" overview, where there
109    * are more columns than pixels
110    */
111
112   private int[] columnsToShow;
113
114   // height in pixels of graph
115   private int graphHeight;
116
117   // flag to indicate whether to halt drawing
118   private volatile boolean redraw = false;
119
120   // reference to alignment, needed to get sequence groups
121   private AlignmentI al;
122
123   private ResidueShaderI shader;
124
125   private OverviewResColourFinder resColFinder;
126
127   private boolean showProgress;
128
129   private AlignmentViewPanel panel;
130
131   private int ndone = 0;
132
133   private AlignmentRowsCollectionI rows;
134
135   private AlignmentColsCollectionI cols;
136
137   private Iterator<Integer> rowIterator;
138
139   private int alignmentHeight;
140
141   private int totalPixels;
142
143   private int lastRowUpdate;
144
145   private int lastUpdate;
146
147   private int[] pixels;
148
149   private BitSet bscol;
150
151   private final int w;
152
153   private final int h;
154
155   public OverviewRenderer(AlignmentViewPanel panel,
156           jalview.api.FeatureRenderer fr, OverviewDimensions od,
157           AlignmentI alignment, ResidueShaderI resshader,
158           OverviewResColourFinder colFinder)
159   {
160     this(panel, fr, od, alignment, resshader, colFinder, true);
161   }
162
163   /**
164    * @param panel
165    * @param fr
166    * @param od
167    * @param alignment
168    * @param resshader
169    * @param colFinder
170    * @param showProgress
171    *          possibly not, in JavaScript and for testng
172    */
173   public OverviewRenderer(AlignmentViewPanel panel,
174           jalview.api.FeatureRenderer fr, OverviewDimensions od,
175           AlignmentI alignment, ResidueShaderI resshader,
176           OverviewResColourFinder colFinder, boolean showProgress)
177   {
178     this.panel = panel;
179     finder = new FeatureColourFinder(fr);
180     al = alignment;
181     shader = resshader;
182     resColFinder = colFinder;
183     this.showProgress = showProgress;
184
185     w = od.getWidth();
186     h = od.getHeight();
187     rows = od.getRows(alignment);
188     cols = od.getColumns(alignment);
189     graphHeight = od.getGraphHeight();
190     alignmentHeight = od.getSequencesHeight();
191
192     pixelsPerSeq = od.getPixelsPerSeq();
193     pixelsPerCol = od.getPixelsPerCol();
194     colsPerPixel = Math.max(1, 1f / pixelsPerCol);
195
196     skippingColumns = (pixelsPerCol < 1);
197   }
198
199   /**
200    * Draw alignment rows and columns onto an image. This method is asynchronous
201    * in JavaScript and interruptible in Java.
202    * 
203    * Whether hidden rows or columns are drawn depends upon the type of
204    * collection.
205    * 
206    * Updated to skip through high-density sequences, where columns/pixels > 1.
207    * 
208    * When the process is complete, the image is passed to the AlignmentViewPanel
209    * provided by the constructor.
210    * 
211    * @param rows
212    *          collection of rows to be drawn
213    * @param cols
214    *          collection of columns to be drawn
215    * @return image containing the drawing
216    * 
217    * @author Bob Hanson 2019.07.30
218    */
219   public void drawMiniMe()
220   {
221     state = STATE_INIT;
222     mainLoop();
223   }
224
225   protected void mainLoop()
226   {
227     out: while (!redraw)
228     {
229       switch (state)
230       {
231       case STATE_INIT:
232         init();
233         state = STATE_NEXT;
234         continue;
235       case STATE_NEXT:
236         if (!rowIterator.hasNext())
237         {
238           state = STATE_DONE;
239           continue;
240         }
241         nextRow();
242         if (!loop())
243         {
244           // Java
245           continue;
246         }
247         // JavaScript
248         return;
249       case STATE_DONE:
250         break out;
251       }
252       // Java will continue without a timeout
253     }
254     done();
255   }
256
257   private void init()
258   {
259     rowIterator = rows.iterator();
260     seqIndex = 0;
261     pixelRow = 0;
262     lastRowUpdate = 0;
263     lastUpdate = 0;
264     totalPixels = w * alignmentHeight;
265     if (showProgress)
266     {
267       changeSupport.firePropertyChange(UPDATE, -1, 0);
268     }
269
270     miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
271     WritableRaster raster = miniMe.getRaster();
272     DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
273     pixels = db.getBankData()[0];
274     bscol = cols.getShownBitSet();
275     if (skippingColumns)
276     {
277       columnsToShow = calcColumnsToShow();
278     }
279
280     Platform.timeCheck(null, Platform.TIME_MARK);
281   }
282
283   private void nextRow()
284   {
285     row = rowIterator.next();
286     SequenceI seq = rows.getSequence(row);
287
288     // rate limiting step when rendering overview for lots of groups
289     SequenceGroup[] allGroups = al.findAllGroups(seq);
290
291     // calculate where this row extends to in pixels
292     int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
293     // this is the key modification -- we use bscol to jump to the next column
294     // when there are more columns than pixels.
295
296     for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
297             .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
298     {
299       // asynchronous exit flag
300       if (redraw)
301       {
302         break;
303       }
304
305       ++colNext;
306       pixelEnd = getNextPixel(colNext, colNext);
307
308       if (pixelCol == pixelEnd)
309       {
310         break;
311       }
312       else if (pixelCol < pixelEnd)
313       {
314         int rgb = getColumnColourFromSequence(allGroups, seq, icol);
315         // fill in the appropriate number of pixels
316         for (int row = pixelRow; row < endRow; ++row)
317         {
318           for (int col = pixelCol; col < pixelEnd; ++col)
319           {
320             // BH 2019.07.27 was:
321             //
322             // miniMe.setRGB(col, row, rgbcolor);
323             //
324             // but just directly writing to the int[] pixel buffer
325             // is three times faster by my experimentation
326             pixels[row * w + col] = rgb;
327             ndone++;
328           }
329         }
330         pixelCol = pixelEnd;
331         // store last update value
332         if (showProgress)
333         {
334           lastUpdate = sendProgressUpdate(
335                   pixelEnd * (endRow - 1 - pixelRow), totalPixels,
336                   lastRowUpdate, lastUpdate);
337         }
338       }
339
340     }
341     if (pixelRow < endRow)
342     {
343       pixelRow = endRow;
344       // store row offset and last update value
345       if (showProgress)
346       {
347         // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
348         // pixelRow + 1, surely
349         lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
350                 lastUpdate);
351         lastUpdate = lastRowUpdate;
352       }
353     }
354   }
355
356   /**
357    * Precalculate the columns that will be used for each pixel in a dense
358    * overview. So we have to skip through the bscol BitSet to pick up one
359    * (representative?) column for each pixel.
360    * 
361    * Note that there is no easy solution if we want to do color averaging, but
362    * this method might be adapted to do that. Or it could be adapted to pick the
363    * "most representative color" for a group of columns.
364    * 
365    * @author Bob Hanson 2019.09.03
366    * @return a -1 terminated int[]
367    */
368   private int[] calcColumnsToShow()
369   {
370     int[] a = new int[w + 1];
371     float colBuffer = 0;
372     float offset = bscol.nextSetBit(0);
373     if (offset < 0)
374     {
375       return new int[] { -1 };
376     }
377     int pixel = 0;
378     a[pixel++] = (int) offset;
379     // for example, say we have 10 pixels per column:
380     // ...............xxxxxxxx....xxxxxx.........xxxxxx......
381     // nextSet(i).....^...........^..............^...........
382     // nextClear..............^.........^..............^.....
383     // run lengths....|--n1--|....|-n2-|.........|-n3-|......
384     // 10 pixel/col...|---pixel1---||-----pixel2------|......
385     // pixel..........^0............^1.......................
386     for (int i, iClear = -1; pixel < w
387             && (i = bscol.nextSetBit(iClear + 1)) >= 0;)
388     {
389       // find the next clear bit
390       iClear = bscol.nextClearBit(i + 1);
391       // add the run length n1, n2, n3 to grow the column buffer
392       colBuffer += iClear - i; // n1, n2, etc.
393       // add columns if we have accumulated enough pixels
394
395       while (colBuffer > colsPerPixel && pixel < w)
396       {
397         colBuffer -= colsPerPixel;
398         offset += colsPerPixel;
399         a[pixel++] = i + (int) offset;
400       }
401       // set back column pointer relative to the next run
402       offset = -colBuffer;
403     }
404     // add a terminator
405     a[pixel] = -1;
406     return a;
407   }
408
409   /**
410    * The next column is either a precalculated pixel (when there are multiple
411    * pixels per column) or the next set bit for the column that aligns with the
412    * next pixel (when there are more columns than pixels).
413    * 
414    * When columns are hidden, this value is precalculated; otherwise it is
415    * calculated here.
416    * 
417    * @param icol
418    * @param pixel
419    *          pixel pointer into columnsToShow
420    * @return
421    */
422   private int getNextCol(int icol, int pixel)
423   {
424     return (skippingColumns ? columnsToShow[pixel]
425             : bscol.nextSetBit(icol + 1));
426   }
427
428   /**
429    * Derive the next pixel from either as the given pixel (when we are skipping
430    * columns because this is a dense overview and the pixel known), or from the
431    * current column based on pixels/column. The latter is used for drawing the
432    * hidden-column mask or for overviews that have more pixels across than
433    * columns.
434    * 
435    * @param icol
436    * @param pixel
437    * @return
438    */
439   private int getNextPixel(int icol, int pixel)
440   {
441     return Math.min(skippingColumns && pixel > 0 ? pixel
442             : Math.round(icol * pixelsPerCol), w);
443   }
444
445   private boolean loop()
446   {
447     if (delay <= 0)
448     {
449       return false;
450     }
451     if (timer == null)
452     {
453       timer = new Timer(delay, new ActionListener()
454       {
455         @Override
456         public void actionPerformed(ActionEvent e)
457         {
458           mainLoop();
459         }
460       });
461       timer.setRepeats(false);
462       timer.start();
463     }
464     else
465     {
466       timer.restart();
467     }
468     return true;
469   }
470
471   private void done()
472   {
473     if (!redraw)
474     {
475       Platform.timeCheck(
476               "overviewrender " + ndone + " pixels row:" + row + " redraw:"
477                       + redraw,
478               Platform.TIME_MARK);
479     }
480
481     overlayHiddenRegions();
482     if (showProgress)
483     {
484       // final update to progress bar if present
485       if (redraw)
486       {
487         // aborted in Java
488         // BH was pixelRow - 1, but that could go negative
489         sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
490       }
491       else
492       {
493         // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
494         sendProgressUpdate(1, 1, 0, 0);
495       }
496     }
497
498     panel.overviewDone(miniMe);
499   }
500
501   /*
502    * Calculate progress update value and fire event
503    * @param rowOffset number of rows to offset calculation by
504    * @return new rowOffset - return value only to be used when at end of a row
505    */
506   private int sendProgressUpdate(int position, int maximum, int rowOffset,
507           int lastUpdate)
508   {
509     int newUpdate = rowOffset
510             + Math.round(MAX_PROGRESS * ((float) position / maximum));
511     if (newUpdate > lastUpdate)
512     {
513       changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
514       return newUpdate;
515     }
516     return newUpdate;
517   }
518
519   /*
520    * Find the RGB value of the colour of a sequence at a specified column position
521    * 
522    * @param seq
523    *          sequence to get colour for
524    * @param lastcol
525    *          column position to get colour for
526    * @return colour of sequence at this position, as RGB
527    */
528   int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
529           int icol)
530   {
531     return (seq == null || icol >= seq.getLength()
532             ? resColFinder.gapColourInt
533             : resColFinder.getResidueColour(true, shader, allGroups, seq,
534                     icol, finder).getRGB());
535   }
536
537   /**
538    * Overlay the hidden regions on the overview image
539    * 
540    */
541   private void overlayHiddenRegions()
542   {
543     if (cols.hasHidden() || rows.hasHidden())
544     {
545       BufferedImage mask = buildHiddenImage();
546
547       Graphics2D g = (Graphics2D) miniMe.getGraphics();
548       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
549               TRANSPARENCY));
550       g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
551       g.dispose();
552     }
553   }
554
555   /**
556    * Build a masking image of hidden columns and rows to be applied on top of
557    * the main overview image.
558    * 
559    * @param rows
560    *          collection of rows the overview is built over
561    * @param cols
562    *          collection of columns the overview is built over
563    * @param width
564    *          width of overview in pixels
565    * @param height
566    *          height of overview in pixels
567    * @return BufferedImage containing mask of hidden regions
568    */
569   private BufferedImage buildHiddenImage()
570   {
571     // new masking image
572     BufferedImage hiddenImage = new BufferedImage(w, h,
573             BufferedImage.TYPE_INT_ARGB);
574
575     Color hidden = resColFinder.getHiddenColour();
576
577     Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
578
579     g2d.setColor(hidden);
580     // set background to transparent
581     // g2d.setComposite(AlphaComposite.Clear);
582     // g2d.fillRect(0, 0, width, height);
583
584     // set next colour to opaque
585     g2d.setComposite(AlphaComposite.Src);
586
587     // System.out.println(cols.getClass().getName());
588     if (cols.hasHidden())
589     {
590       // AllColsCollection only
591       BitSet bs = cols.getHiddenBitSet();
592       for (int pixelCol = -1, icol2 = 0, icol = bs
593               .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2))
594       {
595         if (redraw)
596         {
597           break;
598         }
599         icol2 = bs.nextClearBit(icol + 1);
600         int pixelEnd = getNextPixel(icol2, 0);
601         if (pixelEnd > pixelCol)
602         {
603           pixelCol = getNextPixel(icol, 0);
604           g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
605                   h);
606           pixelCol = pixelEnd;
607         }
608       }
609     }
610     if (rows.hasHidden())
611     {
612       int seqIndex = 0;
613       int pixelRow = 0;
614       for (int alignmentRow : rows)
615       {
616         if (redraw)
617         {
618           break;
619         }
620
621         // calculate where this row extends to in pixels
622         int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
623                 h);
624
625         // get details of this alignment row
626         if (rows.isHidden(alignmentRow))
627         {
628           // BH 2019.09.24 fixes JAL-3440 Java+JavaScript off by one row in
629           // height
630           g2d.fillRect(0, pixelRow, w, endRow - pixelRow);
631         }
632         pixelRow = endRow;
633       }
634     }
635     g2d.dispose();
636     return hiddenImage;
637   }
638
639   /**
640    * Draw the alignment annotation in the overview panel
641    * 
642    * @param anno
643    *          alignment annotation information
644    */
645   public void drawGraph(AlignmentAnnotation anno)
646   {
647     int y = graphHeight;
648     Graphics g = miniMe.getGraphics();
649     g.translate(0, alignmentHeight);
650
651     Annotation[] annotations = anno.annotations;
652     float max = anno.graphMax;
653     g.setColor(Color.white);
654     g.fillRect(0, 0, w, y);
655
656     for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
657             .nextSetBit(0); icol >= 0
658                     && icol < len; icol = getNextCol(icol, colNext))
659     {
660       if (redraw)
661       {
662         if (showProgress)
663         {
664           changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
665         }
666         break;
667       }
668
669       ++colNext;
670       pixelEnd = getNextPixel(colNext, colNext);
671       Annotation ann = annotations[icol];
672       if (ann != null)
673       {
674         Color color = ann.colour;
675         g.setColor(color == null ? Color.black : color);
676         int height = Math.min(y, (int) ((ann.value / max) * y));
677         g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
678                 height);
679       }
680       pixelCol = pixelEnd;
681     }
682
683     g.translate(0, -alignmentHeight);
684     g.dispose();
685
686     if (showProgress)
687     {
688       changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
689               MAX_PROGRESS);
690     }
691
692   }
693
694   /**
695    * Allows redraw flag to be set
696    * 
697    * @param b
698    *          value to set redraw to: true = redraw is occurring, false = no
699    *          redraw
700    */
701   public void setRedraw(boolean b)
702   {
703     synchronized (this)
704     {
705       redraw = b;
706     }
707   }
708
709   public void addPropertyChangeListener(RendererListenerI listener)
710   {
711     changeSupport.addPropertyChangeListener(listener);
712   }
713
714   public void removePropertyChangeListener(RendererListenerI listener)
715   {
716     changeSupport.removePropertyChangeListener(listener);
717   }
718 }