Merge branch 'develop' into feature/JAL-2759
authorkiramt <k.mourao@dundee.ac.uk>
Wed, 8 Nov 2017 16:06:14 +0000 (16:06 +0000)
committerkiramt <k.mourao@dundee.ac.uk>
Wed, 8 Nov 2017 16:06:14 +0000 (16:06 +0000)
Conflicts:
src/jalview/gui/SeqCanvas.java
src/jalview/renderer/ScaleRenderer.java
test/jalview/datamodel/SequenceTest.java

16 files changed:
1  2 
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AnnotationColumnChooser.java
src/jalview/appletgui/AnnotationLabels.java
src/jalview/appletgui/ScalePanel.java
src/jalview/datamodel/Sequence.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationColumnChooser.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/renderer/ScaleRenderer.java
src/jalview/util/MappingUtils.java
test/jalview/datamodel/SequenceTest.java
test/jalview/util/MappingUtilsTest.java

@@@ -343,7 -343,7 +343,7 @@@ public class AlignFrame extends Embmenu
      createAlignFrameWindow(embedded);
      validate();
      alignPanel.adjustAnnotationHeight();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
    }
  
    public AlignViewport getAlignViewport()
        {
          viewport.featureSettings.refreshTable();
        }
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        statusBar.setText(MessageManager
                .getString("label.successfully_added_features_alignment"));
      }
        break;
  
      }
-     alignPanel.paintAlignment(true);
+     // TODO: repaint flags set only if the keystroke warrants it
+     alignPanel.paintAlignment(true, true);
    }
  
    /**
      {
        applyAutoAnnotationSettings_actionPerformed();
      }
-     alignPanel.paintAlignment(true);
+     // TODO: repaint flags set only if warranted
+     alignPanel.paintAlignment(true, true);
    }
  
    /**
      else if (source == invertColSel)
      {
        viewport.invertColumnSelection();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(false, false);
        viewport.sendSelection();
      }
      else if (source == remove2LeftMenuItem)
      else if (source == showColumns)
      {
        viewport.showAllHiddenColumns();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        viewport.sendSelection();
      }
      else if (source == showSeqs)
      {
        viewport.showAllHiddenSeqs();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        // uncomment if we want to slave sequence selections in split frame
        // viewport.sendSelection();
      }
      else if (source == hideColumns)
      {
        viewport.hideSelectedColumns();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        viewport.sendSelection();
      }
      else if (source == hideSequences
              && viewport.getSelectionGroup() != null)
      {
        viewport.hideAllSelectedSeqs();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        // uncomment if we want to slave sequence selections in split frame
        // viewport.sendSelection();
      }
      else if (source == hideAllButSelection)
      {
        toggleHiddenRegions(false, false);
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        viewport.sendSelection();
      }
      else if (source == hideAllSelection)
        viewport.expandColSelection(sg, false);
        viewport.hideAllSelectedSeqs();
        viewport.hideSelectedColumns();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        viewport.sendSelection();
      }
      else if (source == showAllHidden)
      {
        viewport.showAllHiddenColumns();
        viewport.showAllHiddenSeqs();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
        viewport.sendSelection();
      }
      else if (source == showGroupConsensus)
      {
        System.exit(0);
      }
-     else
+     viewport = null;
+     if (alignPanel != null && alignPanel.overviewPanel != null)
      {
+       alignPanel.overviewPanel.dispose();
      }
-     viewport = null;
      alignPanel = null;
      this.dispose();
    }
      }
      viewport.getAlignment().moveSelectedSequencesByOne(sg,
              up ? null : viewport.getHiddenRepSequences(), up);
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
  
      /*
       * Also move cDNA/protein complement sequences
                viewport, complement);
        complement.getAlignment().moveSelectedSequencesByOne(mappedSelection,
                up ? null : complement.getHiddenRepSequences(), up);
-       getSplitFrame().getComplement(this).alignPanel.paintAlignment(true);
+       getSplitFrame().getComplement(this).alignPanel.paintAlignment(true,
+               false);
      }
    }
  
  
    static StringBuffer copiedSequences;
  
 -  static Vector<int[]> copiedHiddenColumns;
 +  static HiddenColumns copiedHiddenColumns;
  
    protected void copy_actionPerformed()
    {
  
      if (viewport.hasHiddenColumns() && viewport.getSelectionGroup() != null)
      {
 -      copiedHiddenColumns = new Vector<>(viewport.getAlignment()
 -              .getHiddenColumns().getHiddenColumnsCopy());
        int hiddenOffset = viewport.getSelectionGroup().getStartRes();
 -      for (int[] region : copiedHiddenColumns)
 -      {
 -        region[0] = region[0] - hiddenOffset;
 -        region[1] = region[1] - hiddenOffset;
 -      }
 +      int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
 +
 +      // create new HiddenColumns object with copy of hidden regions
 +      // between startRes and endRes, offset by startRes
 +      copiedHiddenColumns = new HiddenColumns(
 +              viewport.getAlignment().getHiddenColumns(), hiddenOffset,
 +              hiddenCutoff, hiddenOffset);
      }
      else
      {
          }
          AlignFrame af = new AlignFrame(new Alignment(newSeqs),
                  viewport.applet, newtitle, false);
 -        if (copiedHiddenColumns != null)
 -        {
 -          for (int i = 0; i < copiedHiddenColumns.size(); i++)
 -          {
 -            int[] region = copiedHiddenColumns.elementAt(i);
 -            af.viewport.hideColumns(region[0], region[1]);
 -          }
 -        }
 +        af.viewport.setHiddenColumns(copiedHiddenColumns);
  
          jalview.bin.JalviewLite.addFrame(af, newtitle, frameWidth,
                  frameHeight);
      {
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
      }
    }
  
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
      // updating.
-     alignPanel.paintAlignment(false);
+     alignPanel.paintAlignment(false, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
      viewport.sendSelection();
    }
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
      // updating.
-     alignPanel.paintAlignment(false);
+     alignPanel.paintAlignment(false, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
      viewport.sendSelection();
    }
    public void invertColSel_actionPerformed()
    {
      viewport.invertColumnSelection();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
      viewport.sendSelection();
    }
    {
      viewport.setShowJVSuffix(seqLimits.getState());
      alignPanel.fontChanged();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    protected void colourTextMenuItem_actionPerformed()
    {
      viewport.setColourText(colourTextMenuItem.getState());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    protected void displayNonconservedMenuItem_actionPerformed()
    {
      viewport.setShowUnconserved(displayNonconservedMenuItem.getState());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    protected void wrapMenuItem_actionPerformed()
      scaleAbove.setEnabled(wrapMenuItem.getState());
      scaleLeft.setEnabled(wrapMenuItem.getState());
      scaleRight.setEnabled(wrapMenuItem.getState());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    public void overviewMenuItem_actionPerformed()
    {
      viewport.setGlobalColourScheme(cs);
  
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
    }
  
    protected void modifyPID_actionPerformed()
  
      addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
              viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    public void sortIDMenuItem_actionPerformed()
      AlignmentSorter.sortByID(viewport.getAlignment());
      addHistoryItem(
              new OrderCommand("ID Sort", oldOrder, viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    public void sortLengthMenuItem_actionPerformed()
      AlignmentSorter.sortByLength(viewport.getAlignment());
      addHistoryItem(new OrderCommand("Length Sort", oldOrder,
              viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    public void sortGroupMenuItem_actionPerformed()
      AlignmentSorter.sortByGroup(viewport.getAlignment());
      addHistoryItem(new OrderCommand("Group Sort", oldOrder,
              viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
  
    }
  
            current.insertCharAt(Width - 1, viewport.getGapCharacter());
          }
        }
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(false, false);
      }
  
      if ((viewport.getSelectionGroup() != null
            current.insertCharAt(Width - 1, viewport.getGapCharacter());
          }
        }
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(false, false);
  
      }
  
      addHistoryItem(new OrderCommand(MessageManager
              .formatMessage("label.order_by_params", new String[]
              { title }), oldOrder, viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
        addHistoryItem(new OrderCommand(undoname, oldOrder,
                viewport.getAlignment()));
      }
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
      return true;
    }
  
      {
        // register the association(s) and quit, don't create any windows.
        if (StructureSelectionManager.getStructureSelectionManager(applet)
-               .setMapping(seqs, chains, pdb.getFile(), protocol) == null)
+               .setMapping(seqs, chains, pdb.getFile(), protocol, null) == null)
        {
          System.err.println("Failed to map " + pdb.getFile() + " ("
                  + protocol + ") to any sequences");
@@@ -46,6 -46,7 +46,6 @@@ import java.awt.event.MouseEvent
  import java.awt.event.MouseListener;
  import java.awt.event.TextEvent;
  import java.awt.event.TextListener;
 -import java.util.ArrayList;
  import java.util.Vector;
  
  //import javax.swing.JPanel;
@@@ -292,10 -293,20 +292,10 @@@ public class AnnotationColumnChooser ex
        {
          HiddenColumns oldHidden = av.getAnnotationColumnSelectionState()
                  .getOldHiddenColumns();
          av.getAlignment().setHiddenColumns(oldHidden);
        }
        av.sendSelection();
-       ap.paintAlignment(true);
+       ap.paintAlignment(true, true);
      }
  
    }
            sliderDragging = false;
            valueChanged(true);
          }
-         ap.paintAlignment(true);
+         ap.paintAlignment(true, true);
        }
      });
    }
      if (slider.isEnabled())
      {
        getCurrentAnnotation().threshold.value = slider.getValue() / 1000f;
-       updateView();
-       ap.paintAlignment(false);
+       updateView(); // this also calls paintAlignment(true,true)
      }
    }
  
      filterParams = null;
      av.setAnnotationColumnSelectionState(this);
      av.sendSelection();
-     ap.paintAlignment(true);
+     ap.paintAlignment(true, true);
    }
  
    public HiddenColumns getOldHiddenColumns()
@@@ -23,7 -23,6 +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;
@@@ -51,6 -50,7 +51,6 @@@ 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
      ap.annotationPanel.adjustPanelHeight();
      setSize(getSize().width, ap.annotationPanel.getSize().height);
      ap.validate();
-     ap.paintAlignment(true);
+     // TODO: only paint if we needed to
+     ap.paintAlignment(true, true);
    }
  
    boolean editLabelDescription(AlignmentAnnotation annotation)
                  {
                    ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
                  }
-                 ap.paintAlignment(true);
+                 ap.paintAlignment(true, true);
                }
              });
              popup.add(cbmi);
                  }
                }
              }
-             ap.paintAlignment(false);
+             ap.paintAlignment(false, false);
              PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
              ap.av.sendSelection();
            }
                sg.addSequence(aa[selectedRow].sequenceRef, false);
              }
              ap.av.setSelectionGroup(sg);
-             ap.paintAlignment(false);
+             ap.paintAlignment(false, false);
              PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
              ap.av.sendSelection();
            }
                      + "\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());
      }
    }
  
@@@ -42,7 -42,6 +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;
  
  public class ScalePanel extends Panel
        sg.setStartRes(min);
        sg.setEndRes(max);
      }
-     ap.paintAlignment(false);
+     ap.paintAlignment(false, false);
      av.sendSelection();
    }
  
          {
            av.showColumn(reveal[0]);
            reveal = null;
-           ap.paintAlignment(true);
+           ap.paintAlignment(true, true);
            av.sendSelection();
          }
        });
            {
              av.showAllHiddenColumns();
              reveal = null;
-             ap.paintAlignment(true);
+             ap.paintAlignment(true, true);
              av.sendSelection();
            }
          });
              av.setSelectionGroup(null);
            }
  
-           ap.paintAlignment(true);
+           ap.paintAlignment(true, true);
            av.sendSelection();
          }
        });
  
      if (!stretchingGroup)
      {
-       ap.paintAlignment(false);
+       ap.paintAlignment(false, false);
  
        return;
      }
      }
  
      stretchingGroup = false;
-     ap.paintAlignment(false);
+     ap.paintAlignment(false, false);
      av.sendSelection();
    }
  
      {
        stretchingGroup = true;
        cs.stretchGroup(res, sg, min, max);
-       ap.paintAlignment(false);
+       ap.paintAlignment(false, false);
      }
    }
  
        if (av.getShowHiddenMarkers())
        {
          int widthx = 1 + endx - startx;
 -        List<Integer> positions = hidden.findHiddenRegionPositions();
 -        for (int pos : positions)
 +        Iterator<Integer> it = hidden.getBoundedStartIterator(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);
          }
        }
      }
@@@ -38,8 -38,6 +38,6 @@@ import java.util.List
  import java.util.ListIterator;
  import java.util.Vector;
  
- import com.stevesoft.pat.Regex;
  import fr.orsay.lri.varna.models.rna.RNA;
  
  /**
   */
  public class Sequence extends ASequence implements SequenceI
  {
-   private static final Regex limitrx = new Regex(
-           "[/][0-9]{1,}[-][0-9]{1,}$");
-   private static final Regex endrx = new Regex("[0-9]{1,}$");
    SequenceI datasetSequence;
  
    String name;
@@@ -89,7 -82,7 +82,7 @@@
     */
    int index = -1;
  
-   private SequenceFeatures sequenceFeatureStore;
+   private SequenceFeaturesI sequenceFeatureStore;
  
    /*
     * A cursor holding the approximate current view position to the sequence,
      checkValidRange();
    }
  
+   /**
+    * If 'name' ends in /i-j, where i >= j > 0 are integers, extracts i and j as
+    * start and end respectively and removes the suffix from the name
+    */
    void parseId()
    {
      if (name == null)
                "POSSIBLE IMPLEMENTATION ERROR: null sequence name passed to constructor.");
        name = "";
      }
