JAL-1152 with sticky annotation sort order that updates as sequences are
[jalview.git] / src / jalview / analysis / AnnotationSorter.java
1 package jalview.analysis;
2
3 import jalview.datamodel.AlignmentAnnotation;
4 import jalview.datamodel.AlignmentI;
5 import jalview.datamodel.SequenceI;
6
7 import java.util.Arrays;
8 import java.util.Comparator;
9
10 /**
11  * A helper class to sort all annotations associated with an alignment in
12  * various ways.
13  * 
14  * @author gmcarstairs
15  *
16  */
17 public class AnnotationSorter
18 {
19
20   public enum SortOrder
21   {
22     SEQUENCE_AND_TYPE, TYPE_AND_SEQUENCE
23   }
24   
25   private final AlignmentI alignment;
26
27   public AnnotationSorter(AlignmentI alignmentI)
28   {
29     this.alignment = alignmentI;
30   }
31
32   /**
33    * Default comparator sorts as follows by annotation type within sequence
34    * order:
35    * <ul>
36    * <li>annotations with a reference to a sequence in the alignment are sorted
37    * on sequence ordering</li>
38    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
39    * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
40    * </ul>
41    */
42   private final Comparator<? super AlignmentAnnotation> bySequenceAndType = new Comparator<AlignmentAnnotation>()
43   {
44     @Override
45     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
46     {
47       if (o1 == null && o2 == null)
48       {
49         return 0;
50       }
51       if (o1 == null)
52       {
53         return -1;
54       }
55       if (o2 == null)
56       {
57         return 1;
58       }
59
60       /*
61        * Ignore label (keep existing ordering) for
62        * Conservation/Quality/Consensus etc
63        */
64       if (o1.sequenceRef == null && o2.sequenceRef == null)
65       {
66         return 0;
67       }
68       int sequenceOrder = compareSequences(o1, o2);
69       return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
70     }
71   };
72
73   /**
74    * This comparator sorts as follows by sequence order within annotation type
75    * <ul>
76    * <li>annotations with a reference to a sequence in the alignment are sorted
77    * on label (non-case-sensitive)</li>
78    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
79    * <li>within the same label, sort by order of the related sequences</li>
80    * </ul>
81    */
82   private final Comparator<? super AlignmentAnnotation> byTypeAndSequence = new Comparator<AlignmentAnnotation>()
83   {
84     @Override
85     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
86     {
87       if (o1 == null && o2 == null)
88       {
89         return 0;
90       }
91       if (o1 == null)
92       {
93         return -1;
94       }
95       if (o2 == null)
96       {
97         return 1;
98       }
99
100       /*
101        * Ignore label (keep existing ordering) for
102        * Conservation/Quality/Consensus etc
103        */
104       if (o1.sequenceRef == null && o2.sequenceRef == null)
105       {
106         return 0;
107       }
108
109       /*
110        * Sort non-sequence-related after sequence-related.
111        */
112       if (o1.sequenceRef == null)
113       {
114         return 1;
115       }
116       if (o2.sequenceRef == null)
117       {
118         return -1;
119       }
120       int labelOrder = compareLabels(o1, o2);
121       return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
122     }
123   };
124
125   private final Comparator<? super AlignmentAnnotation> DEFAULT_COMPARATOR = bySequenceAndType;
126   
127   /**
128    * Sort by the specified order.
129    * 
130    * @param alignmentAnnotations
131    * @param order
132    */
133   public void sort(AlignmentAnnotation[] alignmentAnnotations,
134           SortOrder order)
135   {
136     Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
137
138     if (alignmentAnnotations != null)
139     {
140       synchronized (alignmentAnnotations)
141       {
142         Arrays.sort(alignmentAnnotations, comparator);
143       }
144     }
145   }
146
147   /**
148    * Get the comparator for the specified sort order.
149    * 
150    * @param order
151    * @return
152    */
153   private Comparator<? super AlignmentAnnotation> getComparator(
154           SortOrder order)
155   {
156     if (order == null)
157     {
158       return DEFAULT_COMPARATOR;
159     }
160     switch (order)
161     {
162     case SEQUENCE_AND_TYPE:
163       return this.bySequenceAndType;
164     case TYPE_AND_SEQUENCE:
165       return this.byTypeAndSequence;
166     default:
167       throw new UnsupportedOperationException(order.toString());
168     }
169   }
170
171   /**
172    * Non-case-sensitive comparison of annotation labels. Returns zero if either
173    * argument is null.
174    * 
175    * @param o1
176    * @param o2
177    * @return
178    */
179   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
180   {
181     if (o1 == null || o2 == null)
182     {
183       return 0;
184     }
185     String label1 = o1.label;
186     String label2 = o2.label;
187     if (label1 == null && label2 == null)
188     {
189       return 0;
190     }
191     if (label1 == null)
192     {
193       return -1;
194     }
195     if (label2 == null)
196     {
197       return 1;
198     }
199     return label1.toUpperCase().compareTo(label2.toUpperCase());
200   }
201
202   /**
203    * Comparison based on position of associated sequence (if any) in the
204    * alignment. Returns zero if either argument is null.
205    * 
206    * @param o1
207    * @param o2
208    * @return
209    */
210   private int compareSequences(AlignmentAnnotation o1,
211           AlignmentAnnotation o2)
212   {
213     SequenceI seq1 = o1.sequenceRef;
214     SequenceI seq2 = o2.sequenceRef;
215     if (seq1 == null && seq2 == null)
216     {
217       return 0;
218     }
219     if (seq1 == null)
220     {
221       return 1;
222     }
223     if (seq2 == null)
224     {
225       return -1;
226     }
227     // get sequence index - but note -1 means 'at end' so needs special handling
228     int index1 = AlignmentUtils.getSequenceIndex(alignment, seq1);
229     int index2 = AlignmentUtils.getSequenceIndex(alignment, seq2);
230     if (index1 == index2)
231     {
232       return 0;
233     }
234     if (index1 == -1)
235     {
236       return -1;
237     }
238     if (index2 == -1)
239     {
240       return 1;
241     }
242     return Integer.compare(index1, index2);
243   }
244 }