Merge branch 'develop' into features/JAL-1152annotationSorting
authorJim Procter <jprocter@dundee.ac.uk>
Mon, 3 Nov 2014 16:02:18 +0000 (16:02 +0000)
committerJim Procter <jprocter@dundee.ac.uk>
Mon, 3 Nov 2014 16:02:18 +0000 (16:02 +0000)
12 files changed:
resources/lang/Messages.properties
src/jalview/analysis/AlignmentAnnotationUtils.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/AnnotationSorter.java [new file with mode: 0644]
src/jalview/bin/Cache.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/jbgui/GAlignFrame.java
test/jalview/analysis/AlignmentAnnotationUtilsTest.java
test/jalview/analysis/AnnotationSorterTest.java [new file with mode: 0644]

index f23275a..fe04105 100644 (file)
@@ -102,6 +102,7 @@ action.find_all = Find all
 action.find_next = Find next
 action.file = File
 action.view = View
+action.annotations = Annotations
 action.change_params = Change Parameters
 action.apply = Apply
 action.apply_threshold_all_groups = Apply threshold to all groups
@@ -199,8 +200,10 @@ label.average_distance_bloslum62 = Average Distance Using BLOSUM62
 label.neighbour_blosum62 = Neighbour Joining Using BLOSUM62
 label.show_annotations = Show annotations
 label.hide_annotations = Hide annotations
-label.show_all_annotations = Show all annotations
-label.hide_all_annotations = Hide all annotations
+label.show_all_seq_annotations = Show sequence related
+label.hide_all_seq_annotations = Hide sequence related
+label.show_all_al_annotations = Show alignment related
+label.hide_all_al_annotations = Hide alignment related
 label.hide_all = Hide all
 label.add_reference_annotations = Add reference annotations
 label.find_tip = Search alignment, selection or sequence ids for a subsequence (ignoring gaps).<br>Accepts regular expressions - search Help for 'regex' for details.
@@ -238,6 +241,8 @@ label.show_consensus_logo = Show Consensus Logo
 label.norm_consensus_logo = Normalise Consensus Logo
 label.apply_all_groups = Apply to all groups
 label.autocalculated_annotation = Autocalculated Annotation
+label.show_first = Show first
+label.show_last = Show last
 label.min_colour = Minimum Colour
 label.max_colour = Maximum Colour
 label.use_original_colours = Use Original Colours
@@ -479,6 +484,8 @@ label.sort_by = Sort by
 label.sort_by_score = Sort by Score
 label.sort_by_density = Sort by Density
 label.sequence_sort_by_density = Sequence sort by Density
+label.sort_annotations_by_sequence = Sort by sequence
+label.sort_annotations_by_label = Sort by label
 label.reveal = Reveal
 label.hide_columns = Hide Columns
 label.load_jalview_annotations = Load Jalview Annotations or Features File
index b5f77fd..9bdbf73 100644 (file)
@@ -129,21 +129,28 @@ public class AlignmentAnnotationUtils
       }
     }
     /*
-     * finally add the 'composite group labels' to the appropriate lists,
-     * depending on whether the group is identified as visible or hidden
+     * Finally add the 'composite group labels' to the appropriate lists,
+     * depending on whether the group is identified as visible or hidden. Don't
+     * add the same label more than once (there may be many graph groups that
+     * generate it).
      */
     for (String calcId : groupLabels.keySet())
     {
       for (int group : groupLabels.get(calcId).keySet())
       {
         final List<String> groupLabel = groupLabels.get(calcId).get(group);
+        // don't want to duplicate 'same types in different order'
+        Collections.sort(groupLabel);
         if (visibleGraphGroups.get(group))
         {
           if (!shownTypes.containsKey(calcId))
           {
             shownTypes.put(calcId, new ArrayList<List<String>>());
           }
-          shownTypes.get(calcId).add(groupLabel);
+          if (!shownTypes.get(calcId).contains(groupLabel))
+          {
+            shownTypes.get(calcId).add(groupLabel);
+          }
         }
         else
         {
@@ -151,7 +158,10 @@ public class AlignmentAnnotationUtils
           {
             hiddenTypes.put(calcId, new ArrayList<List<String>>());
           }
-          hiddenTypes.get(calcId).add(groupLabel);
+          if (!hiddenTypes.get(calcId).contains(groupLabel))
+          {
+            hiddenTypes.get(calcId).add(groupLabel);
+          }
         }
       }
     }
@@ -196,8 +206,7 @@ public class AlignmentAnnotationUtils
    * @param anns
    * @return
    */
