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