Merge branch 'features/JAL-2349_matrixvis' into merge/develop_JAL-2340_matrixvis
authorJim Procter <jprocter@issues.jalview.org>
Tue, 18 Jun 2019 09:19:49 +0000 (10:19 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Tue, 18 Jun 2019 10:31:12 +0000 (11:31 +0100)
 Conflicts:
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentI.java

1  2 
src/jalview/api/AlignViewportI.java
src/jalview/appletgui/AnnotationPanel.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/AlignmentI.java
src/jalview/gui/AnnotationPanel.java
src/jalview/io/AppletFormatAdapter.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/renderer/ContactMapRenderer.java
src/jalview/viewmodel/AlignmentViewport.java

  package jalview.api;
  
  import jalview.analysis.Conservation;
 +import jalview.analysis.TreeModel;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
 -import jalview.datamodel.CigarArray;
  import jalview.datamodel.ColumnSelection;
+ import jalview.datamodel.ContactListI;
  import jalview.datamodel.ProfilesI;
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceCollectionI;
@@@ -243,6 -244,16 +244,6 @@@ public interface AlignViewportI extend
    void clearSequenceColours();
  
    /**
 -   * This method returns the visible alignment as text, as seen on the GUI, ie
 -   * if columns are hidden they will not be returned in the result. Use this for
 -   * calculating trees, PCA, redundancy etc on views which contain hidden
 -   * columns.
 -   * 
 -   * @return String[]
 -   */
 -  CigarArray getViewAsCigars(boolean selectedRegionOnly);
 -
 -  /**
     * return a compact representation of the current alignment selection to pass
     * to an analysis function
     * 
     */
    SearchResultsI getSearchResults();
  
+   ContactListI getContactList(AlignmentAnnotation _aa, int column);
    /**
     * Updates view settings with the given font. You may need to call
     * AlignmentPanel.fontChanged to update the layout geometry.
     */
    @Override
    void setProteinFontAsCdna(boolean b);
 +
 +  public abstract TreeModel getCurrentTree();
 +
 +  public abstract void setCurrentTree(TreeModel tree);
 +
 +  /**
 +   * @param update
 +   *          - set the flag for updating structures on next repaint
 +   */
 +  void setUpdateStructures(boolean update);
 +
 +  /**
 +   *
 +   * @return true if structure views will be updated on next refresh
 +   */
 +  boolean isUpdateStructures();
 +
 +  /**
 +   * check if structure views need to be updated, and clear the flag afterwards.
 +   * 
 +   * @return if an update is needed
 +   */
 +  boolean needToUpdateStructureViews();
 +
 +  /**
 +   * Adds sequencegroup to the alignment in the view. Also adds a group to the
 +   * complement view if one is defined.
 +   * 
 +   * @param sequenceGroup
 +   *          - a group defined on sequences in the alignment held by the view
 +   */
 +  void addSequenceGroup(SequenceGroup sequenceGroup);
  }
@@@ -51,9 -51,9 +51,9 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.beans.PropertyChangeEvent;
  
 -public class AnnotationPanel extends Panel implements AwtRenderPanelI,
 -        AdjustmentListener, ActionListener, MouseListener,
 -        MouseMotionListener, ViewportListenerI
 +public class AnnotationPanel extends Panel
 +        implements AwtRenderPanelI, AdjustmentListener, ActionListener,
 +        MouseListener, MouseMotionListener, ViewportListenerI
  {
    AlignViewport av;
  
  
      if (anot.length < av.getColumnSelection().getMax())
      {
 -      Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
 +      Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
 +              + 2];
        System.arraycopy(anot, 0, temp, 0, anot.length);
        anot = temp;
        aa[activeRow].annotations = anot;
            anot[index] = new Annotation(label, "", type, 0);
          }
  
 -        anot[index].secondaryStructure = type != 'S' ? type : label
 -                .length() == 0 ? ' ' : label.charAt(0);
 +        anot[index].secondaryStructure = type != 'S' ? type
 +                : label.length() == 0 ? ' ' : label.charAt(0);
          anot[index].displayCharacter = label;
        }
      }
          {
            activeRow = i;
          }