-  public static List<AlignmentAnnotation> asList(
-          AlignmentAnnotation[] anns)
+  public static List<AlignmentAnnotation> asList(AlignmentAnnotation[] anns)
   {
     // TODO use AlignmentAnnotationI instead when it exists
     return (anns == null ? Collections.<AlignmentAnnotation> emptyList()
index 2feeb91..929a855 100644 (file)
  */
 package jalview.analysis;
 
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import jalview.datamodel.SequenceI;
-import jalview.datamodel.AlignmentI;
-
 /**
  * grab bag of useful alignment manipulation operations Expect these to be
  * refactored elsewhere at some point.
@@ -124,4 +124,28 @@ public class AlignmentUtils
     newAl.setDataset(core.getDataset());
     return newAl;
   }
+
+  /**
+   * Returns the index (zero-based position) of a sequence in an alignment, or
+   * -1 if not found.
+   * 
+   * @param al
+   * @param seq
+   * @return
+   */
+  public static int getSequenceIndex(AlignmentI al, SequenceI seq)
+  {
+    int result = -1;
+    int pos = 0;
+    for (SequenceI alSeq : al.getSequences())
+    {
+      if (alSeq == seq)
+      {
+        result = pos;
+        break;
+      }
+      pos++;
+    }
+    return result;
+  }
 }
diff --git a/src/jalview/analysis/AnnotationSorter.java b/src/jalview/analysis/AnnotationSorter.java
new file mode 100644 (file)
index 0000000..b9e9ed5
--- /dev/null
@@ -0,0 +1,284 @@
+package jalview.analysis;
+
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * A helper class to sort all annotations associated with an alignment in
+ * various ways.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class AnnotationSorter
+{
+
+  public enum SequenceAnnotationOrder
+  {
+    SEQUENCE_AND_LABEL, LABEL_AND_SEQUENCE, NONE
+  }
+
+  private final AlignmentI alignment;
+
+  private boolean showAutocalcAbove;
+
+  /**
+   * Constructor given an alignment and the location (top or bottom) of
+   * Consensus and similar.
+   * 
+   * @param alignmentI
+   * @param showAutocalculatedAbove
+   */
+  public AnnotationSorter(AlignmentI alignmentI,
+          boolean showAutocalculatedAbove)
+  {
+    this.alignment = alignmentI;
+    this.showAutocalcAbove = showAutocalculatedAbove;
+  }
+
+  /**
+   * Default comparator sorts as follows by annotation type within sequence
+   * order:
+   * <ul>
+   * <li>annotations with a reference to a sequence in the alignment are sorted
+   * on sequence ordering</li>
+   * <li>other annotations go 'at the end', with their mutual order unchanged</li>
+   * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
+   * </ul>
+   */
+  private final Comparator<? super AlignmentAnnotation> bySequenceAndLabel = new Comparator<AlignmentAnnotation>()
+  {
+    @Override
+    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
+    {
+      if (o1 == null && o2 == null)
+      {
+        return 0;
+      }
+      if (o1 == null)
+      {
+        return -1;
+      }
+      if (o2 == null)
+      {
+        return 1;
+      }
+
+      /*
+       * Ignore label (keep existing ordering) for
+       * Conservation/Quality/Consensus etc
+       */
+      if (o1.sequenceRef == null && o2.sequenceRef == null)
+      {
+        return 0;
+      }
+      int sequenceOrder = compareSequences(o1, o2);
+      return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
+    }
+  };
+
+  /**
+   * This comparator sorts as follows by sequence order within annotation type
+   * <ul>
+   * <li>annotations with a reference to a sequence in the alignment are sorted
+   * on label (non-case-sensitive)</li>
+   * <li>other annotations go 'at the end', with their mutual order unchanged</li>
+   * <li>within the same label, sort by order of the related sequences</li>
+   * </ul>
+   */
+  private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
+  {
+    @Override
+    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
+    {
+      if (o1 == null && o2 == null)
+      {
+        return 0;
+      }
+      if (o1 == null)
+      {
+        return -1;
+      }
+      if (o2 == null)
+      {
+        return 1;
+      }
+
+      /*
+       * Ignore label (keep existing ordering) for
+       * Conservation/Quality/Consensus etc
+       */
+      if (o1.sequenceRef == null && o2.sequenceRef == null)
+      {
+        return 0;
+      }
+
+      /*
+       * Sort non-sequence-related before or after sequence-related.
+       */
+      if (o1.sequenceRef == null)
+      {
+        return showAutocalcAbove ? -1 : 1;
+      }
+      if (o2.sequenceRef == null)
+      {
+        return showAutocalcAbove ? 1 : -1;
+      }
+      int labelOrder = compareLabels(o1, o2);
+      return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
+    }
+  };
+
+  /**
+   * noSort leaves sort order unchanged, within sequence- and
+   * non-sequence-related annotations, but may switch the ordering of these
+   * groups. Note this is guaranteed (at least in Java 7) as Arrays.sort() is
+   * guaranteed to be 'stable' (not change ordering of equal items).
+   */
+  private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
+  {
+    @Override
+    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
+    {
+      if (o1 != null && o2 != null)
+      {
+        if (o1.sequenceRef == null && o2.sequenceRef != null)
+        {
+          return showAutocalcAbove ? -1 : 1;
+        }
+        if (o1.sequenceRef != null && o2.sequenceRef == null)
+        {
+          return showAutocalcAbove ? 1 : -1;
+        }
+      }
+      return 0;
+    }
+  };
+
+  /**
+   * Sort by the specified ordering of sequence-specific annotations.
+   * 
+   * @param alignmentAnnotations
+   * @param order
+   */
+  public void sort(AlignmentAnnotation[] alignmentAnnotations,
+          SequenceAnnotationOrder order)
+  {
+    Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
+
+    if (alignmentAnnotations != null)
+    {
+      synchronized (alignmentAnnotations)
+      {
+        Arrays.sort(alignmentAnnotations, comparator);
+      }
+    }
+  }
+
+  /**
+   * Get the comparator for the specified sort order.
+   * 
+   * @param order
+   * @return
+   */
+  private Comparator<? super AlignmentAnnotation> getComparator(
+          SequenceAnnotationOrder order)
+  {
+    if (order == null)
+    {
+      return noSort;
+    }
+    switch (order)
+    {
+    case NONE:
+      return this.noSort;
+    case SEQUENCE_AND_LABEL:
+      return this.bySequenceAndLabel;
+    case LABEL_AND_SEQUENCE:
+      return this.byLabelAndSequence;
+    default:
+      throw new UnsupportedOperationException(order.toString());
+    }
+  }
+
+  /**
+   * Non-case-sensitive comparison of annotation labels. Returns zero if either
+   * argument is null.
+   * 
+   * @param o1
+   * @param o2
+   * @return
+   */
+  private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
+  {
+    if (o1 == null || o2 == null)
+    {
+      return 0;
+    }
+    String label1 = o1.label;
+    String label2 = o2.label;
+    if (label1 == null && label2 == null)
+    {
+      return 0;
+    }
+    if (label1 == null)
+    {
+      return -1;
+    }
+    if (label2 == null)
+    {
+      return 1;
+    }
+    return label1.toUpperCase().compareTo(label2.toUpperCase());
+  }
+
+  /**
+   * Comparison based on position of associated sequence (if any) in the
+   * alignment. Returns zero if either argument is null.
+   * 
+   * @param o1
+   * @param o2
+   * @return
+   */
+  private int compareSequences(AlignmentAnnotation o1,
+          AlignmentAnnotation o2)
+  {
+    SequenceI seq1 = o1.sequenceRef;
+    SequenceI seq2 = o2.sequenceRef;
+    if (seq1 == null && seq2 == null)
+    {
+      return 0;
+    }
+    /*
+     * Sort non-sequence-related before or after sequence-related.
+     */
+    if (seq1 == null)
+    {
+      return showAutocalcAbove ? -1 : 1;
+    }
+    if (seq2 == null)
+    {
+      return showAutocalcAbove ? 1 : -1;
+    }
+    // get sequence index - but note -1 means 'at end' so needs special handling
+    int index1 = AlignmentUtils.getSequenceIndex(alignment, seq1);
+    int index2 = AlignmentUtils.getSequenceIndex(alignment, seq2);
+    if (index1 == index2)
+    {
+      return 0;
+    }
+    if (index1 == -1)
+    {
+      return -1;
+    }
+    if (index2 == -1)
+    {
+      return 1;
+    }
+    return Integer.compare(index1, index2);
+  }
+}
index 1214371..2a9f53d 100755 (executable)
@@ -24,12 +24,21 @@ import jalview.ws.dbsources.das.api.DasSourceRegistryI;
 import jalview.ws.dbsources.das.datamodel.DasSourceRegistry;
 
 import java.awt.Color;
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.*;
+import java.util.Date;
+import java.util.Properties;
 
-import org.apache.log4j.*;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.SimpleLayout;
 
 /**
  * Stores and retrieves Jalview Application Properties Lists and fields within
@@ -63,6 +72,10 @@ import org.apache.log4j.*;
  * <li>SHOW_QUALITY show alignment quality annotation</li>
  * <li>SHOW_ANNOTATIONS show alignment annotation rows</li>
  * <li>SHOW_CONSERVATION show alignment conservation annotation</li>
+ * <li>SORT_ANNOTATIONS currently either SEQUENCE_AND_LABEL or
+ * LABEL_AND_SEQUENCE</li>
+ * <li>SHOW_AUTOCALC_ABOVE true to show autocalculated annotations above
+ * sequence annotations</li>
  * <li>CENTRE_COLUMN_LABELS centre the labels at each column in a displayed
  * annotation row</li>
  * <li>DEFAULT_COLOUR default colour scheme to apply for a new alignment</li>
@@ -709,15 +722,21 @@ public class Cache
         if (log != null)
         {
           if (re != null)
+          {
             log.debug("Caught runtime exception in googletracker init:", re);
+          }
           if (ex != null)
+          {
             log.warn(
                     "Failed to initialise GoogleTracker for Jalview Desktop with version "
                             + vrs, ex);
+          }
           if (err != null)
+          {
             log.error(
                     "Whilst initing GoogleTracker for Jalview Desktop version "
                             + vrs, err);
+          }
         }
         else
         {
index 6d8fe6d..e6d86e9 100644 (file)
@@ -279,7 +279,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * Make a new AlignFrame from exisiting alignmentPanels
+   * Make a new AlignFrame from existing alignmentPanels
    * 
    * @param ap
    *          AlignmentPanel
@@ -746,10 +746,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     scaleRight.setVisible(av.wrapAlignment);
     annotationPanelMenuItem.setState(av.showAnnotation);
     /*
-     * Show/hide all annotations only enabled if annotation panel is shown
+     * Show/hide annotations only enabled if annotation panel is shown
      */
-    showAllAnnotations.setEnabled(annotationPanelMenuItem.getState());
-    hideAllAnnotations.setEnabled(annotationPanelMenuItem.getState());
+    showAllSeqAnnotations.setEnabled(annotationPanelMenuItem.getState());
+    hideAllSeqAnnotations.setEnabled(annotationPanelMenuItem.getState());
+    showAllAlAnnotations.setEnabled(annotationPanelMenuItem.getState());
+    hideAllAlAnnotations.setEnabled(annotationPanelMenuItem.getState());
     viewBoxesMenuItem.setSelected(av.showBoxes);
     viewTextMenuItem.setSelected(av.showText);
     showNonconservedMenuItem.setSelected(av.getShowUnconserved());
@@ -3100,8 +3102,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     final boolean setVisible = annotationPanelMenuItem.isSelected();
     viewport.setShowAnnotation(setVisible);
     alignPanel.setAnnotationVisible(setVisible);
-    this.showAllAnnotations.setEnabled(setVisible);
-    this.hideAllAnnotations.setEnabled(setVisible);
+    this.showAllSeqAnnotations.setEnabled(setVisible);
+    this.hideAllSeqAnnotations.setEnabled(setVisible);
+    this.showAllAlAnnotations.setEnabled(setVisible);
+    this.hideAllAlAnnotations.setEnabled(setVisible);
   }
 
   @Override
