Merge branch 'feature/JAL-2759' into merge/JAL-2759_2104
authorJim Procter <jprocter@issues.jalview.org>
Tue, 27 Feb 2018 11:16:41 +0000 (11:16 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Tue, 27 Feb 2018 11:16:41 +0000 (11:16 +0000)
13 files changed:
1  2 
src/jalview/appletgui/AnnotationLabels.java
src/jalview/datamodel/Alignment.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/OverviewDimensionsHideHidden.java
src/jalview/viewmodel/OverviewDimensionsShowHidden.java
test/jalview/datamodel/AlignmentTest.java

@@@ -23,6 -23,7 +23,7 @@@ package jalview.appletgui
  import jalview.analysis.AlignmentUtils;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.util.MessageManager;
@@@ -31,7 -32,6 +32,7 @@@ import jalview.util.ParseHtmlBodyAndLin
  import java.awt.Checkbox;
  import java.awt.CheckboxMenuItem;
  import java.awt.Color;
 +import java.awt.Cursor;
  import java.awt.Dimension;
  import java.awt.FlowLayout;
  import java.awt.FontMetrics;
@@@ -51,23 -51,12 +52,22 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.util.Arrays;
  import java.util.Collections;
- import java.util.Vector;
  
  public class AnnotationLabels extends Panel
          implements ActionListener, MouseListener, MouseMotionListener
  {
    Image image;
  
 +  /**
 +   * width in pixels within which height adjuster arrows are shown and active
 +   */
 +  private static final int HEIGHT_ADJUSTER_WIDTH = 50;
 +
 +  /**
 +   * height in pixels for allowing height adjuster to be active
 +   */
 +  private static int HEIGHT_ADJUSTER_HEIGHT = 10;
 +
    boolean active = false;
  
    AlignmentPanel ap;
      this.ap = ap;
      this.av = ap.av;
      setLayout(null);
 -
 -    /**
 -     * this retrieves the adjustable height glyph from resources. we don't use
 -     * it at the moment. java.net.URL url =
 -     * getClass().getResource("/images/idwidth.gif"); Image temp = null;
 -     * 
 -     * if (url != null) { temp =
 -     * java.awt.Toolkit.getDefaultToolkit().createImage(url); }
 -     * 
 -     * try { MediaTracker mt = new MediaTracker(this); mt.addImage(temp, 0);
 -     * mt.waitForID(0); } catch (Exception ex) { }
 -     * 
 -     * BufferedImage bi = new BufferedImage(temp.getHeight(this),
 -     * temp.getWidth(this), BufferedImage.TYPE_INT_RGB); Graphics2D g =
 -     * (Graphics2D) bi.getGraphics(); g.rotate(Math.toRadians(90));
 -     * g.drawImage(temp, 0, -bi.getWidth(this), this); image = (Image) bi;
 -     */
      addMouseListener(this);
      addMouseMotionListener(this);
    }
    @Override
    public void mouseMoved(MouseEvent evt)
    {
 -    resizePanel = evt.getY() < 10 && evt.getX() < 14;
 +    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
 +            && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
 +    setCursor(Cursor.getPredefinedCursor(
 +            resizePanel ? Cursor.S_RESIZE_CURSOR : Cursor.DEFAULT_CURSOR));
      int row = getSelectedRow(evt.getY() + scrollOffset);
  
      if (row > -1)
      resizePanel = false;
      dragEvent = null;
      dragCancelled = false;
 +    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      repaint();
      ap.annotationPanel.repaint();
    }
        resizePanel = true;
        repaint();
      }
 +    setCursor(Cursor.getPredefinedCursor(
 +            resizePanel ? Cursor.S_RESIZE_CURSOR : Cursor.DEFAULT_CURSOR));
    }
  
    @Override
                      + "\t" + sq.getSequenceAsString() + "\n");
      if (av.hasHiddenColumns())
      {
-       jalview.appletgui.AlignFrame.copiedHiddenColumns = new Vector<>(
-               av.getAlignment().getHiddenColumns().getHiddenColumnsCopy());
+       jalview.appletgui.AlignFrame.copiedHiddenColumns = new HiddenColumns(
+               av.getAlignment().getHiddenColumns());
      }
    }
  
        }
      }
      g.translate(0, +scrollOffset);
 -    if (resizePanel)
 -    {
 -      g.setColor(Color.red);
 -      g.setPaintMode();
 -      g.drawLine(2, 8, 5, 2);
 -      g.drawLine(5, 2, 8, 8);
 -    }
 -    else if (!dragCancelled && dragEvent != null && aa != null)
 +
 +    if (!resizePanel && !dragCancelled && dragEvent != null && aa != null)
      {
        g.setColor(Color.lightGray);
        g.drawString(aa[selectedRow].label, dragEvent.getX(),
@@@ -29,10 -29,12 +29,12 @@@ import jalview.util.MessageManager
  
  import java.util.ArrayList;
  import java.util.Arrays;
+ import java.util.BitSet;
  import java.util.Collections;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Hashtable;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
@@@ -49,7 -51,7 +51,7 @@@ public class Alignment implements Align
  {
    private Alignment dataset;
  
 -  protected List<SequenceI> sequences;
 +  private List<SequenceI> sequences;
  
    protected List<SequenceGroup> groups;
  
          return sequences.get(i);
        }
      }
 +
      return null;
    }
  
    public int getWidth()
    {
      int maxLength = -1;
 -
 +  
      for (int i = 0; i < sequences.size(); i++)
      {
        if (getSequenceAt(i).getLength() > maxLength)
          maxLength = getSequenceAt(i).getLength();
        }
      }
 -
 +  
      return maxLength;
    }
 +  /*
 +  @Override
 +  public int getWidth()
 +  {
 +    final Wrapper temp = new Wrapper();
 +  
 +    forEachSequence(new Consumer<SequenceI>()
 +    {
 +      @Override
 +      public void accept(SequenceI s)
 +      {
 +        if (s.getLength() > temp.inner)
 +        {
 +          temp.inner = s.getLength();
 +        }
 +      }
 +    }, 0, sequences.size() - 1);
 +  
 +    return temp.inner;
 +  }
 +  
 +  public static class Wrapper
 +  {
 +    public int inner;
 +  }*/
  
    /**
     * DOCUMENT ME!
      AlignmentAnnotation annot = new AlignmentAnnotation(name, name,
              new Annotation[1], 0f, 0f, AlignmentAnnotation.BAR_GRAPH);
      annot.hasText = false;
 -    annot.setCalcId(new String(calcId));
 +    if (calcId != null)
 +    {
 +      annot.setCalcId(new String(calcId));
 +    }
      annot.autoCalculated = autoCalc;
      if (seqRef != null)
      {
    {
      hiddenCols = cols;
    }
+   @Override
+   public void setupJPredAlignment()
+   {
+     SequenceI repseq = getSequenceAt(0);
+     setSeqrep(repseq);
+     HiddenColumns cs = new HiddenColumns();
+     cs.hideList(repseq.getInsertions());
+     setHiddenColumns(cs);
+   }
+   @Override
+   public HiddenColumns propagateInsertions(SequenceI profileseq,
+           AlignmentView input)
+   {
+     int profsqpos = 0;
+     char gc = getGapCharacter();
+     Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
+     HiddenColumns nview = (HiddenColumns) alandhidden[1];
+     SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
+     return propagateInsertions(profileseq, origseq, nview);
+   }
+   /**
+    * 
+    * @param profileseq
+    *          sequence in al which corresponds to origseq
+    * @param al
+    *          alignment which is to have gaps inserted into it
+    * @param origseq
+    *          sequence corresponding to profileseq which defines gap map for
+    *          modifying al
+    */
+   private HiddenColumns propagateInsertions(SequenceI profileseq,
+           SequenceI origseq, HiddenColumns hc)
+   {
+     // take the set of hidden columns, and the set of gaps in origseq,
+     // and remove all the hidden gaps from hiddenColumns
+     // first get the gaps as a Bitset
+     // then calculate hidden ^ not(gap)
+     BitSet gaps = origseq.gapBitset();
+     hc.andNot(gaps);
+     // for each sequence in the alignment, except the profile sequence,
+     // insert gaps corresponding to each hidden region but where each hidden
+     // column region is shifted backwards by the number of preceding visible
+     // gaps update hidden columns at the same time
+     HiddenColumns newhidden = new HiddenColumns();
+     int numGapsBefore = 0;
+     int gapPosition = 0;
+     Iterator<int[]> it = hc.iterator();
+     while (it.hasNext())
+     {
+       int[] region = it.next();
+       // get region coordinates accounting for gaps
+       // we can rely on gaps not being *in* hidden regions because we already
+       // removed those
+       while (gapPosition < region[0])
+       {
+         gapPosition++;
+         if (gaps.get(gapPosition))
+         {
+           numGapsBefore++;
+         }
+       }
+       int left = region[0] - numGapsBefore;
+       int right = region[1] - numGapsBefore;
+       newhidden.hideColumns(left, right);
+       padGaps(left, right, profileseq);
+     }
+     return newhidden;
+   }
+   /**
+    * Pad gaps in all sequences in alignment except profileseq
+    * 
+    * @param left
+    *          position of first gap to insert
+    * @param right
+    *          position of last gap to insert
+    * @param profileseq
+    *          sequence not to pad
+    */
+   private void padGaps(int left, int right, SequenceI profileseq)
+   {
+     char gc = getGapCharacter();
+     // make a string with number of gaps = length of hidden region
+     StringBuilder sb = new StringBuilder();
+     for (int g = 0; g < right - left + 1; g++)
+     {
+       sb.append(gc);
+     }
+     // loop over the sequences and pad with gaps where required
+     for (int s = 0, ns = getHeight(); s < ns; s++)
+     {
+       SequenceI sqobj = getSequenceAt(s);
+       if ((sqobj != profileseq) && (sqobj.getLength() >= left))
+       {
+         String sq = sqobj.getSequenceAsString();
+         sqobj.setSequence(
+                 sq.substring(0, left) + sb.toString() + sq.substring(left));
+       }
+     }
+   }
  }