-     // Does sequence have the /start-end signature?
-     if (limitrx.search(name))
+     int slashPos = name.lastIndexOf('/');
+     if (slashPos > -1 && slashPos < name.length() - 1)
      {
-       name = limitrx.left();
-       endrx.search(limitrx.stringMatched());
-       setStart(Integer.parseInt(limitrx.stringMatched().substring(1,
-               endrx.matchedFrom() - 1)));
-       setEnd(Integer.parseInt(endrx.stringMatched()));
+       String suffix = name.substring(slashPos + 1);
+       String[] range = suffix.split("-");
+       if (range.length == 2)
+       {
+         try
+         {
+           int from = Integer.valueOf(range[0]);
+           int to = Integer.valueOf(range[1]);
+           if (from > 0 && to >= from)
+           {
+             name = name.substring(0, slashPos);
+             setStart(from);
+             setEnd(to);
+             checkValidRange();
+           }
+         } catch (NumberFormatException e)
+         {
+           // leave name unchanged if suffix is invalid
+         }
+       }
      }
    }
  
+   /**
+    * Ensures that 'end' is not before the end of the sequence, that is,
+    * (end-start+1) is at least as long as the count of ungapped positions. Note
+    * that end is permitted to be beyond the end of the sequence data.
+    */
    void checkValidRange()
    {
      // Note: JAL-774 :
        int endRes = 0;
        for (int j = 0; j < sequence.length; j++)
        {
-         if (!jalview.util.Comparison.isGap(sequence[j]))
+         if (!Comparison.isGap(sequence[j]))
          {
            endRes++;
          }
    {
      if (pdbIds == null)
      {
 -      pdbIds = new Vector<PDBEntry>();
 +      pdbIds = new Vector<>();
        pdbIds.add(entry);
        return true;
      }
    }
  
    /**
-    * DOCUMENT ME!
+    * Sets the sequence name. If the name ends in /start-end, then the start-end
+    * values are parsed out and set, and the suffix is removed from the name.
     * 
-    * @param name
-    *          DOCUMENT ME!
+    * @param theName
     */
    @Override
-   public void setName(String name)
+   public void setName(String theName)
    {
-     this.name = name;
+     this.name = theName;
      this.parseId();
    }
  
      return map;
    }
  
 +  /**
 +   * Build a bitset corresponding to sequence gaps
 +   * 
 +   * @return a BitSet where set values correspond to gaps in the sequence
 +   */
 +  @Override
 +  public BitSet gapBitset()
 +  {
 +    BitSet gaps = new BitSet(sequence.length);
 +    int j = 0;
 +    while (j < sequence.length)
 +    {
 +      if (jalview.util.Comparison.isGap(sequence[j]))
 +      {
 +        gaps.set(j);
 +      }
 +      j++;
 +    }
 +    return gaps;
 +  }
 +
    @Override
    public int[] findPositionMap()
    {
    @Override
    public List<int[]> getInsertions()
    {
 -    ArrayList<int[]> map = new ArrayList<int[]>();
 +    ArrayList<int[]> map = new ArrayList<>();
      int lastj = -1, j = 0;
      int pos = start;
      int seqlen = sequence.length;
    {
      if (this.annotation == null)
      {
 -      this.annotation = new Vector<AlignmentAnnotation>();
 +      this.annotation = new Vector<>();
      }
      if (!this.annotation.contains(annotation))
      {
        return null;
      }
  
 -    Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
 +    Vector<AlignmentAnnotation> subset = new Vector<>();
      Enumeration<AlignmentAnnotation> e = annotation.elements();
      while (e.hasMoreElements())
      {
    public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
            String label)
    {
 -    List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
 +    List<AlignmentAnnotation> result = new ArrayList<>();
      if (this.annotation != null)
      {
        for (AlignmentAnnotation ann : annotation)
      }
      synchronized (dbrefs)
      {
 -      List<DBRefEntry> primaries = new ArrayList<DBRefEntry>();
 +      List<DBRefEntry> primaries = new ArrayList<>();
        DBRefEntry[] tmp = new DBRefEntry[1];
        for (DBRefEntry ref : dbrefs)
        {
       * and we may have included adjacent or enclosing features;
       * remove any that are not enclosing, non-contact features
       */
-     if (endPos > this.end || Comparison.isGap(sequence[toColumn - 1]))
+     boolean endColumnIsGapped = toColumn > 0 && toColumn <= sequence.length
+             && Comparison.isGap(sequence[toColumn - 1]);
+     if (endPos > this.end || endColumnIsGapped)
      {
        ListIterator<SequenceFeature> it = result.listIterator();
        while (it.hasNext())
@@@ -163,8 -163,6 +163,6 @@@ public class AlignFrame extends GAlignF
  
    AlignViewport viewport;
  
-   ViewportRanges vpRanges;
    public AlignViewControllerI avc;
  
    List<AlignmentPanel> alignPanels = new ArrayList<>();
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
      }
  
-     vpRanges = viewport.getRanges();
      avc = new jalview.controller.AlignViewController(this, viewport,
              alignPanel);
      if (viewport.getAlignmentConservationAnnotation() == null)
                    { (viewport.cursorMode ? "on" : "off") }));
            if (viewport.cursorMode)
            {
-             alignPanel.getSeqPanel().seqCanvas.cursorX = vpRanges
+             ViewportRanges ranges = viewport.getRanges();
+             alignPanel.getSeqPanel().seqCanvas.cursorX = ranges
                      .getStartRes();
-             alignPanel.getSeqPanel().seqCanvas.cursorY = vpRanges
+             alignPanel.getSeqPanel().seqCanvas.cursorY = ranges
                      .getStartSeq();
            }
            alignPanel.getSeqPanel().seqCanvas.repaint();
            break;
          }
          case KeyEvent.VK_PAGE_UP:
-           vpRanges.pageUp();
+           viewport.getRanges().pageUp();
            break;
          case KeyEvent.VK_PAGE_DOWN:
-           vpRanges.pageDown();
+           viewport.getRanges().pageDown();
            break;
          }
        }
      }
      viewport.getAlignment().moveSelectedSequencesByOne(sg,
              viewport.getHiddenRepSequences(), up);
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    synchronized void slideSequences(boolean right, int size)
        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,
        {
  
          // propagate alignment changed.
-         vpRanges.setEndSeq(alignment.getHeight());
+         viewport.getRanges().setEndSeq(alignment.getHeight());
          if (annotationAdded)
          {
            // Duplicate sequence annotation in all views.
          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
      {
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
      }
    }
  
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
      // updating.
-     alignPanel.paintAlignment(false);
+     alignPanel.paintAlignment(false, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
    }
  
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
      // updating.
-     alignPanel.paintAlignment(false);
+     alignPanel.paintAlignment(false, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
      viewport.sendSelection();
    }
      // alignPanel to decide if overview needs
      // updating.
  
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
      PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
      viewport.sendSelection();
    }
    public void invertColSel_actionPerformed(ActionEvent e)
    {
      viewport.invertColumnSelection();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
      viewport.sendSelection();
    }
  
        {
          trimRegion = new TrimRegionCommand("Remove Left", true, seqs,
                  column, viewport.getAlignment());
-         vpRanges.setStartRes(0);
+         viewport.getRanges().setStartRes(0);
        }
        else
        {
      // This is to maintain viewport position on first residue
      // of first sequence
      SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-     int startRes = seq.findPosition(vpRanges.getStartRes());
+     ViewportRanges ranges = viewport.getRanges();
+     int startRes = seq.findPosition(ranges.getStartRes());
      // ShiftList shifts;
      // viewport.getAlignment().removeGaps(shifts=new ShiftList());
      // edit.alColumnChanges=shifts.getInverse();
      // if (viewport.hasHiddenColumns)
      // viewport.getColumnSelection().compensateForEdits(shifts);
-     vpRanges.setStartRes(seq.findIndex(startRes) - 1);
+     ranges.setStartRes(seq.findIndex(startRes) - 1);
      viewport.firePropertyChange("alignment", null,
              viewport.getAlignment().getSequences());
  
      // This is to maintain viewport position on first residue
      // of first sequence
      SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-     int startRes = seq.findPosition(vpRanges.getStartRes());
+     int startRes = seq.findPosition(viewport.getRanges().getStartRes());
  
      addHistoryItem(new RemoveGapsCommand("Remove Gaps", seqs, start, end,
              viewport.getAlignment()));
  
-     vpRanges.setStartRes(seq.findIndex(startRes) - 1);
+     viewport.getRanges().setStartRes(seq.findIndex(startRes) - 1);
  
      viewport.firePropertyChange("alignment", null,
              viewport.getAlignment().getSequences());
      /*
       * Create a new AlignmentPanel (with its own, new Viewport)
       */
-     AlignmentPanel newap = new Jalview2XML().copyAlignPanel(alignPanel,
-             true);
+     AlignmentPanel newap = new Jalview2XML().copyAlignPanel(alignPanel);
      if (!copyAnnotation)
      {
        /*
  
      alignPanel.getIdPanel().getIdCanvas()
              .setPreferredSize(alignPanel.calculateIdWidth());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    @Override
    public void idRightAlign_actionPerformed(ActionEvent e)
    {
      viewport.setRightAlignIds(idRightAlign.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    @Override
    public void centreColumnLabels_actionPerformed(ActionEvent e)
    {
      viewport.setCentreColumnLabels(centreColumnLabelsMenuItem.getState());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /*
    protected void colourTextMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setColourText(colourTextMenuItem.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /**
    public void showAllColumns_actionPerformed(ActionEvent e)
    {
      viewport.showAllHiddenColumns();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
  
      viewport.expandColSelection(sg, false);
      viewport.hideAllSelectedSeqs();
      viewport.hideSelectedColumns();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
  
    {
      viewport.showAllHiddenColumns();
      viewport.showAllHiddenSeqs();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
  
    public void hideSelColumns_actionPerformed(ActionEvent e)
    {
      viewport.hideSelectedColumns();
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
  
    protected void scaleAbove_actionPerformed(ActionEvent e)
    {
      viewport.setScaleAboveWrapped(scaleAbove.isSelected());
-     alignPanel.paintAlignment(true);
+     // TODO: do we actually need to update overview for scale above change ?
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
    protected void scaleLeft_actionPerformed(ActionEvent e)
    {
      viewport.setScaleLeftWrapped(scaleLeft.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
    protected void scaleRight_actionPerformed(ActionEvent e)
    {
      viewport.setScaleRightWrapped(scaleRight.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
    public void viewBoxesMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setShowBoxes(viewBoxesMenuItem.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /**
    public void viewTextMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setShowText(viewTextMenuItem.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /**
    protected void renderGapsMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setRenderGaps(renderGapsMenuItem.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    public FeatureSettings featureSettings;
    public void showSeqFeatures_actionPerformed(ActionEvent evt)
    {
      viewport.setShowSequenceFeatures(showSeqFeatures.isSelected());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
    }
  
    /**
  
      viewport.setGlobalColourScheme(cs);
  
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, true);
    }
  
    /**
              viewport.getAlignment().getSequenceAt(0));
      addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
              viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
      AlignmentSorter.sortByID(viewport.getAlignment());
      addHistoryItem(
              new OrderCommand("ID Sort", oldOrder, viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
      AlignmentSorter.sortByLength(viewport.getAlignment());
      addHistoryItem(new OrderCommand("Length Sort", oldOrder,
              viewport.getAlignment()));
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
      addHistoryItem(new OrderCommand("Group Sort", oldOrder,
              viewport.getAlignment()));
  
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
    }
  
    /**
          addHistoryItem(new OrderCommand(order.getName(), oldOrder,
                  viewport.getAlignment()));
  
-         alignPanel.paintAlignment(true);
+         alignPanel.paintAlignment(true, false);
        }
      });
    }
                  viewport.getAlignment());// ,viewport.getSelectionGroup());
          addHistoryItem(new OrderCommand("Sort by " + scoreLabel, oldOrder,
                  viewport.getAlignment()));
-         alignPanel.paintAlignment(true);
+         alignPanel.paintAlignment(true, false);
        }
      });
    }
        addHistoryItem(new OrderCommand(undoname, oldOrder,
                viewport.getAlignment()));
      }
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(true, false);
      return true;
    }
  
                        assocfiles++;
                      }
                    }
-                   alignPanel.paintAlignment(true);
+                   // TODO: do we need to update overview ? only if features are
+                   // shown I guess
+                   alignPanel.paintAlignment(true, false);
                  }
                }
              }
            {
              if (parseFeaturesFile(file, sourceType))
              {
-               alignPanel.paintAlignment(true);
+               alignPanel.paintAlignment(true, true);
              }
            }
            else
          alignPanel.adjustAnnotationHeight();
          viewport.updateSequenceIdColours();
          buildSortByAnnotationScoresMenu();
-         alignPanel.paintAlignment(true);
+         alignPanel.paintAlignment(true, true);
        }
      } catch (Exception ex)
      {
    protected void showUnconservedMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setShowUnconserved(showNonconservedMenuItem.getState());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /*
      {
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
      }
    }
  
        viewport.getAlignment().setSeqrep(null);
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
-       alignPanel.paintAlignment(true);
+       alignPanel.paintAlignment(true, true);
      }
    }
  
      this.alignPanel.av.setSortAnnotationsBy(getAnnotationSortOrder());
      this.alignPanel.av
              .setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
-     alignPanel.paintAlignment(true);
+     alignPanel.paintAlignment(false, false);
    }
  
    /**
@@@ -60,7 -60,6 +60,7 @@@ import java.awt.FontMetrics
  import java.awt.Rectangle;
  import java.util.ArrayList;
  import java.util.Hashtable;
 +import java.util.Iterator;
  import java.util.List;
  import java.util.Vector;
  
@@@ -77,8 -76,6 +77,6 @@@ public class AlignViewport extends Alig
  {
    Font font;
  
-   TreeModel currentTree = null;
    boolean cursorMode = false;
  
    boolean antiAlias = false;
    }
  
    /**
-    * DOCUMENT ME!
-    * 
-    * @param tree
-    *          DOCUMENT ME!
-    */
-   public void setCurrentTree(TreeModel tree)
-   {
-     currentTree = tree;
-   }
-   /**
-    * DOCUMENT ME!
-    * 
-    * @return DOCUMENT ME!
-    */
-   public TreeModel getCurrentTree()
-   {
-     return currentTree;
-   }
-   /**
     * returns the visible column regions of the alignment
     * 
     * @param selectedRegionOnly
     *          area
     * @return
     */
 -  public int[] getViewAsVisibleContigs(boolean selectedRegionOnly)
 +  public Iterator<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly)
    {
 -    int[] viscontigs = null;
 -    int start = 0, end = 0;
 +    int start = 0;
 +    int end = 0;
      if (selectedRegionOnly && selectionGroup != null)
      {
        start = selectionGroup.getStartRes();
      {
        end = alignment.getWidth();
      }
 -    viscontigs = alignment.getHiddenColumns().getVisibleContigs(start, end);
 -    return viscontigs;
 +    return (alignment.getHiddenColumns().getVisContigsIterator(start, end));
    }
  
    /**
     */
    public SequenceI[][] collateForPDB(PDBEntry[] pdbEntries)
    {
 -    List<SequenceI[]> seqvectors = new ArrayList<SequenceI[]>();
 +    List<SequenceI[]> seqvectors = new ArrayList<>();
      for (PDBEntry pdb : pdbEntries)
      {
 -      List<SequenceI> choosenSeqs = new ArrayList<SequenceI>();
 +      List<SequenceI> choosenSeqs = new ArrayList<>();
        for (SequenceI sq : alignment.getSequences())
        {
          Vector<PDBEntry> pdbRefEntries = sq.getDatasetSequence()
      return validCharWidth;
    }
  
 -  private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<String, AutoCalcSetting>();
 +  private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<>();
  
    public AutoCalcSetting getCalcIdSettingsFor(String calcId)
    {
      }
      fr.setTransparency(featureSettings.getTransparency());
    }
  }
@@@ -36,6 -36,7 +36,6 @@@ import java.awt.event.ActionListener
  import java.awt.event.ItemEvent;
  import java.awt.event.ItemListener;
  import java.awt.event.KeyEvent;
 -import java.util.ArrayList;
  
  import javax.swing.ButtonGroup;
  import javax.swing.JCheckBox;
@@@ -240,10 -241,20 +240,10 @@@ public class AnnotationColumnChooser ex
        {
          HiddenColumns oldHidden = av.getAnnotationColumnSelectionState()
                  .getOldHiddenColumns();
          av.getAlignment().setHiddenColumns(oldHidden);
        }
        av.sendSelection();
-       ap.paintAlignment(true);
+       ap.paintAlignment(true, true);
      }
    }
  
        updateView();
        propagateSeqAssociatedThreshold(updateAllAnnotation,
                getCurrentAnnotation());
-       ap.paintAlignment(false);
+       ap.paintAlignment(false, false);
      }
    }
  
      av.getColumnSelection().filterAnnotations(
              getCurrentAnnotation().annotations, filterParams);
  
