JAL-1152 prototype of new Annotations menu with sort options
[jalview.git] / src / jalview / analysis / AnnotationSorter.java
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);
+  }
+}