JAL-1858 handle hidden columns when computing region to redraw
[jalview.git] / src / jalview / gui / SeqCanvas.java
index 760ece0..7035142 100755 (executable)
 package jalview.gui;
 
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BasicStroke;
 import java.awt.BorderLayout;
@@ -36,6 +40,7 @@ import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 import java.awt.Shape;
 import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 import javax.swing.JComponent;
@@ -46,7 +51,7 @@ import javax.swing.JComponent;
  * @author $author$
  * @version $Revision$
  */
-public class SeqCanvas extends JComponent
+public class SeqCanvas extends JComponent implements ViewportListenerI
 {
   final FeatureRenderer fr;
 
@@ -62,8 +67,6 @@ public class SeqCanvas extends JComponent
 
   AlignViewport av;
 
-  SearchResults searchResults = null;
-
   boolean fastPaint = false;
 
   int LABEL_WEST;
@@ -89,6 +92,8 @@ public class SeqCanvas extends JComponent
     setLayout(new BorderLayout());
     PaintRefresher.Register(this, av.getSequenceSetId());
     setBackground(Color.white);
+
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   public SequenceRenderer getSequenceRenderer()
@@ -166,14 +171,17 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      startx = av.getColumnSelection().adjustForHiddenColumns(startx);
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      startx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(startx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     // WEST SCALE
@@ -225,7 +233,8 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     SequenceI seq;
@@ -281,64 +290,54 @@ public class SeqCanvas extends JComponent
     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
             imgHeight, -horizontal * charWidth, -vertical * charHeight);
 
-    int sr = av.startRes;
-    int er = av.endRes;
-    int ss = av.startSeq;
-    int es = av.endSeq;
+    ViewportRanges ranges = av.getRanges();
+    int startRes = ranges.getStartRes();
+    int endRes = ranges.getEndRes();
+    int startSeq = ranges.getStartSeq();
+    int endSeq = ranges.getEndSeq();
     int transX = 0;
     int transY = 0;
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
-      er++;
-      transX = (er - sr - horizontal) * charWidth;
-      sr = er - horizontal;
+      transX = (endRes - startRes - horizontal) * charWidth;
+      startRes = endRes - horizontal;
     }
     else if (horizontal < 0)
     {
-      er = sr - horizontal - 1;
+      endRes = startRes - horizontal;
     }
     else if (vertical > 0) // scroll down
     {
-      ss = es - vertical;
+      startSeq = endSeq - vertical;
 
-      if (ss < av.startSeq)
+      if (startSeq < ranges.getStartSeq())
       { // ie scrolling too fast, more than a page at a time
-        ss = av.startSeq;
+        startSeq = ranges.getStartSeq();
       }
       else
       {
-        transY = imgHeight - (vertical * charHeight);
+        transY = imgHeight - ((vertical + 1) * charHeight);
       }
     }
     else if (vertical < 0)
     {
-      es = ss - vertical;
+      endSeq = startSeq - vertical;
 
-      if (es > av.endSeq)
+      if (endSeq > ranges.getEndSeq())
       {
-        es = av.endSeq;
+        endSeq = ranges.getEndSeq();
       }
     }
 
     gg.translate(transX, transY);
-    drawPanel(gg, sr, er, ss, es, 0);
+    drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
     gg.translate(-transX, -transY);
 
     repaint();
     fastpainting = false;
   }
 
-  /**
-   * Definitions of startx and endx (hopefully): SMJS This is what I'm working
-   * towards! startx is the first residue (starting at 0) to display. endx is
-   * the last residue to display (starting at 0). starty is the first sequence
-   * to display (starting at 0). endy is the last sequence to display (starting
-   * at 0). NOTE 1: The av limits are set in setFont in this class and in the
-   * adjustment listener in SeqPanel when the scrollbars move.
-   */
-
-  // Set this to false to force a full panel paint
   @Override
   public void paintComponent(Graphics g)
   {
@@ -397,13 +396,15 @@ public class SeqCanvas extends JComponent
     gg.setColor(Color.white);
     gg.fillRect(0, 0, imgWidth, imgHeight);
 
+    ViewportRanges ranges = av.getRanges();
     if (av.getWrapAlignment())
     {
-      drawWrappedPanel(gg, getWidth(), getHeight(), av.startRes);
+      drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
     }
     else
     {
-      drawPanel(gg, av.startRes, av.endRes, av.startSeq, av.endSeq, 0);
+      drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
+              ranges.getStartSeq(), ranges.getEndSeq(), 0);
     }
 
     g.drawImage(lcimg, 0, 0, this);
@@ -505,7 +506,7 @@ public class SeqCanvas extends JComponent
 
     av.setWrappedWidth(cWidth);
 
-    av.endRes = av.startRes + cWidth;
+    av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
 
     int endx;
     int ypos = hgap;
