Clover icon

Coverage Report

  1. Project Clover database Wed Nov 12 2025 13:01:44 GMT
  2. Package jalview.analysis

File AnnotationSorter.java

 

Coverage histogram

../../img/srcFileCovDistChart6.png
37% of files have more coverage

Code metrics

102
146
19
2
600
364
90
0.62
7.68
9.5
4.74

Classes

Class Line # Actions
AnnotationSorter 47 140 86
0.5898437559%
AnnotationSorter.SequenceAnnotationOrder 58 6 4
0.1818181918.2%
 

Contributing tests

This file is covered by 233 tests. .

Source view

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 java.util.Locale;
24   
25    import jalview.datamodel.AlignmentAnnotation;
26    import jalview.datamodel.AlignmentI;
27    import jalview.datamodel.BinaryNode;
28    import jalview.datamodel.SequenceI;
29    import jalview.datamodel.SequenceNode;
30   
31    import java.util.ArrayList;
32    import java.util.Arrays;
33    import java.util.Collections;
34    import java.util.Comparator;
35    import java.util.HashMap;
36    import java.util.LinkedHashSet;
37    import java.util.List;
38    import java.util.Map;
39   
40    /**
41    * A helper class to sort all annotations associated with an alignment in
42    * various ways.
43    *
44    * @author gmcarstairs
45    *
46    */
 
