JAL-3383 JAL-3253-applet additional efficiencies; FeatureStore
[jalview.git] / src / jalview / renderer / OverviewRenderer.java
index 46490cd..36b5847 100644 (file)
@@ -22,207 +22,599 @@ package jalview.renderer;
 
 import jalview.api.AlignmentColsCollectionI;
 import jalview.api.AlignmentRowsCollectionI;
+import jalview.api.AlignmentViewPanel;
+import jalview.api.RendererListenerI;
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
-import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.util.Platform;
 import jalview.viewmodel.OverviewDimensions;
 
+import java.awt.AlphaComposite;
 import java.awt.Color;
 import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.beans.PropertyChangeSupport;
+import java.util.BitSet;
+import java.util.Iterator;
+
+import javax.swing.Timer;
 
 public class OverviewRenderer
 {
-  private FeatureColourFinder finder;
+  // transparency of hidden cols/seqs overlay
+  private final float TRANSPARENCY = 0.5f;
+
+  public static final String UPDATE = "OverviewUpdate";
+
+  private static final int MAX_PROGRESS = 100;
 
-  private jalview.api.SequenceRenderer sr;
+  private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
+          this);
+
+  private FeatureColourFinder finder;
 
   // image to render on
   private BufferedImage miniMe;
 
-  // raw number of pixels to allocate to each column
+  /**
+   * Number of pixelsPerCol;
+   */
   private float pixelsPerCol;
 
+  /**
+   * Number of visible columns per pixel.
+   * 
+   */
+  private float colsPerPixel;
+
   // raw number of pixels to allocate to each row
   private float pixelsPerSeq;
 
+  // height in pixels of graph
+  private int graphHeight;
+
   // flag to indicate whether to halt drawing
   private volatile boolean redraw = false;
 
-  public OverviewRenderer(jalview.api.SequenceRenderer seqRenderer,
-          FeatureRenderer fr, OverviewDimensions od)
+  // reference to alignment, needed to get sequence groups
+  private AlignmentI al;
+
+  private ResidueShaderI shader;
+
+  private OverviewResColourFinder resColFinder;
+
+  private boolean showProgress;
+
+  private AlignmentViewPanel panel;
+
+  // private int sequencesHeight;
+
+  public OverviewRenderer(AlignmentViewPanel panel,
+          jalview.api.FeatureRenderer fr, OverviewDimensions od,
+          AlignmentI alignment, ResidueShaderI resshader,
+          OverviewResColourFinder colFinder)
   {
-    sr = seqRenderer;
+    this(panel, fr, od, alignment, resshader, colFinder, true);
+  }
+
+  /**
+   * @param panel
+   * @param fr
+   * @param od
+   * @param alignment
+   * @param resshader
+   * @param colFinder
+   * @param shwoProgress
+   *          possibly not, in JavaScript and for testng
+   */
+  public OverviewRenderer(AlignmentViewPanel panel,
+          jalview.api.FeatureRenderer fr, OverviewDimensions od,
+          AlignmentI alignment, ResidueShaderI resshader,
+          OverviewResColourFinder colFinder, boolean showProgress)
+  {
+    {
+    this.panel = panel;
     finder = new FeatureColourFinder(fr);
+    al = alignment;
+    shader = resshader;
+    resColFinder = colFinder;
+    this.showProgress = showProgress;
+
+    w = od.getWidth();
+    h = od.getHeight();
+    rows = od.getRows(alignment);
+    cols = od.getColumns(alignment);
+    graphHeight = od.getGraphHeight();
+    alignmentHeight = od.getSequencesHeight();
 
-    pixelsPerCol = od.getPixelsPerCol();
     pixelsPerSeq = od.getPixelsPerSeq();
-    miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
-            BufferedImage.TYPE_INT_RGB);
+    pixelsPerCol = od.getPixelsPerCol();
+    colsPerPixel = Math.max(1, 1f / pixelsPerCol);
+    }
   }
 
