JAL-3429 JAL-3253-applet
[jalview.git] / src / jalview / renderer / OverviewRenderer.java
index 45cc944..c3350a5 100644 (file)
@@ -30,7 +30,6 @@ 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;
 
@@ -58,6 +57,26 @@ public class OverviewRenderer
 
   private static final int MAX_PROGRESS = 100;
 
+  final static int STATE_INIT = 0;
+
+  final static int STATE_NEXT = 1;
+
+  final static int STATE_DONE = 2;
+
+  private int state;
+
+  private Timer timer;
+
+  private boolean isJS = Platform.isJS();
+
+  private int delay = (isJS ? 1 : 0);
+
+  private int seqIndex;
+
+  private int pixelRow;
+
+  private Integer row;
+
   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
           this);
 
@@ -66,12 +85,34 @@ public class OverviewRenderer
   // image to render on
   private BufferedImage miniMe;
 
-  // raw number of pixels to allocate to each column
+  /**
+   * Number of pixelsPerCol;
+   */
   private float pixelsPerCol;
 
-  // raw number of pixels to allocate to each row
+  /**
+   * Number of visible columns per pixel.
+   * 
+   */
+  private float colsPerPixel;
+
+  /**
+   * raw number of pixels to allocate to each row
+   */
   private float pixelsPerSeq;
 
+  /**
+   * true when colsPerPixel > 1
+   */
+  private boolean skippingColumns;
+
+  /**
+   * pre-calculated list of columns needed for a "dense" overview, where there
+   * are more columns than pixels
+   */
+
+  private int[] columnsToShow;
+
   // height in pixels of graph
   private int graphHeight;
 
@@ -89,113 +130,142 @@ public class OverviewRenderer
 
   private AlignmentViewPanel panel;
 
-  public OverviewRenderer(AlignmentViewPanel panel, FeatureRenderer fr,
-          OverviewDimensions od,
-          AlignmentI alignment,
-          ResidueShaderI resshader, OverviewResColourFinder colFinder)
+  // private int sequencesHeight;
+
+  public OverviewRenderer(AlignmentViewPanel panel,
+          jalview.api.FeatureRenderer fr, OverviewDimensions od,
+          AlignmentI alignment, ResidueShaderI resshader,
+          OverviewResColourFinder colFinder)
   {
     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,
+          jalview.api.FeatureRenderer fr, OverviewDimensions od,
           AlignmentI alignment, ResidueShaderI resshader,
           OverviewResColourFinder colFinder, boolean showProgress)
   {
-    this.panel = panel;
-    finder = new FeatureColourFinder(fr);
-    resColFinder = colFinder;
-
-    al = alignment;
-    shader = resshader;
-
-    pixelsPerCol = od.getPixelsPerCol();
-    pixelsPerSeq = od.getPixelsPerSeq();
-    graphHeight = od.getGraphHeight();
-    miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
-            BufferedImage.TYPE_INT_RGB);
-    this.showProgress = showProgress;
-  }
-
-  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;
+    {
+      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();
+
+      pixelsPerSeq = od.getPixelsPerSeq();
+      pixelsPerCol = od.getPixelsPerCol();
+      colsPerPixel = Math.max(1, 1f / pixelsPerCol);
+
+      skippingColumns = (pixelsPerCol < 1);
 
-  int pixelRow;
+    }
+  }
 