-     if (getActionOption() == ACTION_OPTION_HIDE)
+     boolean hideCols = getActionOption() == ACTION_OPTION_HIDE;
+     if (hideCols)
      {
        av.hideSelectedColumns();
      }
  
      filterParams = null;
      av.setAnnotationColumnSelectionState(this);
-     ap.paintAlignment(true);
+     // only update overview and structures if columns were hidden
+     ap.paintAlignment(hideCols, hideCols);
    }
  
    public HiddenColumns getOldHiddenColumns()
@@@ -24,7 -24,6 +24,7 @@@ 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;
@@@ -50,6 -49,7 +50,6 @@@ 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.regex.Pattern;
@@@ -705,7 -705,7 +705,7 @@@ public class AnnotationLabels extends J
          d = ap.annotationSpaceFillerHolder.getPreferredSize();
          ap.annotationSpaceFillerHolder
                  .setPreferredSize(new Dimension(d.width, d.height - dif));
-         ap.paintAlignment(true);
+         ap.paintAlignment(true, false);
        }
  
        ap.addNotify();
                }
              }
  
-             ap.paintAlignment(false);
+             ap.paintAlignment(false, false);
              PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
              ap.av.sendSelection();
            }
                sg.addSequence(aa[selectedRow].sequenceRef, false);
              }
              ap.av.setSelectionGroup(sg);
-             ap.paintAlignment(false);
+             ap.paintAlignment(false, false);
              PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
              ap.av.sendSelection();
            }
      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
@@@ -216,34 -216,6 +216,6 @@@ public class Jalview2XM
      }
    }
  
