Merge remote-tracking branch 'origin/features/JAL-2609fastPaintWrapped'
authorkiramt <k.mourao@dundee.ac.uk>
Tue, 10 Oct 2017 13:34:50 +0000 (14:34 +0100)
committerkiramt <k.mourao@dundee.ac.uk>
Tue, 10 Oct 2017 13:34:50 +0000 (14:34 +0100)
into bug/JAL-2690attemptedmerge

Conflicts:
src/jalview/gui/SeqCanvas.java

1  2 
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java

@@@ -31,7 -31,6 +31,7 @@@ import jalview.util.Comparison
  import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
  
 +import java.awt.AlphaComposite; // ok
  import java.awt.BasicStroke;
  import java.awt.BorderLayout;
  import java.awt.Color;
@@@ -58,7 -57,7 +58,7 @@@ public class SeqCanvas extends JCompone
  
    final FeatureRenderer fr;
  
 -  final SequenceRenderer sr;
 +  final SequenceRenderer seqRdr; //ok
  
    BufferedImage img;
  
    {
      this.av = ap.av;
      fr = new FeatureRenderer(ap);
 -    sr = new SequenceRenderer(av);
 +    seqRdr = new SequenceRenderer(av); //ok
      setLayout(new BorderLayout());
      PaintRefresher.Register(this, av.getSequenceSetId());
      setBackground(Color.white);
  
    public SequenceRenderer getSequenceRenderer()
    {
 -    return sr;
 +    return seqRdr; //ok
    }
  
    public FeatureRenderer getFeatureRenderer()
      }
    }
  
 +  //ok
    /**
     * Does a fast paint of an alignment in response to a scroll. Most of the
     * visible region is simply copied and shifted, and then any newly visible
     */
    public void fastPaint(int horizontal, int vertical)
    {
 -    if (fastpainting || gg == null)
 +    if (fastpainting || gg == null || img == null) //ok
      {
        return;
      }
      {
        int charHeight = av.getCharHeight();
        int charWidth = av.getCharWidth();
 -
 +    
        ViewportRanges ranges = av.getRanges();
        int startRes = ranges.getStartRes();
        int endRes = ranges.getEndRes();
        int transX = 0;
        int transY = 0;
  
 -      gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
 -              imgHeight, -horizontal * charWidth, -vertical * charHeight);
 +    gg.copyArea(horizontal * charWidth, vertical * charHeight,
 +            img.getWidth(), img.getHeight(), -horizontal * charWidth,
 +            -vertical * charHeight);
  
 -      if (horizontal > 0) // scrollbar pulled right, image to the left
 -      {
 -        transX = (endRes - startRes - horizontal) * charWidth;
 -        startRes = endRes - horizontal;
 +    if (horizontal > 0) // scrollbar pulled right, image to the left
 +    {
 +      transX = (endRes - startRes - horizontal) * charWidth;
 +      startRes = endRes - horizontal;
 +    }
 +    else if (horizontal < 0)
 +    {
 +      endRes = startRes - horizontal;
 +    }
 +    else if (vertical > 0) // scroll down
 +    {
 +      startSeq = endSeq - vertical;
 +
 +      if (startSeq < ranges.getStartSeq())
 +      { // ie scrolling too fast, more than a page at a time
 +        startSeq = ranges.getStartSeq();
        }
 -      else if (horizontal < 0)
 +      else
        {
 -        endRes = startRes - horizontal;
 +        transY = img.getHeight() - ((vertical + 1) * charHeight);
        }
 -      else if (vertical > 0) // scroll down
 -      {
 -        startSeq = endSeq - vertical;
 +    }
 +    else if (vertical < 0)
 +    {
 +      endSeq = startSeq - vertical;
  
 -        if (startSeq < ranges.getStartSeq())
 -        { // ie scrolling too fast, more than a page at a time
 -          startSeq = ranges.getStartSeq();
 -        }
 -        else
 -        {
 -          transY = imgHeight - ((vertical + 1) * charHeight);
 -        }
 -      }
 -      else if (vertical < 0)
 +      if (endSeq > ranges.getEndSeq())
        {
 -        endSeq = startSeq - vertical;
 -
 -        if (endSeq > ranges.getEndSeq())
 -        {
 -          endSeq = ranges.getEndSeq();
 -        }
 +        endSeq = ranges.getEndSeq();
        }
 +    }
  
 -      gg.translate(transX, transY);
 -      drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
 -      gg.translate(-transX, -transY);
 +    gg.translate(transX, transY);
 +    drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
 +    gg.translate(-transX, -transY);
  
 -      repaint();
 +    repaint();
      } finally
      {
        fastpainting = false;
      }
    }
 +  //ok
  
 +  //ok
    @Override
    public void paintComponent(Graphics g)
    {
 +    super.paintComponent(g);    
 +    
      int charHeight = av.getCharHeight();
      int charWidth = av.getCharWidth();
 -    BufferedImage lcimg = img; // take reference since other threads may null
 -    // img and call later.
 -    super.paintComponent(g);
  
 -    if (lcimg != null && (fastPaint
 +    ViewportRanges ranges = av.getRanges();
 +
 +    int width = getWidth();
 +    int height = getHeight();
 +
 +    width -= (width % charWidth);
 +    height -= (height % charHeight);
 +
 +    // selectImage is the selection group outline image
 +    BufferedImage selectImage = drawSelectionGroup(
 +            ranges.getStartRes(), ranges.getEndRes(),
 +            ranges.getStartSeq(), ranges.getEndSeq());
 +
 +    if ((img != null) && (fastPaint
              || (getVisibleRect().width != g.getClipBounds().width)
              || (getVisibleRect().height != g.getClipBounds().height)))
      {
 +      BufferedImage lcimg = buildLocalImage(selectImage);
        g.drawImage(lcimg, 0, 0, this);
        fastPaint = false;
 -      return;
      }
 +    else if ((width > 0) && (height > 0))
 +    {
 +      // img is a cached version of the last view we drew, if any
 +      // if we have no img or the size has changed, make a new one
 +      if (img == null || width != img.getWidth()
 +              || height != img.getHeight())
 +      {
 +        img = setupImage();
 +        if (img == null)
 +        {
 +          return;
 +        }
 +        gg = (Graphics2D) img.getGraphics();
 +        gg.setFont(av.getFont());
 +      }
  
 -    // this draws the whole of the alignment
 -    imgWidth = getWidth();
 -    imgHeight = getHeight();
 +      if (av.antiAlias)
 +      {
 +        gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 +                RenderingHints.VALUE_ANTIALIAS_ON);
 +      }
 +
 +      gg.setColor(Color.white);
 +      gg.fillRect(0, 0, img.getWidth(), img.getHeight());
  
 -    imgWidth -= (imgWidth % charWidth);
 -    imgHeight -= (imgHeight % charHeight);
 +      if (av.getWrapAlignment())
 +      {
 +        drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
 +      }
 +      else
 +      {
 +        drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
 +                ranges.getStartSeq(), ranges.getEndSeq(), 0);
 +      }
 +
 +      // lcimg is a local *copy* of img which we'll draw selectImage on top of
 +      BufferedImage lcimg = buildLocalImage(selectImage);
 +      g.drawImage(lcimg, 0, 0, this);
 +    }
 +  }
 +//ok
 +  
 +  /**
 +   * Draw an alignment panel for printing
 +   * 
 +   * @param g1
 +   *          Graphics object to draw with
 +   * @param startRes
 +   *          start residue of print area
 +   * @param endRes
 +   *          end residue of print area
 +   * @param startSeq
 +   *          start sequence of print area
 +   * @param endSeq
 +   *          end sequence of print area
 +   */
 +  public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
 +          int startSeq, int endSeq)
 +  {
 +    drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
  
 -    if ((imgWidth < 1) || (imgHeight < 1))
 +    BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
 +            startSeq, endSeq);
 +    if (selectImage != null)
      {
 -      return;
 +      ((Graphics2D) g1).setComposite(AlphaComposite
 +              .getInstance(AlphaComposite.SRC_OVER));
 +      g1.drawImage(selectImage, 0, 0, this);
      }
 +  }
  
 -    if (lcimg == null || imgWidth != lcimg.getWidth()
 -            || imgHeight != lcimg.getHeight())
 +  /**
 +   * Draw a wrapped alignment panel for printing
 +   * 
 +   * @param g
 +   *          Graphics object to draw with
 +   * @param canvasWidth
 +   *          width of drawing area
 +   * @param canvasHeight
 +   *          height of drawing area
 +   * @param startRes
 +   *          start residue of print area
 +   */
 +  public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
 +          int canvasHeight, int startRes)
 +  {
 +    SequenceGroup group = av.getSelectionGroup();
 +
 +    drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
 +
 +    if (group != null)
      {
 +      BufferedImage selectImage = null;
        try
        {
 -        lcimg = img = new BufferedImage(imgWidth, imgHeight,
 -                BufferedImage.TYPE_INT_RGB);
 -        gg = (Graphics2D) img.getGraphics();
 -        gg.setFont(av.getFont());
 +        selectImage = new BufferedImage(canvasWidth, canvasHeight,
 +                BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
        } catch (OutOfMemoryError er)
        {
          System.gc();
 -        System.err.println("SeqCanvas OutOfMemory Redraw Error.\n" + er);
 -        new OOMWarning("Creating alignment image for display", er);
 -
 -        return;
 +        System.err.println("Print image OutOfMemory Error.\n" + er);
 +        new OOMWarning("Creating wrapped alignment image for printing", er);
 +      }
 +      if (selectImage != null)
 +      {
 +        Graphics2D g2 = selectImage.createGraphics();
 +        setupSelectionGroup(g2, selectImage);
 +        drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
 +                startRes);
 +
 +        g2.setComposite(
 +                AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
 +        g.drawImage(selectImage, 0, 0, this);
 +        g2.dispose();
        }
      }
 +  }
  
 -    if (av.antiAlias)
 +  /*
 +   * Make a local image by combining the cached image img
 +   * with any selection
 +   */
 +  private BufferedImage buildLocalImage(BufferedImage selectImage)
 +  {
 +    // clone the cached image
 +    BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
 +            img.getType());
 +    Graphics2D g2d = lcimg.createGraphics();
 +    g2d.drawImage(img, 0, 0, null);
 +
 +    // overlay selection group on lcimg
 +    if (selectImage != null)
      {
 -      gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 -              RenderingHints.VALUE_ANTIALIAS_ON);
 +      g2d.setComposite(
 +              AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
 +      g2d.drawImage(selectImage, 0, 0, this);
      }
 +    g2d.dispose();
  
 -    gg.setColor(Color.white);
 -    gg.fillRect(0, 0, imgWidth, imgHeight);
 +    return lcimg;
 +  }
  
 -    ViewportRanges ranges = av.getRanges();
 -    if (av.getWrapAlignment())
 +  /*
 +   * Set up a buffered image of the correct height and size for the sequence canvas
 +   */
 +  private BufferedImage setupImage()
 +  {
 +    BufferedImage lcimg = null;
 +
 +    int charWidth = av.getCharWidth();
 +    int charHeight = av.getCharHeight();
++    
 +    int width = getWidth();
 +    int height = getHeight();
 +
 +    width -= (width % charWidth);
 +    height -= (height % charHeight);
 +
 +    if ((width < 1) || (height < 1))
      {
 -      drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
 +      return null;
      }
 -    else
 +
 +    try
      {
 -      drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
 -              ranges.getStartSeq(), ranges.getEndSeq(), 0);
 -    }
 +      lcimg = new BufferedImage(width, height,
 +              BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
 +    } catch (OutOfMemoryError er)
 +    {
 +      System.gc();
 +      System.err.println(
 +              "Group image OutOfMemory Redraw Error.\n" + er);
 +      new OOMWarning("Creating alignment image for display", er);
  
 -    g.drawImage(lcimg, 0, 0, this);
 +      return null;
 +    }
  
 +    return lcimg;
    }
  
    /**
       */
      wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
      // add sequences
 -    wrappedRepeatHeightPx += av.getRanges().getViewportHeight() * charHeight;
 +    wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
 +            * charHeight;
      // add annotations panel height if shown
      wrappedRepeatHeightPx += getAnnotationHeight();
  
       * this is just canvas width less scale left and right (if shown), 
       * as a whole multiple of character widths 
       */
 -    int wrappedWidthInResidues = (canvasWidth - labelWidthEast - labelWidthWest)
 -            / charWidth;
 +    int wrappedWidthInResidues = (canvasWidth - labelWidthEast
 +            - labelWidthWest) / charWidth;
  
      /*
       * number of visible widths (the last one may be part height),
     * @param endColumn
     * @param canvasHeight
     */
 -  protected void drawWrappedWidth(Graphics g, int ypos,
 -          int startColumn, int endColumn, int canvasHeight)
 +  protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
 +          int endColumn, int canvasHeight)
    {
      int charHeight = av.getCharHeight();
      int charWidth = av.getCharWidth();
      }
    }
  
 +  /*
 +   * Draw a selection group over a wrapped alignment
 +   */
 +  private void drawWrappedSelection(Graphics2D g, SequenceGroup group,
 +          int canvasWidth,
 +          int canvasHeight, int startRes)
 +  {
 +      int charHeight = av.getCharHeight();
 +      int charWidth = av.getCharWidth();
 +        
 +    // height gap above each panel
 +    int hgap = charHeight;
 +    if (av.getScaleAboveWrapped())
 +    {
 +      hgap += charHeight;
 +    }
 +
 +    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
 +            / charWidth;
 +    int cHeight = av.getAlignment().getHeight() * charHeight;
 +
 +    int startx = startRes;
 +    int endx;
 +    int ypos = hgap; // vertical offset
 +    int maxwidth = av.getAlignment().getWidth();
 +
 +    if (av.hasHiddenColumns())
 +    {
 +      maxwidth = av.getAlignment().getHiddenColumns()
 +              .findColumnPosition(maxwidth);
 +    }
 +
 +    // chop the wrapped alignment extent up into panel-sized blocks and treat
 +    // each block as if it were a block from an unwrapped alignment
 +    while ((ypos <= canvasHeight) && (startx < maxwidth))
 +    {
 +      // set end value to be start + width, or maxwidth, whichever is smaller
 +      endx = startx + cWidth - 1;
 +
 +      if (endx > maxwidth)
 +      {
 +        endx = maxwidth;
 +      }
 +
 +      g.translate(labelWidthWest, 0);
 +
 +      drawUnwrappedSelection(g, group, startx, endx, 0,
 +              av.getAlignment().getHeight() - 1,
 +              ypos);
 +
 +      g.translate(-labelWidthWest, 0);
 +
 +      // update vertical offset
 +      ypos += cHeight + getAnnotationHeight() + hgap;
 +
 +      // update horizontal offset
 +      startx += cWidth;
 +    }
 +  }
 +
    int getAnnotationHeight()
    {
      if (!av.isShowAnnotation())
      int charWidth = av.getCharWidth();
  
      g.setFont(av.getFont());
 -    sr.prepare(g, av.isRenderGaps());
 +    seqRdr.prepare(g, av.isRenderGaps());
  
      SequenceI nextSeq;
  
          // empty
          continue;
        }
 -      sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
 +      seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
                startRes, endRes, offset + ((i - startSeq) * charHeight));
  
        if (av.isShowSequenceFeatures())
          {
            for (int r = 0; r < visibleResults.length; r += 2)
            {
 -            sr.drawHighlightedText(nextSeq, visibleResults[r],
 -                    visibleResults[r + 1],
 -                    (visibleResults[r] - startRes) * charWidth,
 -                    offset + ((i - startSeq) * charHeight));
 +            seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
 +                    visibleResults[r + 1], (visibleResults[r] - startRes)
 +                            * charWidth, offset
 +                            + ((i - startSeq) * charHeight));
            }
          }
        }
        if (av.cursorMode && cursorY == i && cursorX >= startRes
                && cursorX <= endRes)
        {
 -        sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
 +        seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
                  offset + ((i - startSeq) * charHeight));
        }
      }
    void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
            int startSeq, int endSeq, int offset)
    {
 -    int charHeight = av.getCharHeight();
 -    int charWidth = av.getCharWidth();
 -
      Graphics2D g = (Graphics2D) g1;
      //
      // ///////////////////////////////////
      // Now outline any areas if necessary
      // ///////////////////////////////////
 -    SequenceGroup group = av.getSelectionGroup();
  
 -    int sx = -1;
 -    int sy = -1;
 -    int ex = -1;
 +    SequenceGroup group = null;
      int groupIndex = -1;
 -    int visWidth = (endRes - startRes + 1) * charWidth;
  
 -    if ((group == null) && (av.getAlignment().getGroups().size() > 0))
 +    if (av.getAlignment().getGroups().size() > 0)
      {
        group = av.getAlignment().getGroups().get(0);
        groupIndex = 0;
  
      if (group != null)
      {
 +      g.setStroke(new BasicStroke());
 +      g.setColor(group.getOutlineColour());
 +      
        do
        {
 -        int oldY = -1;
 -        int i = 0;
 -        boolean inGroup = false;
 -        int top = -1;
 -        int bottom = -1;
 +        drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
 +                endSeq, offset);
 +
 +        groupIndex++;
 +
 +        g.setStroke(new BasicStroke());
  
 -        for (i = startSeq; i <= endSeq; i++)
 +        if (groupIndex >= av.getAlignment().getGroups().size())
          {
 -          sx = (group.getStartRes() - startRes) * charWidth;
 -          sy = offset + ((i - startSeq) * charHeight);
 -          ex = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
 -                  - 1;
 +          break;
 +        }
  
 -          if (sx + ex < 0 || sx > visWidth)
 -          {
 -            continue;
 -          }
 +        group = av.getAlignment().getGroups().get(groupIndex);
  
 -          if ((sx <= (endRes - startRes) * charWidth)
 -                  && group.getSequences(null)
 -                          .contains(av.getAlignment().getSequenceAt(i)))
 -          {
 -            if ((bottom == -1) && !group.getSequences(null)
 -                    .contains(av.getAlignment().getSequenceAt(i + 1)))
 -            {
 -              bottom = sy + charHeight;
 -            }
 -
 -            if (!inGroup)
 -            {
 -              if (((top == -1) && (i == 0)) || !group.getSequences(null)
 -                      .contains(av.getAlignment().getSequenceAt(i - 1)))
 -              {
 -                top = sy;
 -              }
 -
 -              oldY = sy;
 -              inGroup = true;
 -
 -              if (group == av.getSelectionGroup())
 -              {
 -                g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
 -                        BasicStroke.JOIN_ROUND, 3f, new float[]
 -                        { 5f, 3f }, 0f));
 -                g.setColor(Color.RED);
 -              }
 -              else
 -              {
 -                g.setStroke(new BasicStroke());
 -                g.setColor(group.getOutlineColour());
 -              }
 -            }
 -          }
 -          else
 +      } while (groupIndex < av.getAlignment().getGroups().size());
 +
 +    }
 +
 +  }
 +
 +
 +  /*
 +   * Draw the selection group as a separate image and overlay
 +   */
 +  private BufferedImage drawSelectionGroup(int startRes, int endRes,
 +          int startSeq, int endSeq)
 +  {
 +    // get a new image of the correct size
 +    BufferedImage selectionImage = setupImage();
 +
 +    if (selectionImage == null)
 +    {
 +      return null;
 +    }
 +
 +    SequenceGroup group = av.getSelectionGroup();
 +    if (group == null)
 +    {
 +      // nothing to draw
 +      return null;
 +    }
 +
 +    // set up drawing colour
 +    Graphics2D g = (Graphics2D) selectionImage.getGraphics();
 +
 +    setupSelectionGroup(g, selectionImage);
 +
 +    if (!av.getWrapAlignment())
 +    {
 +      drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
 +              0);
 +    }
 +    else
 +    {
 +      drawWrappedSelection(g, group, getWidth(), getHeight(),
 +              av.getRanges().getStartRes());
 +    }
 +
 +    g.dispose();
 +    return selectionImage;
 +  }
 +
 +  /*
 +   * Set up graphics for selection group
 +   */
 +  private void setupSelectionGroup(Graphics2D g,
 +          BufferedImage selectionImage)
 +  {
 +    // set background to transparent
 +    g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
 +    g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
 +
 +    // set up foreground to draw red dashed line
 +    g.setComposite(AlphaComposite.Src);
 +    g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
 +            BasicStroke.JOIN_ROUND, 3f, new float[]
 +    { 5f, 3f }, 0f));
 +    g.setColor(Color.RED);
 +  }
 +
 +  /*
 +   * Draw a selection group over an unwrapped alignment
 +   * @param g graphics object to draw with
 +   * @param group selection group
 +   * @param startRes start residue of area to draw
 +   * @param endRes end residue of area to draw
 +   * @param startSeq start sequence of area to draw
 +   * @param endSeq end sequence of area to draw
 +   * @param offset vertical offset (used when called from wrapped alignment code)
 +   */
 +  private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
 +          int startRes, int endRes, int startSeq, int endSeq, int offset)
 +  {
 +      int charWidth = av.getCharWidth();
 +        
 +    if (!av.hasHiddenColumns())
 +    {
 +      drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
 +              offset);
 +    }
 +    else
 +    {
 +      // package into blocks of visible columns
 +      int screenY = 0;
 +      int blockStart = startRes;
 +      int blockEnd = endRes;
 +
 +      for (int[] region : av.getAlignment().getHiddenColumns()
 +              .getHiddenColumnsCopy())
 +      {
 +        int hideStart = region[0];
 +        int hideEnd = region[1];
 +
 +        if (hideStart <= blockStart)
 +        {
 +          blockStart += (hideEnd - hideStart) + 1;
 +          continue;
 +        }
 +
 +        blockEnd = hideStart - 1;
 +
 +        g.translate(screenY * charWidth, 0);
 +        drawPartialGroupOutline(g, group,
 +                blockStart, blockEnd, startSeq, endSeq, offset);
 +
 +        g.translate(-screenY * charWidth, 0);
 +        screenY += blockEnd - blockStart + 1;
 +        blockStart = hideEnd + 1;
 +
 +        if (screenY > (endRes - startRes))
 +        {
 +          // already rendered last block
 +          break;
 +        }
 +      }
 +
 +      if (screenY <= (endRes - startRes))
 +      {
 +        // remaining visible region to render
 +        blockEnd = blockStart + (endRes - startRes) - screenY;
 +        g.translate(screenY * charWidth, 0);
 +        drawPartialGroupOutline(g, group,
 +                blockStart, blockEnd, startSeq, endSeq, offset);
 +        
 +        g.translate(-screenY * charWidth, 0);
 +      }
 +    }
 +  }
 +
 +  /*
 +   * Draw the selection group as a separate image and overlay
 +   */
 +  private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
 +          int startRes, int endRes, int startSeq, int endSeq,
 +          int verticalOffset)
 +  {
 +      int charHeight = av.getCharHeight();
 +      int charWidth = av.getCharWidth();
 +        
 +    int visWidth = (endRes - startRes + 1) * charWidth;
 +
 +    int oldY = -1;
 +    int i = 0;
 +    boolean inGroup = false;
 +    int top = -1;
 +    int bottom = -1;
 +
 +    int sx = -1;
 +    int sy = -1;
 +    int xwidth = -1;
 +
 +    for (i = startSeq; i <= endSeq; i++)
 +    {
 +      // position of start residue of group relative to startRes, in pixels
 +      sx = (group.getStartRes() - startRes) * charWidth;
 +
 +      // width of group in pixels
 +      xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
 +              - 1;
 +
 +      sy = verticalOffset + (i - startSeq) * charHeight;
 +
 +      if (sx + xwidth < 0 || sx > visWidth)
 +      {
 +        continue;
 +      }
 +
 +      if ((sx <= (endRes - startRes) * charWidth)
 +              && group.getSequences(null)
 +                      .contains(av.getAlignment().getSequenceAt(i)))
 +      {
 +        if ((bottom == -1) && !group.getSequences(null)
 +                .contains(av.getAlignment().getSequenceAt(i + 1)))
 +        {
 +          bottom = sy + charHeight;
 +        }
 +
 +        if (!inGroup)
 +        {
 +          if (((top == -1) && (i == 0)) || !group.getSequences(null)
 +                  .contains(av.getAlignment().getSequenceAt(i - 1)))
            {
 -            if (inGroup)
 -            {
 -              if (sx >= 0 && sx < visWidth)
 -              {
 -                g.drawLine(sx, oldY, sx, sy);
 -              }
 -
 -              if (sx + ex < visWidth)
 -              {
 -                g.drawLine(sx + ex, oldY, sx + ex, sy);
 -              }
 -
 -              if (sx < 0)
 -              {
 -                ex += sx;
 -                sx = 0;
 -              }
 -
 -              if (sx + ex > visWidth)
 -              {
 -                ex = visWidth;
 -              }
 -
 -              else if (sx + ex >= (endRes - startRes + 1) * charWidth)
 -              {
 -                ex = (endRes - startRes + 1) * charWidth;
 -              }
 -
 -              if (top != -1)
 -              {
 -                g.drawLine(sx, top, sx + ex, top);
 -                top = -1;
 -              }
 -
 -              if (bottom != -1)
 -              {
 -                g.drawLine(sx, bottom, sx + ex, bottom);
 -                bottom = -1;
 -              }
 -
 -              inGroup = false;
 -            }
 +            top = sy;
            }
 -        }
  
 +          oldY = sy;
 +          inGroup = true;
 +        }
 +      }
 +      else
 +      {
          if (inGroup)
          {
 -          sy = offset + ((i - startSeq) * charHeight);
 +          // if start position is visible, draw vertical line to left of
 +          // group
            if (sx >= 0 && sx < visWidth)
            {
              g.drawLine(sx, oldY, sx, sy);
            }
  
 -          if (sx + ex < visWidth)
 +          // if end position is visible, draw vertical line to right of
 +          // group
 +          if (sx + xwidth < visWidth)
            {
 -            g.drawLine(sx + ex, oldY, sx + ex, sy);
 +            g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
            }
  
            if (sx < 0)
            {
 -            ex += sx;
 +            xwidth += sx;
              sx = 0;
            }
  
 -          if (sx + ex > visWidth)
 +          // don't let width extend beyond current block, or group extent
 +          // fixes JAL-2672
 +          if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
            {
 -            ex = visWidth;
 +            xwidth = (endRes - startRes + 1) * charWidth - sx;
            }
 -          else if (sx + ex >= (endRes - startRes + 1) * charWidth)
 -          {
 -            ex = (endRes - startRes + 1) * charWidth;
 -          }
 -
 +          
 +          // draw horizontal line at top of group
            if (top != -1)
            {
 -            g.drawLine(sx, top, sx + ex, top);
 +            g.drawLine(sx, top, sx + xwidth, top);
              top = -1;
            }
  
 +          // draw horizontal line at bottom of group
            if (bottom != -1)
            {
 -            g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
 +            g.drawLine(sx, bottom, sx + xwidth, bottom);
              bottom = -1;
            }
  
            inGroup = false;
          }
 +      }
 +    }
  
 -        groupIndex++;
 +    if (inGroup)
 +    {
 +      sy = verticalOffset + ((i - startSeq) * charHeight);
 +      if (sx >= 0 && sx < visWidth)
 +      {
 +        g.drawLine(sx, oldY, sx, sy);
 +      }
  
 -        g.setStroke(new BasicStroke());
 +      if (sx + xwidth < visWidth)
 +      {
 +        g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
 +      }
  
 -        if (groupIndex >= av.getAlignment().getGroups().size())
 -        {
 -          break;
 -        }
 +      if (sx < 0)
 +      {
 +        xwidth += sx;
 +        sx = 0;
 +      }
  
 -        group = av.getAlignment().getGroups().get(groupIndex);
 +      if (sx + xwidth > visWidth)
 +      {
 +        xwidth = visWidth;
 +      }
 +      else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
 +      {
 +        xwidth = (endRes - startRes + 1) * charWidth;
 +      }
  
 -      } while (groupIndex < av.getAlignment().getGroups().size());
 +      if (top != -1)
 +      {
 +        g.drawLine(sx, top, sx + xwidth, top);
 +        top = -1;
 +      }
  
 -    }
 +      if (bottom != -1)
 +      {
 +        g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
 +        bottom = -1;
 +      }
  
 +      inGroup = false;
 +    }
    }
 -
 +  
    /**
     * Highlights search results in the visible region by rendering as white text
     * on a black background. Any previous highlighting is removed. Answers true
    {
      String eventName = evt.getPropertyName();
  
 -    int scrollX = 0;
 -    if (eventName.equals(ViewportRanges.STARTRES))
 +    if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
      {
 -      // Make sure we're not trying to draw a panel
 -      // larger than the visible window
 -      ViewportRanges vpRanges = av.getRanges();
 -      scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
 -      int range = vpRanges.getEndRes() - vpRanges.getStartRes();
 -      if (scrollX > range)
 -      {
 -        scrollX = range;
 -      }
 -      else if (scrollX < -range)
 -      {
 -        scrollX = -range;
 -      }
 +      fastPaint = true;
 +      repaint();
      }
 -
 -    // Both scrolling and resizing change viewport ranges: scrolling changes
 -    // both start and end points, but resize only changes end values.
 -    // Here we only want to fastpaint on a scroll, with resize using a normal
 -    // paint, so scroll events are identified as changes to the horizontal or
 -    // vertical start value.
 -    if (eventName.equals(ViewportRanges.STARTRES))
 +    else if (eventName.equals(ViewportRanges.STARTRES))
      {
 -      // scroll - startres and endres both change
 -      if (av.getWrapAlignment())
 +      int scrollX = 0;
 +      if (eventName.equals(ViewportRanges.STARTRES))
        {
 -        fastPaintWrapped(scrollX);
 +        // Make sure we're not trying to draw a panel
 +        // larger than the visible window
 +        ViewportRanges vpRanges = av.getRanges();
 +        scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
 +        int range = vpRanges.getEndRes() - vpRanges.getStartRes();
 +        if (scrollX > range)
 +        {
 +          scrollX = range;
 +        }
 +        else if (scrollX < -range)
 +        {
 +          scrollX = -range;
 +        }
 +
 +        // Both scrolling and resizing change viewport ranges: scrolling changes
 +        // both start and end points, but resize only changes end values.
 +        // Here we only want to fastpaint on a scroll, with resize using a normal
 +        // paint, so scroll events are identified as changes to the horizontal or
 +        // vertical start value.
 +        
 +        // scroll - startres and endres both change
 +        if (av.getWrapAlignment())
 +        {
 +          fastPaintWrapped(scrollX);
 +        }
 +        else
 +        {
 +          fastPaint(scrollX, 0);
 +        }
        }
 -      else
 +      else if (eventName.equals(ViewportRanges.STARTSEQ))
        {
 -        fastPaint(scrollX, 0);
 +        // scroll
 +        fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
        }
      }
 -    else if (eventName.equals(ViewportRanges.STARTSEQ))
 -    {
 -      fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
 -    }
    }
  
    /**
      }
    }
  
 +  
    /**
     * Redraws any positions in the search results in the visible region of a
     * wrapped alignment. Any highlights are drawn depending on the search results
@@@ -215,7 -215,7 +215,7 @@@ public class SeqPanel extends JPane
                + hgap + seqCanvas.getAnnotationHeight();
  
        int y = evt.getY();
-       y -= hgap;
+       y = Math.max(0, y - hgap);
        x = Math.max(0, x - seqCanvas.labelWidthWest);
  
        int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
      }
  
      mouseDragging = true;
 -    if (scrollThread != null)
 +    if ((scrollThread != null) && (scrollThread.isRunning()))
      {
        scrollThread.setEvent(evt);
      }
        oldSeq = 0;
      }
  
 -    if (scrollThread != null)
 +    if ((scrollThread != null) && (scrollThread.isRunning()))
      {
 -      scrollThread.running = false;
 +      scrollThread.stopScrolling();
        scrollThread = null;
      }
    }
        return;
      }
  
 -    if (mouseDragging)
 +    if (mouseDragging && scrollThread == null)
      {
        scrollThread = new ScrollThread();
      }
  
      if (stretchGroup == null)
      {
 -      // Only if left mouse button do we want to change group sizes
 +      createStretchGroup(res, sequence);
 +    }
  
 -      // define a new group here
 -      SequenceGroup sg = new SequenceGroup();
 -      sg.setStartRes(res);
 -      sg.setEndRes(res);
 -      sg.addSequence(sequence, false);
 -      av.setSelectionGroup(sg);
 -      stretchGroup = sg;
 +    if (stretchGroup != null)
 +    {
 +      stretchGroup.addPropertyChangeListener(seqCanvas);
 +    }
  
 -      if (av.getConservationSelected())
 -      {
 -        SliderPanel.setConservationSlider(ap, av.getResidueShading(),
 -                ap.getViewName());
 -      }
 +    seqCanvas.repaint();
 +  }
  
 -      if (av.getAbovePIDThreshold())
 -      {
 -        SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
 -                ap.getViewName());
 -      }
 -      // TODO: stretchGroup will always be not null. Is this a merge error ?
 -      if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
 -      {
 -        // Edit end res position of selected group
 -        changeEndRes = true;
 -      }
 -      else if ((stretchGroup != null)
 -              && (stretchGroup.getStartRes() == res))
 -      {
 -        // Edit end res position of selected group
 -        changeStartRes = true;
 -      }
 -      stretchGroup.getWidth();
 +  private void createStretchGroup(int res, SequenceI sequence)
 +  {
 +    // Only if left mouse button do we want to change group sizes
 +    // define a new group here
 +    SequenceGroup sg = new SequenceGroup();
 +    sg.setStartRes(res);
 +    sg.setEndRes(res);
 +    sg.addSequence(sequence, false);
 +    av.setSelectionGroup(sg);
 +    stretchGroup = sg;
 +
 +    if (av.getConservationSelected())
 +    {
 +      SliderPanel.setConservationSlider(ap, av.getResidueShading(),
 +              ap.getViewName());
      }
  
 -    seqCanvas.repaint();
 +    if (av.getAbovePIDThreshold())
 +    {
 +      SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
 +              ap.getViewName());
 +    }
 +    // TODO: stretchGroup will always be not null. Is this a merge error ?
 +    // or is there a threading issue here?
 +    if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
 +    {
 +      // Edit end res position of selected group
 +      changeEndRes = true;
 +    }
 +    else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
 +    {
 +      // Edit end res position of selected group
 +      changeStartRes = true;
 +    }
 +    stretchGroup.getWidth();
 +
    }
  
    /**
      {
        return;
      }
 +
 +    stretchGroup.removePropertyChangeListener(seqCanvas);
 +
      // always do this - annotation has own state
      // but defer colourscheme update until hidden sequences are passed in
      boolean vischange = stretchGroup.recalcConservation(true);
  
      mouseDragging = true;
  
 -    if (scrollThread != null)
 +    if ((scrollThread != null) && (scrollThread.isRunning()))
      {
        scrollThread.setEvent(evt);
      }
 -
 -    seqCanvas.repaint();
    }
  
    void scrollCanvas(MouseEvent evt)
    {
      if (evt == null)
      {
 -      if (scrollThread != null)
 +      if ((scrollThread != null) && (scrollThread.isRunning()))
        {
 -        scrollThread.running = false;
 +        scrollThread.stopScrolling();
          scrollThread = null;
        }
        mouseDragging = false;
    {
      MouseEvent evt;
  
 -    boolean running = false;
 +    private volatile boolean threadRunning = true;
  
      public ScrollThread()
      {
  
      public void stopScrolling()
      {
 -      running = false;
 +      threadRunning = false;
 +    }
 +
 +    public boolean isRunning()
 +    {
 +      return threadRunning;
      }
  
      @Override
      public void run()
      {
 -      running = true;
 -
 -      while (running)
 +      while (threadRunning)
        {
          if (evt != null)
          {
            if (mouseDragging && (evt.getY() < 0)
                    && (av.getRanges().getStartSeq() > 0))
            {
 -            running = av.getRanges().scrollUp(true);
 +            av.getRanges().scrollUp(true);
            }
  
            if (mouseDragging && (evt.getY() >= getHeight()) && (av
                    .getAlignment().getHeight() > av.getRanges().getEndSeq()))
            {
 -            running = av.getRanges().scrollUp(false);
 +            av.getRanges().scrollUp(false);
            }
  
            if (mouseDragging && (evt.getX() < 0))
            {
 -            running = av.getRanges().scrollRight(false);
 +            av.getRanges().scrollRight(false);
            }
            else if (mouseDragging && (evt.getX() >= getWidth()))
            {
 -            running = av.getRanges().scrollRight(true);
 +            av.getRanges().scrollRight(true);
            }
          }