Merge branch 'develop' into features/JAL-2446NCList
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 29 May 2017 11:08:24 +0000 (12:08 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 29 May 2017 11:08:24 +0000 (12:08 +0100)
Conflicts:
src/jalview/appletgui/SeqPanel.java

1  2 
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/IdPanel.java
src/jalview/appletgui/SeqPanel.java
src/jalview/datamodel/SequenceI.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/IdPanel.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/Jalview2XML_V1.java
src/jalview/gui/SeqPanel.java

@@@ -601,24 -601,22 +601,22 @@@ public class AlignFrame extends Embmenu
      case KeyEvent.VK_PAGE_UP:
        if (viewport.getWrapAlignment())
        {
-         alignPanel.scrollUp(true);
+         ranges.scrollUp(true);
        }
        else
        {
-         alignPanel.setScrollValues(ranges.getStartRes(),
-                 2 * ranges.getStartSeq() - ranges.getEndSeq());
+         ranges.pageUp();
        }
        break;
  
      case KeyEvent.VK_PAGE_DOWN:
        if (viewport.getWrapAlignment())
        {
-         alignPanel.scrollUp(false);
+         ranges.scrollUp(false);
        }
        else
        {
-         alignPanel
-                 .setScrollValues(ranges.getStartRes(), ranges.getEndSeq());
+         ranges.pageDown();
        }
        break;
  
      return null;
    }
  
 +  private List<String> getDisplayedFeatureGroups()
 +  {
 +    if (alignPanel.getFeatureRenderer() != null
 +            && viewport.getFeaturesDisplayed() != null)
 +    {
 +      return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups();
 +
 +    }
 +    return null;
 +  }
 +
    public String outputFeatures(boolean displayTextbox, String format)
    {
      String features;
      if (format.equalsIgnoreCase("Jalview"))
      {
        features = formatter.printJalviewFormat(viewport.getAlignment()
 -              .getSequencesArray(), getDisplayedFeatureCols());
 +              .getSequencesArray(), getDisplayedFeatureCols(),
 +              getDisplayedFeatureGroups(), true);
      }
      else
      {
        features = formatter.printGffFormat(viewport.getAlignment()
 -              .getSequencesArray(), getDisplayedFeatureCols());
 +              .getSequencesArray(), getDisplayedFeatureCols(),
 +              getDisplayedFeatureGroups(), true);
      }
  
      if (displayTextbox)
@@@ -20,6 -20,7 +20,6 @@@
   */
  package jalview.appletgui;
  
 -import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