@@ -5767,20 +5771,42 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * Action on selection of menu option to Show or Hide all annotations.
+   * Action on selection of menu options to Show or Hide annotations.
    * 
-   * @param visibile
+   * @param visible
+   * @param forSequences
+   *          update sequence-related annotations
+   * @param forAlignment
+   *          update non-sequence-related annotations
    */
   @Override
-  protected void setAllAnnotationsVisibility(boolean visible)
+  protected void setAnnotationsVisibility(boolean visible,
+          boolean forSequences, boolean forAlignment)
   {
     for (AlignmentAnnotation aa : alignPanel.getAlignment()
             .getAlignmentAnnotation())
     {
-      aa.visible = visible;
+      boolean apply = (aa.sequenceRef == null && forAlignment)
+              || (aa.sequenceRef != null && forSequences);
+      if (apply)
+      {
+        aa.visible = visible;
+      }
     }
     this.alignPanel.paintAlignment(true);
   }
+
+  /**
+   * Store selected annotation sort order for the view and repaint.
+   */
+  @Override
+  protected void sortAnnotations_actionPerformed()
+  {
+    this.alignPanel.av.setSortAnnotationsBy(getAnnotationSortOrder());
+    this.alignPanel.av
+            .setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
+    alignPanel.updateAnnotation(applyAutoAnnotationSettings.getState());
+  }
 }
 
 class PrintThread extends Thread
index 6d6531f..d331e82 100644 (file)
@@ -38,6 +38,7 @@
  */
 package jalview.gui;
 
+import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.NJTree;
 import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
@@ -99,6 +100,8 @@ public class AlignViewport extends AlignmentViewport implements
 
   boolean showAnnotation = true;
 
+  SequenceAnnotationOrder sortAnnotationsBy = null;
+
   int charHeight;
 
   int charWidth;
@@ -358,12 +361,14 @@ public class AlignViewport extends AlignmentViewport implements
       }
     }
 
-    wrapAlignment = jalview.bin.Cache.getDefault("WRAP_ALIGNMENT", false);
-    showUnconserved = jalview.bin.Cache.getDefault("SHOW_UNCONSERVED",
-            false);
-    sortByTree = jalview.bin.Cache.getDefault("SORT_BY_TREE", false);
-    followSelection = jalview.bin.Cache.getDefault("FOLLOW_SELECTIONS",
-            true);
+    wrapAlignment = Cache.getDefault("WRAP_ALIGNMENT", false);
+    showUnconserved = Cache.getDefault("SHOW_UNCONSERVED", false);
+    sortByTree = Cache.getDefault("SORT_BY_TREE", false);
+    followSelection = Cache.getDefault("FOLLOW_SELECTIONS", true);
+    sortAnnotationsBy = SequenceAnnotationOrder.valueOf(Cache.getDefault(
+            "SORT_ANNOTATIONS", SequenceAnnotationOrder.NONE.name()));
+    showAutocalculatedAbove = Cache
+            .getDefault("SHOW_AUTOCALC_ABOVE", false);
   }
 
   /**
@@ -969,8 +974,10 @@ public class AlignViewport extends AlignmentViewport implements
   {
     // TODO: JAL-1126
     if (historyList == null || redoList == null)
+    {
       return new long[]
       { -1, -1 };
+    }
     return new long[]
     { historyList.hashCode(), this.redoList.hashCode() };
   }
@@ -1207,7 +1214,9 @@ public class AlignViewport extends AlignmentViewport implements
         Vector pdbs = alignment.getSequenceAt(i).getDatasetSequence()
                 .getPDBId();
         if (pdbs == null)
+        {
           continue;
+        }
         SequenceI sq;
         for (int p = 0; p < pdbs.size(); p++)
         {
@@ -1215,7 +1224,9 @@ public class AlignViewport extends AlignmentViewport implements
           if (p1.getId().equals(pdb.getId()))
           {
             if (!seqs.contains(sq = alignment.getSequenceAt(i)))
+            {
               seqs.add(sq);
+            }
 
             continue;
           }
@@ -1247,6 +1258,8 @@ public class AlignViewport extends AlignmentViewport implements
 
   private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<String, AutoCalcSetting>();
 
+  private boolean showAutocalculatedAbove;
+
   public AutoCalcSetting getCalcIdSettingsFor(String calcId)
   {
     return calcIdParams.get(calcId);
@@ -1264,4 +1277,24 @@ public class AlignViewport extends AlignmentViewport implements
       Cache.log.debug("trigger update for " + calcId);
     }
   }
+
+  protected SequenceAnnotationOrder getSortAnnotationsBy()
+  {
+    return sortAnnotationsBy;
+  }
+
+  protected void setSortAnnotationsBy(SequenceAnnotationOrder sortAnnotationsBy)
+  {
+    this.sortAnnotationsBy = sortAnnotationsBy;
+  }
+
+  protected boolean isShowAutocalculatedAbove()
+  {
+    return showAutocalculatedAbove;
+  }
+
+  protected void setShowAutocalculatedAbove(boolean showAutocalculatedAbove)
+  {
+    this.showAutocalculatedAbove = showAutocalculatedAbove;
+  }
 }
index cdac5b4..d610931 100644 (file)
  */
 package jalview.gui;
 
