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