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