-import java.beans.*;
-import java.io.*;
-
-import java.awt.*;
-import java.awt.event.*;
-import java.awt.print.*;
-import javax.swing.*;
-
+import jalview.analysis.AnnotationSorter;
 import jalview.api.AlignmentViewPanel;
 import jalview.bin.Cache;
-import jalview.datamodel.*;
-import jalview.jbgui.*;
-import jalview.schemes.*;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.jbgui.GAlignmentPanel;
+import jalview.schemes.ResidueProperties;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.MessageManager;
 
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+
+import javax.swing.SwingUtilities;
+
 /**
  * DOCUMENT ME!
  * 
@@ -179,7 +196,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     int afwidth = (alignFrame != null ? alignFrame.getWidth() : 300);
     int maxwidth = Math.max(20,
-            Math.min(afwidth - 200, (int) 2 * afwidth / 3));
+            Math.min(afwidth - 200, 2 * afwidth / 3));
     return calculateIdWidth(maxwidth);
   }
 
@@ -718,8 +735,17 @@ public class AlignmentPanel extends GAlignmentPanel implements
     }
   }
 
+  /**
+   * Repaint the alignment including the annotations and overview panels (if
+   * shown).
+   */
   public void paintAlignment(boolean updateOverview)
   {
+    final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
+            av.isShowAutocalculatedAbove());
+    sorter.sort(getAlignment()
+            .getAlignmentAnnotation(),
+            av.getSortAnnotationsBy());
     repaint();
 
     if (updateOverview)
@@ -841,7 +867,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     // / How many sequences and residues can we fit on a printable page?
     int totalRes = (pwidth - idWidth) / av.getCharWidth();
 
-    int totalSeq = (int) ((pheight - scaleHeight) / av.getCharHeight()) - 1;
+    int totalSeq = (pheight - scaleHeight) / av.getCharHeight() - 1;
 
     int pagesWide = (av.getAlignment().getWidth() / totalRes) + 1;
 
@@ -954,10 +980,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
       int offset = -alabels.scrollOffset;
       pg.translate(0, offset);
       pg.translate(-idWidth - 3, (endSeq - startSeq) * av.charHeight + 3);
-      alabels.drawComponent((Graphics2D) pg, idWidth);
+      alabels.drawComponent(pg, idWidth);
       pg.translate(idWidth + 3, 0);
       annotationPanel.renderer.drawComponent(annotationPanel, av,
-              (Graphics2D) pg, -1, startRes, endRes + 1);
+              pg, -1, startRes, endRes + 1);
       pg.translate(0, -offset);
     }
 
index 2845122..1c7be46 100644 (file)
@@ -1875,7 +1875,6 @@ public class PopupMenu extends JPopupMenu
      * 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))
@@ -1896,7 +1895,7 @@ public class PopupMenu extends JPopupMenu
         // adjust for gaps
         copyAnn.adjustForAlignment();
         // add to the alignment and set visible
-        this.ap.getAlignment().addAnnotation(copyAnn, insertPosition++);
+        this.ap.getAlignment().addAnnotation(copyAnn);
         copyAnn.visible = true;
       }
     }
index afb0dc1..dba77dd 100755 (executable)
@@ -20,6 +20,7 @@
  */
 package jalview.jbgui;
 
+import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.bin.Cache;
 import jalview.gui.JvSwingUtils;
 import jalview.schemes.ColourSchemeProperty;
@@ -65,6 +66,8 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenu viewMenu = new JMenu();
 
+  protected JMenu annotationsMenu = new JMenu();
+
   protected JMenu colourMenu = new JMenu();
 
   protected JMenu calculateMenu = new JMenu();
@@ -304,9 +307,17 @@ public class GAlignFrame extends JInternalFrame
 
   JMenuItem showAllhidden = new JMenuItem();
 
-  protected JMenuItem showAllAnnotations = new JMenuItem();
+  protected JMenuItem showAllSeqAnnotations = new JMenuItem();
+
+  protected JMenuItem hideAllSeqAnnotations = new JMenuItem();
+
+  protected JMenuItem showAllAlAnnotations = new JMenuItem();
+
+  protected JMenuItem hideAllAlAnnotations = new JMenuItem();
+
+  protected JCheckBoxMenuItem sortAnnBySequence = new JCheckBoxMenuItem();
 
-  protected JMenuItem hideAllAnnotations = new JMenuItem();
+  protected JCheckBoxMenuItem sortAnnByLabel = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem hiddenMarkers = new JCheckBoxMenuItem();
 
@@ -360,8 +371,16 @@ public class GAlignFrame extends JInternalFrame
 
   protected JCheckBoxMenuItem applyAutoAnnotationSettings = new JCheckBoxMenuItem();
 
+  protected JCheckBoxMenuItem showAutoFirst = new JCheckBoxMenuItem();
+
+  protected JCheckBoxMenuItem showAutoLast = new JCheckBoxMenuItem();
+
   private JMenuItem grpsFromSelection = new JMenuItem();
 
+  private SequenceAnnotationOrder annotationSortOrder;
+
+  private boolean showAutoCalculatedAbove = false;
+
   public GAlignFrame()
   {
     try
@@ -611,6 +630,7 @@ public class GAlignFrame extends JInternalFrame
     });
     editMenu.setText(MessageManager.getString("action.edit"));
     viewMenu.setText(MessageManager.getString("action.view"));