@@@ -1862,23 -1862,17 +1862,17 @@@ public class AlignFrame extends GAlignF
        return;
      }
  
-     ArrayList<int[]> hiddenColumns = null;
+     HiddenColumns hiddenColumns = null;
      if (viewport.hasHiddenColumns())
      {
-       hiddenColumns = new ArrayList<>();
        int hiddenOffset = viewport.getSelectionGroup().getStartRes();
        int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
-       ArrayList<int[]> hiddenRegions = viewport.getAlignment()
-               .getHiddenColumns().getHiddenColumnsCopy();
-       for (int[] region : hiddenRegions)
-       {
-         if (region[0] >= hiddenOffset && region[1] <= hiddenCutoff)
-         {
-           hiddenColumns
-                   .add(new int[]
-                   { region[0] - hiddenOffset, region[1] - hiddenOffset });
-         }
-       }
+       // create new HiddenColumns object with copy of hidden regions
+       // between startRes and endRes, offset by startRes
+       hiddenColumns = new HiddenColumns(
+               viewport.getAlignment().getHiddenColumns(), hiddenOffset,
+               hiddenCutoff, hiddenOffset);
      }
  
      Desktop.jalviewClipboard = new Object[] { seqs,
          if (Desktop.jalviewClipboard != null
                  && Desktop.jalviewClipboard[2] != null)
          {
-           List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-           for (int[] region : hc)
-           {
-             af.viewport.hideColumns(region[0], region[1]);
-           }
+           HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+           af.viewport.setHiddenColumns(hc);
          }
  
          // >>>This is a fix for the moment, until a better solution is
        if (Desktop.jalviewClipboard != null
                && Desktop.jalviewClipboard[2] != null)
        {
-         List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-         for (int region[] : hc)
-         {
-           af.viewport.hideColumns(region[0], region[1]);
-         }
+         HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+         af.viewport.setHiddenColumns(hc);
        }
  
        // >>>This is a fix for the moment, until a better solution is
              new JnetAnnotationMaker();
              JnetAnnotationMaker.add_annotation(predictions,
                      viewport.getAlignment(), 0, false);
-             SequenceI repseq = viewport.getAlignment().getSequenceAt(0);
-             viewport.getAlignment().setSeqrep(repseq);
-             HiddenColumns cs = new HiddenColumns();
-             cs.hideInsertionsFor(repseq);
-             viewport.getAlignment().setHiddenColumns(cs);
+             viewport.getAlignment().setupJPredAlignment();
              isAnnotation = true;
            }
            // else if (IdentifyFile.FeaturesFile.equals(format))
              MessageManager.getString("option.trim_retrieved_seqs"));
      trimrs.setToolTipText(
              MessageManager.getString("label.trim_retrieved_sequences"));
 -    trimrs.setSelected(Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true));
 +    trimrs.setSelected(
 +            Cache.getDefault(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES, true));
      trimrs.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
          trimrs.setSelected(trimrs.isSelected());
 -        Cache.setProperty("TRIM_FETCHED_DATASET_SEQS",
 +        Cache.setProperty(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES,
                  Boolean.valueOf(trimrs.isSelected()).toString());
        };
      });
@@@ -419,8 -419,8 +419,8 @@@ public class AlignmentPanel extends GAl
      if (av.hasHiddenColumns())
      {
        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-       start = hidden.findColumnPosition(start);
-       end = hidden.findColumnPosition(end);
+       start = hidden.absoluteToVisibleColumn(start);
+       end = hidden.absoluteToVisibleColumn(end);
        if (start == end)
        {
          if (!hidden.isVisible(r[0]))
        {
          // reset the width to exclude hidden columns
          width = av.getAlignment().getHiddenColumns()
-                 .findColumnPosition(width);
+                 .absoluteToVisibleColumn(width);
        }
  
        hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
    @Override
    public void paintComponent(Graphics g)
    {
 -    invalidate();
 +    invalidate(); // needed so that the id width adjuster works correctly
  
      Dimension d = getIdPanel().getIdCanvas().getPreferredSize();
      idPanelHolder.setPreferredSize(d);
      hscrollFillerPanel.setPreferredSize(new Dimension(d.width, 12));
 -    validate();
 +
 +    validate(); // needed so that the id width adjuster works correctly
  
      /*
 -     * set scroll bar positions
 +     * set scroll bar positions - tried to remove but necessary for split panel to resize correctly
 +     * though I still think this call should be elsewhere.
       */
      ViewportRanges ranges = av.getRanges();
      setScrollValues(ranges.getStartRes(), ranges.getStartSeq());
      if (av.hasHiddenColumns())
      {
        maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth) - 1;
+               .absoluteToVisibleColumn(maxwidth) - 1;
      }
  
      int resWidth = getSeqPanel().seqCanvas
      if (av.hasHiddenColumns())
      {
        maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth);
+               .absoluteToVisibleColumn(maxwidth);
      }
  
      int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
      if (av.hasHiddenColumns())
      {
        maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth) - 1;
+               .absoluteToVisibleColumn(maxwidth) - 1;
      }
  
      int height = ((maxwidth / chunkWidth) + 1) * cHeight;
     */
    protected void scrollToCentre(SearchResultsI sr, int verticalOffset)
    {
 -    /*
 -     * To avoid jumpy vertical scrolling (if some sequences are gapped or not
 -     * mapped), we can make the scroll-to location a sequence above the one
 -     * actually mapped.
 -     */
 -    SequenceI mappedTo = sr.getResults().get(0).getSequence();
 -    List<SequenceI> seqs = av.getAlignment().getSequences();
 -
 -    /*
 -     * This is like AlignmentI.findIndex(seq) but here we are matching the
 -     * dataset sequence not the aligned sequence
 -     */
 -    boolean matched = false;
 -    for (SequenceI seq : seqs)
 -    {
 -      if (mappedTo == seq.getDatasetSequence())
 -      {
 -        matched = true;
 -        break;
 -      }
 -    }
 -    if (!matched)
 -    {
 -      return; // failsafe, shouldn't happen
 -    }
 -
 -    /*
 -     * Scroll to position but centring the target residue.
 -     */
      scrollToPosition(sr, verticalOffset, true, true);
    }
  
   */
  package jalview.gui;
  
 +import jalview.analysis.AlignSeq;
  import jalview.analysis.AlignmentUtils;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.io.FileFormat;
  import jalview.io.FormatAdapter;
 +import jalview.util.Comparison;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  
  import java.awt.Color;
 +import java.awt.Cursor;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
 -import java.awt.Image;
 -import java.awt.MediaTracker;
  import java.awt.RenderingHints;
  import java.awt.Toolkit;
  import java.awt.datatransfer.StringSelection;