47    public class AnnotationSorter
48    {
49   
50    /**
51    * enum for annotation sort options. The text description is used in the
52    * Preferences drop-down options. The enum name is saved in the preferences
53    * file.
54    *
55    * @author gmcarstairs
56    *
57    */
 
58    public enum SequenceAnnotationOrder
59    {
60    // Text descriptions surface in the Preferences Sort by... options
61    SEQUENCE_AND_LABEL("Sequence"), LABEL_AND_SEQUENCE("Label"),
62    NONE("No sort");
63   
64    private String description;
65   
 
66  162 toggle private SequenceAnnotationOrder(String s)
67    {
68  162 description = s;
69    }
70   
 
71  0 toggle @Override
72    public String toString()
73    {
74  0 return description;
75    }
76   
 
77  0 toggle public static SequenceAnnotationOrder forDescription(String d)
78    {
79  0 for (SequenceAnnotationOrder order : values())
80    {
81  0 if (order.toString().equals(d))
82    {
83  0 return order;
84    }
85    }
86  0 return null;
87    }
88    }
89   
90    // the alignment with respect to which annotations are sorted
91    private final AlignmentI alignment;
92   
93    // user preference for placement of non-sequence annotations
94    private boolean showAutocalcAbove;
95   
96    // working map of sequence index in alignment
97    private final Map<SequenceI, Integer> sequenceIndices = new HashMap<SequenceI, Integer>();
98   
99    static TreeModel lastTree = null;
100   
101    static boolean sortTreeAscending = true;
102    /**
103    * Constructor given an alignment and the location (top or bottom) of
104    * Consensus and similar.
105    *
106    * @param alignmentI
107    * @param showAutocalculatedAbove
108    */
 
109  7662 toggle public AnnotationSorter(AlignmentI alignmentI,
110    boolean showAutocalculatedAbove)
111    {
112  7662 this.alignment = alignmentI;
113  7662 this.showAutocalcAbove = showAutocalculatedAbove;
114    }
115   
116    /**
117    * Default comparator sorts as follows by annotation type within sequence
118    * order:
119    * <ul>
120    * <li>annotations with a reference to a sequence in the alignment are sorted
121    * on sequence ordering</li>
122    * <li>other annotations go 'at the end', with their mutual order
123    * unchanged</li>
124    * <li>within the same sequence ref, sort by label (non-case-sensitive)</li>
125    * </ul>
126    */
127    private final Comparator<? super AlignmentAnnotation> bySequenceAndLabel = new Comparator<AlignmentAnnotation>()
128    {
 
129  170536 toggle @Override
130    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
131    {
132  170536 if (o1 == null && o2 == null)
133    {
134  0 return 0;
135    }
136  170536 if (o1 == null)
137    {
138  0 return -1;
139    }
140  170536 if (o2 == null)
141    {
142  0 return 1;
143    }
144   
145    // TODO how to treat sequence-related autocalculated annotation
146  170536 boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
147  170536 boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
148    /*
149    * Ignore label (keep existing ordering) for
150    * Conservation/Quality/Consensus etc
151    */
152  170536 if (o1auto && o2auto)
153    {
154  2 return 0;
155    }
156   
157    /*
158    * Sort autocalculated before or after sequence-related.
159    */
160  170534 if (o1auto)
161    {
162  7 return showAutocalcAbove ? -1 : 1;
163    }
164  170527 if (o2auto)
165    {
166  1 return showAutocalcAbove ? 1 : -1;
167    }
168  170526 int computedOrder = compareSequences(o1, o2);
169  170526 if (computedOrder == 0)
170    {
171  13930 computedOrder = compareLabels(o1, o2);
172    }
173  170526 if (computedOrder == 0)
174    {
175  973 computedOrder = compareDescriptions(o1, o2);
176    }
177  170526 return computedOrder;
178    }
179   
 
180  0 toggle @Override
181    public String toString()
182    {
183  0 return "Sort by sequence and label";
184    }
185    };
186   
187    /**
188    * This comparator sorts as follows by sequence order within annotation type
189    * <ul>
190    * <li>annotations with a reference to a sequence in the alignment are sorted
191    * on label (non-case-sensitive)</li>
192    * <li>other annotations go 'at the end', with their mutual order
193    * unchanged</li>
194    * <li>within the same label, sort by order of the related sequences</li>
195    * </ul>
196    */
197    private final Comparator<? super AlignmentAnnotation> byLabelAndSequence = new Comparator<AlignmentAnnotation>()
198    {
 
199  172006 toggle @Override
200    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
201    {
202  172006 if (o1 == null && o2 == null)
203    {
204  0 return 0;
205    }
206  172006 if (o1 == null)
207    {
208  0 return -1;
209    }
210  172006 if (o2 == null)
211    {
212  0 return 1;
213    }
214   
215    // TODO how to treat sequence-related autocalculated annotation
216  172006 boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
217  172006 boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
218    /*
219    * Ignore label (keep existing ordering) for
220    * Conservation/Quality/Consensus etc
221    */
222  172006 if (o1auto && o2auto)
223    {
224  2 return 0;
225    }
226   
227    /*
228    * Sort autocalculated before or after sequence-related.
229    */
230  172004 if (o1auto)
231    {
232  7 return showAutocalcAbove ? -1 : 1;
233    }
234  171997 if (o2auto)
235    {
236  1 return showAutocalcAbove ? 1 : -1;
237    }
238  171996 int labelOrder = compareLabels(o1, o2);
239  171996 return labelOrder == 0 ? compareSequences(o1, o2) : labelOrder;
240    }
241   
 
242  0 toggle @Override
243    public String toString()
244    {
245  0 return "Sort by label and sequence";
246    }
247    };
248   
249    /**
250    * noSort leaves sort order unchanged, within sequence- and autocalculated
251    * annotations, but may switch the ordering of these groups. Note this is
252    * guaranteed (at least in Java 7) as Arrays.sort() is guaranteed to be
253    * 'stable' (not change ordering of equal items).
254    */
255    private Comparator<? super AlignmentAnnotation> noSort = new Comparator<AlignmentAnnotation>()
256    {
 
257  33068 toggle @Override
258    public int compare(AlignmentAnnotation o1, AlignmentAnnotation o2)
259    {
260    // TODO how to treat sequence-related autocalculated annotation
261  33068 boolean o1auto = o1.autoCalculated && o1.sequenceRef == null;
262  33068 boolean o2auto = o2.autoCalculated && o2.sequenceRef == null;
263    // TODO skip this test to allow customised ordering of all annotations
264    // - needs a third option: place autocalculated first / last / none
265  33068 if (o1 != null && o2 != null)
266    {
267  33068 if (o1auto && !o2auto)
268    {
269  2024 return showAutocalcAbove ? -1 : 1;
270    }
271  31044 if (!o1auto && o2auto)
272    {
273  523 return showAutocalcAbove ? 1 : -1;
274    }
275    }
276  30521 return 0;
277    }
278   
 
279  0 toggle @Override
280    public String toString()
281    {
282  0 return "No sort";
283    }
284    };
285   
286    /**
287    * Sort by the specified ordering of sequence-specific annotations.
288    *
289    * @param alignmentAnnotations
290    * @param order
291    */
 
292  7668 toggle public void sort(AlignmentAnnotation[] alignmentAnnotations,
293    SequenceAnnotationOrder order)
294    {
295  7668 if (alignmentAnnotations == null)
296    {
297  0 return;
298    }
299    // cache 'alignment sequence position' for the annotations
300  7668 saveSequenceIndices(alignmentAnnotations);
301   
302  7666 Comparator<? super AlignmentAnnotation> comparator = getComparator(
303    order);
304   
305  7668 if (alignmentAnnotations != null)
306    {
307  7667 synchronized (alignmentAnnotations)
308    {
309  7668 Arrays.sort(alignmentAnnotations, comparator);
310    }
311    }
312    }
313   
314    /**
315    * Sorts the given array of sequence related (not auto calculated)
316    * AlignmentAnnotation objects based on the order specified by the
317    * provided TreeModel. Any annotations not present in the
318    * tree order will be appended at the end in their original order. The
319    * ordering is reversed for every alternate method call.
320    *
321    * @param alignmentAnnotations
322    * the array of AlignmentAnnotation objects to be sorted. This array
323    * is updated with the new order.
324    * @param tree
325    * the TreeModel representing the desired order to sort by.
326    */
 
327  0 toggle public void sortByTree(AlignmentAnnotation[] alignmentAnnotations,
328    TreeModel tree)
329    {
330  0 if(tree == null || alignmentAnnotations == null || alignmentAnnotations.length < 1) {
331  0 return;
332    }
333   
334    // If the previous sorting was not based on the current tree,
335    // then set the sorting to ascending order. Descending order otherwise.
336  0 if (lastTree != tree)
337    {
338  0 sortTreeAscending = true;
339  0 lastTree = tree;
340    }
341    // If the previous sorting was based on the current tree,
342    // then reverse the ordering.
343    else
344    {
345  0 sortTreeAscending = !sortTreeAscending;
346    }
347   
348    // Get annotation order from the tree in ascending order
349  0 List<AlignmentAnnotation> annotOrderByTree = new ArrayList<>();
350  0 annotOrderByTree = _sortByTree(tree.getTopNode(), annotOrderByTree);
351   
352    // Create a LinkedHashSet to retain order and avoid duplicates
353  0 LinkedHashSet<AlignmentAnnotation> sortedSet = new LinkedHashSet<>();
354   
355  0 if (annotOrderByTree != null && !annotOrderByTree.isEmpty())
356    {
357   
358    // Reverse the annotation order from the tree if not ascending
359  0 if (!sortTreeAscending)
360    {
361  0 Collections.reverse(annotOrderByTree);
362    }
363   
364    // Add annotations from annotOrderByTree to the sorted set
365  0 for (AlignmentAnnotation aa : annotOrderByTree)
366    {
367  0 sortedSet.add(aa);
368    }
369   
370  0 int index = 0;
371   
372    // Add the rest from alignmentAnnotations (if not already added)
373  0 for (AlignmentAnnotation aa : alignmentAnnotations)
374    {
375    //Add auto calculated annotations first in the original order
376    // in the original array
377  0 if(aa.autoCalculated) {
378  0 alignmentAnnotations[index++] = aa;
379    }
380    //Add the rest into sorted set if not already added
381    else {
382  0 sortedSet.add(aa);
383    }
384    }
385   
386    // Update the original array with the sorted annotations set
387    // The annotations in sorted set are not auto calculated
388    // Added in the original array followed by auto calculated annotations
389  0 for (AlignmentAnnotation aa : sortedSet)
390    {
391  0 alignmentAnnotations[index++] = aa;
392    }
393   
394    }
395    }
396   
397   
398    /**
399    * Recursively traverses a binary tree and collects AlignmentAnnotation objects
400    * into a list following the tree's structure. The method avoids duplicate
401    * annotations.
402    *
403    * @param node The current node in the binary tree.
404    * @param annotOrderByTree The list that saves alignment annotations in order.
405    * @return The ordered list of AlignmentAnnotation objects from the tree.
406    */
 
407  0 toggle private List<AlignmentAnnotation> _sortByTree(BinaryNode node,
408    List<AlignmentAnnotation> annotOrderByTree)
409    {
410    // Base case: if the node is null, return the current list
411  0 if (node == null)
412    {
413  0 return annotOrderByTree;
414    }
415   
416    // Get the left and right children of the current node
417  0 BinaryNode left = (BinaryNode) node.left();
418  0 BinaryNode right = (BinaryNode) node.right();
419   
420    // If the current node is a leaf node
421  0 if ((left == null) && (right == null))
422    {
423  0 if (!(node instanceof SequenceNode
424    && ((SequenceNode) node).isPlaceholder())
425    && (node.getAlignmentAnnotation() != null ))
426    {
427   
428    // Add the annotation if it's not already in the list
429  0 if (!annotOrderByTree.contains(node.getAlignmentAnnotation()))
430    {
431  0 annotOrderByTree.add(node.getAlignmentAnnotation());
432    }
433   
434    }
435   
436    // Return the current state of the list for leaf nodes
437  0 return annotOrderByTree;
438    }
439    else
440    {
441    // Recursively traverse the left and right subtrees. For internal nodes.
442  0 _sortByTree(left, annotOrderByTree);
443  0 _sortByTree(right, annotOrderByTree);
444    }
445   
446    // Return the final list after traversal
447  0 return annotOrderByTree;
448    }
449   
450    /**
451    * Calculate and save in a temporary map the position of each annotation's
452    * sequence (if it has one) in the alignment. Faster to do this once than for
453    * every annotation comparison.
454    *
455    * @param alignmentAnnotations
456    */
 
457  7668 toggle private void saveSequenceIndices(
458    AlignmentAnnotation[] alignmentAnnotations)
459    {
460  7668 sequenceIndices.clear();
461  7668 for (AlignmentAnnotation ann : alignmentAnnotations)
462    {
463  95649 SequenceI seq = ann.sequenceRef;
464  95650 if (seq != null)
465    {
466  64370 int index = AlignmentUtils.getSequenceIndex(alignment, seq);
467  64372 sequenceIndices.put(seq, index);
468    }
469    }
470    }
471   
472    /**
473    * Get the comparator for the specified sort order.
474    *
475    * @param order
476    * @return
477    */
 
478  7666 toggle private Comparator<? super AlignmentAnnotation> getComparator(
479    SequenceAnnotationOrder order)
480    {
481  7666 if (order == null)
482    {
483  0 return noSort;
484    }
485  7666 switch (order)
486    {
487  7648 case NONE:
488  7648 return this.noSort;
489  8 case SEQUENCE_AND_LABEL:
490  8 return this.bySequenceAndLabel;
491  11 case LABEL_AND_SEQUENCE:
492  11 return this.byLabelAndSequence;
493  0 default:
494  0 throw new UnsupportedOperationException(order.toString());
495    }
496    }
497   
498    /**
499    * Non-case-sensitive comparison of annotation labels. Returns zero if either
500    * argument is null.
501    *
502    * @param o1
503    * @param o2
504    * @return
505    */
 
506  185926 toggle private int compareLabels(AlignmentAnnotation o1, AlignmentAnnotation o2)
507    {
508  185926 if (o1 == null || o2 == null)
509    {
510  0 return 0;
511    }
512  185926 String label1 = o1.label;
513  185926 String label2 = o2.label;
514  185926 return compareString(label1, label2);
515    }
516   
517    /**
518    * Non-case-sensitive comparison of annotation descriptions. Returns zero if
519    * either argument is null.
520    *
521    * @param o1
522    * @param o2
523    * @return
524    */
 
525  973 toggle private int compareDescriptions(AlignmentAnnotation o1,
526    AlignmentAnnotation o2)
527    {
528  973 if (o1 == null || o2 == null)
529    {
530  0 return 0;
531    }
532  973 String label1 = o1.description;
533  973 String label2 = o2.description;
534  973 return compareString(label1, label2);
535    }
536   
 
537  186899 toggle private int compareString(String label1, String label2)
538    {
539  186899 if (label1 == null && label2 == null)
540    {
541  0 return 0;
542    }
543  186899 if (label1 == null)
544    {
545  0 return -1;
546    }
547  186899 if (label2 == null)
548    {
549  0 return 1;
550    }
551  186899 return label1.toUpperCase(Locale.ROOT)
552    .compareTo(label2.toUpperCase(Locale.ROOT));
553    }
554   
555    /**
556    * Comparison based on position of associated sequence (if any) in the
557    * alignment. Returns zero if either argument is null.
558    *
559    * @param o1
560    * @param o2
561    * @return
562    */
 
563  237429 toggle private int compareSequences(AlignmentAnnotation o1,
564    AlignmentAnnotation o2)
565    {
566  237429 SequenceI seq1 = o1.sequenceRef;
567  237429 SequenceI seq2 = o2.sequenceRef;
568  237429 if (seq1 == null && seq2 == null)
569    {
570  0 return 0;
571    }
572    /*
573    * Sort non-sequence-related before or after sequence-related.
574    */
575  237429 if (seq1 == null)
576    {
577  0 return showAutocalcAbove ? -1 : 1;
578    }
579  237429 if (seq2 == null)
580    {
581  0 return showAutocalcAbove ? 1 : -1;
582    }
583    // get sequence index - but note -1 means 'at end' so needs special handling
584  237429 int index1 = sequenceIndices.get(seq1);
585  237429 int index2 = sequenceIndices.get(seq2);
586  237429 if (index1 == index2)
587    {
588  15876 return 0;
589    }
590  221553 if (index1 == -1)
591    {
592  0 return -1;
593    }
594  221553 if (index2 == -1)
595    {
596  0 return 1;
597    }
598  221553 return Integer.compare(index1, index2);
599    }
600    }