JAL-3383 minor code tidying and commenting
[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 static 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 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 sequencesHeight;
132
133   public OverviewRenderer(AlignmentViewPanel panel,
134           jalview.api.FeatureRenderer fr, OverviewDimensions od,
135           AlignmentI alignment, ResidueShaderI resshader,
136           OverviewResColourFinder colFinder)
137   {
138     this(panel, fr, od, alignment, resshader, colFinder, true);
139   }
140
141   /**
142    * @param panel
143    * @param fr
144    * @param od
145    * @param alignment
146    * @param resshader
147    * @param colFinder
148    * @param shwoProgress
149    *          possibly not, in JavaScript and for testng
150    */
151   public OverviewRenderer(AlignmentViewPanel panel,
152           jalview.api.FeatureRenderer fr, OverviewDimensions od,
153           AlignmentI alignment, ResidueShaderI resshader,
154           OverviewResColourFinder colFinder, boolean showProgress)
155   {
156     {
157       this.panel = panel;
158       finder = new FeatureColourFinder(fr);
159       al = alignment;
160       shader = resshader;
161       resColFinder = colFinder;
162       this.showProgress = showProgress;
163
164       w = od.getWidth();
165       h = od.getHeight();
166       rows = od.getRows(alignment);
167       cols = od.getColumns(alignment);
168       graphHeight = od.getGraphHeight();
169       alignmentHeight = od.getSequencesHeight();
170
171       pixelsPerSeq = od.getPixelsPerSeq();
172       pixelsPerCol = od.getPixelsPerCol();
173       colsPerPixel = Math.max(1, 1f / pixelsPerCol);
174
175       skippingColumns = (pixelsPerCol < 1);
176
177     }
178   }
179
180   /**
181    * Draw alignment rows and columns onto an image. This method is asynchronous
182    * in JavaScript and interruptible in Java.
183    * 
184    * Whether hidden rows or columns are drawn depends upon the type of
185    * collection.
186    * 
187    * Updated to skip through high-density sequences, where columns/pixels > 1.
188    * 
189    * When the process is complete, the image is passed to the AlignmentViewPanel
190    * provided by the constructor.
191    * 
192    * @param rows
193    *          collection of rows to be drawn
194    * @param cols
195    *          collection of columns to be drawn
196    * @return image containing the drawing
197    * 
198    * @author Bob Hanson 2019.07.30
199    */
200   public void drawMiniMe()
201   {
202     state = STATE_INIT;
203     mainLoop();
204   }
205
206   protected void mainLoop()
207   {
208     out: while (!redraw)
209     {
210       switch (state)
211       {
212       case STATE_INIT:
213         init();
214         state = STATE_NEXT;
215         continue;
216       case STATE_NEXT:
217         if (!rowIterator.hasNext())
218         {
219           state = STATE_DONE;
220           continue;
221         }
222         nextRow();
223         if (!loop())
224         {
225           // Java
226           continue;
227         }
228         // JavaScript
229         return;
230       case STATE_DONE:
231         break out;
232       }
233       // Java will continue without a timeout
234     }
235     done();
236   }
237
238   private void init()
239   {
240     rowIterator = rows.iterator();
241     seqIndex = 0;
242     pixelRow = 0;
243     lastRowUpdate = 0;
244     lastUpdate = 0;
245     totalPixels = w * alignmentHeight;
246     if (showProgress)
247     {
248       changeSupport.firePropertyChange(UPDATE, -1, 0);
249     }
250
251     miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
252     WritableRaster raster = miniMe.getRaster();
253     DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
254     pixels = db.getBankData()[0];
255     bscol = cols.getShownBitSet();
256     if (skippingColumns)
257     {
258       columnsToShow = calcColumnsToShow();
259     }
260
261     Platform.timeCheck(null, Platform.TIME_MARK);
262   }
263
264   private void nextRow()
265   {
266     row = rowIterator.next();
267     SequenceI seq = rows.getSequence(row);
268
269     // rate limiting step when rendering overview for lots of groups
270     SequenceGroup[] allGroups = al.findAllGroups(seq);
271
272     // calculate where this row extends to in pixels
273     int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
274     // this is the key modification -- we use bscol to jump to the next column
275     // when there are more columns than pixels.
276
277     for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
278             .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
279     {
280       // asynchronous exit flag
281       if (redraw)
282       {
283         break;
284       }
285
286       ++colNext;
287       pixelEnd = getNextPixel(colNext, colNext);
288
289       if (pixelCol == pixelEnd)
290       {
291         break;
292       }
293       else if (pixelCol < pixelEnd)
294       {
295         int rgb = getColumnColourFromSequence(allGroups, seq, icol);
296         // fill in the appropriate number of pixels
297         for (int row = pixelRow; row < endRow; ++row)
298         {
299           for (int col = pixelCol; col < pixelEnd; ++col)
300           {
301             // BH 2019.07.27 was:
302             //
303             // miniMe.setRGB(col, row, rgbcolor);
304             //
305             // but just directly writing to the int[] pixel buffer
306             // is three times faster by my experimentation
307             pixels[row * w + col] = rgb;
308             ndone++;
309           }
310         }
311         pixelCol = pixelEnd;
312         // store last update value
313         if (showProgress)
314         {
315           lastUpdate = sendProgressUpdate(
316                   pixelEnd * (endRow - 1 - pixelRow), totalPixels,
317                   lastRowUpdate, lastUpdate);
318         }
319       }
320
321     }
322     if (pixelRow < endRow)
323     {
324       pixelRow = endRow;
325       // store row offset and last update value
326       if (showProgress)
327       {
328         // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
329         // pixelRow + 1, surely
330         lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
331                 lastUpdate);
332         lastUpdate = lastRowUpdate;
333       }
334     }
335   }
336
337   /**
338    * Precalculate the columns that will be used for each pixel in a dense
339    * overview. So we have to skip through the bscol BitSet to pick up one
340    * (representative?) column for each pixel.
341    * 
342    * Note that there is no easy solution if we want to do color averaging, but
343    * this method might be adapted to do that. Or it could be adapted to pick the
344    * "most representative color" for a group of columns.
345    * 
346    * @author Bob Hanson 2019.09.03
347    * @return a -1 terminated int[]
348    */
349   private int[] calcColumnsToShow()
350   {
351     int[] a = new int[w + 1];
352     float colBuffer = 0;
353     float offset = bscol.nextSetBit(0);
354     if (offset < 0)
355     {
356       return new int[] { -1 };
357     }
358     int pixel = 0;
359     a[pixel++] = (int) offset;
360     // for example, say we have 10 pixels per column:
361     // ...............xxxxxxxx....xxxxxx.........xxxxxx......
362     // nextSet(i).....^...........^..............^...........
363     // nextClear..............^.........^..............^.....
364     // run lengths....|--n1--|....|-n2-|.........|-n3-|......
365     // 10 pixel/col...|---pixel1---||-----pixel2------|......
366     // pixel..........^0............^1.......................
367     for (int i, iClear = -1; pixel < w
368             && (i = bscol.nextSetBit(iClear + 1)) >= 0;)
369     {
370       // find the next clear bit
371       iClear = bscol.nextClearBit(i + 1);
372       // add the run length n1, n2, n3 to grow the column buffer
373       colBuffer += iClear - i; // n1, n2, etc.
374       // add columns if we have accumulated enough pixels
375
376       while (colBuffer > colsPerPixel && pixel < w)
377       {
378         colBuffer -= colsPerPixel;
379         offset += colsPerPixel;
380         a[pixel++] = i + (int) offset;
381       }
382       // set back column pointer relative to the next run
383       offset = -colBuffer;
384     }
385     // add a terminator
386     a[pixel] = -1;
387     return a;
388   }
389
390   /**
391    * The next column is either a precalculated pixel (when there are multiple
392    * pixels per column) or the next set bit for the column that aligns with the
393    * next pixel (when there are more columns than pixels).
394    * 
395    * When columns are hidden, this value is precalculated; otherwise it is
396    * calculated here.
397    * 
398    * @param icol
399    * @param pixel
400    *          pixel pointer into columnsToShow
401    * @return
402    */
403   private int getNextCol(int icol, int pixel)
404   {
405     return (skippingColumns ? columnsToShow[pixel]
406             : bscol.nextSetBit(icol + 1));
407   }
408
409   /**
410    * Derive the next pixel from either as the given pixel (when we are skipping
411    * columns because this is a dense overview and the pixel known), or from the
412    * current column based on pixels/column. The latter is used for drawing the
413    * hidden-column mask or for overviews that have more pixels across than
414    * columns.
415    * 
416    * @param icol
417    * @param pixel
418    * @return
419    */
420   private int getNextPixel(int icol, int pixel)
421   {
422     return Math.min(skippingColumns && pixel > 0 ? pixel
423             : Math.round(icol * pixelsPerCol), w);
424   }
425
426   private ActionListener listener = new ActionListener()
427   {
428     @Override
429     public void actionPerformed(ActionEvent e)
430     {
431       mainLoop();
432     }
433
434   };
435
436   private boolean loop()
437   {
438     if (delay <= 0)
439     {
440       return false;
441     }
442     if (timer == null)
443     {
444       timer = new Timer(delay, listener);
445       timer.setRepeats(false);
446       timer.start();
447     }
448     else
449     {
450       timer.restart();
451     }
452     return true;
453   }
454
455   private void done()
456   {
457     if (!redraw)
458     {
459       Platform.timeCheck(
460               "overviewrender " + ndone + " pixels row:" + row + " redraw:"
461                       + redraw,
462               Platform.TIME_MARK);
463     }
464
465     overlayHiddenRegions();
466     if (showProgress)
467     {
468       // final update to progress bar if present
469       if (redraw)
470       {
471         // aborted in Java
472         // BH was pixelRow - 1, but that could go negative
473         sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
474       }
475       else
476       {
477         // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
478         sendProgressUpdate(1, 1, 0, 0);
479       }
480     }
481
482     panel.overviewDone(miniMe);
483   }
484
485   int ndone = 0;
486
487   private AlignmentRowsCollectionI rows;
488
489   private AlignmentColsCollectionI cols;
490
491   Iterator<Integer> rowIterator;
492
493   int alignmentHeight;
494
495   int totalPixels;
496
497   int lastRowUpdate;
498
499   int lastUpdate;
500
501   int[] pixels;
502
503   BitSet bscol;
504
505   int w, h;
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.getResidueColourInt(true, shader, allGroups, seq,
540              icol, finder));
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 - 1 - 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 }