JAL-3253 jalview.bin.Instance streamlining
[jalview.git] / src / jalview / analysis / AlignmentSorter.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.analysis.scoremodels.PIDModel;
24 import jalview.analysis.scoremodels.SimilarityParams;
25 import jalview.bin.Instance;
26 import jalview.datamodel.AlignmentAnnotation;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.AlignmentOrder;
29 import jalview.datamodel.SequenceFeature;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.datamodel.SequenceI;
32 import jalview.datamodel.SequenceNode;
33 import jalview.util.QuickSort;
34
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.Iterator;
38 import java.util.List;
39
40 /**
41  * Routines for manipulating the order of a multiple sequence alignment TODO:
42  * this class retains some global states concerning sort-order which should be
43  * made attributes for the caller's alignment visualization. TODO: refactor to
44  * allow a subset of selected sequences to be sorted within the context of a
45  * whole alignment. Sort method template is: SequenceI[] tobesorted, [ input
46  * data mapping to each tobesorted element to use ], Alignment context of
47  * tobesorted that are to be re-ordered, boolean sortinplace, [special data - ie
48  * seuqence to be sorted w.r.t.]) sortinplace implies that the sorted vector
49  * resulting from applying the operation to tobesorted should be mapped back to
50  * the original positions in alignment. Otherwise, normal behaviour is to re
51  * order alignment so that tobesorted is sorted and grouped together starting
52  * from the first tobesorted position in the alignment. e.g. (a,tb2,b,tb1,c,tb3
53  * becomes a,tb1,tb2,tb3,b,c)
54  */
55 public class AlignmentSorter
56 {
57
58   private AlignmentSorter()
59   {
60     // private singleton
61   }
62
63   private static AlignmentSorter getInstance()
64   {
65     Instance j = Instance.getInstance();
66     return (j.alignmentSorter == null
67             ? j.alignmentSorter = new AlignmentSorter()
68             : j.alignmentSorter);
69   }
70
71   /**
72    * types of feature ordering: Sort by score : average score - or total score -
73    * over all features in region Sort by feature label text: (or if null -
74    * feature type text) - numerical or alphabetical Sort by feature density:
75    * based on counts - ignoring individual text or scores for each feature
76    */
77   public static final String FEATURE_SCORE = "average_score";
78
79   public static final String FEATURE_LABEL = "text";
80
81   public static final String FEATURE_DENSITY = "density";
82
83   /*
84    * todo: refactor searches to follow a basic pattern: (search property, last
85    * search state, current sort direction)
86    */
87   boolean sortIdAscending = true;
88
89   int lastGroupHash = 0;
90
91   boolean sortGroupAscending = true;
92
93   AlignmentOrder lastOrder = null;
94
95   boolean sortOrderAscending = true;
96
97   TreeModel lastTree = null;
98
99   boolean sortTreeAscending = true;
100
101
102   /**
103    * last Annotation Label used for sort by Annotation score
104    */
105   private String lastSortByAnnotation;
106
107   /**
108    * string hash of last arguments to sortByFeature (sort order toggles if this
109    * is unchanged between sorts)
110    */
111   private String sortByFeatureCriteria;
112
113   private boolean sortByFeatureAscending = true;
114
115   private boolean sortLengthAscending;
116
117   /**
118    * Sorts sequences in the alignment by Percentage Identity with the given
119    * reference sequence, sorting the highest identity to the top
120    * 
121    * @param align
122    *          AlignmentI
123    * @param s
124    *          SequenceI
125    * @param end
126    */
127   public static void sortByPID(AlignmentI align, SequenceI s)
128   {
129     int nSeq = align.getHeight();
130
131     float[] scores = new float[nSeq];
132     SequenceI[] seqs = new SequenceI[nSeq];
133     String refSeq = s.getSequenceAsString();
134
135     SimilarityParams pidParams = new SimilarityParams(true, true, true,
136             true);
137     for (int i = 0; i < nSeq; i++)
138     {
139       scores[i] = (float) PIDModel.computePID(
140               align.getSequenceAt(i).getSequenceAsString(), refSeq,
141               pidParams);
142       seqs[i] = align.getSequenceAt(i);
143     }
144
145     QuickSort.sort(scores, seqs);
146
147     setReverseOrder(align, seqs);
148   }
149
150   /**
151    * Reverse the order of the sort
152    * 
153    * @param align
154    *          DOCUMENT ME!
155    * @param seqs
156    *          DOCUMENT ME!
157    */
158   private static void setReverseOrder(AlignmentI align, SequenceI[] seqs)
159   {
160     int nSeq = seqs.length;
161
162     int len = 0;
163
164     if ((nSeq % 2) == 0)
165     {
166       len = nSeq / 2;
167     }
168     else
169     {
170       len = (nSeq + 1) / 2;
171     }
172
173     // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
174     List<SequenceI> asq = align.getSequences();
175     synchronized (asq)
176     {
177       for (int i = 0; i < len; i++)
178       {
179         // SequenceI tmp = seqs[i];
180         asq.set(i, seqs[nSeq - i - 1]);
181         asq.set(nSeq - i - 1, seqs[i]);
182       }
183     }
184   }
185
186   /**
187    * Sets the Alignment object with the given sequences
188    * 
189    * @param align
190    *          Alignment object to be updated
191    * @param tmp
192    *          sequences as a vector
193    */
194   private static void setOrder(AlignmentI align, List<SequenceI> tmp)
195   {
196     setOrder(align, vectorSubsetToArray(tmp, align.getSequences()));
197   }
198
199   /**
200    * Sets the Alignment object with the given sequences
201    * 
202    * @param align
203    *          DOCUMENT ME!
204    * @param seqs
205    *          sequences as an array
206    */
207   public static void setOrder(AlignmentI align, SequenceI[] seqs)
208   {
209     // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
210     List<SequenceI> algn = align.getSequences();
211     synchronized (algn)
212     {
213       List<SequenceI> tmp = new ArrayList<>();
214
215       for (int i = 0; i < seqs.length; i++)
216       {
217         if (algn.contains(seqs[i]))
218         {
219           tmp.add(seqs[i]);
220         }
221       }
222
223       algn.clear();
224       // User may have hidden seqs, then clicked undo or redo
225       for (int i = 0; i < tmp.size(); i++)
226       {
227         algn.add(tmp.get(i));
228       }
229     }
230   }
231
232   /**
233    * Sorts by ID. Numbers are sorted before letters.
234    * 
235    * @param align
236    *          The alignment object to sort
237    */
238   public static void sortByID(AlignmentI align)
239   {
240     int nSeq = align.getHeight();
241
242     String[] ids = new String[nSeq];
243     SequenceI[] seqs = new SequenceI[nSeq];
244
245     for (int i = 0; i < nSeq; i++)
246     {
247       ids[i] = align.getSequenceAt(i).getName();
248       seqs[i] = align.getSequenceAt(i);
249     }
250
251     QuickSort.sort(ids, seqs);
252
253     AlignmentSorter as = getInstance();
254     if (as.sortIdAscending)
255     {
256       setReverseOrder(align, seqs);
257     }
258     else
259     {
260       setOrder(align, seqs);
261     }
262
263     as.sortIdAscending = !as.sortIdAscending;
264   }
265
266   /**
267    * Sorts by sequence length
268    * 
269    * @param align
270    *          The alignment object to sort
271    */
272   public static void sortByLength(AlignmentI align)
273   {
274     int nSeq = align.getHeight();
275
276     float[] length = new float[nSeq];
277     SequenceI[] seqs = new SequenceI[nSeq];
278
279     for (int i = 0; i < nSeq; i++)
280     {
281       seqs[i] = align.getSequenceAt(i);
282       length[i] = (seqs[i].getEnd() - seqs[i].getStart());
283     }
284
285     QuickSort.sort(length, seqs);
286
287     AlignmentSorter as = getInstance();
288
289     if (as.sortLengthAscending)
290     {
291       setReverseOrder(align, seqs);
292     }
293     else
294     {
295       setOrder(align, seqs);
296     }
297
298     as.sortLengthAscending = !as.sortLengthAscending;
299   }
300
301   /**
302    * Sorts the alignment by size of group. <br>
303    * Maintains the order of sequences in each group by order in given alignment
304    * object.
305    * 
306    * @param align
307    *          sorts the given alignment object by group
308    */
309   public static void sortByGroup(AlignmentI align)
310   {
311     // MAINTAINS ORIGNAL SEQUENCE ORDER,
312     // ORDERS BY GROUP SIZE
313     List<SequenceGroup> groups = new ArrayList<>();
314
315     AlignmentSorter as = getInstance();
316
317     if (groups.hashCode() != as.lastGroupHash)
318     {
319       as.sortGroupAscending = true;
320       as.lastGroupHash = groups.hashCode();
321     }
322     else
323     {
324       as.sortGroupAscending = !as.sortGroupAscending;
325     }
326
327     // SORTS GROUPS BY SIZE
328     // ////////////////////
329     for (SequenceGroup sg : align.getGroups())
330     {
331       for (int j = 0; j < groups.size(); j++)
332       {
333         SequenceGroup sg2 = groups.get(j);
334
335         if (sg.getSize() > sg2.getSize())
336         {
337           groups.add(j, sg);
338
339           break;
340         }
341       }
342
343       if (!groups.contains(sg))
344       {
345         groups.add(sg);
346       }
347     }
348
349     // NOW ADD SEQUENCES MAINTAINING ALIGNMENT ORDER
350     // /////////////////////////////////////////////
351     List<SequenceI> seqs = new ArrayList<>();
352
353     for (int i = 0; i < groups.size(); i++)
354     {
355       SequenceGroup sg = groups.get(i);
356       SequenceI[] orderedseqs = sg.getSequencesInOrder(align);
357
358       for (int j = 0; j < orderedseqs.length; j++)
359       {
360         seqs.add(orderedseqs[j]);
361       }
362     }
363
364     if (as.sortGroupAscending)
365     {
366       setOrder(align, seqs);
367     }
368     else
369     {
370       setReverseOrder(align,
371               vectorSubsetToArray(seqs, align.getSequences()));
372     }
373   }
374
375   /**
376    * Select sequences in order from tmp that is present in mask, and any
377    * remaining sequences in mask not in tmp
378    * 
379    * @param tmp
380    *          thread safe collection of sequences
381    * @param mask
382    *          thread safe collection of sequences
383    * 
384    * @return intersect(tmp,mask)+intersect(complement(tmp),mask)
385    */
386   private static SequenceI[] vectorSubsetToArray(List<SequenceI> tmp,
387           List<SequenceI> mask)
388   {
389     // or?
390     // tmp2 = tmp.retainAll(mask);
391     // return tmp2.addAll(mask.removeAll(tmp2))
392
393     ArrayList<SequenceI> seqs = new ArrayList<>();
394     int i, idx;
395     boolean[] tmask = new boolean[mask.size()];
396
397     for (i = 0; i < mask.size(); i++)
398     {
399       tmask[i] = true;
400     }
401
402     for (i = 0; i < tmp.size(); i++)
403     {
404       SequenceI sq = tmp.get(i);
405       idx = mask.indexOf(sq);
406       if (idx > -1 && tmask[idx])
407       {
408         tmask[idx] = false;
409         seqs.add(sq);
410       }
411     }
412
413     for (i = 0; i < tmask.length; i++)
414     {
415       if (tmask[i])
416       {
417         seqs.add(mask.get(i));
418       }
419     }
420
421     return seqs.toArray(new SequenceI[seqs.size()]);
422   }
423
424   /**
425    * Sorts by a given AlignmentOrder object
426    * 
427    * @param align
428    *          Alignment to order
429    * @param order
430    *          specified order for alignment
431    */
432   public static void sortBy(AlignmentI align, AlignmentOrder order)
433   {
434     // Get an ordered vector of sequences which may also be present in align
435     List<SequenceI> tmp = order.getOrder();
436
437     AlignmentSorter as = getInstance();
438
439     if (as.lastOrder == order)
440     {
441       as.sortOrderAscending = !as.sortOrderAscending;
442     }
443     else
444     {
445       as.sortOrderAscending = true;
446     }
447
448     if (as.sortOrderAscending)
449     {
450       setOrder(align, tmp);
451     }
452     else
453     {
454       setReverseOrder(align,
455               vectorSubsetToArray(tmp, align.getSequences()));
456     }
457   }
458
459   /**
460    * DOCUMENT ME!
461    * 
462    * @param align
463    *          alignment to order
464    * @param tree
465    *          tree which has
466    * 
467    * @return DOCUMENT ME!
468    */
469   private static List<SequenceI> getOrderByTree(AlignmentI align,
470           TreeModel tree)
471   {
472     int nSeq = align.getHeight();
473
474     List<SequenceI> tmp = new ArrayList<>();
475
476     tmp = _sortByTree(tree.getTopNode(), tmp, align.getSequences());
477
478     if (tmp.size() != nSeq)
479     {
480       // TODO: JBPNote - decide if this is always an error
481       // (eg. not when a tree is associated to another alignment which has more
482       // sequences)
483       if (tmp.size() != nSeq)
484       {
485         addStrays(align, tmp);
486       }
487
488       if (tmp.size() != nSeq)
489       {
490         System.err.println("WARNING: tmp.size()=" + tmp.size() + " != nseq="
491                 + nSeq
492                 + " in getOrderByTree - tree contains sequences not in alignment");
493       }
494     }
495
496     return tmp;
497   }
498
499   /**
500    * Sorts the alignment by a given tree
501    * 
502    * @param align
503    *          alignment to order
504    * @param tree
505    *          tree which has
506    */
507   public static void sortByTree(AlignmentI align, TreeModel tree)
508   {
509     List<SequenceI> tmp = getOrderByTree(align, tree);
510
511     AlignmentSorter as = getInstance();
512
513     // tmp should properly permute align with tree.
514     if (as.lastTree != tree)
515     {
516       as.sortTreeAscending = true;
517       as.lastTree = tree;
518     }
519     else
520     {
521       as.sortTreeAscending = !as.sortTreeAscending;
522     }
523
524     if (as.sortTreeAscending)
525     {
526       setOrder(align, tmp);
527     }
528     else
529     {
530       setReverseOrder(align,
531               vectorSubsetToArray(tmp, align.getSequences()));
532     }
533   }
534
535   /**
536    * DOCUMENT ME!
537    * 
538    * @param align
539    *          DOCUMENT ME!
540    * @param tmp
541    *          DOCUMENT ME!
542    */
543   private static void addStrays(AlignmentI align, List<SequenceI> tmp)
544   {
545     int nSeq = align.getHeight();
546
547     for (int i = 0; i < nSeq; i++)
548     {
549       if (!tmp.contains(align.getSequenceAt(i)))
550       {
551         tmp.add(align.getSequenceAt(i));
552       }
553     }
554
555     if (nSeq != tmp.size())
556     {
557       System.err
558               .println("ERROR: Size still not right even after addStrays");
559     }
560   }
561
562   /**
563    * DOCUMENT ME!
564    * 
565    * @param node
566    *          DOCUMENT ME!
567    * @param tmp
568    *          DOCUMENT ME!
569    * @param seqset
570    *          DOCUMENT ME!
571    * 
572    * @return DOCUMENT ME!
573    */
574   private static List<SequenceI> _sortByTree(SequenceNode node,
575           List<SequenceI> tmp, List<SequenceI> seqset)
576   {
577     if (node == null)
578     {
579       return tmp;
580     }
581
582     SequenceNode left = (SequenceNode) node.left();
583     SequenceNode right = (SequenceNode) node.right();
584
585     if ((left == null) && (right == null))
586     {
587       if (!node.isPlaceholder() && (node.element() != null))
588       {
589         if (node.element() instanceof SequenceI)
590         {
591           if (!tmp.contains(node.element())) // && (seqset==null ||
592                                              // seqset.size()==0 ||
593                                              // seqset.contains(tmp)))
594           {
595             tmp.add((SequenceI) node.element());
596           }
597         }
598       }
599
600       return tmp;
601     }
602     else
603     {
604       _sortByTree(left, tmp, seqset);
605       _sortByTree(right, tmp, seqset);
606     }
607
608     return tmp;
609   }
610
611   // Ordering Objects
612   // Alignment.sortBy(OrderObj) - sequence of sequence pointer refs in
613   // appropriate order
614   //
615
616   /**
617    * recover the order of sequences given by the safe numbering scheme introducd
618    * SeqsetUtils.uniquify.
619    */
620   public static void recoverOrder(SequenceI[] alignment)
621   {
622     float[] ids = new float[alignment.length];
623
624     for (int i = 0; i < alignment.length; i++)
625     {
626       ids[i] = (new Float(alignment[i].getName().substring(8)))
627               .floatValue();
628     }
629
630     jalview.util.QuickSort.sort(ids, alignment);
631   }
632
633   /**
634    * Sort sequence in order of increasing score attribute for annotation with a
635    * particular scoreLabel. Or reverse if same label was used previously
636    * 
637    * @param scoreLabel
638    *          exact label for sequence associated AlignmentAnnotation scores to
639    *          use for sorting.
640    * @param alignment
641    *          sequences to be sorted
642    */
643   public static void sortByAnnotationScore(String scoreLabel,
644           AlignmentI alignment)
645   {
646     SequenceI[] seqs = alignment.getSequencesArray();
647     boolean[] hasScore = new boolean[seqs.length]; // per sequence score
648     // presence
649     int hasScores = 0; // number of scores present on set
650     double[] scores = new double[seqs.length];
651     double min = 0, max = 0;
652     for (int i = 0; i < seqs.length; i++)
653     {
654       AlignmentAnnotation[] scoreAnn = seqs[i].getAnnotation(scoreLabel);
655       if (scoreAnn != null)
656       {
657         hasScores++;
658         hasScore[i] = true;
659         scores[i] = scoreAnn[0].getScore(); // take the first instance of this
660         // score.
661         if (hasScores == 1)
662         {
663           max = min = scores[i];
664         }
665         else
666         {
667           if (max < scores[i])
668           {
669             max = scores[i];
670           }
671           if (min > scores[i])
672           {
673             min = scores[i];
674           }
675         }
676       }
677       else
678       {
679         hasScore[i] = false;
680       }
681     }
682     if (hasScores == 0)
683     {
684       return; // do nothing - no scores present to sort by.
685     }
686     if (hasScores < seqs.length)
687     {
688       for (int i = 0; i < seqs.length; i++)
689       {
690         if (!hasScore[i])
691         {
692           scores[i] = (max + i + 1.0);
693         }
694       }
695     }
696
697     jalview.util.QuickSort.sort(scores, seqs);
698
699     AlignmentSorter as = getInstance();
700
701     if (as.lastSortByAnnotation != scoreLabel)
702     {
703       as.lastSortByAnnotation = scoreLabel;
704       setOrder(alignment, seqs);
705     }
706     else
707     {
708       setReverseOrder(alignment, seqs);
709     }
710   }
711
712   /**
713    * Sort sequences by feature score or density, optionally restricted by
714    * feature types, feature groups, or alignment start/end positions.
715    * <p>
716    * If the sort is repeated for the same combination of types and groups, sort
717    * order is reversed.
718    * 
719    * @param featureTypes
720    *          a list of feature types to include (or null for all)
721    * @param groups
722    *          a list of feature groups to include (or null for all)
723    * @param startCol
724    *          start column position to include (base zero)
725    * @param endCol
726    *          end column position to include (base zero)
727    * @param alignment
728    *          the alignment to be sorted
729    * @param method
730    *          either "average_score" or "density" ("text" not yet implemented)
731    */
732   public static void sortByFeature(List<String> featureTypes,
733           List<String> groups, final int startCol, final int endCol,
734           AlignmentI alignment, String method)
735   {
736     if (method != FEATURE_SCORE && method != FEATURE_LABEL
737             && method != FEATURE_DENSITY)
738     {
739       String msg = String
740               .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
741                       FEATURE_SCORE, FEATURE_DENSITY);
742       System.err.println(msg);
743       return;
744     }
745
746     flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol);
747
748     SequenceI[] seqs = alignment.getSequencesArray();
749
750     boolean[] hasScore = new boolean[seqs.length]; // per sequence score
751     // presence
752     int hasScores = 0; // number of scores present on set
753     double[] scores = new double[seqs.length];
754     int[] seqScores = new int[seqs.length];
755     Object[][] feats = new Object[seqs.length][];
756     double min = 0d;
757     double max = 0d;
758
759     for (int i = 0; i < seqs.length; i++)
760     {
761       /*
762        * get sequence residues overlapping column region
763        * and features for residue positions and specified types
764        */
765       String[] types = featureTypes == null ? null : featureTypes
766               .toArray(new String[featureTypes.size()]);
767       List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
768               endCol + 1, types);
769
770       seqScores[i] = 0;
771       scores[i] = 0.0;
772
773       Iterator<SequenceFeature> it = sfs.listIterator();
774       while (it.hasNext())
775       {
776         SequenceFeature sf = it.next();
777
778         /*
779          * accept all features with null or empty group, otherwise
780          * check group is one of the currently visible groups
781          */
782         String featureGroup = sf.getFeatureGroup();
783         if (groups != null && featureGroup != null
784                 && !"".equals(featureGroup)
785                 && !groups.contains(featureGroup))
786         {
787           it.remove();
788         }
789         else
790         {
791           float score = sf.getScore();
792           if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
793           {
794             if (seqScores[i] == 0)
795             {
796               hasScores++;
797             }
798             seqScores[i]++;
799             hasScore[i] = true;
800             scores[i] += score;
801             // take the first instance of this score // ??
802           }
803         }
804       }
805
806       feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]);
807       if (!sfs.isEmpty())
808       {
809         if (method == FEATURE_LABEL)
810         {
811           // order the labels by alphabet (not yet implemented)
812           String[] labs = new String[sfs.size()];
813           for (int l = 0; l < sfs.size(); l++)
814           {
815             SequenceFeature sf = sfs.get(l);
816             String description = sf.getDescription();
817             labs[l] = (description != null ? description : sf.getType());
818           }
819           QuickSort.sort(labs, feats[i]);
820         }
821       }
822       if (hasScore[i])
823       {
824         // compute average score
825         scores[i] /= seqScores[i];
826         // update the score bounds.
827         if (hasScores == 1)
828         {
829           min = scores[i];
830           max = min;
831         }
832         else
833         {
834           max = Math.max(max, scores[i]);
835           min = Math.min(min, scores[i]);
836         }
837       }
838     }
839
840     boolean doSort = false;
841
842     if (FEATURE_SCORE.equals(method))
843     {
844       if (hasScores == 0)
845       {
846         return; // do nothing - no scores present to sort by.
847       }
848       // pad score matrix
849       if (hasScores < seqs.length)
850       {
851         for (int i = 0; i < seqs.length; i++)
852         {
853           if (!hasScore[i])
854           {
855             scores[i] = (max + 1 + i);
856           }
857           else
858           {
859             // int nf = (feats[i] == null) ? 0
860             // : ((SequenceFeature[]) feats[i]).length;
861             // // System.err.println("Sorting on Score: seq " +
862             // seqs[i].getName()
863             // + " Feats: " + nf + " Score : " + scores[i]);
864           }
865         }
866       }
867       doSort = true;
868     }
869     else if (FEATURE_DENSITY.equals(method))
870     {
871       for (int i = 0; i < seqs.length; i++)
872       {
873         int featureCount = feats[i] == null ? 0
874                 : ((SequenceFeature[]) feats[i]).length;
875         scores[i] = featureCount;
876         // System.err.println("Sorting on Density: seq "+seqs[i].getName()+
877         // " Feats: "+featureCount+" Score : "+scores[i]);
878       }
879       doSort = true;
880     }
881     if (doSort)
882     {
883       QuickSort.sortByDouble(scores, seqs, getInstance().sortByFeatureAscending);
884     }
885     setOrder(alignment, seqs);
886   }
887
888   /**
889    * Builds a string hash of criteria for sorting, and if unchanged from last
890    * time, reverse the sort order
891    * 
892    * @param method
893    * @param featureTypes
894    * @param groups
895    * @param startCol
896    * @param endCol
897    */
898   protected static void flipFeatureSortIfUnchanged(String method,
899           List<String> featureTypes, List<String> groups,
900           final int startCol, final int endCol)
901   {
902     StringBuilder sb = new StringBuilder(64);
903     sb.append(startCol).append(method).append(endCol);
904     if (featureTypes != null)
905     {
906       Collections.sort(featureTypes);
907       sb.append(featureTypes.toString());
908     }
909     if (groups != null)
910     {
911       Collections.sort(groups);
912       sb.append(groups.toString());
913     }
914     String scoreCriteria = sb.toString();
915
916     /*
917      * if resorting on the same criteria, toggle sort order
918      */
919     AlignmentSorter as = getInstance();
920     if (as.sortByFeatureCriteria == null
921             || !scoreCriteria.equals(as.sortByFeatureCriteria))
922     {
923       as.sortByFeatureAscending = true;
924     }
925     else
926     {
927       as.sortByFeatureAscending = !as.sortByFeatureAscending;
928     }
929     as.sortByFeatureCriteria = scoreCriteria;
930   }
931
932 }