-   void clearSeqRefs()
-   {
-     if (_cleartables)
-     {
-       if (seqRefIds != null)
-       {
-         seqRefIds.clear();
-       }
-       if (seqsToIds != null)
-       {
-         seqsToIds.clear();
-       }
-       if (incompleteSeqs != null)
-       {
-         incompleteSeqs.clear();
-       }
-       // seqRefIds = null;
-       // seqsToIds = null;
-     }
-     else
-     {
-       // do nothing
-       warn("clearSeqRefs called when _cleartables was not set. Doing nothing.");
-       // seqRefIds = new Hashtable();
-       // seqsToIds = new IdentityHashMap();
-     }
-   }
    void initSeqRefs()
    {
      if (seqsToIds == null)
  
      // SAVE TREES
      // /////////////////////////////////
-     if (!storeDS && av.currentTree != null)
+     if (!storeDS && av.getCurrentTree() != null)
      {
        // FIND ANY ASSOCIATED TREES
        // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
              {
                Tree tree = new Tree();
                tree.setTitle(tp.getTitle());
-               tree.setCurrentTree((av.currentTree == tp.getTree()));
+               tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
                tree.setNewick(tp.getTree().print());
                tree.setThreshold(tp.treeCanvas.threshold);
  
          }
          else
          {
 -          ArrayList<int[]> hiddenRegions = hidden.getHiddenColumnsCopy();
 -          for (int[] region : hiddenRegions)
 +          Iterator<int[]> hiddenRegions = hidden.iterator();
 +          while (hiddenRegions.hasNext())
            {
 +            int[] region = hiddenRegions.next();
              HiddenColumns hc = new HiddenColumns();
              hc.setStart(region[0]);
              hc.setEnd(region[1]);
  
        jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
        af = loadJalviewAlign(jprovider);
+       af.setMenusForViewport();
  
      } catch (MalformedURLException e)
      {
        StructureData filedat = oldFiles.get(id);
        String pdbFile = filedat.getFilePath();
        SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
-       binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE);
+       binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
+               null);
        binding.addSequenceForStructFile(pdbFile, seq);
      }
      // and add the AlignmentPanel's reference to the view panel
  
    }
  
-   public jalview.gui.AlignmentPanel copyAlignPanel(AlignmentPanel ap,
-           boolean keepSeqRefs)
+   /**
+    * Provides a 'copy' of an alignment view (on action New View) by 'saving' the
+    * view as XML (but not to file), and then reloading it
+    * 
+    * @param ap
+    * @return
+    */
+   public AlignmentPanel copyAlignPanel(AlignmentPanel ap)
    {
      initSeqRefs();
      JalviewModel jm = saveState(ap, null, null, null);
  
-     if (!keepSeqRefs)
-     {
-       clearSeqRefs();
-       jm.getJalviewModelSequence().getViewport(0).setSequenceSetId(null);
-     }
-     else
-     {
-       uniqueSetSuffix = "";
-       jm.getJalviewModelSequence().getViewport(0).setId(null); // we don't
-       // overwrite the
-       // view we just
-       // copied
-     }
+     uniqueSetSuffix = "";
+     jm.getJalviewModelSequence().getViewport(0).setId(null);
+     // we don't overwrite the view we just copied
      if (this.frefedSequence == null)
      {
-       frefedSequence = new Vector();
+       frefedSequence = new Vector<SeqFref>();
      }
  
      viewportsAdded.clear();
      return af.alignPanel;
    }
  
-   /**
-    * flag indicating if hashtables should be cleared on finalization TODO this
-    * flag may not be necessary
-    */
-   private final boolean _cleartables = true;
    private Hashtable jvids2vobj;
  
-   /*
-    * (non-Javadoc)
-    * 
-    * @see java.lang.Object#finalize()
-    */
-   @Override
-   protected void finalize() throws Throwable
-   {
-     // really make sure we have no buried refs left.
-     if (_cleartables)
-     {
-       clearSeqRefs();
-     }
-     this.seqRefIds = null;
-     this.seqsToIds = null;
-     super.finalize();
-   }
    private void warn(String msg)
    {
      warn(msg, null);
@@@ -42,7 -42,6 +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;
@@@ -169,13 -168,13 +169,13 @@@ public class ScalePanel extends JPane
          {
            av.showColumn(reveal[0]);
            reveal = null;
-           ap.paintAlignment(true);
+           ap.paintAlignment(true, true);
            av.sendSelection();
          }
        });
        pop.add(item);
  
 -      if (av.getAlignment().getHiddenColumns().hasHiddenColumns())
 +      if (av.getAlignment().getHiddenColumns().hasManyHiddenColumns())
        {
          item = new JMenuItem(MessageManager.getString("action.reveal_all"));
          item.addActionListener(new ActionListener()
            {
              av.showAllHiddenColumns();
              reveal = null;
-             ap.paintAlignment(true);
+             ap.paintAlignment(true, true);
              av.sendSelection();
            }
          });
              av.setSelectionGroup(null);
            }
  
-           ap.paintAlignment(true);
+           ap.paintAlignment(true, true);
            av.sendSelection();
          }
        });
        sg.setEndRes(max);
      }
      av.setSelectionGroup(sg);
-     ap.paintAlignment(false);
+     ap.paintAlignment(false, false);
      av.sendSelection();
    }
  
        }
        else
        {
-         ap.paintAlignment(false);
+         ap.paintAlignment(false, false);
        }
        return;
      }
        }
      }
      stretchingGroup = false;
-     ap.paintAlignment(false);
+     ap.paintAlignment(false, false);
      av.sendSelection();
    }
  
      {
        stretchingGroup = true;
        cs.stretchGroup(res, sg, min, max);
-       ap.paintAlignment(false);
+       ap.paintAlignment(false, false);
      }
    }
  
  
        if (av.getShowHiddenMarkers())
        {
 -        List<Integer> positions = hidden.findHiddenRegionPositions();
 -        for (int pos : positions)
 +        Iterator<Integer> it = hidden.getBoundedStartIterator(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);
          }
        }
      }
@@@ -22,12 -22,12 +22,13 @@@ package jalview.gui
  
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.HiddenColumns;
 +import jalview.datamodel.HiddenColumns.VisibleBlocksVisBoundsIterator;
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.renderer.ScaleRenderer;
  import jalview.renderer.ScaleRenderer.ScaleMark;
+ import jalview.util.Comparison;
  import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
  
@@@ -42,58 -42,61 +43,62 @@@ 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;
  
  /**
-  * DOCUMENT ME!
+  * The Swing component on which the alignment sequences, and annotations (if
+  * shown), are drawn. This includes scales above, left and right (if shown) in
+  * Wrapped mode, but not the scale above in Unwrapped mode.
   * 
   */
  public class SeqCanvas extends JComponent implements ViewportListenerI
  {
-   private static String ZEROS = "0000000000";
+   private static final String ZEROS = "0000000000";
  
    final FeatureRenderer fr;
  
-   final SequenceRenderer seqRdr;
    BufferedImage img;
  
-   Graphics2D gg;
    AlignViewport av;
  
-   boolean fastPaint = false;
+   int cursorX = 0;
  
-   int labelWidthWest;
+   int cursorY = 0;
  
-   int labelWidthEast;
+   private final SequenceRenderer seqRdr;
  
-   int cursorX = 0;
+   private boolean fastPaint = false;
  
-   int cursorY = 0;
+   private boolean fastpainting = false;
  
-   int charHeight = 0;
+   private AnnotationPanel annotations;
  
-   int charWidth = 0;
+   /*
+    * measurements for drawing a wrapped alignment
+    */
+   private int labelWidthEast; // label right width in pixels if shown
+   private int labelWidthWest; // label left width in pixels if shown
  
-   boolean fastpainting = false;
+   private int wrappedSpaceAboveAlignment; // gap between widths
  
-   AnnotationPanel annotations;
+   private int wrappedRepeatHeightPx; // height in pixels of wrapped width
+   private int wrappedVisibleWidths; // number of wrapped widths displayed
+   private Graphics2D gg;
  
    /**
     * Creates a new SeqCanvas object.
     * 
-    * @param av
-    *          DOCUMENT ME!
+    * @param ap
     */
    public SeqCanvas(AlignmentPanel ap)
    {
      this.av = ap.av;
-     updateViewport();
      fr = new FeatureRenderer(ap);
      seqRdr = new SequenceRenderer(av);
      setLayout(new BorderLayout());
      return fr;
    }
  
-   private void updateViewport()
-   {
-     charHeight = av.getCharHeight();
-     charWidth = av.getCharWidth();
-   }
    /**
-    * DOCUMENT ME!
+    * Draws the scale above a region of a wrapped alignment, consisting of a
+    * column number every major interval (10 columns).
     * 
     * @param g
-    *          DOCUMENT ME!
+    *          the graphics context to draw on, positioned at the start (bottom
+    *          left) of the line on which to draw any scale marks
     * @param startx
-    *          DOCUMENT ME!
+    *          start alignment column (0..)
     * @param endx
-    *          DOCUMENT ME!
+    *          end alignment column (0..)
     * @param ypos
-    *          DOCUMENT ME!
+    *          y offset to draw at
     */
    private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
    {
-     updateViewport();
-     for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
-             endx))
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
+     /*
+      * white fill the scale space (for the fastPaint case)
+      */
+     g.setColor(Color.white);
+     g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
+             charHeight * 3 / 2 + 2);
+     g.setColor(Color.black);
+     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
+             endx);
+     for (ScaleMark mark : marks)
      {
        int mpos = mark.column; // (i - startx - 1)
        if (mpos < 0)
          {
            g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
          }
-         g.drawLine((mpos * charWidth) + (charWidth / 2),
-                 (ypos + 2) - (charHeight / 2),
-                 (mpos * charWidth) + (charWidth / 2), ypos - 2);
+         /*
+          * draw a tick mark below the column number, centred on the column;
+          * height of tick mark is 4 pixels less than half a character
+          */
+         int xpos = (mpos * charWidth) + (charWidth / 2);
+         g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
        }
      }
    }
  
    /**
-    * DOCUMENT ME!
+    * Draw the scale to the left or right of a wrapped alignment
     * 
     * @param g
-    *          DOCUMENT ME!
+    *          graphics context, positioned at the start of the scale to be drawn
     * @param startx
-    *          DOCUMENT ME!
+    *          first column of wrapped width (0.. excluding any hidden columns)
     * @param endx
-    *          DOCUMENT ME!
+    *          last column of wrapped width (0.. excluding any hidden columns)
     * @param ypos
-    *          DOCUMENT ME!
+    *          vertical offset at which to begin the scale
+    * @param left
+    *          if true, scale is left of residues, if false, scale is right
     */
-   void drawWestScale(Graphics g, int startx, int endx, int ypos)
+   void drawVerticalScale(Graphics g, final int startx, final int endx,
+           final int ypos, final boolean left)
    {
-     FontMetrics fm = getFontMetrics(av.getFont());
-     ypos += charHeight;
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
  
-     if (av.hasHiddenColumns())
-     {
-       startx = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(startx);
-       endx = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(endx);
-     }
+     int yPos = ypos + charHeight;
+     int startX = startx;
+     int endX = endx;
  
-     int maxwidth = av.getAlignment().getWidth();
      if (av.hasHiddenColumns())
      {
-       maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth) - 1;
+       HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
+       startX = hiddenColumns.adjustForHiddenColumns(startx);
+       endX = hiddenColumns.adjustForHiddenColumns(endx);
      }