+    annotationsMenu.setText(MessageManager.getString("action.annotations"));
     colourMenu.setText(MessageManager.getString("action.colour"));
     calculateMenu.setText(MessageManager.getString("action.calculate"));
     webService.setText(MessageManager.getString("action.web_service"));
@@ -1073,29 +1093,78 @@ public class GAlignFrame extends JInternalFrame
         annotationPanelMenuItem_actionPerformed(e);
       }
     });
-    /*
-     * Show/hide all annotations only enabled if annotation panel is shown
-     */
-    showAllAnnotations.setText(MessageManager
-            .getString("label.show_all_annotations"));
-    showAllAnnotations.setEnabled(annotationPanelMenuItem.getState());
-    showAllAnnotations.addActionListener(new ActionListener()
+    showAllAlAnnotations.setText(MessageManager
+            .getString("label.show_all_al_annotations"));
+    final boolean isAnnotationPanelShown = annotationPanelMenuItem
+            .getState();
+    showAllAlAnnotations.setEnabled(isAnnotationPanelShown);
+    showAllAlAnnotations.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        showAllAnnotations_actionPerformed(false, true);
+      }
+    });
+    hideAllAlAnnotations.setText(MessageManager
+            .getString("label.hide_all_al_annotations"));
+    hideAllAlAnnotations.setEnabled(isAnnotationPanelShown);
+    hideAllAlAnnotations.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        hideAllAnnotations_actionPerformed(false, true);
+      }
+    });
+    showAllSeqAnnotations.setText(MessageManager
+            .getString("label.show_all_seq_annotations"));
+    showAllSeqAnnotations.setEnabled(isAnnotationPanelShown);
+    showAllSeqAnnotations.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        showAllAnnotations_actionPerformed(true, false);
+      }
+    });
+    hideAllSeqAnnotations.setText(MessageManager
+            .getString("label.hide_all_seq_annotations"));
+    hideAllSeqAnnotations.setEnabled(isAnnotationPanelShown);
+    hideAllSeqAnnotations.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        hideAllAnnotations_actionPerformed(true, false);
+      }
+    });
+    sortAnnBySequence.setText(MessageManager
+            .getString("label.sort_annotations_by_sequence"));
+    sortAnnBySequence.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        showAllAnnotations_actionPerformed();
+        boolean newState = sortAnnBySequence.getState();
+        sortAnnByLabel.setState(false);
+        setAnnotationSortOrder(newState ? SequenceAnnotationOrder.SEQUENCE_AND_LABEL
+                : SequenceAnnotationOrder.NONE);
+        sortAnnotations_actionPerformed();
       }
     });
-    hideAllAnnotations.setText(MessageManager
-            .getString("label.hide_all_annotations"));
-    hideAllAnnotations.setEnabled(annotationPanelMenuItem.getState());
-    hideAllAnnotations.addActionListener(new ActionListener()
+    sortAnnByLabel.setText(MessageManager
+            .getString("label.sort_annotations_by_label"));
+    sortAnnByLabel.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        hideAllAnnotations_actionPerformed();
+        boolean newState = sortAnnByLabel.getState();
+        sortAnnBySequence.setState(false);
+        setAnnotationSortOrder(newState ? SequenceAnnotationOrder.LABEL_AND_SEQUENCE
+                : SequenceAnnotationOrder.NONE);
+        sortAnnotations_actionPerformed();
       }
     });
     colourTextMenuItem.setText(MessageManager
@@ -1338,13 +1407,38 @@ public class GAlignFrame extends JInternalFrame
     applyAutoAnnotationSettings.setVisible(true);
     applyAutoAnnotationSettings.addActionListener(new ActionListener()
     {
-
       @Override
       public void actionPerformed(ActionEvent e)
       {
         applyAutoAnnotationSettings_actionPerformed(e);
       }
+    });
 
+    showAutoFirst.setText(MessageManager.getString("label.show_first"));
+    showAutoFirst.setState(Cache.getDefault("SHOW_AUTOCALC_ABOVE", false));
+    showAutoFirst.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        boolean sortFirst = showAutoFirst.getState();
+        setShowAutoCalculatedAbove(sortFirst);
+        showAutoLast.setState(!sortFirst);
+        sortAnnotations_actionPerformed();
+      }
+    });
+    showAutoLast.setText(MessageManager.getString("label.show_last"));
+    showAutoLast.setState(!showAutoFirst.getState());
+    showAutoLast.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        boolean sortLast = showAutoLast.getState();
+        setShowAutoCalculatedAbove(!sortLast);
+        showAutoFirst.setState(!sortLast);
+        sortAnnotations_actionPerformed();
+      }
     });
 
     nucleotideColour.setText(MessageManager.getString("label.nucleotide"));
@@ -2101,6 +2195,7 @@ public class GAlignFrame extends JInternalFrame
     alignFrameMenuBar.add(editMenu);
     alignFrameMenuBar.add(selectMenu);
     alignFrameMenuBar.add(viewMenu);
+    alignFrameMenuBar.add(annotationsMenu);
     alignFrameMenuBar.add(formatMenu);
     alignFrameMenuBar.add(colourMenu);
     alignFrameMenuBar.add(calculateMenu);
@@ -2150,9 +2245,19 @@ public class GAlignFrame extends JInternalFrame
     viewMenu.add(hideMenu);
     viewMenu.addSeparator();
     viewMenu.add(followHighlightMenuItem);
-    viewMenu.add(annotationPanelMenuItem);
-    viewMenu.add(showAllAnnotations);
-    viewMenu.add(hideAllAnnotations);
+    annotationsMenu.add(annotationPanelMenuItem);
+    annotationsMenu.addSeparator();
+    annotationsMenu.add(showAllAlAnnotations);
+    annotationsMenu.add(hideAllAlAnnotations);
+    annotationsMenu.addSeparator();
+    annotationsMenu.add(showAllSeqAnnotations);
+    annotationsMenu.add(hideAllSeqAnnotations);
+    annotationsMenu.add(sortAnnBySequence);
+    annotationsMenu.add(sortAnnByLabel);
+    annotationsMenu.addSeparator();
+    autoAnnMenu.add(showAutoFirst);
+    autoAnnMenu.add(showAutoLast);
+    autoAnnMenu.addSeparator();
     autoAnnMenu.add(applyAutoAnnotationSettings);
     autoAnnMenu.add(showConsensusHistogram);
     autoAnnMenu.add(showSequenceLogo);
@@ -2160,7 +2265,7 @@ public class GAlignFrame extends JInternalFrame
     autoAnnMenu.addSeparator();
     autoAnnMenu.add(showGroupConservation);
     autoAnnMenu.add(showGroupConsensus);