+  final static int STATE_INIT = 0;
+  final static int STATE_NEXT = 1;
+  final static int STATE_DONE = 2;
+
+  int state;
+
+  boolean isJS = Platform.isJS();
+
+  Timer timer;
+  int delay = (isJS ? 1 : 0);
+
+  int seqIndex;
+
+  int pixelRow;
+
+  private Integer row;
+
   /**
-   * Draw alignment rows and columns onto an image
+   * Draw alignment rows and columns onto an image. This method is asynchronous
+   * in JavaScript and interruptible in Java.
+   * 
+   * Whether hidden rows or columns are drawn depends upon the type of
+   * collection.
+   * 
+   * Updated to skip through high-density sequences, where columns/pixels > 1.
    * 
-   * @param rit
-   *          Iterator over rows to be drawn
-   * @param cit
-   *          Iterator over columns to be drawn
+   * When the process is complete, the image is passed to the AlignmentViewPanel
+   * provided by the constructor.
+   * 
+   * @param rows
+   *          collection of rows to be drawn
+   * @param cols
+   *          collection of columns to be drawn
    * @return image containing the drawing
+   * 
+   * @author Bob Hanson 2019.07.30
    */
-  public BufferedImage draw(AlignmentRowsCollectionI rows,
-          AlignmentColsCollectionI cols)
+  public void drawMiniMe()
+  {
+    state = STATE_INIT;
+    mainLoop();
+  }
+
+  protected void mainLoop()
+  {
+    out: while (!redraw)
+    {
+      switch (state)
+      {
+      case STATE_INIT:
+        init();
+        state = STATE_NEXT;
+        continue;
+      case STATE_NEXT:
+        if (!rowIterator.hasNext())
+        {
+          state = STATE_DONE;
+          continue;
+        }
+        nextRow();
+        if (!loop())
+        {
+          // Java
+          continue;
+        }
+        // JavaScript
+        return;
+      case STATE_DONE:
+        break out;
+      }
+      // Java will continue without a timeout
+    }
+    done();
+  }
+
+  private void init()
   {
-    int rgbcolor = Color.white.getRGB();
-    int seqIndex = 0;
-    int pixelRow = 0;
+    rowIterator = rows.iterator();
+    seqIndex = 0;
+    pixelRow = 0;
+    lastRowUpdate = 0;
+    lastUpdate = 0;
+    totalPixels = w * alignmentHeight;
+
+    if (showProgress)
+    {
+      changeSupport.firePropertyChange(UPDATE, -1, 0);
+    }
+
+    miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
+    WritableRaster raster = miniMe.getRaster();
+    DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
+    pixels = db.getBankData()[0];
+    bscol = cols.getOverviewBitSet();
+    Platform.timeCheck(null, Platform.TIME_MARK);
+  }
+
+  private void nextRow()
+  {
+    row = rowIterator.next();
+    // get details of this alignment row
+    SequenceI seq = rows.getSequence(row);
+
+    // rate limiting step when rendering overview for lots of groups
+    SequenceGroup[] allGroups = al.findAllGroups(seq);
 
-    for (int alignmentRow : rows)
+    // calculate where this row extends to in pixels
+    int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
+    for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
+            .nextSetBit(0); icol >= 0; icol = getNextCol(icol, pixelEnd))
     {
       if (redraw)
       {
         break;
       }
 
-      // get details of this alignment row
-      boolean hidden = rows.isHidden(alignmentRow);
-      SequenceI seq = rows.getSequence(alignmentRow);
+      ++colNext;
+      pixelEnd = getNextPixel(colNext, colNext);
 
-      // calculate where this row extends to in pixels
-      int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
-              miniMe.getHeight() - 1);
-
-      int colIndex = 0;
-      int pixelCol = 0;
-      for (int alignmentCol : cols)
+      if (pixelCol == pixelEnd)
       {
-        if (redraw)
+        break;
+      }
+      else if (pixelCol < pixelEnd)
+      {
+        int rgb = getColumnColourFromSequence(allGroups, seq, icol);
+        // fill in the appropriate number of pixels
+        // System.out.println(
+        // "OR colNext=" + colNext + " " + pixelCol
+        // + "-" + pixelEnd + " icol=" + icol + " " + rgb + " "
+        // + pixelsPerCol);
+        for (int row = pixelRow; row < endRow; ++row)
         {
-          break;
+          for (int col = pixelCol; col < pixelEnd; ++col)
+          {
+            // BH 2019.07.27 was:
+            //
+            // miniMe.setRGB(col, row, rgbcolor);
+            //
+            // but just directly writing to the int[] pixel buffer
+            // is three times faster by my experimentation
+            pixels[row * w + col] = rgb;
+            ndone++;
+          }
+        }
+        pixelCol = pixelEnd;
+        // store last update value
+        if (showProgress)
+        {
+          lastUpdate = sendProgressUpdate(
+                  pixelEnd * (endRow - 1 - pixelRow),
+                  totalPixels, lastRowUpdate, lastUpdate);
         }
+      }
+
+    }
+    if (pixelRow < endRow)
+    {
+      pixelRow = endRow;
+      // store row offset and last update value
+      if (showProgress)
+      {
+        // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
+        // pixelRow + 1, surely
+        lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
+                lastUpdate);
+        lastUpdate = lastRowUpdate;
+      }
+    }
+  }
 