+     FontMetrics fm = getFontMetrics(av.getFont());
  
      for (int i = 0; i < av.getAlignment().getHeight(); i++)
      {
        SequenceI seq = av.getAlignment().getSequenceAt(i);
-       int index = startx;
-       int value = -1;
  
-       while (index < endx)
+       /*
+        * find sequence position of first non-gapped position -
+        * to the right if scale left, to the left if scale right
+        */
+       int index = left ? startX : endX;
+       int value = -1;
+       while (index >= startX && index <= endX)
        {
-         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
+         if (!Comparison.isGap(seq.getCharAt(index)))
+         {
+           value = seq.findPosition(index);
+           break;
+         }
+         if (left)
          {
            index++;
-           continue;
          }
-         value = av.getAlignment().getSequenceAt(i).findPosition(index);
-         break;
-       }
-       if (value != -1)
-       {
-         int x = labelWidthWest - fm.stringWidth(String.valueOf(value))
-                 - charWidth / 2;
-         g.drawString(value + "", x,
-                 (ypos + (i * charHeight)) - (charHeight / 5));
-       }
-     }
-   }
-   /**
-    * DOCUMENT ME!
-    * 
-    * @param g
-    *          DOCUMENT ME!
-    * @param startx
-    *          DOCUMENT ME!
-    * @param endx
-    *          DOCUMENT ME!
-    * @param ypos
-    *          DOCUMENT ME!
-    */
-   void drawEastScale(Graphics g, int startx, int endx, int ypos)
-   {
-     ypos += charHeight;
-     if (av.hasHiddenColumns())
-     {
-       endx = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(endx);
-     }
-     SequenceI seq;
-     // EAST SCALE
-     for (int i = 0; i < av.getAlignment().getHeight(); i++)
-     {
-       seq = av.getAlignment().getSequenceAt(i);
-       int index = endx;
-       int value = -1;
-       while (index > startx)
-       {
-         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
+         else
          {
            index--;
-           continue;
          }
-         value = seq.findPosition(index);
-         break;
        }
  
+       /*
+        * white fill the space for the scale
+        */
+       g.setColor(Color.white);
+       int y = (yPos + (i * charHeight)) - (charHeight / 5);
+       // fillRect origin is top left of rectangle
+       g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast,
+               charHeight + 1);
        if (value != -1)
        {
-         g.drawString(String.valueOf(value), 0,
-                 (ypos + (i * charHeight)) - (charHeight / 5));
+         /*
+          * draw scale value, right justified within its width less half a
+          * character width padding on the right
+          */
+         int labelSpace = left ? labelWidthWest : labelWidthEast;
+         labelSpace -= charWidth / 2; // leave space to the right
+         String valueAsString = String.valueOf(value);
+         int labelLength = fm.stringWidth(valueAsString);
+         int xOffset = labelSpace - labelLength;
+         g.setColor(Color.black);
+         g.drawString(valueAsString, xOffset, y);
        }
      }
    }
  
    /**
-    * need to make this thread safe move alignment rendering in response to
-    * slider adjustment
+    * 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
+    * columns or rows are drawn. The scroll may be horizontal or vertical, but
+    * not both at once. Scrolling may be the result of
+    * <ul>
+    * <li>dragging a scroll bar</li>
+    * <li>clicking in the scroll bar</li>
+    * <li>scrolling by trackpad, middle mouse button, or other device</li>
+    * <li>by moving the box in the Overview window</li>
+    * <li>programmatically to make a highlighted position visible</li>
+    * </ul>
     * 
     * @param horizontal
-    *          shift along
+    *          columns to shift right (positive) or left (negative)
     * @param vertical
-    *          shift up or down in repaint
+    *          rows to shift down (positive) or up (negative)
     */
    public void fastPaint(int horizontal, int vertical)
    {
      }
      fastpainting = true;
      fastPaint = true;
-     updateViewport();
  
-     ViewportRanges ranges = av.getRanges();
-     int startRes = ranges.getStartRes();
-     int endRes = ranges.getEndRes();
-     int startSeq = ranges.getStartSeq();
-     int endSeq = ranges.getEndSeq();
-     int transX = 0;
-     int transY = 0;
+     try
+     {
+       int charHeight = av.getCharHeight();
+       int charWidth = av.getCharWidth();
+     
+       ViewportRanges ranges = av.getRanges();
+       int startRes = ranges.getStartRes();
+       int endRes = ranges.getEndRes();
+       int startSeq = ranges.getStartSeq();
+       int endSeq = ranges.getEndSeq();
+       int transX = 0;
+       int transY = 0;
  
      gg.copyArea(horizontal * charWidth, vertical * charHeight,
              img.getWidth(), img.getHeight(), -horizontal * charWidth,
      gg.translate(-transX, -transY);
  
      repaint();
-     fastpainting = false;
+     } finally
+     {
+       fastpainting = false;
+     }
    }
  
    @Override
    public void paintComponent(Graphics g)
    {
-     super.paintComponent(g);
-     updateViewport();
+     super.paintComponent(g);    
+     
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
  
      ViewportRanges ranges = av.getRanges();
  
        g.drawImage(lcimg, 0, 0, this);
      }
    }
+   
    /**
     * Draw an alignment panel for printing
     * 
    {
      BufferedImage lcimg = null;
  
+     int charWidth = av.getCharWidth();
+     int charHeight = av.getCharHeight();
+     
      int width = getWidth();
      int height = getHeight();
  
     */
    public int getWrappedCanvasWidth(int canvasWidth)
    {
-     FontMetrics fm = getFontMetrics(av.getFont());
+     int charWidth = av.getCharWidth();
  
-     labelWidthEast = 0;
-     labelWidthWest = 0;
+     FontMetrics fm = getFontMetrics(av.getFont());
  
-     if (av.getScaleRightWrapped())
+     int labelWidth = 0;
+     
+     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
      {
-       labelWidthEast = getLabelWidth(fm);
+       labelWidth = getLabelWidth(fm);
      }
  
-     if (av.getScaleLeftWrapped())
-     {
-       labelWidthWest = labelWidthEast > 0 ? labelWidthEast
-               : getLabelWidth(fm);
-     }
+     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
+     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
  
      return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
    }
  
    /**
-    * Returns a pixel width suitable for showing the largest sequence coordinate
-    * (end position) in the alignment. Returns 2 plus the number of decimal
-    * digits to be shown (3 for 1-10, 4 for 11-99 etc).
+    * Returns a pixel width sufficient to show the largest sequence coordinate
+    * (end position) in the alignment, calculated as the FontMetrics width of
+    * zeroes "0000000" limited to the number of decimal digits to be shown (3 for
+    * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for
+    * half a character width space on either side.
     * 
     * @param fm
     * @return
        maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
      }
  
-     int length = 2;
+     int length = 0;
      for (int i = maxWidth; i > 0; i /= 10)
      {
        length++;
      }
  
-     return fm.stringWidth(ZEROS.substring(0, length));
+     return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth();
    }
  
    /**
-    * DOCUMENT ME!
+    * Draws as many widths of a wrapped alignment as can fit in the visible
+    * window
     * 
     * @param g
-    *          DOCUMENT ME!
     * @param canvasWidth
-    *          DOCUMENT ME!
+    *          available width in pixels
     * @param canvasHeight
-    *          DOCUMENT ME!
-    * @param startRes
-    *          DOCUMENT ME!
+    *          available height in pixels
+    * @param startColumn
+    *          the first column (0...) of the alignment to draw
     */
-   private void drawWrappedPanel(Graphics g, int canvasWidth,
-           int canvasHeight, int startRes)
+   public void drawWrappedPanel(Graphics g, int canvasWidth,
+           int canvasHeight, final int startColumn)
    {
-     updateViewport();
-     AlignmentI al = av.getAlignment();
+     int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
+             canvasHeight);
  
-     int labelWidth = 0;
-     if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
+     av.setWrappedWidth(wrappedWidthInResidues);
+     ViewportRanges ranges = av.getRanges();
+     ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
+     /*
+      * draw one width at a time (including any scales or annotation shown),
+      * until we have run out of either alignment or vertical space available
+      */
+     int ypos = wrappedSpaceAboveAlignment;
+     int maxWidth = ranges.getVisibleAlignmentWidth();
+     int start = startColumn;
+     int currentWidth = 0;
+     while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
      {
-       FontMetrics fm = getFontMetrics(av.getFont());
-       labelWidth = getLabelWidth(fm);
+       int endColumn = Math
+               .min(maxWidth, start + wrappedWidthInResidues - 1);
+       drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
+       ypos += wrappedRepeatHeightPx;
+       start += wrappedWidthInResidues;
+       currentWidth++;
      }
  
-     labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
-     labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
+     drawWrappedDecorators(g, startColumn);
+   }
  
-     int hgap = charHeight;
-     if (av.getScaleAboveWrapped())
+   /**
+    * Calculates and saves values needed when rendering a wrapped alignment.
+    * These depend on many factors, including
+    * <ul>
+    * <li>canvas width and height</li>
+    * <li>number of visible sequences, and height of annotations if shown</li>
+    * <li>font and character width</li>
+    * <li>whether scales are shown left, right or above the alignment</li>
+    * </ul>
+    * 
+    * @param canvasWidth
+    * @param canvasHeight
+    * @return the number of residue columns in each width
+    */
+   protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
+   {
+     int charHeight = av.getCharHeight();
+     /*
+      * vertical space in pixels between wrapped widths of alignment
+      * - one character height, or two if scale above is drawn
+      */
+     wrappedSpaceAboveAlignment = charHeight
+             * (av.getScaleAboveWrapped() ? 2 : 1);
+     /*
+      * height in pixels of the wrapped widths
+      */
+     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
+     // add sequences
+     wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
+             * charHeight;
+     // add annotations panel height if shown
+     wrappedRepeatHeightPx += getAnnotationHeight();
+     /*
+      * number of visible widths (the last one may be part height),
+      * ensuring a part height includes at least one sequence
+      */
+     ViewportRanges ranges = av.getRanges();
+     wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
+     int remainder = canvasHeight % wrappedRepeatHeightPx;
+     if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
      {
-       hgap += charHeight;
+       wrappedVisibleWidths++;
      }
  
-     int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
-     int cHeight = av.getAlignment().getHeight() * charHeight;
+     /*
+      * compute width in residues; this also sets East and West label widths
+      */
+     int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
  
-     av.setWrappedWidth(cWidth);
+     /*
+      *  limit visibleWidths to not exceed width of alignment
+      */
+     int xMax = ranges.getVisibleAlignmentWidth();
+     int startToEnd = xMax - ranges.getStartRes();
+     int maxWidths = startToEnd / wrappedWidthInResidues;
+     if (startToEnd % wrappedWidthInResidues > 0)
+     {
+       maxWidths++;
+     }
+     wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
  
-     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
+     return wrappedWidthInResidues;
+   }
  