-    viewMenu.add(autoAnnMenu);
+    annotationsMenu.add(autoAnnMenu);
     viewMenu.addSeparator();
     viewMenu.add(showSeqFeatures);
     // viewMenu.add(showSeqFeaturesHeight);
@@ -2272,27 +2377,54 @@ public class GAlignFrame extends JInternalFrame
   }
 
   /**
+   * Action on clicking sort annotations by type.
+   * 
+   * @param sortOrder
+   */
+  protected void sortAnnotations_actionPerformed()
+  {
+  }
+
+  /**
    * Action on clicking Show all annotations.
+   * 
+   * @param forSequences
+   *          update sequence-related annotations
+   * @param forAlignment
+   *          update non-sequence-related annotations
    */
-  protected void showAllAnnotations_actionPerformed()
+  protected void showAllAnnotations_actionPerformed(boolean forSequences,
+          boolean forAlignment)
   {
-    setAllAnnotationsVisibility(true);
+    setAnnotationsVisibility(true, forSequences, forAlignment);
   }
 
   /**
    * Action on clicking Hide all annotations.
+   * 
+   * @param forSequences
+   *          update sequence-related annotations
+   * @param forAlignment
+   *          update non-sequence-related annotations
    */
-  protected void hideAllAnnotations_actionPerformed()
+  protected void hideAllAnnotations_actionPerformed(boolean forSequences,
+          boolean forAlignment)
   {
-    setAllAnnotationsVisibility(false);
+    setAnnotationsVisibility(false, forSequences, forAlignment);
   }
 
   /**
-   * Set the visibility of all annotations to true or false.
+   * Set the visibility of annotations to true or false. Can act on
+   * sequence-related annotations, or alignment-related, or both.
    * 
    * @param visible
+   * @param forSequences
+   *          update sequence-related annotations
+   * @param forAlignment
+   *          update non-sequence-related annotations
    */
-  protected void setAllAnnotationsVisibility(boolean visible)
+  protected void setAnnotationsVisibility(boolean visible,
+          boolean forSequences, boolean forAlignment)
   {
 
   }
@@ -2941,4 +3073,24 @@ public class GAlignFrame extends JInternalFrame
     // TODO Auto-generated method stub
 
   }
+
+  protected boolean isShowAutoCalculatedAbove()
+  {
+    return showAutoCalculatedAbove;
+  }
+
+  protected void setShowAutoCalculatedAbove(boolean showAutoCalculatedAbove)
+  {
+    this.showAutoCalculatedAbove = showAutoCalculatedAbove;
+  }
+
+  protected SequenceAnnotationOrder getAnnotationSortOrder()
+  {
+    return annotationSortOrder;
+  }
+
+  protected void setAnnotationSortOrder(SequenceAnnotationOrder annotationSortOrder)
+  {
+    this.annotationSortOrder = annotationSortOrder;
+  }
 }
index 1da1939..19a5163 100644 (file)
@@ -36,7 +36,7 @@ public class AlignmentAnnotationUtilsTest
           "TIETHKEEELTA-" + EOL;
   // @formatter:on
 
-  private static final int SEQ_ANN_COUNT = 10;
+  private static final int SEQ_ANN_COUNT = 12;
 
   private AlignmentI alignment;
 
@@ -212,6 +212,7 @@ public class AlignmentAnnotationUtilsTest
   @Test
   public void testGetShownHiddenTypes_withGraphGroups()
   {
+    final int GROUP_3 = 3;
     final int GROUP_4 = 4;
     final int GROUP_5 = 5;
     final int GROUP_6 = 6;
@@ -222,10 +223,10 @@ public class AlignmentAnnotationUtilsTest
     SequenceI[] seqs = alignment.getSequencesArray();
   
     /*
-     * Configure annotation properties for test
+     * Annotations for selection group and graph group
+     * 
+     * Hidden annotations Label2, Label3, in (hidden) group 5
      */
-    // annotations for selection group and graph group
-    // hidden annotations Label2, Label3, in (hidden) group 5
     anns[2].sequenceRef = seqs[3];
     anns[2].visible = false;
     anns[2].graph = AlignmentAnnotation.LINE_GRAPH;
@@ -236,6 +237,19 @@ public class AlignmentAnnotationUtilsTest
     anns[3].graphGroup = GROUP_5;
     // need to ensure annotations have the same calcId as well
     anns[3].setCalcId("CalcId2");
+    // annotations for a different hidden group generating the same group label
+    anns[10].sequenceRef = seqs[0];
+    anns[10].visible = false;
+    anns[10].graph = AlignmentAnnotation.LINE_GRAPH;
+    anns[10].graphGroup = GROUP_3;
+    anns[10].label = "Label3";
+    anns[10].setCalcId("CalcId2");
+    anns[11].sequenceRef = seqs[3];
+    anns[11].visible = false;
+    anns[11].graph = AlignmentAnnotation.LINE_GRAPH;
+    anns[11].graphGroup = GROUP_3;
+    anns[11].label = "Label2";
+    anns[11].setCalcId("CalcId2");
   
     // annotations Label1 (hidden), Label5 (visible) in group 6 (visible)
     anns[1].sequenceRef = seqs[3];
@@ -248,9 +262,29 @@ public class AlignmentAnnotationUtilsTest
     anns[5].graph = AlignmentAnnotation.LINE_GRAPH;
     anns[5].graphGroup = GROUP_6;
     anns[5].setCalcId("CalcId1");
+    /*
+     * Annotations 0 and 4 are visible, for a different CalcId and graph group.
+     * They produce the same label as annotations 1 and 5, which should not be
+     * duplicated in the results. This case corresponds to (e.g.) many
+     * occurrences of an IUPred Short/Long annotation group, one per sequence.
+     */
+    anns[4].sequenceRef = seqs[0];
+    anns[4].visible = false;
+    anns[4].graph = AlignmentAnnotation.LINE_GRAPH;
+    anns[4].graphGroup = GROUP_4;
+    anns[4].label = "Label1";
+    anns[4].setCalcId("CalcId1");
+    anns[0].sequenceRef = seqs[0];
+    anns[0].visible = true;
+    anns[0].graph = AlignmentAnnotation.LINE_GRAPH;
+    anns[0].graphGroup = GROUP_4;
+    anns[0].label = "Label5";
+    anns[0].setCalcId("CalcId1");
   
-    // annotations outwith selection group - should be ignored
-    // hidden grouped annotations
+    /*
+     * Annotations outwith selection group - should be ignored.
+     */
+    // Hidden grouped annotations
     anns[6].sequenceRef = seqs[2];
     anns[6].visible = false;
     anns[6].graph = AlignmentAnnotation.LINE_GRAPH;
@@ -259,6 +293,7 @@ public class AlignmentAnnotationUtilsTest
     anns[8].visible = false;
     anns[8].graph = AlignmentAnnotation.LINE_GRAPH;
     anns[8].graphGroup = GROUP_4;
+
     // visible grouped annotations Label7, Label9
     anns[7].sequenceRef = seqs[2];
     anns[7].visible = true;
@@ -276,13 +311,16 @@ public class AlignmentAnnotationUtilsTest
   
     consoleDebug(shownTypes, hiddenTypes);
   
-    // CalcId1 / Label1, Label5 (only) should be 'shown', as a compound type
+    // CalcId1 / Label1, Label5 (only) should be 'shown', once, as a compound
+    // type
+    assertEquals(1, shownTypes.size());
     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.size());
     assertEquals(1, hiddenTypes.get("CalcId2").size());
     assertEquals(2, hiddenTypes.get("CalcId2").get(0).size());
     assertEquals("Label2", hiddenTypes.get("CalcId2").get(0).get(0));