@@@ -56,11 -57,11 +56,11 @@@ public class IdPanel extends Panel impl
  
    UrlProviderI urlProvider = null;
  
 -  public IdPanel(AlignViewport av, AlignmentPanel parent)
 +  public IdPanel(AlignViewport viewport, AlignmentPanel parent)
    {
 -    this.av = av;
 +    this.av = viewport;
      alignPanel = parent;
 -    idCanvas = new IdCanvas(av);
 +    idCanvas = new IdCanvas(viewport);
      setLayout(new BorderLayout());
      add(idCanvas, BorderLayout.CENTER);
      idCanvas.addMouseListener(this);
  
      // make a list of label,url pairs
      HashMap<String, String> urlList = new HashMap<String, String>();
 -    if (av.applet != null)
 +    if (viewport.applet != null)
      {
        for (int i = 1; i < 10; i++)
        {
 -        label = av.applet.getParameter("linkLabel_" + i);
 -        url = av.applet.getParameter("linkURL_" + i);
 +        label = viewport.applet.getParameter("linkLabel_" + i);
 +        url = viewport.applet.getParameter("linkURL_" + i);
  
          // only add non-null parameters
          if (label != null)
@@@ -88,7 -89,7 +88,7 @@@
        if (!urlList.isEmpty())
        {
          // set default as first entry in list
 -        String defaultUrl = av.applet.getParameter("linkLabel_1");
 +        String defaultUrl = viewport.applet.getParameter("linkLabel_1");
          UrlProviderFactoryI factory = new AppletUrlProviderFactory(
                  defaultUrl, urlList);
          urlProvider = factory.createUrlProvider();
  
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
  
 -    // look for non-pos features
      StringBuffer tooltiptext = new StringBuffer();
 -    if (sequence != null)
 +    if (sequence == null)
      {
 -      if (sequence.getDescription() != null)
 +      return;
 +    }
 +    if (sequence.getDescription() != null)
 +    {
 +      tooltiptext.append(sequence.getDescription());
 +      tooltiptext.append("\n");
 +    }
 +
 +    for (SequenceFeature sf : sequence.getFeatures()
 +            .getNonPositionalFeatures())
 +    {
 +      boolean nl = false;
 +      if (sf.getFeatureGroup() != null)
        {
 -        tooltiptext.append(sequence.getDescription());
 -        tooltiptext.append("\n");
 +        tooltiptext.append(sf.getFeatureGroup());
 +        nl = true;
        }
 -
 -      SequenceFeature sf[] = sequence.getSequenceFeatures();
 -      for (int sl = 0; sf != null && sl < sf.length; sl++)
 +      if (sf.getType() != null)
        {
 -        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
 -        {
 -          boolean nl = false;
 -          if (sf[sl].getFeatureGroup() != null)
 -          {
 -            tooltiptext.append(sf[sl].getFeatureGroup());
 -            nl = true;
 -          }
 -          ;
 -          if (sf[sl].getType() != null)
 -          {
 -            tooltiptext.append(" ");
 -            tooltiptext.append(sf[sl].getType());
 -            nl = true;
 -          }
 -          ;
 -          if (sf[sl].getDescription() != null)
 -          {
 -            tooltiptext.append(" ");
 -            tooltiptext.append(sf[sl].getDescription());
 -            nl = true;
 -          }
 -          ;
 -          if (!Float.isNaN(sf[sl].getScore()) && sf[sl].getScore() != 0f)
 -          {
 -            tooltiptext.append(" Score = ");
 -            tooltiptext.append(sf[sl].getScore());
 -            nl = true;
 -          }
 -          ;
 -          if (sf[sl].getStatus() != null && sf[sl].getStatus().length() > 0)
 -          {
 -            tooltiptext.append(" (");
 -            tooltiptext.append(sf[sl].getStatus());
 -            tooltiptext.append(")");
 -            nl = true;
 -          }
 -          ;
 -          if (nl)
 -          {
 -            tooltiptext.append("\n");
 -          }
 -        }
 +        tooltiptext.append(" ");
 +        tooltiptext.append(sf.getType());
 +        nl = true;
 +      }
 +      if (sf.getDescription() != null)
 +      {
 +        tooltiptext.append(" ");
 +        tooltiptext.append(sf.getDescription());
 +        nl = true;
 +      }
 +      if (!Float.isNaN(sf.getScore()) && sf.getScore() != 0f)
 +      {
 +        tooltiptext.append(" Score = ");
 +        tooltiptext.append(sf.getScore());
 +        nl = true;
 +      }
 +      if (sf.getStatus() != null && sf.getStatus().length() > 0)
 +      {
 +        tooltiptext.append(" (");
 +        tooltiptext.append(sf.getStatus());
 +        tooltiptext.append(")");
 +        nl = true;
 +      }
 +      if (nl)
 +      {
 +        tooltiptext.append("\n");
        }
      }
 +
      if (tooltiptext.length() == 0)
      {
        // nothing to display - so clear tooltip if one is visible
  
      if ((e.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
      {
 -      Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq);
 +      SequenceI sq = av.getAlignment().getSequenceAt(seq);
  
 -      // build a new links menu based on the current links + any non-positional
 -      // features
 +      /*
 +       *  build a new links menu based on the current links
 +       *  and any non-positional features
 +       */
        List<String> nlinks;
        if (urlProvider != null)
        {
        {
          nlinks = new ArrayList<String>();
        }
 -      SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures();
 -      for (int sl = 0; sf != null && sl < sf.length; sl++)
 +
 +      for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
        {
 -        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
 +        if (sf.links != null)
          {
 -          if (sf[sl].links != null && sf[sl].links.size() > 0)
 +          for (String link : sf.links)
            {
 -            for (int l = 0, lSize = sf[sl].links.size(); l < lSize; l++)
 -            {
 -              nlinks.add(sf[sl].links.elementAt(l));
 -            }
 +            nlinks.add(link);
            }
          }
        }
      if (av.getRanges().getStartSeq() > index
              || av.getRanges().getEndSeq() < index)
      {
-       alignPanel.setScrollValues(av.getRanges().getStartRes(), index);
+       av.getRanges().setStartSeq(index);
      }
    }
  
  
      boolean up = true;
  
 -    public ScrollThread(boolean up)
 +    public ScrollThread(boolean isUp)
      {
 -      this.up = up;
 +      this.up = isUp;
        start();
      }
  
        running = true;
        while (running)
        {
-         if (alignPanel.scrollUp(up))
+         if (av.getRanges().scrollUp(up))
          {
            // scroll was ok, so add new sequence to selection
            int seq = av.getRanges().getStartSeq();
@@@ -39,6 -39,7 +39,7 @@@ import jalview.structure.SelectionSourc
  import jalview.structure.SequenceListener;
  import jalview.structure.StructureSelectionManager;
  import jalview.structure.VamsasSource;
+ import jalview.util.Comparison;
  import jalview.util.MappingUtils;
  import jalview.util.MessageManager;
  import jalview.viewmodel.AlignmentViewport;
@@@ -53,7 -54,6 +54,7 @@@ import java.awt.event.InputEvent
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
 +import java.util.List;
  import java.util.Vector;
  
  public class SeqPanel extends Panel implements MouseMotionListener,
      endEditing();
      if (av.getWrapAlignment())
      {
-       ap.scrollToWrappedVisible(seqCanvas.cursorX);
+       av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
      }
      else
      {
        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
        while (seqCanvas.cursorY < ranges.getStartSeq())
        {
-         ap.scrollUp(true);
+         ranges.scrollUp(true);
        }
        while (seqCanvas.cursorY > ranges.getEndSeq())
        {
-         ap.scrollUp(false);
+         ranges.scrollUp(false);
        }
        while (seqCanvas.cursorX < hidden.adjustForHiddenColumns(ranges
                .getStartRes()))
        {
  
-         if (!ap.scrollRight(false))
+         if (!ranges.scrollRight(false))
          {
            break;
          }
        while (seqCanvas.cursorX > hidden.adjustForHiddenColumns(ranges
                .getEndRes()))
        {
-         if (!ap.scrollRight(true))
+         if (!ranges.scrollRight(true))
          {
            break;
          }
     * 
     * @param sequence
     *          aligned sequence object
-    * @param res
+    * @param column
     *          alignment column
     * @param seq
     *          index of sequence in alignment
-    * @return position of res in sequence
+    * @return position of column in sequence or -1 if at gap
     */
-   void setStatusMessage(SequenceI sequence, int res, int seq)
+   void setStatusMessage(SequenceI sequence, int column, int seq)
    {
      // TODO remove duplication of identical gui method
      StringBuilder text = new StringBuilder(32);
      /*
       * Try to translate the display character to residue name (null for gap).
       */
-     final String displayChar = String.valueOf(sequence.getCharAt(res));
+     final String displayChar = String.valueOf(sequence.getCharAt(column));
      if (av.getAlignment().isNucleotide())
      {
        residue = ResidueProperties.nucleotideName.get(displayChar);
      int pos = -1;
      if (residue != null)
      {
-       pos = sequence.findPosition(res);
+       pos = sequence.findPosition(column);
        text.append(" (").append(Integer.toString(pos)).append(")");
      }
  
          av.setSelectionGroup(null);
        }
  
 -      SequenceFeature[] features = findFeaturesAtRes(sequence,
 +      List<SequenceFeature> features = findFeaturesAtRes(sequence,
                sequence.findPosition(findRes(evt)));
  
 -      if (features != null && features.length > 0)
 +      if (!features.isEmpty())
        {
          SearchResultsI highlight = new SearchResults();
 -        highlight.addResult(sequence, features[0].getBegin(),
 -                features[0].getEnd());
 +        highlight.addResult(sequence, features.get(0).getBegin(), features
 +                .get(0).getEnd());
          seqCanvas.highlightSearchResults(highlight);
 -      }
 -      if (features != null && features.length > 0)
 -      {
 +        SequenceFeature[] featuresArray = features
 +                .toArray(new SequenceFeature[features.size()]);
          seqCanvas.getFeatureRenderer().amendFeatures(
 -                new SequenceI[] { sequence }, features, false, ap);
 +                new SequenceI[] { sequence }, featuresArray, false, ap);
  
          seqCanvas.highlightSearchResults(null);
        }
    {
      if (av.isFollowHighlight())
      {
+       // don't allow highlight of protein/cDNA to also scroll a complementary
+       // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
+       // over residue to change abruptly, causing highlighted residue in panel 2
+       // to change, causing a scroll in panel 1 etc)
+       ap.setToScrollComplementPanel(false);
        if (ap.scrollToPosition(results, true))
        {
          ap.alignFrame.repaint();
        }
+       ap.setToScrollComplementPanel(true);
      }
      setStatusMessage(results);
      seqCanvas.highlightSearchResults(results);
    @Override
    public void mouseMoved(MouseEvent evt)
    {
-     int res = findRes(evt);
+     final int column = findRes(evt);
      int seq = findSeq(evt);
  
-     if (seq >= av.getAlignment().getHeight() || seq < 0 || res < 0)
+     if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0)
      {
        if (tooltip != null)
        {
      }
  
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-     if (res > sequence.getLength())
+     if (column > sequence.getLength())
      {
        if (tooltip != null)
        {
        return;
      }
  
-     int respos = sequence.findPosition(res);
-     if (ssm != null)
+     final char ch = sequence.getCharAt(column);
+     int respos = Comparison.isGap(ch) ? -1 : sequence.findPosition(column);
+     if (ssm != null && respos != -1)
      {
-       mouseOverSequence(sequence, res, respos);
+       mouseOverSequence(sequence, column, respos);
      }
  
      StringBuilder text = new StringBuilder();
              .append(" ID: ").append(sequence.getName());
  
      String obj = null;
-     final String ch = String.valueOf(sequence.getCharAt(res));
-     if (av.getAlignment().isNucleotide())
+     if (respos != -1)
      {
-       obj = ResidueProperties.nucleotideName.get(ch);
-       if (obj != null)
+       if (av.getAlignment().isNucleotide())
        {
-         text.append(" Nucleotide: ").append(obj);
+         obj = ResidueProperties.nucleotideName.get(ch);
+         if (obj != null)
+         {
+           text.append(" Nucleotide: ").append(obj);
+         }
+       }
+       else
+       {
+         obj = (ch == 'x' || ch == 'X') ? "X" : ResidueProperties.aa2Triplet
+                 .get(String.valueOf(ch));
+         if (obj != null)
+         {
+           text.append(" Residue: ").append(obj);
+         }
        }
-     }
-     else
-     {
-       obj = "X".equalsIgnoreCase(ch) ? "X" : ResidueProperties.aa2Triplet
-               .get(ch);
        if (obj != null)
        {
-         text.append(" Residue: ").append(obj);
+         text.append(" (").append(Integer.toString(respos)).append(")");
        }
      }
  
-     if (obj != null)
-     {
-       text.append(" (").append(Integer.toString(respos)).append(")");
-     }
      ap.alignFrame.statusBar.setText(text.toString());
  
      StringBuilder tooltipText = new StringBuilder();
      {
        for (int g = 0; g < groups.length; g++)
        {
-         if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
+         if (groups[g].getStartRes() <= column && groups[g].getEndRes() >= column)
          {
            if (!groups[g].getName().startsWith("JTreeGroup")
                    && !groups[g].getName().startsWith("JGroup"))
        }
      }
  
-     // use aa to see if the mouse pointer is on a
-     List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
-             sequence.findPosition(res));
-     int index = 0;
-     while (index < allFeatures.size())
+     /*
+      * add feature details to tooltip if over one or more features
+      */
+     if (respos != -1)
      {
-       SequenceFeature sf = allFeatures.get(index);
-       tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
-       if (sf.getDescription() != null)
 -      SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
 -              sequence.findPosition(column));
 -
 -      int index = 0;
 -      while (index < allFeatures.length)
++      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
++              respos);
++      for (SequenceFeature sf : allFeatures)
        {
-         tooltipText.append(" " + sf.getDescription());
-       }
 -        SequenceFeature sf = allFeatures[index];
 -
+         tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
  
-       if (sf.getValue("status") != null)
-       {
-         String status = sf.getValue("status").toString();
-         if (status.length() > 0)
+         if (sf.getDescription() != null)
          {
-           tooltipText.append(" (" + sf.getValue("status") + ")");
+           tooltipText.append(" " + sf.getDescription());
          }
-       }
-       tooltipText.append("\n");
  
-       index++;
+         if (sf.getValue("status") != null)
+         {
+           String status = sf.getValue("status").toString();
+           if (status.length() > 0)
+           {
+             tooltipText.append(" (" + sf.getValue("status") + ")");
+           }
+         }
+         tooltipText.append("\n");
 -
 -        index++;
+       }
      }
  
      if (tooltip == null)
      }
    }
  
 -  SequenceFeature[] findFeaturesAtRes(SequenceI sequence, int res)
 +  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
    {
 -    Vector tmp = new Vector();
 -    SequenceFeature[] features = sequence.getSequenceFeatures();
 -    if (features != null)
 -    {
 -      for (int i = 0; i < features.length; i++)
 -      {
 -        if (av.getFeaturesDisplayed() == null
 -                || !av.getFeaturesDisplayed().isVisible(
 -                        features[i].getType()))
 -        {
 -          continue;
 -        }
 -
 -        if (features[i].featureGroup != null
 -                && !seqCanvas.fr.checkGroupVisibility(
 -                        features[i].featureGroup, false))
 -        {
 -          continue;
 -        }
 -
 -        if ((features[i].getBegin() <= res)
 -                && (features[i].getEnd() >= res))
 -        {
 -          tmp.addElement(features[i]);
 -        }
 -      }
 -    }
 -
 -    features = new SequenceFeature[tmp.size()];
 -    tmp.copyInto(features);
 -
 -    return features;
 +    return seqCanvas.getFeatureRenderer().findFeaturesAtRes(sequence, res);
    }
  
    Tooltip tooltip;
      // DETECT RIGHT MOUSE BUTTON IN AWT
      if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
      {
 -      SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
 +      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
                sequence.findPosition(res));
  
        Vector<String> links = null;
 -      if (allFeatures != null)
 +      for (int i = 0; i < allFeatures.size(); i++)
        {
 -        for (int i = 0; i < allFeatures.length; i++)
 +        SequenceFeature sf = allFeatures.get(i);
 +        if (sf.links != null)
          {
 -          if (allFeatures[i].links != null)
 +          if (links == null)
            {
 -            if (links == null)
 -            {
 -              links = new Vector<String>();
 -            }
 -            for (int j = 0; j < allFeatures[i].links.size(); j++)
 -            {
 -              links.addElement(allFeatures[i].links.elementAt(j));
 -            }
 +            links = new Vector<String>();
            }
 +          links.addAll(sf.links);
          }
        }
        APopupMenu popup = new APopupMenu(ap, null, links);
            if (mouseDragging && evt.getY() < 0
                    && av.getRanges().getStartSeq() > 0)
            {
-             running = ap.scrollUp(true);
+             running = av.getRanges().scrollUp(true);
            }
  
            if (mouseDragging && evt.getY() >= getSize().height
                    && av.getAlignment().getHeight() > av.getRanges()
                            .getEndSeq())
            {
-             running = ap.scrollUp(false);
+             running = av.getRanges().scrollUp(false);
            }
  
            if (mouseDragging && evt.getX() < 0)
            {
-             running = ap.scrollRight(false);
+             running = av.getRanges().scrollRight(false);
            }
  
            else if (mouseDragging && evt.getX() >= getSize().width)
            {
-             running = ap.scrollRight(true);
+             running = av.getRanges().scrollRight(true);
            }
          }
  
@@@ -20,8 -20,6 +20,8 @@@
   */
  package jalview.datamodel;
  
 +import jalview.datamodel.features.SequenceFeaturesI;
 +
  import java.util.List;
  import java.util.Vector;
  
@@@ -176,7 -174,7 +176,7 @@@ public interface SequenceI extends ASeq
    public String getDescription();
  
    /**
 -   * Return the alignment column for a sequence position
 +   * Return the alignment column (from 1..) for a sequence position
     * 
     * @param pos
     *          lying from start to end
    public int findIndex(int pos);
  
    /**
-    * Returns the sequence position for an alignment position
+    * Returns the sequence position for an alignment position.
     * 
     * @param i
     *          column index in alignment (from 0..<length)
     * 
-    * @return residue number for residue (left of and) nearest ith column
+    * @return TODO: JAL-2562 - residue number for residue (left of and) nearest
+    *         ith column
     */
    public int findPosition(int i);
  
    /**
 +   * Returns the range of sequence positions included in the given alignment
 +   * position range. If no positions are included (the range is entirely gaps),
 +   * then returns null.
 +   * 
 +   * <pre>
 +   * Example: 
 +   * >Seq/8-13
 +   * ABC--DE-F
 +   * findPositions(1, 4) returns Range(9, 9) // B only
 +   * findPositions(3, 4) returns null // all gaps
 +   * findPositions(2, 6) returns Range(10, 12) // CDE
 +   * findPositions(3, 7) returns Range(11,12) // DE
 +   * </pre>
 +   * 
 +   * @param fromCol
 +   *          first aligned column position (base 0, inclusive)
 +   * @param toCol
 +   *          last aligned column position (base 0, inclusive)
 +   * 
 +   * @return
 +   */
 +  public Range findPositions(int fromCol, int toCol);
 +
 +  /**
     * Returns an int array where indices correspond to each residue in the
     * sequence and the element value gives its position in the alignment
     * 
    public void insertCharAt(int position, int count, char ch);
  
    /**
 -   * Gets array holding sequence features associated with this sequence. The
 -   * array may be held by the sequence's dataset sequence if that is defined.
 +   * Answers a list of all sequence features associated with this sequence. The
 +   * list may be held by the sequence's dataset sequence if that is defined.
     * 
     * @return hard reference to array
     */
 -  public SequenceFeature[] getSequenceFeatures();
 +  public List<SequenceFeature> getSequenceFeatures();
 +
 +  /**
 +   * Answers the object holding features for the sequence
 +   * 
 +   * @return
 +   */
 +  SequenceFeaturesI getFeatures();
  
    /**
 -   * Replaces the array of sequence features associated with this sequence with
 -   * a new array reference. If this sequence has a dataset sequence, then this
 -   * method will update the dataset sequence's feature array
 +   * Replaces the sequence features associated with this sequence with the given
 +   * features. If this sequence has a dataset sequence, then this method will
 +   * update the dataset sequence's features instead.
     * 
     * @param features
 -   *          New array of sequence features
     */
 -  public void setSequenceFeatures(SequenceFeature[] features);
 +  public void setSequenceFeatures(List<SequenceFeature> features);
  
    /**
     * DOCUMENT ME!
  
    /**
     * Adds the given sequence feature and returns true, or returns false if it is
 -   * already present on the sequence
 +   * already present on the sequence, or if the feature type is null.
     * 
     * @param sf
     * @return
     *         list
     */
    public List<DBRefEntry> getPrimaryDBRefs();
 +
 +  /**
 +   * Returns a (possibly empty) list of sequence features that overlap the range
 +   * from-to (inclusive), optionally restricted to one or more specified feature
 +   * types
 +   * 
 +   * @param from
 +   * @param to
 +   * @param types
 +   * @return
 +   */
 +  List<SequenceFeature> findFeatures(int from, int to, String... types);
  }
@@@ -34,9 -34,9 +34,10 @@@ import jalview.jbgui.GAlignmentPanel
  import jalview.math.AlignmentDimension;
  import jalview.schemes.ResidueProperties;
  import jalview.structure.StructureSelectionManager;
 +import jalview.util.Comparison;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
+ import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
  
  import java.awt.BorderLayout;
@@@ -49,6 -49,8 +50,8 @@@ import java.awt.Graphics
  import java.awt.Insets;
  import java.awt.event.AdjustmentEvent;
  import java.awt.event.AdjustmentListener;
+ import java.awt.event.ComponentAdapter;
+ import java.awt.event.ComponentEvent;
  import java.awt.print.PageFormat;
  import java.awt.print.Printable;
  import java.awt.print.PrinterException;
@@@ -68,7 -70,8 +71,8 @@@ import javax.swing.SwingUtilities
   * @version $Revision: 1.161 $
   */
  public class AlignmentPanel extends GAlignmentPanel implements
-         AdjustmentListener, Printable, AlignmentViewPanel
+         AdjustmentListener, Printable, AlignmentViewPanel,
+         ViewportListenerI
  {
    public AlignViewport av;
  
  
    /*
     * Flag set while scrolling to follow complementary cDNA/protein scroll. When
-    * true, suppresses invoking the same method recursively.
+    * false, suppresses invoking the same method recursively.
     */
-   private boolean dontScrollComplement;
+   private boolean scrollComplementaryPanel = true;
  
    private PropertyChangeListener propertyChangeListener;
  
      hscroll.addAdjustmentListener(this);
      vscroll.addAdjustmentListener(this);
  
+     addComponentListener(new ComponentAdapter()
+     {
+       @Override
+       public void componentResized(ComponentEvent evt)
+       {
+         // reset the viewport ranges when the alignment panel is resized
+         // in particular, this initialises the end residue value when Jalview
+         // is initialised
+         if (av.getWrapAlignment())
+         {
+           int widthInRes = getSeqPanel().seqCanvas
+                   .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+           vpRanges.setViewportWidth(widthInRes);
+         }
+         else
+         {
+           int widthInRes = getSeqPanel().seqCanvas.getWidth()
+                   / av.getCharWidth();
+           int heightInSeq = getSeqPanel().seqCanvas.getHeight()
+                   / av.getCharHeight();
+           
+           vpRanges.setViewportWidth(widthInRes);
+           vpRanges.setViewportHeight(heightInSeq);
+         }
+       }
+     });
      final AlignmentPanel ap = this;
      propertyChangeListener = new PropertyChangeListener()
      {
        }
      };
      av.addPropertyChangeListener(propertyChangeListener);
+     av.getRanges().addPropertyChangeListener(this);
      fontChanged();
      adjustAnnotationHeight();
      updateLayout();
      getIdPanel().getIdCanvas().setPreferredSize(d);
      hscrollFillerPanel.setPreferredSize(d);
  
-     if (overviewPanel != null)
-     {
-       overviewPanel.setBoxPosition();
-     }
      if (this.alignFrame.getSplitViewContainer() != null)
      {
        ((SplitFrame) this.alignFrame.getSplitViewContainer()).adjustLayout();
        }
        int start = r[0];
        int end = r[1];
-       // DEBUG
-       // System.err.println(this.av.viewName + " Seq : " + seqIndex
-       // + " Scroll to " + start + "," + end);
  
        /*
         * To centre results, scroll to positions half the visible width
         */
        seqIndex = Math.max(0, seqIndex - verticalOffset);
  
-       // System.out.println("start=" + start + ", end=" + end + ", startv="
-       // + av.getStartRes() + ", endv=" + av.getEndRes() + ", starts="
-       // + av.getStartSeq() + ", ends=" + av.getEndSeq());
        if (!av.getWrapAlignment())
        {
          if ((startv = vpRanges.getStartRes()) >= start)
            /*
             * Scroll left to make start of search results visible
             */
-           // setScrollValues(start - 1, seqIndex); // plus one residue
            setScrollValues(start, seqIndex);
          }
          else if ((endv = vpRanges.getEndRes()) <= end)
            /*
             * Scroll right to make end of search results visible
             */
-           // setScrollValues(startv + 1 + end - endv, seqIndex); // plus one
            setScrollValues(startv + end - endv, seqIndex);
          }
          else if ((starts = vpRanges.getStartSeq()) > seqIndex)
        }
        else
        {
-         scrollToWrappedVisible(start);
+         vpRanges.scrollToWrappedVisible(start);
        }
      }
-     if (redrawOverview && overviewPanel != null)
-     {
-       overviewPanel.setBoxPosition();
-     }
      paintAlignment(redrawOverview);
      return true;
    }
  
-   void scrollToWrappedVisible(int res)
-   {
-     int cwidth = getSeqPanel().seqCanvas
-             .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-     if (res < vpRanges.getStartRes()
-             || res >= (vpRanges.getStartRes() + cwidth))
-     {
-       vscroll.setValue((res / cwidth));
-       vpRanges.setStartRes(vscroll.getValue() * cwidth);
-     }
-   }
    /**
     * DOCUMENT ME!
     * 
        annotationSpaceFillerHolder.setVisible(true);
      }
  
-     idSpaceFillerPanel1.setVisible(!wrap);
-     repaint();
-   }
-   // return value is true if the scroll is valid
-   public boolean scrollUp(boolean up)
-   {
-     if (up)
+     if (wrap)
      {
-       if (vscroll.getValue() < 1)
-       {
-         return false;
-       }
-       fastPaint = false;
-       vscroll.setValue(vscroll.getValue() - 1);
+       int widthInRes = getSeqPanel().seqCanvas
+               .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+       vpRanges.setViewportWidth(widthInRes);
      }
      else
      {
-       if ((vextent + vscroll.getValue()) >= av.getAlignment().getHeight())
-       {
-         return false;
-       }
+       int widthInRes = (getSeqPanel().seqCanvas.getWidth() / av
+               .getCharWidth()) - 1;
+       int heightInSeq = (getSeqPanel().seqCanvas.getHeight() / av
+               .getCharHeight()) - 1;
  
-       fastPaint = false;
-       vscroll.setValue(vscroll.getValue() + 1);
+       vpRanges.setViewportWidth(widthInRes);
+       vpRanges.setViewportHeight(heightInSeq);
      }
  
-     fastPaint = true;
+     idSpaceFillerPanel1.setVisible(!wrap);
  
-     return true;
+     repaint();
    }
  
-   /**
-    * DOCUMENT ME!
-    * 
-    * @param right
-    *          DOCUMENT ME!
-    * 
-    * @return DOCUMENT ME!
-    */
-   public boolean scrollRight(boolean right)
-   {
-     if (!right)
-     {
-       if (hscroll.getValue() < 1)
-       {
-         return false;
-       }
-       fastPaint = false;
-       hscroll.setValue(hscroll.getValue() - 1);
-     }
-     else
-     {
-       if ((hextent + hscroll.getValue()) >= av.getAlignment().getWidth())
-       {
-         return false;
-       }
-       fastPaint = false;
-       hscroll.setValue(hscroll.getValue() + 1);
-     }
-     fastPaint = true;
-     return true;
-   }
  
    /**
     * Adjust row/column scrollers to show a visible position in the alignment.
     *          visible row to scroll to
     * 
     */
-   public void setScrollValues(int x, int y)
+   public void setScrollValues(int xpos, int ypos)
    {
+     int x = xpos;
+     int y = ypos;
      if (av == null || av.getAlignment() == null)
      {
        return;
      }
-     int width = av.getAlignment().getWidth();
-     int height = av.getAlignment().getHeight();
  
-     if (av.hasHiddenColumns())
+     if (av.getWrapAlignment())
      {
-       // reset the width to exclude hidden columns
-       width = av.getAlignment().getHiddenColumns()
-               .findColumnPosition(width);
+       setScrollingForWrappedPanel(x);
      }
+     else
+     {
+       int width = av.getAlignment().getWidth();
+       int height = av.getAlignment().getHeight();
  
-     hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
-     vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
+       if (av.hasHiddenColumns())
+       {
+         // reset the width to exclude hidden columns
+         width = av.getAlignment().getHiddenColumns().findColumnPosition(width);
+       }
  
-     if (hextent > width)
-     {
-       hextent = width;
-     }
+       hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
+       vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
  
-     if (vextent > height)
-     {
-       vextent = height;
-     }
+       if (hextent > width)
+       {
+         hextent = width;
+       }
  
-     if ((hextent + x) > width)
-     {
-       x = width - hextent;
-     }
+       if (vextent > height)
+       {
+         vextent = height;
+       }
  
-     if ((vextent + y) > height)
-     {
-       y = height - vextent;
-     }
+       if ((hextent + x) > width)
+       {
+         x = width - hextent;
+       }
  
-     if (y < 0)
-     {
-       y = 0;
-     }
+       if ((vextent + y) > height)
+       {
+         y = height - vextent;
+       }
  
-     if (x < 0)
-     {
-       x = 0;
-     }
+       if (y < 0)
+       {
+         y = 0;
+       }
  
-     // update endRes after x has (possibly) been adjusted
-     vpRanges.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
-             .getCharWidth())) - 1);
+       if (x < 0)
+       {
+         x = 0;
+       }
  
-     /*
-      * each scroll adjustment triggers adjustmentValueChanged, which resets the
-      * 'do not scroll complement' flag; ensure it is the same for both
-      * operations
-      */
-     boolean flag = isDontScrollComplement();
-     hscroll.setValues(x, hextent, 0, width);
-     setDontScrollComplement(flag);
-     vscroll.setValues(y, vextent, 0, height);
+       // update the scroll values
+       hscroll.setValues(x, hextent, 0, width);
+       vscroll.setValues(y, vextent, 0, height);
+     }
    }
  
    /**
-    * DOCUMENT ME!
+    * Respond to adjustment event when horizontal or vertical scrollbar is
+    * changed
     * 
     * @param evt
-    *          DOCUMENT ME!
+    *          adjustment event encoding whether hscroll or vscroll changed
     */
    @Override
    public void adjustmentValueChanged(AdjustmentEvent evt)
    {
      int oldX = vpRanges.getStartRes();
+     int oldwidth = vpRanges.getViewportWidth();
      int oldY = vpRanges.getStartSeq();
+     int oldheight = vpRanges.getViewportHeight();
  
-     if (evt.getSource() == hscroll)
-     {
-       int x = hscroll.getValue();
-       vpRanges.setStartRes(x);
-       vpRanges.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
-               .getCharWidth())) - 1);
-     }
-     if (evt.getSource() == vscroll)
+     if (av.getWrapAlignment())
      {
-       int offy = vscroll.getValue();
-       if (av.getWrapAlignment())
+       if (evt.getSource() == hscroll)
        {
-         if (offy > -1)
+         return; // no horizontal scroll when wrapped
+       }
+       else if (evt.getSource() == vscroll)
+       {
+         int offy = vscroll.getValue();
+         int rowSize = getSeqPanel().seqCanvas
+                 .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+         // if we're scrolling to the position we're already at, stop
+         // this prevents infinite recursion of events when the scroll/viewport
+         // ranges values are the same
+         if ((offy * rowSize == oldX) && (oldwidth == rowSize))
          {
-           int rowSize = getSeqPanel().seqCanvas
-                   .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-           vpRanges.setStartRes(offy * rowSize);
-           vpRanges.setEndRes((offy + 1) * rowSize);
+           return;
          }
-         else
+         else if (offy > -1)
          {
-           // This is only called if file loaded is a jar file that
-           // was wrapped when saved and user has wrap alignment true
-           // as preference setting
-           SwingUtilities.invokeLater(new Runnable()
-           {
-             @Override
-             public void run()
-             {
-               setScrollValues(vpRanges.getStartRes(),
-                       vpRanges.getStartSeq());
-             }
-           });
+           vpRanges.setViewportStartAndWidth(offy * rowSize, rowSize);
          }
        }
        else
        {
-         vpRanges.setStartSeq(offy);
-         vpRanges.setEndSeq(offy
-                 + (getSeqPanel().seqCanvas.getHeight() / av.getCharHeight())
-                 - 1);
+         // This is only called if file loaded is a jar file that
+         // was wrapped when saved and user has wrap alignment true
+         // as preference setting
+         SwingUtilities.invokeLater(new Runnable()
+         {
+           @Override
+           public void run()
+         {
+             // When updating scrolling to use ViewportChange events, this code
+             // could not be validated and it is not clear if it is now being
+             // called. Log warning here in case it is called and unforeseen
+             // problems occur
+             Cache.log
+                     .warn("Unexpected path through code: Wrapped jar file opened with wrap alignment set in preferences");
+             // scroll to start of panel
+             vpRanges.setStartRes(0);
+             vpRanges.setStartSeq(0);
+           }
+         });
        }
-     }
-     if (overviewPanel != null)
-     {
-       overviewPanel.setBoxPosition();
-     }
-     int scrollX = vpRanges.getStartRes() - oldX;
-     int scrollY = vpRanges.getStartSeq() - oldY;
-     if (av.getWrapAlignment() || !fastPaint)
-     {
        repaint();
      }
      else
      {
-       // Make sure we're not trying to draw a panel
-       // larger than the visible window
-       if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
+       // horizontal scroll
+       if (evt.getSource() == hscroll)
        {
-         scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
-       }
-       else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
-       {
-         scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
-       }
+         int x = hscroll.getValue();
+         int width = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
  
-       if (scrollX != 0 || scrollY != 0)
+         // if we're scrolling to the position we're already at, stop
+         // this prevents infinite recursion of events when the scroll/viewport
+         // ranges values are the same
+         if ((x == oldX) && (width == oldwidth))
+         {
+           return;
+         }
+         vpRanges.setViewportStartAndWidth(x, width);
+       }
+       else if (evt.getSource() == vscroll)
        {
-         getIdPanel().getIdCanvas().fastPaint(scrollY);
-         getSeqPanel().seqCanvas.fastPaint(scrollX, scrollY);
-         getScalePanel().repaint();
-         if (av.isShowAnnotation() && scrollX != 0)
+         int y = vscroll.getValue();
+         int height = getSeqPanel().seqCanvas.getHeight()
+                 / av.getCharHeight();
+         // if we're scrolling to the position we're already at, stop
+         // this prevents infinite recursion of events when the scroll/viewport
+         // ranges values are the same
+         if ((y == oldY) && (height == oldheight))
          {
-           getAnnotationPanel().fastPaint(scrollX);
+           return;
          }
+         vpRanges.setViewportStartAndHeight(y, height);
+       }
+       if (!fastPaint)
+       {
+         repaint();
        }
