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