diff --git a/test/jalview/analysis/AnnotationSorterTest.java b/test/jalview/analysis/AnnotationSorterTest.java
new file mode 100644 (file)
index 0000000..ba2162d
--- /dev/null
@@ -0,0 +1,345 @@
+package jalview.analysis;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class AnnotationSorterTest
+{
+  private static final int NUM_SEQS = 6;
+
+  private static final int NUM_ANNS = 7;
+
+  private static final String SS = "secondary structure";
+
+  AlignmentAnnotation[] anns = new AlignmentAnnotation[0];
+
+  Alignment al = null;
+
+  /*
+   * Set up 6 sequences and 7 annotations.
+   */
+  @Before
+  public void setUp()
+  {
+    al = buildAlignment(NUM_SEQS);
+    anns = buildAnnotations(NUM_ANNS);
+  }
+
+  /**
+   * Construct an array of numAnns annotations
+   * 
+   * @param numAnns
+   * 
+   * @return
+   */
+  protected AlignmentAnnotation[] buildAnnotations(int numAnns)
+  {
+    List<AlignmentAnnotation> annlist = new ArrayList<AlignmentAnnotation>();
+    for (int i = 0; i < numAnns; i++)
+    {
+      AlignmentAnnotation ann = new AlignmentAnnotation(SS + i, "", 0);
+      annlist.add(ann);
+    }
+    return annlist.toArray(anns);
+  }
+
+  /**
+   * Make an alignment with numSeqs sequences in it.
+   * 
+   * @param numSeqs
+   * 
+   * @return
+   */
+  private Alignment buildAlignment(int numSeqs)
+  {
+    SequenceI[] seqs = new Sequence[numSeqs];
+    for (int i = 0; i < numSeqs; i++)
+    {
+      seqs[i] = new Sequence("Sequence" + i, "axrdkfp");
+    }
+    return new Alignment(seqs);
+  }
+
+  /**
+   * Test sorting by annotation type (label) within sequence order, including
+   * <ul>
+   * <li>annotations with no sequence reference - sort to end keeping mutual
+   * ordering</li>
+   * <li>annotations with sequence ref = sort in sequence order</li>
+   * <li>multiple annotations for same sequence ref - sort by label
+   * non-case-specific</li>
+   * <li>annotations with reference to sequence not in alignment - treat like no
+   * sequence ref</li>
+   * </ul>
+   */
+  @Test
+  public void testSortBySequenceAndType_autocalcLast()
+  {
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].sequenceRef = null;                anns[3].label = "Quality";
+    anns[4].sequenceRef = null;                anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "label5";
+    anns[6].sequenceRef = al.getSequenceAt(3); anns[6].label = "IRP";
+    // @formatter:on
+
+    AnnotationSorter testee = new AnnotationSorter(al, false);
+    testee.sort(anns, SequenceAnnotationOrder.SEQUENCE_AND_LABEL);
+    assertEquals("label5", anns[0].label); // for sequence 0
+    assertEquals("label0", anns[1].label); // for sequence 1
+    assertEquals("iron", anns[2].label); // sequence 3 /iron
+    assertEquals("IRP", anns[3].label); // sequence 3/IRP
+    assertEquals("structure", anns[4].label); // sequence 3/structure
+    assertEquals("Quality", anns[5].label); // non-sequence annotations
+    assertEquals("Consensus", anns[6].label); // retain ordering
+  }
+
+  /**
+   * Variant with autocalculated annotations sorting to front
+   */
+  @Test
+  public void testSortBySequenceAndType_autocalcFirst()
+  {
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].sequenceRef = null;                anns[3].label = "Quality";
+    anns[4].sequenceRef = null;                anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "label5";
+    anns[6].sequenceRef = al.getSequenceAt(3); anns[6].label = "IRP";
+    // @formatter:on
+
+    AnnotationSorter testee = new AnnotationSorter(al, true);
+    testee.sort(anns, SequenceAnnotationOrder.SEQUENCE_AND_LABEL);
+    assertEquals("Quality", anns[0].label); // non-sequence annotations
+    assertEquals("Consensus", anns[1].label); // retain ordering
+    assertEquals("label5", anns[2].label); // for sequence 0
+    assertEquals("label0", anns[3].label); // for sequence 1
+    assertEquals("iron", anns[4].label); // sequence 3 /iron
+    assertEquals("IRP", anns[5].label); // sequence 3/IRP
+    assertEquals("structure", anns[6].label); // sequence 3/structure
+  }
+
+  /**
+   * Test sorting by annotation type (label) within sequence order, including
+   * <ul>
+   * <li>annotations with no sequence reference - sort to end keeping mutual
+   * ordering</li>
+   * <li>annotations with sequence ref = sort in sequence order</li>
+   * <li>multiple annotations for same sequence ref - sort by label
+   * non-case-specific</li>
+   * <li>annotations with reference to sequence not in alignment - treat like no
+   * sequence ref</li>
+   * </ul>
+   */
+  @Test
+  public void testSortByTypeAndSequence_autocalcLast()
+  {
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].sequenceRef = null;                anns[3].label = "Quality";
+    anns[4].sequenceRef = null;                anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "IRON";
+    anns[6].sequenceRef = al.getSequenceAt(2); anns[6].label = "Structure";
+    // @formatter:on
+
+    AnnotationSorter testee = new AnnotationSorter(al, false);
+    testee.sort(anns, SequenceAnnotationOrder.LABEL_AND_SEQUENCE);
+    assertEquals("IRON", anns[0].label); // IRON / sequence 0
+    assertEquals("iron", anns[1].label); // iron / sequence 3
+    assertEquals("label0", anns[2].label); // label0 / sequence 1
+    assertEquals("Structure", anns[3].label); // Structure / sequence 2
+    assertEquals("structure", anns[4].label); // structure / sequence 3
+    assertEquals("Quality", anns[5].label); // non-sequence annotations
+    assertEquals("Consensus", anns[6].label); // retain ordering
+  }
+
+  /**
+   * Variant of test with autocalculated annotations sorted to front
+   */
+  @Test
+  public void testSortByTypeAndSequence_autocalcFirst()
+  {
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].sequenceRef = null;                anns[3].label = "Quality";
+    anns[4].sequenceRef = null;                anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "IRON";
+    anns[6].sequenceRef = al.getSequenceAt(2); anns[6].label = "Structure";
+    // @formatter:on
+
+    AnnotationSorter testee = new AnnotationSorter(al, true);
+    testee.sort(anns, SequenceAnnotationOrder.LABEL_AND_SEQUENCE);
+    assertEquals("Quality", anns[0].label); // non-sequence annotations
+    assertEquals("Consensus", anns[1].label); // retain ordering
+    assertEquals("IRON", anns[2].label); // IRON / sequence 0
+    assertEquals("iron", anns[3].label); // iron / sequence 3
+    assertEquals("label0", anns[4].label); // label0 / sequence 1
+    assertEquals("Structure", anns[5].label); // Structure / sequence 2
+    assertEquals("structure", anns[6].label); // structure / sequence 3
+  }
+
+  /**
+   * Variant of test with autocalculated annotations sorted to front but
+   * otherwise no change.
+   */
+  @Test
+  public void testNoSort_autocalcFirst()
+  {
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].sequenceRef = null;                anns[3].label = "Quality";
+    anns[4].sequenceRef = null;                anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "IRON";
+    anns[6].sequenceRef = al.getSequenceAt(2); anns[6].label = "Structure";
+    // @formatter:on
+
+    AnnotationSorter testee = new AnnotationSorter(al, true);
+    testee.sort(anns, SequenceAnnotationOrder.NONE);
+    assertEquals("Quality", anns[0].label); // non-sequence annotations
+    assertEquals("Consensus", anns[1].label); // retain ordering
+    assertEquals("label0", anns[2].label);
+    assertEquals("structure", anns[3].label);
+    assertEquals("iron", anns[4].label);
+    assertEquals("IRON", anns[5].label);
+    assertEquals("Structure", anns[6].label);
+  }
+
+  @Test
+  public void testSort_timingPresorted()
+  {
+    final long targetTime = 100; // ms
+    final int numSeqs = 10000;
+    final int numAnns = 20000;
+    al = buildAlignment(numSeqs);
+    anns = buildAnnotations(numAnns);
+
+    /*
+     * Set the annotations presorted by label
+     */
+    Random r = new Random();
+    final SequenceI[] sequences = al.getSequencesArray();
+    for (int i = 0; i < anns.length; i++)
+    {
+      SequenceI randomSequenceRef = sequences[r.nextInt(sequences.length)];
+      anns[i].sequenceRef = randomSequenceRef;
+      anns[i].label = "label" + i;
+    }
+    long startTime = System.currentTimeMillis();
+    AnnotationSorter testee = new AnnotationSorter(al, false);
+    testee.sort(anns, SequenceAnnotationOrder.LABEL_AND_SEQUENCE);
+    long endTime = System.currentTimeMillis();
+    final long elapsed = endTime - startTime;
+    System.out.println("Timing test for presorted " + numSeqs
+            + " sequences and "
+            + numAnns + " annotations took " + elapsed + "ms");
+    assertTrue("Sort took more than " + targetTime + "ms",
+            elapsed <= targetTime);
+  }
+
+  /**
+   * Timing test for sorting randomly sorted annotations
+   */
+  @Test
+  public void testSort_timingUnsorted()
+  {
+    final int numSeqs = 2000;
+    final int numAnns = 4000;
+    al = buildAlignment(numSeqs);
+    anns = buildAnnotations(numAnns);
+
+    /*
+     * Set the annotations in random order with respect to the sequences
+     */
+    Random r = new Random();
+    final SequenceI[] sequences = al.getSequencesArray();
+    for (int i = 0; i < anns.length; i++)
+    {
+      SequenceI randomSequenceRef = sequences[r.nextInt(sequences.length)];
+      anns[i].sequenceRef = randomSequenceRef;
+      anns[i].label = "label" + i;
+    }
+    long startTime = System.currentTimeMillis();
+    AnnotationSorter testee = new AnnotationSorter(al, false);
+    testee.sort(anns, SequenceAnnotationOrder.SEQUENCE_AND_LABEL);
+    long endTime = System.currentTimeMillis();
+    final long elapsed = endTime - startTime;
+    System.out.println("Timing test for unsorted " + numSeqs
+            + " sequences and "
+            + numAnns + " annotations took " + elapsed + "ms");
+  }
+
+  /**
+   * Timing test for sorting annotations with a limited range of types (labels).
+   */
+  @Test
+  public void testSort_timingSemisorted()
+  {
+    final int numSeqs = 2000;
+    final int numAnns = 4000;
+    al = buildAlignment(numSeqs);
+    anns = buildAnnotations(numAnns);
+
+    String[] labels = new String[]
+    { "label1", "label2", "label3", "label4", "label5", "label6" };
+
+    /*
+     * Set the annotations in sequence order with randomly assigned labels.
+     */
+    Random r = new Random();
+    final SequenceI[] sequences = al.getSequencesArray();
+    for (int i = 0; i < anns.length; i++)
+    {
+      SequenceI sequenceRef = sequences[i % sequences.length];
+      anns[i].sequenceRef = sequenceRef;
+      anns[i].label = labels[r.nextInt(labels.length)];
+    }
+    long startTime = System.currentTimeMillis();
+    AnnotationSorter testee = new AnnotationSorter(al, false);
+    testee.sort(anns, SequenceAnnotationOrder.LABEL_AND_SEQUENCE);
+    long endTime = System.currentTimeMillis();
+    long elapsed = endTime - startTime;
+    System.out.println("Sort by type for semisorted " + numSeqs
+            + " sequences and "
+            + numAnns + " annotations took " + elapsed + "ms");
+
+    // now resort by sequence
+    startTime = System.currentTimeMillis();
+    testee.sort(anns, SequenceAnnotationOrder.SEQUENCE_AND_LABEL);
+    endTime = System.currentTimeMillis();
+    elapsed = endTime - startTime;
+    System.out.println("Resort by sequence for semisorted " + numSeqs
+            + " sequences and " + numAnns + " annotations took " + elapsed
+            + "ms");
+
+    // now resort by type
+    startTime = System.currentTimeMillis();
+    testee.sort(anns, SequenceAnnotationOrder.LABEL_AND_SEQUENCE);
+    endTime = System.currentTimeMillis();
+    elapsed = endTime - startTime;
+    System.out.println("Resort by type for semisorted " + numSeqs
+            + " sequences and " + numAnns + " annotations took " + elapsed
+            + "ms");
+  }
+}