-         else if (aa[i].graph > 0)
+         else if (aa[i].graph != AlignmentAnnotation.NO_GRAPH)
          {
            // Stretch Graph
            graphStretch = i;
        }
      }
  
 -    if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK
 +    if ((evt.getModifiersEx()
 +            & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK
              && activeRow != -1)
      {
        if (av.getColumnSelection() == null
    {
      if (graphStretch > -1)
      {
 -      av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
 -              - evt.getY();
 -      if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
 +      av.getAlignment()
 +              .getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
 +                      - evt.getY();
 +      if (av.getAlignment()
 +              .getAlignmentAnnotation()[graphStretch].graphHeight < 0)
        {
 -        av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
 +        av.getAlignment()
 +                .getAlignmentAnnotation()[graphStretch].graphHeight = 0;
        }
        graphStretchY = evt.getY();
        av.calcPanelHeight();
        needValidating = true;
 -      ap.paintAlignment(true);
 +      // TODO: only update overview visible geometry
 +      ap.paintAlignment(true, false);
      }
      else
      {
      if (av.hasHiddenColumns())
      {
        column = av.getAlignment().getHiddenColumns()
 -              .adjustForHiddenColumns(column);
 +              .visibleToAbsoluteColumn(column);
      }
  
      if (row > -1 && column < aa[row].annotations.length
              String name;
              if (av.getAlignment().isNucleotide())
              {
 -              name = ResidueProperties.nucleotideName.get(String
 -                      .valueOf(residue));
 -              text.append(" Nucleotide: ").append(
 -                      name != null ? name : residue);
 +              name = ResidueProperties.nucleotideName
 +                      .get(String.valueOf(residue));
 +              text.append(" Nucleotide: ")
 +                      .append(name != null ? name : residue);
              }
              else
              {
 -              name = 'X' == residue ? "X" : ('*' == residue ? "STOP"
 -                      : ResidueProperties.aa2Triplet.get(String
 -                              .valueOf(residue)));
 -              text.append(" Residue: ").append(
 -                      name != null ? name : residue);
 +              name = 'X' == residue ? "X"
 +                      : ('*' == residue ? "STOP"
 +                              : ResidueProperties.aa2Triplet
 +                                      .get(String.valueOf(residue)));
 +              text.append(" Residue: ")
 +                      .append(name != null ? name : residue);
              }
              int residuePos = seqref.findPosition(column);
              text.append(" (").append(residuePos).append(")");
  
      gg.setColor(Color.white);
      gg.fillRect(0, 0, getSize().width, getSize().height);
 -    drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
 -            .getEndRes() + 1);
 +    drawComponent(gg, av.getRanges().getStartRes(),
 +            av.getRanges().getEndRes() + 1);
  
      g.drawImage(image, 0, 0, this);
    }
  
    public void fastPaint(int horizontal)
    {
 -    if (horizontal == 0
 +    if (horizontal == 0 || gg == null
              || av.getAlignment().getAlignmentAnnotation() == null
              || av.getAlignment().getAlignmentAnnotation().length < 1)
      {
  
      gg.copyArea(0, 0, imgWidth, getSize().height,
              -horizontal * av.getCharWidth(), 0);
 -    int sr = av.getRanges().getStartRes(), er = av.getRanges().getEndRes() + 1, transX = 0;
 +    int sr = av.getRanges().getStartRes(),
 +            er = av.getRanges().getEndRes() + 1, transX = 0;
  
      if (horizontal > 0) // scrollbar pulled right, image to the left
      {
      {
        fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
      }
 +    else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
 +    {
 +      fastPaint(((int[]) evt.getNewValue())[0]
 +              - ((int[]) evt.getOldValue())[0]);
 +    }
 +    else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
 +    {
 +      repaint();
 +    }
    }
  }
@@@ -28,13 -28,11 +28,14 @@@ import jalview.util.LinkedIdentityHashS
  import jalview.util.MessageManager;
  
  import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.BitSet;
  import java.util.Collections;
  import java.util.Enumeration;
+ import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Hashtable;
 +import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
@@@ -47,11 -45,11 +48,11 @@@ import java.util.Vector
   * @author JimP
   * 
   */
 -public class Alignment implements AlignmentI
 +public class Alignment implements AlignmentI, AutoCloseable
  {
    private Alignment dataset;
  
 -  protected List<SequenceI> sequences;
 +  private List<SequenceI> sequences;
  
    protected List<SequenceGroup> groups;
  
     */
    public static AlignmentI createAlignment(CigarArray compactAlignment)
    {
 -    throw new Error(
 -            MessageManager
 -                    .getString("error.alignment_cigararray_not_implemented"));
 +    throw new Error(MessageManager
 +            .getString("error.alignment_cigararray_not_implemented"));
      // this(compactAlignment.refCigars);
    }
  
      return AlignmentUtils.getSequencesByName(this);
    }
  
 -
    @Override
    public SequenceI getSequenceAt(int i)
    {
          return sequences.get(i);
        }
      }
 +
      return null;
    }
  
    }
  
    @Override
 -  public void finalize() throws Throwable
 +  public void close()
    {
      if (getDataset() != null)
      {
 -      getDataset().removeAlignmentRef();
 +      try
 +      {
 +        getDataset().removeAlignmentRef();
 +      } catch (Throwable e)
 +      {
 +        e.printStackTrace();
 +      }
      }
  
      nullReferences();
 -    super.finalize();
    }
  
    /**
        return;
      }
      // remove annotation very quickly
 -    AlignmentAnnotation[] t, todelete = new AlignmentAnnotation[annotations.length], tokeep = new AlignmentAnnotation[annotations.length];
 +    AlignmentAnnotation[] t,
 +            todelete = new AlignmentAnnotation[annotations.length],
 +            tokeep = new AlignmentAnnotation[annotations.length];
      int i, p, k;
      if (gp == null)
      {
        sqname = sq.getName();
        if (sqname.equals(token) // exact match
                || (b && // allow imperfect matches - case varies
 -              (sqname.equalsIgnoreCase(token))))
 +                      (sqname.equalsIgnoreCase(token))))
        {
          return getSequenceAt(i);
        }
      return -1;
    }
  
 -
    @Override
    public int getHeight()
    {
    public int getWidth()
    {
      int maxLength = -1;
 -
 +  
      for (int i = 0; i < sequences.size(); i++)
      {
 -      if (getSequenceAt(i).getLength() > maxLength)
 -      {
 -        maxLength = getSequenceAt(i).getLength();
 -      }
 +      maxLength = Math.max(maxLength, getSequenceAt(i).getLength());
      }
      return maxLength;
    }
  
 +  @Override
 +  public int getVisibleWidth()
 +  {
 +    int w = getWidth();
 +    if (hiddenCols != null)
 +    {
 +      w -= hiddenCols.getSize();
 +    }
 +    return w;
 +  }
 +
    /**
     * DOCUMENT ME!
     * 
              }
              if (dbr.getMap().getTo().getDatasetSequence() != null)
              {
 -              throw new Error(
 -                      "Implementation error: Map.getTo() for dbref " + dbr
 -                              + " from " + curDs.getName()
 -                              + " is not a dataset sequence.");
 +              throw new Error("Implementation error: Map.getTo() for dbref "
 +                      + dbr + " from " + curDs.getName()
 +                      + " is not a dataset sequence.");
              }
              // we recurse to add all forward references to dataset sequences via
              // DBRefs/etc
        current = getSequenceAt(i);
        // This should really be a sequence method
        ends[i * 2] = current.findIndex(current.getStart());
 -      ends[i * 2 + 1] = current.findIndex(current.getStart()
 -              + current.getLength());
 +      ends[i * 2 + 1] = current
 +              .findIndex(current.getStart() + current.getLength());
        boolean hitres = false;
        for (int j = 0, rs = 0, ssiz = current.getLength(); j < ssiz; j++)
        {
    {
      // TODO JAL-1270 needs test coverage
      // currently tested for use in jalview.gui.SequenceFetcher
 -    boolean samegap = toappend.getGapCharacter() == getGapCharacter();
      char oldc = toappend.getGapCharacter();
 +    boolean samegap = oldc == getGapCharacter();
      boolean hashidden = toappend.getHiddenSequences() != null
              && toappend.getHiddenSequences().hiddenSequences != null;
      // get all sequences including any hidden ones
 -    List<SequenceI> sqs = (hashidden) ? toappend.getHiddenSequences()
 -            .getFullAlignment().getSequences() : toappend.getSequences();
 +    List<SequenceI> sqs = (hashidden)
 +            ? toappend.getHiddenSequences().getFullAlignment()
 +                    .getSequences()
 +            : toappend.getSequences();
      if (sqs != null)
      {
        // avoid self append deadlock by
          {
            if (!samegap)
            {
 -            char[] oldseq = addedsq.getSequence();
 -            for (int c = 0; c < oldseq.length; c++)
 -            {
 -              if (oldseq[c] == oldc)
 -              {
 -                oldseq[c] = gapCharacter;
 -              }
 -            }
 +            addedsq.replace(oldc, gapCharacter);
            }
            toappendsq.add(addedsq);
          }
              if (ourval instanceof String)
              {
                // append strings
 -              this.setProperty(k, ((String) ourval) + "; "
 -                      + ((String) toapprop));
 +              this.setProperty(k,
 +                      ((String) ourval) + "; " + ((String) toapprop));
              }
              else
              {
      AlignmentAnnotation annot = new AlignmentAnnotation(name, name,
              new Annotation[1], 0f, 0f, AlignmentAnnotation.BAR_GRAPH);
      annot.hasText = false;
 -    annot.setCalcId(new String(calcId));
 +    if (calcId != null)
 +    {
 +      annot.setCalcId(new String(calcId));
 +    }
      annot.autoCalculated = autoCalc;
      if (seqRef != null)
      {
    @Override
    public Iterable<AlignmentAnnotation> findAnnotation(String calcId)
    {
      AlignmentAnnotation[] alignmentAnnotation = getAlignmentAnnotation();
      if (alignmentAnnotation != null)
      {
 -      for (AlignmentAnnotation a : alignmentAnnotation)
 -      {
 -        if (a.getCalcId() == calcId
 -                || (a.getCalcId() != null && calcId != null && a
 -                        .getCalcId().equals(calcId)))
 -        {
 -          aa.add(a);
 -        }
 -      }
 +      return AlignmentAnnotation.findAnnotation(
 +              Arrays.asList(getAlignmentAnnotation()), calcId);
      }
 -    return aa;
 +    return Arrays.asList(new AlignmentAnnotation[] {});
    }
  
    @Override
    public Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
            String calcId, String label)
    {
 -    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
 -    for (AlignmentAnnotation ann : getAlignmentAnnotation())
 -    {
 -      if ((calcId == null || (ann.getCalcId() != null && ann.getCalcId()
 -              .equals(calcId)))
 -              && (seq == null || (ann.sequenceRef != null && ann.sequenceRef == seq))
 -              && (label == null || (ann.label != null && ann.label
 -                      .equals(label))))
 -      {
 -        aa.add(ann);
 -      }
 -    }
 -    return aa;
 +    return AlignmentAnnotation.findAnnotations(
 +            Arrays.asList(getAlignmentAnnotation()), seq, calcId, label);
    }
  
    @Override
    }
  
    @Override
 -  public int[] getVisibleStartAndEndIndex(List<int[]> hiddenCols)
 +  public boolean setHiddenColumns(HiddenColumns cols)
    {
-     boolean changed = cols == null ? hiddenCols != null
-             : !cols.equals(hiddenCols);
-     hiddenCols = cols;
-     return changed;
 -    int[] alignmentStartEnd = new int[] { 0, getWidth() - 1 };
 -    int startPos = alignmentStartEnd[0];
 -    int endPos = alignmentStartEnd[1];
++  boolean changed = cols == null ? hiddenCols != null
++          : !cols.equals(hiddenCols);
++  hiddenCols = cols;
++  return changed;
 +  }
 +  @Override
 +  public void setupJPredAlignment()
 +  {
 +    SequenceI repseq = getSequenceAt(0);
 +    setSeqrep(repseq);
 +    HiddenColumns cs = new HiddenColumns();
 +    cs.hideList(repseq.getInsertions());
 +    setHiddenColumns(cs);
 +  }
  
 -    int[] lowestRange = new int[] { -1, -1 };
 -    int[] higestRange = new int[] { -1, -1 };
 +  @Override
 +  public HiddenColumns propagateInsertions(SequenceI profileseq,
 +          AlignmentView input)
 +  {
 +    int profsqpos = 0;
  
 -    for (int[] hiddenCol : hiddenCols)
 -    {
 -      lowestRange = (hiddenCol[0] <= startPos) ? hiddenCol : lowestRange;
 -      higestRange = (hiddenCol[1] >= endPos) ? hiddenCol : higestRange;
 -    }
 +    char gc = getGapCharacter();
 +    Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
 +    HiddenColumns nview = (HiddenColumns) alandhidden[1];
 +    SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
 +    return propagateInsertions(profileseq, origseq, nview);
 +  }
  
 -    if (lowestRange[0] == -1 && lowestRange[1] == -1)
 -    {
 -      startPos = alignmentStartEnd[0];
 -    }
 -    else
 +  /**
 +   * 
 +   * @param profileseq
 +   *          sequence in al which corresponds to origseq
 +   * @param al
 +   *          alignment which is to have gaps inserted into it
 +   * @param origseq
 +   *          sequence corresponding to profileseq which defines gap map for
 +   *          modifying al
 +   */
 +  private HiddenColumns propagateInsertions(SequenceI profileseq,
 +          SequenceI origseq, HiddenColumns hc)
 +  {
 +    // take the set of hidden columns, and the set of gaps in origseq,
 +    // and remove all the hidden gaps from hiddenColumns
 +
 +    // first get the gaps as a Bitset
 +    // then calculate hidden ^ not(gap)
 +    BitSet gaps = origseq.gapBitset();
 +    hc.andNot(gaps);
 +
 +    // for each sequence in the alignment, except the profile sequence,
 +    // insert gaps corresponding to each hidden region but where each hidden
 +    // column region is shifted backwards by the number of preceding visible
 +    // gaps update hidden columns at the same time
 +    HiddenColumns newhidden = new HiddenColumns();
 +
 +    int numGapsBefore = 0;
 +    int gapPosition = 0;
 +    Iterator<int[]> it = hc.iterator();
 +    while (it.hasNext())
      {
 -      startPos = lowestRange[1] + 1;
 +      int[] region = it.next();
 +
 +      // get region coordinates accounting for gaps
 +      // we can rely on gaps not being *in* hidden regions because we already
 +      // removed those
 +      while (gapPosition < region[0])
 +      {
 +        gapPosition++;
 +        if (gaps.get(gapPosition))
 +        {
 +          numGapsBefore++;
 +        }
 +      }
 +
 +      int left = region[0] - numGapsBefore;
 +      int right = region[1] - numGapsBefore;
 +
 +      newhidden.hideColumns(left, right);
 +      padGaps(left, right, profileseq);
      }
 +    return newhidden;
 +  }
 +
 +  /**
 +   * Pad gaps in all sequences in alignment except profileseq
 +   * 
 +   * @param left
 +   *          position of first gap to insert
 +   * @param right
 +   *          position of last gap to insert
 +   * @param profileseq
 +   *          sequence not to pad
 +   */
 +  private void padGaps(int left, int right, SequenceI profileseq)
 +  {
 +    char gc = getGapCharacter();
  
 -    if (higestRange[0] == -1 && higestRange[1] == -1)
 +    // make a string with number of gaps = length of hidden region
 +    StringBuilder sb = new StringBuilder();
 +    for (int g = 0; g < right - left + 1; g++)
      {
 -      endPos = alignmentStartEnd[1];
 +      sb.append(gc);
      }
 -    else
 +
 +    // loop over the sequences and pad with gaps where required
 +    for (int s = 0, ns = getHeight(); s < ns; s++)
      {
 -      endPos = higestRange[0] - 1;
 +      SequenceI sqobj = getSequenceAt(s);
 +      if ((sqobj != profileseq) && (sqobj.getLength() >= left))
 +      {
 +        String sq = sqobj.getSequenceAsString();
 +        sqobj.setSequence(
 +                sq.substring(0, left) + sb.toString() + sq.substring(left));
 +      }
      }
 -    return new int[] { startPos, endPos };
 -  }
 -  @Override
 -  public void setHiddenColumns(HiddenColumns cols)
 -  {
 -    hiddenCols = cols;
    }
  
 -  Map<Object, ContactMatrixI> contactmaps = new HashMap<Object, ContactMatrixI>();
++  Map<Object, ContactMatrixI> contactmaps = new HashMap<>();
+   @Override
+   public
+   ContactListI getContactListFor(AlignmentAnnotation _aa, int column)
+   {
+     ContactMatrixI cm = contactmaps.get(_aa.annotationId);
+     if (cm == null)
+     {
+       return null;
+     }
+     return cm.getContactList(column);
+   }
+   @Override
+   public AlignmentAnnotation addContactList(ContactMatrixI cm)
+   {
+     Annotation _aa[] = new Annotation[getWidth()];
+     Annotation dummy = new Annotation(0.0f);
+     for (int i = 0; i < _aa.length; _aa[i++] = dummy)
+     {
+       ;
+     }
+     AlignmentAnnotation aa = new AlignmentAnnotation("Contact Matrix",
+             "Contact Matrix", _aa);
+     aa.graph = AlignmentAnnotation.CUSTOMRENDERER;
+     aa.graphMin = cm.getMin();
+     aa.graphMax = cm.getMax();
+     aa.editable = false;
+     // aa.autoCalculated = true;
+     contactmaps.put(aa.annotationId, cm);
+     addAnnotation(aa);
+     return aa;
+   }
  }
@@@ -24,8 -24,6 +24,8 @@@ import jalview.analysis.Rna
  import jalview.analysis.SecStrConsensus.SimpleBP;
  import jalview.analysis.WUSSParseException;
  
 +import java.util.ArrayList;
 +import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
@@@ -98,13 -96,14 +98,13 @@@ public class AlignmentAnnotatio
     * Updates the _rnasecstr field Determines the positions that base pair and
     * the positions of helices based on secondary structure from a Stockholm file
     * 
 -   * @param RNAannot
 +   * @param rnaAnnotation
     */
 -  private void _updateRnaSecStr(CharSequence RNAannot)
 +  private void _updateRnaSecStr(CharSequence rnaAnnotation)
    {
      try
      {
 -      bps = Rna.getModeleBP(RNAannot);
 -      _rnasecstr = Rna.getBasePairs(bps);
 +      _rnasecstr = Rna.getHelixMap(rnaAnnotation);
        invalidrnastruc = -1;
      } catch (WUSSParseException px)
      {
      {
        return;
      }
 -    Rna.HelixMap(_rnasecstr);
 -    // setRNAStruc(RNAannot);
  
      if (_rnasecstr != null && _rnasecstr.length > 0)
      {
    }
  
    /**
 +   * Get the RNA Secondary Structure SequenceFeature Array if present
 +   */
 +  public SequenceFeature[] getRnaSecondaryStructure()
 +  {
 +    return this._rnasecstr;
 +  }
 +
 +  /**
 +   * Check the RNA Secondary Structure is equivalent to one in given
 +   * AlignmentAnnotation param
 +   */
 +  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that)
 +  {
 +    return rnaSecondaryStructureEquivalent(that, true);
 +  }
 +
 +  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that, boolean compareType)
 +  {
 +    SequenceFeature[] thisSfArray = this.getRnaSecondaryStructure();
 +    SequenceFeature[] thatSfArray = that.getRnaSecondaryStructure();
 +    if (thisSfArray == null || thatSfArray == null)
 +    {
 +      return thisSfArray == null && thatSfArray == null;
 +    }
 +    if (thisSfArray.length != thatSfArray.length)
 +    {
 +      return false;
 +    }
 +    Arrays.sort(thisSfArray, new SFSortByEnd()); // probably already sorted
 +                                                   // like this
 +    Arrays.sort(thatSfArray, new SFSortByEnd()); // probably already sorted
 +                                                   // like this
 +    for (int i=0; i < thisSfArray.length; i++) {
 +      SequenceFeature thisSf = thisSfArray[i];
 +      SequenceFeature thatSf = thatSfArray[i];
 +      if (compareType) {
 +        if (thisSf.getType() == null || thatSf.getType() == null) {
 +          if (thisSf.getType() == null && thatSf.getType() == null) {
 +            continue;
 +          } else {
 +            return false;
 +          }
 +        }
 +        if (! thisSf.getType().equals(thatSf.getType())) {
 +          return false;
 +        }
 +      }
 +      if (!(thisSf.getBegin() == thatSf.getBegin()
 +              && thisSf.getEnd() == thatSf.getEnd()))
 +      {
 +        return false;
 +      }
 +    }
 +    return true;
 +
 +  }
 +
 +  /**
     * map of positions in the associated annotation
     */
    private Map<Integer, Annotation> sequenceMapping;
  
    public static final int LINE_GRAPH = 2;
  
+   public static final int CUSTOMRENDERER = 4;
    public boolean belowAlignment = true;
  
    public SequenceGroup groupRef = null;
  
    private boolean isrna;
  
 -  /*
 -   * (non-Javadoc)
 -   * 
 -   * @see java.lang.Object#finalize()
 -   */
 -  @Override
 -  protected void finalize() throws Throwable
 -  {
 -    sequenceRef = null;
 -    groupRef = null;
 -    super.finalize();
 -  }
 -
    public static int getGraphValueFromString(String string)
    {
      if (string.equalsIgnoreCase("BAR_GRAPH"))
      }
    }
  
 -  // JBPNote: what does this do ?
 -  public void ConcenStru(CharSequence RNAannot) throws WUSSParseException
 -  {
 -    bps = Rna.getModeleBP(RNAannot);
 -  }
 -
    /**
     * Creates a new AlignmentAnnotation object.
     * 
      char firstChar = 0;
      for (int i = 0; i < annotations.length; i++)
      {
 +      // DEBUG System.out.println(i + ": " + annotations[i]);
        if (annotations[i] == null)
        {
          continue;
        if (annotations[i].secondaryStructure == 'H'
                || annotations[i].secondaryStructure == 'E')
        {
 +        // DEBUG System.out.println( "/H|E/ '" +
 +        // annotations[i].secondaryStructure + "'");
          hasIcons |= true;
        }
        else
        // Check for RNA secondary structure
        {
 -        // System.out.println(annotations[i].secondaryStructure);
 +        // DEBUG System.out.println( "/else/ '" +
 +        // annotations[i].secondaryStructure + "'");
          // TODO: 2.8.2 should this ss symbol validation check be a function in
          // RNA/ResidueProperties ?
          if (annotations[i].secondaryStructure == '('
                  || annotations[i].secondaryStructure == 'B'
                  || annotations[i].secondaryStructure == 'C'
                  || annotations[i].secondaryStructure == 'D'
 -                || annotations[i].secondaryStructure == 'E'
 +                // || annotations[i].secondaryStructure == 'E' // ambiguous on
 +                // its own -- already checked above
                  || annotations[i].secondaryStructure == 'F'
                  || annotations[i].secondaryStructure == 'G'
 -                || annotations[i].secondaryStructure == 'H'
 +                // || annotations[i].secondaryStructure == 'H' // ambiguous on
 +                // its own -- already checked above
                  || annotations[i].secondaryStructure == 'I'
                  || annotations[i].secondaryStructure == 'J'
                  || annotations[i].secondaryStructure == 'K'
          firstChar = annotations[i].displayCharacter.charAt(0);
          // check to see if it looks like a sequence or is secondary structure
          // labelling.
 -        if (annotations[i].secondaryStructure != ' '
 -                && !hasIcons
 -                &&
 -                // Uncomment to only catch case where
 -                // displayCharacter==secondary
 -                // Structure
 -                // to correctly redisplay SS annotation imported from Stockholm,
 -                // exported to JalviewXML and read back in again.
 -                // &&
 -                // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
 -                firstChar != ' '
 -                && firstChar != '$'
 -                && firstChar != 0xCE
 -                && firstChar != '('
 -                && firstChar != '['
 -                && firstChar != '>'
 -                && firstChar != '{'
 -                && firstChar != 'A'
 -                && firstChar != 'B'
 -                && firstChar != 'C'
 -                && firstChar != 'D'
 -                && firstChar != 'E'
 -                && firstChar != 'F'
 -                && firstChar != 'G'
 -                && firstChar != 'H'
 -                && firstChar != 'I'
 -                && firstChar != 'J'
 -                && firstChar != 'K'
 -                && firstChar != 'L'
 -                && firstChar != 'M'
 -                && firstChar != 'N'
 -                && firstChar != 'O'
 -                && firstChar != 'P'
 -                && firstChar != 'Q'
 -                && firstChar != 'R'
 -                && firstChar != 'S'
 -                && firstChar != 'T'
 -                && firstChar != 'U'
 -                && firstChar != 'V'
 -                && firstChar != 'W'
 -                && firstChar != 'X'
 -                && firstChar != 'Y'
 -                && firstChar != 'Z'
 +        if (annotations[i].secondaryStructure != ' ' && !hasIcons &&
 +        // Uncomment to only catch case where
 +        // displayCharacter==secondary
 +        // Structure
 +        // to correctly redisplay SS annotation imported from Stockholm,
 +        // exported to JalviewXML and read back in again.
 +        // &&
 +        // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
 +                firstChar != ' ' && firstChar != '$' && firstChar != 0xCE
 +                && firstChar != '(' && firstChar != '[' && firstChar != '<'
 +                && firstChar != '{' && firstChar != 'A' && firstChar != 'B'
 +                && firstChar != 'C' && firstChar != 'D' && firstChar != 'E'
 +                && firstChar != 'F' && firstChar != 'G' && firstChar != 'H'
 +                && firstChar != 'I' && firstChar != 'J' && firstChar != 'K'
 +                && firstChar != 'L' && firstChar != 'M' && firstChar != 'N'
 +                && firstChar != 'O' && firstChar != 'P' && firstChar != 'Q'
 +                && firstChar != 'R' && firstChar != 'S' && firstChar != 'T'
 +                && firstChar != 'U' && firstChar != 'V' && firstChar != 'W'
 +                && firstChar != 'X' && firstChar != 'Y' && firstChar != 'Z'
                  && firstChar != '-'
                  && firstChar < jalview.schemes.ResidueProperties.aaIndex.length)
          {
      {
        return ((index + offset < 0) || (index + offset) >= max
                || annotations[index + offset] == null
 -              || (annotations[index + offset].secondaryStructure <= ' ') ? ' '
 -              : annotations[index + offset].displayCharacter == null
 -                      || annotations[index + offset].displayCharacter
 -                              .length() == 0 ? annotations[index + offset].secondaryStructure
 -                      : annotations[index + offset].displayCharacter
 -                              .charAt(0));
 +              || (annotations[index + offset].secondaryStructure <= ' ')
 +                      ? ' '
 +                      : annotations[index + offset].displayCharacter == null
 +                              || annotations[index
 +                                      + offset].displayCharacter
 +                                              .length() == 0
 +                                                      ? annotations[index
 +                                                              + offset].secondaryStructure
 +                                                      : annotations[index
 +                                                              + offset].displayCharacter
 +                                                                      .charAt(0));
      }
  
      @Override
  
        for (int i = offset; i < mx; i++)
        {
 -        string[i] = (annotations[i] == null || (annotations[i].secondaryStructure <= 32)) ? ' '
 -                : (annotations[i].displayCharacter == null
 -                        || annotations[i].displayCharacter.length() == 0 ? annotations[i].secondaryStructure
 -                        : annotations[i].displayCharacter.charAt(0));
 +        string[i] = (annotations[i] == null
 +                || (annotations[i].secondaryStructure <= 32))
 +                        ? ' '
 +                        : (annotations[i].displayCharacter == null
 +                                || annotations[i].displayCharacter
 +                                        .length() == 0
 +                                                ? annotations[i].secondaryStructure
 +                                                : annotations[i].displayCharacter
 +                                                        .charAt(0));
        }
        return new String(string);
      }
      this.calcId = annotation.calcId;
      if (annotation.properties != null)
      {
 -      properties = new HashMap<String, String>();
 +      properties = new HashMap<>();
        for (Map.Entry<String, String> val : annotation.properties.entrySet())
        {
          properties.put(val.getKey(), val.getValue());
        if (annotation.sequenceMapping != null)
        {
          Integer p = null;
 -        sequenceMapping = new HashMap<Integer, Annotation>();
 +        sequenceMapping = new HashMap<>();
          Iterator<Integer> pos = annotation.sequenceMapping.keySet()
                  .iterator();
          while (pos.hasNext())
      Annotation[] temp = new Annotation[endRes - startRes + 1];
      if (startRes < annotations.length)
      {
 -      System.arraycopy(annotations, startRes, temp, 0, endRes - startRes
 -              + 1);
 +      System.arraycopy(annotations, startRes, temp, 0,
 +              endRes - startRes + 1);
      }
      if (sequenceRef != null)
      {
        int epos = sequenceRef.findPosition(endRes);
        if (sequenceMapping != null)
        {
 -        Map<Integer, Annotation> newmapping = new HashMap<Integer, Annotation>();
 +        Map<Integer, Annotation> newmapping = new HashMap<>();
          Iterator<Integer> e = sequenceMapping.keySet().iterator();
          while (e.hasNext())
          {
      {
        return;
      }
 -    sequenceMapping = new HashMap<Integer, Annotation>();
 +    sequenceMapping = new HashMap<>();
  
      int seqPos;
  
            seqPos = i + startRes;
          }
  
 -        sequenceMapping.put(new Integer(seqPos), annotations[i]);
 +        sequenceMapping.put(Integer.valueOf(seqPos), annotations[i]);
        }
      }
  
      {
        for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
        {
 -        index = new Integer(a);
 +        index = Integer.valueOf(a);
          Annotation annot = sequenceMapping.get(index);
          if (annot != null)
          {
        {
          if (i + 1 < iSize)
          {
 -          System.arraycopy(annotations, i + 1, annotations, i, iSize - i
 -                  - 1);
 +          System.arraycopy(annotations, i + 1, annotations, i,
 +                  iSize - i - 1);
          }
          iSize--;
        }
      {
        if (sequenceRef != null)
        {
 -        boolean rIsDs = sequenceRef.getDatasetSequence() == null, tIsDs = sequenceI
 -                .getDatasetSequence() == null;
 +        boolean rIsDs = sequenceRef.getDatasetSequence() == null,
 +                tIsDs = sequenceI.getDatasetSequence() == null;
          if (sequenceRef != sequenceI
 -                && (rIsDs && !tIsDs && sequenceRef != sequenceI
 -                        .getDatasetSequence())
 -                && (!rIsDs && tIsDs && sequenceRef.getDatasetSequence() != sequenceI)
 -                && (!rIsDs && !tIsDs && sequenceRef.getDatasetSequence() != sequenceI
 -                        .getDatasetSequence())
 +                && (rIsDs && !tIsDs
 +                        && sequenceRef != sequenceI.getDatasetSequence())
 +                && (!rIsDs && tIsDs
 +                        && sequenceRef.getDatasetSequence() != sequenceI)
 +                && (!rIsDs && !tIsDs
 +                        && sequenceRef.getDatasetSequence() != sequenceI
 +                                .getDatasetSequence())
                  && !sequenceRef.equals(sequenceI))
          {
            // if sequenceRef isn't intersecting with sequenceI
      {
        return;
      }
 -    hidden.makeVisibleAnnotation(this);
 +    makeVisibleAnnotation(hidden);
    }
  
    public void setPadGaps(boolean padgaps, char gapchar)
    /**
     * properties associated with the calcId
     */
 -  protected Map<String, String> properties = new HashMap<String, String>();
 +  protected Map<String, String> properties = new HashMap<>();
  
    /**
     * base colour for line graphs. If null, will be set automatically by
        throw new Error(
                "liftOver currently not implemented for transfer of annotation between different types of seqeunce");
      }
 -    boolean mapIsTo = (sp2sq != null) ? (sp2sq.getTo() == sq || sp2sq
 -            .getTo() == sq.getDatasetSequence()) : false;
 +    boolean mapIsTo = (sp2sq != null)
 +            ? (sp2sq.getTo() == sq
 +                    || sp2sq.getTo() == sq.getDatasetSequence())
 +            : false;
  
      // TODO build a better annotation element map and get rid of annotations[]
 -    Map<Integer, Annotation> mapForsq = new HashMap<Integer, Annotation>();
 +    Map<Integer, Annotation> mapForsq = new HashMap<>();
      if (sequenceMapping != null)
      {
        if (sp2sq != null)
        {
          for (Entry<Integer, Annotation> ie : sequenceMapping.entrySet())
          {
 -          Integer mpos = Integer.valueOf(mapIsTo ? sp2sq
 -                  .getMappedPosition(ie.getKey()) : sp2sq.getPosition(ie
 -                  .getKey()));
 +          Integer mpos = Integer
 +                  .valueOf(mapIsTo ? sp2sq.getMappedPosition(ie.getKey())
 +                          : sp2sq.getPosition(ie.getKey()));
            if (mpos >= sq.getStart() && mpos <= sq.getEnd())
            {
              mapForsq.put(mpos, ie.getValue());
      if (mapping != null)
      {
        Map<Integer, Annotation> old = sequenceMapping;
 -      Map<Integer, Annotation> remap = new HashMap<Integer, Annotation>();
 +      Map<Integer, Annotation> remap = new HashMap<>();
        int index = -1;
        for (int mp[] : mapping.values())
        {
    {
      if (properties == null)
      {
 -      properties = new HashMap<String, String>();
 +      properties = new HashMap<>();
      }
      properties.put(property, value);
    }
         * up to and excluding the target column; if the count is less
         * than 1, the opening bracket is unmatched, so return its match
         */
 -      String closer = String.valueOf(Rna
 -              .getMatchingClosingParenthesis(symbol));
 +      String closer = String
 +              .valueOf(Rna.getMatchingClosingParenthesis(symbol));
        String opener = String.valueOf(symbol);
        int count = 0;
        for (int j = col + 1; j < column; j++)
    {
      return graphMin < graphMax;
    }
 +
 +  /**
 +   * delete any columns in alignmentAnnotation that are hidden (including
 +   * sequence associated annotation).
 +   * 
 +   * @param hiddenColumns
 +   *          the set of hidden columns
 +   */
 +  public void makeVisibleAnnotation(HiddenColumns hiddenColumns)
 +  {
 +    if (annotations != null)
 +    {
 +      makeVisibleAnnotation(0, annotations.length, hiddenColumns);
 +    }
 +  }
 +
 +  /**
 +   * delete any columns in alignmentAnnotation that are hidden (including
 +   * sequence associated annotation).
 +   * 
 +   * @param start
 +   *          remove any annotation to the right of this column
 +   * @param end
 +   *          remove any annotation to the left of this column
 +   * @param hiddenColumns
 +   *          the set of hidden columns
 +   */
 +  public void makeVisibleAnnotation(int start, int end,
 +          HiddenColumns hiddenColumns)
 +  {
 +    if (annotations != null)
 +    {
 +      if (hiddenColumns.hasHiddenColumns())
 +      {
 +        removeHiddenAnnotation(start, end, hiddenColumns);
 +      }
 +      else
 +      {
 +        restrict(start, end);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * The actual implementation of deleting hidden annotation columns
 +   * 
 +   * @param start
 +   *          remove any annotation to the right of this column
 +   * @param end
 +   *          remove any annotation to the left of this column
 +   * @param hiddenColumns
 +   *          the set of hidden columns
 +   */
 +  private void removeHiddenAnnotation(int start, int end,
 +          HiddenColumns hiddenColumns)
 +  {
 +    // mangle the alignmentAnnotation annotation array
 +    ArrayList<Annotation[]> annels = new ArrayList<>();
 +    Annotation[] els = null;
 +
 +    int w = 0;
 +
 +    Iterator<int[]> blocks = hiddenColumns.getVisContigsIterator(start,
 +            end + 1, false);
 +
 +    int copylength;
 +    int annotationLength;
 +    while (blocks.hasNext())
 +    {
 +      int[] block = blocks.next();
 +      annotationLength = block[1] - block[0] + 1;
 +
 +      if (blocks.hasNext())
 +      {
 +        // copy just the visible segment of the annotation row
 +        copylength = annotationLength;
 +      }
 +      else
 +      {
 +        if (annotationLength + block[0] <= annotations.length)
 +        {
 +          // copy just the visible segment of the annotation row
 +          copylength = annotationLength;
 +        }
 +        else
 +        {
 +          // copy to the end of the annotation row
 +          copylength = annotations.length - block[0];
 +        }
 +      }
 +
 +      els = new Annotation[annotationLength];
 +      annels.add(els);
 +      System.arraycopy(annotations, block[0], els, 0, copylength);
 +      w += annotationLength;
 +    }
 +
 +    if (w != 0)
 +    {
 +      annotations = new Annotation[w];
 +
 +      w = 0;
 +      for (Annotation[] chnk : annels)
 +      {
 +        System.arraycopy(chnk, 0, annotations, w, chnk.length);
 +        w += chnk.length;
 +      }
 +    }
 +  }
 +
 +  public static Iterable<AlignmentAnnotation> findAnnotations(
 +          Iterable<AlignmentAnnotation> list, SequenceI seq, String calcId,
 +          String label)
 +  {
 +
 +    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
 +    for (AlignmentAnnotation ann : list)
 +    {
 +      if ((calcId == null || (ann.getCalcId() != null
 +              && ann.getCalcId().equals(calcId)))
 +              && (seq == null || (ann.sequenceRef != null
 +                      && ann.sequenceRef == seq))
 +              && (label == null
 +                      || (ann.label != null && ann.label.equals(label))))
 +      {
 +        aa.add(ann);
 +      }
 +    }
 +    return aa;
 +  }
 +
 +  /**
 +   * Answer true if any annotation matches the calcId passed in (if not null).
 +   * 
 +   * @param list
 +   *          annotation to search
 +   * @param calcId
 +   * @return
 +   */
 +  public static boolean hasAnnotation(List<AlignmentAnnotation> list,
 +          String calcId)
 +  {
 +
 +    if (calcId != null && !"".equals(calcId))
 +    {
 +      for (AlignmentAnnotation a : list)
 +      {
 +        if (a.getCalcId() == calcId)
 +        {
 +          return true;
 +        }
 +      }
 +    }
 +    return false;
 +  }
 +
 +  public static Iterable<AlignmentAnnotation> findAnnotation(
 +          List<AlignmentAnnotation> list, String calcId)
 +  {
 +
 +    List<AlignmentAnnotation> aa = new ArrayList<>();
 +    if (calcId == null)
 +    {
 +      return aa;
 +    }
 +    for (AlignmentAnnotation a : list)
 +    {
 +
 +      if (a.getCalcId() == calcId || (a.getCalcId() != null
 +              && calcId != null && a.getCalcId().equals(calcId)))
 +      {
 +        aa.add(a);
 +      }
 +    }
 +    return aa;
 +  }
 +
  }
@@@ -5,16 -5,16 +5,16 @@@
   * This file is part of Jalview.
   * 
   * Jalview is free software: you can redistribute it and/or
 - * modify it under the terms of the GNU General License 
 + * modify it under the terms of the GNU General Public License 
   * as published by the Free Software Foundation, either version 3
   * of the License, or (at your option) any later version.
   *  
   * Jalview is distributed in the hope that it will be useful, but 
   * WITHOUT ANY WARRANTY; without even the implied warranty 
   * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 - * PURPOSE.  See the GNU General License for more details.
 + * PURPOSE.  See the GNU General Public License for more details.
   * 
 - * You should have received a copy of the GNU General License
 + * You should have received a copy of the GNU General Public License
   * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
   * The Jalview Authors are detailed in the 'AUTHORS' file.
   */
@@@ -48,29 -48,15 +48,29 @@@ public interface AlignmentI extends Ann
  
    /**
     * 
 -   * Calculates the maximum width of the alignment, including gaps.
 +   * Answers the width of the alignment, including gaps, that is, the length of
 +   * the longest sequence, or -1 if there are no sequences. Avoid calling this
 +   * method repeatedly where possible, as it has to perform a calculation. Note
 +   * that this width includes any hidden columns.
     * 
 -   * @return Greatest sequence length within alignment, or -1 if no sequences
 -   *         present
 +   * @return
 +   * @see AlignmentI#getVisibleWidth()
     */
    @Override
    int getWidth();
  
    /**
 +   * 
 +   * Answers the visible width of the alignment, including gaps, that is, the
 +   * length of the longest sequence, excluding any hidden columns. Answers -1 if
 +   * there are no sequences. Avoid calling this method repeatedly where
 +   * possible, as it has to perform a calculation.
 +   * 
 +   * @return
 +   */
 +  int getVisibleWidth();
 +
 +  /**
     * Calculates if this set of sequences (visible and invisible) are all the
     * same length
     * 
    AlignedCodonFrame getMapping(SequenceI mapFrom, SequenceI mapTo);
  
    /**
 -   * Calculate the visible start and end index of an alignment. The result is
 -   * returned an int array where: int[0] = startIndex, and int[1] = endIndex.
 +   * Set the hidden columns collection on the alignment. Answers true if the
 +   * hidden column selection changed, else false.
     * 
 -   * @param hiddenCols
 +   * @param cols
     * @return
     */
 -  public int[] getVisibleStartAndEndIndex(List<int[]> hiddenCols);
 +  public boolean setHiddenColumns(HiddenColumns cols);
 +
 +  /**
 +   * Set the first sequence as representative and hide its insertions. Typically
 +   * used when loading JPred files.
 +   */
 +  public void setupJPredAlignment();
 +
 +  /**
 +   * Add gaps into the sequences aligned to profileseq under the given
 +   * AlignmentView
 +   * 
 +   * @param profileseq
 +   *          sequence in al which sequences are aligned to
 +   * @param input
 +   *          alignment view where sequence corresponding to profileseq is first
 +   *          entry
 +   * @return new HiddenColumns for new alignment view, with insertions into
 +   *         profileseq marked as hidden.
 +   */
 +  public HiddenColumns propagateInsertions(SequenceI profileseq,
 +          AlignmentView input);
  
+   /**
+    * resolve a contact list instance (if any) associated with the annotation row
+    * and column position
+    * 
+    * @param _aa
+    * @param column
+    * @return
+    */
+   ContactListI getContactListFor(AlignmentAnnotation _aa, int column);
+   AlignmentAnnotation addContactList(ContactMatrixI cm);
 -
 -  public void setHiddenColumns(HiddenColumns cols);
  }
@@@ -21,7 -21,6 +21,7 @@@
  package jalview.gui;
  
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
@@@ -31,7 -30,6 +31,7 @@@ import jalview.renderer.AwtRenderPanelI
  import jalview.schemes.ResidueProperties;
  import jalview.util.Comparison;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
  
@@@ -77,11 -75,6 +77,11 @@@ public class AnnotationPanel extends JP
          MouseListener, MouseWheelListener, MouseMotionListener,
          ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
  {
 +  enum DragMode
 +  {
 +    Select, Resize, Undefined
 +  };
 +
    String HELIX = MessageManager.getString("label.helix");
  
    String SHEET = MessageManager.getString("label.sheet");
    // Used For mouse Dragging and resizing graphs
    int graphStretch = -1;
  
 -  int graphStretchY = -1;
 +  int mouseDragLastX = -1;
  
 -  int min; // used by mouseDragged to see if user
 +  int mouseDragLastY = -1;
  
 -  int max; // used by mouseDragged to see if user
 +  DragMode dragMode = DragMode.Undefined;
  
    boolean mouseDragging = false;
  
      if (e.isShiftDown())
      {
        e.consume();
 -      if (e.getWheelRotation() > 0)
 +      double wheelRotation = e.getPreciseWheelRotation();
 +      if (wheelRotation > 0)
        {
          av.getRanges().scrollRight(true);
        }
 -      else
 +      else if (wheelRotation < 0)
        {
          av.getRanges().scrollRight(false);
        }
    @Override
    public Dimension getPreferredScrollableViewportSize()
    {
 -    return getPreferredSize();
 +    Dimension ps = getPreferredSize();
 +    return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
    }
  
    @Override
  
      if (anot.length < av.getColumnSelection().getMax())
      {
 -      Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
 +      Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
 +              + 2];
        System.arraycopy(anot, 0, temp, 0, anot.length);
        anot = temp;
        aa[activeRow].annotations = anot;
            anot[index] = new Annotation(label, "", type, 0);
          }
  
 -        anot[index].secondaryStructure = type != 'S' ? type : label
 -                .length() == 0 ? ' ' : label.charAt(0);
 +        anot[index].secondaryStructure = type != 'S' ? type
 +                : label.length() == 0 ? ' ' : label.charAt(0);
          anot[index].displayCharacter = label;
  
        }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Action on right mouse pressed on Mac is to show a pop-up menu for the
 +   * annotation. Action on left mouse pressed is to find which annotation is
 +   * pressed and mark the start of a column selection or graph resize operation.
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mousePressed(MouseEvent evt)
      {
        return;
      }
 +    mouseDragLastX = evt.getX();
 +    mouseDragLastY = evt.getY();
  
 +    /*
 +     * add visible annotation heights until we reach the y
 +     * position, to find which annotation it is in
 +     */
      int height = 0;
      activeRow = -1;
  
          {
            activeRow = i;
          }
-         else if (aa[i].graph > 0)
+         else if (aa[i].graph != 0)
          {
 -          // Stretch Graph
 +          /*
 +           * we have clicked on a resizable graph annotation
 +           */
            graphStretch = i;
 -          graphStretchY = y;
          }
 -
          break;
        }
      }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Action on mouse up is to clear mouse drag data and call mouseReleased on
 +   * ScalePanel, to deal with defining the selection group (if any) defined by
 +   * the mouse drag
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseReleased(MouseEvent evt)
    {
      graphStretch = -1;
 -    graphStretchY = -1;
 +    mouseDragLastX = -1;
 +    mouseDragLastY = -1;
      mouseDragging = false;
 +    dragMode = DragMode.Undefined;
      ap.getScalePanel().mouseReleased(evt);
  
      /*
    @Override
    public void mouseDragged(MouseEvent evt)
    {
 -    if (graphStretch > -1)
 +    /*
 +     * todo: if dragMode is Undefined:
 +     * - set to Select if dx > dy
 +     * - set to Resize if dy > dx
 +     * - do nothing if dx == dy
 +     */
 +    final int x = evt.getX();
 +    final int y = evt.getY();
 +    if (dragMode == DragMode.Undefined)
      {
 -      av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
 -              - evt.getY();
 -      if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
 +      int dx = Math.abs(x - mouseDragLastX);
 +      int dy = Math.abs(y - mouseDragLastY);
 +      if (graphStretch == -1 || dx > dy)
        {
 -        av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
 +        /*
 +         * mostly horizontal drag, or not a graph annotation
 +         */
 +        dragMode = DragMode.Select;
 +      }
 +      else if (dy > dx)
 +      {
 +        /*
 +         * mostly vertical drag
 +         */
 +        dragMode = DragMode.Resize;
        }
 -      graphStretchY = evt.getY();
 -      adjustPanelHeight();
 -      ap.paintAlignment(true);
      }
 -    else
 +
 +    if (dragMode == DragMode.Undefined)
 +    {
 +      /*
 +       * drag is diagonal - defer deciding whether to
 +       * treat as up/down or left/right
 +       */
 +      return;
 +    }
 +
 +    try
 +    {
 +      if (dragMode == DragMode.Resize)
 +      {
 +        /*
 +         * resize graph annotation if mouse was dragged up or down
 +         */
 +        int deltaY = mouseDragLastY - evt.getY();
 +        if (deltaY != 0)
 +        {
 +          AlignmentAnnotation graphAnnotation = av.getAlignment()
 +                  .getAlignmentAnnotation()[graphStretch];
 +          int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
 +          graphAnnotation.graphHeight = newHeight;
 +          adjustPanelHeight();
 +          ap.paintAlignment(false, false);
 +        }
 +      }
 +      else
 +      {
 +        /*
 +         * for mouse drag left or right, delegate to 
 +         * ScalePanel to adjust the column selection
 +         */
 +        ap.getScalePanel().mouseDragged(evt);
 +      }
 +    } finally
      {
 -      ap.getScalePanel().mouseDragged(evt);
 +      mouseDragLastX = x;
 +      mouseDragLastY = y;
      }
    }
  
    @Override
    public void mouseMoved(MouseEvent evt)
    {
 +    int yPos = evt.getY();
      AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
  
 -    if (aa == null)
 -    {
 -      this.setToolTipText(null);
 -      return;
 -    }
 -
 -    int row = -1;
 -    int height = 0;
 -
 -    for (int i = 0; i < aa.length; i++)
 -    {
 -      if (aa[i].visible)
 -      {
 -        height += aa[i].height;
 -      }
 -
 -      if (evt.getY() < height)
 -      {
 -        row = i;
 -        break;
 -      }
 -    }
 +    int row = getRowIndex(yPos, aa);
  
      if (row == -1)
      {
  
      int column = (evt.getX() / av.getCharWidth())
              + av.getRanges().getStartRes();
 +    column = Math.min(column, av.getRanges().getEndRes());
  
      if (av.hasHiddenColumns())
      {
        column = av.getAlignment().getHiddenColumns()
 -              .adjustForHiddenColumns(column);
 +              .visibleToAbsoluteColumn(column);
      }
  
      AlignmentAnnotation ann = aa[row];
      if (row > -1 && ann.annotations != null
              && column < ann.annotations.length)
      {
 -      buildToolTip(ann, column, aa);
 -      setStatusMessage(column, ann);
 +      setToolTipText(buildToolTip(ann, column, aa));
 +      String msg = getStatusMessage(av.getAlignment(), column, ann);
 +      ap.alignFrame.setStatus(msg);
      }
      else
      {
        this.setToolTipText(null);
 -      ap.alignFrame.statusBar.setText(" ");
 +      ap.alignFrame.setStatus(" ");
 +    }
 +  }
 +
 +  /**
 +   * Answers the index in the annotations array of the visible annotation at the
 +   * given y position. This is done by adding the heights of visible annotations
 +   * until the y position has been exceeded. Answers -1 if no annotations are
 +   * visible, or the y position is below all annotations.
 +   * 
 +   * @param yPos
 +   * @param aa
 +   * @return
 +   */
 +  static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
 +  {
 +    if (aa == null)
 +    {
 +      return -1;
      }
 +    int row = -1;
 +    int height = 0;
 +
 +    for (int i = 0; i < aa.length; i++)
 +    {
 +      if (aa[i].visible)
 +      {
 +        height += aa[i].height;
 +      }
 +
 +      if (height > yPos)
 +      {
 +        row = i;
 +        break;
 +      }
 +    }
 +    return row;
    }
  
    /**
 -   * Builds a tooltip for the annotation at the current mouse position.
 +   * Answers a tooltip for the annotation at the current mouse position
     * 
     * @param ann
     * @param column
     * @param anns
     */
 -  void buildToolTip(AlignmentAnnotation ann, int column,
 +  static String buildToolTip(AlignmentAnnotation ann, int column,
            AlignmentAnnotation[] anns)
    {
 +    String tooltip = null;
      if (ann.graphGroup > -1)
      {
        StringBuilder tip = new StringBuilder(32);
        if (tip.length() != 6)
        {
          tip.setLength(tip.length() - 4);
 -        this.setToolTipText(tip.toString() + "</html>");
 +        tooltip = tip.toString() + "</html>";
        }
      }
 -    else if (ann.annotations[column] != null)
 +    else if (column < ann.annotations.length
 +            && ann.annotations[column] != null)
      {
        String description = ann.annotations[column].description;
        if (description != null && description.length() > 0)
        {
 -        this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
 +        tooltip = JvSwingUtils.wrapTooltip(true, description);
 +      }
 +      else
 +      {
 +        tooltip = null; // no tooltip if null or empty description
        }
      }
      else
      {
        // clear the tooltip.
 -      this.setToolTipText(null);
 +      tooltip = null;
      }
 +    return tooltip;
    }
  
    /**
 -   * Constructs and displays the status bar message
 +   * Constructs and returns the status bar message
     * 
 +   * @param al
     * @param column
     * @param ann
     */
 -  void setStatusMessage(int column, AlignmentAnnotation ann)
 +  static String getStatusMessage(AlignmentI al, int column,
 +          AlignmentAnnotation ann)
    {
      /*
       * show alignment column and annotation description if any
      text.append(MessageManager.getString("label.column")).append(" ")
              .append(column + 1);
  
 -    if (ann.annotations[column] != null)
 +    if (column < ann.annotations.length && ann.annotations[column] != null)
      {
        String description = ann.annotations[column].description;
        if (description != null && description.trim().length() > 0)
      SequenceI seqref = ann.sequenceRef;
      if (seqref != null)
      {
 -      int seqIndex = av.getAlignment().findIndex(seqref);
 +      int seqIndex = al.findIndex(seqref);
        if (seqIndex != -1)
        {
 -        text.append(", ")
 -                .append(MessageManager.getString("label.sequence"))
 +        text.append(", ").append(MessageManager.getString("label.sequence"))
                  .append(" ").append(seqIndex + 1);
          char residue = seqref.getCharAt(column);
          if (!Comparison.isGap(residue))
          {
            text.append(" ");
            String name;
 -          if (av.getAlignment().isNucleotide())
 +          if (al.isNucleotide())
            {
 -            name = ResidueProperties.nucleotideName.get(String
 -                    .valueOf(residue));
 -            text.append(" Nucleotide: ").append(
 -                    name != null ? name : residue);
 +            name = ResidueProperties.nucleotideName
 +                    .get(String.valueOf(residue));
 +            text.append(" Nucleotide: ")
 +                    .append(name != null ? name : residue);
            }
            else
            {
 -            name = 'X' == residue ? "X" : ('*' == residue ? "STOP"
 -                    : ResidueProperties.aa2Triplet.get(String
 -                            .valueOf(residue)));
 +            name = 'X' == residue ? "X"
 +                    : ('*' == residue ? "STOP"
 +                            : ResidueProperties.aa2Triplet
 +                                    .get(String.valueOf(residue)));
              text.append(" Residue: ").append(name != null ? name : residue);
            }
            int residuePos = seqref.findPosition(column);
        }
      }
  
 -    ap.alignFrame.statusBar.setText(text.toString());
 +    return text.toString();
    }
  
    /**
    @Override
    public void paintComponent(Graphics g)
    {
 +    super.paintComponent(g);
 +
      g.setColor(Color.white);
      g.fillRect(0, 0, getWidth(), getHeight());
  
          return;
        }
      }
 -    imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes() + 1)
 -            * av.getCharWidth();
 +    imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
 +            + 1) * av.getCharWidth();
      if (imgWidth < 1)
      {
        return;
      {
        try
        {
 -        image = new BufferedImage(imgWidth, ap.getAnnotationPanel()
 -                .getHeight(), BufferedImage.TYPE_INT_RGB);
 +        image = new BufferedImage(imgWidth,
 +                ap.getAnnotationPanel().getHeight(),
 +                BufferedImage.TYPE_INT_RGB);
        } catch (OutOfMemoryError oom)
        {
          try
        gg.fillRect(0, 0, imgWidth, image.getHeight());
        imageFresh = true;
      }
 -
 -    drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
 -            .getEndRes() + 1);
 +    
 +    drawComponent(gg, av.getRanges().getStartRes(),
 +            av.getRanges().getEndRes() + 1);
      imageFresh = false;
      g.drawImage(image, 0, 0, this);
    }
      int er = av.getRanges().getEndRes() + 1;
      int transX = 0;
  
 -    long stime = System.currentTimeMillis();
      gg.copyArea(0, 0, imgWidth, getHeight(),
              -horizontal * av.getCharWidth(), 0);
 -    long mtime = System.currentTimeMillis();
  
      if (horizontal > 0) // scrollbar pulled right, image to the left
      {
      drawComponent(gg, sr, er);
  
      gg.translate(-transX, 0);
 -    long dtime = System.currentTimeMillis();
 +
      fastPaint = true;
 -    repaint();
 -    long rtime = System.currentTimeMillis();
 -    if (debugRedraw)
 -    {
 -      System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
 -              + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
 -              + "\tRepaint call:\t" + (rtime - dtime));
 -    }
  
 +    // Call repaint on alignment panel so that repaints from other alignment
 +    // panel components can be aggregated. Otherwise performance of the overview
 +    // window and others may be adversely affected.
 +    av.getAlignPanel().repaint();
    }
  
    private volatile boolean lastImageGood = false;
        // and draw a faded image until the calculation
        // has completed
        if (lastImageGood
 -              && (fadedImage == null || fadedImage.getWidth() != imgWidth || fadedImage
 -                      .getHeight() != image.getHeight()))
 +              && (fadedImage == null || fadedImage.getWidth() != imgWidth
 +                      || fadedImage.getHeight() != image.getHeight()))
        {
          // System.err.println("redraw faded image ("+(fadedImage==null ?
          // "null image" : "") + " lastGood="+lastImageGood+")");
          fadedG.setColor(Color.white);
          fadedG.fillRect(0, 0, imgWidth, image.getHeight());
  
 -        fadedG.setComposite(AlphaComposite.getInstance(
 -                AlphaComposite.SRC_OVER, .3f));
 +        fadedG.setComposite(
 +                AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
          fadedG.drawImage(image, 0, 0, this);
  
        }
  
        return;
      }
 -    lastImageGood = renderer.drawComponent(this, av, g, activeRow,
 -            startRes, endRes);
 +    lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
 +            endRes);
      if (!lastImageGood && fadedImage == null)
      {
        fadedImage = oldFaded;
      {
        fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
      }
 +    else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
 +    {
 +      fastPaint(((int[]) evt.getNewValue())[0]
 +              - ((int[]) evt.getOldValue())[0]);
 +    }
 +    else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
 +    {
 +      repaint();
 +    }
 +  }
 +
 +  /**
 +   * computes the visible height of the annotation panel
 +   * 
 +   * @param adjustPanelHeight
 +   *          - when false, just adjust existing height according to other
 +   *          windows
 +   * @param annotationHeight
 +   * @return height to use for the ScrollerPreferredVisibleSize
 +   */
 +  public int adjustForAlignFrame(boolean adjustPanelHeight,
 +          int annotationHeight)
 +  {
 +    /*
 +     * Estimate available height in the AlignFrame for alignment +
 +     * annotations. Deduct an estimate for title bar, menu bar, scale panel,
 +     * hscroll, status bar, insets. 
 +     */
 +    int stuff = (ap.getViewName() != null ? 30 : 0)
 +            + (Platform.isAMac() ? 120 : 140);
 +    int availableHeight = ap.alignFrame.getHeight() - stuff;
 +    int rowHeight = av.getCharHeight();
 +
 +    if (adjustPanelHeight)
 +    {
 +      int alignmentHeight = rowHeight * av.getAlignment().getHeight();
 +
 +      /*
 +       * If not enough vertical space, maximize annotation height while keeping
 +       * at least two rows of alignment visible
 +       */
 +      if (annotationHeight + alignmentHeight > availableHeight)
 +      {
 +        annotationHeight = Math.min(annotationHeight,
 +                availableHeight - 2 * rowHeight);
 +      }
 +    }
 +    else
 +    {
 +      // maintain same window layout whilst updating sliders
 +      annotationHeight = Math.min(ap.annotationScroller.getSize().height,
 +              availableHeight - 2 * rowHeight);
 +    }
 +    return annotationHeight;
    }
  }
@@@ -26,6 -26,7 +26,7 @@@ import jalview.datamodel.Alignment
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
+ import jalview.datamodel.SeqDistanceContactMatrix;
  import jalview.datamodel.PDBEntry.Type;
  import jalview.datamodel.SequenceI;
  import jalview.ext.jmol.JmolParser;
@@@ -85,9 -86,8 +86,9 @@@ public class AppletFormatAdapte
    public static String getSupportedFormats()
    {
      return "Formats currently supported are\n"
 -          + prettyPrint(FileFormats.getInstance().getReadableFormats());
 +            + prettyPrint(FileFormats.getInstance().getReadableFormats());
    }
 +
    public AppletFormatAdapter()
    {
    }
          String structureParser = StructureImportSettings
                  .getDefaultPDBFileParser();
          boolean isParseWithJMOL = structureParser.equalsIgnoreCase(
 -                        StructureImportSettings.StructureParser.JMOL_PARSER
 -                                .toString());
 +                StructureImportSettings.StructureParser.JMOL_PARSER
 +                        .toString());
          StructureImportSettings.addSettings(annotFromStructure,
                  localSecondaryStruct, serviceSecondaryStruct);
          if (isParseWithJMOL)
                    localSecondaryStruct, serviceSecondaryStruct, inFile,
                    sourceType);
          }
 -        ((StructureFile) alignFile).setDbRefType(FileFormat.PDB
 -                .equals(fileFormat) ? Type.PDB : Type.MMCIF);
 +        ((StructureFile) alignFile).setDbRefType(
 +                FileFormat.PDB.equals(fileFormat) ? Type.PDB : Type.MMCIF);
        }
        else
        {
          // alignFile = fileFormat.getAlignmentFile(inFile, sourceType);
 -        alignFile = fileFormat.getReader(new FileParse(inFile,
 -                sourceType));
 +        alignFile = fileFormat.getReader(new FileParse(inFile, sourceType));
        }
        return buildAlignmentFromFile();
      } catch (Exception e)
      {
        e.printStackTrace();
 -      System.err.println("Failed to read alignment using the '"
 -              + fileFormat + "' reader.\n" + e);
 +      System.err.println("Failed to read alignment using the '" + fileFormat
 +              + "' reader.\n" + e);
  
        if (e.getMessage() != null
                && e.getMessage().startsWith(INVALID_CHARACTERS))
  
      alignFile.addGroups(al);
  
+     al.addContactList(new SeqDistanceContactMatrix(al.getWidth()));
      return al;
    }
  
            AlignmentViewPanel ap, boolean selectedOnly)
    {
  
 -    AlignmentView selvew = ap.getAlignViewport().getAlignmentView(
 -            selectedOnly, false);
 -    AlignmentI aselview = selvew.getVisibleAlignment(ap.getAlignViewport()
 -            .getGapCharacter());
 +    AlignmentView selvew = ap.getAlignViewport()
 +            .getAlignmentView(selectedOnly, false);
 +    AlignmentI aselview = selvew
 +            .getVisibleAlignment(ap.getAlignViewport().getGapCharacter());
      List<AlignmentAnnotation> ala = (ap.getAlignViewport()
              .getVisibleAlignmentAnnotation(selectedOnly));
      if (ala != null)
      } catch (Exception e)
      {
        System.err.println("Failed to write alignment as a '"
 -              + format.getName()
 -              + "' file\n");
 +              + format.getName() + "' file\n");
        e.printStackTrace();
      }
  
            System.gc();
            long memf = -r.totalMemory() + r.freeMemory();
            long t1 = -System.currentTimeMillis();
 -          AlignmentI al = afa
 -                  .readFile(args[i], DataSourceType.FILE,
 -                          new IdentifyFile().identify(args[i],
 -                                  DataSourceType.FILE));
 +          AlignmentI al = afa.readFile(args[i], DataSourceType.FILE,
 +                  new IdentifyFile().identify(args[i],
 +                          DataSourceType.FILE));
            t1 += System.currentTimeMillis();
            System.gc();
            memf += r.totalMemory() - r.freeMemory();
                      + " sequences and " + al.getWidth() + " columns.");
              try
              {
 -              System.out.println(new AppletFormatAdapter().formatSequences(
 -                      FileFormat.Fasta, al, true));
 +              System.out.println(new AppletFormatAdapter()
 +                      .formatSequences(FileFormat.Fasta, al, true));
              } catch (Exception e)
              {
 -              System.err
 -                      .println("Couln't format the alignment for output as a FASTA file.");
 +              System.err.println(
 +                      "Couln't format the alignment for output as a FASTA file.");
                e.printStackTrace(System.err);
              }
            }
              System.out.println("Couldn't read alignment");
            }
            System.out.println("Read took " + (t1 / 1000.0) + " seconds.");
 -          System.out
 -                  .println("Difference between free memory now and before is "
 +          System.out.println(
 +                  "Difference between free memory now and before is "
                            + (memf / (1024.0 * 1024.0) * 1.0) + " MB");
          } catch (Exception e)
          {
      {
        if (debug)
        {
 -        System.out.println("Trying to get contents of resource as "
 -                + protocol + ":");
 +        System.out.println(
 +                "Trying to get contents of resource as " + protocol + ":");
        }
        fp = new FileParse(file, protocol);
        if (!fp.isValid())
@@@ -30,6 -30,8 +30,8 @@@ import jalview.datamodel.Annotation
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.ProfilesI;
+ import jalview.renderer.api.AnnotationRendererFactoryI;
+ import jalview.renderer.api.AnnotationRowRendererI;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.NucleotideColourScheme;
  import jalview.schemes.ResidueProperties;
@@@ -76,7 -78,7 +78,7 @@@ public class AnnotationRendere
    ResidueShaderI profcolour = null;
  
    private ColumnSelection columnSelection;
 -  
 +
    private HiddenColumns hiddenColumns;
  
    private ProfilesI hconsensus;
      hStrucConsensus = null;
      fadedImage = null;
      annotationPanel = null;
+     rendererFactoryI = null;
    }
  
    void drawStemAnnot(Graphics g, Annotation[] row_annotations, int lastSSX,
            boolean validRes, boolean validEnd)
    {
      g.setColor(STEM_COLOUR);
 -    int sCol = (lastSSX / charWidth) + startRes;
 +    int sCol = (lastSSX / charWidth)
 +            + hiddenColumns.visibleToAbsoluteColumn(startRes);
      int x1 = lastSSX;
      int x2 = (x * charWidth);
  
           * display a backward arrow
           */
          g.fillPolygon(new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
 -                new int[] { y + iconOffset, y + 14 + iconOffset,
 -                    y + 8 + iconOffset }, 3);
 +                new int[]
 +                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
 +                3);
          x1 += 5;
        }
        if (diffdownstream)
           * if annotation ending with an opeing base pair half of the stem, 
           * display a forward arrow
           */
 -        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 }, new int[] {
 -            y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset }, 3);
 +        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 },
 +                new int[]
 +                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
 +                3);
          x2 -= 5;
        }
        if (diffupstream)
      // System.out.println(nonCanColor);
  
      g.setColor(nonCanColor);
 -    int sCol = (lastSSX / charWidth) + startRes;
 +    int sCol = (lastSSX / charWidth)
 +            + hiddenColumns.visibleToAbsoluteColumn(startRes);
      int x1 = lastSSX;
      int x2 = (x * charWidth);
  
      boolean diffdownstream = !validRes || !validEnd
              || row_annotations[column] == null
              || !dc.equals(row_annotations[column].displayCharacter);
 -    // System.out.println("Column "+column+" diff up: "+diffupstream+" down:"+diffdownstream);
 +    // System.out.println("Column "+column+" diff up: "+diffupstream+"
 +    // down:"+diffdownstream);
      // If a closing base pair half of the stem, display a backward arrow
      if (column > 0 && Rna.isClosingParenthesis(dc))
      {
        // dc.equals(row_annotations[column-2].displayCharacter))
        {
          g.fillPolygon(new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
 -                new int[] { y + iconOffset, y + 14 + iconOffset,
 -                    y + 8 + iconOffset }, 3);
 +                new int[]
 +                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
 +                3);
          x1 += 5;
        }
        if (diffdownstream)
        // display a forward arrow
        if (diffdownstream)
        {
 -        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 }, new int[] {
 -            y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset }, 3);
 +        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 },
 +                new int[]
 +                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
 +                3);
          x2 -= 5;
        }
        if (diffupstream)
        useClip = false;
      }
  