-        // calculate where this column extends to in pixels
-        int endCol = Math.min(
-                Math.round((colIndex + 1) * pixelsPerCol) - 1,
-                miniMe.getWidth() - 1);
+  /**
+   * The next column is either the next set bit (when there are multiple pixels
+   * per column) or the next set bit for the column that aligns with the next
+   * pixel (when there are more columns than pixels).
+   * 
+   * @param icol
+   * @param pixel
+   * @return
+   */
+  private int getNextCol(int icol, int pixel)
+  {
+    return bscol.nextSetBit(
+            pixelsPerCol >= 1 ? icol + 1 : (int) (pixel * colsPerPixel));
+  }
 
-        // don't do expensive colour determination if we're not going to use it
-        // NB this is important to avoid performance issues in the overview
-        // panel
-        if (pixelCol <= endCol)
-        {
-          // determine the colour based on the sequence and column position
-          rgbcolor = getColumnColourFromSequence(seq,
-                  hidden || cols.isHidden(alignmentCol), alignmentCol,
-                  finder);
+  private int getNextPixel(int icol, int pixel)
+  {
+    return Math.min(
+            pixelsPerCol >= 1 || pixel == 0
+                    ? Math.round(icol * pixelsPerCol)
+                    : pixel,
+            w);
+  }
 
-          // fill in the appropriate number of pixels
-          for (int row = pixelRow; row <= endRow; ++row)
-          {
-            for (int col = pixelCol; col <= endCol; ++col)
-            {
-              miniMe.setRGB(col, row, rgbcolor);
-            }
-          }
+  private ActionListener listener = new ActionListener()
+  {
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      mainLoop();
+    }
 
-          pixelCol = endCol + 1;
-        }
-        colIndex++;
+  };
+
+  private boolean loop()
+  {
+    if (delay <= 0)
+    {
+      return false;
+    }
+    if (timer == null)
+    {
+      timer = new Timer(delay, listener);
+      timer.setRepeats(false);
+      timer.start();
+    }
+    else
+    {
+      timer.restart();
+    }
+    return true;
+  }
+
+  private void done()
+  {
+    if (!redraw)
+    {
+      Platform.timeCheck(
+              "overviewrender " + ndone + " pixels row:" + row + " redraw:"
+                      + redraw,
+              Platform.TIME_MARK);
+    }
+
+    overlayHiddenRegions();
+    if (showProgress)
+    {
+      // final update to progress bar if present
+      if (redraw)
+      {
+        // aborted in Java
+        // BH was pixelRow - 1, but that could go negative
+        sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
       }
-      pixelRow = endRow + 1;
-      seqIndex++;
+      else
+      {
+        // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
+        sendProgressUpdate(1, 1, 0, 0);
+      }
+    }
+    panel.overviewDone(miniMe);
+  }
+
+  int ndone = 0;
+
+  private AlignmentRowsCollectionI rows;
+
+  private AlignmentColsCollectionI cols;
+
+  Iterator<Integer> rowIterator;
+
+  int alignmentHeight;
+
+  int totalPixels;
+
+  int lastRowUpdate;
+
+  int lastUpdate;
+
+  int[] pixels;
+
+  BitSet bscol;
+
+  int w, h;
+
+  /*
+   * Calculate progress update value and fire event
+   * @param rowOffset number of rows to offset calculation by
+   * @return new rowOffset - return value only to be used when at end of a row
+   */
+  private int sendProgressUpdate(int position, int maximum, int rowOffset,
+          int lastUpdate)
+  {
+    int newUpdate = rowOffset
+            + Math.round(MAX_PROGRESS * ((float) position / maximum));
+    if (newUpdate > lastUpdate)
+    {
+      changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
+      return newUpdate;
     }
-    return miniMe;
+    return newUpdate;
   }
 
   /*
-   * Find the colour of a sequence at a specified column position
+   * Find the RGB value of the colour of a sequence at a specified column position
+   * 
+   * @param seq
+   *          sequence to get colour for
+   * @param lastcol
+   *          column position to get colour for
+   * @return colour of sequence at this position, as RGB
    */
