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