-     int endx;
-     int ypos = hgap;
-     int maxwidth = av.getAlignment().getWidth();
+   /**
+    * Draws one width of a wrapped alignment, including sequences and
+    * annnotations, if shown, but not scales or hidden column markers
+    * 
+    * @param g
+    * @param ypos
+    * @param startColumn
+    * @param endColumn
+    * @param canvasHeight
+    */
+   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
+           int endColumn, int canvasHeight)
+   {
+     ViewportRanges ranges = av.getRanges();
+     int viewportWidth = ranges.getViewportWidth();
  
-     if (av.hasHiddenColumns())
+     int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
+     /*
+      * move right before drawing by the width of the scale left (if any)
+      * plus column offset from left margin (usually zero, but may be non-zero
+      * when fast painting is drawing just a few columns)
+      */
+     int charWidth = av.getCharWidth();
+     int xOffset = labelWidthWest
+             + ((startColumn - ranges.getStartRes()) % viewportWidth)
+             * charWidth;
+     g.translate(xOffset, 0);
+     // When printing we have an extra clipped region,
+     // the Printable page which we need to account for here
+     Shape clip = g.getClip();
+     if (clip == null)
      {
-       maxwidth = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(maxwidth);
+       g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
+     }
+     else
+     {
+       g.setClip(0, (int) clip.getBounds().getY(),
+               viewportWidth * charWidth, (int) clip.getBounds().getHeight());
      }
  
-     int annotationHeight = getAnnotationHeight();
+     /*
+      * white fill the region to be drawn (so incremental fast paint doesn't
+      * scribble over an existing image)
+      */
+     gg.setColor(Color.white);
+     gg.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
+             wrappedRepeatHeightPx);
  
-     while ((ypos <= canvasHeight) && (startRes < maxwidth))
-     {
-       endx = startRes + cWidth - 1;
+     drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
+             ypos);
  
-       if (endx > maxwidth)
+     int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
+     if (av.isShowAnnotation())
+     {
+       g.translate(0, cHeight + ypos + 3);
+       if (annotations == null)
        {
-         endx = maxwidth;
+         annotations = new AnnotationPanel(av);
        }
  
-       g.setFont(av.getFont());
-       g.setColor(Color.black);
+       annotations.renderer.drawComponent(annotations, av, g, -1,
+               startColumn, endx + 1);
+       g.translate(0, -cHeight - ypos - 3);
+     }
+     g.setClip(clip);
+     g.translate(-xOffset, 0);
+   }
+   /**
+    * Draws scales left, right and above (if shown), and any hidden column
+    * markers, on all widths of the wrapped alignment
+    * 
+    * @param g
+    * @param startColumn
+    */
+   protected void drawWrappedDecorators(Graphics g, final int startColumn)
+   {
+     int charWidth = av.getCharWidth();
+     g.setFont(av.getFont());
+     g.setColor(Color.black);
+     int ypos = wrappedSpaceAboveAlignment;
+     ViewportRanges ranges = av.getRanges();
+     int viewportWidth = ranges.getViewportWidth();
+     int maxWidth = ranges.getVisibleAlignmentWidth();
+     int widthsDrawn = 0;
+     int startCol = startColumn;
+     while (widthsDrawn < wrappedVisibleWidths)
+     {
+       int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1);
  
        if (av.getScaleLeftWrapped())
        {
-         drawWestScale(g, startRes, endx, ypos);
+         drawVerticalScale(g, startCol, endColumn - 1, ypos, true);
        }
  
        if (av.getScaleRightWrapped())
        {
-         g.translate(canvasWidth - labelWidthEast, 0);
-         drawEastScale(g, startRes, endx, ypos);
-         g.translate(-(canvasWidth - labelWidthEast), 0);
+         int x = labelWidthWest + viewportWidth * charWidth;
+         g.translate(x, 0);
+         drawVerticalScale(g, startCol, endColumn, ypos, false);
+         g.translate(-x, 0);
        }
  
+       /*
+        * white fill region of scale above and hidden column markers
+        * (to support incremental fast paint of image)
+        */
+       g.translate(labelWidthWest, 0);
+       g.setColor(Color.white);
+       g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
+               * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
+       g.setColor(Color.black);
+       g.translate(-labelWidthWest, 0);
        g.translate(labelWidthWest, 0);
  
        if (av.getScaleAboveWrapped())
        {
-         drawNorthScale(g, startRes, endx, ypos);
+         drawNorthScale(g, startCol, endColumn, ypos);
        }
  
        if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
        {
-         g.setColor(Color.blue);
-         int res;
-         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-         Iterator<Integer> it = hidden.getBoundedStartIterator(startRes,
-                 endx + 1);
-         while (it.hasNext())
-         {
-           res = it.next() - startRes;
-           gg.fillPolygon(
-                   new int[]
-           { res * charWidth - charHeight / 4,
-               res * charWidth + charHeight / 4, res * charWidth },
-                   new int[]
-           { ypos - (charHeight / 2), ypos - (charHeight / 2),
-               ypos - (charHeight / 2) + 8 }, 3);
-         }
+         drawHiddenColumnMarkers(g, ypos, startCol, endColumn);
        }
  
-       // When printing we have an extra clipped region,
-       // the Printable page which we need to account for here
-       Shape clip = g.getClip();
+       g.translate(-labelWidthWest, 0);
  
-       if (clip == null)
-       {
-         g.setClip(0, 0, cWidth * charWidth, canvasHeight);
-       }
-       else
-       {
-         g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
-                 (int) clip.getBounds().getHeight());
-       }
+       ypos += wrappedRepeatHeightPx;
+       startCol += viewportWidth;
+       widthsDrawn++;
+     }
+   }
  
-       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
+   /**
+    * Draws markers (triangles) above hidden column positions between startColumn
+    * and endColumn.
+    * 
+    * @param g
+    * @param ypos
+    * @param startColumn
+    * @param endColumn
+    */
+   protected void drawHiddenColumnMarkers(Graphics g, int ypos,
+           int startColumn, int endColumn)
+   {
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
  
-       if (av.isShowAnnotation())
-       {
-         g.translate(0, cHeight + ypos + 3);
-         if (annotations == null)
-         {
-           annotations = new AnnotationPanel(av);
-         }
+     g.setColor(Color.blue);
++    int res;
+     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 -    List<Integer> positions = hidden.findHiddenRegionPositions();
 -    for (int pos : positions)
 +
-         annotations.renderer.drawComponent(annotations, av, g, -1, startRes,
-                 endx + 1);
-         g.translate(0, -cHeight - ypos - 3);
-       }
-       g.setClip(clip);
-       g.translate(-labelWidthWest, 0);
++    Iterator<Integer> it = hidden.getBoundedStartIterator(startColumn,
++            endColumn);
++    while (it.hasNext())
+     {
 -      int res = pos - startColumn;
++      res = it.next() - startColumn;
  
-       ypos += cHeight + annotationHeight + hgap;
+       if (res < 0 || res > endColumn - startColumn + 1)
+       {
+         continue;
+       }
  
-       startRes += cWidth;
+       /*
+        * draw a downward-pointing triangle at the hidden columns location
+        * (before the following visible column)
+        */
+       int xMiddle = res * charWidth;
+       int[] xPoints = new int[] { xMiddle - charHeight / 4,
+           xMiddle + charHeight / 4, xMiddle };
+       int yTop = ypos - (charHeight / 2);
+       int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
+       g.fillPolygon(xPoints, yPoints, 3);
      }
    }
  
            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())
     * marker.
     * 
     * @param g1
-    *          Graphics object to draw with
+    *          the graphics context, positioned at the first residue to be drawn
     * @param startRes
-    *          offset of the first column in the visible region (0..)
+    *          offset of the first column to draw (0..)
     * @param endRes
-    *          offset of the last column in the visible region (0..)
+    *          offset of the last column to draw (0..)
     * @param startSeq
-    *          offset of the first sequence in the visible region (0..)
+    *          offset of the first sequence to draw (0..)
     * @param endSeq