-  private int getColumnColourFromSequence(jalview.datamodel.SequenceI seq,
-          boolean isHidden, int lastcol, FeatureColourFinder fcfinder)
+  int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
+          int icol)
   {
-    Color color = Color.white;
+    return (seq == null || icol >= seq.getLength()
+            ? resColFinder.GAP_COLOUR
+            : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
+                    icol, finder));
+  }
 
-    if ((seq != null) && (seq.getLength() > lastcol))
+  /**
+   * Overlay the hidden regions on the overview image
+   * 
+   */
+  private void overlayHiddenRegions()
+  {
+    if (cols.hasHidden() || rows.hasHidden())
     {
-      color = sr.getResidueColour(seq, lastcol, fcfinder);
+      BufferedImage mask = buildHiddenImage();
+
+      Graphics2D g = (Graphics2D) miniMe.getGraphics();
+      g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
+              TRANSPARENCY));
+      g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
+      g.dispose();
     }
+  }
+
+  /**
+   * Build a masking image of hidden columns and rows to be applied on top of
+   * the main overview image.
+   * 
+   * @param rows
+   *          collection of rows the overview is built over
+   * @param cols
+   *          collection of columns the overview is built over
+   * @param width
+   *          width of overview in pixels
+   * @param height
+   *          height of overview in pixels
+   * @return BufferedImage containing mask of hidden regions
+   */
+  private BufferedImage buildHiddenImage()
+  {
+    // new masking image
+    BufferedImage hiddenImage = new BufferedImage(w, h,
+            BufferedImage.TYPE_INT_ARGB);
+
+    Color hidden = resColFinder.getHiddenColour();
+
+    Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
+
+    g2d.setColor(hidden);
+    // set background to transparent
+    // g2d.setComposite(AlphaComposite.Clear);
+    // g2d.fillRect(0, 0, width, height);
 
-    if (isHidden)
+    // set next colour to opaque
+    g2d.setComposite(AlphaComposite.Src);
+
+    // System.out.println(cols.getClass().getName());
+    if (cols.hasHidden())
     {
-      color = color.darker().darker();
+      // AllColsCollection only
+      BitSet bs = cols.getHiddenBitSet();
+      for (int pixelCol = -1, icol2 = 0, icol = bs
+              .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2))
+      {
+        if (redraw)
+        {
+          break;
+        }
+        icol2 = bs.nextClearBit(icol + 1);
+        int pixelEnd = getNextPixel(icol2, 0);
+        if (pixelEnd > pixelCol)
+        {
+          pixelCol = getNextPixel(icol, 0);
+          g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
+                  h);
+          pixelCol = pixelEnd;
+        }
+      }
     }
