JAL-1551 spotlessApply
[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
552               && ((SequenceNode) node).isPlaceholder())
553               && (node.element() != null))
554       {
555         if (node.element() instanceof SequenceI)
556         {
557           if (!tmp.contains(node.element())) // && (seqset==null ||
558                                              // seqset.size()==0 ||
559                                              // seqset.contains(tmp)))
560           {
561             tmp.add((SequenceI) node.element());
562           }
563         }
564       }
565
566       return tmp;
567     }
568     else
569     {
570       _sortByTree(left, tmp, seqset);
571       _sortByTree(right, tmp, seqset);
572     }
573
574     return tmp;
575   }
576
577   // Ordering Objects
578   // Alignment.sortBy(OrderObj) - sequence of sequence pointer refs in
579   // appropriate order
580   //
581
582   /**
583    * recover the order of sequences given by the safe numbering scheme introducd
584    * SeqsetUtils.uniquify.
585    */
586   public static void recoverOrder(SequenceI[] alignment)
587   {
588     float[] ids = new float[alignment.length];
589
590     for (int i = 0; i < alignment.length; i++)
591     {
592       ids[i] = (Float.valueOf(alignment[i].getName().substring(8)))
593               .floatValue();
594     }
595
596     jalview.util.QuickSort.sort(ids, alignment);
597   }
598
599   /**
600    * Sort sequence in order of increasing score attribute for annotation with a
601    * particular scoreLabel. Or reverse if same label was used previously
602    * 
603    * @param scoreLabel
604    *          exact label for sequence associated AlignmentAnnotation scores to
605    *          use for sorting.
606    * @param alignment
607    *          sequences to be sorted
608    */
609   public static void sortByAnnotationScore(String scoreLabel,
610           AlignmentI alignment)
611   {
612     SequenceI[] seqs = alignment.getSequencesArray();
613     boolean[] hasScore = new boolean[seqs.length]; // per sequence score
614     // presence
615     int hasScores = 0; // number of scores present on set
616     double[] scores = new double[seqs.length];
617     double min = 0, max = 0;
618     for (int i = 0; i < seqs.length; i++)
619     {
620       AlignmentAnnotation[] scoreAnn = seqs[i].getAnnotation(scoreLabel);
621       if (scoreAnn != null)
622       {
623         hasScores++;
624         hasScore[i] = true;
625         scores[i] = scoreAnn[0].getScore(); // take the first instance of this
626         // score.
627         if (hasScores == 1)
628         {
629           max = min = scores[i];
630         }
631         else
632         {
633           if (max < scores[i])
634           {
635             max = scores[i];
636           }
637           if (min > scores[i])
638           {
639             min = scores[i];
640           }
641         }
642       }
643       else
644       {
645         hasScore[i] = false;
646       }
647     }
648     if (hasScores == 0)
649     {
650       return; // do nothing - no scores present to sort by.
651     }
652     if (hasScores < seqs.length)
653     {
654       for (int i = 0; i < seqs.length; i++)
655       {
656         if (!hasScore[i])
657         {
658           scores[i] = (max + i + 1.0);
659         }
660       }
661     }
662
663     jalview.util.QuickSort.sort(scores, seqs);
664     if (lastSortByAnnotation != scoreLabel)
665     {
666       lastSortByAnnotation = scoreLabel;
667       setOrder(alignment, seqs);
668     }
669     else
670     {
671       setReverseOrder(alignment, seqs);
672     }
673   }
674
675   /**
676    * types of feature ordering: Sort by score : average score - or total score -
677    * over all features in region Sort by feature label text: (or if null -
678    * feature type text) - numerical or alphabetical Sort by feature density:
679    * based on counts - ignoring individual text or scores for each feature
680    */
681   public static String FEATURE_SCORE = "average_score";
682
683   public static String FEATURE_LABEL = "text";
684
685   public static String FEATURE_DENSITY = "density";
686
687   /**
688    * Sort sequences by feature score or density, optionally restricted by
689    * feature types, feature groups, or alignment start/end positions.
690    * <p>
691    * If the sort is repeated for the same combination of types and groups, sort
692    * order is reversed.
693    * 
694    * @param featureTypes
695    *          a list of feature types to include (or null for all)
696    * @param groups
697    *          a list of feature groups to include (or null for all)
698    * @param startCol
699    *          start column position to include (base zero)
700    * @param endCol
701    *          end column position to include (base zero)
702    * @param alignment
703    *          the alignment to be sorted
704    * @param method
705    *          either "average_score" or "density" ("text" not yet implemented)
706    */
707   public static void sortByFeature(List<String> featureTypes,
708           List<String> groups, final int startCol, final int endCol,
709           AlignmentI alignment, String method)
710   {
711     if (method != FEATURE_SCORE && method != FEATURE_LABEL
712             && method != FEATURE_DENSITY)
713     {
714       String msg = String.format(
715               "Implementation Error - sortByFeature method must be either '%s' or '%s'",
716               FEATURE_SCORE, FEATURE_DENSITY);
717       System.err.println(msg);
718       return;
719     }
720
721     flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol,
722             endCol);
723
724     SequenceI[] seqs = alignment.getSequencesArray();
725
726     boolean[] hasScore = new boolean[seqs.length]; // per sequence score
727     // presence
728     int hasScores = 0; // number of scores present on set
729     double[] scores = new double[seqs.length];
730     int[] seqScores = new int[seqs.length];
731     Object[][] feats = new Object[seqs.length][];
732     double min = 0d;
733     double max = 0d;
734
735     for (int i = 0; i < seqs.length; i++)
736     {
737       /*
738        * get sequence residues overlapping column region
739        * and features for residue positions and specified types
740        */
741       String[] types = featureTypes == null ? null
742               : featureTypes.toArray(new String[featureTypes.size()]);
743       List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
744               endCol + 1, types);
745
746       seqScores[i] = 0;
747       scores[i] = 0.0;
748
749       Iterator<SequenceFeature> it = sfs.listIterator();
750       while (it.hasNext())
751       {
752         SequenceFeature sf = it.next();
753
754         /*
755          * accept all features with null or empty group, otherwise
756          * check group is one of the currently visible groups
757          */
758         String featureGroup = sf.getFeatureGroup();
759         if (groups != null && featureGroup != null
760                 && !"".equals(featureGroup)
761                 && !groups.contains(featureGroup))
762         {
763           it.remove();
764         }
765         else
766         {
767           float score = sf.getScore();
768           if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
769           {
770             if (seqScores[i] == 0)
771             {
772               hasScores++;
773             }
774             seqScores[i]++;
775             hasScore[i] = true;
776             scores[i] += score;
777             // take the first instance of this score // ??
778           }
779         }
780       }
781
782       feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]);
783       if (!sfs.isEmpty())
784       {
785         if (method == FEATURE_LABEL)
786         {
787           // order the labels by alphabet (not yet implemented)
788           String[] labs = new String[sfs.size()];
789           for (int l = 0; l < sfs.size(); l++)
790           {
791             SequenceFeature sf = sfs.get(l);
792             String description = sf.getDescription();
793             labs[l] = (description != null ? description : sf.getType());
794           }
795           QuickSort.sort(labs, feats[i]);
796         }
797       }
798       if (hasScore[i])
799       {
800         // compute average score
801         scores[i] /= seqScores[i];
802         // update the score bounds.
803         if (hasScores == 1)
804         {
805           min = scores[i];
806           max = min;
807         }
808         else
809         {
810           max = Math.max(max, scores[i]);
811           min = Math.min(min, scores[i]);
812         }
813       }
814     }
815
816     if (FEATURE_SCORE.equals(method))
817     {
818       if (hasScores == 0)
819       {
820         return; // do nothing - no scores present to sort by.
821       }
822       // pad score matrix
823       if (hasScores < seqs.length)
824       {
825         for (int i = 0; i < seqs.length; i++)
826         {
827           if (!hasScore[i])
828           {
829             scores[i] = (max + 1 + i);
830           }
831           else
832           {
833             // int nf = (feats[i] == null) ? 0
834             // : ((SequenceFeature[]) feats[i]).length;
835             // // System.err.println("Sorting on Score: seq " +
836             // seqs[i].getName()
837             // + " Feats: " + nf + " Score : " + scores[i]);
838           }
839         }
840       }
841       QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
842     }
843     else if (FEATURE_DENSITY.equals(method))
844     {
845       for (int i = 0; i < seqs.length; i++)
846       {
847         int featureCount = feats[i] == null ? 0
848                 : ((SequenceFeature[]) feats[i]).length;
849         scores[i] = featureCount;
850         // System.err.println("Sorting on Density: seq "+seqs[i].getName()+
851         // " Feats: "+featureCount+" Score : "+scores[i]);
852       }
853       QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
854     }
855
856     setOrder(alignment, seqs);
857   }
858
859   /**
860    * Builds a string hash of criteria for sorting, and if unchanged from last
861    * time, reverse the sort order
862    * 
863    * @param method
864    * @param featureTypes
865    * @param groups
866    * @param startCol
867    * @param endCol
868    */
869   protected static void flipFeatureSortIfUnchanged(String method,
870           List<String> featureTypes, List<String> groups,
871           final int startCol, final int endCol)
872   {
873     StringBuilder sb = new StringBuilder(64);
874     sb.append(startCol).append(method).append(endCol);
875     if (featureTypes != null)
876     {
877       Collections.sort(featureTypes);
878       sb.append(featureTypes.toString());
879     }
880     if (groups != null)
881     {
882       Collections.sort(groups);
883       sb.append(groups.toString());
884     }
885     String scoreCriteria = sb.toString();
886
887     /*
888      * if resorting on the same criteria, toggle sort order
889      */
890     if (sortByFeatureCriteria == null
891             || !scoreCriteria.equals(sortByFeatureCriteria))
892     {
893       sortByFeatureAscending = true;
894     }
895     else
896     {
897       sortByFeatureAscending = !sortByFeatureAscending;
898     }
899     sortByFeatureCriteria = scoreCriteria;
900   }
901
902 }