-  private Integer row;
+  /**
+   * 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.
+   * 
+   * 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 void drawMiniMe()
+  {
+    state = STATE_INIT;
+    mainLoop();
+  }
 
-  void mainLoop()
+  protected void mainLoop()
   {
-    while (!redraw)
+    out: while (!redraw)
     {
       switch (state)
       {
       case STATE_INIT:
-        seqIndex = 0;
-        pixelRow = 0;
+        init();
         state = STATE_NEXT;
         continue;
       case STATE_NEXT:
-        if (iter.hasNext())
+        if (!rowIterator.hasNext())
         {
-          nextRow();
+          state = STATE_DONE;
+          continue;
         }
-        else
+        nextRow();
+        if (!loop())
         {
-          state = STATE_DONE;
+          // Java
+          continue;
         }
-        break;
-      case STATE_DONE:
-        done();
-        return;
-      }
-      if (delay > 0)
-      {
-        jsloop();
+        // JavaScript
         return;
+      case STATE_DONE:
+        break out;
       }
+      // Java will continue without a timeout
     }
     done();
   }
 
-  private void jsloop()
+  private void init()
   {
-    if (timer == null)
+    rowIterator = rows.iterator();
+    seqIndex = 0;
+    pixelRow = 0;
+    lastRowUpdate = 0;
+    lastUpdate = 0;
+    totalPixels = w * alignmentHeight;
+    if (showProgress)
     {
-      timer = new Timer(delay, new ActionListener()
-      {
-        @Override
-        public void actionPerformed(ActionEvent e)
-        {
-          mainLoop();
-        }
-
-      });
-      timer.setRepeats(false);
-      timer.start();
+      changeSupport.firePropertyChange(UPDATE, -1, 0);
     }
-    else
+
+    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();
+    if (skippingColumns)
     {
-      timer.restart();
+      columnsToShow = calcColumnsToShow();
     }
+
+    Platform.timeCheck(null, Platform.TIME_MARK);
   }
 
   private void nextRow()
   {
-    row = iter.next();
-    // System.out.println("OR row " + r);
-    // get details of this alignment row
+    row = rowIterator.next();
     SequenceI seq = rows.getSequence(row);
 
     // rate limiting step when rendering overview for lots of groups
@@ -203,30 +273,32 @@ public class OverviewRenderer
 
     // calculate where this row extends to in pixels
     int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
+    // this is the key modification -- we use bscol to jump to the next column
+    // when there are more columns than pixels.
 
-    for (int pixelCol = 0, colIndex = 0, c = bscol
-            .nextSetBit(0); c >= 0; c = bscol.nextSetBit(c + 1))
+    for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
+            .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
     {
+      // asynchronous exit flag
       if (redraw)
       {
         break;
       }
 
-      // calculate where this column extends to in pixels
-      int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol), w);
+      ++colNext;
+      pixelEnd = getNextPixel(colNext, colNext);
 
-      // 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)
+      if (pixelCol == pixelEnd)
       {
-        // System.out.println("OR pc ec " + pixelCol + " " + endCol);
-        int rgb = getColumnColourFromSequence(allGroups, seq, c);
+        break;
+      }
+      else if (pixelCol < pixelEnd)
+      {
+        int rgb = getColumnColourFromSequence(allGroups, seq, icol);
         // fill in the appropriate number of pixels
         for (int row = pixelRow; row < endRow; ++row)
         {
-          for (int col = pixelCol; col < endCol; ++col)
+          for (int col = pixelCol; col < pixelEnd; ++col)
           {
             // BH 2019.07.27 was:
             //
@@ -238,16 +310,16 @@ public class OverviewRenderer
             ndone++;
           }
         }
-        // }
-
-        pixelCol = endCol;
+        pixelCol = pixelEnd;
         // store last update value
         if (showProgress)
         {
-          lastUpdate = sendProgressUpdate(endCol * (endRow - 1 - pixelRow),
-                  totalPixels, lastRowUpdate, lastUpdate);
+          lastUpdate = sendProgressUpdate(
+                  pixelEnd * (endRow - 1 - pixelRow), totalPixels,
+                  lastRowUpdate, lastUpdate);
         }
       }
+
     }
     if (pixelRow < endRow)
     {
@@ -264,14 +336,135 @@ public class OverviewRenderer
     }
   }
 
+  /**
+   * Precalculate the columns that will be used for each pixel in a dense
+   * overview. So we have to skip through the bscol BitSet to pick up one
+   * (representative?) column for each pixel.
+   * 
+   * Note that there is no easy solution if we want to do color averaging, but
+   * this method might be adapted to do that. Or it could be adapted to pick the
+   * "most representative color" for a group of columns.
+   * 
+   * @author Bob Hanson 2019.09.03
+   * @return a -1 terminated int[]
+   */
+  private int[] calcColumnsToShow()
+  {
+    int[] a = new int[w + 1];
+    float colBuffer = 0;
+    float offset = bscol.nextSetBit(0);
+    if (offset < 0)
+    {
+      return new int[] { -1 };
+    }
+    int pixel = 0;
+    a[pixel++] = (int) offset;
+    // for example, say we have 10 pixels per column:
+    // ...............xxxxxxxx....xxxxxx.........xxxxxx......
+    // nextSet(i).....^...........^..............^...........
+    // nextClear..............^.........^..............^.....
+    // run lengths....|--n1--|....|-n2-|.........|-n3-|......
+    // 10 pixel/col...|---pixel1---||-----pixel2------|......
+    // pixel..........^0............^1.......................
+    for (int i, iClear = -1; pixel < w
+            && (i = bscol.nextSetBit(iClear + 1)) >= 0;)
+    {
+      // find the next clear bit
+      iClear = bscol.nextClearBit(i + 1);
+      // add the run length n1, n2, n3 to grow the column buffer
+      colBuffer += iClear - i; // n1, n2, etc.
+      // add columns if we have accumulated enough pixels
+
+      while (colBuffer > colsPerPixel && pixel < w)
+      {
+        colBuffer -= colsPerPixel;
+        offset += colsPerPixel;
+        a[pixel++] = i + (int) offset;
+      }
+      // set back column pointer relative to the next run
+      offset = -colBuffer;
+    }
+    // add a terminator
+    a[pixel] = -1;
+    return a;
+  }
+
+  /**
+   * The next column is either a precalculated pixel (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).
+   * 
+   * When columns are hidden, this value is precalculated; otherwise it is
+   * calculated here.
+   * 
+   * @param icol
+   * @param pixel
+   *          pixel pointer into columnsToShow
+   * @return
+   */
+  private int getNextCol(int icol, int pixel)
+  {
+    return (skippingColumns ? columnsToShow[pixel]
+            : bscol.nextSetBit(icol + 1));
+  }
+
+  /**
+   * Derive the next pixel from either as the given pixel (when we are skipping
+   * columns because this is a dense overview and the pixel known), or from the
+   * current column based on pixels/column. The latter is used for drawing the
+   * hidden-column mask or for overviews that have more pixels across than
+   * columns.
+   * 
+   * @param icol
+   * @param pixel
+   * @return
+   */
+  private int getNextPixel(int icol, int pixel)
+  {
+    return Math.min(skippingColumns && pixel > 0 ? pixel
+            : Math.round(icol * pixelsPerCol), w);
+  }
+
+  private ActionListener listener = new ActionListener()
+  {
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      mainLoop();
+    }
+
+  };
+
+  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()
   {
-    Platform.timeCheck(
-            "overviewrender " + ndone + " pixels row:" + row + " redraw:"
-                    + redraw,
-            Platform.TIME_MARK);
+    if (!redraw)
+    {
+      Platform.timeCheck(
+              "overviewrender " + ndone + " pixels row:" + row + " redraw:"
+                      + redraw,
+              Platform.TIME_MARK);
+    }
 
-    overlayHiddenRegions(rows, cols);
+    overlayHiddenRegions();
     if (showProgress)
     {
       // final update to progress bar if present
@@ -296,7 +489,7 @@ public class OverviewRenderer
 
   private AlignmentColsCollectionI cols;
 
-  Iterator<Integer> iter;
+  Iterator<Integer> rowIterator;
 
   int alignmentHeight;
 
@@ -308,53 +501,10 @@ public class OverviewRenderer
 
   int[] pixels;
 
-  BitSet bscol = new BitSet();
+  BitSet bscol;
 
   int w, h;
 
-  /**
-   * Draw alignment rows and columns onto an image
-   * 
-   * @param rit
-   *          Iterator over rows to be drawn
-   * @param cit
-   *          Iterator over columns to be drawn
-   * @return image containing the drawing
-   */
-  public BufferedImage draw(AlignmentRowsCollectionI rows,
-          AlignmentColsCollectionI cols)
-  {
-    this.rows = rows;
-    this.cols = cols;
-    iter = rows.iterator();
-
-    w = miniMe.getWidth();
-    h = miniMe.getHeight();
-    alignmentHeight = h - graphHeight;
-    totalPixels = w * alignmentHeight;
-    lastRowUpdate = 0;
-    lastUpdate = 0;
-
-    if (showProgress)
-    {
-      changeSupport.firePropertyChange(UPDATE, -1, 0);
-    }
-
-    WritableRaster raster = miniMe.getRaster();
-    DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
-    Platform.timeCheck(null, Platform.TIME_MARK);
-    pixels = db.getBankData()[0];
-    bscol.clear();
-    for (int c : cols)
-    {
-      bscol.set(c);
-    }
-    state = STATE_INIT;
-    mainLoop();
-
-    return miniMe;
-  }
-
   /*
    * Calculate progress update value and fire event
    * @param rowOffset number of rows to offset calculation by
@@ -387,25 +537,19 @@ public class OverviewRenderer
   {
     return (seq == null || icol >= seq.getLength()
             ? resColFinder.GAP_COLOUR
-            : resColFinder.getResidueColour(true, shader, allGroups, seq,
-                    icol, finder)).getRGB();
+            : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
+                    icol, finder));
   }
 
   /**
    * Overlay the hidden regions on the overview image
    * 
-   * @param rows
-   *          collection of rows the overview is built over
-   * @param cols
-   *          collection of columns the overview is built over
    */
-  private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
-          AlignmentColsCollectionI cols)
+  private void overlayHiddenRegions()
   {
     if (cols.hasHidden() || rows.hasHidden())
     {
-      BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
-              miniMe.getHeight());
+      BufferedImage mask = buildHiddenImage();
 
       Graphics2D g = (Graphics2D) miniMe.getGraphics();
       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
@@ -429,17 +573,17 @@ public class OverviewRenderer
    *          height of overview in pixels
    * @return BufferedImage containing mask of hidden regions
    */
-  private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
-          AlignmentColsCollectionI cols, int width, int height)
+  private BufferedImage buildHiddenImage()
   {
     // new masking image
-    BufferedImage hiddenImage = new BufferedImage(width, height,
+    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);
@@ -447,31 +591,26 @@ public class OverviewRenderer
     // set next colour to opaque
     g2d.setComposite(AlphaComposite.Src);
 
+    // System.out.println(cols.getClass().getName());
     if (cols.hasHidden())
     {
-      int colIndex = 0;
-      int pixelCol = 0;
-      for (int alignmentCol : cols)
+      // 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;
         }
-
-        // calculate where this column extends to in pixels
-        int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol),
-                width);
-
-        // endCol is one more than old endCol
-        if (pixelCol < endCol)
+        icol2 = bs.nextClearBit(icol + 1);
+        int pixelEnd = getNextPixel(icol2, 0);
+        if (pixelEnd > pixelCol)
         {
-          // determine the colour based on the sequence and column position
-          if (cols.isHidden(alignmentCol))
-          {
-            g2d.setColor(hidden);
-            g2d.fillRect(pixelCol, 0, endCol - pixelCol, height);
-          }
-          pixelCol = endCol;
+          pixelCol = getNextPixel(icol, 0);
+          g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
+                  h);
+          pixelCol = pixelEnd;
         }
       }
     }
