JAL-1807 Bob's JalviewJS prototype first commit
[jalviewjs.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     if (alignmentAnnotations == null)
213     {
214       return;
215     }
216     // cache 'alignment sequence position' for the annotations
217     saveSequenceIndices(alignmentAnnotations);
218
219     Comparator<? super AlignmentAnnotation> comparator = getComparator(order);
220
221     if (alignmentAnnotations != null)
222     {
223       synchronized (alignmentAnnotations)
224       {
225         Arrays.sort(alignmentAnnotations, comparator);
226       }
227     }
228   }
229
230   /**
231    * Calculate and save in a temporary map the position of each annotation's
232    * sequence (if it has one) in the alignment. Faster to do this once than for
233    * every annotation comparison.
234    * 
235    * @param alignmentAnnotations
236    */
237   private void saveSequenceIndices(
238           AlignmentAnnotation[] alignmentAnnotations)
239   {
240     sequenceIndices.clear();
241     for (AlignmentAnnotation ann : alignmentAnnotations) {
242       SequenceI seq = ann.sequenceRef;
243       if (seq != null) {
244         int index = AlignmentUtils.getSequenceIndex(alignment, seq);
245         sequenceIndices.put(seq, index);
246       }
247     }
248   }
249
250   /**
251    * Get the comparator for the specified sort order.
252    * 
253    * @param order
254    * @return
255    */
256   private Comparator<? super AlignmentAnnotation> getComparator(
257           SequenceAnnotationOrder order)
258   {
259     if (order == null)
260     {
261       return noSort;
262     }
263     switch (order)
264     {
265     case NONE:
266       return this.noSort;
267     case SEQUENCE_AND_LABEL:
268       return this.bySequenceAndLabel;
269     case LABEL_AND_SEQUENCE:
270       return this.byLabelAndSequence;
271     default:
272       throw new UnsupportedOperationException(order.toString());
273     }
274   }
275
276   /**
277    * Non-case-sensitive comparison of annotation labels. Returns zero if either
278    * argument is null.
279    * 
280    * @param o1
281    * @param o2
282    * @return
283    */
284   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
285   {
286     if (o1 == null || o2 == null)
287     {
288       return 0;
289     }
290     String label1 = o1.label;
291     String label2 = o2.label;
292     if (label1 == null && label2 == null)
293     {
294       return 0;
295     }
296     if (label1 == null)
297     {
298       return -1;
299     }
300     if (label2 == null)
301     {
302       return 1;
303     }
304     return label1.toUpperCase().compareTo(label2.toUpperCase());
305   }
306
307   /**
308    * Comparison based on position of associated sequence (if any) in the
309    * alignment. Returns zero if either argument is null.
310    * 
311    * @param o1
312    * @param o2
313    * @return
314    */
315   private int compareSequences(AlignmentAnnotation o1,
316           AlignmentAnnotation o2)
317   {
318     SequenceI seq1 = o1.sequenceRef;
319     SequenceI seq2 = o2.sequenceRef;
320     if (seq1 == null && seq2 == null)
321     {
322       return 0;
323     }
324     /*
325      * Sort non-sequence-related before or after sequence-related.
326      */
327     if (seq1 == null)
328     {
329       return showAutocalcAbove ? -1 : 1;
330     }
331     if (seq2 == null)
332     {
333       return showAutocalcAbove ? 1 : -1;
334     }
335     // get sequence index - but note -1 means 'at end' so needs special handling
336     int index1 = sequenceIndices.get(seq1);
337     int index2 = sequenceIndices.get(seq2);
338     if (index1 == index2)
339     {
340       return 0;
341     }
342     if (index1 == -1)
343     {
344       return -1;
345     }
346     if (index2 == -1)
347     {
348       return 1;
349     }
350     return Integer.compare(index1, index2);
351   }
352 }