22c75c3b7437068e12b621ef17d52e26579630eb
[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           // Java
198           continue;
199         }
200         // JavaScript
201         return;
202       case STATE_DONE:
203         break out;
204       }
205       // Java will continue without a timeout
206     }
207     done();
208   }
209
210   private void init()
211   {
212     rowIterator = rows.iterator();
213     seqIndex = 0;
214     pixelRow = 0;
215     lastRowUpdate = 0;
216     lastUpdate = 0;
217     totalPixels = w * alignmentHeight;
218
219     if (showProgress)
220     {
221       changeSupport.firePropertyChange(UPDATE, -1, 0);
222     }
223
224     miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
225     WritableRaster raster = miniMe.getRaster();
226     DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
227     Platform.timeCheck(null, Platform.TIME_MARK);
228     pixels = db.getBankData()[0];
229     bscol = cols.getOverviewBitSet();
230   }
231
232   private void nextRow()
233   {
234     row = rowIterator.next();
235     // get details of this alignment row
236     SequenceI seq = rows.getSequence(row);
237
238     // rate limiting step when rendering overview for lots of groups
239     SequenceGroup[] allGroups = al.findAllGroups(seq);
240
241     // calculate where this row extends to in pixels
242     int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
243     for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
244             .nextSetBit(0); icol >= 0; icol = getNextCol(icol, pixelEnd))
245     {
246       if (redraw)
247       {
248         break;
249       }
250
251       ++colNext;
252       pixelEnd = getNextPixel(colNext, colNext);
253
254       if (pixelCol == pixelEnd)
255       {
256         break;
257       }
258       else if (pixelCol < pixelEnd)
259       {
260         int rgb = getColumnColourFromSequence(allGroups, seq, icol);
261         // fill in the appropriate number of pixels
262         // System.out.println(
263         // "OR colNext=" + colNext + " " + pixelCol
264         // + "-" + pixelEnd + " icol=" + icol + " " + rgb + " "
265         // + pixelsPerCol);
266         for (int row = pixelRow; row < endRow; ++row)
267         {
268           for (int col = pixelCol; col < pixelEnd; ++col)
269           {
270             // BH 2019.07.27 was:
271             //
272             // miniMe.setRGB(col, row, rgbcolor);
273             //
274             // but just directly writing to the int[] pixel buffer
275             // is three times faster by my experimentation
276             pixels[row * w + col] = rgb;
277             ndone++;
278           }
279         }
280         pixelCol = pixelEnd;
281         // store last update value
282         if (showProgress)
283         {
284           lastUpdate = sendProgressUpdate(
285                   pixelEnd * (endRow - 1 - pixelRow),
286                   totalPixels, lastRowUpdate, lastUpdate);
287         }
288       }
289
290     }
291     if (pixelRow < endRow)
292     {
293       pixelRow = endRow;
294       // store row offset and last update value
295       if (showProgress)
296       {
297         // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
298         // pixelRow + 1, surely
299         lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
300                 lastUpdate);
301         lastUpdate = lastRowUpdate;
302       }
303     }
304   }
305
306   /**
307    * The next column is either the next set bit (when there are multiple pixels
308    * per column) or the next set bit for the column that aligns with the next
309    * pixel (when there are more columns than pixels).
310    * 
311    * @param icol
312    * @param pixel
313    * @return
314    */
315   private int getNextCol(int icol, int pixel)
316   {
317     return bscol.nextSetBit(
318             pixelsPerCol >= 1 ? icol + 1 : (int) (pixel * colsPerPixel));
319   }
320
321   private int getNextPixel(int icol, int pixel)
322   {
323     return Math.min(
324             pixelsPerCol >= 1 || pixel == 0
325                     ? Math.round(icol * pixelsPerCol)
326                     : pixel,
327             w);
328   }
329
330   private boolean loop()
331   {
332     if (delay <= 0)
333     {
334       return false;
335     }
336     if (timer == null)
337     {
338       timer = new Timer(delay, new ActionListener()
339       {
340         @Override
341         public void actionPerformed(ActionEvent e)
342         {
343           mainLoop();
344         }
345
346       });
347       timer.setRepeats(false);
348       timer.start();
349     }
350     else
351     {
352       timer.restart();
353     }
354     return true;
355   }
356
357   private void done()
358   {
359     Platform.timeCheck(
360             "overviewrender " + ndone + " pixels row:" + row + " redraw:"
361                     + redraw,
362             Platform.TIME_MARK);
363
364     overlayHiddenRegions();
365     if (showProgress)
366     {
367       // final update to progress bar if present
368       if (redraw)
369       {
370         // aborted in Java
371         // BH was pixelRow - 1, but that could go negative
372         sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
373       }
374       else
375       {
376         // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
377         sendProgressUpdate(1, 1, 0, 0);
378       }
379     }
380     panel.overviewDone(miniMe);
381   }
382
383   int ndone = 0;
384
385   private AlignmentRowsCollectionI rows;
386
387   private AlignmentColsCollectionI cols;
388
389   Iterator<Integer> rowIterator;
390
391   int alignmentHeight;
392
393   int totalPixels;
394
395   int lastRowUpdate;
396
397   int lastUpdate;
398
399   int[] pixels;
400
401   BitSet bscol;
402
403   int w, h;
404
405   /*
406    * Calculate progress update value and fire event
407    * @param rowOffset number of rows to offset calculation by
408    * @return new rowOffset - return value only to be used when at end of a row
409    */
410   private int sendProgressUpdate(int position, int maximum, int rowOffset,
411           int lastUpdate)
412   {
413     int newUpdate = rowOffset
414             + Math.round(MAX_PROGRESS * ((float) position / maximum));
415     if (newUpdate > lastUpdate)
416     {
417       changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
418       return newUpdate;
419     }
420     return newUpdate;
421   }
422
423   /*
424    * Find the RGB value of the colour of a sequence at a specified column position
425    * 
426    * @param seq
427    *          sequence to get colour for
428    * @param lastcol
429    *          column position to get colour for
430    * @return colour of sequence at this position, as RGB
431    */
432   int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
433           int icol)
434   {
435     return (seq == null || icol >= seq.getLength()
436             ? resColFinder.GAP_COLOUR
437             : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
438                     icol, finder));
439   }
440
441   /**
442    * Overlay the hidden regions on the overview image
443    * 
444    */
445   private void overlayHiddenRegions()
446   {
447     if (cols.hasHidden() || rows.hasHidden())
448     {
449       BufferedImage mask = buildHiddenImage();
450
451       Graphics2D g = (Graphics2D) miniMe.getGraphics();
452       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
453               TRANSPARENCY));
454       g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
455       g.dispose();
456     }
457   }
458
459   /**
460    * Build a masking image of hidden columns and rows to be applied on top of
461    * the main overview image.
462    * 
463    * @param rows
464    *          collection of rows the overview is built over
465    * @param cols
466    *          collection of columns the overview is built over
467    * @param width
468    *          width of overview in pixels
469    * @param height
470    *          height of overview in pixels
471    * @return BufferedImage containing mask of hidden regions
472    */
473   private BufferedImage buildHiddenImage()
474   {
475     // new masking image
476     BufferedImage hiddenImage = new BufferedImage(w, h,
477             BufferedImage.TYPE_INT_ARGB);
478
479     Color hidden = resColFinder.getHiddenColour();
480
481     Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
482
483     g2d.setColor(hidden);
484     // set background to transparent
485     // g2d.setComposite(AlphaComposite.Clear);
486     // g2d.fillRect(0, 0, width, height);
487
488     // set next colour to opaque
489     g2d.setComposite(AlphaComposite.Src);
490
491     // System.out.println(cols.getClass().getName());
492     if (cols.hasHidden())
493     {
494       // AllColsCollection only
495       BitSet bs = cols.getHiddenBitSet();
496       for (int pixelCol = -1, icol2 = 0, icol = bs
497               .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2))
498       {
499         if (redraw)
500         {
501           break;
502         }
503         icol2 = bs.nextClearBit(icol + 1);
504         int pixelEnd = getNextPixel(icol2, 0);
505         if (pixelEnd > pixelCol)
506         {
507           pixelCol = getNextPixel(icol, 0);
508           g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
509                   h);
510           pixelCol = pixelEnd;
511         }
512       }
513     }
514     if (rows.hasHidden())
515     {
516       int seqIndex = 0;
517       int pixelRow = 0;
518       for (int alignmentRow : rows)
519       {
520         if (redraw)
521         {
522           break;
523         }
524
525         // calculate where this row extends to in pixels
526         int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
527                 h);
528
529         // get details of this alignment row
530         if (rows.isHidden(alignmentRow))
531         {
532           g2d.fillRect(0, pixelRow, w, endRow - 1 - pixelRow);
533         }
534         pixelRow = endRow;
535       }
536     }
537     g2d.dispose();
538     return hiddenImage;
539   }
540
541   /**
542    * Draw the alignment annotation in the overview panel
543    * 
544    * @param anno
545    *          alignment annotation information
546    */
547   public void drawGraph(AlignmentAnnotation anno)
548   {
549     int y = graphHeight;
550     Graphics g = miniMe.getGraphics();
551     g.translate(0, alignmentHeight);
552
553     Annotation[] annotations = anno.annotations;
554     float max = anno.graphMax;
555     g.setColor(Color.white);
556     g.fillRect(0, 0, w, y);
557
558     for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
559             .nextSetBit(0); icol >= 0
560                     && icol < len; icol = getNextCol(icol, pixelEnd))
561     {
562       if (redraw)
563       {
564         if (showProgress)
565         {
566           changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
567         }
568         break;
569       }
570
571       ++colNext;
572       pixelEnd = getNextPixel(colNext, colNext);
573       Annotation ann = annotations[icol];
574       if (ann != null)
575       {
576         Color color = ann.colour;
577         g.setColor(color == null ? Color.black : color);
578         int height = Math.min(y, (int) ((ann.value / max) * y));
579         g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
580                 height);
581       }
582       pixelCol = pixelEnd;
583     }
584
585     g.translate(0, -alignmentHeight);
586     g.dispose();
587
588     if (showProgress)
589     {
590       changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
591               MAX_PROGRESS);
592     }
593
594   }
595
596   /**
597    * Allows redraw flag to be set
598    * 
599    * @param b
600    *          value to set redraw to: true = redraw is occurring, false = no
601    *          redraw
602    */
603   public void setRedraw(boolean b)
604   {
605     synchronized (this)
606     {
607       redraw = b;
608     }
609   }
610
611   public void addPropertyChangeListener(RendererListenerI listener)
612   {
613     changeSupport.addPropertyChangeListener(listener);
614   }
615
616   public void removePropertyChangeListener(RendererListenerI listener)
617   {
618     changeSupport.removePropertyChangeListener(listener);
619   }
620 }