JAL-4159 sometimes the progress bar may not be available - null check
[jalview.git] / src / jalview / analysis / AAFrequency.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.datamodel.AlignedCodonFrame;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.Annotation;
27 import jalview.datamodel.Profile;
28 import jalview.datamodel.ProfileI;
29 import jalview.datamodel.Profiles;
30 import jalview.datamodel.ProfilesI;
31 import jalview.datamodel.ResidueCount;
32 import jalview.datamodel.ResidueCount.SymbolCounts;
33 import jalview.datamodel.SecondaryStructureCount;
34 import jalview.datamodel.SeqCigar;
35 import jalview.datamodel.SequenceI;
36 import jalview.ext.android.SparseIntArray;
37 import jalview.util.Comparison;
38 import jalview.util.Constants;
39 import jalview.util.Format;
40 import jalview.util.MappingUtils;
41 import jalview.util.QuickSort;
42
43 import java.awt.Color;
44 import java.util.Arrays;
45 import java.util.Hashtable;
46 import java.util.List;
47
48 /**
49  * Takes in a vector or array of sequences and column start and column end and
50  * returns a new Hashtable[] of size maxSeqLength, if Hashtable not supplied.
51  * This class is used extensively in calculating alignment colourschemes that
52  * depend on the amount of conservation in each alignment column.
53  * 
54  * @author $author$
55  * @version $Revision$
56  */
57 public class AAFrequency
58 {
59   public static final String PROFILE = "P";
60
61   /*
62    * Quick look-up of String value of char 'A' to 'Z'
63    */
64   private static final String[] CHARS = new String['Z' - 'A' + 1];
65
66   static
67   {
68     for (char c = 'A'; c <= 'Z'; c++)
69     {
70       CHARS[c - 'A'] = String.valueOf(c);
71     }
72   }
73
74   public static final ProfilesI calculate(List<SequenceI> list, int start,
75           int end)
76   {
77     return calculate(list, start, end, false);
78   }
79
80   public static final ProfilesI calculate(List<SequenceI> sequences,
81           int start, int end, boolean profile)
82   {
83     SequenceI[] seqs = new SequenceI[sequences.size()];
84     int width = 0;
85     synchronized (sequences)
86     {
87       for (int i = 0; i < sequences.size(); i++)
88       {
89         seqs[i] = sequences.get(i);
90         int length = seqs[i].getLength();
91         if (length > width)
92         {
93           width = length;
94         }
95       }
96
97       if (end >= width)
98       {
99         end = width;
100       }
101
102       ProfilesI reply = calculate(seqs, width, start, end, profile);
103       return reply;
104     }
105   }
106
107   /**
108    * Calculate the consensus symbol(s) for each column in the given range.
109    * 
110    * @param sequences
111    * @param width
112    *          the full width of the alignment
113    * @param start
114    *          start column (inclusive, base zero)
115    * @param end
116    *          end column (exclusive)
117    * @param saveFullProfile
118    *          if true, store all symbol counts
119    */
120   public static final ProfilesI calculate(final SequenceI[] sequences,
121           int width, int start, int end, boolean saveFullProfile)
122   {
123     // long now = System.currentTimeMillis();
124     int seqCount = sequences.length;
125     boolean nucleotide = false;
126     int nucleotideCount = 0;
127     int peptideCount = 0;
128
129     ProfileI[] result = new ProfileI[width];
130
131     for (int column = start; column < end; column++)
132     {
133       /*
134        * Apply a heuristic to detect nucleotide data (which can
135        * be counted in more compact arrays); here we test for
136        * more than 90% nucleotide; recheck every 10 columns in case
137        * of misleading data e.g. highly conserved Alanine in peptide!
138        * Mistakenly guessing nucleotide has a small performance cost,
139        * as it will result in counting in sparse arrays.
140        * Mistakenly guessing peptide has a small space cost, 
141        * as it will use a larger than necessary array to hold counts. 
142        */
143       if (nucleotideCount > 100 && column % 10 == 0)
144       {
145         nucleotide = (9 * peptideCount < nucleotideCount);
146       }
147       ResidueCount residueCounts = new ResidueCount(nucleotide);
148
149       for (int row = 0; row < seqCount; row++)
150       {
151         if (sequences[row] == null)
152         {
153           jalview.bin.Console.errPrintln(
154                   "WARNING: Consensus skipping null sequence - possible race condition.");
155           continue;
156         }
157         if (sequences[row].getLength() > column)
158         {
159           char c = sequences[row].getCharAt(column);
160           residueCounts.add(c);
161           if (Comparison.isNucleotide(c))
162           {
163             nucleotideCount++;
164           }
165           else if (!Comparison.isGap(c))
166           {
167             peptideCount++;
168           }
169         }
170         else
171         {
172           /*
173            * count a gap if the sequence doesn't reach this column
174            */
175           residueCounts.addGap();
176         }
177       }
178
179       int maxCount = residueCounts.getModalCount();
180       String maxResidue = residueCounts.getResiduesForCount(maxCount);
181       int gapCount = residueCounts.getGapCount();
182       ProfileI profile = new Profile(seqCount, gapCount, maxCount,
183               maxResidue);
184
185       if (saveFullProfile)
186       {
187         profile.setCounts(residueCounts);
188       }
189
190       result[column] = profile;
191     }
192     return new Profiles(result);
193     // long elapsed = System.currentTimeMillis() - now;
194     // jalview.bin.Console.outPrintln(elapsed);
195   }
196
197   
198   public static final ProfilesI calculateSS(List<SequenceI> list, int start,
199           int end)
200   {
201     return calculateSS(list, start, end, false);
202   }
203   
204   public static final ProfilesI calculateSS(List<SequenceI> sequences,
205           int start, int end, boolean profile)
206   {
207     SequenceI[] seqs = new SequenceI[sequences.size()];
208     int width = 0;
209     synchronized (sequences)
210     {
211       for (int i = 0; i < sequences.size(); i++)
212       {
213         seqs[i] = sequences.get(i);
214         int length = seqs[i].getLength();
215         if (length > width)
216         {
217           width = length;
218         }
219       }
220
221       if (end >= width)
222       {
223         end = width;
224       }
225
226       ProfilesI reply = calculateSS(seqs, width, start, end, profile);
227       return reply;
228     }
229   }
230   
231   public static final ProfilesI calculateSS(final SequenceI[] sequences,
232           int width, int start, int end, boolean saveFullProfile)
233   {
234
235     int seqCount = sequences.length;
236     
237     ProfileI[] result = new ProfileI[width];
238
239     for (int column = start; column < end; column++)
240     {
241       
242       int ssCount = 0;
243     
244       SecondaryStructureCount ssCounts = new SecondaryStructureCount();
245
246       for (int row = 0; row < seqCount; row++)
247       {
248         if (sequences[row] == null)
249         {
250           jalview.bin.Console.errPrintln(
251                   "WARNING: Consensus skipping null sequence - possible race condition.");
252           continue;
253         }
254         
255         char c = sequences[row].getCharAt(column);
256         AlignmentAnnotation aa = AlignmentUtils.getDisplayedAlignmentAnnotation(sequences[row]);
257         if(aa!=null) {
258           ssCount++;
259         }
260         
261         if (sequences[row].getLength() > column && !Comparison.isGap(c) && aa !=null)
262         {
263           
264           int seqPosition = sequences[row].findPosition(column);
265           
266           char ss = AlignmentUtils.findSSAnnotationForGivenSeqposition(
267                   aa, seqPosition); 
268           if(ss == '*') {
269             continue;
270           }        
271           ssCounts.add(ss);                    
272         }
273         else if(Comparison.isGap(c) && aa!=null) {
274           ssCounts.addGap();
275         }
276       }
277
278       int maxSSCount = ssCounts.getModalCount();
279       String maxSS = ssCounts.getSSForCount(maxSSCount);
280       int gapCount = ssCounts.getGapCount();
281       ProfileI profile = new Profile(maxSS, ssCount, gapCount, 
282               maxSSCount);
283
284       if (saveFullProfile)
285       {
286         profile.setSSCounts(ssCounts);
287       }
288
289       result[column] = profile;
290     }
291     return new Profiles(result);
292   }
293
294   /**
295    * Make an estimate of the profile size we are going to compute i.e. how many
296    * different characters may be present in it. Overestimating has a cost of
297    * using more memory than necessary. Underestimating has a cost of needing to
298    * extend the SparseIntArray holding the profile counts.
299    * 
300    * @param profileSizes
301    *          counts of sizes of profiles so far encountered
302    * @return
303    */
304   static int estimateProfileSize(SparseIntArray profileSizes)
305   {
306     if (profileSizes.size() == 0)
307     {
308       return 4;
309     }
310
311     /*
312      * could do a statistical heuristic here e.g. 75%ile
313      * for now just return the largest value
314      */
315     return profileSizes.keyAt(profileSizes.size() - 1);
316   }
317
318   /**
319    * Derive the consensus annotations to be added to the alignment for display.
320    * This does not recompute the raw data, but may be called on a change in
321    * display options, such as 'ignore gaps', which may in turn result in a
322    * change in the derived values.
323    * 
324    * @param consensus
325    *          the annotation row to add annotations to
326    * @param profiles
327    *          the source consensus data
328    * @param startCol
329    *          start column (inclusive)
330    * @param endCol
331    *          end column (exclusive)
332    * @param ignoreGaps
333    *          if true, normalise residue percentages ignoring gaps
334    * @param showSequenceLogo
335    *          if true include all consensus symbols, else just show modal
336    *          residue
337    * @param nseq
338    *          number of sequences
339    */
340   public static void completeConsensus(AlignmentAnnotation consensus,
341           ProfilesI profiles, int startCol, int endCol, boolean ignoreGaps,
342           boolean showSequenceLogo, long nseq)
343   {
344     // long now = System.currentTimeMillis();
345     if (consensus == null || consensus.annotations == null
346             || consensus.annotations.length < endCol)
347     {
348       /*
349        * called with a bad alignment annotation row 
350        * wait for it to be initialised properly
351        */
352       return;
353     }
354
355     for (int i = startCol; i < endCol; i++)
356     {
357       ProfileI profile = profiles.get(i);
358       if (profile == null)
359       {
360         /*
361          * happens if sequences calculated over were 
362          * shorter than alignment width
363          */
364         consensus.annotations[i] = null;
365         return;
366       }
367
368       final int dp = getPercentageDp(nseq);
369
370       float value = profile.getPercentageIdentity(ignoreGaps);
371
372       String description = getTooltip(profile, value, showSequenceLogo,
373               ignoreGaps, dp);
374
375       String modalResidue = profile.getModalResidue();
376       if ("".equals(modalResidue))
377       {
378         modalResidue = "-";
379       }
380       else if (modalResidue.length() > 1)
381       {
382         modalResidue = "+";
383       }
384       consensus.annotations[i] = new Annotation(modalResidue, description,
385               ' ', value);
386     }
387     // long elapsed = System.currentTimeMillis() - now;
388     // jalview.bin.Console.outPrintln(-elapsed);
389   }
390   
391  
392  public static void completeSSConsensus(AlignmentAnnotation ssConsensus,
393          ProfilesI profiles, int startCol, int endCol, boolean ignoreGaps,
394          boolean showSequenceLogo, long nseq)
395  {
396    // long now = System.currentTimeMillis();
397    if (ssConsensus == null || ssConsensus.annotations == null
398            || ssConsensus.annotations.length < endCol)
399    {
400      /*
401       * called with a bad alignment annotation row 
402       * wait for it to be initialised properly
403       */
404      return;
405    }
406
407    for (int i = startCol; i < endCol; i++)
408    {
409      ProfileI profile = profiles.get(i);
410      if (profile == null)
411      {
412        /*
413         * happens if sequences calculated over were 
414         * shorter than alignment width
415         */
416        ssConsensus.annotations[i] = null;
417        return;
418      }
419
420      final int dp = getPercentageDp(nseq);
421
422      float value = profile.getSSPercentageIdentity(ignoreGaps);
423
424      String description = getSSTooltip(profile, value, showSequenceLogo,
425              ignoreGaps, dp);
426
427      String modalSS = profile.getModalSS();
428      if ("".equals(modalSS))
429      {
430        modalSS = "-";
431      }
432      else if (modalSS.length() > 1)
433      {
434        modalSS = "+";
435      }
436      ssConsensus.annotations[i] = new Annotation(modalSS, description,
437              ' ', value);
438    }
439    // long elapsed = System.currentTimeMillis() - now;
440    // jalview.bin.Console.outPrintln(-elapsed);
441  }
442
443   /**
444    * Derive the gap count annotation row.
445    * 
446    * @param gaprow
447    *          the annotation row to add annotations to
448    * @param profiles
449    *          the source consensus data
450    * @param startCol
451    *          start column (inclusive)
452    * @param endCol
453    *          end column (exclusive)
454    */
455   public static void completeGapAnnot(AlignmentAnnotation gaprow,
456           ProfilesI profiles, int startCol, int endCol, long nseq)
457   {
458     if (gaprow == null || gaprow.annotations == null
459             || gaprow.annotations.length < endCol)
460     {
461       /*
462        * called with a bad alignment annotation row 
463        * wait for it to be initialised properly
464        */
465       return;
466     }
467     // always set ranges again
468     gaprow.graphMax = nseq;
469     gaprow.graphMin = 0;
470     double scale = 0.8 / nseq;
471     for (int i = startCol; i < endCol; i++)
472     {
473       ProfileI profile = profiles.get(i);
474       if (profile == null)
475       {
476         /*
477          * happens if sequences calculated over were 
478          * shorter than alignment width
479          */
480         gaprow.annotations[i] = null;
481         return;
482       }
483
484       final int gapped = profile.getNonGapped();
485
486       String description = "" + gapped;
487
488       gaprow.annotations[i] = new Annotation("", description, '\0', gapped,
489               jalview.util.ColorUtils.bleachColour(Color.DARK_GRAY,
490                       (float) scale * gapped));
491     }
492   }
493
494   /**
495    * Returns a tooltip showing either
496    * <ul>
497    * <li>the full profile (percentages of all residues present), if
498    * showSequenceLogo is true, or</li>
499    * <li>just the modal (most common) residue(s), if showSequenceLogo is
500    * false</li>
501    * </ul>
502    * Percentages are as a fraction of all sequence, or only ungapped sequences
503    * if ignoreGaps is true.
504    * 
505    * @param profile
506    * @param pid
507    * @param showSequenceLogo
508    * @param ignoreGaps
509    * @param dp
510    *          the number of decimal places to format percentages to
511    * @return
512    */
513   static String getTooltip(ProfileI profile, float pid,
514           boolean showSequenceLogo, boolean ignoreGaps, int dp)
515   {
516     ResidueCount counts = profile.getCounts();
517
518     String description = null;
519     if (counts != null && showSequenceLogo)
520     {
521       int normaliseBy = ignoreGaps ? profile.getNonGapped()
522               : profile.getHeight();
523       description = counts.getTooltip(normaliseBy, dp);
524     }
525     else
526     {
527       StringBuilder sb = new StringBuilder(64);
528       String maxRes = profile.getModalResidue();
529       if (maxRes.length() > 1)
530       {
531         sb.append("[").append(maxRes).append("]");
532       }
533       else
534       {
535         sb.append(maxRes);
536       }
537       if (maxRes.length() > 0)
538       {
539         sb.append(" ");
540         Format.appendPercentage(sb, pid, dp);
541         sb.append("%");
542       }
543       description = sb.toString();
544     }
545     return description;
546   }
547   
548   static String getSSTooltip(ProfileI profile, float pid,
549           boolean showSequenceLogo, boolean ignoreGaps, int dp)
550   {
551     SecondaryStructureCount counts = profile.getSSCounts();
552
553     String description = null;
554     if (counts != null && showSequenceLogo)
555     {
556       int normaliseBy = ignoreGaps ? profile.getNonGapped()
557               : profile.getHeight();
558       description = counts.getTooltip(normaliseBy, dp);
559     }
560     else
561     {
562       StringBuilder sb = new StringBuilder(64);
563       String maxSS = profile.getModalSS();
564       if (maxSS.length() > 1)
565       {
566         sb.append("[").append(maxSS).append("]");
567       }
568       else
569       {
570         sb.append(maxSS);
571       }
572       if (maxSS.length() > 0)
573       {
574         sb.append(" ");
575         Format.appendPercentage(sb, pid, dp);
576         sb.append("%");
577       }
578       description = sb.toString();
579     }
580     return description;
581   }
582
583   /**
584    * Returns the sorted profile for the given consensus data. The returned array
585    * contains
586    * 
587    * <pre>
588    *    [profileType, numberOfValues, totalPercent, charValue1, percentage1, charValue2, percentage2, ...]
589    * in descending order of percentage value
590    * </pre>
591    * 
592    * @param profile
593    *          the data object from which to extract and sort values
594    * @param ignoreGaps
595    *          if true, only non-gapped values are included in percentage
596    *          calculations
597    * @return
598    */
599   public static int[] extractProfile(ProfileI profile, boolean ignoreGaps)
600   {
601     char[] symbols;
602     int[] values;
603     
604     if (profile.getCounts() != null)
605     {
606       ResidueCount counts = profile.getCounts();
607       SymbolCounts symbolCounts = counts.getSymbolCounts();
608       symbols = symbolCounts.symbols;
609       values = symbolCounts.values;
610       
611     }
612     else if(profile.getSSCounts() != null) 
613     {
614       SecondaryStructureCount counts = profile.getSSCounts();
615       // to do
616       SecondaryStructureCount.SymbolCounts symbolCounts = counts.getSymbolCounts();
617       symbols = symbolCounts.symbols;
618       values = symbolCounts.values;
619     }
620     else {
621       return null;
622     }
623     
624
625     QuickSort.sort(values, symbols);
626     int totalPercentage = 0;
627     final int divisor = ignoreGaps ? profile.getNonGapped()
628             : profile.getHeight();
629
630     /*
631      * traverse the arrays in reverse order (highest counts first)
632      */
633     int[] result = new int[3 + 2 * symbols.length];
634     int nextArrayPos = 3;
635     int nonZeroCount = 0;
636
637     for (int i = symbols.length - 1; i >= 0; i--)
638     {
639       int theChar = symbols[i];
640       int charCount = values[i];
641       final int percentage = (charCount * 100) / divisor;
642       if (percentage == 0)
643       {
644         /*
645          * this count (and any remaining) round down to 0% - discard
646          */
647         break;
648       }
649       nonZeroCount++;
650       result[nextArrayPos++] = theChar;
651       result[nextArrayPos++] = percentage;
652       totalPercentage += percentage;
653     }
654
655     /*
656      * truncate array if any zero values were discarded
657      */
658     if (nonZeroCount < symbols.length)
659     {
660       int[] tmp = new int[3 + 2 * nonZeroCount];
661       System.arraycopy(result, 0, tmp, 0, tmp.length);
662       result = tmp;
663     }
664
665     /*
666      * fill in 'header' values
667      */
668     result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
669     result[1] = nonZeroCount;
670     result[2] = totalPercentage;
671
672     return result;
673   }
674
675   /**
676    * Extract a sorted extract of cDNA codon profile data. The returned array
677    * contains
678    * 
679    * <pre>
680    *    [profileType, numberOfValues, totalPercentage, charValue1, percentage1, charValue2, percentage2, ...]
681    * in descending order of percentage value, where the character values encode codon triplets
682    * </pre>
683    * 
684    * @param hashtable
685    * @return
686    */
687   public static int[] extractCdnaProfile(
688           Hashtable<String, Object> hashtable, boolean ignoreGaps)
689   {
690     // this holds #seqs, #ungapped, and then codon count, indexed by encoded
691     // codon triplet
692     int[] codonCounts = (int[]) hashtable.get(PROFILE);
693     int[] sortedCounts = new int[codonCounts.length - 2];
694     System.arraycopy(codonCounts, 2, sortedCounts, 0,
695             codonCounts.length - 2);
696
697     int[] result = new int[3 + 2 * sortedCounts.length];
698     // first value is just the type of profile data
699     result[0] = AlignmentAnnotation.CDNA_PROFILE;
700
701     char[] codons = new char[sortedCounts.length];
702     for (int i = 0; i < codons.length; i++)
703     {
704       codons[i] = (char) i;
705     }
706     QuickSort.sort(sortedCounts, codons);
707     int totalPercentage = 0;
708     int distinctValuesCount = 0;
709     int j = 3;
710     int divisor = ignoreGaps ? codonCounts[1] : codonCounts[0];
711     for (int i = codons.length - 1; i >= 0; i--)
712     {
713       final int codonCount = sortedCounts[i];
714       if (codonCount == 0)
715       {
716         break; // nothing else of interest here
717       }
718       final int percentage = codonCount * 100 / divisor;
719       if (percentage == 0)
720       {
721         /*
722          * this (and any remaining) values rounded down to 0 - discard
723          */
724         break;
725       }
726       distinctValuesCount++;
727       result[j++] = codons[i];
728       result[j++] = percentage;
729       totalPercentage += percentage;
730     }
731     result[2] = totalPercentage;
732
733     /*
734      * Just return the non-zero values
735      */
736     // todo next value is redundant if we limit the array to non-zero counts
737     result[1] = distinctValuesCount;
738     return Arrays.copyOfRange(result, 0, j);
739   }
740
741   /**
742    * Compute a consensus for the cDNA coding for a protein alignment.
743    * 
744    * @param alignment
745    *          the protein alignment (which should hold mappings to cDNA
746    *          sequences)
747    * @param hconsensus
748    *          the consensus data stores to be populated (one per column)
749    */
750   public static void calculateCdna(AlignmentI alignment,
751           Hashtable<String, Object>[] hconsensus)
752   {
753     final char gapCharacter = alignment.getGapCharacter();
754     List<AlignedCodonFrame> mappings = alignment.getCodonFrames();
755     if (mappings == null || mappings.isEmpty())
756     {
757       return;
758     }
759
760     int cols = alignment.getWidth();
761     for (int col = 0; col < cols; col++)
762     {
763       // todo would prefer a Java bean for consensus data
764       Hashtable<String, Object> columnHash = new Hashtable<>();
765       // #seqs, #ungapped seqs, counts indexed by (codon encoded + 1)
766       int[] codonCounts = new int[66];
767       codonCounts[0] = alignment.getSequences().size();
768       int ungappedCount = 0;
769       for (SequenceI seq : alignment.getSequences())
770       {
771         if (seq.getCharAt(col) == gapCharacter)
772         {
773           continue;
774         }
775         List<char[]> codons = MappingUtils.findCodonsFor(seq, col,
776                 mappings);
777         for (char[] codon : codons)
778         {
779           int codonEncoded = CodingUtils.encodeCodon(codon);
780           if (codonEncoded >= 0)
781           {
782             codonCounts[codonEncoded + 2]++;
783             ungappedCount++;
784             break;
785           }
786         }
787       }
788       codonCounts[1] = ungappedCount;
789       // todo: sort values here, save counts and codons?
790       columnHash.put(PROFILE, codonCounts);
791       hconsensus[col] = columnHash;
792     }
793   }
794
795   /**
796    * Derive displayable cDNA consensus annotation from computed consensus data.
797    * 
798    * @param consensusAnnotation
799    *          the annotation row to be populated for display
800    * @param consensusData
801    *          the computed consensus data
802    * @param showProfileLogo
803    *          if true show all symbols present at each position, else only the
804    *          modal value
805    * @param nseqs
806    *          the number of sequences in the alignment
807    */
808   public static void completeCdnaConsensus(
809           AlignmentAnnotation consensusAnnotation,
810           Hashtable<String, Object>[] consensusData,
811           boolean showProfileLogo, int nseqs)
812   {
813     if (consensusAnnotation == null
814             || consensusAnnotation.annotations == null
815             || consensusAnnotation.annotations.length < consensusData.length)
816     {
817       // called with a bad alignment annotation row - wait for it to be
818       // initialised properly
819       return;
820     }
821
822     // ensure codon triplet scales with font size
823     consensusAnnotation.scaleColLabel = true;
824     for (int col = 0; col < consensusData.length; col++)
825     {
826       Hashtable<String, Object> hci = consensusData[col];
827       if (hci == null)
828       {
829         // gapped protein column?
830         continue;
831       }
832       // array holds #seqs, #ungapped, then codon counts indexed by codon
833       final int[] codonCounts = (int[]) hci.get(PROFILE);
834       int totalCount = 0;
835
836       /*
837        * First pass - get total count and find the highest
838        */
839       final char[] codons = new char[codonCounts.length - 2];
840       for (int j = 2; j < codonCounts.length; j++)
841       {
842         final int codonCount = codonCounts[j];
843         codons[j - 2] = (char) (j - 2);
844         totalCount += codonCount;
845       }
846
847       /*
848        * Sort array of encoded codons by count ascending - so the modal value
849        * goes to the end; start by copying the count (dropping the first value)
850        */
851       int[] sortedCodonCounts = new int[codonCounts.length - 2];
852       System.arraycopy(codonCounts, 2, sortedCodonCounts, 0,
853               codonCounts.length - 2);
854       QuickSort.sort(sortedCodonCounts, codons);
855
856       int modalCodonEncoded = codons[codons.length - 1];
857       int modalCodonCount = sortedCodonCounts[codons.length - 1];
858       String modalCodon = String
859               .valueOf(CodingUtils.decodeCodon(modalCodonEncoded));
860       if (sortedCodonCounts.length > 1 && sortedCodonCounts[codons.length
861               - 2] == sortedCodonCounts[codons.length - 1])
862       {
863         /*
864          * two or more codons share the modal count
865          */
866         modalCodon = "+";
867       }
868       float pid = sortedCodonCounts[sortedCodonCounts.length - 1] * 100
869               / (float) totalCount;
870
871       /*
872        * todo ? Replace consensus hashtable with sorted arrays of codons and
873        * counts (non-zero only). Include total count in count array [0].
874        */
875
876       /*
877        * Scan sorted array backwards for most frequent values first. Show
878        * repeated values compactly.
879        */
880       StringBuilder mouseOver = new StringBuilder(32);
881       StringBuilder samePercent = new StringBuilder();
882       String percent = null;
883       String lastPercent = null;
884       int percentDecPl = getPercentageDp(nseqs);
885
886       for (int j = codons.length - 1; j >= 0; j--)
887       {
888         int codonCount = sortedCodonCounts[j];
889         if (codonCount == 0)
890         {
891           /*
892            * remaining codons are 0% - ignore, but finish off the last one if
893            * necessary
894            */
895           if (samePercent.length() > 0)
896           {
897             mouseOver.append(samePercent).append(": ").append(percent)
898                     .append("% ");
899           }
900           break;
901         }
902         int codonEncoded = codons[j];
903         final int pct = codonCount * 100 / totalCount;
904         String codon = String
905                 .valueOf(CodingUtils.decodeCodon(codonEncoded));
906         StringBuilder sb = new StringBuilder();
907         Format.appendPercentage(sb, pct, percentDecPl);
908         percent = sb.toString();
909         if (showProfileLogo || codonCount == modalCodonCount)
910         {
911           if (percent.equals(lastPercent) && j > 0)
912           {
913             samePercent.append(samePercent.length() == 0 ? "" : ", ");
914             samePercent.append(codon);
915           }
916           else
917           {
918             if (samePercent.length() > 0)
919             {
920               mouseOver.append(samePercent).append(": ").append(lastPercent)
921                       .append("% ");
922             }
923             samePercent.setLength(0);
924             samePercent.append(codon);
925           }
926           lastPercent = percent;
927         }
928       }
929
930       consensusAnnotation.annotations[col] = new Annotation(modalCodon,
931               mouseOver.toString(), ' ', pid);
932     }
933   }
934
935   /**
936    * Returns the number of decimal places to show for profile percentages. For
937    * less than 100 sequences, returns zero (the integer percentage value will be
938    * displayed). For 100-999 sequences, returns 1, for 1000-9999 returns 2, etc.
939    * 
940    * @param nseq
941    * @return
942    */
943   protected static int getPercentageDp(long nseq)
944   {
945     int scale = 0;
946     while (nseq >= 100)
947     {
948       scale++;
949       nseq /= 10;
950     }
951     return scale;
952   }
953 }