-    *          offset of the last sequence in the visible region (0..)
+    *          offset of the last sequence to draw (0..)
     * @param yOffset
     *          vertical offset at which to draw (for wrapped alignments)
     */
    public void drawPanel(Graphics g1, final int startRes, final int endRes,
            final int startSeq, final int endSeq, final int yOffset)
    {
-     updateViewport();
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
      if (!av.hasHiddenColumns())
      {
        draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
      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();
 +      VisibleBlocksVisBoundsIterator regions = (VisibleBlocksVisBoundsIterator) hidden
 +              .getVisibleBlocksIterator(startRes, endRes, 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);
        }
      }
  
    private void draw(Graphics g, int startRes, int endRes, int startSeq,
            int endSeq, int offset)
    {
+     int charHeight = av.getCharHeight();
+     int charWidth = av.getCharWidth();
      g.setFont(av.getFont());
      seqRdr.prepare(g, av.isRenderGaps());
  
    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,
      {
        // 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();
 +      VisibleBlocksVisBoundsIterator regions = (VisibleBlocksVisBoundsIterator) hidden
 +              .getVisibleBlocksIterator(startRes, endRes, 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);
        }
      }
    }
            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;
        return false;
      }
      boolean wrapped = av.getWrapAlignment();
      try
      {
        fastPaint = !noFastPaint;
        fastpainting = fastPaint;
  
-       updateViewport();
        /*
         * to avoid redrawing the whole visible region, we instead
         * redraw just the minimal regions to remove previous highlights
      {
        fastPaint = true;
        repaint();
+       return;
      }
-     else if (av.getWrapAlignment())
+     int scrollX = 0;
+     if (eventName.equals(ViewportRanges.STARTRES))
      {
-       if (eventName.equals(ViewportRanges.STARTRES))
+       // 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.getViewportWidth();
+       if (scrollX > range)
        {
-         repaint();
+         scrollX = range;
+       }
+       else if (scrollX < -range)
+       {
+         scrollX = -range;
        }
      }
-     else
+     // 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 (eventName.equals(ViewportRanges.STARTRES))
      {
-       int scrollX = 0;
-       if (eventName.equals(ViewportRanges.STARTRES))
+       if (av.getWrapAlignment())
        {
-         // 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;
-         }
+         fastPaintWrapped(scrollX);
        }
-       // 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
        {
-         // scroll - startres and endres both change
          fastPaint(scrollX, 0);
        }
-       else if (eventName.equals(ViewportRanges.STARTSEQ))
+     }
+     else if (eventName.equals(ViewportRanges.STARTSEQ))
+     {
+       // scroll
+       fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+     }
+   }
+   /**
+    * Does a minimal update of the image for a scroll movement. This method
+    * handles scroll movements of up to one width of the wrapped alignment (one
+    * click in the vertical scrollbar). Larger movements (for example after a
+    * scroll to highlight a mapped position) trigger a full redraw instead.
+    * 
+    * @param scrollX
+    *          number of positions scrolled (right if positive, left if negative)
+    */
+   protected void fastPaintWrapped(int scrollX)
+   {
+     ViewportRanges ranges = av.getRanges();
+     if (Math.abs(scrollX) > ranges.getViewportWidth())
+     {
+       /*
+        * shift of more than one view width is 
+        * overcomplicated to handle in this method
+        */
+       fastPaint = false;
+       repaint();
+       return;
+     }
+     if (fastpainting || gg == null)
+     {
+       return;
+     }
+     fastPaint = true;
+     fastpainting = true;
+     try
+     {
+       calculateWrappedGeometry(getWidth(), getHeight());
+       /*
+        * relocate the regions of the alignment that are still visible
+        */
+       shiftWrappedAlignment(-scrollX);
+       /*
+        * add new columns (sequence, annotation)
+        * - at top left if scrollX < 0 
+        * - at right of last two widths if scrollX > 0
+        */
+       if (scrollX < 0)
+       {
+         int startRes = ranges.getStartRes();
+         drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
+                 - scrollX - 1, getHeight());
+       }
+       else
        {
-         // scroll
-         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+         fastPaintWrappedAddRight(scrollX);
        }
+       /*
+        * draw all scales (if  shown) and hidden column markers
+        */
+       drawWrappedDecorators(gg, ranges.getStartRes());
+       repaint();
+     } finally
+     {
+       fastpainting = false;
+     }
+   }
+   /**
+    * Draws the specified number of columns at the 'end' (bottom right) of a
+    * wrapped alignment view, including sequences and annotations if shown, but
+    * not scales. Also draws the same number of columns at the right hand end of
+    * the second last width shown, if the last width is not full height (so
+    * cannot simply be copied from the graphics image).
+    * 
+    * @param columns
+    */
+   protected void fastPaintWrappedAddRight(int columns)
+   {
+     if (columns == 0)
+     {
+       return;
+     }
+     ViewportRanges ranges = av.getRanges();
+     int viewportWidth = ranges.getViewportWidth();
+     int charWidth = av.getCharWidth();
+     /**
+      * draw full height alignment in the second last row, last columns, if the
+      * last row was not full height
+      */
+     int visibleWidths = wrappedVisibleWidths;
+     int canvasHeight = getHeight();
+     boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
+     if (lastWidthPartHeight)
+     {
+       int widthsAbove = Math.max(0, visibleWidths - 2);
+       int ypos = wrappedRepeatHeightPx * widthsAbove
+               + wrappedSpaceAboveAlignment;
+       int endRes = ranges.getEndRes();
+       endRes += widthsAbove * viewportWidth;
+       int startRes = endRes - columns;
+       int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+               * charWidth;
+       /*
+        * white fill first to erase annotations
+        */
+       gg.translate(xOffset, 0);
+       gg.setColor(Color.white);
+       gg.fillRect(labelWidthWest, ypos,
+               (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
+       gg.translate(-xOffset, 0);
+       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+     }
+     /*
+      * draw newly visible columns in last wrapped width (none if we
+      * have reached the end of the alignment)
+      * y-offset for drawing last width is height of widths above,
+      * plus one gap row
+      */
+     int widthsAbove = visibleWidths - 1;
+     int ypos = wrappedRepeatHeightPx * widthsAbove
+             + wrappedSpaceAboveAlignment;
+     int endRes = ranges.getEndRes();
+     endRes += widthsAbove * viewportWidth;
+     int startRes = endRes - columns + 1;
+     /*
+      * white fill first to erase annotations
+      */
+     int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+             * charWidth;
+     gg.translate(xOffset, 0);
+     gg.setColor(Color.white);
+     int width = viewportWidth * charWidth - xOffset;
+     gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
+     gg.translate(-xOffset, 0);
+     gg.setFont(av.getFont());
+     gg.setColor(Color.black);
+     if (startRes < ranges.getVisibleAlignmentWidth())
+     {
+       drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+     }
+     /*
+      * and finally, white fill any space below the visible alignment
+      */
+     int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
+     if (heightBelow > 0)
+     {
+       gg.setColor(Color.white);
+       gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
      }
    }
  
    /**
+    * Shifts the visible alignment by the specified number of columns - left if
+    * negative, right if positive. Copies and moves sequences and annotations (if
+    * shown). Scales, hidden column markers and any newly visible columns must be
+    * drawn separately.
+    * 
+    * @param positions
+    */
+   protected void shiftWrappedAlignment(int positions)
+   {
+     if (positions == 0)
+     {
+       return;
+     }
+     int charWidth = av.getCharWidth();
+     int canvasHeight = getHeight();
+     ViewportRanges ranges = av.getRanges();
+     int viewportWidth = ranges.getViewportWidth();
+     int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
+             * charWidth;
+     int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
+     int xMax = ranges.getVisibleAlignmentWidth();
+     if (positions > 0)
+     {
+       /*
+        * shift right (after scroll left)
+        * for each wrapped width (starting with the last), copy (width-positions) 
+        * columns from the left margin to the right margin, and copy positions 
+        * columns from the right margin of the row above (if any) to the 
+        * left margin of the current row
+        */
+       /*
+        * get y-offset of last wrapped width, first row of sequences
+        */
+       int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
+       y += wrappedSpaceAboveAlignment;
+       int copyFromLeftStart = labelWidthWest;
+       int copyFromRightStart = copyFromLeftStart + widthToCopy;
+       while (y >= 0)
+       {
+         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
+                 positions * charWidth, 0);
+         if (y > 0)
+         {
+           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
+                   positions * charWidth, heightToCopy, -widthToCopy,
+                   wrappedRepeatHeightPx);
+         }
+         y -= wrappedRepeatHeightPx;
+       }
+     }
+     else
+     {
+       /*
+        * shift left (after scroll right)
+        * for each wrapped width (starting with the first), copy (width-positions) 
+        * columns from the right margin to the left margin, and copy positions 
+        * columns from the left margin of the row below (if any) to the 
+        * right margin of the current row
+        */
+       int xpos = av.getRanges().getStartRes();
+       int y = wrappedSpaceAboveAlignment;
+       int copyFromRightStart = labelWidthWest - positions * charWidth;
+       while (y < canvasHeight)
+       {
+         gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
+                 positions * charWidth, 0);
+         if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
+                 && (xpos + viewportWidth <= xMax))
+         {
+           gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
+                   * charWidth, heightToCopy, widthToCopy,
+                   -wrappedRepeatHeightPx);
+         }
+         y += wrappedRepeatHeightPx;
+         xpos += viewportWidth;
+       }
+     }
+   }
+   
+   /**
     * Redraws any positions in the search results in the visible region of a
     * wrapped alignment. Any highlights are drawn depending on the search results
     * set on the Viewport, not the <code>results</code> argument. This allows
      {
        return false;
      }
-   
+     int charHeight = av.getCharHeight();
      boolean matchFound = false;
  
+     calculateWrappedGeometry(getWidth(), getHeight());
      int wrappedWidth = av.getWrappedWidth();
-     int wrappedHeight = getRepeatHeightWrapped();
+     int wrappedHeight = wrappedRepeatHeightPx;
  
      ViewportRanges ranges = av.getRanges();
      int canvasHeight = getHeight();
    }
  
    /**
-    * Answers the height in pixels of a repeating section of the wrapped
-    * alignment, including space above, scale above if shown, sequences, and
-    * annotation panel if shown
+    * Answers the width in pixels of the left scale labels (0 if not shown)
     * 
     * @return
     */
-   protected int getRepeatHeightWrapped()
+   int getLabelWidthWest()
    {
-     // gap (and maybe scale) above
-     int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
-     // add sequences
-     repeatHeight += av.getRanges().getViewportHeight() * charHeight;
-     // add annotations panel height if shown
-     repeatHeight += getAnnotationHeight();
-     return repeatHeight;
+     return labelWidthWest;
    }
  }
@@@ -34,12 -34,24 +34,24 @@@ import java.util.List
   */
  public class ScaleRenderer
  {
+   /**
+    * Represents one major or minor scale mark
+    */
    public final class ScaleMark
    {
+     /**
+      * true for a major scale mark, false for minor
+      */
      public final boolean major;
  
+     /**
+      * visible column position (0..) e.g. 19
+      */
      public final int column;
  
+     /**
+      * text (if any) to show e.g. "20"
+      */
      public final String text;
  
      ScaleMark(boolean isMajor, int col, String txt)
        column = col;
        text = txt;
      }
    }
  
    /**
 -   * Calculates position markers on the alignment ruler
 +   * calculate positions markers on the alignment ruler
     * 
     * @param av
     * @param startx
 -   *          left-most column in visible view (0..)
 +   *          left-most column in visible view
     * @param endx
 -   *          - right-most column in visible view (0..)
 -   * @return
 +   *          - right-most column in visible view
 +   * @return List of ScaleMark holding boolean: true/false for major/minor mark,
 +   *         marker position in alignment column coords, a String to be rendered
 +   *         at the position (or null)
     */
    public List<ScaleMark> calculateMarks(AlignViewportI av, int startx,
            int endx)
      int scalestartx = (startx / 10) * 10;
  
      SequenceI refSeq = av.getAlignment().getSeqrep();
 -    int refSp = 0, refStartI = 0, refEndI = -1;
 +    int refSp = 0;
 +    int refStartI = 0;
 +    int refEndI = -1;
      if (refSeq != null)
      {
 -      // find bounds and set origin appopriately
 +      // find bounds and set origin appropriately
        // locate first visible position for this sequence
 -      int[] refbounds = av.getAlignment().getHiddenColumns()
 -              .locateVisibleBoundsOfSequence(refSeq);
 +      refSp = av.getAlignment().getHiddenColumns()
 +              .locateVisibleStartOfSequence(refSeq);
 +
 +      refStartI = refSeq.findIndex(refSeq.getStart()) - 1;
 +
 +      int seqlength = refSeq.getLength();
 +      // get sequence position past the end of the sequence
 +      int pastEndPos = refSeq.findPosition(seqlength + 1);
 +      refEndI = refSeq.findIndex(pastEndPos - 1) - 1;
  
 -      refSp = refbounds[0];
 -      refStartI = refbounds[4];
 -      refEndI = refbounds[5];
        scalestartx = refSp + ((scalestartx - refSp) / 10) * 10;
      }
  
      {
        scalestartx += 5;
      }
 -    List<ScaleMark> marks = new ArrayList<ScaleMark>();
 +    List<ScaleMark> marks = new ArrayList<>();
 +    String string;
 +    int refN, iadj;
      // todo: add a 'reference origin column' to set column number relative to
 -    for (int i = scalestartx; i <= endx; i += 5)
 +    for (int i = scalestartx; i < endx; i += 5)
      {
        if (((i - refSp) % 10) == 0)
        {
 -        String text;
          if (refSeq == null)
          {
 -          int iadj = av.getAlignment().getHiddenColumns()
 +          iadj = av.getAlignment().getHiddenColumns()
                    .adjustForHiddenColumns(i - 1) + 1;
 -          text = String.valueOf(iadj);
 +          string = String.valueOf(iadj);
          }
          else
          {
 -          int iadj = av.getAlignment().getHiddenColumns()
 +          iadj = av.getAlignment().getHiddenColumns()
                    .adjustForHiddenColumns(i - 1);
 -          int refN = refSeq.findPosition(iadj);
 +          refN = refSeq.findPosition(iadj);
            // TODO show bounds if position is a gap
            // - ie L--R -> "1L|2R" for
            // marker
            if (iadj < refStartI)
            {
 -            text = String.valueOf(iadj - refStartI);
 +            string = String.valueOf(iadj - refStartI);
            }
            else if (iadj > refEndI)
            {
 -            text = "+" + String.valueOf(iadj - refEndI);
 +            string = "+" + String.valueOf(iadj - refEndI);
            }
            else
            {
 -            text = String.valueOf(refN) + refSeq.getCharAt(iadj);
 +            string = String.valueOf(refN) + refSeq.getCharAt(iadj);
            }
          }
 -        marks.add(new ScaleMark(true, i - startx - 1, text));
 +        marks.add(new ScaleMark(true, i - startx - 1, string));
        }
        else
        {
@@@ -542,11 -542,9 +542,11 @@@ public final class MappingUtil
                toSequences, fromGapChar);
      }
  
 -    for (int[] hidden : hiddencols.getHiddenColumnsCopy())
 +    Iterator<int[]> regions = hiddencols.iterator();
 +    while (regions.hasNext())
      {
 -      mapHiddenColumns(hidden, codonFrames, newHidden, fromSequences,
 +      mapHiddenColumns(regions.next(), codonFrames, newHidden,
 +              fromSequences,
                toSequences, fromGapChar);
      }
      return; // mappedColumns;
      }
      return copy;
    }