@@@ -50,9 -49,10 +51,11 @@@ import java.awt.event.MouseEvent
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.awt.geom.AffineTransform;
+ import java.awt.image.BufferedImage;
 +import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
+ import java.util.Iterator;
  import java.util.regex.Pattern;
  
  import javax.swing.JCheckBoxMenuItem;
@@@ -63,74 -63,63 +66,74 @@@ import javax.swing.SwingUtilities
  import javax.swing.ToolTipManager;
  
  /**
 - * DOCUMENT ME!
 - * 
 - * @author $author$
 - * @version $Revision$
 + * The panel that holds the labels for alignment annotations, providing
 + * tooltips, context menus, drag to reorder rows, and drag to adjust panel
 + * height
   */
  public class AnnotationLabels extends JPanel
          implements MouseListener, MouseMotionListener, ActionListener
  {
 +  /**
 +   * width in pixels within which height adjuster arrows are shown and active
 +   */
 +  private static final int HEIGHT_ADJUSTER_WIDTH = 50;
 +
 +  /**
 +   * height in pixels for allowing height adjuster to be active
 +   */
 +  private static int HEIGHT_ADJUSTER_HEIGHT = 10;
 +
    private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
            .compile("<");
  
 -  String TOGGLE_LABELSCALE = MessageManager
 +  private static final Font font = new Font("Arial", Font.PLAIN, 11);
 +
 +  private static final String TOGGLE_LABELSCALE = MessageManager
            .getString("label.scale_label_to_column");
  
 -  String ADDNEW = MessageManager.getString("label.add_new_row");
 +  private static final String ADDNEW = MessageManager
 +          .getString("label.add_new_row");
  
 -  String EDITNAME = MessageManager
 +  private static final String EDITNAME = MessageManager
            .getString("label.edit_label_description");
  
 -  String HIDE = MessageManager.getString("label.hide_row");
 +  private static final String HIDE = MessageManager
 +          .getString("label.hide_row");
  
 -  String DELETE = MessageManager.getString("label.delete_row");
 +  private static final String DELETE = MessageManager
 +          .getString("label.delete_row");
  
 -  String SHOWALL = MessageManager.getString("label.show_all_hidden_rows");
 +  private static final String SHOWALL = MessageManager
 +          .getString("label.show_all_hidden_rows");
  
 -  String OUTPUT_TEXT = MessageManager.getString("label.export_annotation");
 +  private static final String OUTPUT_TEXT = MessageManager
 +          .getString("label.export_annotation");
  
 -  String COPYCONS_SEQ = MessageManager
 +  private static final String COPYCONS_SEQ = MessageManager
            .getString("label.copy_consensus_sequence");
  
 -  boolean resizePanel = false;
 -
 -  Image image;
 +  private final boolean debugRedraw = false;
  
 -  AlignmentPanel ap;
 +  private AlignmentPanel ap;
  
    AlignViewport av;
  
 -  boolean resizing = false;
 -
 -  MouseEvent dragEvent;
 +  private MouseEvent dragEvent;
  
 -  int oldY;
 +  private int oldY;
  
 -  int selectedRow;
 +  private int selectedRow;
  
    private int scrollOffset = 0;
  
 -  Font font = new Font("Arial", Font.PLAIN, 11);
 -
    private boolean hasHiddenRows;
  
 +  private boolean resizePanel = false;
 +
    /**
 -   * Creates a new AnnotationLabels object.
 +   * Creates a new AnnotationLabels object
     * 
     * @param ap
 -   *          DOCUMENT ME!
     */
    public AnnotationLabels(AlignmentPanel ap)
    {
      av = ap.av;
      ToolTipManager.sharedInstance().registerComponent(this);
  
 -    java.net.URL url = getClass().getResource("/images/idwidth.gif");
 -    Image temp = null;
 -
 -    if (url != null)
 -    {
 -      temp = java.awt.Toolkit.getDefaultToolkit().createImage(url);
 -    }
 -
 -    try
 -    {
 -      MediaTracker mt = new MediaTracker(this);
 -      mt.addImage(temp, 0);
 -      mt.waitForID(0);
 -    } catch (Exception ex)
 -    {
 -    }
 -
 -    BufferedImage bi = new BufferedImage(temp.getHeight(this),
 -            temp.getWidth(this), BufferedImage.TYPE_INT_RGB);
 -    Graphics2D g = (Graphics2D) bi.getGraphics();
 -    g.rotate(Math.toRadians(90));
 -    g.drawImage(temp, 0, -bi.getWidth(this), this);
 -    image = bi;
 -
      addMouseListener(this);
      addMouseMotionListener(this);
      addMouseWheelListener(ap.getAnnotationPanel());
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Reorders annotation rows after a drag of a label
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseReleased(MouseEvent evt)
      getSelectedRow(evt.getY() - getScrollOffset());
      int end = selectedRow;
  
 +    /*
 +     * if dragging to resize instead, start == end
 +     */
      if (start != end)
      {
        // Swap these annotations
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param evt
 -   *          DOCUMENT ME!
 -   */
 -  @Override
 -  public void mouseEntered(MouseEvent evt)
 -  {
 -    if (evt.getY() < 10)
 -    {
 -      resizePanel = true;
 -      repaint();
 -    }
 -  }
 -
 -  /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param evt
 -   *          DOCUMENT ME!
 +   * Removes the height adjuster image on leaving the panel, unless currently
 +   * dragging it
     */
    @Override
    public void mouseExited(MouseEvent evt)
    {
 -    if (dragEvent == null)
 +    if (resizePanel && dragEvent == null)
      {
        resizePanel = false;
        repaint();
    }
  
    /**
 -   * DOCUMENT ME!
 +   * A mouse drag may be either an adjustment of the panel height (if flag
 +   * resizePanel is set on), or a reordering of the annotation rows. The former
 +   * is dealt with by this method, the latter in mouseReleased.
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseDragged(MouseEvent evt)
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Updates the tooltip as the mouse moves over the labels
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseMoved(MouseEvent evt)
    {
 -    resizePanel = evt.getY() < 10;
 +    showOrHideAdjuster(evt);
  
      getSelectedRow(evt.getY() - getScrollOffset());
  
      }
    }
  
 +  /**
 +   * Shows the height adjuster image if the mouse moves into the top left
 +   * region, or hides it if the mouse leaves the regio
 +   * 
 +   * @param evt
 +   */
 +  protected void showOrHideAdjuster(MouseEvent evt)
 +  {
 +    boolean was = resizePanel;
 +    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
 +
 +    if (resizePanel != was)
 +    {
 +      setCursor(Cursor.getPredefinedCursor(
 +              resizePanel ? Cursor.S_RESIZE_CURSOR
 +                      : Cursor.DEFAULT_CURSOR));
 +      repaint();
 +    }
 +  }
 +
    @Override
    public void mouseClicked(MouseEvent evt)
    {
              // process modifiers
              SequenceGroup sg = ap.av.getSelectionGroup();
              if (sg == null || sg == aa[selectedRow].groupRef
 -                    || !(jalview.util.Platform.isControlDown(evt)
 -                            || evt.isShiftDown()))
 +                    || !(Platform.isControlDown(evt) || evt.isShiftDown()))
              {
 -              if (jalview.util.Platform.isControlDown(evt)
 -                      || evt.isShiftDown())
 +              if (Platform.isControlDown(evt) || evt.isShiftDown())
                {
                  // clone a new selection group from the associated group
                  ap.av.setSelectionGroup(
                // we make a copy rather than edit the current selection if no
                // modifiers pressed
                // see Enhancement JAL-1557
 -              if (!(jalview.util.Platform.isControlDown(evt)
 -                      || evt.isShiftDown()))
 +              if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
                {
                  sg = new SequenceGroup(sg);
                  sg.clear();
                }
                else
                {
 -                if (jalview.util.Platform.isControlDown(evt))
 +                if (Platform.isControlDown(evt))
                  {
                    sg.addOrRemove(aa[selectedRow].sequenceRef, true);
                  }
      if (dseqs[0] == null)
      {
        dseqs[0] = new Sequence(sq);
 -      dseqs[0].setSequence(jalview.analysis.AlignSeq.extractGaps(
 -              jalview.util.Comparison.GapChars, sq.getSequenceAsString()));
 +      dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
 +              sq.getSequenceAsString()));
  
        sq.setDatasetSequence(dseqs[0]);
      }
      Alignment ds = new Alignment(dseqs);
      if (av.hasHiddenColumns())
      {
-       omitHidden = av.getAlignment().getHiddenColumns()
-               .getVisibleSequenceStrings(0, sq.getLength(), seqs);
+       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
+               .getVisContigsIterator(0, sq.getLength(), false);
+       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
      }
  
      int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
      Toolkit.getDefaultToolkit().getSystemClipboard()
              .setContents(new StringSelection(output), Desktop.instance);
  
-     ArrayList<int[]> hiddenColumns = null;
+     HiddenColumns hiddenColumns = null;
  
      if (av.hasHiddenColumns())
      {
-       hiddenColumns = av.getAlignment().getHiddenColumns()
-               .getHiddenColumnsCopy();
+       hiddenColumns = new HiddenColumns(
+               av.getAlignment().getHiddenColumns());
      }
  
      Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
      drawComponent(g, false, width);
    }
  
 -  private final boolean debugRedraw = false;
 -
    /**
     * Draw the full set of annotation Labels for the alignment at the given
     * cursor
        }
      }
  
 -    if (resizePanel)
 -    {
 -      g.drawImage(image, 2, 0 - getScrollOffset(), this);
 -    }
 -    else if (dragEvent != null && aa != null)
 +    if (!resizePanel && dragEvent != null && aa != null)
      {
        g.setColor(Color.lightGray);
        g.drawString(aa[selectedRow].label, dragEvent.getX(),
    {
      return scrollOffset;
    }
 +
 +  @Override
 +  public void mouseEntered(MouseEvent e)
 +  {
 +  }
  }
@@@ -724,7 -724,7 +724,7 @@@ public class AnnotationPanel extends JP
      if (av.hasHiddenColumns())
      {
        column = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(column);
+               .visibleToAbsoluteColumn(column);
      }
  
      AlignmentAnnotation ann = aa[row];
    @Override
    public void paintComponent(Graphics g)
    {
 +    super.paintComponent(g);
 +
      g.setColor(Color.white);
      g.fillRect(0, 0, getWidth(), getHeight());
  
        gg.fillRect(0, 0, imgWidth, image.getHeight());
        imageFresh = true;
      }
 -
 +    
      drawComponent(gg, av.getRanges().getStartRes(),
              av.getRanges().getEndRes() + 1);
      imageFresh = false;
      int er = av.getRanges().getEndRes() + 1;
      int transX = 0;
  
 -    long stime = System.currentTimeMillis();
      gg.copyArea(0, 0, imgWidth, getHeight(),
              -horizontal * av.getCharWidth(), 0);
 -    long mtime = System.currentTimeMillis();
  
      if (horizontal > 0) // scrollbar pulled right, image to the left
      {
      drawComponent(gg, sr, er);
  
      gg.translate(-transX, 0);
 -    long dtime = System.currentTimeMillis();
 +
      fastPaint = true;
 -    repaint();
 -    long rtime = System.currentTimeMillis();
 -    if (debugRedraw)
 -    {
 -      System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
 -              + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
 -              + "\tRepaint call:\t" + (rtime - dtime));
 -    }
  
 +    // Call repaint on alignment panel so that repaints from other alignment
 +    // panel components can be aggregated. Otherwise performance of the overview
 +    // window and others may be adversely affected.
 +    av.getAlignPanel().repaint();
    }
  
    private volatile boolean lastImageGood = false;
@@@ -83,7 -83,7 +83,7 @@@ public class IdCanvas extends JPanel im
      this.av = av;
      PaintRefresher.Register(this, av.getSequenceSetId());
      av.getRanges().addPropertyChangeListener(this);
 -  }
 +    }
  
    /**
     * DOCUMENT ME!
      gg.translate(0, -transY);
  
      fastPaint = true;
 -    repaint();
 +
 +    // Call repaint on alignment panel so that repaints from other alignment
 +    // panel components can be aggregated. Otherwise performance of the overview
 +    // window and others may be adversely affected.
 +    av.getAlignPanel().repaint();
    }
  
    /**
    @Override
    public void paintComponent(Graphics g)
    {
 +    super.paintComponent(g);
 +
      g.setColor(Color.white);
      g.fillRect(0, 0, getWidth(), getHeight());
 -
 +    
      if (fastPaint)
      {
        fastPaint = false;
        g.drawImage(image, 0, 0, this);
 -
 +    
        return;
      }
 -
 +    
      int oldHeight = imgHeight;
 -
 +    
      imgHeight = getHeight();
      imgHeight -= (imgHeight % av.getCharHeight());
 -
 +    
      if (imgHeight < 1)
      {
        return;
      }
 -
 +    
      if (oldHeight != imgHeight || image.getWidth(this) != getWidth())
      {
 -      image = new BufferedImage(getWidth(), imgHeight,
 -              BufferedImage.TYPE_INT_RGB);
 +      image = new BufferedImage(getWidth(), imgHeight,
 +                BufferedImage.TYPE_INT_RGB);
      }
 -
 +    
      gg = (Graphics2D) image.getGraphics();
 -
 +    
      // Fill in the background
      gg.setColor(Color.white);
      gg.fillRect(0, 0, getWidth(), imgHeight);
 -
 +    
      drawIds(av.getRanges().getStartSeq(), av.getRanges().getEndSeq());
 -
 +    
      g.drawImage(image, 0, 0, this);
    }
  
      if (av.hasHiddenColumns())
      {
        maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth) - 1;
+               .absoluteToVisibleColumn(maxwidth) - 1;
      }
  
      int annotationHeight = 0;
@@@ -42,6 -42,7 +42,7 @@@ import java.awt.event.MouseEvent
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.beans.PropertyChangeEvent;
+ import java.util.Iterator;
  import java.util.List;
  
  import javax.swing.JMenuItem;
@@@ -112,7 -113,7 +113,7 @@@ public class ScalePanel extends JPane
  
      if (av.hasHiddenColumns())
      {
-       x = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(x);
+       x = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(x);
      }
  
      if (x >= av.getAlignment().getWidth())
        });
        pop.add(item);
  
-       if (av.getAlignment().getHiddenColumns().hasHiddenColumns())
+       if (av.getAlignment().getHiddenColumns().hasMultiHiddenColumnRegions())
        {
          item = new JMenuItem(MessageManager.getString("action.reveal_all"));
          item.addActionListener(new ActionListener()
      if (av.hasHiddenColumns())
      {
        res = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(res);
+               .visibleToAbsoluteColumn(res);
      }
  
      if (res >= av.getAlignment().getWidth())
      int res = (evt.getX() / av.getCharWidth())
              + av.getRanges().getStartRes();
      res = Math.max(0, res);
-     res = hidden.adjustForHiddenColumns(res);
+     res = hidden.visibleToAbsoluteColumn(res);
      res = Math.min(res, av.getAlignment().getWidth() - 1);
      min = Math.min(res, min);
      max = Math.max(res, max);
      reveal = av.getAlignment().getHiddenColumns()
              .getRegionWithEdgeAtRes(res);
  
-     res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
+     res = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(res);
  
      ToolTipManager.sharedInstance().registerComponent(this);
      this.setToolTipText(
    @Override
    public void paintComponent(Graphics g)
    {
 +    super.paintComponent(g);
 +
      /*
       * shouldn't get called in wrapped mode as the scale above is
       * drawn instead by SeqCanvas.drawNorthScale
          {
            if (hidden.isVisible(sel))
            {
-             sel = hidden.findColumnPosition(sel);
+             sel = hidden.absoluteToVisibleColumn(sel);
            }
            else
            {
  
        if (av.getShowHiddenMarkers())
        {
-         List<Integer> positions = hidden.findHiddenRegionPositions();
-         for (int pos : positions)
+         Iterator<Integer> it = hidden.getStartRegionIterator(startx,
+                 startx + widthx + 1);
+         while (it.hasNext())
          {
-           res = pos - startx;
-           if (res < 0 || res > widthx)
-           {
-             continue;
-           }
+           res = it.next() - startx;
  
            gg.fillPolygon(
                    new int[]
-                   { -1 + res * avCharWidth - avCharHeight / 4,
-                       -1 + res * avCharWidth + avCharHeight / 4,
-                       -1 + res * avCharWidth },
-                   new int[]
-                   { y, y, y + 2 * yOf }, 3);
+           { -1 + res * avCharWidth - avCharHeight / 4,
+               -1 + res * avCharWidth + avCharHeight / 4,
+               -1 + res * avCharWidth }, new int[]
+           { y, y, y + 2 * yOf }, 3);
          }
        }
      }
              || evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
      {
        // scroll event, repaint panel
 -      repaint();
 +      
 +      // Call repaint on alignment panel so that repaints from other alignment
 +    // panel components can be aggregated. Otherwise performance of the overview
 +    // window and others may be adversely affected.
 +      av.getAlignPanel().repaint();
      }
    }
  
@@@ -25,6 -25,7 +25,7 @@@ import jalview.datamodel.HiddenColumns
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
+ import jalview.datamodel.VisibleContigsIterator;
  import jalview.renderer.ScaleRenderer;
  import jalview.renderer.ScaleRenderer.ScaleMark;
  import jalview.util.Comparison;
@@@ -42,6 -43,7 +43,7 @@@ import java.awt.RenderingHints
  import java.awt.Shape;
  import java.awt.image.BufferedImage;
  import java.beans.PropertyChangeEvent;
+ import java.util.Iterator;
  import java.util.List;
  
  import javax.swing.JComponent;
@@@ -198,8 -200,8 +200,8 @@@ public class SeqCanvas extends JCompone
      if (av.hasHiddenColumns())
      {
        HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
-       startX = hiddenColumns.adjustForHiddenColumns(startx);
-       endX = hiddenColumns.adjustForHiddenColumns(endx);
+       startX = hiddenColumns.visibleToAbsoluteColumn(startx);
+       endX = hiddenColumns.visibleToAbsoluteColumn(endx);
      }
      FontMetrics fm = getFontMetrics(av.getFont());
  
        int endSeq = ranges.getEndSeq();
        int transX = 0;
        int transY = 0;
 -
 +      
        gg.copyArea(horizontal * charWidth, vertical * charHeight,
                img.getWidth(), img.getHeight(), -horizontal * charWidth,
                -vertical * charHeight);
        drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
        gg.translate(-transX, -transY);
  
 -      repaint();
 +      // Call repaint on alignment panel so that repaints from other alignment
 +      // panel components can be aggregated. Otherwise performance of the
 +      // overview window and others may be adversely affected.
 +      av.getAlignPanel().repaint();
      } finally
      {
        fastpainting = false;
      
      int charHeight = av.getCharHeight();
      int charWidth = av.getCharWidth();
 -
 +    
      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)))
          gg = (Graphics2D) img.getGraphics();
          gg.setFont(av.getFont());
        }
 -
 +    
        if (av.antiAlias)
        {
          gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                  RenderingHints.VALUE_ANTIALIAS_ON);
        }
 -
 +    
        gg.setColor(Color.white);
        gg.fillRect(0, 0, img.getWidth(), img.getHeight());
 -
 +    
        if (av.getWrapAlignment())
        {
          drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
          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);
    private BufferedImage buildLocalImage(BufferedImage selectImage)
    {
      // clone the cached image
 -    BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
 -            img.getType());
 +        BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
 +                  img.getType());
 +
 +    // BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
 +    // img.getType());
      Graphics2D g2d = lcimg.createGraphics();
      g2d.drawImage(img, 0, 0, null);
  
  
      try
      {
 -      lcimg = new BufferedImage(width, height,
 -              BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
 +      lcimg = new BufferedImage(width, height,
 +                BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
      } catch (OutOfMemoryError er)
      {
        System.gc();
      int charWidth = av.getCharWidth();
  
      g.setColor(Color.blue);
+     int res;
      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-     List<Integer> positions = hidden.findHiddenRegionPositions();
-     for (int pos : positions)
+     Iterator<Integer> it = hidden.getStartRegionIterator(startColumn,
+             endColumn);
+     while (it.hasNext())
      {
-       int res = pos - startColumn;
+       res = it.next() - startColumn;
  
        if (res < 0 || res > endColumn - startColumn + 1)
        {
      if (av.hasHiddenColumns())
      {
        maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth);
+               .absoluteToVisibleColumn(maxwidth);
      }
  
      // chop the wrapped alignment extent up into panel-sized blocks and treat
      else
      {
        int screenY = 0;
-       final int screenYMax = endRes - startRes;
-       int blockStart = startRes;
-       int blockEnd = endRes;
+       int blockStart;
+       int blockEnd;
  
-       for (int[] region : av.getAlignment().getHiddenColumns()
-               .getHiddenColumnsCopy())
-       {
-         int hideStart = region[0];
-         int hideEnd = region[1];
+       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+       VisibleContigsIterator regions = hidden
+               .getVisContigsIterator(startRes, endRes + 1, true);
  
-         if (hideStart <= blockStart)
-         {
-           blockStart += (hideEnd - hideStart) + 1;
-           continue;
-         }
+       while (regions.hasNext())
+       {
+         int[] region = regions.next();
+         blockEnd = region[1];
+         blockStart = region[0];
  
          /*
           * 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, yOffset);
           * draw the downline of the hidden column marker (ScalePanel draws the
           * triangle on top) if we reached it
           */
-         if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
+         if (av.getShowHiddenMarkers()
+                 && (regions.hasNext() || regions.endsAtHidden()))
          {
            g1.setColor(Color.blue);
  
  
          g1.translate(-screenY * charWidth, 0);
          screenY += blockEnd - blockStart + 1;
-         blockStart = hideEnd + 1;
-         if (screenY > screenYMax)
-         {
-           // already rendered last block
-           return;
-         }
-       }
-       if (screenY <= screenYMax)
-       {
-         // remaining visible region to render
-         blockEnd = blockStart + screenYMax - screenY;
-         g1.translate(screenY * charWidth, 0);
-         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
-         g1.translate(-screenY * charWidth, 0);
        }
      }
  
        if (av.hasSearchResults())
        {
          SearchResultsI searchResults = av.getSearchResults();
 -        int[] visibleResults = searchResults.getResults(nextSeq,
 -                startRes, endRes);
 +        int[] visibleResults = searchResults.getResults(nextSeq, startRes,
 +                endRes);
          if (visibleResults != null)
          {
            for (int r = 0; r < visibleResults.length; r += 2)
            {
              seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
 -                    visibleResults[r + 1], (visibleResults[r] - startRes)
 -                            * charWidth, offset
 -                            + ((i - startSeq) * charHeight));
 +                    visibleResults[r + 1],
 +                    (visibleResults[r] - startRes) * charWidth,
 +                    offset + ((i - startSeq) * charHeight));
            }
          }
        }
  
        // convert the cursorX into a position on the visible alignment
        int cursor_xpos = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(cursorX);
+               .absoluteToVisibleColumn(cursorX);
  
        if (av.getAlignment().getHiddenColumns().isVisible(cursorX))
        {
      {
        // package into blocks of visible columns
        int screenY = 0;
-       int blockStart = startRes;
-       int blockEnd = endRes;
+       int blockStart;
+       int blockEnd;
  
-       for (int[] region : av.getAlignment().getHiddenColumns()
-               .getHiddenColumnsCopy())
+       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+       VisibleContigsIterator regions = hidden
+               .getVisContigsIterator(startRes, endRes + 1, true);
+       while (regions.hasNext())
        {
-         int hideStart = region[0];
-         int hideEnd = region[1];
-         if (hideStart <= blockStart)
-         {
-           blockStart += (hideEnd - hideStart) + 1;
-           continue;
-         }
-         blockEnd = hideStart - 1;
+         int[] region = regions.next();
+         blockEnd = region[1];
+         blockStart = region[0];
  
          g.translate(screenY * charWidth, 0);
          drawPartialGroupOutline(g, group,
  
          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);
        }
      }
    }
      if (av.hasHiddenColumns())
      {
        firstVisibleColumn = alignment.getHiddenColumns()
-               .adjustForHiddenColumns(firstVisibleColumn);
+               .visibleToAbsoluteColumn(firstVisibleColumn);
        lastVisibleColumn = alignment.getHiddenColumns()
-               .adjustForHiddenColumns(lastVisibleColumn);
+               .visibleToAbsoluteColumn(lastVisibleColumn);
      }
  
      for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
        if (av.hasHiddenColumns())
        {
          firstCol = alignment.getHiddenColumns()
-                 .findColumnPosition(firstCol);
-         lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
+                 .absoluteToVisibleColumn(firstCol);
+         lastCol = alignment.getHiddenColumns().absoluteToVisibleColumn(lastCol);
        }
        int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
        int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
          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.
-     if (eventName.equals(ViewportRanges.STARTRES))
-     {
-       if (av.getWrapAlignment())
+       // 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))
+       {
+         if (av.getWrapAlignment())
+           {
+             fastPaintWrapped(scrollX);
+           }
+           else
+           {
+             fastPaint(scrollX, 0);
+           }
+       }
+       else if (eventName.equals(ViewportRanges.STARTSEQ))
+       {
+         // scroll
+         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+       }
+       else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+       {
+         if (av.getWrapAlignment())
          {
            fastPaintWrapped(scrollX);
          }
        {
          fastPaintWrapped(scrollX);
        }
-       else
-       {
-         fastPaint(scrollX, 0);
-       }
-       // bizarrely, we only need to scroll on the x value here as fastpaint
-       // copies the full height of the image anyway. Passing in the y value
-       // causes nasty repaint artefacts, which only disappear on a full
-       // repaint.
      }
    }
  
    {
      ViewportRanges ranges = av.getRanges();
  
 -    // if (Math.abs(scrollX) > ranges.getViewportWidth())
 -    // JAL-2836, 2836 temporarily removed wrapped fastpaint for release 2.10.3
 -    if (true)
 +    if (Math.abs(scrollX) > ranges.getViewportWidth())
      {
        /*
         * shift of more than one view width is 
      if (av.hasHiddenColumns())
      {
        firstVisibleColumn = alignment.getHiddenColumns()
-               .adjustForHiddenColumns(firstVisibleColumn);
+               .visibleToAbsoluteColumn(firstVisibleColumn);
        lastVisibleColumn = alignment.getHiddenColumns()
-               .adjustForHiddenColumns(lastVisibleColumn);
+               .visibleToAbsoluteColumn(lastVisibleColumn);
      }
  
      int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
                if (av.hasHiddenColumns())
                {
                  displayColumn = alignment.getHiddenColumns()
-                         .findColumnPosition(displayColumn);
+                         .absoluteToVisibleColumn(displayColumn);
                }
  
                /*
@@@ -67,6 -67,7 +67,7 @@@ import java.util.BitSet
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.Hashtable;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
@@@ -1740,8 -1741,12 +1741,12 @@@ public abstract class AlignmentViewpor
      if (alignment.getHiddenColumns() != null
              && alignment.getHiddenColumns().hasHiddenColumns())
      {
-       selection = alignment.getHiddenColumns()
-               .getVisibleSequenceStrings(start, end, seqs);
+       for (i = 0; i < iSize; i++)
+       {
+         Iterator<int[]> blocks = alignment.getHiddenColumns()
+                 .getVisContigsIterator(start, end + 1, false);
+         selection[i] = seqs[i].getSequenceStringFromIterator(blocks);
+       }
      }
      else
      {
        {
          if (start == 0)
          {
-           start = hidden.adjustForHiddenColumns(start);
+           start = hidden.visibleToAbsoluteColumn(start);
          }
  
-         end = hidden.getHiddenBoundaryRight(start);
+         end = hidden.getNextHiddenBoundary(false, start);
          if (start == end)
          {
            end = max;
  
        if (hidden != null && hidden.hasHiddenColumns())
        {
-         start = hidden.adjustForHiddenColumns(end);
-         start = hidden.getHiddenBoundaryLeft(start) + 1;
+         start = hidden.visibleToAbsoluteColumn(end);
+         start = hidden.getNextHiddenBoundary(true, start) + 1;
        }
      } while (end < max);
  
          AlignmentAnnotation clone = new AlignmentAnnotation(annot);
          if (selectedOnly && selectionGroup != null)
          {
-           alignment.getHiddenColumns().makeVisibleAnnotation(
+           clone.makeVisibleAnnotation(
                    selectionGroup.getStartRes(), selectionGroup.getEndRes(),
-                   clone);
+                   alignment.getHiddenColumns());
          }
          else
          {
-           alignment.getHiddenColumns().makeVisibleAnnotation(clone);
+           clone.makeVisibleAnnotation(alignment.getHiddenColumns());
          }
          ala.add(clone);
        }
      int lastSeq = alignment.getHeight() - 1;
      List<AlignedCodonFrame> seqMappings = null;
      for (int seqNo = ranges
 -            .getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
 +            .getStartSeq(); seqNo <= lastSeq; seqNo++, seqOffset++)
      {
        sequence = getAlignment().getSequenceAt(seqNo);
        if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
@@@ -50,8 -50,6 +50,8 @@@ public class OverviewDimensionsHideHidd
    public void updateViewportFromMouse(int mousex, int mousey,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      int xAsRes = getLeftXFromCentreX(mousex, hiddenCols);
      int yAsSeq = getTopYFromCentreY(mousey, hiddenSeqs);
  
    public void adjustViewportFromMouse(int mousex, int mousey,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      // calculate translation in pixel terms:
      // get mouse location in viewport coords, add translation in viewport
      // coords, and update viewport as usual
 -    int vpx = Math.round((float) mousex * alwidth / width);
 -    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
 +    int vpx = Math.round(mousex * widthRatio);
 +    int vpy = Math.round(mousey * heightRatio);
  
      updateViewportFromTopLeft(vpx + xdiff, vpy + ydiff, hiddenSeqs,
              hiddenCols);
  
    }
  
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh alwidth, alheight and width/height ratios
 +   */
    @Override
    protected void updateViewportFromTopLeft(int leftx, int topy,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
      int xAsRes = leftx;
      int yAsSeq = topy;
 -    resetAlignmentDims();
  
      if (xAsRes < 0)
      {
    public AlignmentColsCollectionI getColumns(AlignmentI al)
    {
      return new VisibleColsCollection(0,
-             ranges.getAbsoluteAlignmentWidth() - 1, al);
+             ranges.getAbsoluteAlignmentWidth() - 1, al.getHiddenColumns());
    }
  
    @Override
    {
      alwidth = ranges.getVisibleAlignmentWidth();
      alheight = ranges.getVisibleAlignmentHeight();
 +
 +    widthRatio = (float) alwidth / width;
 +    heightRatio = (float) alheight / sequencesHeight;
    }
  
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh widthRatio
 +   */
    @Override
    protected int getLeftXFromCentreX(int mousex, HiddenColumns hidden)
    {
 -    int vpx = Math.round((float) mousex * alwidth / width);
 +    int vpx = Math.round(mousex * widthRatio);
      return vpx - ranges.getViewportWidth() / 2;
    }
  
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh heightRatio
 +   */
    @Override
    protected int getTopYFromCentreY(int mousey, HiddenSequences hidden)
    {
 -    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
 +    int vpy = Math.round(mousey * heightRatio);
      return vpy - ranges.getViewportHeight() / 2;
    }
  
    public void setDragPoint(int x, int y, HiddenSequences hiddenSeqs,
            HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      // get alignment position of x and box (can get directly from vpranges) and
      // calculate difference between the positions
 -    int vpx = Math.round((float) x * alwidth / width);
 -    int vpy = Math.round((float) y * alheight / sequencesHeight);
 +    int vpx = Math.round(x * widthRatio);
 +    int vpy = Math.round(y * heightRatio);
  
      xdiff = ranges.getStartRes() - vpx;
      ydiff = ranges.getStartSeq() - vpy;
@@@ -72,15 -72,13 +72,15 @@@ public class OverviewDimensionsShowHidd
    public void updateViewportFromMouse(int mousex, int mousey,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      // convert mousex and mousey to alignment units as well as
      // translating to top left corner of viewport - this is an absolute position
      int xAsRes = getLeftXFromCentreX(mousex, hiddenCols);
      int yAsSeq = getTopYFromCentreY(mousey, hiddenSeqs);
  
      // convert to visible positions
-     int visXAsRes = hiddenCols.findColumnPosition(xAsRes);
+     int visXAsRes = hiddenCols.absoluteToVisibleColumn(xAsRes);
      yAsSeq = hiddenSeqs.adjustForHiddenSeqs(
              hiddenSeqs.findIndexWithoutHiddenSeqs(yAsSeq));
      yAsSeq = Math.max(yAsSeq, 0); // -1 if before first visible sequence
    public void adjustViewportFromMouse(int mousex, int mousey,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      // calculate translation in pixel terms:
      // get mouse location in viewport coords, add translation in viewport
      // coords,
      // convert back to pixel coords
-     int vpx = Math.round(mousex * widthRatio);
-     int visXAsRes = hiddenCols.findColumnPosition(vpx) + xdiff;
+     int vpx = Math.round((float) mousex * alwidth / width);
+     int visXAsRes = hiddenCols.absoluteToVisibleColumn(vpx) + xdiff;
  
 -    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
 +    int vpy = Math.round(mousey * heightRatio);
      int visYAsRes = hiddenSeqs.findIndexWithoutHiddenSeqs(vpy) + ydiff;
  
      // update viewport accordingly
      updateViewportFromTopLeft(visXAsRes, visYAsRes, hiddenSeqs, hiddenCols);
    }
  
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh alwidth, alheight and width/height ratios
 +   */
    @Override
    protected void updateViewportFromTopLeft(int leftx, int topy,
            HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
    {
      int visXAsRes = leftx;
      int visYAsSeq = topy;
 -    resetAlignmentDims();
  
      if (visXAsRes < 0)
      {
      int vpwidth = ranges.getViewportWidth();
  
      // check in case we went off the edge of the alignment
-     int visAlignWidth = hiddenCols.findColumnPosition(alwidth - 1);
+     int visAlignWidth = hiddenCols.absoluteToVisibleColumn(alwidth - 1);
      if (visXAsRes + vpwidth - 1 > visAlignWidth)
      {
        // went past the end of the alignment, adjust backwards
        // if last position was before the end of the alignment, need to update
        if (ranges.getEndRes() < visAlignWidth)
        {
-         visXAsRes = hiddenCols.findColumnPosition(hiddenCols
-                 .subtractVisibleColumns(vpwidth - 1, alwidth - 1));
+         visXAsRes = hiddenCols.absoluteToVisibleColumn(hiddenCols
+                 .offsetByVisibleColumns(-(vpwidth - 1), alwidth - 1));
        }
        else
        {
            HiddenColumns hiddenCols)
    {
      // work with absolute values of startRes and endRes
-     int startRes = hiddenCols.adjustForHiddenColumns(ranges.getStartRes());
-     int endRes = hiddenCols.adjustForHiddenColumns(ranges.getEndRes());
+     int startRes = hiddenCols.visibleToAbsoluteColumn(ranges.getStartRes());
+     int endRes = hiddenCols.visibleToAbsoluteColumn(ranges.getEndRes());
  
      // work with absolute values of startSeq and endSeq
      int startSeq = hiddenSeqs.adjustForHiddenSeqs(ranges.getStartSeq());
    {
      alwidth = ranges.getAbsoluteAlignmentWidth();
      alheight = ranges.getAbsoluteAlignmentHeight();
 +
 +    widthRatio = (float) alwidth / width;
 +    heightRatio = (float) alheight / sequencesHeight;
    }
  
 +
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh widthRatio
 +   */
    @Override
    protected int getLeftXFromCentreX(int mousex, HiddenColumns hidden)
    {
-     int vpx = Math.round(mousex * widthRatio);
-     return hidden.subtractVisibleColumns(ranges.getViewportWidth() / 2,
+     int vpx = Math.round((float) mousex * alwidth / width);
+     return hidden.offsetByVisibleColumns(-ranges.getViewportWidth() / 2,
              vpx);
    }
  
 +  /**
 +   * {@inheritDoc} Callers should have already called resetAlignmentDims to
 +   * refresh heightRatio
 +   */
    @Override
    protected int getTopYFromCentreY(int mousey, HiddenSequences hidden)
    {
 -    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
 +    int vpy = Math.round(mousey * heightRatio);
      return hidden.subtractVisibleRows(ranges.getViewportHeight() / 2, vpy);
    }
  
    public void setDragPoint(int x, int y, HiddenSequences hiddenSeqs,
            HiddenColumns hiddenCols)
    {
 +    resetAlignmentDims();
 +
      // get alignment position of x and box (can get directly from vpranges) and
      // calculate difference between the positions
 -    int vpx = Math.round((float) x * alwidth / width);
 -    int vpy = Math.round((float) y * alheight / sequencesHeight);
 +    int vpx = Math.round(x * widthRatio);
 +    int vpy = Math.round(y * heightRatio);
  
-     xdiff = ranges.getStartRes() - hiddenCols.findColumnPosition(vpx);
+     xdiff = ranges.getStartRes() - hiddenCols.absoluteToVisibleColumn(vpx);
      ydiff = ranges.getStartSeq()
              - hiddenSeqs.findIndexWithoutHiddenSeqs(vpy);
    }
@@@ -34,6 -34,7 +34,7 @@@ import jalview.io.DataSourceType
  import jalview.io.FileFormat;
  import jalview.io.FileFormatI;
  import jalview.io.FormatAdapter;
+ import jalview.util.Comparison;
  import jalview.util.MapList;
  
  import java.io.IOException;
@@@ -668,17 -669,6 +669,17 @@@ public class AlignmentTes
      // third found.. so
      assertFalse(iter.hasNext());
  
 +    // search for annotation on one sequence with a particular label - expect
 +    // one
 +    SequenceI sqfound;
 +    anns = al.findAnnotations(sqfound = al.getSequenceAt(1), null,
 +            "Secondary Structure");
 +    iter = anns.iterator();
 +    assertTrue(iter.hasNext());
 +    // expect reference to sequence 1 in the alignment
 +    assertTrue(sqfound == iter.next().sequenceRef);
 +    assertFalse(iter.hasNext());
 +
      // null on all parameters == find all annotations
      anns = al.findAnnotations(null, null, null);
      iter = anns.iterator();
      // hidden sequences, properties
    }
  
 +  /**
 +   * test that calcId == null on findOrCreate doesn't raise an NPE, and yields
 +   * an annotation with a null calcId
 +   * 
 +   */
 +  @Test(groups = "Functional")
 +  public void testFindOrCreateForNullCalcId()
 +  {
 +    SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-");
 +    AlignmentI alignment = new Alignment(new SequenceI[] { seq });
 +
 +    AlignmentAnnotation ala = alignment.findOrCreateAnnotation(
 +            "Temperature Factor", null, false, seq, null);
 +    assertNotNull(ala);
 +    assertEquals(seq, ala.sequenceRef);
 +    assertEquals("", ala.calcId);
 +  }
++
+   @Test(groups = "Functional")
+   public void testPropagateInsertions()
+   {
+     // create an alignment with no gaps - this will be the profile seq and other
+     // JPRED seqs
+     AlignmentGenerator gen = new AlignmentGenerator(false);
+     AlignmentI al = gen.generate(25, 10, 1234, 0, 0);
+     // get the profileseq
+     SequenceI profileseq = al.getSequenceAt(0);
+     SequenceI gappedseq = new Sequence(profileseq);
+     gappedseq.insertCharAt(5, al.getGapCharacter());
+     gappedseq.insertCharAt(6, al.getGapCharacter());
+     gappedseq.insertCharAt(7, al.getGapCharacter());
+     gappedseq.insertCharAt(8, al.getGapCharacter());
+     // force different kinds of padding
+     al.getSequenceAt(3).deleteChars(2, 23);
+     al.getSequenceAt(4).deleteChars(2, 27);
+     al.getSequenceAt(5).deleteChars(10, 27);
+     // create an alignment view with the gapped sequence
+     SequenceI[] seqs = new SequenceI[1];
+     seqs[0] = gappedseq;
+     AlignmentI newal = new Alignment(seqs);
+     HiddenColumns hidden = new HiddenColumns();
+     hidden.hideColumns(15, 17);
+     AlignmentView view = new AlignmentView(newal, hidden, null, true, false,
+             false);
+     // confirm that original contigs are as expected
+     Iterator<int[]> visible = hidden.getVisContigsIterator(0, 25, false);
+     int[] region = visible.next();
+     assertEquals("[0, 14]", Arrays.toString(region));
+     region = visible.next();
+     assertEquals("[18, 24]", Arrays.toString(region));
+     // propagate insertions
+     HiddenColumns result = al.propagateInsertions(profileseq, view);
+     // confirm that the contigs have changed to account for the gaps
+     visible = result.getVisContigsIterator(0, 25, false);
+     region = visible.next();
+     assertEquals("[0, 10]", Arrays.toString(region));
+     region = visible.next();
+     assertEquals("[14, 24]", Arrays.toString(region));
+     // confirm the alignment has been changed so that the other sequences have
+     // gaps inserted where the columns are hidden
+     assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[10]));
+     assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[11]));
+     assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[12]));
+     assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[13]));
+     assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[14]));
+   }
+   @Test(groups = "Functional")
+   public void testPropagateInsertionsOverlap()
+   {
+     // test propagateInsertions where gaps and hiddenColumns overlap
+     // create an alignment with no gaps - this will be the profile seq and other
+     // JPRED seqs
+     AlignmentGenerator gen = new AlignmentGenerator(false);
+     AlignmentI al = gen.generate(20, 10, 1234, 0, 0);
+     // get the profileseq
+     SequenceI profileseq = al.getSequenceAt(0);
+     SequenceI gappedseq = new Sequence(profileseq);
+     gappedseq.insertCharAt(5, al.getGapCharacter());
+     gappedseq.insertCharAt(6, al.getGapCharacter());
+     gappedseq.insertCharAt(7, al.getGapCharacter());
+     gappedseq.insertCharAt(8, al.getGapCharacter());
+     // create an alignment view with the gapped sequence
+     SequenceI[] seqs = new SequenceI[1];
+     seqs[0] = gappedseq;
+     AlignmentI newal = new Alignment(seqs);
+     // hide columns so that some overlap with the gaps
+     HiddenColumns hidden = new HiddenColumns();
+     hidden.hideColumns(7, 10);
+     AlignmentView view = new AlignmentView(newal, hidden, null, true, false,
+             false);
+     // confirm that original contigs are as expected
+     Iterator<int[]> visible = hidden.getVisContigsIterator(0, 20, false);
+     int[] region = visible.next();
+     assertEquals("[0, 6]", Arrays.toString(region));
+     region = visible.next();
+     assertEquals("[11, 19]", Arrays.toString(region));
+     assertFalse(visible.hasNext());
+     // propagate insertions
+     HiddenColumns result = al.propagateInsertions(profileseq, view);
+     // confirm that the contigs have changed to account for the gaps
+     visible = result.getVisContigsIterator(0, 20, false);
+     region = visible.next();
+     assertEquals("[0, 4]", Arrays.toString(region));
+     region = visible.next();
+     assertEquals("[7, 19]", Arrays.toString(region));
+     assertFalse(visible.hasNext());
+     // confirm the alignment has been changed so that the other sequences have
+     // gaps inserted where the columns are hidden
+     assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[4]));
+     assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[5]));
+     assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[6]));
+     assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[7]));
+   }
+   @Test(groups = { "Functional" })
+   public void testPadGaps()
+   {
+     SequenceI seq1 = new Sequence("seq1", "ABCDEF--");
+     SequenceI seq2 = new Sequence("seq2", "-JKLMNO--");
+     SequenceI seq3 = new Sequence("seq2", "-PQR");
+     AlignmentI a = new Alignment(new SequenceI[] { seq1, seq2, seq3 });
+     a.setGapCharacter('.'); // this replaces existing gaps
+     assertEquals("ABCDEF..", seq1.getSequenceAsString());
+     a.padGaps();
+     // trailing gaps are pruned, short sequences padded with gap character
+     assertEquals("ABCDEF.", seq1.getSequenceAsString());
+     assertEquals(".JKLMNO", seq2.getSequenceAsString());
+     assertEquals(".PQR...", seq3.getSequenceAsString());
+   }
  }