JAL-1152 annotation sort algorithm optimisation
[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 import java.util.HashMap;
10 import java.util.Map;
11
12 /**
13  * A helper class to sort all annotations associated with an alignment in
14  * various ways.
15  * 
16  * @author gmcarstairs
17  *
18  */
19 public class AnnotationSorter
20 {
21
22   /**
23    * enum for annotation sort options. The text description is used in the
24    * Preferences drop-down options. The enum name is saved in the preferences
25    * file.
26    * 
27    * @author gmcarstairs
28    *
29    */
30   public enum SequenceAnnotationOrder
31   {
32     // Text descriptions surface in the Preferences Sort by... options
33     SEQUENCE_AND_LABEL("Sequence"), LABEL_AND_SEQUENCE("Label"), NONE(
34             "No sort");
35
36     private String description;
37
38     private SequenceAnnotationOrder(String s)
39     {
40       description = s;
41     }
42
43     @Override
44     public String toString()
45     {
46       return description;
47     }
48
49     public static SequenceAnnotationOrder forDescription(String d) {
50       for (SequenceAnnotationOrder order : values())
51       {
52         if (order.toString().equals(d))
53         {
54           return order;
55         }
56       }
57       return null;
58     }
59   }
60
61   // the alignment with respect to which annotations are sorted
62   private final AlignmentI alignment;
63
64   // user preference for placement of non-sequence annotations
65   private boolean showAutocalcAbove;
66
67   // working map of sequence index in alignment
68   private final Map<SequenceI, Integer> sequenceIndices = new HashMap<SequenceI, Integer>();
69
70   /**
71    * Constructor given an alignment and the location (top or bottom) of
72    * Consensus and similar.
73    * 
74    * @param alignmentI
75    * @param showAutocalculatedAbove
76    */
77   public AnnotationSorter(AlignmentI alignmentI,
78           boolean showAutocalculatedAbove)
79   {
80     this.alignment = alignmentI;
81     this.showAutocalcAbove = showAutocalculatedAbove;
82   }
83
84   /**
85    * Default comparator sorts as follows by annotation type within sequence
86    * order:
87    * <ul>
88    * <li>annotations with a reference to a sequence in the alignment are sorted
89    * on sequence ordering</li>
90    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
91    * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
92    * </ul>
93    */
94   private final Comparator<? super AlignmentAnnotation> bySequenceAndLabel = new Comparator<AlignmentAnnotation>()
95   {
96     @Override
97     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
98     {
99       if (o1 == null && o2 == null)
100       {
101         return 0;
102       }
103       if (o1 == null)
104       {
105         return -1;
106       }
107       if (o2 == null)
108       {
109         return 1;
110       }
111
112       /*
113        * Ignore label (keep existing ordering) for
114        * Conservation/Quality/Consensus etc
115        */
116       if (o1.sequenceRef == null && o2.sequenceRef == null)
117       {
118         return 0;
119       }
120       int sequenceOrder = compareSequences(o1, o2);
121       return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
122     }
123   };
124
125   /**
126    * This comparator sorts as follows by sequence order within annotation type
127    * <ul>
128    * <li>annotations with a reference to a sequence in the alignment are sorted
129    * on label (non-case-sensitive)</li>
130    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
131    * <li>within the same label, sort by order of the related sequences</li>
132    * </ul>
133    */
134   private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
135   {
136     @Override
137     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
138     {
139       if (o1 == null && o2 == null)
140       {
141         return 0;
142       }
143       if (o1 == null)
144       {
145         return -1;
146       }
147       if (o2 == null)
148       {
149         return 1;
150       }
151
152       /*
153        * Ignore label (keep existing ordering) for
154        * Conservation/Quality/Consensus etc
155        */
156       if (o1.sequenceRef == null && o2.sequenceRef == null)
157       {
158         return 0;
159       }
160
161       /*
162        * Sort non-sequence-related before or after sequence-related.
163        */
164       if (o1.sequenceRef == null)
165       {
166         return showAutocalcAbove ? -1 : 1;
167       }
168       if (o2.sequenceRef == null)
169       {
170         return showAutocalcAbove ? 1 : -1;
171       }
172       int labelOrder = compareLabels(o1, o2);
173       return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
174     }
175   };
176
177   /**
178    * noSort leaves sort order unchanged, within sequence- and
179    * non-sequence-related annotations, but may switch the ordering of these
180    * groups. Note this is guaranteed (at least in Java 7) as Arrays.sort() is
181    * guaranteed to be 'stable' (not change ordering of equal items).
182    */
183   private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
184   {
185     @Override
186     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
187     {
188       if (o1 != null && o2 != null)
189       {
190         if (o1.sequenceRef == null && o2.sequenceRef != null)
191         {
192           return showAutocalcAbove ? -1 : 1;
193         }
194         if (o1.sequenceRef != null && o2.sequenceRef == null)
195         {
196           return showAutocalcAbove ? 1 : -1;
197         }
198       }
199       return 0;
200     }
201   };
202
203   /**
204    * Sort by the specified ordering of sequence-specific annotations.
205    * 
206    * @param alignmentAnnotations
207    * @param order
208    */
209   public void sort(AlignmentAnnotation[] alignmentAnnotations,
210           SequenceAnnotationOrder order)
211   {
212     // cache 'alignment sequence position' for the annotations
213     saveSequenceIndices(alignmentAnnotations);
214
215     Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
216
217     if (alignmentAnnotations != null)
218     {
219       synchronized (alignmentAnnotations)
220       {
221         Arrays.sort(alignmentAnnotations, comparator);
222       }
223     }
224   }
225
226   /**
227    * Calculate and save in a temporary map the position of each annotation's
228    * sequence (if it has one) in the alignment. Faster to do this once than for
229    * every annotation comparison.
230    * 
231    * @param alignmentAnnotations
232    */
233   private void saveSequenceIndices(
234           AlignmentAnnotation[] alignmentAnnotations)
235   {
236     sequenceIndices.clear();
237     for (AlignmentAnnotation ann : alignmentAnnotations) {
238       SequenceI seq = ann.sequenceRef;
239       if (seq != null) {
240         int index = AlignmentUtils.getSequenceIndex(alignment, seq);
241         sequenceIndices.put(seq, index);
242       }
243     }
244   }
245
246   /**
247    * Get the comparator for the specified sort order.
248    * 
249    * @param order
250    * @return
251    */
252   private Comparator<? super AlignmentAnnotation> getComparator(
253           SequenceAnnotationOrder order)
254   {
255     if (order == null)
256     {
257       return noSort;
258     }
259     switch (order)
260     {
261     case NONE:
262       return this.noSort;
263     case SEQUENCE_AND_LABEL:
264       return this.bySequenceAndLabel;
265     case LABEL_AND_SEQUENCE:
266       return this.byLabelAndSequence;
267     default:
268       throw new UnsupportedOperationException(order.toString());
269     }
270   }
271
272   /**
273    * Non-case-sensitive comparison of annotation labels. Returns zero if either
274    * argument is null.
275    * 
276    * @param o1
277    * @param o2
278    * @return
279    */
280   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
281   {
282     if (o1 == null || o2 == null)
283     {
284       return 0;
285     }
286     String label1 = o1.label;
287     String label2 = o2.label;
288     if (label1 == null && label2 == null)
289     {
290       return 0;
291     }
292     if (label1 == null)
293     {
294       return -1;
295     }
296     if (label2 == null)
297     {
298       return 1;
299     }
300     return label1.toUpperCase().compareTo(label2.toUpperCase());
301   }
302
303   /**
304    * Comparison based on position of associated sequence (if any) in the
305    * alignment. Returns zero if either argument is null.
306    * 
307    * @param o1
308    * @param o2
309    * @return
310    */
311   private int compareSequences(AlignmentAnnotation o1,
312           AlignmentAnnotation o2)
313   {
314     SequenceI seq1 = o1.sequenceRef;
315     SequenceI seq2 = o2.sequenceRef;
316     if (seq1 == null && seq2 == null)
317     {
318       return 0;
319     }
320     /*
321      * Sort non-sequence-related before or after sequence-related.
322      */
323     if (seq1 == null)
324     {
325       return showAutocalcAbove ? -1 : 1;
326     }
327     if (seq2 == null)
328     {
329       return showAutocalcAbove ? 1 : -1;
330     }
331     // get sequence index - but note -1 means 'at end' so needs special handling
332     int index1 = sequenceIndices.get(seq1);
333     int index2 = sequenceIndices.get(seq2);
334     if (index1 == index2)
335     {
336       return 0;
337     }
338     if (index1 == -1)
339     {
340       return -1;
341     }
342     if (index2 == -1)
343     {
344       return 1;
345     }
346     return Integer.compare(index1, index2);
347   }
348 }