f16d9ea24edcbb64c87905da627217fa5a14e4d3
[jalview.git] / src / jalview / analysis / AnnotationSorter.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.analysis;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.SequenceI;
26
27 import java.util.Arrays;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 import java.util.Map;
31
32 /**
33  * A helper class to sort all annotations associated with an alignment in
34  * various ways.
35  * 
36  * @author gmcarstairs
37  *
38  */
39 public class AnnotationSorter
40 {
41   /**
42    * enum for annotation sort options. The text description is used in the
43    * Preferences drop-down options. The enum name is saved in the preferences
44    * file.
45    * 
46    * @author gmcarstairs
47    *
48    */
49   public enum SequenceAnnotationOrder
50   {
51     // Text descriptions surface in the Preferences Sort by... options
52     SEQUENCE_AND_LABEL("Sequence"), LABEL_AND_SEQUENCE("Label"),
53     NONE("No sort"),
54
55     /**
56      * custom is set if user drags to reorder annotations
57      */
58     CUSTOM("Customised");
59
60     private String description;
61
62     private SequenceAnnotationOrder(String s)
63     {
64       description = s;
65     }
66
67     @Override
68     public String toString()
69     {
70       return description;
71     }
72
73     public static SequenceAnnotationOrder forDescription(String d)
74     {
75       for (SequenceAnnotationOrder order : values())
76       {
77         if (order.toString().equals(d))
78         {
79           return order;
80         }
81       }
82       return null;
83     }
84   }
85
86   // the alignment with respect to which annotations are sorted
87   private final AlignmentI alignment;
88
89   // user preference for placement of non-sequence annotations
90   private boolean showAutocalcAbove;
91
92   // working map of sequence index in alignment
93   private final Map<SequenceI, Integer> sequenceIndices = new HashMap<>();
94
95   // if true, sort only repositions auto-calculated annotation (to top or
96   // bottom)
97   private final boolean autocalcOnly;
98
99   /**
100    * Constructor given an alignment and the location (top or bottom) of
101    * Consensus and similar.
102    * 
103    * @param alignmentI
104    * @param showAutocalculatedAbove
105    * @param autoCalcOnly
106    */
107   public AnnotationSorter(AlignmentI alignmentI,
108           boolean showAutocalculatedAbove, boolean autoCalcOnly)
109   {
110     this.alignment = alignmentI;
111     this.showAutocalcAbove = showAutocalculatedAbove;
112     this.autocalcOnly = autoCalcOnly;
113   }
114
115   /**
116    * Default comparator sorts as follows by annotation type within sequence
117    * order:
118    * <ul>
119    * <li>annotations with a reference to a sequence in the alignment are sorted
120    * on sequence ordering</li>
121    * <li>other annotations go 'at the end', with their mutual order
122    * unchanged</li>
123    * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
124    * </ul>
125    */
126   private final Comparator<? super AlignmentAnnotation> bySequenceAndLabel = new Comparator<AlignmentAnnotation>()
127   {
128     @Override
129     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
130     {
131       if (o1 == null && o2 == null)
132       {
133         return 0;
134       }
135       if (o1 == null)
136       {
137         return -1;
138       }
139       if (o2 == null)
140       {
141         return 1;
142       }
143
144       // TODO how to treat sequence-related autocalculated annotation
145       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
146       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
147       /*
148        * Ignore label (keep existing ordering) for
149        * Conservation/Quality/Consensus etc
150        */
151       if (o1auto && o2auto)
152       {
153         return 0;
154       }
155
156       /*
157        * Sort autocalculated before or after sequence-related.
158        */
159       if (o1auto)
160       {
161         return showAutocalcAbove ? -1 : 1;
162       }
163       if (o2auto)
164       {
165         return showAutocalcAbove ? 1 : -1;
166       }
167       if (autocalcOnly)
168       {
169         return 0; // don't reorder other annotations
170       }
171       int sequenceOrder = compareSequences(o1, o2);
172       return sequenceOrder == 0 ? compareLabels(o1, o2) : sequenceOrder;
173     }
174
175     @Override
176     public String toString()
177     {
178       return "Sort by sequence and label";
179     }
180   };
181
182   /**
183    * This comparator sorts as follows by sequence order within annotation type
184    * <ul>
185    * <li>annotations with a reference to a sequence in the alignment are sorted
186    * on label (non-case-sensitive)</li>
187    * <li>other annotations go 'at the end', with their mutual order
188    * unchanged</li>
189    * <li>within the same label, sort by order of the related sequences</li>
190    * </ul>
191    */
192   private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
193   {
194     @Override
195     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
196     {
197       if (o1 == null && o2 == null)
198       {
199         return 0;
200       }
201       if (o1 == null)
202       {
203         return -1;
204       }
205       if (o2 == null)
206       {
207         return 1;
208       }
209
210       // TODO how to treat sequence-related autocalculated annotation
211       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
212       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
213       /*
214        * Ignore label (keep existing ordering) for
215        * Conservation/Quality/Consensus etc
216        */
217       if (o1auto && o2auto)
218       {
219         return 0;
220       }
221
222       /*
223        * Sort autocalculated before or after sequence-related.
224        */
225       if (o1auto)
226       {
227         return showAutocalcAbove ? -1 : 1;
228       }
229       if (o2auto)
230       {
231         return showAutocalcAbove ? 1 : -1;
232       }
233       if (autocalcOnly)
234       {
235         return 0; // don't reorder other annotations
236       }
237       int labelOrder = compareLabels(o1, o2);
238       return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
239     }
240
241     @Override
242     public String toString()
243     {
244       return "Sort by label and sequence";
245     }
246   };
247
248   /**
249    * noSort leaves sort order unchanged, within sequence- and autocalculated
250    * annotations, but may switch the ordering of these groups. Note this is
251    * guaranteed (at least in Java 7) as Arrays.sort() is guaranteed to be
252    * 'stable' (not change ordering of equal items).
253    */
254   private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
255   {
256     @Override
257     public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
258     {
259       // TODO how to treat sequence-related autocalculated annotation
260       boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
261       boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
262       // TODO skip this test to allow customised ordering of all annotations
263       // - needs a third option: place autocalculated first / last / none
264       if (o1 != null && o2 != null)
265       {
266         if (o1auto && !o2auto)
267         {
268           return showAutocalcAbove ? -1 : 1;
269         }
270         if (!o1auto && o2auto)
271         {
272           return showAutocalcAbove ? 1 : -1;
273         }
274       }
275       return 0;
276     }
277
278     @Override
279     public String toString()
280     {
281       return "No sort";
282     }
283   };
284
285   /**
286    * Sort by the specified ordering of sequence-specific annotations.
287    * 
288    * @param alignmentAnnotations
289    * @param order
290    */
291   public void sort(AlignmentAnnotation[] alignmentAnnotations,
292           SequenceAnnotationOrder order)
293   {
294     if (alignmentAnnotations == null
295             || order == SequenceAnnotationOrder.CUSTOM)
296     {
297       return;
298     }
299
300     /*
301      * cache 'alignment sequence positions' if required for sorting
302      */
303     if (order == SequenceAnnotationOrder.SEQUENCE_AND_LABEL
304             || order == SequenceAnnotationOrder.LABEL_AND_SEQUENCE)
305     {
306       saveSequenceIndices(alignmentAnnotations);
307     }
308
309     Comparator<? super AlignmentAnnotation> comparator = getComparator(
310             order);
311
312     if (alignmentAnnotations != null)
313     {
314       synchronized (alignmentAnnotations)
315       {
316         Arrays.sort(alignmentAnnotations, comparator);
317       }
318     }
319   }
320
321   /**
322    * Calculate and save in a temporary map the position of each annotation's
323    * sequence (if it has one) in the alignment. Faster to do this once than for
324    * every annotation comparison.
325    * 
326    * @param alignmentAnnotations
327    */
328   private void saveSequenceIndices(
329           AlignmentAnnotation[] alignmentAnnotations)
330   {
331     sequenceIndices.clear();
332
333     Map<SequenceI, Integer> seqPositions = alignment.getSequencePositions();
334
335     for (AlignmentAnnotation ann : alignmentAnnotations)
336     {
337       SequenceI seq = ann.sequenceRef;
338       if (seq != null)
339       {
340         Integer index = seqPositions.get(seq);
341         if (index != null)
342         {
343           sequenceIndices.put(seq, index);
344         }
345       }
346     }
347   }
348
349   /**
350    * Get the comparator for the specified sort order.
351    * 
352    * @param order
353    * @return
354    */
355   private Comparator<? super AlignmentAnnotation> getComparator(
356           SequenceAnnotationOrder order)
357   {
358     if (order == null)
359     {
360       return noSort;
361     }
362     switch (order)
363     {
364     case NONE:
365       return this.noSort;
366     case SEQUENCE_AND_LABEL:
367       return this.bySequenceAndLabel;
368     case LABEL_AND_SEQUENCE:
369       return this.byLabelAndSequence;
370     default:
371       throw new UnsupportedOperationException(order.toString());
372     }
373   }
374
375   /**
376    * Non-case-sensitive comparison of annotation labels. Returns zero if either
377    * argument is null.
378    * 
379    * @param o1
380    * @param o2
381    * @return
382    */
383   private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
384   {
385     if (o1 == null || o2 == null)
386     {
387       return 0;
388     }
389     String label1 = o1.label;
390     String label2 = o2.label;
391     if (label1 == null && label2 == null)
392     {
393       return 0;
394     }
395     if (label1 == null)
396     {
397       return -1;
398     }
399     if (label2 == null)
400     {
401       return 1;
402     }
403     return label1.toUpperCase().compareTo(label2.toUpperCase());
404   }
405
406   /**
407    * Comparison based on position of associated sequence (if any) in the
408    * alignment
409    * 
410    * @param o1
411    * @param o2
412    * @return
413    */
414   private int compareSequences(AlignmentAnnotation o1,
415           AlignmentAnnotation o2)
416   {
417     SequenceI seq1 = o1.sequenceRef;
418     SequenceI seq2 = o2.sequenceRef;
419     if (seq1 == null && seq2 == null)
420     {
421       return 0;
422     }
423
424     /*
425      * Sort non-sequence-related before or after sequence-related
426      */
427     if (seq1 == null)
428     {
429       return showAutocalcAbove ? -1 : 1;
430     }
431     if (seq2 == null)
432     {
433       return showAutocalcAbove ? 1 : -1;
434     }
435
436     /*
437      * else sort by associated sequence position
438      */
439     Integer index1 = sequenceIndices.get(seq1);
440     Integer index2 = sequenceIndices.get(seq2);
441     if (index1 == null)
442     {
443       return index2 == null ? 0 : -1;
444     }
445     if (index2 == null)
446     {
447       return 1;
448     }
449     return Integer.compare(index1.intValue(), index2.intValue());
450   }
451 }