+     rendererFactoryI = AnnotationRendererFactory.getRendererFactory();
      updateFromAlignViewport(av);
    }
  
         * the alignment has no colourscheme set
         * (would like to use user preference but n/a for applet)
         */
 -      ColourSchemeI col = av.getAlignment().isNucleotide() ? new NucleotideColourScheme()
 +      ColourSchemeI col = av.getAlignment().isNucleotide()
 +              ? new NucleotideColourScheme()
                : new ZappoColourScheme();
        profcolour = new ResidueShader(col);
      }
      // properties/rendering attributes as a global 'alignment group' which holds
      // all vis settings for the alignment as a whole rather than a subset
      //
 -    if (aa.autoCalculated
 -            && (aa.label.startsWith("Consensus") || aa.label
 -                    .startsWith("cDNA Consensus")))
 +    if (aa.autoCalculated && (aa.label.startsWith("Consensus")
 +            || aa.label.startsWith("cDNA Consensus")))
      {
        boolean forComplement = aa.label.startsWith("cDNA Consensus");
        if (aa.groupRef != null && aa.groupRef.consensusData != null
        {
          if (forComplement)
          {
 -          return AAFrequency.extractCdnaProfile(
 -                  complementConsensus[column], av_ignoreGapsConsensus);
 +          return AAFrequency.extractCdnaProfile(complementConsensus[column],
 +                  av_ignoreGapsConsensus);
          }
          else
          {
  
    boolean rna = false;
  
+   private AnnotationRendererFactoryI rendererFactoryI;
    /**
     * Render the annotation rows associated with an alignment.
     * 
              .getAlignmentStrucConsensusAnnotation();
      final AlignmentAnnotation complementConsensusAnnot = av
              .getComplementConsensusAnnotation();
 -    boolean renderHistogram = true, renderProfile = true, normaliseProfile = false, isRNA = rna;
 +    boolean renderHistogram = true, renderProfile = true,
 +            normaliseProfile = false, isRNA = rna;
  
      BitSet graphGroupDrawn = new BitSet();
      int charOffset = 0; // offset for a label
        lastSS = ' ';
        lastSSX = 0;
  
 -      if (!useClip
 -              || ((y - charHeight) < visHeight && (y + row.height + charHeight * 2) >= sOffset))
 +      if (!useClip || ((y - charHeight) < visHeight
 +              && (y + row.height + charHeight * 2) >= sOffset))
        {// if_in_visible_region
          if (!clipst)
          {
          {
            y += charHeight;
            usedFaded = true;
 -          g.drawImage(fadedImage, 0, y - row.height, imgWidth, y, 0, y
 -                  - row.height, imgWidth, y, annotationPanel);
 +          g.drawImage(fadedImage, 0, y - row.height, imgWidth, y, 0,
 +                  y - row.height, imgWidth, y, annotationPanel);
            g.setColor(Color.black);
            // g.drawString("Calculating "+aa[i].label+"....",20, y-row.height/2);
  
          {
            if (hasHiddenColumns)
            {
 -            column = hiddenColumns.adjustForHiddenColumns(startRes + x);
 +            column = hiddenColumns.visibleToAbsoluteColumn(startRes + x);
              if (column > row_annotations.length - 1)
              {
                break;
            {
              validRes = true;
            }
 -          final String displayChar = validRes ? row_annotations[column].displayCharacter
 +          final String displayChar = validRes
 +                  ? row_annotations[column].displayCharacter
                    : null;
            if (x > -1)
            {
  
                if (column == 0 || row.graph > 0)
                {
 -                g.drawString(displayChar, (x * charWidth) + charOffset, y
 -                        + iconOffset);
 +                g.drawString(displayChar, (x * charWidth) + charOffset,
 +                        y + iconOffset);
                }
 -              else if (row_annotations[column - 1] == null
 -                      || (labelAllCols
 -                              || !displayChar
 -                                      .equals(row_annotations[column - 1].displayCharacter) || (displayChar
 -                              .length() < 2 && row_annotations[column].secondaryStructure == ' ')))
 +              else if (row_annotations[column - 1] == null || (labelAllCols
 +                      || !displayChar.equals(
 +                              row_annotations[column - 1].displayCharacter)
 +                      || (displayChar.length() < 2
 +                              && row_annotations[column].secondaryStructure == ' ')))
                {
 -                g.drawString(displayChar, x * charWidth + charOffset, y
 -                        + iconOffset);
 +                g.drawString(displayChar, x * charWidth + charOffset,
 +                        y + iconOffset);
                }
                g.setFont(ofont);
              }
                {
  
                  int nb_annot = x - temp;
 -                // System.out.println("\t type :"+lastSS+"\t x :"+x+"\t nbre annot :"+nb_annot);
 +                // System.out.println("\t type :"+lastSS+"\t x :"+x+"\t nbre
 +                // annot :"+nb_annot);
                  switch (lastSS)
                  {
                  case '(': // Stem case for RNA secondary structure
                    break;
                  default:
                    g.setColor(Color.gray);
 -                  g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth)
 -                          - lastSSX, 2);
 +                  g.fillRect(lastSSX, y + 6 + iconOffset,
 +                          (x * charWidth) - lastSSX, 2);
                    temp = x;
                    break;
                  }
                      row.graphMin, row.graphMax, y, renderHistogram,
                      renderProfile, normaliseProfile);
            }
+           else
+           {
+             AnnotationRowRendererI renderer = rendererFactoryI
+                     .getRendererFor(row);
+             if (renderer != null)
+             {
+               renderer.renderRow(g, charWidth, charHeight,
+                       hasHiddenColumns, av, hiddenColumns, columnSelection,
+                       row, row_annotations, startRes, endRes, row.graphMin,
+                       row.graphMax, y);
+             }
+             if (debugRedraw)
+             {
+               if (renderer == null)
+               {
+                 System.err.println("No renderer found for "
+                         + row.toString());
+               }
+               else
+               {
+                 System.err.println("rendered with "
+                         + renderer.getClass().toString());
+               }
+             }
+           }
          }
        }
        else
          {
            clipend = true;
          }
 -      }// end if_in_visible_region
 +      } // end if_in_visible_region
        if (row.graph > 0 && row.hasText)
        {
          y += charHeight;
        {
          if (clipst)
          {
 -          System.err.println("Start clip at : " + yfrom + " (index " + f_i
 -                  + ")");
 +          System.err.println(
 +                  "Start clip at : " + yfrom + " (index " + f_i + ")");
          }
          if (clipend)
          {
 -          System.err.println("End clip at : " + yto + " (index " + f_to
 -                  + ")");
 +          System.err.println(
 +                  "End clip at : " + yto + " (index " + f_to + ")");
          }
        }
        ;
    private Color sdNOTCANONICAL_COLOUR;
  
    void drawGlyphLine(Graphics g, Annotation[] row, int lastSSX, int x,
 -          int y, int iconOffset, int startRes, int column,
 -          boolean validRes, boolean validEnd)
 +          int y, int iconOffset, int startRes, int column, boolean validRes,
 +          boolean validEnd)
    {
      g.setColor(GLYPHLINE_COLOR);
      g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX, 2);
  
    void drawSheetAnnot(Graphics g, Annotation[] row,
  
 -  int lastSSX, int x, int y, int iconOffset, int startRes, int column,
 -          boolean validRes, boolean validEnd)
 +          int lastSSX, int x, int y, int iconOffset, int startRes,
 +          int column, boolean validRes, boolean validEnd)
    {
      g.setColor(SHEET_COLOUR);
  
      if (!validEnd || !validRes || row == null || row[column] == null
              || row[column].secondaryStructure != 'E')
      {
 -      g.fillRect(lastSSX, y + 4 + iconOffset,
 -              (x * charWidth) - lastSSX - 4, 7);
 -      g.fillPolygon(new int[] { (x * charWidth) - 4, (x * charWidth) - 4,
 -          (x * charWidth) }, new int[] { y + iconOffset,
 -          y + 14 + iconOffset, y + 7 + iconOffset }, 3);
 +      g.fillRect(lastSSX, y + 4 + iconOffset, (x * charWidth) - lastSSX - 4,
 +              7);
 +      g.fillPolygon(
 +              new int[]
 +              { (x * charWidth) - 4, (x * charWidth) - 4, (x * charWidth) },
 +              new int[]
 +              { y + iconOffset, y + 14 + iconOffset, y + 7 + iconOffset },
 +              3);
      }
      else
      {
 -      g.fillRect(lastSSX, y + 4 + iconOffset,
 -              (x + 1) * charWidth - lastSSX, 7);
 +      g.fillRect(lastSSX, y + 4 + iconOffset, (x + 1) * charWidth - lastSSX,
 +              7);
      }
  
    }
  
    void drawHelixAnnot(Graphics g, Annotation[] row, int lastSSX, int x,
 -          int y, int iconOffset, int startRes, int column,
 -          boolean validRes, boolean validEnd)
 +          int y, int iconOffset, int startRes, int column, boolean validRes,
 +          boolean validEnd)
    {
      g.setColor(HELIX_COLOUR);
  
 -    int sCol = (lastSSX / charWidth) + startRes;
 +    int sCol = (lastSSX / charWidth)
 +            + hiddenColumns.visibleToAbsoluteColumn(startRes);
      int x1 = lastSSX;
      int x2 = (x * charWidth);
  
        else
        {
          // g.setColor(Color.magenta);
 -        g.fillRoundRect(lastSSX + ofs, y + 4 + iconOffset, x2 - x1 - ofs
 -                + 1, 8, 0, 0);
 +        g.fillRoundRect(lastSSX + ofs, y + 4 + iconOffset,
 +                x2 - x1 - ofs + 1, 8, 0, 0);
  
        }
  
    }
  
    void drawLineGraph(Graphics g, AlignmentAnnotation _aa,
 -          Annotation[] aa_annotations, int sRes, int eRes, int y,
 -          float min, float max, int graphHeight)
 +          Annotation[] aa_annotations, int sRes, int eRes, int y, float min,
 +          float max, int graphHeight)
    {
      if (sRes > aa_annotations.length)
      {
        column = sRes + x;
        if (hasHiddenColumns)
        {
 -        column = hiddenColumns.adjustForHiddenColumns(column);
 +        column = hiddenColumns.visibleToAbsoluteColumn(column);
        }
  
        if (column > aaMax)
          g.setColor(aa_annotations[column].colour);
        }
  
 -      y1 = y
 -              - (int) (((aa_annotations[column - 1].value - min) / range) * graphHeight);
 -      y2 = y
 -              - (int) (((aa_annotations[column].value - min) / range) * graphHeight);
 +      y1 = y - (int) (((aa_annotations[column - 1].value - min) / range)
 +              * graphHeight);
 +      y2 = y - (int) (((aa_annotations[column].value - min) / range)
 +              * graphHeight);
  
 -      g.drawLine(x * charWidth - charWidth / 2, y1, x * charWidth
 -              + charWidth / 2, y2);
 +      g.drawLine(x * charWidth - charWidth / 2, y1,
 +              x * charWidth + charWidth / 2, y2);
        x++;
      }
  
        g.setColor(_aa.threshold.colour);
        Graphics2D g2 = (Graphics2D) g;
        g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
 -              BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f }, 0f));
 +              BasicStroke.JOIN_ROUND, 3f, new float[]
 +              { 5f, 3f }, 0f));
  
        y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
        g.drawLine(0, y2, (eRes - sRes) * charWidth, y2);
        column = sRes + x;
        if (hasHiddenColumns)
        {
 -        column = hiddenColumns.adjustForHiddenColumns(column);
 +        column = hiddenColumns.visibleToAbsoluteColumn(column);
        }
  
        if (column > aaMax)
          g.setColor(aa_annotations[column].colour);
        }
  
 -      y1 = y
 -              - (int) (((aa_annotations[column].value - min) / (range)) * _aa.graphHeight);
 +      y1 = y - (int) (((aa_annotations[column].value - min) / (range))
 +              * _aa.graphHeight);
  
        if (renderHistogram)
        {
              ht += scl;
              // next profl[] position is profile % for the character(s)
              scl = htn * scale * profl[c++];
 -            lm = ofont.getLineMetrics(dc, 0, 1, g.getFontMetrics()
 -                    .getFontRenderContext());
 -            Font font = ofont.deriveFont(AffineTransform.getScaleInstance(
 -                    wdth, scl / lm.getAscent()));
 +            lm = ofont.getLineMetrics(dc, 0, 1,
 +                    g.getFontMetrics().getFontRenderContext());
 +            Font font = ofont.deriveFont(AffineTransform
 +                    .getScaleInstance(wdth, scl / lm.getAscent()));
              g.setFont(font);
              lm = g.getFontMetrics().getLineMetrics(dc, 0, 1, g);
  
              }
              g.setColor(colour == Color.white ? Color.lightGray : colour);
  
 -            hght = (ht + (scl - lm.getDescent() - lm.getBaselineOffsets()[lm
 -                    .getBaselineIndex()]));
 +            hght = (ht + (scl - lm.getDescent()
 +                    - lm.getBaselineOffsets()[lm.getBaselineIndex()]));
  
              g.drawChars(dc, 0, dc.length, x * charWidth, (int) hght);
              valuesProcessed++;
        g.setColor(_aa.threshold.colour);
        Graphics2D g2 = (Graphics2D) g;
        g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
 -              BasicStroke.JOIN_ROUND, 3f, new float[] { 5f, 3f }, 0f));
 +              BasicStroke.JOIN_ROUND, 3f, new float[]
 +              { 5f, 3f }, 0f));
  
 -      y2 = (int) (y - ((_aa.threshold.value - min) / range)
 -              * _aa.graphHeight);
 +      y2 = (int) (y
 +              - ((_aa.threshold.value - min) / range) * _aa.graphHeight);
        g.drawLine(0, y2, (eRes - sRes) * charWidth, y2);
        g2.setStroke(new BasicStroke());
      }
index 0000000,bed54e8..7413e65
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,118 +1,118 @@@
+ /**
+  * 
+  */
+ package jalview.renderer;
+ import jalview.api.AlignViewportI;
+ import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.Annotation;
+ import jalview.datamodel.ColumnSelection;
+ import jalview.datamodel.ContactListI;
+ import jalview.datamodel.ContactRange;
+ import jalview.datamodel.HiddenColumns;
+ import jalview.renderer.api.AnnotationRowRendererI;
+ import java.awt.Color;
+ import java.awt.Graphics;
+ /**
+  * @author jprocter
+  *
+  */
+ public class ContactMapRenderer implements AnnotationRowRendererI
+ {
+   @Override
+   public void renderRow(Graphics g, int charWidth, int charHeight,
+           boolean hasHiddenColumns, AlignViewportI viewport, HiddenColumns hiddenColumns,
+           ColumnSelection columnSelection, AlignmentAnnotation _aa,
+           Annotation[] aa_annotations, int sRes, int eRes, float min,
+           float max, int y)
+   {
+     if (sRes > aa_annotations.length)
+     {
+       return;
+     }
+     eRes = Math.min(eRes, aa_annotations.length);
+     int x = 0, y2 = y;
+     g.setColor(Color.pink);
+     g.drawLine(x, y2, (eRes - sRes) * charWidth, y2);
+     int column;
+     int aaMax = aa_annotations.length - 1;
+     while (x < eRes - sRes)
+     {
+       column = sRes + x;
+       if (hasHiddenColumns)
+       {
 -        column = hiddenColumns.adjustForHiddenColumns(column);
++        column = hiddenColumns.visibleToAbsoluteColumn(column);
+       }
+       if (column > aaMax)
+       {
+         break;
+       }
+       if (aa_annotations[column] == null)
+       {
+         x++;
+         continue;
+       }
+       /*
+        * {profile type, #values, total count, char1, pct1, char2, pct2...}
+        */
+       ContactListI contacts = viewport.getContactList(_aa, column);
+       if (contacts == null)
+       {
+         return;
+       }
+       // cell height to render
+       double scale = (_aa.graphHeight < contacts.getContactHeight()) ? 1
+               : ((double) _aa.graphHeight)
+                       / (double) contacts.getContactHeight();
+       int cstart, cend = -1;
+       for (int ht = y2, eht = y2 - _aa.graphHeight; ht >= eht; ht -= scale)
+       {
+         cstart = cend + 1;
+         cend = Math.max(cstart + 1, contacts.getContactHeight()
+                 * ((ht - y2) / _aa.graphHeight));
+         // TODO show maximum colour for range - sort of done
+         // also need a 'getMaxPosForRange(start,end)'
+         g.setColor(getColorForRange(min, max, contacts, cstart, cend));
+         if (scale > 1)
+         {
+           g.fillRect(x * charWidth, ht, charWidth, 1 + (int) scale);
+         }
+         else
+         {
+           g.drawLine(x * charWidth, ht, (x + 1) * charWidth, ht);
+         }
+       }
+       x++;
+     }
+   }
+   Color minColor = Color.white, maxColor = Color.magenta;
+   Color shadeFor(float min, float max, float value)
+   {
+     return jalview.util.ColorUtils.getGraduatedColour(value, 0, minColor,
+             max, maxColor);
+   }
+   public Color getColorForRange(float min, float max, ContactListI cl,
+           int i, int j)
+   {
+     ContactRange cr = cl.getRangeFor(i, j);
+     // average for moment - probably more interested in maxIntProj though
+     return shadeFor(min, max, (float) cr.getMean());
+   }
+ }
@@@ -22,7 -22,6 +22,7 @@@ package jalview.viewmodel
  
  import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
  import jalview.analysis.Conservation;
 +import jalview.analysis.TreeModel;
  import jalview.api.AlignCalcManagerI;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
@@@ -34,7 -33,9 +34,8 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.Annotation;
 -import jalview.datamodel.CigarArray;
  import jalview.datamodel.ColumnSelection;
+ import jalview.datamodel.ContactListI;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.ProfilesI;
@@@ -67,7 -68,6 +68,7 @@@ import java.util.BitSet
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.Hashtable;
 +import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
   * @author jimp
   * 
   */
 -public abstract class AlignmentViewport implements AlignViewportI,
 -        CommandListener, VamsasSource
 +public abstract class AlignmentViewport
 +        implements AlignViewportI, CommandListener, VamsasSource
  {
 -  final protected ViewportRanges ranges;
 +  protected ViewportRanges ranges;
  
    protected ViewStyleI viewStyle = new ViewStyle();
  
    public void setWrapAlignment(boolean state)
    {
      viewStyle.setWrapAlignment(state);
 +    ranges.setWrappedMode(state);
    }
  
    /**
      viewStyle.setSeqNameItalics(default1);
    }
  
 -
 -
    @Override
    public AlignmentI getAlignment()
    {
        {
          residueShading.setConservation(hconservation);
        }
 +      /*
 +       * reset conservation flag in case just set to false if
 +       * Conservation was null (calculation still in progress)
 +       */
 +      residueShading.setConservationApplied(getConservationSelected());
        residueShading.alignmentChanged(alignment, hiddenRepSequences);
      }
  
           * retain any colour thresholds per group while
           * changing choice of colour scheme (JAL-2386)
           */
 -        sg.setColourScheme(cs);
 +        sg.setColourScheme(
 +                cs == null ? null : cs.getInstance(this, sg));
          if (cs != null)
          {
 -          sg.getGroupColourScheme()
 -                  .alignmentChanged(sg, hiddenRepSequences);
 +          sg.getGroupColourScheme().alignmentChanged(sg,
 +                  hiddenRepSequences);
          }
        }
      }
    @Override
    public ColourSchemeI getGlobalColourScheme()
    {
 -    return residueShading == null ? null : residueShading
 -            .getColourScheme();
 +    return residueShading == null ? null : residueShading.getColourScheme();
    }
  
    @Override
      {
        return;
      }
 -    if (calculator
 -            .getRegisteredWorkersOfClass(jalview.workers.ConservationThread.class) == null)
 +    if (calculator.getRegisteredWorkersOfClass(
 +            jalview.workers.ConservationThread.class) == null)
      {
 -      calculator.registerWorker(new jalview.workers.ConservationThread(
 -              this, ap));
 +      calculator.registerWorker(
 +              new jalview.workers.ConservationThread(this, ap));
      }
    }
  
      {
        return;
      }
 -    if (calculator.getRegisteredWorkersOfClass(ConsensusThread.class) == null)
 +    if (calculator
 +            .getRegisteredWorkersOfClass(ConsensusThread.class) == null)
      {
        calculator.registerWorker(new ConsensusThread(this, ap));
      }
        }
        if (doConsensus)
        {
 -        if (calculator
 -                .getRegisteredWorkersOfClass(ComplementConsensusThread.class) == null)
 +        if (calculator.getRegisteredWorkersOfClass(
 +                ComplementConsensusThread.class) == null)
          {
            calculator
                    .registerWorker(new ComplementConsensusThread(this, ap));
      {
        return;
      }
 -    if (calculator.getRegisteredWorkersOfClass(StrucConsensusThread.class) == null)
 +    if (calculator.getRegisteredWorkersOfClass(
 +            StrucConsensusThread.class) == null)
      {
        calculator.registerWorker(new StrucConsensusThread(this, ap));
      }
      groupConsensus = null;
      groupConservation = null;
      hconsensus = null;
 +    hconservation = null;
      hcomplementConsensus = null;
 -    // colour scheme may hold reference to consensus
 -    residueShading = null;
 -    // TODO remove listeners from changeSupport?
 +    gapcounts = null;
 +    calculator = null;
 +    residueShading = null; // may hold a reference to Consensus
      changeSupport = null;
 +    ranges = null;
 +    currentTree = null;
 +    selectionGroup = null;
      setAlignment(null);
    }
  
    {
      if (sequenceSetID != null)
      {
 -      System.err
 -              .println("Warning - overwriting a sequenceSetId for a viewport!");
 +      System.err.println(
 +              "Warning - overwriting a sequenceSetId for a viewport!");
      }
      sequenceSetID = new String(newid);
    }
    public void removePropertyChangeListener(
            java.beans.PropertyChangeListener listener)
    {
 -    changeSupport.removePropertyChangeListener(listener);
 +    if (changeSupport != null)
 +    {
 +      changeSupport.removePropertyChangeListener(listener);
 +    }
    }
  
    /**
    // common hide/show seq stuff
    public void showAllHiddenSeqs()
    {
 +    int startSeq = ranges.getStartSeq();
 +    int endSeq = ranges.getEndSeq();
 +
      if (alignment.getHiddenSequences().getSize() > 0)
      {
        if (selectionGroup == null)
          selectionGroup = new SequenceGroup();
          selectionGroup.setEndRes(alignment.getWidth() - 1);
        }
 -      List<SequenceI> tmp = alignment.getHiddenSequences().showAll(
 -              hiddenRepSequences);
 +      List<SequenceI> tmp = alignment.getHiddenSequences()
 +              .showAll(hiddenRepSequences);
        for (SequenceI seq : tmp)
        {
          selectionGroup.addSequence(seq, false);
  
        hiddenRepSequences = null;
  
 +      ranges.setStartEndSeq(startSeq, endSeq + tmp.size());
 +
        firePropertyChange("alignment", null, alignment.getSequences());
        // used to set hasHiddenRows/hiddenRepSequences here, after the property
        // changed event
  
    public void showSequence(int index)
    {
 -    List<SequenceI> tmp = alignment.getHiddenSequences().showSequence(
 -            index, hiddenRepSequences);
 +    int startSeq = ranges.getStartSeq();
 +    int endSeq = ranges.getEndSeq();
 +
 +    List<SequenceI> tmp = alignment.getHiddenSequences().showSequence(index,
 +            hiddenRepSequences);
      if (tmp.size() > 0)
      {
        if (selectionGroup == null)
          selectionGroup.addSequence(seq, false);
          setSequenceAnnotationsVisible(seq, true);
        }
 +
 +      ranges.setStartEndSeq(startSeq, endSeq + tmp.size());
 +
        firePropertyChange("alignment", null, alignment.getSequences());
        sendSelection();
      }
  
    public void hideSequence(SequenceI[] seq)
    {
 +    /*
 +     * cache offset to first visible sequence
 +     */
 +    int startSeq = ranges.getStartSeq();
 +
      if (seq != null)
      {
        for (int i = 0; i < seq.length; i++)
          alignment.getHiddenSequences().hideSequence(seq[i]);
          setSequenceAnnotationsVisible(seq[i], false);
        }
 +      ranges.setStartSeq(startSeq);
        firePropertyChange("alignment", null, alignment.getSequences());
      }
    }
      }
  
      int gsize = selectionGroup.getSize();
 -    SequenceI[] hseqs = selectionGroup.getSequences().toArray(
 -            new SequenceI[gsize]);
 +    SequenceI[] hseqs = selectionGroup.getSequences()
 +            .toArray(new SequenceI[gsize]);
  
      hideSequence(hseqs);
      setSelectionGroup(null);
     */
    public boolean isHiddenRepSequence(SequenceI seq)
    {
 -    return (hiddenRepSequences != null && hiddenRepSequences
 -            .containsKey(seq));
 +    return (hiddenRepSequences != null
 +            && hiddenRepSequences.containsKey(seq));
    }
  
    /**
    @Override
    public int adjustForHiddenSeqs(int alignmentIndex)
    {
 -    return alignment.getHiddenSequences().adjustForHiddenSeqs(
 -            alignmentIndex);
 +    return alignment.getHiddenSequences()
 +            .adjustForHiddenSeqs(alignmentIndex);
    }
  
    @Override
    public void invertColumnSelection()
    {
      colSel.invertColumnSelection(0, alignment.getWidth(), alignment);
 +    isColSelChanged(true);
    }
  
    @Override
    }
  
    @Override
 -  public CigarArray getViewAsCigars(boolean selectedRegionOnly)
 -  {
 -    return new CigarArray(alignment, alignment.getHiddenColumns(),
 -            (selectedRegionOnly ? selectionGroup : null));
 -  }
 -
 -  @Override
    public jalview.datamodel.AlignmentView getAlignmentView(
            boolean selectedOnly)
    {
            boolean selectedOnly, boolean markGroups)
    {
      return new AlignmentView(alignment, alignment.getHiddenColumns(),
 -            selectionGroup, alignment.getHiddenColumns() != null
 +            selectionGroup,
 +            alignment.getHiddenColumns() != null
                      && alignment.getHiddenColumns().hasHiddenColumns(),
 -            selectedOnly,
 -            markGroups);
 +            selectedOnly, markGroups);
    }
  
    @Override
      if (alignment.getHiddenColumns() != null
              && alignment.getHiddenColumns().hasHiddenColumns())
      {
 -      selection = alignment.getHiddenColumns().getVisibleSequenceStrings(
 -              start, end, seqs);
 +      for (i = 0; i < iSize; i++)
 +      {
 +        Iterator<int[]> blocks = alignment.getHiddenColumns()
 +                .getVisContigsIterator(start, end + 1, false);
 +        selection[i] = seqs[i].getSequenceStringFromIterator(blocks);
 +      }
      }
      else
      {
        {
          if (start == 0)
          {
 -          start = hidden.adjustForHiddenColumns(start);
 +          start = hidden.visibleToAbsoluteColumn(start);
          }
  
 -        end = hidden.getHiddenBoundaryRight(start);
 +        end = hidden.getNextHiddenBoundary(false, start);
          if (start == end)
          {
            end = max;
  
        if (hidden != null && hidden.hasHiddenColumns())
        {
 -        start = hidden.adjustForHiddenColumns(end);
 -        start = hidden.getHiddenBoundaryLeft(start) + 1;
 +        start = hidden.visibleToAbsoluteColumn(end);
 +        start = hidden.getNextHiddenBoundary(true, start) + 1;
        }
      } while (end < max);
  
          AlignmentAnnotation clone = new AlignmentAnnotation(annot);
          if (selectedOnly && selectionGroup != null)
          {
 -          alignment.getHiddenColumns().makeVisibleAnnotation(
 -                  selectionGroup.getStartRes(),
 -                  selectionGroup.getEndRes(), clone);
 +          clone.makeVisibleAnnotation(
 +                  selectionGroup.getStartRes(), selectionGroup.getEndRes(),
 +                  alignment.getHiddenColumns());
          }
          else
          {
 -          alignment.getHiddenColumns().makeVisibleAnnotation(clone);
 +          clone.makeVisibleAnnotation(alignment.getHiddenColumns());
          }
          ala.add(clone);
        }
      {
        gapcounts = new AlignmentAnnotation("Occupancy",
                MessageManager.getString("label.occupancy_descr"),
 -              new Annotation[1], 0f,
 -              alignment.getHeight(), AlignmentAnnotation.BAR_GRAPH);
 +              new Annotation[1], 0f, alignment.getHeight(),
 +              AlignmentAnnotation.BAR_GRAPH);
        gapcounts.hasText = true;
        gapcounts.autoCalculated = true;
        gapcounts.scaleColLabel = true;
        {
          conservation = new AlignmentAnnotation("Conservation",
                  MessageManager.formatMessage("label.conservation_descr",
 -                        getConsPercGaps()), new Annotation[1],
 -                0f, 11f, AlignmentAnnotation.BAR_GRAPH);
 +                        getConsPercGaps()),
 +                new Annotation[1], 0f, 11f, AlignmentAnnotation.BAR_GRAPH);
          conservation.hasText = true;
          conservation.autoCalculated = true;
          alignment.addAnnotation(conservation);
      viewStyle = new ViewStyle(settingsForView);
      if (residueShading != null)
      {
 -      residueShading.setConservationApplied(settingsForView
 -              .isConservationColourSelected());
 +      residueShading.setConservationApplied(
 +              settingsForView.isConservationColourSelected());
      }
    }
  
      return sortAnnotationsBy;
    }
  
 -  public void setSortAnnotationsBy(SequenceAnnotationOrder sortAnnotationsBy)
 +  public void setSortAnnotationsBy(
 +          SequenceAnnotationOrder sortAnnotationsBy)
    {
      this.sortAnnotationsBy = sortAnnotationsBy;
    }
        return 0;
      }
      boolean iAmProtein = !getAlignment().isNucleotide();
 -    AlignmentI proteinAlignment = iAmProtein ? getAlignment() : complement
 -            .getAlignment();
 +    AlignmentI proteinAlignment = iAmProtein ? getAlignment()
 +            : complement.getAlignment();
      if (proteinAlignment == null)
      {
        return 0;
       */
      int lastSeq = alignment.getHeight() - 1;
      List<AlignedCodonFrame> seqMappings = null;
 -    for (int seqNo = ranges.getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
 +    for (int seqNo = ranges
 +            .getStartSeq(); seqNo <= lastSeq; seqNo++, seqOffset++)
      {
        sequence = getAlignment().getSequenceAt(seqNo);
        if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
        {
          continue;
        }
 -      seqMappings = MappingUtils
 -              .findMappingsForSequenceAndOthers(sequence, mappings,
 -                      getCodingComplement().getAlignment().getSequences());
 +      seqMappings = MappingUtils.findMappingsForSequenceAndOthers(sequence,
 +              mappings,
 +              getCodingComplement().getAlignment().getSequences());
        if (!seqMappings.isEmpty())
        {
          break;
     */
    private SearchResultsI searchResults = null;
  
 +  protected TreeModel currentTree = null;
 +
    @Override
    public boolean hasSearchResults()
    {
      return searchResults;
    }
  
+   @Override
+   public ContactListI getContactList(AlignmentAnnotation _aa, int column)
+   {
+     return alignment.getContactListFor(_aa, column);
+   }
    /**
     * get the consensus sequence as displayed under the PID consensus annotation
     * row.
          }
        }
      }
 -  
 +
      SequenceI sq = new Sequence("Consensus", seqs.toString());
      sq.setDescription("Percentage Identity Consensus "
              + ((ignoreGapsInConsensusCalculation) ? " without gaps" : ""));
      return sq;
    }
 +
 +  @Override
 +  public void setCurrentTree(TreeModel tree)
 +  {
 +    currentTree = tree;
 +  }
 +
 +  @Override
 +  public TreeModel getCurrentTree()
 +  {
 +    return currentTree;
 +  }
 +
 +  /**
 +   * flag set to indicate if structure views might be out of sync with sequences
 +   * in the alignment
 +   */
 +
 +  private boolean needToUpdateStructureViews = false;
 +
 +  @Override
 +  public boolean isUpdateStructures()
 +  {
 +    return needToUpdateStructureViews;
 +  }
 +
 +  @Override
 +  public void setUpdateStructures(boolean update)
 +  {
 +    needToUpdateStructureViews = update;
 +  }
 +
 +  @Override
 +  public boolean needToUpdateStructureViews()
 +  {
 +    boolean update = needToUpdateStructureViews;
 +    needToUpdateStructureViews = false;
 +    return update;
 +  }
 +
 +  @Override
 +  public void addSequenceGroup(SequenceGroup sequenceGroup)
 +  {
 +    alignment.addGroup(sequenceGroup);
 +
 +    Color col = sequenceGroup.idColour;
 +    if (col != null)
 +    {
 +      col = col.brighter();
 +
 +      for (SequenceI sq : sequenceGroup.getSequences())
 +      {
 +        setSequenceColour(sq, col);
 +      }
 +    }
 +
 +    if (codingComplement != null)
 +    {
 +      SequenceGroup mappedGroup = MappingUtils
 +              .mapSequenceGroup(sequenceGroup, this, codingComplement);
 +      if (mappedGroup.getSequences().size() > 0)
 +      {
 +        codingComplement.getAlignment().addGroup(mappedGroup);
 +
 +        if (col != null)
 +        {
 +          for (SequenceI seq : mappedGroup.getSequences())
 +          {
 +            codingComplement.setSequenceColour(seq, col);
 +          }
 +        }
 +      }
 +      // propagate the structure view update flag according to our own setting
 +      codingComplement.setUpdateStructures(needToUpdateStructureViews);
 +    }
 +  }
  }