-     }
-     /*
-      * If there is one, scroll the (Protein/cDNA) complementary alignment to
-      * match, unless we are ourselves doing that.
-      */
-     if (isDontScrollComplement())
-     {
-       setDontScrollComplement(false);
-     }
-     else
-     {
-       av.scrollComplementaryAlignment();
      }
    }
  
      validate();
  
      /*
-      * set scroll bar positions; first suppress this being 'followed' in any
-      * complementary split pane
+      * set scroll bar positions
       */
-     setDontScrollComplement(true);
+     setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
+   }
  
-     if (av.getWrapAlignment())
+   /*
+    * Set vertical scroll bar parameters for wrapped panel
+    * @param res 
+    *    the residue to scroll to
+    */
+   private void setScrollingForWrappedPanel(int res)
+   {
+     // get the width of the alignment in residues
+     int maxwidth = av.getAlignment().getWidth();
+     if (av.hasHiddenColumns())
      {
-       int maxwidth = av.getAlignment().getWidth();
-       if (av.hasHiddenColumns())
-       {
          maxwidth = av.getAlignment().getHiddenColumns()
                  .findColumnPosition(maxwidth) - 1;
-       }
-       int canvasWidth = getSeqPanel().seqCanvas
-               .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-       if (canvasWidth > 0)
-       {
-         int max = maxwidth
-                 / getSeqPanel().seqCanvas
-                         .getWrappedCanvasWidth(getSeqPanel().seqCanvas
-                                 .getWidth()) + 1;
-         vscroll.setMaximum(max);
-         vscroll.setUnitIncrement(1);
-         vscroll.setVisibleAmount(1);
-       }
      }
-     else
+     // get the width of the canvas in residues
+     int canvasWidth = getSeqPanel().seqCanvas
+             .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+     if (canvasWidth > 0)
      {
-       setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
+       // position we want to scroll to is number of canvasWidth's to get there
+       int current = res / canvasWidth;
+       // max scroll position: add one because extent is 1 and scrollbar value
+       // can only be set to at most max - extent
+       int max = maxwidth / canvasWidth + 1;
+       vscroll.setUnitIncrement(1);
+       vscroll.setValues(current, 1, 0, max);
      }
    }
  
            sy = s * av.getCharHeight() + scaleHeight;
  
            SequenceI seq = av.getAlignment().getSequenceAt(s);
            SequenceGroup[] groups = av.getAlignment().findAllGroups(seq);
            for (res = 0; res < alwidth; res++)
            {
 -            StringBuilder text = new StringBuilder();
 +            StringBuilder text = new StringBuilder(512);
              String triplet = null;
              if (av.getAlignment().isNucleotide())
              {
                continue;
              }
  
 -            int alIndex = seq.findPosition(res);
 +            int seqPos = seq.findPosition(res);
              gSize = groups.length;
              for (g = 0; g < gSize; g++)
              {
                          .append((idWidth + (res + 1) * av.getCharWidth()))
                          .append(",").append((av.getCharHeight() + sy))
                          .append("\"").append(" onMouseOver=\"toolTip('")
 -                        .append(alIndex).append(" ").append(triplet);
 +                        .append(seqPos).append(" ").append(triplet);
                }
  
                if (groups[g].getStartRes() < res
                }
              }
  
 -            if (features != null)
 +            if (text.length() < 1)
              {
 -              if (text.length() < 1)
 -              {
 -                text.append("<area shape=\"rect\" coords=\"")
 -                        .append((idWidth + res * av.getCharWidth()))
 -                        .append(",").append(sy).append(",")
 -                        .append((idWidth + (res + 1) * av.getCharWidth()))
 -                        .append(",").append((av.getCharHeight() + sy))
 -                        .append("\"").append(" onMouseOver=\"toolTip('")
 -                        .append(alIndex).append(" ").append(triplet);
 -              }
 -              fSize = features.length;
 -              for (f = 0; f < fSize; f++)
 +              text.append("<area shape=\"rect\" coords=\"")
 +                      .append((idWidth + res * av.getCharWidth()))
 +                      .append(",").append(sy).append(",")
 +                      .append((idWidth + (res + 1) * av.getCharWidth()))
 +                      .append(",").append((av.getCharHeight() + sy))
 +                      .append("\"").append(" onMouseOver=\"toolTip('")
 +                      .append(seqPos).append(" ").append(triplet);
 +            }
 +            if (!Comparison.isGap(seq.getCharAt(res)))
 +            {
 +              List<SequenceFeature> features = seq.getFeatures()
 +                      .findFeatures(seqPos, seqPos);
 +              for (SequenceFeature sf : features)
                {
 -
 -                if ((features[f].getBegin() <= seq.findPosition(res))
 -                        && (features[f].getEnd() >= seq.findPosition(res)))
 +                if (sf.isContactFeature())
                  {
 -                  if (features[f].isContactFeature())
 -                  {
 -                    if (features[f].getBegin() == seq.findPosition(res)
 -                            || features[f].getEnd() == seq
 -                                    .findPosition(res))
 -                    {
 -                      text.append("<br>").append(features[f].getType())
 -                              .append(" ").append(features[f].getBegin())
 -                              .append(":").append(features[f].getEnd());
 -                    }
 -                  }
 -                  else
 +                  text.append("<br>").append(sf.getType()).append(" ")
 +                          .append(sf.getBegin()).append(":")
 +                          .append(sf.getEnd());
 +                }
 +                else
 +                {
 +                  text.append("<br>");
 +                  text.append(sf.getType());
 +                  String description = sf.getDescription();
 +                  if (description != null
 +                          && !sf.getType().equals(description))
                    {
 -                    text.append("<br>");
 -                    text.append(features[f].getType());
 -                    if (features[f].getDescription() != null
 -                            && !features[f].getType().equals(
 -                                    features[f].getDescription()))
 -                    {
 -                      text.append(" ").append(features[f].getDescription());
 -                    }
 -
 -                    if (features[f].getValue("status") != null)
 -                    {
 -                      text.append(" (").append(features[f].getValue("status"))
 -                              .append(")");
 -                    }
 +                    description = description.replace("\"", "&quot;");
 +                    text.append(" ").append(description);
                    }
                  }
 -
 +                String status = sf.getStatus();
 +                if (status != null && !"".equals(status))
 +                {
 +                  text.append(" (").append(status).append(")");
 +                }
 +              }
 +              if (text.length() > 1)
 +              {
 +                text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
 +                out.println(text.toString());
                }
 -            }
 -            if (text.length() > 1)
 -            {
 -              text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
 -              out.println(text.toString());
              }
            }
          }
     * 
     * @param b
     */
