JAL-1152 check 'autocalculated' not 'null sequenceRef'; sort by sequence
[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       // TODO how to treat sequence-related autocalculated annotation
113       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
114       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
115       /*
116        * Ignore label (keep existing ordering) for
117        * Conservation/Quality/Consensus etc
118        */
119       if (o1auto && o2auto)
120       {
121         return 0;
122       }
123
124       /*
125        * Sort autocalculated before or after sequence-related.
126        */
127       if (o1auto)
128       {
129         return showAutocalcAbove ? -1 : 1;
130       }
131       if (o2auto)
132       {
133         return showAutocalcAbove ? 1 : -1;
134       }
135       int sequenceOrder = compareSequences(o1, o2);
136       return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
137     }
138
139     @Override
140     public String toString()
141     {
142       return "Sort by sequence and label";
143     }
144   };
145
146   /**
147    * This comparator sorts as follows by sequence order within annotation type
148    * <ul>
149    * <li>annotations with a reference to a sequence in the alignment are sorted
150    * on label (non-case-sensitive)</li>
151    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
152    * <li>within the same label, sort by order of the related sequences</li>
153    * </ul>
154    */
155   private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
156   {
157     @Override
158     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
159     {
160       if (o1 == null && o2 == null)
161       {
162         return 0;
163       }
164       if (o1 == null)
165       {
166         return -1;
167       }
168       if (o2 == null)
169       {
170         return 1;
171       }
172
173       // TODO how to treat sequence-related autocalculated annotation
174       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
175       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
176       /*
177        * Ignore label (keep existing ordering) for
178        * Conservation/Quality/Consensus etc
179        */
180       if (o1auto && o2auto)
181       {
182         return 0;
183       }
184
185       /*
186        * Sort autocalculated before or after sequence-related.
187        */
188       if (o1auto)
189       {
190         return showAutocalcAbove ? -1 : 1;
191       }
192       if (o2auto)
193       {
194         return showAutocalcAbove ? 1 : -1;
195       }
196       int labelOrder = compareLabels(o1, o2);
197       return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
198     }
199
200     @Override
201     public String toString()
202     {
203       return "Sort by label and sequence";
204     }
205   };
206
207   /**
208    * noSort leaves sort order unchanged, within sequence- and autocalculated
209    * annotations, but may switch the ordering of these groups. Note this is
210    * guaranteed (at least in Java 7) as Arrays.sort() is guaranteed to be
211    * 'stable' (not change ordering of equal items).
212    */
213   private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
214   {
215     @Override
216     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
217     {
218       // TODO how to treat sequence-related autocalculated annotation
219       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
220       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
221       // TODO skip this test to allow customised ordering of all annotations
222       // - needs a third option: place autocalculated first / last / none
223       if (o1 != null && o2 != null)
224       {
225         if (o1auto && !o2auto)
226         {
227           return showAutocalcAbove ? -1 : 1;
228         }
229         if (!o1auto && o2auto)
230         {
231           return showAutocalcAbove ? 1 : -1;
232         }
233       }
234       return 0;
235     }
236
237     @Override
238     public String toString()
239     {
240       return "No sort";
241     }
242   };
243
244   /**
245    * Sort by the specified ordering of sequence-specific annotations.
246    * 
247    * @param alignmentAnnotations
248    * @param order
249    */
250   public void sort(AlignmentAnnotation[] alignmentAnnotations,
251           SequenceAnnotationOrder order)
252   {
253     if (alignmentAnnotations == null)
254     {
255       return;
256     }
257     // cache 'alignment sequence position' for the annotations
258     saveSequenceIndices(alignmentAnnotations);
259
260     Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
261
262     if (alignmentAnnotations != null)
263     {
264       synchronized (alignmentAnnotations)
265       {
266         Arrays.sort(alignmentAnnotations, comparator);
267       }
268     }
269   }
270
271   /**
272    * Calculate and save in a temporary map the position of each annotation's
273    * sequence (if it has one) in the alignment. Faster to do this once than for
274    * every annotation comparison.
275    * 
276    * @param alignmentAnnotations
277    */
278   private void saveSequenceIndices(
279           AlignmentAnnotation[] alignmentAnnotations)
280   {
281     sequenceIndices.clear();
282     for (AlignmentAnnotation ann : alignmentAnnotations) {
283       SequenceI seq = ann.sequenceRef;
284       if (seq != null) {
285         int index = AlignmentUtils.getSequenceIndex(alignment, seq);
286         sequenceIndices.put(seq, index);
287       }
288     }
289   }
290
291   /**
292    * Get the comparator for the specified sort order.
293    * 
294    * @param order
295    * @return
296    */
297   private Comparator<? super AlignmentAnnotation> getComparator(
298           SequenceAnnotationOrder order)
299   {
300     if (order == null)
301     {
302       return noSort;
303     }
304     switch (order)
305     {
306     case NONE:
307       return this.noSort;
308     case SEQUENCE_AND_LABEL:
309       return this.bySequenceAndLabel;
310     case LABEL_AND_SEQUENCE:
311       return this.byLabelAndSequence;
312     default:
313       throw new UnsupportedOperationException(order.toString());
314     }
315   }
316
317   /**
318    * Non-case-sensitive comparison of annotation labels. Returns zero if either
319    * argument is null.
320    * 
321    * @param o1
322    * @param o2
323    * @return
324    */
325   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
326   {
327     if (o1 == null || o2 == null)
328     {
329       return 0;
330     }
331     String label1 = o1.label;
332     String label2 = o2.label;
333     if (label1 == null && label2 == null)
334     {
335       return 0;
336     }
337     if (label1 == null)
338     {
339       return -1;
340     }
341     if (label2 == null)
342     {
343       return 1;
344     }
345     return label1.toUpperCase().compareTo(label2.toUpperCase());
346   }
347
348   /**
349    * Comparison based on position of associated sequence (if any) in the
350    * alignment. Returns zero if either argument is null.
351    * 
352    * @param o1
353    * @param o2
354    * @return
355    */
356   private int compareSequences(AlignmentAnnotation o1,
357           AlignmentAnnotation o2)
358   {
359     SequenceI seq1 = o1.sequenceRef;
360     SequenceI seq2 = o2.sequenceRef;
361     if (seq1 == null && seq2 == null)
362     {
363       return 0;
364     }
365     /*
366      * Sort non-sequence-related before or after sequence-related.
367      */
368     if (seq1 == null)
369     {
370       return showAutocalcAbove ? -1 : 1;
371     }
372     if (seq2 == null)
373     {
374       return showAutocalcAbove ? 1 : -1;
375     }
376     // get sequence index - but note -1 means 'at end' so needs special handling
377     int index1 = sequenceIndices.get(seq1);
378     int index2 = sequenceIndices.get(seq2);
379     if (index1 == index2)
380     {
381       return 0;
382     }
383     if (index1 == -1)
384     {
385       return -1;
386     }
387     if (index2 == -1)
388     {
389       return 1;
390     }
391     return Integer.compare(index1, index2);
392   }
393 }