JAL-1264 further refactoring and tests
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 23 Oct 2014 14:37:42 +0000 (15:37 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 23 Oct 2014 14:37:42 +0000 (15:37 +0100)
1  2 
src/jalview/analysis/AlignmentAnnotationUtils.java
src/jalview/datamodel/Sequence.java
src/jalview/gui/PopupMenu.java
test/jalview/analysis/AlignmentAnnotationUtilsTest.java

index 0000000,0000000..b5f77fd
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,206 @@@
++package jalview.analysis;
++
++import jalview.datamodel.AlignmentAnnotation;
++import jalview.datamodel.SequenceI;
++import jalview.renderer.AnnotationRenderer;
++
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.BitSet;
++import java.util.Collections;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++
++public class AlignmentAnnotationUtils
++{
++
++  /**
++   * Helper method to populate lists of annotation types for the Show/Hide
++   * Annotations menus. If sequenceGroup is not null, this is restricted to
++   * annotations which are associated with sequences in the selection group.
++   * <p/>
++   * If an annotation row is currently visible, its type (label) is added (once
++   * only per type), to the shownTypes list. If it is currently hidden, it is
++   * added to the hiddenTypesList.
++   * <p/>
++   * For rows that belong to a line graph group, so are always rendered
++   * together:
++   * <ul>
++   * <li>Treat all rows in the group as visible, if at least one of them is</li>
++   * <li>Build a list of all the annotation types that belong to the group</li>
++   * </ul>
++   * 
++   * @param shownTypes
++   *          a map, keyed by calcId (annotation source), whose entries are the
++   *          lists of annotation types found for the calcId; each annotation
++   *          type in turn may be a list (in the case of grouped annotations)
++   * @param hiddenTypes
++   *          a map, similar to shownTypes, but for hidden annotation types
++   * @param annotations
++   *          the annotations on the alignment to scan
++   * @param forSequences
++   *          the sequences to restrict search to
++   */
++  public static void getShownHiddenTypes(
++          Map<String, List<List<String>>> shownTypes,
++          Map<String, List<List<String>>> hiddenTypes,
++          List<AlignmentAnnotation> annotations,
++          List<SequenceI> forSequences)
++  {
++    BitSet visibleGraphGroups = AlignmentAnnotationUtils
++            .getVisibleLineGraphGroups(annotations);
++
++    /*
++     * Build a lookup, by calcId (annotation source), of all annotation types in
++     * each graph group.
++     */
++    Map<String, Map<Integer, List<String>>> groupLabels = new HashMap<String, Map<Integer, List<String>>>();
++
++    // trackers for which calcId!label combinations we have dealt with
++    List<String> addedToShown = new ArrayList<String>();
++    List<String> addedToHidden = new ArrayList<String>();
++
++    for (AlignmentAnnotation aa : annotations)
++    {
++      if (forSequences != null
++              && (aa.sequenceRef != null && forSequences
++                      .contains(aa.sequenceRef)))
++      {
++        String calcId = aa.getCalcId();
++
++        /*
++         * Build a 'composite label' for types in line graph groups.
++         */
++        final List<String> labelAsList = new ArrayList<String>();
++        final String displayLabel = aa.label;
++        labelAsList.add(displayLabel);
++        if (aa.graph == AlignmentAnnotation.LINE_GRAPH
++                && aa.graphGroup > -1)
++        {
++          if (!groupLabels.containsKey(calcId))
++          {
++            groupLabels.put(calcId, new HashMap<Integer, List<String>>());
++          }
++          Map<Integer, List<String>> groupLabelsForCalcId = groupLabels
++                  .get(calcId);
++          if (groupLabelsForCalcId.containsKey(aa.graphGroup))
++          {
++            if (!groupLabelsForCalcId.get(aa.graphGroup).contains(
++                    displayLabel))
++            {
++              groupLabelsForCalcId.get(aa.graphGroup).add(displayLabel);
++            }
++          }
++          else
++          {
++            groupLabelsForCalcId.put(aa.graphGroup, labelAsList);
++          }
++        }
++        else
++        /*
++         * 'Simple case' - not a grouped annotation type - list of one label
++         * only
++         */
++        {
++          String rememberAs = calcId + "!" + displayLabel;
++          if (aa.visible && !addedToShown.contains(rememberAs))
++          {
++            if (!shownTypes.containsKey(calcId))
++            {
++              shownTypes.put(calcId, new ArrayList<List<String>>());
++            }
++            shownTypes.get(calcId).add(labelAsList);
++            addedToShown.add(rememberAs);
++          }
++          else
++          {
++            if (!aa.visible && !addedToHidden.contains(rememberAs))
++            {
++              if (!hiddenTypes.containsKey(calcId))
++              {
++                hiddenTypes.put(calcId, new ArrayList<List<String>>());
++              }
++              hiddenTypes.get(calcId).add(labelAsList);
++              addedToHidden.add(rememberAs);
++            }
++          }
++        }
++      }
++    }
++    /*
++     * finally add the 'composite group labels' to the appropriate lists,
++     * depending on whether the group is identified as visible or hidden
++     */
++    for (String calcId : groupLabels.keySet())
++    {
++      for (int group : groupLabels.get(calcId).keySet())
++      {
++        final List<String> groupLabel = groupLabels.get(calcId).get(group);
++        if (visibleGraphGroups.get(group))
++        {
++          if (!shownTypes.containsKey(calcId))
++          {
++            shownTypes.put(calcId, new ArrayList<List<String>>());
++          }
++          shownTypes.get(calcId).add(groupLabel);
++        }
++        else
++        {
++          if (!hiddenTypes.containsKey(calcId))
++          {
++            hiddenTypes.put(calcId, new ArrayList<List<String>>());
++          }
++          hiddenTypes.get(calcId).add(groupLabel);
++        }
++      }
++    }
++  }
++
++  /**
++   * Returns a BitSet (possibly empty) of those graphGroups for line graph
++   * annotations, which have at least one member annotation row marked visible.
++   * <p/>
++   * Only one row in each visible group is marked visible, but when it is drawn,
++   * so are all the other rows in the same group.
++   * <p/>
++   * This lookup set allows us to check whether rows apparently marked not
++   * visible are in fact shown.
++   * 
++   * @see AnnotationRenderer#drawComponent
++   * @param annotations
++   * @return
++   */
++  public static BitSet getVisibleLineGraphGroups(
++          List<AlignmentAnnotation> annotations)
++  {
++    BitSet result = new BitSet();
++    for (AlignmentAnnotation ann : annotations)
++    {
++      if (ann.graph == AlignmentAnnotation.LINE_GRAPH && ann.visible)
++      {
++        int gg = ann.graphGroup;
++        if (gg > -1)
++        {
++          result.set(gg);
++        }
++      }
++    }
++    return result;
++  }
++
++  /**
++   * Converts an array of AlignmentAnnotation into a List of
++   * AlignmentAnnotation. A null array is converted to an empty list.
++   * 
++   * @param anns
++   * @return
++   */
++  public static List<AlignmentAnnotation> asList(
++          AlignmentAnnotation[] anns)
++  {
++    // TODO use AlignmentAnnotationI instead when it exists
++    return (anns == null ? Collections.<AlignmentAnnotation> emptyList()
++            : Arrays.asList(anns));
++  }
++}
@@@ -493,7 -493,7 +493,9 @@@ public class Sequence implements Sequen
    public char[] getSequence(int start, int end)
    {
      if (start < 0)
++    {
        start = 0;
++    }
      // JBPNote - left to user to pad the result here (TODO:Decide on this
      // policy)
      if (start >= sequence.length)
      {
        this.annotation.removeElement(annotation);
        if (this.annotation.size() == 0)
++      {
          this.annotation = null;
++      }
      }
    }
  
        for (int i = 0; i < annotations.length; i++)
        {
          if (annotations[i] != null)
++        {
            addAlignmentAnnotation(annotations[i]);
++        }
        }
      }
    }
      return anns;
    }
  
