JAL-1152 bug fix for 'no sort' case + tests for autocalc placement
[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 SequenceAnnotationOrder
21   {
22     SEQUENCE_AND_LABEL, LABEL_AND_SEQUENCE, NONE
23   }
24
25   private final AlignmentI alignment;
26
27   private boolean showAutocalcAbove;
28
29   /**
30    * Constructor given an alignment and the location (top or bottom) of
31    * Consensus and similar.
32    * 
33    * @param alignmentI
34    * @param showAutocalculatedAbove
35    */
36   public AnnotationSorter(AlignmentI alignmentI,
37           boolean showAutocalculatedAbove)
38   {
39     this.alignment = alignmentI;
40     this.showAutocalcAbove = showAutocalculatedAbove;
41   }
42
43   /**
44    * Default comparator sorts as follows by annotation type within sequence
45    * order:
46    * <ul>
47    * <li>annotations with a reference to a sequence in the alignment are sorted
48    * on sequence ordering</li>
49    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
50    * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
51    * </ul>
52    */
53   private final Comparator<? super AlignmentAnnotation> bySequenceAndLabel = new Comparator<AlignmentAnnotation>()
54   {
55     @Override
56     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
57     {
58       if (o1 == null && o2 == null)
59       {
60         return 0;
61       }
62       if (o1 == null)
63       {
64         return -1;
65       }
66       if (o2 == null)
67       {
68         return 1;
69       }
70
71       /*
72        * Ignore label (keep existing ordering) for
73        * Conservation/Quality/Consensus etc
74        */
75       if (o1.sequenceRef == null && o2.sequenceRef == null)
76       {
77         return 0;
78       }
79       int sequenceOrder = compareSequences(o1, o2);
80       return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
81     }
82   };
83
84   /**
85    * This comparator sorts as follows by sequence order within annotation type
86    * <ul>
87    * <li>annotations with a reference to a sequence in the alignment are sorted
88    * on label (non-case-sensitive)</li>
89    * <li>other annotations go 'at the end', with their mutual order unchanged</li>
90    * <li>within the same label, sort by order of the related sequences</li>
91    * </ul>
92    */
93   private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
94   {
95     @Override
96     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
97     {
98       if (o1 == null && o2 == null)
99       {
100         return 0;
101       }
102       if (o1 == null)
103       {
104         return -1;
105       }
106       if (o2 == null)
107       {
108         return 1;
109       }
110
111       /*
112        * Ignore label (keep existing ordering) for
113        * Conservation/Quality/Consensus etc
114        */
115       if (o1.sequenceRef == null && o2.sequenceRef == null)
116       {
117         return 0;
118       }
119
120       /*
121        * Sort non-sequence-related before or after sequence-related.
122        */
123       if (o1.sequenceRef == null)
124       {
125         return showAutocalcAbove ? -1 : 1;
126       }
127       if (o2.sequenceRef == null)
128       {
129         return showAutocalcAbove ? 1 : -1;
130       }
131       int labelOrder = compareLabels(o1, o2);
132       return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
133     }
134   };
135
136   /**
137    * noSort leaves sort order unchanged, within sequence- and
138    * non-sequence-related annotations, but may switch the ordering of these
139    * groups. Note this is guaranteed (at least in Java 7) as Arrays.sort() is
140    * guaranteed to be 'stable' (not change ordering of equal items).
141    */
142   private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
143   {
144     @Override
145     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
146     {
147       if (o1 != null && o2 != null)
148       {
149         if (o1.sequenceRef == null && o2.sequenceRef != null)
150         {
151           return showAutocalcAbove ? -1 : 1;
152         }
153         if (o1.sequenceRef != null && o2.sequenceRef == null)
154         {
155           return showAutocalcAbove ? 1 : -1;
156         }
157       }
158       return 0;
159     }
160   };
161
162   /**
163    * Sort by the specified ordering of sequence-specific annotations.
164    * 
165    * @param alignmentAnnotations
166    * @param order
167    */
168   public void sort(AlignmentAnnotation[] alignmentAnnotations,
169           SequenceAnnotationOrder order)
170   {
171     Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
172
173     if (alignmentAnnotations != null)
174     {
175       synchronized (alignmentAnnotations)
176       {
177         Arrays.sort(alignmentAnnotations, comparator);
178       }
179     }
180   }
181
182   /**
183    * Get the comparator for the specified sort order.
184    * 
185    * @param order
186    * @return
187    */
188   private Comparator<? super AlignmentAnnotation> getComparator(
189           SequenceAnnotationOrder order)
190   {
191     if (order == null)
192     {
193       return noSort;
194     }
195     switch (order)
196     {
197     case NONE:
198       return this.noSort;
199     case SEQUENCE_AND_LABEL:
200       return this.bySequenceAndLabel;
201     case LABEL_AND_SEQUENCE:
202       return this.byLabelAndSequence;
203     default:
204       throw new UnsupportedOperationException(order.toString());
205     }
206   }
207
208   /**
209    * Non-case-sensitive comparison of annotation labels. Returns zero if either
210    * argument is null.
211    * 
212    * @param o1
213    * @param o2
214    * @return
215    */
216   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
217   {
218     if (o1 == null || o2 == null)
219     {
220       return 0;
221     }
222     String label1 = o1.label;
223     String label2 = o2.label;
224     if (label1 == null && label2 == null)
225     {
226       return 0;
227     }
228     if (label1 == null)
229     {
230       return -1;
231     }
232     if (label2 == null)
233     {
234       return 1;
235     }
236     return label1.toUpperCase().compareTo(label2.toUpperCase());
237   }
238
239   /**
240    * Comparison based on position of associated sequence (if any) in the
241    * alignment. Returns zero if either argument is null.
242    * 
243    * @param o1
244    * @param o2
245    * @return
246    */
247   private int compareSequences(AlignmentAnnotation o1,
248           AlignmentAnnotation o2)
249   {
250     SequenceI seq1 = o1.sequenceRef;
251     SequenceI seq2 = o2.sequenceRef;
252     if (seq1 == null && seq2 == null)
253     {
254       return 0;
255     }
256     /*
257      * Sort non-sequence-related before or after sequence-related.
258      */
259     if (seq1 == null)
260     {
261       return showAutocalcAbove ? -1 : 1;
262     }
263     if (seq2 == null)
264     {
265       return showAutocalcAbove ? 1 : -1;
266     }
267     // get sequence index - but note -1 means 'at end' so needs special handling
268     int index1 = AlignmentUtils.getSequenceIndex(alignment, seq1);
269     int index2 = AlignmentUtils.getSequenceIndex(alignment, seq2);
270     if (index1 == index2)
271     {
272       return 0;
273     }
274     if (index1 == -1)
275     {
276       return -1;
277     }
278     if (index2 == -1)
279     {
280       return 1;
281     }
282     return Integer.compare(index1, index2);
283   }
284 }