JAL-1152 prototype of new Annotations menu with sort options
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 30 Oct 2014 14:18:42 +0000 (14:18 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 30 Oct 2014 14:18:42 +0000 (14:18 +0000)
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/gui/AlignFrame.java
src/jalview/gui/PopupMenu.java
src/jalview/jbgui/GAlignFrame.java
test/jalview/analysis/AnnotationSorterTest.java [new file with mode: 0644]

index f23275a..24b184f 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
@@ -479,6 +480,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 annotations by sequence order
+label.sort_annotations_by_type = Sort annotations by type
 label.reveal = Reveal
 label.hide_columns = Hide Columns
 label.load_jalview_annotations = Load Jalview Annotations or Features File
index b5f77fd..ef8e670 100644 (file)
@@ -1,6 +1,7 @@
 package jalview.analysis;
 
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.AnnotationRenderer;
 
@@ -196,8 +197,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..4ee2b86
--- /dev/null
@@ -0,0 +1,229 @@
+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
+{
+
+  private final AlignmentI alignment;
+
+  public AnnotationSorter(AlignmentI alignmentI)
+  {
+    this.alignment = alignmentI;
+  }
+
+  /**
+   * 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> bySequenceAndType = 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> byTypeAndSequence = 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 after sequence-related.
+       */
+      if (o1.sequenceRef == null)
+      {
+        return 1;
+      }
+      if (o2.sequenceRef == null)
+      {
+        return -1;
+      }
+      int labelOrder = compareLabels(o1, o2);
+      return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
+    }
+  };
+
+  /**
+   * Sort by annotation type (label), within sequence order.
+   * Non-sequence-related annotations sort to the end.
+   * 
+   * @param alignmentAnnotations
+   */
+  public void sortBySequenceAndType(
+          AlignmentAnnotation[] alignmentAnnotations)
+  {
+    if (alignmentAnnotations != null)
+    {
+      synchronized (alignmentAnnotations)
+      {
+        Arrays.sort(alignmentAnnotations, bySequenceAndType);
+      }
+    }
+  }
+
+  /**
+   * Sort by sequence order within annotation type (label). Non-sequence-related
+   * annotations sort to the end.
+   * 
+   * @param alignmentAnnotations
+   */
+  public void sortByTypeAndSequence(
+          AlignmentAnnotation[] alignmentAnnotations)
+  {
+    if (alignmentAnnotations != null)
+    {
+      synchronized (alignmentAnnotations)
+      {
+        Arrays.sort(alignmentAnnotations, byTypeAndSequence);
+      }
+    }
+  }
+
+  /**
+   * 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;
+    }
+    if (seq1 == null)
+    {
+      return 1;
+    }
+    if (seq2 == null)
+    {
+      return -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 d9c0c6a..a048721 100644 (file)
@@ -23,6 +23,7 @@ package jalview.gui;
 import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentSorter;
 import jalview.analysis.AlignmentUtils;
+import jalview.analysis.AnnotationSorter;
 import jalview.analysis.Conservation;
 import jalview.analysis.CrossRef;
 import jalview.analysis.NJTree;
@@ -377,7 +378,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                         .getKeyCode() >= KeyEvent.VK_NUMPAD0 && evt
                         .getKeyCode() <= KeyEvent.VK_NUMPAD9))
                 && Character.isDigit(evt.getKeyChar()))
+        {
           alignPanel.seqPanel.numberPressed(evt.getKeyChar());
+        }
 
         switch (evt.getKeyCode())
         {
@@ -389,32 +392,48 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
         case KeyEvent.VK_DOWN:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             moveSelectedSequences(false);
+          }
           if (viewport.cursorMode)
+          {
             alignPanel.seqPanel.moveCursor(0, 1);
+          }
           break;
 
         case KeyEvent.VK_UP:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             moveSelectedSequences(true);
+          }
           if (viewport.cursorMode)
+          {
             alignPanel.seqPanel.moveCursor(0, -1);
+          }
 
           break;
 
         case KeyEvent.VK_LEFT:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             slideSequences(false, alignPanel.seqPanel.getKeyboardNo1());
+          }
           else
+          {
             alignPanel.seqPanel.moveCursor(-1, 0);
+          }
 
           break;
 
         case KeyEvent.VK_RIGHT:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             slideSequences(true, alignPanel.seqPanel.getKeyboardNo1());
+          }
           else
+          {
             alignPanel.seqPanel.moveCursor(1, 0);
+          }
           break;
 
         case KeyEvent.VK_SPACE:
@@ -551,14 +570,18 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         {
         case KeyEvent.VK_LEFT:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             viewport.firePropertyChange("alignment", null, viewport
                     .getAlignment().getSequences());
+          }
           break;
 
         case KeyEvent.VK_RIGHT:
           if (evt.isAltDown() || !viewport.cursorMode)
+          {
             viewport.firePropertyChange("alignment", null, viewport
                     .getAlignment().getSequences());
+          }
           break;
         }
       }
@@ -1458,7 +1481,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void undoMenuItem_actionPerformed(ActionEvent e)
   {
     if (viewport.historyList.empty())
+    {
       return;
+    }
     CommandI command = (CommandI) viewport.historyList.pop();
     viewport.redoList.push(command);
     command.undoCommand(getViewAlignments());
@@ -1611,37 +1636,53 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     for (int i = 0; i < viewport.getAlignment().getHeight(); i++)
     {
       if (!sg.contains(viewport.getAlignment().getSequenceAt(i)))
+      {
         invertGroup.add(viewport.getAlignment().getSequenceAt(i));
+      }
     }
 
     SequenceI[] seqs1 = sg.toArray(new SequenceI[0]);
 
     SequenceI[] seqs2 = new SequenceI[invertGroup.size()];
     for (int i = 0; i < invertGroup.size(); i++)
+    {
       seqs2[i] = (SequenceI) invertGroup.elementAt(i);
+    }
 
     SlideSequencesCommand ssc;
     if (right)
+    {
       ssc = new SlideSequencesCommand("Slide Sequences", seqs2, seqs1,
               size, viewport.getGapCharacter());
+    }
     else
+    {
       ssc = new SlideSequencesCommand("Slide Sequences", seqs1, seqs2,
               size, viewport.getGapCharacter());
+    }
 
     int groupAdjustment = 0;
     if (ssc.getGapsInsertedBegin() && right)
     {
       if (viewport.cursorMode)
+      {
         alignPanel.seqPanel.moveCursor(size, 0);
+      }
       else
+      {
         groupAdjustment = size;
+      }
     }
     else if (!ssc.getGapsInsertedBegin() && !right)
     {
       if (viewport.cursorMode)
+      {
         alignPanel.seqPanel.moveCursor(-size, 0);
+      }
       else
+      {
         groupAdjustment = -size;
+      }
     }
 
     if (groupAdjustment != 0)
@@ -1662,7 +1703,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
 
     if (!appendHistoryItem)
+    {
       addHistoryItem(ssc);
+    }
 
     repaint();
   }
@@ -1997,7 +2040,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           {
             AlignmentAnnotation sann[] = sequences[i].getAnnotation();
             if (sann == null)
+            {
               continue;
+            }
             for (int avnum = 0; avnum < alview.length; avnum++)
             {
               if (alview[avnum] != alignment)
@@ -4643,7 +4688,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
               if (ds.getSequences() == null
                       || !ds.getSequences().contains(
                               sprods[s].getDatasetSequence()))
+              {
                 ds.addSequence(sprods[s].getDatasetSequence());
+              }
               sprods[s].updatePDBIds();
             }
             Alignment al = new Alignment(sprods);
@@ -5734,6 +5781,26 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     this.alignPanel.paintAlignment(true);
   }
+
+  @Override
+  protected void sortAnnotationsByType_actionPerformed()
+  {
+    AnnotationSorter sorter = new AnnotationSorter(
+            this.alignPanel.getAlignment());
+    sorter.sortByTypeAndSequence(this.alignPanel.getAlignment()
+            .getAlignmentAnnotation());
+    alignPanel.updateAnnotation(applyAutoAnnotationSettings.getState());
+  }
+
+  @Override
+  protected void sortAnnotationsBySequence_actionPerformed()
+  {
+    AnnotationSorter sorter = new AnnotationSorter(
+            this.alignPanel.getAlignment());
+    sorter.sortBySequenceAndType(this.alignPanel.getAlignment()
+            .getAlignmentAnnotation());
+    alignPanel.updateAnnotation(applyAutoAnnotationSettings.getState());
+  }
 }
 
 class PrintThread extends Thread
index 3da4494..8e0e6e3 100644 (file)
@@ -22,6 +22,7 @@ package jalview.gui;
 
 import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentAnnotationUtils;
+import jalview.analysis.AnnotationSorter;
 import jalview.analysis.Conservation;
 import jalview.commands.ChangeCaseCommand;
 import jalview.commands.EditCommand;
@@ -1875,7 +1876,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,10 +1896,15 @@ 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;
       }
     }
+    // TODO: save annotation sort order on AlignViewport
+    // do sorting from AlignmentPanel.updateAnnotation()
+    new AnnotationSorter(this.ap.getAlignment())
+            .sortBySequenceAndType(this.ap.getAlignment()
+                    .getAlignmentAnnotation());
     refresh();
   }
 
index afb0dc1..5e60a85 100755 (executable)
@@ -65,6 +65,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();
@@ -308,6 +310,10 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem hideAllAnnotations = new JMenuItem();
 
+  protected JMenuItem sortAnnBySequence = new JMenuItem();
+
+  protected JMenuItem sortAnnByType = new JMenuItem();
+
   protected JCheckBoxMenuItem hiddenMarkers = new JCheckBoxMenuItem();
 
   JMenuItem invertColSel = new JMenuItem();
@@ -611,6 +617,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"));
@@ -1098,6 +1105,26 @@ public class GAlignFrame extends JInternalFrame
         hideAllAnnotations_actionPerformed();
       }
     });
+    sortAnnBySequence.setText(MessageManager
+            .getString("label.sort_annotations_by_sequence"));
+    sortAnnBySequence.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        sortAnnotationsBySequence_actionPerformed();
+      }
+    });
+    sortAnnByType.setText(MessageManager
+            .getString("label.sort_annotations_by_type"));
+    sortAnnByType.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        sortAnnotationsByType_actionPerformed();
+      }
+    });
     colourTextMenuItem.setText(MessageManager
             .getString("label.colour_text"));
     colourTextMenuItem
@@ -2101,6 +2128,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 +2178,11 @@ 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.add(showAllAnnotations);
+    annotationsMenu.add(hideAllAnnotations);
+    annotationsMenu.add(sortAnnBySequence);
+    annotationsMenu.add(sortAnnByType);
     autoAnnMenu.add(applyAutoAnnotationSettings);
     autoAnnMenu.add(showConsensusHistogram);
     autoAnnMenu.add(showSequenceLogo);
@@ -2160,7 +2190,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,6 +2302,20 @@ public class GAlignFrame extends JInternalFrame
   }
 
   /**
+   * Action on clicking sort annotations by type.
+   */
+  protected void sortAnnotationsByType_actionPerformed()
+  {
+  }
+
+  /**
+   * Action on clicking sort annotations by sequence
+   */
+  protected void sortAnnotationsBySequence_actionPerformed()
+  {
+  }
+
+  /**
    * Action on clicking Show all annotations.
    */
   protected void showAllAnnotations_actionPerformed()
diff --git a/test/jalview/analysis/AnnotationSorterTest.java b/test/jalview/analysis/AnnotationSorterTest.java
new file mode 100644 (file)
index 0000000..879aa9b
--- /dev/null
@@ -0,0 +1,175 @@
+package jalview.analysis;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+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()
+  {
+    // @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);
+    testee.sortBySequenceAndType(anns);
+    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
+  }
+
+  /**
+   * 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()
+  {
+    // @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);
+    testee.sortByTypeAndSequence(anns);
+    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
+  }
+
+  @Test
+  public void testSortBySequenceAndType_timing()
+  {
+    final long targetTime = 300;        // ms
+    final int numSeqs = 10000;
+    final int numAnns = 20000;
+    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;
+    }
+    long startTime = System.currentTimeMillis();
+    AnnotationSorter testee = new AnnotationSorter(al);
+    testee.sortByTypeAndSequence(anns);
+    long endTime = System.currentTimeMillis();
+    final long elapsed = endTime - startTime;
+    System.out.println("Timing test for " + numSeqs + " sequences and "
+            + numAnns + " annotations took " + elapsed + "ms");
+    assertTrue("Sort took more than " + targetTime + "ms",
+            elapsed <= targetTime);
+  }
+}