--  /**
--   * Returns a list of any annotations on the sequence that match the given
--   * calcId (source) and label (type). Null values do not match.
--   * 
--   * @param calcId
--   * @param label
--   * @return
--   */
--  @Override
--  public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
--          String label)
--  {
--    List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
--    if (annotation != null)
--    {
--      for (AlignmentAnnotation ann : annotation)
--      {
--        if (ann.calcId != null && ann.calcId.equals(calcId)
--                && ann.label != null && ann.label.equals(label))
--        {
--          result.add(ann);
--        }
--      }
--    }
--    return result;
--  }
--
    public boolean updatePDBIds()
    {
      if (datasetSequence != null)
      return rna;
    }
  
++  /**
++   * Returns a (possibly empty) list of any annotations that match on given
++   * calcId (source) and label (type). Null values do not match.
++   * 
++   * @param calcId
++   * @param label
++   */
++  @Override
++  public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
++          String label)
++  {
++    List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
++    if (this.annotation != null) {
++      for (AlignmentAnnotation ann : annotation) {
++        if (ann.calcId != null && ann.calcId.equals(calcId)
++                && ann.label != null && ann.label.equals(label))
++        {
++          result.add(ann);
++        }
++      }
++    }
++    return result;
++  }
++
  }
@@@ -21,6 -21,6 +21,7 @@@
  package jalview.gui;
  
  import jalview.analysis.AAFrequency;
++import jalview.analysis.AlignmentAnnotationUtils;
  import jalview.analysis.Conservation;
  import jalview.commands.ChangeCaseCommand;
  import jalview.commands.EditCommand;
@@@ -34,7 -34,7 +35,6 @@@ import jalview.datamodel.SequenceGroup
  import jalview.datamodel.SequenceI;
  import jalview.io.FormatAdapter;
  import jalview.io.SequenceAnnotationReport;
--import jalview.renderer.AnnotationRenderer;
  import jalview.schemes.AnnotationColourGradient;
  import jalview.schemes.Blosum62ColourScheme;
  import jalview.schemes.BuriedColourScheme;
@@@ -60,12 -60,12 +60,14 @@@ import java.awt.event.ActionEvent
  import java.awt.event.ActionListener;
  import java.util.ArrayList;
  import java.util.Arrays;
--import java.util.BitSet;
  import java.util.Collection;
++import java.util.Collections;
  import java.util.HashMap;
  import java.util.Hashtable;
++import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
++import java.util.TreeMap;
  import java.util.Vector;
  
  import javax.swing.ButtonGroup;
@@@ -192,7 -192,7 +194,7 @@@ public class PopupMenu extends JPopupMe
  
    JMenu hideAnnotationsMenu = new JMenu();
  
--  JMenuItem addDatasequenceAnnotations = new JMenuItem();
++  JMenuItem addReferenceAnnotations = new JMenuItem();
  
    JMenuItem sequenceFeature = new JMenuItem();
  
      }
  
      /*
--     * Build menus for annotation types that may be shown or hidden.
++     * Build menus for annotation types that may be shown or hidden, and for
++     * 'reference annotations' that may be added to the alignment. The scope is
++     * annotations for the current selection group (if there is one), else the
++     * current sequence (if there is one), else not applicable (e.g. for popup
++     * menu within the sequence).
       */