@@ -513,7 +514,8 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     while ((ypos <= canvasHeight) && (startRes < maxwidth))
@@ -551,11 +553,10 @@ public class SeqCanvas extends JComponent
       {
         g.setColor(Color.blue);
         int res;
-        for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
-                .size(); i++)
+        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+        for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
         {
-          res = av.getColumnSelection().findHiddenRegionPosition(i)
-                  - startRes;
+          res = hidden.findHiddenRegionPosition(i) - startRes;
 
           if (res < 0 || res > endx - startRes)
           {
@@ -586,7 +587,7 @@ public class SeqCanvas extends JComponent
                 (int) clip.getBounds().getHeight());
       }
 
-      drawPanel(g, startRes, endx, 0, al.getHeight(), ypos);
+      drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
 
       if (av.isShowAnnotation())
       {
@@ -627,34 +628,38 @@ public class SeqCanvas extends JComponent
   }
 
   /**
-   * DOCUMENT ME!
+   * Draws the visible region of the alignment on the graphics context. If there
+   * are hidden column markers in the visible region, then each sub-region
+   * between the markers is drawn separately, followed by the hidden column
+   * marker.
    * 
    * @param g1
-   *          DOCUMENT ME!
    * @param startRes
-   *          DOCUMENT ME!
+   *          offset of the first column in the visible region (0..)
    * @param endRes
-   *          DOCUMENT ME!
+   *          offset of the last column in the visible region (0..)
    * @param startSeq
-   *          DOCUMENT ME!
+   *          offset of the first sequence in the visible region (0..)
    * @param endSeq
-   *          DOCUMENT ME!
-   * @param offset
-   *          DOCUMENT ME!
+   *          offset of the last sequence in the visible region (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
    */
-  public void drawPanel(Graphics g1, int startRes, int endRes,
-          int startSeq, int endSeq, int offset)
+  public void drawPanel(Graphics g1, final int startRes, final int endRes,
+          final int startSeq, final int endSeq, final int yOffset)
   {
     updateViewport();
     if (!av.hasHiddenColumns())
     {
-      draw(g1, startRes, endRes, startSeq, endSeq, offset);
+      draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
     }
     else
     {
-      List<int[]> regions = av.getColumnSelection().getHiddenColumns();
+      List<int[]> regions = av.getAlignment().getHiddenColumns()
+              .getHiddenRegions();
 
       int screenY = 0;
+      final int screenYMax = endRes - startRes;
       int blockStart = startRes;
       int blockEnd = endRes;
 
@@ -669,38 +674,47 @@ public class SeqCanvas extends JComponent
           continue;
         }
 
-        blockEnd = hideStart - 1;
+        /*
+         * draw up to just before the next hidden region, or the end of
+         * the visible region, whichever comes first
+         */
+        blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
+                - screenY);
 
         g1.translate(screenY * charWidth, 0);
 
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
+        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
 
-        if (av.getShowHiddenMarkers())
+        /*
+         * draw the downline of the hidden column marker (ScalePanel draws the
+         * triangle on top) if we reached it
+         */
+        if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
         {
           g1.setColor(Color.blue);
 
           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
-                  0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
-                  (endSeq - startSeq) * charHeight + offset);
+                  0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
+                  (endSeq - startSeq + 1) * charHeight + yOffset);
         }
 
         g1.translate(-screenY * charWidth, 0);
         screenY += blockEnd - blockStart + 1;
         blockStart = hideEnd + 1;
 
-        if (screenY > (endRes - startRes))
+        if (screenY > screenYMax)
         {
           // already rendered last block
           return;
         }
       }
 
-      if (screenY <= (endRes - startRes))
+      if (screenY <= screenYMax)
       {
         // remaining visible region to render
-        blockEnd = blockStart + (endRes - startRes) - screenY;
+        blockEnd = blockStart + screenYMax - screenY;
         g1.translate(screenY * charWidth, 0);
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
+        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
 
         g1.translate(-screenY * charWidth, 0);
       }
@@ -708,8 +722,21 @@ public class SeqCanvas extends JComponent
 
   }
 