+    if (rows.hasHidden())
+    {
+      int seqIndex = 0;
+      int pixelRow = 0;
+      for (int alignmentRow : rows)
+      {
+        if (redraw)
+        {
+          break;
+        }
+
+        // calculate where this row extends to in pixels
+        int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
+                h);
 
-    return color.getRGB();
+        // get details of this alignment row
+        if (rows.isHidden(alignmentRow))
+        {
+          g2d.fillRect(0, pixelRow, w, endRow - 1 - pixelRow);
+        }
+        pixelRow = endRow;
+      }
+    }
+    g2d.dispose();
+    return hiddenImage;
   }
 
   /**
    * Draw the alignment annotation in the overview panel
    * 
-   * @param g
-   *          the graphics object to draw on
    * @param anno
    *          alignment annotation information
-   * @param charWidth
-   *          alignment character width value
-   * @param y
-   *          y-position for the annotation graph
-   * @param cols
-   *          the collection of columns used in the overview panel
    */
-  public void drawGraph(Graphics g, AlignmentAnnotation anno, int charWidth,
-          int y, AlignmentColsCollectionI cols)
+  public void drawGraph(AlignmentAnnotation anno)
   {
+    int y = graphHeight;
+    Graphics g = miniMe.getGraphics();
+    g.translate(0, alignmentHeight);
+
     Annotation[] annotations = anno.annotations;
+    float max = anno.graphMax;
     g.setColor(Color.white);
-    g.fillRect(0, 0, miniMe.getWidth(), y);
+    g.fillRect(0, 0, w, y);
 
-    int height;
-    int colIndex = 0;
-    int pixelCol = 0;
-    for (int alignmentCol : cols)
+    for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
+            .nextSetBit(0); icol >= 0
+                    && icol < len; icol = getNextCol(icol, pixelEnd))
     {
       if (redraw)
       {
+        if (showProgress)
+        {
+          changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
+        }
         break;
       }
-      if (alignmentCol >= annotations.length)
+
+      ++colNext;
+      pixelEnd = getNextPixel(colNext, colNext);
+      Annotation ann = annotations[icol];
+      if (ann != null)
       {
-        break; // no more annotations to draw here
+        Color color = ann.colour;
+        g.setColor(color == null ? Color.black : color);
+        int height = Math.min(y, (int) ((ann.value / max) * y));
+        g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
+                height);
       }
-      else
-      {
-        int endCol = Math.min(
-                Math.round((colIndex + 1) * pixelsPerCol) - 1,
-                miniMe.getWidth() - 1);
-
-        if (annotations[alignmentCol] != null)
-        {
-          if (annotations[alignmentCol].colour == null)
-          {
-            g.setColor(Color.black);
-          }
-          else
-          {
-            g.setColor(annotations[alignmentCol].colour);
-          }
+      pixelCol = pixelEnd;
+    }
 
-          height = (int) ((annotations[alignmentCol].value / anno.graphMax) * y);
-          if (height > y)
-          {
-            height = y;
-          }
+    g.translate(0, -alignmentHeight);
+    g.dispose();
 
-          g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
-        }
-        pixelCol = endCol + 1;
-        colIndex++;
-      }
+    if (showProgress)
+    {
+      changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
+              MAX_PROGRESS);
     }
+
   }
 
+  /**
+   * Allows redraw flag to be set
+   * 
+   * @param b
+   *          value to set redraw to: true = redraw is occurring, false = no
+   *          redraw
+   */
   public void setRedraw(boolean b)
   {
     synchronized (this)
@@ -230,4 +622,14 @@ public class OverviewRenderer
       redraw = b;
     }
   }
+
+  public void addPropertyChangeListener(RendererListenerI listener)
+  {
+    changeSupport.addPropertyChangeListener(listener);
+  }
+
+  public void removePropertyChangeListener(RendererListenerI listener)
+  {
+    changeSupport.removePropertyChangeListener(listener);
+  }
 }