--    buildAnnotationTypesMenus();
++    final List<SequenceI> sequenceScope = getSequenceScope(seq);
++    if (!sequenceScope.isEmpty())
++    {
++      buildAnnotationTypesMenus(sequenceScope);
++      configureReferenceAnnotationsMenu(addReferenceAnnotations, sequenceScope);
++    }
  
      try
      {
  
            menuItem = new JMenuItem();
            menuItem.setText(pdb.getId());
--          menuItem.addActionListener(new ActionListener() 
++          menuItem.addActionListener(new ActionListener()
            {
              @Override
--            public void actionPerformed(ActionEvent e) {
--            // TODO re JAL-860: optionally open dialog or provide a menu entry
--            // allowing user to open just one structure per sequence
--            // new AppJmol(pdb, ap.av.collateForPDB(new PDBEntry[]
--            // { pdb })[0], null, ap);
--           new StructureViewer(ap.getStructureSelectionManager())
--                    .viewStructures(pdb,
--                            ap.av.collateForPDB(new PDBEntry[]
--                            { pdb })[0], null, ap);
++            public void actionPerformed(ActionEvent e)
++            {
++              // TODO re JAL-860: optionally open dialog or provide a menu entry
++              // allowing user to open just one structure per sequence
++              // new AppJmol(pdb, ap.av.collateForPDB(new PDBEntry[]
++              // { pdb })[0], null, ap);
++              new StructureViewer(ap.getStructureSelectionManager())
++                      .viewStructures(pdb,
++                              ap.av.collateForPDB(new PDBEntry[]
++                              { pdb })[0], null, ap);
              }
            });
            viewStructureMenu.add(menuItem);
     * composite type name, e.g.
     * <p>
     * IUPredWS (Long), IUPredWS (Short)
++   * 
++   * @param forSequences
     */
--  protected void buildAnnotationTypesMenus()
++  protected void buildAnnotationTypesMenus(List<SequenceI> forSequences)
    {
      showAnnotationsMenu.removeAll();
      hideAnnotationsMenu.removeAll();
      final List<String> all = Arrays.asList(ALL_ANNOTATIONS);
--    addAnnotationTypeToShowHide(showAnnotationsMenu, "", all, true, true);
--    addAnnotationTypeToShowHide(hideAnnotationsMenu, "", all, true, false);
++    addAnnotationTypeToShowHide(showAnnotationsMenu, forSequences, "", all,
++            true, true);
++    addAnnotationTypeToShowHide(hideAnnotationsMenu, forSequences, "", all,
++            true, false);
      showAnnotationsMenu.addSeparator();
      hideAnnotationsMenu.addSeparator();
  
      final AlignmentAnnotation[] annotations = ap.getAlignment()
              .getAlignmentAnnotation();
--    BitSet visibleGraphGroups = PopupMenu
--            .getVisibleLineGraphGroups(annotations);
  
      /*
       * Find shown/hidden annotations types, distinguished by source (calcId),
       */
      Map<String, List<List<String>>> shownTypes = new HashMap<String, List<List<String>>>();
      Map<String, List<List<String>>> hiddenTypes = new HashMap<String, List<List<String>>>();
--    PopupMenu.getAnnotationTypesForShowHide(shownTypes, hiddenTypes,
--            visibleGraphGroups, annotations, selectionGroup);
++    AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes,
++            hiddenTypes,
++            AlignmentAnnotationUtils.asList(annotations),
++            forSequences);
  
      for (String calcId : hiddenTypes.keySet())
      {
        for (List<String> type : hiddenTypes.get(calcId))
        {
--        addAnnotationTypeToShowHide(showAnnotationsMenu, calcId, type,
--                false, true);
++        addAnnotationTypeToShowHide(showAnnotationsMenu, forSequences,
++                calcId, type, false, true);
        }
      }
++    // grey out 'show annotations' if none are hidden
++    showAnnotationsMenu.setEnabled(!hiddenTypes.isEmpty());
  
      for (String calcId : shownTypes.keySet())
      {
        for (List<String> type : shownTypes.get(calcId))
        {
--        addAnnotationTypeToShowHide(hideAnnotationsMenu, calcId, type,
--                false, false);
++        addAnnotationTypeToShowHide(hideAnnotationsMenu, forSequences,
++                calcId, type, false, false);
        }
      }
++    // grey out 'hide annotations' if none are shown
++    hideAnnotationsMenu.setEnabled(!shownTypes.isEmpty());
    }
  
    /**
--   * Helper method to populate lists of annotation types for the Show/Hide
--   * Annotations menus. If sequenceGroup is not null, this is restricted to
--   * annotations which are associated with sequences in the selection group.
--   * <p/>
--   * If an annotation row is currently visible, its type (label) is added (once
--   * only per type), to the shownTypes list. If it is currently hidden, it is
--   * added to the hiddenTypesList.
--   * <p/>
--   * For rows that belong to a line graph group, so are always rendered
--   * together:
--   * <ul>
--   * <li>Treat all rows in the group as visible, if at least one of them is</li>
--   * <li>Build a comma-separated label with all the types that belong to the
--   * group</li>
--   * </ul>
++   * Returns a list of sequences - either the current selection group (if there
++   * is one), else the specified single sequence.
     * 
--   * @param shownTypes
--   *          a map, keyed by calcId (annotation source), whose entries are the
--   *          lists of annotation types found for the calcId; each annotation
--   *          type in turn may be a list (in the case of grouped annotations)
--   * @param hiddenTypes
--   *          a map, similar to shownTypes, but for hidden annotation types
--   * @param visibleGraphGroups
--   *          a lookup keyed by graphGroup identifier
--   * @param annotations
--   *          the annotations on the alignment to scan
--   * @param sequenceGroup
--   *          the sequence group to restrict search to
++   * @param seq
++   * @return
     */
--  public static void getAnnotationTypesForShowHide(
--          Map<String, List<List<String>>> shownTypes,
--          Map<String, List<List<String>>> hiddenTypes,
--          BitSet visibleGraphGroups, AlignmentAnnotation[] annotations,
--          SequenceGroup sequenceGroup)
++  protected List<SequenceI> getSequenceScope(SequenceI seq)
    {
--    /*
--     * Build a lookup, by calcId (annotation source), of all annotation types in
--     * each graph group.
--     */
--    Map<String, Map<Integer, List<String>>> groupLabels = new HashMap<String, Map<Integer, List<String>>>();
--
--    // trackers for which calcId!label combinations we have dealt with
--    List<String> addedToShown = new ArrayList<String>();
--    List<String> addedToHidden = new ArrayList<String>();
--
--    for (AlignmentAnnotation aa : annotations)
--    {
--
--      if (sequenceGroup == null
--              || (aa.sequenceRef != null && sequenceGroup.getSequences()
--                      .contains(aa.sequenceRef)))
--      {
--        String calcId = aa.getCalcId();
--
--        /*
--         * Build a 'composite label' for types in line graph groups.
--         */
--        final List<String> labelAsList = new ArrayList<String>();
--        final String displayLabel = aa.label;
--        labelAsList.add(displayLabel);
--        if (aa.graph == AlignmentAnnotation.LINE_GRAPH
--                && aa.graphGroup > -1)
--        {
--          if (!groupLabels.containsKey(calcId))
--          {
--            groupLabels.put(calcId, new HashMap<Integer, List<String>>());
--          }
--          Map<Integer, List<String>> groupLabelsForCalcId = groupLabels
--                  .get(calcId);
--          if (groupLabelsForCalcId.containsKey(aa.graphGroup))
--          {
--            if (!groupLabelsForCalcId.get(aa.graphGroup).contains(
--                    displayLabel))
--            {
--              groupLabelsForCalcId.get(aa.graphGroup).add(displayLabel);
--            }
--          }
--          else
--          {
--            groupLabelsForCalcId.put(aa.graphGroup, labelAsList);
--          }
--        }
--        else
--        /*
--         * 'Simple case' - not a grouped annotation type - list of one label
--         * only
--         */
--        {
--          String rememberAs = calcId + "!" + displayLabel;
--          if (aa.visible && !addedToShown.contains(rememberAs))
--          {
--            if (!shownTypes.containsKey(calcId))
--            {
--              shownTypes.put(calcId, new ArrayList<List<String>>());
--            }
--            shownTypes.get(calcId).add(labelAsList);
--            addedToShown.add(rememberAs);
--          }
--          else
--          {
--            if (!aa.visible && !addedToHidden.contains(rememberAs))
--            {
--              if (!hiddenTypes.containsKey(calcId))
--              {
--                hiddenTypes.put(calcId, new ArrayList<List<String>>());
--              }
--              hiddenTypes.get(calcId).add(labelAsList);
--              addedToHidden.add(rememberAs);
--            }
--          }
--        }
--      }
--    }
--    /*
--     * finally add the 'composite group labels' to the appropriate lists,
--     * depending on whether the group is identified as visible or hidden
--     */
--    for (String calcId : groupLabels.keySet())
++    List<SequenceI> forSequences = null;
++    final SequenceGroup selectionGroup = ap.av.getSelectionGroup();
++    if (selectionGroup != null && selectionGroup.getSize() > 0)
      {
--      for (int group : groupLabels.get(calcId).keySet())
--      {
--        final List<String> groupLabel = groupLabels.get(calcId).get(group);
--        if (visibleGraphGroups.get(group))
--        {
--          if (!shownTypes.containsKey(calcId))
--          {
--            shownTypes.put(calcId, new ArrayList<List<String>>());
--          }
--          shownTypes.get(calcId).add(groupLabel);
--        }
--        else
--        {
--          if (!hiddenTypes.containsKey(calcId))
--          {
--            hiddenTypes.put(calcId, new ArrayList<List<String>>());
--          }
--          hiddenTypes.get(calcId).add(groupLabel);
--        }
--      }
++      forSequences = selectionGroup.getSequences();
      }
--  }
--
--  /**
--   * Returns a BitSet (possibly empty) of those graphGroups for line graph
--   * annotations, which have at least one member annotation row marked visible.
--   * The logic is that only one row in the group is marked visible, but when it
--   * is drawn, so are all the other rows in the same group.
--   * <p/>
--   * This lookup set allows us to check whether rows marked not visible are in
--   * fact shown.
--   * 
--   * @see AnnotationRenderer#drawComponent
--   * @param annotations
--   * @return
--   */
--  public static BitSet getVisibleLineGraphGroups(
--          AlignmentAnnotation[] annotations)
--  {
--    // todo move to a utility class
--    BitSet result = new BitSet();
--    for (AlignmentAnnotation ann : annotations)
++    else
      {
--      if (ann.graph == AlignmentAnnotation.LINE_GRAPH && ann.visible)
--      {
--        int gg = ann.graphGroup;
--        if (gg > -1)
--        {
--          result.set(gg);
--        }
--      }
++      forSequences = seq == null ? Collections.<SequenceI> emptyList()
++              : Arrays.asList(seq);
      }
--    return result;
++    return forSequences;
    }
  
    /**
     * 
     * @param showOrHideMenu
     *          the menu to add to
++   * @param forSequences
++   *          the sequences whose annotations may be shown or hidden
     * @param calcId
     * @param types
     *          the label to add
     *          type, else hide
     */
    protected void addAnnotationTypeToShowHide(JMenu showOrHideMenu,
--          String calcId, final List<String> types, final boolean allTypes,
++          final List<SequenceI> forSequences, String calcId,
++          final List<String> types, final boolean allTypes,
            final boolean actionIsShow)
    {
      String label = types.toString(); // [a, b, c]
        @Override
        public void actionPerformed(ActionEvent e)
        {
--        showHideAnnotation_actionPerformed(types, allTypes, actionIsShow);
++        showHideAnnotation_actionPerformed(types, forSequences, allTypes,
++                actionIsShow);
        }
      });
      showOrHideMenu.add(item);
  
    /**
     * Action on selecting a list of annotation type (or the 'all types' values)
--   * to show or hide for the selection.
++   * to show or hide for the specified sequences.
     * 
     * @param types
++   * @param forSequences
     * @param anyType
     * @param doShow
     */
    protected void showHideAnnotation_actionPerformed(
--          Collection<String> types, boolean anyType, boolean doShow)
++          Collection<String> types, List<SequenceI> forSequences,
++          boolean anyType, boolean doShow)
    {
      for (AlignmentAnnotation aa : ap.getAlignment()
              .getAlignmentAnnotation())
      {
        if (anyType || types.contains(aa.label))
        {
          if ((aa.sequenceRef != null)
--                && ap.av.getSelectionGroup().getSequences()
--                        .contains(aa.sequenceRef))
++                && forSequences.contains(aa.sequenceRef))
          {
            aa.visible = doShow;
          }
              .getString("label.show_annotations"));
      hideAnnotationsMenu.setText(MessageManager
              .getString("label.hide_annotations"));
--    configureReferenceAnnotationsMenu();
      sequenceFeature.setText(MessageManager
              .getString("label.create_sequence_feature"));
      sequenceFeature.addActionListener(new ActionListener()
      add(groupMenu);
      add(sequenceMenu);
      this.add(structureMenu);
++    // annotations configuration panel suppressed for now
      // groupMenu.add(chooseAnnotations);
--    groupMenu.add(showAnnotationsMenu);
--    groupMenu.add(hideAnnotationsMenu);
--    groupMenu.add(addDatasequenceAnnotations);
++
++    /*
++     * Add show/hide annotations to either Selection menu (if a selection group
++     * in force), else to the Sequence menu.
++     */
++    SequenceGroup sg = this.ap.av.getSelectionGroup();
++    if (sg != null && sg.getSize() > 0)
++    {
++      groupMenu.add(showAnnotationsMenu);
++      groupMenu.add(hideAnnotationsMenu);
++      groupMenu.add(addReferenceAnnotations);
++    }
++    else
++    {
++      sequenceMenu.add(showAnnotationsMenu);
++      sequenceMenu.add(hideAnnotationsMenu);
++      sequenceMenu.add(addReferenceAnnotations);
++    }
      groupMenu.add(editMenu);
      groupMenu.add(outputMenu);
      groupMenu.add(sequenceFeature);
  
    /**
     * Check for any annotations on the underlying dataset sequences (for the
--   * current selection group) which are not on the alignment. If any are found,
--   * enable the option to add them to the alignment. The criteria for 'on the
--   * alignment' is finding an annotation that matches on
--   * sequenceRef.datasetSequence, calcId and label.
++   * current selection group) which are not on the alignment annotations for the
++   * sequence. If any are found, enable the option to add them to the alignment.
++   * The criteria for 'on the alignment' is finding an alignment annotation on
++   * the sequence, that matches on calcId and label. A tooltip is also
++   * constructed that displays the source (calcId) and type (label) of the
++   * annotations that can be added.
++   * 
++   * @param menuItem
++   * @param forSequences
     */
--  protected void configureReferenceAnnotationsMenu()
++  protected void configureReferenceAnnotationsMenu(
++          JMenuItem menuItem, List<SequenceI> forSequences)
    {
--    addDatasequenceAnnotations.setText(MessageManager
++    menuItem.setText(MessageManager
              .getString("label.add_reference_annotations"));
--    addDatasequenceAnnotations.setEnabled(false);
++    menuItem.setEnabled(false);
++    if (forSequences == null)
++    {
++      return;
++    }
  
      /*
--     * Temporary store so we can write distinct calcId / type pairs on the
--     * tooltip.
++     * Temporary store to hold distinct calcId / type pairs for the tooltip.
++     * Using TreeMap means calcIds are shown in alphabetical order.
       */
--    Map<String, String> tipEntries = new HashMap<String, String>();
++    Map<String, String> tipEntries = new TreeMap<String, String>();
      StringBuilder tooltip = new StringBuilder(64);
      tooltip.append(MessageManager.getString("label.add_annotations_for"));
  
--    // this menu option only applies for a Selection
--    if (this.ap.av.getSelectionGroup() == null)
--    {
--      return;
--    }
--
      /*
       * For each sequence selected in the alignment, make a list of any
       * annotations on the underlying dataset sequence which are not already on
       * 
       * Build a map of { alignmentSequence, <List of annotations to add> }
       */
--    final Map<SequenceI, List<AlignmentAnnotation>> candidates = new HashMap<SequenceI, List<AlignmentAnnotation>>();
--    for (SequenceI seq : this.ap.av.getSelectionGroup().getSequences())
++    final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<SequenceI, List<AlignmentAnnotation>>();
++    for (SequenceI seq : forSequences)
      {
        SequenceI dataset = seq.getDatasetSequence();
        if (dataset == null)
         * Found annotations that could be added. Enable the menu item, and
         * configure its tooltip and action.
         */
--      addDatasequenceAnnotations.setEnabled(true);
++      menuItem.setEnabled(true);
        for (String calcId : tipEntries.keySet())
        {
          tooltip.append("<br/>" + calcId + "/" + tipEntries.get(calcId));
        }
        String tooltipText = JvSwingUtils.wrapTooltip(true,
                tooltip.toString());
--      addDatasequenceAnnotations.setToolTipText(tooltipText);
++      menuItem.setToolTipText(tooltipText);
  
--      addDatasequenceAnnotations.addActionListener(new ActionListener()
++      menuItem.addActionListener(new ActionListener()
        {
          @Override
          public void actionPerformed(ActionEvent e)
    protected void addReferenceAnnotations_actionPerformed(
            Map<SequenceI, List<AlignmentAnnotation>> candidates)
    {
++    /*
++     * Add annotations at the top of the annotation, in the same order as their
++     * related sequences.
++     */
++    int insertPosition = 0;
      for (SequenceI seq : candidates.keySet())
      {
        for (AlignmentAnnotation ann : candidates.get(seq))
          }
          copyAnn.restrict(startRes, endRes);
  
--        // add to the sequence (sets correct copyAnn.datasetSequence)
++        // add to the sequence (sets copyAnn.datasetSequence)
          seq.addAlignmentAnnotation(copyAnn);
          // adjust for gaps
          copyAnn.adjustForAlignment();
          // add to the alignment and set visible
--        this.ap.getAlignment().addAnnotation(copyAnn);
++        this.ap.getAlignment().addAnnotation(copyAnn, insertPosition++);
          copyAnn.visible = true;
        }
      }
      if (sg != null)
      {
        if (sequence == null)
++      {
          sequence = sg.getSequenceAt(0);
++      }
  
        EditNameDialog dialog = new EditNameDialog(
                sequence.getSequenceAsString(sg.getStartRes(),
@@@ -1,16 -1,16 +1,17 @@@
--package jalview.gui;
++package jalview.analysis;
  
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertTrue;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
--import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.io.AppletFormatAdapter;
  
  import java.io.IOException;
++import java.util.ArrayList;
  import java.util.BitSet;
++import java.util.Collection;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import org.junit.Before;
  import org.junit.Test;
  
--/**
-- * Unit tests for PopupMenu
-- * 
-- * @author gmcarstairs
-- *
-- */
--public class PopupMenuTest
++public class AlignmentAnnotationUtilsTest
  {
    // 4 sequences x 13 positions
--  final static String TEST_DATA = ">FER_CAPAA Ferredoxin\n"
--          + "TIETHKEAELVG-\n"
--          + ">FER_CAPAN Ferredoxin, chloroplast precursor\n"
--          + "TIETHKEAELVG-\n"
--          + ">FER1_SOLLC Ferredoxin-1, chloroplast precursor\n"
--          + "TIETHKEEELTA-\n" + ">Q93XJ9_SOLTU Ferredoxin I precursor\n"
--          + "TIETHKEEELTA-\n";
++  final static String EOL = "\n";
++
++  // @formatter:off
++  final static String TEST_DATA = 
++          ">FER_CAPAA Ferredoxin" + EOL +
++          "TIETHKEAELVG-" + EOL +
++          ">FER_CAPAN Ferredoxin, chloroplast precursor" + EOL +
++          "TIETHKEAELVG-" + EOL +
++          ">FER1_SOLLC Ferredoxin-1, chloroplast precursor" + EOL +
++          "TIETHKEEELTA-" + EOL + 
++          ">Q93XJ9_SOLTU Ferredoxin I precursor" + EOL +
++          "TIETHKEEELTA-" + EOL;
++  // @formatter:on
  
    private static final int SEQ_ANN_COUNT = 10;
  
--  private static final int AUTO_ANNS = 3;
++  private AlignmentI alignment;
  
--  AlignmentI alignment;
++  /**
++   * Test method that converts a (possibly null) array to a list.
++   */
++  @Test
++  public void testAsList()
++  {
++    // null array
++    Collection<AlignmentAnnotation> c1 = AlignmentAnnotationUtils
++            .asList(null);
++    assertTrue(c1.isEmpty());
++    
++    // empty array
++    AlignmentAnnotation[] anns = new AlignmentAnnotation[0];
++    c1 = AlignmentAnnotationUtils.asList(anns);
++    assertTrue(c1.isEmpty());
++
++    // non-empty array
++    anns = new AlignmentAnnotation[2];
++    anns[0] = new AlignmentAnnotation("label0", "desc0", 0.0f);
++    anns[1] = new AlignmentAnnotation("label1", "desc1", 1.0f);
++    c1 = AlignmentAnnotationUtils.asList(anns);
++    assertEquals(2, c1.size());
++    assertTrue(c1.contains(anns[0]));
++    assertTrue(c1.contains(anns[1]));
++  }
  
--  AlignmentPanel parentPanel;
++  /**
++   * This output is not part of the test but may help make sense of it...
++   * 
++   * @param shownTypes
++   * @param hiddenTypes
++   */
++  protected void consoleDebug(Map<String, List<List<String>>> shownTypes,
++          Map<String, List<List<String>>> hiddenTypes)
++  {
++    for (String calcId : shownTypes.keySet())
++    {
++      System.out.println("Visible annotation types for calcId=" + calcId);
++      for (List<String> type : shownTypes.get(calcId))
++      {
++        System.out.println("   " + type);
++      }
++    }
++    for (String calcId : hiddenTypes.keySet())
++    {
++      System.out.println("Hidden annotation types for calcId=" + calcId);
++      for (List<String> type : hiddenTypes.get(calcId))
++      {
++        System.out.println("   " + type);
++      }
++    }
++  }
++
++  /**
++   * Add a sequence group to the alignment with the specified sequences (base 0)
++   * in it
++   * 
++   * @param i
++   * @param more
++   */
++  private List<SequenceI> selectSequences(int... selected)
++  {
++    List<SequenceI> result = new ArrayList<SequenceI>();
++    SequenceI[] seqs = alignment.getSequencesArray();
++    for (int i : selected)
++    {
++      result.add(seqs[i]);
++    }
++    return result;
++  }
  
    @Before
    public void setUp() throws IOException
    {
--    AlignmentI al = new jalview.io.FormatAdapter().readFile(TEST_DATA,
++    alignment = new jalview.io.FormatAdapter().readFile(TEST_DATA,
              AppletFormatAdapter.PASTE, "FASTA");
--    AlignFrame af = new AlignFrame(al, 700, 500);
--    parentPanel = new AlignmentPanel(af, af.getViewport());
--    alignment = parentPanel.getAlignment();
--
++  
      AlignmentAnnotation[] anns = new AlignmentAnnotation[SEQ_ANN_COUNT];
      for (int i = 0; i < anns.length; i++)
      {
    }
  
    /**
     * Test a mixture of show/hidden annotations in/outside selection group.
     */
    @Test
--  public void testGetAnnotationTypesForShowHide_forSelectionGroup()
++  public void testGetShownHiddenTypes_forSelectionGroup()
    {
      Map<String, List<List<String>>> shownTypes = new HashMap<String, List<List<String>>>();
      Map<String, List<List<String>>> hiddenTypes = new HashMap<String, List<List<String>>>();
      AlignmentAnnotation[] anns = alignment.getAlignmentAnnotation();
--    BitSet visibleGraphGroups = new BitSet();
--    selectSequences(0, 3);
--    SequenceI[] seqs = parentPanel.getAlignment().getSequencesArray();
--
++    SequenceI[] seqs = alignment.getSequencesArray();
++  
      /*
--     * Configure annotation properties for test (offsetting for auto-calculated
--     * rows).
++     * Configure annotation properties for test
       */
      // not in selection group (should be ignored):
      // hidden annotation Label4 not in selection group
--    anns[AUTO_ANNS + 4].sequenceRef = seqs[2];
--    anns[AUTO_ANNS + 4].visible = false;
--    anns[AUTO_ANNS + 7].sequenceRef = seqs[1];
--    anns[AUTO_ANNS + 7].visible = true;
--
++    anns[4].sequenceRef = seqs[2];
++    anns[4].visible = false;
++    anns[7].sequenceRef = seqs[1];
++    anns[7].visible = true;
++  
      /*
       * in selection group, hidden:
       */
--    anns[AUTO_ANNS + 2].sequenceRef = seqs[3]; // CalcId2/Label2
--    anns[AUTO_ANNS + 2].visible = false;
--    anns[AUTO_ANNS + 3].sequenceRef = seqs[3]; // CalcId3/Label2
--    anns[AUTO_ANNS + 3].visible = false;
--    anns[AUTO_ANNS + 3].label = "Label2";
--    anns[AUTO_ANNS + 4].sequenceRef = seqs[3]; // CalcId2/Label3
--    anns[AUTO_ANNS + 4].visible = false;
--    anns[AUTO_ANNS + 4].label = "Label3";
--    anns[AUTO_ANNS + 4].setCalcId("CalcId2");
--    anns[AUTO_ANNS + 8].sequenceRef = seqs[0]; // CalcId9/Label9
--    anns[AUTO_ANNS + 8].visible = false;
--    anns[AUTO_ANNS + 8].label = "Label9";
--    anns[AUTO_ANNS + 8].setCalcId("CalcId9");
++    anns[2].sequenceRef = seqs[3]; // CalcId2/Label2
++    anns[2].visible = false;
++    anns[3].sequenceRef = seqs[3]; // CalcId3/Label2
++    anns[3].visible = false;
++    anns[3].label = "Label2";
++    anns[4].sequenceRef = seqs[3]; // CalcId2/Label3
++    anns[4].visible = false;
++    anns[4].label = "Label3";
++    anns[4].setCalcId("CalcId2");
++    anns[8].sequenceRef = seqs[0]; // CalcId9/Label9
++    anns[8].visible = false;
++    anns[8].label = "Label9";
++    anns[8].setCalcId("CalcId9");
      /*
       * in selection group, visible
       */
--    anns[AUTO_ANNS + 6].sequenceRef = seqs[0]; // CalcId6/Label6
--    anns[AUTO_ANNS + 6].visible = true;
--    anns[AUTO_ANNS + 9].sequenceRef = seqs[3]; // CalcId9/Label9
--    anns[AUTO_ANNS + 9].visible = true;
--
--    PopupMenu.getAnnotationTypesForShowHide(shownTypes, hiddenTypes,
--            visibleGraphGroups, anns, parentPanel.av.getSelectionGroup());
--
++    anns[6].sequenceRef = seqs[0]; // CalcId6/Label6
++    anns[6].visible = true;
++    anns[9].sequenceRef = seqs[3]; // CalcId9/Label9
++    anns[9].visible = true;
++  
++    List<SequenceI> selected = selectSequences(0, 3);
++    AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
++            AlignmentAnnotationUtils.asList(anns),
++            selected);
++  
      // check results; note CalcId9/Label9 is both hidden and shown (for
      // different sequences) so should be in both
      // shown: CalcId6/Label6 and CalcId9/Label9
      assertEquals(1, shownTypes.get("CalcId9").size());
      assertEquals(1, shownTypes.get("CalcId9").get(0).size());
      assertEquals("Label9", shownTypes.get("CalcId9").get(0).get(0));
--
++  
      // hidden: CalcId2/Label2, CalcId2/Label3, CalcId3/Label2, CalcId9/Label9
      assertEquals(3, hiddenTypes.size());
      assertEquals(2, hiddenTypes.get("CalcId2").size());
      assertEquals(1, hiddenTypes.get("CalcId9").size());
      assertEquals(1, hiddenTypes.get("CalcId9").get(0).size());
      assertEquals("Label9", hiddenTypes.get("CalcId9").get(0).get(0));
--
++  
      consoleDebug(shownTypes, hiddenTypes);
    }
  
    /**
--   * This output is not part of the test but may help make sense of it...
--   * 
--   * @param shownTypes
--   * @param hiddenTypes
--   */
--  protected void consoleDebug(Map<String, List<List<String>>> shownTypes,
--          Map<String, List<List<String>>> hiddenTypes)
--  {
--    for (String calcId : shownTypes.keySet())
--    {
--      System.out.println("Visible annotation types for calcId=" + calcId);
--      for (List<String> type : shownTypes.get(calcId))
--      {
--        System.out.println("   " + type);
--      }
--    }
--    for (String calcId : hiddenTypes.keySet())
--    {
--      System.out.println("Hidden annotation types for calcId=" + calcId);
--      for (List<String> type : hiddenTypes.get(calcId))
--      {
--        System.out.println("   " + type);
--      }
--    }
--  }
--
--  /**
     * Test case where there are 'grouped' annotations, visible and hidden, within
     * and without the selection group.
     */
    @Test
--  public void testGetAnnotationTypesForShowHide_withGraphGroups()
++  public void testGetShownHiddenTypes_withGraphGroups()
    {
      final int GROUP_4 = 4;
      final int GROUP_5 = 5;
      final int GROUP_6 = 6;
--
++  
      Map<String, List<List<String>>> shownTypes = new HashMap<String, List<List<String>>>();
      Map<String, List<List<String>>> hiddenTypes = new HashMap<String, List<List<String>>>();
      AlignmentAnnotation[] anns = alignment.getAlignmentAnnotation();
--    BitSet visibleGraphGroups = new BitSet();
--    visibleGraphGroups.set(GROUP_4);
--    visibleGraphGroups.set(GROUP_6);
--    selectSequences(0, 3);
--    SequenceI[] seqs = parentPanel.getAlignment().getSequencesArray();
--
++    SequenceI[] seqs = alignment.getSequencesArray();
++  
      /*
--     * Configure annotation properties for test (offsetting for auto-calculated
--     * rows).
++     * Configure annotation properties for test
       */
      // annotations for selection group and graph group
      // hidden annotations Label2, Label3, in (hidden) group 5
--    anns[AUTO_ANNS + 2].sequenceRef = seqs[3];
--    anns[AUTO_ANNS + 2].visible = false;
--    anns[AUTO_ANNS + 2].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 2].graphGroup = GROUP_5; // not a visible group
--    anns[AUTO_ANNS + 3].sequenceRef = seqs[0];
--    anns[AUTO_ANNS + 3].visible = false;
--    anns[AUTO_ANNS + 3].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 3].graphGroup = GROUP_5;
++    anns[2].sequenceRef = seqs[3];
++    anns[2].visible = false;
++    anns[2].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[2].graphGroup = GROUP_5; // not a visible group
++    anns[3].sequenceRef = seqs[0];
++    anns[3].visible = false;
++    anns[3].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[3].graphGroup = GROUP_5;
      // need to ensure annotations have the same calcId as well
--    anns[AUTO_ANNS + 3].setCalcId("CalcId2");
--
++    anns[3].setCalcId("CalcId2");
++  
      // annotations Label1 (hidden), Label5 (visible) in group 6 (visible)
--    anns[AUTO_ANNS + 1].sequenceRef = seqs[3];
++    anns[1].sequenceRef = seqs[3];
      // being in a visible group should take precedence over this visibility
--    anns[AUTO_ANNS + 1].visible = false;
--    anns[AUTO_ANNS + 1].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 1].graphGroup = GROUP_6;
--    anns[AUTO_ANNS + 5].sequenceRef = seqs[0];
--    anns[AUTO_ANNS + 5].visible = true; // visibleGraphGroups overrides this
--    anns[AUTO_ANNS + 5].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 5].graphGroup = GROUP_6;
--    anns[AUTO_ANNS + 5].setCalcId("CalcId1");
--
++    anns[1].visible = false;
++    anns[1].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[1].graphGroup = GROUP_6;
++    anns[5].sequenceRef = seqs[0];
++    anns[5].visible = true;
++    anns[5].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[5].graphGroup = GROUP_6;
++    anns[5].setCalcId("CalcId1");
++  
      // annotations outwith selection group - should be ignored
      // hidden grouped annotations
--    anns[AUTO_ANNS + 6].sequenceRef = seqs[2];
--    anns[AUTO_ANNS + 6].visible = false;
--    anns[AUTO_ANNS + 6].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 6].graphGroup = GROUP_4;
--    anns[AUTO_ANNS + 8].sequenceRef = seqs[1];
--    anns[AUTO_ANNS + 8].visible = false;
--    anns[AUTO_ANNS + 8].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 8].graphGroup = GROUP_4;
++    anns[6].sequenceRef = seqs[2];
++    anns[6].visible = false;
++    anns[6].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[6].graphGroup = GROUP_4;
++    anns[8].sequenceRef = seqs[1];
++    anns[8].visible = false;
++    anns[8].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[8].graphGroup = GROUP_4;
      // visible grouped annotations Label7, Label9
--    anns[AUTO_ANNS + 7].sequenceRef = seqs[2];
--    anns[AUTO_ANNS + 7].visible = true;
--    anns[AUTO_ANNS + 7].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 7].graphGroup = GROUP_4;
--    anns[AUTO_ANNS + 9].sequenceRef = seqs[1];
--    anns[AUTO_ANNS + 9].visible = true;
--    anns[AUTO_ANNS + 9].graph = AlignmentAnnotation.LINE_GRAPH;
--    anns[AUTO_ANNS + 9].graphGroup = GROUP_4;
--
--    PopupMenu.getAnnotationTypesForShowHide(shownTypes, hiddenTypes,
--            visibleGraphGroups, anns, parentPanel.av.getSelectionGroup());
--
++    anns[7].sequenceRef = seqs[2];
++    anns[7].visible = true;
++    anns[7].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[7].graphGroup = GROUP_4;
++    anns[9].sequenceRef = seqs[1];
++    anns[9].visible = true;
++    anns[9].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[9].graphGroup = GROUP_4;
++  
++    List<SequenceI> selected = selectSequences(0, 3);
++    AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
++            AlignmentAnnotationUtils.asList(anns),
++            selected);
++  
      consoleDebug(shownTypes, hiddenTypes);
--
++  
      // CalcId1 / Label1, Label5 (only) should be 'shown', as a compound type
      assertEquals(1, shownTypes.get("CalcId1").size());
      assertEquals(2, shownTypes.get("CalcId1").get(0).size());
      assertEquals("Label1", shownTypes.get("CalcId1").get(0).get(0));
      assertEquals("Label5", shownTypes.get("CalcId1").get(0).get(1));
--
++  
      // CalcId2 / Label2, Label3 (only) should be 'hidden'
      assertEquals(1, hiddenTypes.get("CalcId2").size());
      assertEquals(2, hiddenTypes.get("CalcId2").get(0).size());
    }
  
    /**
--   * Add a sequence group to the alignment with the specified sequences (base 0)
--   * in it
--   * 
--   * @param i
--   * @param more
++   * Test method that determines visible graph groups.
     */
--  private void selectSequences(int... selected)
++  @Test
++  public void testGetVisibleGraphGroups()
    {
--    SequenceI[] seqs = parentPanel.getAlignment().getSequencesArray();
--    SequenceGroup sg = new SequenceGroup();
--    for (int i : selected)
--    {
--      sg.addSequence(seqs[i], false);
--    }
--    parentPanel.av.setSelectionGroup(sg);
++    AlignmentAnnotation[] anns = alignment.getAlignmentAnnotation();
++    /*
++     * a bar graph group is not included
++     */
++    anns[0].graph = AlignmentAnnotation.BAR_GRAPH;
++    anns[0].graphGroup = 1;
++    anns[0].visible = true;
++  
++    /*
++     * a line graph group is included as long as one of its members is visible
++     */
++    anns[1].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[1].graphGroup = 5;
++    anns[1].visible = false;
++    anns[2].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[2].graphGroup = 5;
++    anns[2].visible = true;
++  
++    /*
++     * a line graph group with no visible rows is not included
++     */
++    anns[3].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[3].graphGroup = 3;
++    anns[3].visible = false;
++  
++    // a visible line graph with no graph group is not included
++    anns[4].graph = AlignmentAnnotation.LINE_GRAPH;
++    anns[4].graphGroup = -1;
++    anns[4].visible = true;
++  
++    BitSet result = AlignmentAnnotationUtils
++            .getVisibleLineGraphGroups(AlignmentAnnotationUtils
++                    .asList(anns));
++    assertTrue(result.get(5));
++    assertFalse(result.get(0));
++    assertFalse(result.get(1));
++    assertFalse(result.get(2));
++    assertFalse(result.get(3));
++  }
++
++  /**
++   * Test for case where no sequence is selected. Shouldn't normally arise but
++   * check it handles it gracefully.
++   */
++  @Test
++  public void testGetShownHiddenTypes_noSequenceSelected()
++  {
++    Map<String, List<List<String>>> shownTypes = new HashMap<String, List<List<String>>>();
++    Map<String, List<List<String>>> hiddenTypes = new HashMap<String, List<List<String>>>();
++    AlignmentAnnotation[] anns = alignment.getAlignmentAnnotation();
++    // selected sequences null
++    AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
++            AlignmentAnnotationUtils.asList(anns), null);
++    assertTrue(shownTypes.isEmpty());
++    assertTrue(hiddenTypes.isEmpty());
++
++    List<SequenceI> sequences = new ArrayList<SequenceI>();
++    // selected sequences empty list
++    AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
++            AlignmentAnnotationUtils.asList(anns), sequences);
++    assertTrue(shownTypes.isEmpty());
++    assertTrue(hiddenTypes.isEmpty());
    }
  }