-   protected void setDontScrollComplement(boolean b)
+   protected void setToScrollComplementPanel(boolean b)
    {
-     this.dontScrollComplement = b;
+     this.scrollComplementaryPanel = b;
    }
  
-   protected boolean isDontScrollComplement()
+   /**
+    * Get whether to scroll complement panel
+    * 
+    * @return true if cDNA/protein complement panels should be scrolled
+    */
+   protected boolean isSetToScrollComplementPanel()
    {
-     return this.dontScrollComplement;
+     return this.scrollComplementaryPanel;
    }
  
    /**
      }
    }
  
+   @Override
+   /**
+    * Property change event fired when a change is made to the viewport ranges 
+    * object associated with this alignment panel's viewport
+    */
+   public void propertyChange(PropertyChangeEvent evt)
+   {
+     // update this panel's scroll values based on the new viewport ranges values
+     int x = vpRanges.getStartRes();
+     int y = vpRanges.getStartSeq();
+     setScrollValues(x, y);
+     // now update any complementary alignment (its viewport ranges object
+     // is different so does not get automatically updated)
+     if (isSetToScrollComplementPanel())
+     {
+       setToScrollComplementPanel(false);
+       av.scrollComplementaryAlignment();
+       setToScrollComplementPanel(true);
+     }
+   }
    /**
     * Set the reference to the PCA/Tree chooser dialog for this panel. This
     * reference should be nulled when the dialog is closed.
@@@ -152,22 -152,22 +152,22 @@@ public class IdPanel extends JPanel imp
      {
        if (e.isShiftDown())
        {
-         alignPanel.scrollRight(true);
+         av.getRanges().scrollRight(true);
        }
        else
        {
-         alignPanel.scrollUp(false);
+         av.getRanges().scrollUp(false);
        }
      }
      else
      {
        if (e.isShiftDown())
        {
-         alignPanel.scrollRight(false);
+         av.getRanges().scrollRight(false);
        }
        else
        {
-         alignPanel.scrollUp(true);
+         av.getRanges().scrollUp(true);
        }
      }
    }
    {
      int seq2 = alignPanel.getSeqPanel().findSeq(e);
      Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
 -    // build a new links menu based on the current links + any non-positional
 -    // features
 +
 +    /*
 +     *  build a new links menu based on the current links
 +     *  and any non-positional features
 +     */
      List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
 -    SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures();
 -    if (sfs != null)
 +    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
      {
 -      for (SequenceFeature sf : sfs)
 +      if (sf.links != null)
        {
 -        if (sf.begin == sf.end && sf.begin == 0)
 +        for (String link : sf.links)
          {
 -          if (sf.links != null && sf.links.size() > 0)
 -          {
 -            for (int l = 0, lSize = sf.links.size(); l < lSize; l++)
 -            {
 -              nlinks.add(sf.links.elementAt(l));
 -            }
 -          }
 +          nlinks.add(link);
          }
        }
      }
      if ((av.getRanges().getStartSeq() > index)
              || (av.getRanges().getEndSeq() < index))
      {
-       alignPanel.setScrollValues(av.getRanges().getStartRes(), index);
+       av.getRanges().setStartSeq(index);
      }
    }
  
  
        while (running)
        {
-         if (alignPanel.scrollUp(up))
+         if (av.getRanges().scrollUp(up))
          {
            // scroll was ok, so add new sequence to selection
            int seq = av.getRanges().getStartSeq();
@@@ -32,7 -32,6 +32,7 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.GraphLine;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.RnaViewerModel;
 +import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.StructureViewerModel;
@@@ -883,43 -882,48 +883,43 @@@ public class Jalview2XM
  
        // TODO: omit sequence features from each alignment view's XML dump if we
        // are storing dataset
 -      if (jds.getSequenceFeatures() != null)
 +      List<jalview.datamodel.SequenceFeature> sfs = jds
 +              .getSequenceFeatures();
 +      for (SequenceFeature sf : sfs)
        {
 -        jalview.datamodel.SequenceFeature[] sf = jds.getSequenceFeatures();
 -        int index = 0;
 -        while (index < sf.length)
 -        {
 -          Features features = new Features();
 +        Features features = new Features();
  
 -          features.setBegin(sf[index].getBegin());
 -          features.setEnd(sf[index].getEnd());
 -          features.setDescription(sf[index].getDescription());
 -          features.setType(sf[index].getType());
 -          features.setFeatureGroup(sf[index].getFeatureGroup());
 -          features.setScore(sf[index].getScore());
 -          if (sf[index].links != null)
 +        features.setBegin(sf.getBegin());
 +        features.setEnd(sf.getEnd());
 +        features.setDescription(sf.getDescription());
 +        features.setType(sf.getType());
 +        features.setFeatureGroup(sf.getFeatureGroup());
 +        features.setScore(sf.getScore());
 +        if (sf.links != null)
 +        {
 +          for (int l = 0; l < sf.links.size(); l++)
            {
 -            for (int l = 0; l < sf[index].links.size(); l++)
 -            {
 -              OtherData keyValue = new OtherData();
 -              keyValue.setKey("LINK_" + l);
 -              keyValue.setValue(sf[index].links.elementAt(l).toString());
 -              features.addOtherData(keyValue);
 -            }
 +            OtherData keyValue = new OtherData();
 +            keyValue.setKey("LINK_" + l);
 +            keyValue.setValue(sf.links.elementAt(l).toString());
 +            features.addOtherData(keyValue);
            }
 -          if (sf[index].otherDetails != null)
 +        }
 +        if (sf.otherDetails != null)
 +        {
 +          String key;
 +          Iterator<String> keys = sf.otherDetails.keySet().iterator();
 +          while (keys.hasNext())
            {
 -            String key;
 -            Iterator<String> keys = sf[index].otherDetails.keySet()
 -                    .iterator();
 -            while (keys.hasNext())
 -            {
 -              key = keys.next();
 -              OtherData keyValue = new OtherData();
 -              keyValue.setKey(key);
 -              keyValue.setValue(sf[index].otherDetails.get(key).toString());
 -              features.addOtherData(keyValue);
 -            }
 +            key = keys.next();
 +            OtherData keyValue = new OtherData();
 +            keyValue.setKey(key);
 +            keyValue.setValue(sf.otherDetails.get(key).toString());
 +            features.addOtherData(keyValue);
            }
 -
 -          jseq.addFeatures(features);
 -          index++;
          }
 +
 +        jseq.addFeatures(features);
        }
  
        if (jdatasq.getAllPDBEntries() != null)
            Features[] features = jseqs[i].getFeatures();
            for (int f = 0; f < features.length; f++)
            {
 -            jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
 -                    features[f].getType(), features[f].getDescription(),
 -                    features[f].getStatus(), features[f].getBegin(),
 -                    features[f].getEnd(), features[f].getFeatureGroup());
 -
 -            sf.setScore(features[f].getScore());
 +            SequenceFeature sf = new SequenceFeature(features[f].getType(),
 +                    features[f].getDescription(), features[f].getBegin(),
 +                    features[f].getEnd(), features[f].getScore(),
 +                    features[f].getFeatureGroup());
 +            sf.setStatus(features[f].getStatus());
              for (int od = 0; od < features[f].getOtherDataCount(); od++)
              {
                OtherData keyValue = features[f].getOtherData(od);
      af.viewport.setShowUnconserved(view.hasShowUnconserved() ? view
              .isShowUnconserved() : false);
      af.viewport.getRanges().setStartRes(view.getStartRes());
-     af.viewport.getRanges().setStartSeq(view.getStartSeq());
+     // startSeq set in af.alignPanel.updateLayout below
      af.alignPanel.updateLayout();
      ColourSchemeI cs = null;
      // apply colourschemes
@@@ -36,7 -36,6 +36,7 @@@ import jalview.binding.Tree
  import jalview.binding.UserColours;
  import jalview.binding.Viewport;
  import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceFeature;
  import jalview.io.FileFormat;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemeProperty;
@@@ -225,10 -224,11 +225,10 @@@ public class Jalview2XML_V
          Features[] features = JSEQ[i].getFeatures();
          for (int f = 0; f < features.length; f++)
          {
 -          jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
 -                  features[f].getType(), features[f].getDescription(),
 -                  features[f].getStatus(), features[f].getBegin(),
 +          SequenceFeature sf = new SequenceFeature(features[f].getType(),
 +                  features[f].getDescription(), features[f].getBegin(),
                    features[f].getEnd(), null);
 -
 +          sf.setStatus(features[f].getStatus());
            al.getSequenceAt(i).getDatasetSequence().addSequenceFeature(sf);
          }
        }
      af.setBounds(view.getXpos(), view.getYpos(), view.getWidth(),
              view.getHeight());
      af.viewport.getRanges().setStartRes(view.getStartRes());
-     af.viewport.getRanges().setStartSeq(view.getStartSeq());
+     // startSeq set in af.alignPanel.updateLayout below
      af.viewport.setShowAnnotation(view.getShowAnnotation());
      af.viewport.setAbovePIDThreshold(view.getPidSelected());
      af.viewport.setColourText(view.getShowColourText());
@@@ -84,16 -84,6 +84,16 @@@ public class SeqPanel extends JPanel im
    /** DOCUMENT ME!! */
    public AlignmentPanel ap;
  
 +  /*
 +   * last column position for mouseMoved event
 +   */
 +  private int lastMouseColumn;
 +
 +  /*
 +   * last sequence offset for mouseMoved event
 +   */
 +  private int lastMouseSeq;
 +
    protected int lastres;
  
    protected int startseq;
        ssm.addStructureViewerListener(this);
        ssm.addSelectionListener(this);
      }
 +
 +    lastMouseColumn = -1;
 +    lastMouseSeq = -1;
    }
  
    int startWrapBlock = -1;
      endEditing();
      if (av.getWrapAlignment())
      {
-       ap.scrollToWrappedVisible(seqCanvas.cursorX);
+       av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
      }
      else
      {
-       while (seqCanvas.cursorY < av.getRanges().getStartSeq())
-       {
-         ap.scrollUp(true);
-       }
-       while (seqCanvas.cursorY > av.getRanges().getEndSeq())
-       {
-         ap.scrollUp(false);
-       }
-       if (!av.getWrapAlignment())
-       {
-         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-         while (seqCanvas.cursorX < hidden.adjustForHiddenColumns(av
-                 .getRanges().getStartRes()))
-         {
-           if (!ap.scrollRight(false))
-           {
-             break;
-           }
-         }
-         while (seqCanvas.cursorX > hidden.adjustForHiddenColumns(av
-                 .getRanges().getEndRes()))
-         {
-           if (!ap.scrollRight(true))
-           {
-             break;
-           }
-         }
-       }
+       av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY,
+               av);
      }
      setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
              seqCanvas.cursorX, seqCanvas.cursorY);
  
      if (av.isFollowHighlight())
      {
-       /*
-        * if scrollToPosition requires a scroll adjustment, this flag prevents
-        * another scroll event being propagated back to the originator
-        * 
-        * @see AlignmentPanel#adjustmentValueChanged
-        */
-       ap.setDontScrollComplement(true);
+       // don't allow highlight of protein/cDNA to also scroll a complementary
+       // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
+       // over residue to change abruptly, causing highlighted residue in panel 2
+       // to change, causing a scroll in panel 1 etc)
+       ap.setToScrollComplementPanel(false);
        if (ap.scrollToPosition(results, false))
        {
          seqCanvas.revalidate();
        }
+       ap.setToScrollComplementPanel(true);
      }
      setStatusMessage(results);
      seqCanvas.highlightSearchResults(results);
      {
        return;
      }
 +    if (column == lastMouseColumn && seq == lastMouseSeq)
 +    {
 +      /*
 +       * just a pixel move without change of residue
 +       */
 +      return;
 +    }
 +    lastMouseColumn = column;
 +    lastMouseSeq = seq;
  
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
  
        }
      }
  