-  // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
-  // int x1, int x2, int y1, int y2, int startx, int starty,
+  /**
+   * Draws a region of the visible alignment
+   * 
+   * @param g1
+   * @param startRes
+   *          offset of the first column in the visible region (0..)
+   * @param endRes
+   *          offset of the last column in the visible region (0..)
+   * @param startSeq
+   *          offset of the first sequence in the visible region (0..)
+   * @param endSeq
+   *          offset of the last sequence in the visible region (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
+   */
   private void draw(Graphics g, int startRes, int endRes, int startSeq,
           int endSeq, int offset)
   {
@@ -720,7 +747,7 @@ public class SeqCanvas extends JComponent
 
     // / First draw the sequences
     // ///////////////////////////
-    for (int i = startSeq; i < endSeq; i++)
+    for (int i = startSeq; i <= endSeq; i++)
     {
       nextSeq = av.getAlignment().getSequenceAt(i);
       if (nextSeq == null)
@@ -735,15 +762,17 @@ public class SeqCanvas extends JComponent
       if (av.isShowSequenceFeatures())
       {
         fr.drawSequence(g, nextSeq, startRes, endRes, offset
-                + ((i - startSeq) * charHeight));
+                + ((i - startSeq) * charHeight), false);
       }
 
-      // / Highlight search Results once all sequences have been drawn
-      // ////////////////////////////////////////////////////////
-      if (searchResults != null)
+      /*
+       * highlight search Results once sequence has been drawn
+       */
+      if (av.hasSearchResults())
       {
-        int[] visibleResults = searchResults.getResults(nextSeq, startRes,
-                endRes);
+        SearchResultsI searchResults = av.getSearchResults();
+        int[] visibleResults = searchResults.getResults(nextSeq,
+                startRes, endRes);
         if (visibleResults != null)
         {
           for (int r = 0; r < visibleResults.length; r += 2)
@@ -804,7 +833,7 @@ public class SeqCanvas extends JComponent
         int top = -1;
         int bottom = -1;
 
-        for (i = startSeq; i < endSeq; i++)
+        for (i = startSeq; i <= endSeq; i++)
         {
           sx = (group.getStartRes() - startRes) * charWidth;
           sy = offset + ((i - startSeq) * charHeight);
@@ -960,17 +989,139 @@ public class SeqCanvas extends JComponent
   }
 
   /**
-   * DOCUMENT ME!
+   * Highlights search results in the visible region by rendering as white text
+   * on a black background. Any previous highlighting is removed.
    * 
    * @param results
-   *          DOCUMENT ME!
    */
-  public void highlightSearchResults(SearchResults results)
+  public void highlightSearchResults(SearchResultsI results)
   {
-    img = null;
+    updateViewport();
+    fastpainting = true;
+    fastPaint = true;
 
-    searchResults = results;
+    /*
+     * to avoid redrawing the whole visible region, we instead
+     * calculate the minimal redraw to remove previous highlights
+     * and add new ones;
+     * first make a temporary SearchResults that includes 
+     * both the new and any existing search results
+     */
+    SearchResultsI combined = results;
+    SearchResultsI previous = av.getSearchResults();
+    if (previous != null)
+    {
+      combined = new SearchResults();
+      combined.addSearchResults(results);
+      combined.addSearchResults(previous);
+    }
 
-    repaint();
+    /*
+     * calculate the minimal rectangle to redraw that 
+     * includes both new and existing search results
+     */
+    int firstSeq = Integer.MAX_VALUE;
+    int lastSeq = -1;
+    int firstCol = Integer.MAX_VALUE;
+    int lastCol = -1;
+    boolean matchFound = false;
+
+    ViewportRanges ranges = av.getRanges();
+    int firstVisibleColumn = ranges.getStartRes();
+    int lastVisibleColumn = ranges.getEndRes();
+    if (av.hasHiddenColumns())
+    {
+      firstVisibleColumn = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(firstVisibleColumn);
+      lastVisibleColumn = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(lastVisibleColumn);
+    }
+
+    for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+            .getEndSeq(); seqNo++)
+    {
+      SequenceI seq = av.getAlignment().getSequenceAt(seqNo);
+
+      int[] visibleResults = combined.getResults(seq, firstVisibleColumn,
+              lastVisibleColumn);
+      if (visibleResults != null)
+      {
+        for (int i = 0; i < visibleResults.length - 1; i += 2)
+        {
+          int firstMatchedColumn = visibleResults[i];
+          int lastMatchedColumn = visibleResults[i + 1];
+          if (firstMatchedColumn <= lastVisibleColumn
+                  && lastMatchedColumn >= firstVisibleColumn)
+          {
+            /*
+             * found a search results match in the visible region - 
+             * remember the first and last sequence matched, and the first
+             * and last visible columns in the matched positions
+             */
+            matchFound = true;
+            firstSeq = Math.min(firstSeq, seqNo);
+            lastSeq = Math.max(lastSeq, seqNo);
+            firstMatchedColumn = Math.max(firstMatchedColumn,
+                    firstVisibleColumn);
+            lastMatchedColumn = Math.min(lastMatchedColumn,
+                    lastVisibleColumn);
+            firstCol = Math.min(firstCol, firstMatchedColumn);
+            lastCol = Math.max(lastCol, lastMatchedColumn);
+          }
+        }
+      }
+    }
+
+    av.setSearchResults(results);
+
+    if (matchFound)
+    {
+      if (av.hasHiddenColumns())
+      {
+        firstCol = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(firstCol);
+        lastCol = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(lastCol);
+      }
+      int transX = (firstCol - firstVisibleColumn) * av.getCharWidth();
+      int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
+      gg.translate(transX, transY);
+      drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
+      gg.translate(-transX, -transY);
+
+      repaint();
+    }
+
+    fastpainting = false; // todo in finally block?
+  }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    if (!av.getWrapAlignment())
+    {
+      if (evt.getPropertyName().equals("startres")
+              || evt.getPropertyName().equals("endres"))
+      {
+        // Make sure we're not trying to draw a panel
+        // larger than the visible window
+        ViewportRanges vpRanges = av.getRanges();
+        int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
+        if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
+        {
+          scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
+        }
+        else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
+        {
+          scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
+        }
+        fastPaint(scrollX, 0);
+      }
+      else if (evt.getPropertyName().equals("startseq")
+              || evt.getPropertyName().equals("endseq"))
+      {
+        fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+      }
+    }
   }
 }