b88ef3a128f19e955e593e5a0a23a0e4bfdf8a1c
[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(
112               align.getSequenceAt(i).getSequenceAsString(), refSeq,
113               pidParams);
114       seqs[i] = align.getSequenceAt(i);
115     }
116
117     QuickSort.sort(scores, seqs);
118
119     setReverseOrder(align, seqs);
120   }
121
122   /**
123    * Reverse the order of the sort
124    * 
125    * @param align
126    *          DOCUMENT ME!
127    * @param seqs
128    *          DOCUMENT ME!
129    */
130   private static void setReverseOrder(AlignmentI align, SequenceI[] seqs)
131   {
132     int nSeq = seqs.length;
133
134     int len = 0;
135
136     if ((nSeq % 2) == 0)
137     {
138       len = nSeq / 2;
139     }
140     else
141     {
142       len = (nSeq + 1) / 2;
143     }
144
145     // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
146     List<SequenceI> asq = align.getSequences();
147     synchronized (asq)
148     {
149       for (int i = 0; i < len; i++)
150       {
151         // SequenceI tmp = seqs[i];
152         asq.set(i, seqs[nSeq - i - 1]);
153         asq.set(nSeq - i - 1, seqs[i]);
154       }
155     }
156   }
157
158   /**
159    * Sets the Alignment object with the given sequences
160    * 
161    * @param align
162    *          Alignment object to be updated
163    * @param tmp
164    *          sequences as a vector
165    */
166   private static void setOrder(AlignmentI align, List<SequenceI> tmp)
167   {
168     setOrder(align, vectorSubsetToArray(tmp, align.getSequences()));
169   }
170
171   /**
172    * Sets the Alignment object with the given sequences
173    * 
174    * @param align
175    *          DOCUMENT ME!
176    * @param seqs
177    *          sequences as an array
178    */
179   public static void setOrder(AlignmentI align, SequenceI[] seqs)
180   {
181     // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
182     List<SequenceI> algn = align.getSequences();
183     synchronized (algn)
184     {
185       List<SequenceI> tmp = new ArrayList<>();
186
187       for (int i = 0; i < seqs.length; i++)
188       {
189         if (algn.contains(seqs[i]))
190         {
191           tmp.add(seqs[i]);
192         }
193       }
194
195       algn.clear();
196       // User may have hidden seqs, then clicked undo or redo
197       for (int i = 0; i < tmp.size(); i++)
198       {
199         algn.add(tmp.get(i));
200       }
201     }
202   }
203
204   /**
205    * Sorts by ID. Numbers are sorted before letters.
206    * 
207    * @param align
208    *          The alignment object to sort
209    */
210   public static void sortByID(AlignmentI align)
211   {
212     int nSeq = align.getHeight();
213
214     String[] ids = new String[nSeq];
215     SequenceI[] seqs = new SequenceI[nSeq];
216
217     for (int i = 0; i < nSeq; i++)
218     {
219       ids[i] = align.getSequenceAt(i).getName();
220       seqs[i] = align.getSequenceAt(i);
221     }
222
223     QuickSort.sort(ids, seqs);
224
225     if (sortIdAscending)
226     {
227       setReverseOrder(align, seqs);
228     }
229     else
230     {
231       setOrder(align, seqs);
232     }
233
234     sortIdAscending = !sortIdAscending;
235   }
236
237   /**
238    * Sorts by sequence length
239    * 
240    * @param align
241    *          The alignment object to sort
242    */
243   public static void sortByLength(AlignmentI align)
244   {
245     int nSeq = align.getHeight();
246
247     float[] length = new float[nSeq];
248     SequenceI[] seqs = new SequenceI[nSeq];
249
250     for (int i = 0; i < nSeq; i++)
251     {
252       seqs[i] = align.getSequenceAt(i);
253       length[i] = (seqs[i].getEnd() - seqs[i].getStart());
254     }
255
256     QuickSort.sort(length, seqs);
257
258     if (sortLengthAscending)
259     {
260       setReverseOrder(align, seqs);
261     }
262     else
263     {
264       setOrder(align, seqs);
265     }
266
267     sortLengthAscending = !sortLengthAscending;
268   }
269
270   /**
271    * Sorts the alignment by size of group. <br>
272    * Maintains the order of sequences in each group by order in given alignment
273    * object.
274    * 
275    * @param align
276    *          sorts the given alignment object by group
277    */
278   public static void sortByGroup(AlignmentI align)
279   {
280     // MAINTAINS ORIGNAL SEQUENCE ORDER,
281     // ORDERS BY GROUP SIZE
282     List<SequenceGroup> groups = new ArrayList<>();
283
284     if (groups.hashCode() != lastGroupHash)
285     {
286       sortGroupAscending = true;
287       lastGroupHash = groups.hashCode();
288     }
289     else
290     {
291       sortGroupAscending = !sortGroupAscending;
292     }
293
294     // SORTS GROUPS BY SIZE
295     // ////////////////////
296     for (SequenceGroup sg : align.getGroups())
297     {
298       for (int j = 0; j < groups.size(); j++)
299       {
300         SequenceGroup sg2 = groups.get(j);
301
302         if (sg.getSize() > sg2.getSize())
303         {
304           groups.add(j, sg);
305
306           break;
307         }
308       }
309
310       if (!groups.contains(sg))
311       {
312         groups.add(sg);
313       }
314     }
315
316     // NOW ADD SEQUENCES MAINTAINING ALIGNMENT ORDER
317     // /////////////////////////////////////////////
318     List<SequenceI> seqs = new ArrayList<>();
319
320     for (int i = 0; i < groups.size(); i++)
321     {
322       SequenceGroup sg = groups.get(i);
323       SequenceI[] orderedseqs = sg.getSequencesInOrder(align);
324
325       for (int j = 0; j < orderedseqs.length; j++)
326       {
327         seqs.add(orderedseqs[j]);
328       }
329     }
330
331     if (sortGroupAscending)
332     {
333       setOrder(align, seqs);
334     }
335     else
336     {
337       setReverseOrder(align,
338               vectorSubsetToArray(seqs, align.getSequences()));
339     }
340   }
341
342   /**
343    * Select sequences in order from tmp that is present in mask, and any
344    * remaining sequences in mask not in tmp
345    * 
346    * @param tmp
347    *          thread safe collection of sequences
348    * @param mask
349    *          thread safe collection of sequences
350    * 
351    * @return intersect(tmp,mask)+intersect(complement(tmp),mask)
352    */
353   private static SequenceI[] vectorSubsetToArray(List<SequenceI> tmp,
354           List<SequenceI> mask)
355   {
356     // or?
357     // tmp2 = tmp.retainAll(mask);
358     // return tmp2.addAll(mask.removeAll(tmp2))
359
360     ArrayList<SequenceI> seqs = new ArrayList<>();
361     int i, idx;
362     boolean[] tmask = new boolean[mask.size()];
363
364     for (i = 0; i < mask.size(); i++)
365     {
366       tmask[i] = true;
367     }
368
369     for (i = 0; i < tmp.size(); i++)
370     {
371       SequenceI sq = tmp.get(i);
372       idx = mask.indexOf(sq);
373       if (idx > -1 && tmask[idx])
374       {
375         tmask[idx] = false;
376         seqs.add(sq);
377       }
378     }
379
380     for (i = 0; i < tmask.length; i++)
381     {
382       if (tmask[i])
383       {
384         seqs.add(mask.get(i));
385       }
386     }
387
388     return seqs.toArray(new SequenceI[seqs.size()]);
389   }
390
391   /**
392    * Sorts by a given AlignmentOrder object
393    * 
394    * @param align
395    *          Alignment to order
396    * @param order
397    *          specified order for alignment
398    */
399   public static void sortBy(AlignmentI align, AlignmentOrder order)
400   {
401     // Get an ordered vector of sequences which may also be present in align
402     List<SequenceI> tmp = order.getOrder();
403
404     if (lastOrder == order)
405     {
406       sortOrderAscending = !sortOrderAscending;
407     }
408     else
409     {
410       sortOrderAscending = true;
411     }
412
413     if (sortOrderAscending)
414     {
415       setOrder(align, tmp);
416     }
417     else
418     {
419       setReverseOrder(align,
420               vectorSubsetToArray(tmp, align.getSequences()));
421     }
422   }
423
424   /**
425    * DOCUMENT ME!
426    * 
427    * @param align
428    *          alignment to order
429    * @param tree
430    *          tree which has
431    * 
432    * @return DOCUMENT ME!
433    */
434   private static List<SequenceI> getOrderByTree(AlignmentI align,
435           TreeModel tree)
436   {
437     int nSeq = align.getHeight();
438
439     List<SequenceI> tmp = new ArrayList<>();
440
441     tmp = _sortByTree(tree.getTopNode(), tmp, align.getSequences());
442
443     if (tmp.size() != nSeq)
444     {
445       // TODO: JBPNote - decide if this is always an error
446       // (eg. not when a tree is associated to another alignment which has more
447       // sequences)
448       if (tmp.size() != nSeq)
449       {
450         addStrays(align, tmp);
451       }
452
453       if (tmp.size() != nSeq)
454       {
455         System.err.println("WARNING: tmp.size()=" + tmp.size() + " != nseq="
456                 + nSeq
457                 + " in getOrderByTree - tree contains sequences not in alignment");
458       }
459     }
460
461     return tmp;
462   }
463
464   /**
465    * Sorts the alignment by a given tree
466    * 
467    * @param align
468    *          alignment to order
469    * @param tree
470    *          tree which has
471    */
472   public static void sortByTree(AlignmentI align, TreeModel tree)
473   {
474     List<SequenceI> tmp = getOrderByTree(align, tree);
475
476     // tmp should properly permute align with tree.
477     if (lastTree != tree)
478     {
479       sortTreeAscending = true;
480       lastTree = tree;
481     }
482     else
483     {
484       sortTreeAscending = !sortTreeAscending;
485     }
486
487     if (sortTreeAscending)
488     {
489       setOrder(align, tmp);
490     }
491     else
492     {
493       setReverseOrder(align,
494               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] = (Float.valueOf(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   /**
685    * Sort sequences by feature score or density, optionally restricted by
686    * feature types, feature groups, or alignment start/end positions.
687    * <p>
688    * If the sort is repeated for the same combination of types and groups, sort
689    * order is reversed.
690    * 
691    * @param featureTypes
692    *          a list of feature types to include (or null for all)
693    * @param groups
694    *          a list of feature groups to include (or null for all)
695    * @param startCol
696    *          start column position to include (base zero)
697    * @param endCol
698    *          end column position to include (base zero)
699    * @param alignment
700    *          the alignment to be sorted
701    * @param method
702    *          either "average_score" or "density" ("text" not yet implemented)
703    */
704   public static void sortByFeature(List<String> featureTypes,
705           List<String> groups, final int startCol, final int endCol,
706           AlignmentI alignment, String method)
707   {
708     if (method != FEATURE_SCORE && method != FEATURE_LABEL
709             && method != FEATURE_DENSITY)
710     {
711       String msg = String
712               .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
713                       FEATURE_SCORE, FEATURE_DENSITY);
714       System.err.println(msg);
715       return;
716     }
717
718     flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol);
719
720     SequenceI[] seqs = alignment.getSequencesArray();
721
722     boolean[] hasScore = new boolean[seqs.length]; // per sequence score
723     // presence
724     int hasScores = 0; // number of scores present on set
725     double[] scores = new double[seqs.length];
726     int[] seqScores = new int[seqs.length];
727     Object[][] feats = new Object[seqs.length][];
728     double min = 0d;
729     double max = 0d;
730
731     for (int i = 0; i < seqs.length; i++)
732     {
733       /*
734        * get sequence residues overlapping column region
735        * and features for residue positions and specified types
736        */
737       String[] types = featureTypes == null ? null : featureTypes
738               .toArray(new String[featureTypes.size()]);
739       List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
740               endCol + 1, types);
741
742       seqScores[i] = 0;
743       scores[i] = 0.0;
744
745       Iterator<SequenceFeature> it = sfs.listIterator();
746       while (it.hasNext())
747       {
748         SequenceFeature sf = it.next();
749
750         /*
751          * accept all features with null or empty group, otherwise
752          * check group is one of the currently visible groups
753          */
754         String featureGroup = sf.getFeatureGroup();
755         if (groups != null && featureGroup != null
756                 && !"".equals(featureGroup)
757                 && !groups.contains(featureGroup))
758         {
759           it.remove();
760         }
761         else
762         {
763           float score = sf.getScore();
764           if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
765           {
766             if (seqScores[i] == 0)
767             {
768               hasScores++;
769             }
770             seqScores[i]++;
771             hasScore[i] = true;
772             scores[i] += score;
773             // take the first instance of this score // ??
774           }
775         }
776       }
777
778       feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]);
779       if (!sfs.isEmpty())
780       {
781         if (method == FEATURE_LABEL)
782         {
783           // order the labels by alphabet (not yet implemented)
784           String[] labs = new String[sfs.size()];
785           for (int l = 0; l < sfs.size(); l++)
786           {
787             SequenceFeature sf = sfs.get(l);
788             String description = sf.getDescription();
789             labs[l] = (description != null ? description : sf.getType());
790           }
791           QuickSort.sort(labs, feats[i]);
792         }
793       }
794       if (hasScore[i])
795       {
796         // compute average score
797         scores[i] /= seqScores[i];
798         // update the score bounds.
799         if (hasScores == 1)
800         {
801           min = scores[i];
802           max = min;
803         }
804         else
805         {
806           max = Math.max(max, scores[i]);
807           min = Math.min(min, scores[i]);
808         }
809       }
810     }
811
812     if (FEATURE_SCORE.equals(method))
813     {
814       if (hasScores == 0)
815       {
816         return; // do nothing - no scores present to sort by.
817       }
818       // pad score matrix
819       if (hasScores < seqs.length)
820       {
821         for (int i = 0; i < seqs.length; i++)
822         {
823           if (!hasScore[i])
824           {
825             scores[i] = (max + 1 + i);
826           }
827           else
828           {
829             // int nf = (feats[i] == null) ? 0
830             // : ((SequenceFeature[]) feats[i]).length;
831             // // System.err.println("Sorting on Score: seq " +
832             // seqs[i].getName()
833             // + " Feats: " + nf + " Score : " + scores[i]);
834           }
835         }
836       }
837       QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
838     }
839     else if (FEATURE_DENSITY.equals(method))
840     {
841       for (int i = 0; i < seqs.length; i++)
842       {
843         int featureCount = feats[i] == null ? 0
844                 : ((SequenceFeature[]) feats[i]).length;
845         scores[i] = featureCount;
846         // System.err.println("Sorting on Density: seq "+seqs[i].getName()+
847         // " Feats: "+featureCount+" Score : "+scores[i]);
848       }
849       QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
850     }
851
852     setOrder(alignment, seqs);
853   }
854
855   /**
856    * Builds a string hash of criteria for sorting, and if unchanged from last
857    * time, reverse the sort order
858    * 
859    * @param method
860    * @param featureTypes
861    * @param groups
862    * @param startCol
863    * @param endCol
864    */
865   protected static void flipFeatureSortIfUnchanged(String method,
866           List<String> featureTypes, List<String> groups,
867           final int startCol, final int endCol)
868   {
869     StringBuilder sb = new StringBuilder(64);
870     sb.append(startCol).append(method).append(endCol);
871     if (featureTypes != null)
872     {
873       Collections.sort(featureTypes);
874       sb.append(featureTypes.toString());
875     }
876     if (groups != null)
877     {
878       Collections.sort(groups);
879       sb.append(groups.toString());
880     }
881     String scoreCriteria = sb.toString();
882
883     /*
884      * if resorting on the same criteria, toggle sort order
885      */
886     if (sortByFeatureCriteria == null
887             || !scoreCriteria.equals(sortByFeatureCriteria))
888     {
889       sortByFeatureAscending = true;
890     }
891     else
892     {
893       sortByFeatureAscending = !sortByFeatureAscending;
894     }
895     sortByFeatureCriteria = scoreCriteria;
896   }
897
898 }