-     if (av.isShowSequenceFeatures())
+     if (av.isShowSequenceFeatures() && pos != -1)
      {
        List<SequenceFeature> features = ap.getFeatureRenderer()
                .findFeaturesAtRes(sequence.getDatasetSequence(), pos);
      }
      else
      {
 -      if (lastTooltip == null
 -              || !lastTooltip.equals(tooltipText.toString()))
 +      String textString = tooltipText.toString();
 +      if (lastTooltip == null || !lastTooltip.equals(textString))
        {
          String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
 -                tooltipText.toString());
 -        // String formatedTooltipText = tooltipText.toString();
 +                textString);
          setToolTipText(formatedTooltipText);
 -        lastTooltip = tooltipText.toString();
 +        lastTooltip = textString;
        }
 -
      }
 -
    }
  
    private Point lastp = null;
    /**
     * Sets the status message in alignment panel, showing the sequence number
     * (index) and id, residue and residue position for the given sequence and
-    * column position. Returns the calculated residue position in the sequence.
+    * column position. Returns the calculated residue position in the sequence,
+    * or -1 for a gapped column position.
     * 
     * @param sequence
     *          aligned sequence object
     *          alignment column
     * @param seq
     *          index of sequence in alignment
-    * @return position of res in sequence
+    * @return position of column in sequence or -1 if at a gap
     */
    int setStatusMessage(SequenceI sequence, final int column, int seq)
    {
      {
        if (e.isShiftDown())
        {
-         ap.scrollRight(true);
+         av.getRanges().scrollRight(true);
  
        }
        else
        {
-         ap.scrollUp(false);
+         av.getRanges().scrollUp(false);
        }
      }
      else
      {
        if (e.isShiftDown())
        {
-         ap.scrollRight(false);
+         av.getRanges().scrollRight(false);
        }
        else
        {
-         ap.scrollUp(true);
+         av.getRanges().scrollUp(true);
        }
      }
      // TODO Update tooltip for new position.
            if (mouseDragging && (evt.getY() < 0)
                    && (av.getRanges().getStartSeq() > 0))
            {
-             running = ap.scrollUp(true);
+             running = av.getRanges().scrollUp(true);
            }
  
            if (mouseDragging && (evt.getY() >= getHeight())
                    && (av.getAlignment().getHeight() > av.getRanges()
                            .getEndSeq()))
            {
-             running = ap.scrollUp(false);
+             running = av.getRanges().scrollUp(false);
            }
  
            if (mouseDragging && (evt.getX() < 0))
            {
-             running = ap.scrollRight(false);
+             running = av.getRanges().scrollRight(false);
            }
            else if (mouseDragging && (evt.getX() >= getWidth()))
            {
-             running = ap.scrollRight(true);
+             running = av.getRanges().scrollRight(true);
            }
          }