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