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