+   /**
+    * Removes the specified number of positions from the given ranges. Provided
+    * to allow a stop codon to be stripped from a CDS sequence so that it matches
+    * the peptide translation length.
+    * 
+    * @param positions
+    * @param ranges
+    *          a list of (single) [start, end] ranges
+    * @return
+    */
+   public static void removeEndPositions(int positions,
+           List<int[]> ranges)
+   {
+     int toRemove = positions;
+     Iterator<int[]> it = new ReverseListIterator<>(ranges);
+     while (toRemove > 0)
+     {
+       int[] endRange = it.next();
+       if (endRange.length != 2)
+       {
+         /*
+          * not coded for [start1, end1, start2, end2, ...]
+          */
+         System.err
+                 .println("MappingUtils.removeEndPositions doesn't handle multiple  ranges");
+         return;
+       }
+       int length = endRange[1] - endRange[0] + 1;
+       if (length <= 0)
+       {
+         /*
+          * not coded for a reverse strand range (end < start)
+          */
+         System.err
+                 .println("MappingUtils.removeEndPositions doesn't handle reverse strand");
+         return;
+       }
+       if (length > toRemove)
+       {
+         endRange[1] -= toRemove;
+         toRemove = 0;
+       }
+       else
+       {
+         toRemove -= length;
+         it.remove();
+       }
+     }
+   }
  }
@@@ -41,13 -41,13 +41,13 @@@ import java.util.BitSet
  import java.util.List;
  import java.util.Vector;
  
 -import junit.extensions.PA;
 -
  import org.testng.Assert;
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.BeforeMethod;
  import org.testng.annotations.Test;
  
 +import junit.extensions.PA;
 +
  public class SequenceTest
  {
  
      Assert.assertEquals(pdbe1a,
              sq.getDatasetSequence().getPDBEntry("1PDB"),
              "PDB Entry '1PDB' not found on dataset sequence via getPDBEntry.");
 -    ArrayList<Annotation> annotsList = new ArrayList<Annotation>();
 +    ArrayList<Annotation> annotsList = new ArrayList<>();
      System.out.println(">>>>>> " + sq.getSequenceAsString().length());
      annotsList.add(new Annotation("A", "A", 'X', 0.1f));
      annotsList.add(new Annotation("A", "A", 'X', 0.1f));
    }
  
    @Test(groups = { "Functional" })
 +  public void testGapBitset()
 +  {
 +    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
 +    BitSet bs = sq.gapBitset();
 +    BitSet expected = new BitSet();
 +    expected.set(0);
 +    expected.set(4, 7);
 +    expected.set(9);
 +    expected.set(11, 13);
 +
 +    assertTrue(bs.equals(expected));
 +
 +  }
++
+   public void testFindFeatures_largeEndPos()
+   {
+     /*
+      * imitate a PDB sequence where end is larger than end position
+      */
+     SequenceI sq = new Sequence("test", "-ABC--DEF--", 1, 20);
+     sq.createDatasetSequence();
+   
+     assertTrue(sq.findFeatures(1, 9).isEmpty());
+     // should be no array bounds exception - JAL-2772
+     assertTrue(sq.findFeatures(1, 15).isEmpty());
+   
+     // add feature on BCD
+     SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+             null);
+     sq.addSequenceFeature(sfBCD);
+   
+     // no features in columns 1-2 (-A)
+     List<SequenceFeature> found = sq.findFeatures(1, 2);
+     assertTrue(found.isEmpty());
+   
+     // columns 1-6 (-ABC--) includes BCD
+     found = sq.findFeatures(1, 6);
+     assertEquals(1, found.size());
+     assertTrue(found.contains(sfBCD));
+     // columns 10-11 (--) should find nothing
+     found = sq.findFeatures(10, 11);
+     assertEquals(0, found.size());
+   }
+   @Test(groups = { "Functional" })
+   public void testSetName()
+   {
+     SequenceI sq = new Sequence("test", "-ABC---DE-F--");
+     assertEquals("test", sq.getName());
+     assertEquals(1, sq.getStart());
+     assertEquals(6, sq.getEnd());
+     sq.setName("testing");
+     assertEquals("testing", sq.getName());
+     sq.setName("test/8-10");
+     assertEquals("test", sq.getName());
+     assertEquals(8, sq.getStart());
+     assertEquals(13, sq.getEnd()); // note end is recomputed
+     sq.setName("testing/7-99");
+     assertEquals("testing", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd()); // end may be beyond physical end
+     sq.setName("/2-3");
+     assertEquals("", sq.getName());
+     assertEquals(2, sq.getStart());
+     assertEquals(7, sq.getEnd());
+     sq.setName("test/"); // invalid
+     assertEquals("test/", sq.getName());
+     assertEquals(2, sq.getStart());
+     assertEquals(7, sq.getEnd());
+     sq.setName("test/6-13/7-99");
+     assertEquals("test/6-13", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/0-5"); // 0 is invalid - ignored
+     assertEquals("test/0-5", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/a-5"); // a is invalid - ignored
+     assertEquals("test/a-5", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/6-5"); // start > end is invalid - ignored
+     assertEquals("test/6-5", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/5"); // invalid - ignored
+     assertEquals("test/5", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/-5"); // invalid - ignored
+     assertEquals("test/-5", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/5-"); // invalid - ignored
+     assertEquals("test/5-", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName("test/5-6-7"); // invalid - ignored
+     assertEquals("test/5-6-7", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+     sq.setName(null); // invalid, gets converted to space
+     assertEquals("", sq.getName());
+     assertEquals(7, sq.getStart());
+     assertEquals(99, sq.getEnd());
+   }
+   @Test(groups = { "Functional" })
+   public void testCheckValidRange()
+   {
+     Sequence sq = new Sequence("test/7-12", "-ABC---DE-F--");
+     assertEquals(7, sq.getStart());
+     assertEquals(12, sq.getEnd());
+     /*
+      * checkValidRange ensures end is at least the last residue position
+      */
+     PA.setValue(sq, "end", 2);
+     sq.checkValidRange();
+     assertEquals(12, sq.getEnd());
+     /*
+      * end may be beyond the last residue position
+      */
+     PA.setValue(sq, "end", 22);
+     sq.checkValidRange();
+     assertEquals(22, sq.getEnd());
+   }
  }
@@@ -50,7 -50,6 +50,7 @@@ import java.awt.Color
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Arrays;
 +import java.util.Iterator;
  import java.util.List;
  
  import org.testng.annotations.BeforeClass;
@@@ -914,9 -913,9 +914,9 @@@ public class MappingUtilsTes
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
      assertEquals("[]", dnaSelection.getSelected().toString());
 -    List<int[]> hidden = dnaHidden.getHiddenColumnsCopy();
 -    assertEquals(1, hidden.size());
 -    assertEquals("[0, 4]", Arrays.toString(hidden.get(0)));
 +    Iterator<int[]> regions = dnaHidden.iterator();
 +    assertEquals(1, dnaHidden.getNumberOfRegions());
 +    assertEquals("[0, 4]", Arrays.toString(regions.next()));
  
      /*
       * Column 1 in protein picks up Seq1/K which maps to cols 0-3 in dna
      proteinSelection.hideSelectedColumns(1, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
 -    hidden = dnaHidden.getHiddenColumnsCopy();
 -    assertEquals(1, hidden.size());
 -    assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
 +    regions = dnaHidden.iterator();
 +    assertEquals(1, dnaHidden.getNumberOfRegions());
 +    assertEquals("[0, 3]", Arrays.toString(regions.next()));
  
      /*
       * Column 2 in protein picks up gaps only - no mapping
      proteinSelection.hideSelectedColumns(2, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
 -    assertTrue(dnaHidden.getHiddenColumnsCopy().isEmpty());
 +    assertEquals(0, dnaHidden.getNumberOfRegions());
  
      /*
       * Column 3 in protein picks up Seq1/P, Seq2/Q, Seq3/S which map to columns
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
      assertEquals("[0, 1, 2, 3]", dnaSelection.getSelected().toString());
 -    hidden = dnaHidden.getHiddenColumnsCopy();
 -    assertEquals(1, hidden.size());
 -    assertEquals("[5, 10]", Arrays.toString(hidden.get(0)));
 +    regions = dnaHidden.iterator();
 +    assertEquals(1, dnaHidden.getNumberOfRegions());
 +    assertEquals("[5, 10]", Arrays.toString(regions.next()));
  
      /*
       * Combine hiding columns 1 and 3 to get discontiguous hidden columns
      proteinSelection.hideSelectedColumns(3, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
 -    hidden = dnaHidden.getHiddenColumnsCopy();
 -    assertEquals(2, hidden.size());
 -    assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
 -    assertEquals("[5, 10]", Arrays.toString(hidden.get(1)));
 +    regions = dnaHidden.iterator();
 +    assertEquals(2, dnaHidden.getNumberOfRegions());
 +    assertEquals("[0, 3]", Arrays.toString(regions.next()));
 +    assertEquals("[5, 10]", Arrays.toString(regions.next()));
    }
  
    @Test(groups = { "Functional" })
      assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
    }
  
+   @Test(groups = "Functional")
+   public void testRemoveEndPositions()
+   {
+     List<int[]> ranges = new ArrayList<>();
+     /*
+      * case 1: truncate last range
+      */
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 30 });
+     MappingUtils.removeEndPositions(5, ranges);
+     assertEquals(2, ranges.size());
+     assertEquals(25, ranges.get(1)[1]);
+     /*
+      * case 2: remove last range
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 22 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(10, ranges.get(0)[1]);
+     /*
+      * case 3: truncate penultimate range
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 21 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(9, ranges.get(0)[1]);
+     /*
+      * case 4: remove last two ranges
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 20 });
+     ranges.add(new int[] { 30, 30 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(9, ranges.get(0)[1]);
+   }
  }