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