@@ -488,13 +627,12 @@ public class OverviewRenderer
 
         // calculate where this row extends to in pixels
         int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
-                height);
+                h);
 
         // get details of this alignment row
         if (rows.isHidden(alignmentRow))
         {
-          g2d.setColor(hidden);
-          g2d.fillRect(0, pixelRow, width, endRow - 1 - pixelRow);
+          g2d.fillRect(0, pixelRow, w, endRow - 1 - pixelRow);
         }
         pixelRow = endRow;
       }
@@ -506,27 +644,23 @@ public class OverviewRenderer
   /**
    * Draw the alignment annotation in the overview panel
    * 
-   * @param g
-   *          the graphics object to draw on
    * @param anno
    *          alignment annotation information
-   * @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 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);
-    int width = miniMe.getWidth();
-    g.fillRect(0, 0, width, y);
+    g.fillRect(0, 0, w, y);
 
-    int colIndex = 0;
-    int pixelCol = 0;
-    for (int icol : cols)
+    for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
+            .nextSetBit(0); icol >= 0
+                    && icol < len; icol = getNextCol(icol, colNext))
     {
       if (redraw)
       {
@@ -537,27 +671,29 @@ public class OverviewRenderer
         break;
       }
 
-      if (icol >= annotations.length)
-      {
-        break; // no more annotations to draw here
-      }
-      int endCol = Math.min(Math.round((++colIndex) * pixelsPerCol), width);
+      ++colNext;
+      pixelEnd = getNextPixel(colNext, colNext);
       Annotation ann = annotations[icol];
       if (ann != null)
       {
         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, endCol - pixelCol, height);
+        g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
+                height);
       }
-      pixelCol = endCol;
+      pixelCol = pixelEnd;
     }
+
+    g.translate(0, -alignmentHeight);
+    g.dispose();
+
     if (showProgress)
     {
       changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
               MAX_PROGRESS);
     }
+
   }
 
   /**