Merge branch 'bug/JAL-98consensusMemory' into develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 26 Oct 2016 15:22:45 +0000 (16:22 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 26 Oct 2016 15:22:45 +0000 (16:22 +0100)
27 files changed:
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/Conservation.java
src/jalview/analysis/Profile.java [new file with mode: 0644]
src/jalview/analysis/ResidueCount.java [new file with mode: 0644]
src/jalview/api/AlignViewportI.java
src/jalview/datamodel/SequenceGroup.java
src/jalview/ext/android/ContainerHelpers.java [new file with mode: 0644]
src/jalview/ext/android/SparseIntArray.java [new file with mode: 0644]
src/jalview/ext/android/SparseShortArray.java [new file with mode: 0644]
src/jalview/renderer/AnnotationRenderer.java
src/jalview/schemes/Blosum62ColourScheme.java
src/jalview/schemes/ColourSchemeI.java
src/jalview/schemes/FollowerColourScheme.java
src/jalview/schemes/PIDColourScheme.java
src/jalview/schemes/ResidueColourScheme.java
src/jalview/util/Format.java
src/jalview/util/SparseCount.java [new file with mode: 0644]
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/ComplementConsensusThread.java
src/jalview/workers/ConsensusThread.java
test/jalview/analysis/AAFrequencyTest.java
test/jalview/analysis/ResidueCountTest.java [new file with mode: 0644]
test/jalview/ext/android/SparseIntArrayTest.java [new file with mode: 0644]
test/jalview/ext/android/SparseShortArrayTest.java [new file with mode: 0644]
test/jalview/schemes/ResidueColourSchemeTest.java [new file with mode: 0644]
test/jalview/util/FormatTest.java [new file with mode: 0644]
test/jalview/util/SparseCountTest.java [new file with mode: 0644]

index fb49541..61e11c4 100755 (executable)
  */
 package jalview.analysis;
 
+import jalview.analysis.ResidueCount.SymbolCounts;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.SequenceI;
+import jalview.ext.android.SparseIntArray;
+import jalview.util.Comparison;
 import jalview.util.Format;
 import jalview.util.MappingUtils;
 import jalview.util.QuickSort;
@@ -44,20 +47,8 @@ import java.util.List;
  */
 public class AAFrequency
 {
-  private static final int TO_UPPER_CASE = 'A' - 'a'; // -32
-
-  public static final String MAXCOUNT = "C";
-
-  public static final String MAXRESIDUE = "R";
-
-  public static final String PID_GAPS = "G";
-
-  public static final String PID_NOGAPS = "N";
-
   public static final String PROFILE = "P";
 
-  public static final String ENCODED_CHARS = "E";
-
   /*
    * Quick look-up of String value of char 'A' to 'Z'
    */
@@ -71,13 +62,13 @@ public class AAFrequency
     }
   }
 
-  public static final Hashtable[] calculate(List<SequenceI> list,
+  public static final Profile[] calculate(List<SequenceI> list,
           int start, int end)
   {
     return calculate(list, start, end, false);
   }
 
-  public static final Hashtable[] calculate(List<SequenceI> sequences,
+  public static final Profile[] calculate(List<SequenceI> sequences,
           int start, int end, boolean profile)
   {
     SequenceI[] seqs = new SequenceI[sequences.size()];
@@ -93,7 +84,7 @@ public class AAFrequency
         }
       }
 
-      Hashtable[] reply = new Hashtable[width];
+      Profile[] reply = new Profile[width];
 
       if (end >= width)
       {
@@ -105,289 +96,242 @@ public class AAFrequency
     }
   }
 
-  public static final void calculate(SequenceI[] sequences, int start,
-          int end, Hashtable[] result, boolean profile)
+  /**
+   * Calculate the consensus symbol(s) for each column in the given range.
+   * 
+   * @param sequences
+   * @param start
+   *          start column (inclusive, base zero)
+   * @param end
+   *          end column (exclusive)
+   * @param result
+   *          array in which to store profile per column
+   * @param saveFullProfile
+   *          if true, store all symbol counts
+   */
+  public static final void calculate(final SequenceI[] sequences,
+          int start, int end, Profile[] result, boolean saveFullProfile)
   {
-    Hashtable residueHash;
-    int maxCount, nongap, i, j, v;
-    int jSize = sequences.length;
-    String maxResidue;
-    char c = '-';
-    float percentage;
+    // long now = System.currentTimeMillis();
+    int seqCount = sequences.length;
+    boolean nucleotide = false;
+    int nucleotideCount = 0;
+    int peptideCount = 0;
 
-    int[] values = new int[255];
-
-    char[] seq;
-
-    for (i = start; i < end; i++)
+    for (int column = start; column < end; column++)
     {
-      residueHash = new Hashtable();
-      maxCount = 0;
-      maxResidue = "";
-      nongap = 0;
-      values = new int[255];
+      /*
+       * Apply a heuristic to detect nucleotide data (which can
+       * be counted in more compact arrays); here we test for
+       * more than 90% nucleotide; recheck every 10 columns in case
+       * of misleading data e.g. highly conserved Alanine in peptide!
+       * Mistakenly guessing nucleotide has a small performance cost,
+       * as it will result in counting in sparse arrays.
+       * Mistakenly guessing peptide has a small space cost, 
+       * as it will use a larger than necessary array to hold counts. 
+       */
+      if (nucleotideCount > 100 && column % 10 == 0)
+      {
+        nucleotide = (9 * peptideCount < nucleotideCount);
+      }
+      ResidueCount residueCounts = new ResidueCount(nucleotide);
 
-      for (j = 0; j < jSize; j++)
+      for (int row = 0; row < seqCount; row++)
       {
-        if (sequences[j] == null)
+        if (sequences[row] == null)
         {
           System.err
                   .println("WARNING: Consensus skipping null sequence - possible race condition.");
           continue;
         }
-        seq = sequences[j].getSequence();
-        if (seq.length > i)
+        char[] seq = sequences[row].getSequence();
+        if (seq.length > column)
         {
-          c = seq[i];
-
-          if (c == '.' || c == ' ')
+          char c = seq[column];
+          residueCounts.add(c);
+          if (Comparison.isNucleotide(c))
           {
-            c = '-';
+            nucleotideCount++;
           }
-
-          if (c == '-')
+          else if (!Comparison.isGap(c))
           {
-            values['-']++;
-            continue;
+            peptideCount++;
           }
-          else if ('a' <= c && c <= 'z')
-          {
-            c += TO_UPPER_CASE;
-          }
-
-          nongap++;
-          values[c]++;
-
         }
         else
         {
-          values['-']++;
-        }
-      }
-      if (jSize == 1)
-      {
-        maxResidue = String.valueOf(c);
-        maxCount = 1;
-      }
-      else
-      {
-        for (v = 'A'; v <= 'Z'; v++)
-        {
-          // TODO why ignore values[v] == 1?
-          if (values[v] < 1 /* 2 */|| values[v] < maxCount)
-          {
-            continue;
-          }
-
-          if (values[v] > maxCount)
-          {
-            maxResidue = CHARS[v - 'A'];
-          }
-          else if (values[v] == maxCount)
-          {
-            maxResidue += CHARS[v - 'A'];
-          }
-          maxCount = values[v];
+          /*
+           * count a gap if the sequence doesn't reach this column
+           */
+          residueCounts.addGap();
         }
       }
-      if (maxResidue.length() == 0)
-      {
-        maxResidue = "-";
-      }
-      if (profile)
-      {
-        // TODO use a 1-dimensional array with jSize, nongap in [0] and [1]
-        residueHash.put(PROFILE, new int[][] { values,
-            new int[] { jSize, nongap } });
-      }
-      residueHash.put(MAXCOUNT, new Integer(maxCount));
-      residueHash.put(MAXRESIDUE, maxResidue);
 
-      percentage = ((float) maxCount * 100) / jSize;
-      residueHash.put(PID_GAPS, new Float(percentage));
+      int maxCount = residueCounts.getModalCount();
+      String maxResidue = residueCounts.getResiduesForCount(maxCount);
+      int gapCount = residueCounts.getGapCount();
+      Profile profile = new Profile(seqCount, gapCount, maxCount,
+              maxResidue);
 
-      if (nongap > 0)
+      if (saveFullProfile)
       {
-        // calculate for non-gapped too
-        percentage = ((float) maxCount * 100) / nongap;
+        profile.setCounts(residueCounts);
       }
-      residueHash.put(PID_NOGAPS, new Float(percentage));
 
-      result[i] = residueHash;
+      result[column] = profile;
     }
+    // long elapsed = System.currentTimeMillis() - now;
+    // System.out.println(elapsed);
   }
 
   /**
-   * Compute all or part of the annotation row from the given consensus
-   * hashtable
+   * Make an estimate of the profile size we are going to compute i.e. how many
+   * different characters may be present in it. Overestimating has a cost of
+   * using more memory than necessary. Underestimating has a cost of needing to
+   * extend the SparseIntArray holding the profile counts.
    * 
-   * @param consensus
-   *          - pre-allocated annotation row
-   * @param hconsensus
-   * @param iStart
-   * @param width
-   * @param ignoreGapsInConsensusCalculation
-   * @param includeAllConsSymbols
-   * @param nseq
+   * @param profileSizes
+   *          counts of sizes of profiles so far encountered
+   * @return
    */
-  public static void completeConsensus(AlignmentAnnotation consensus,
-          Hashtable[] hconsensus, int iStart, int width,
-          boolean ignoreGapsInConsensusCalculation,
-          boolean includeAllConsSymbols, long nseq)
+  static int estimateProfileSize(SparseIntArray profileSizes)
   {
-    completeConsensus(consensus, hconsensus, iStart, width,
-            ignoreGapsInConsensusCalculation, includeAllConsSymbols, null,
-            nseq);
+    if (profileSizes.size() == 0)
+    {
+      return 4;
+    }
+
+    /*
+     * could do a statistical heuristic here e.g. 75%ile
+     * for now just return the largest value
+     */
+    return profileSizes.keyAt(profileSizes.size() - 1);
   }
 
   /**
    * Derive the consensus annotations to be added to the alignment for display.
    * This does not recompute the raw data, but may be called on a change in
-   * display options, such as 'show logo', which may in turn result in a change
-   * in the derived values.
+   * display options, such as 'ignore gaps', which may in turn result in a
+   * change in the derived values.
    * 
    * @param consensus
    *          the annotation row to add annotations to
-   * @param hconsensus
+   * @param profiles
    *          the source consensus data
    * @param iStart
    *          start column
    * @param width
    *          end column
-   * @param ignoreGapsInConsensusCalculation
-   *          if true, use the consensus calculated ignoring gaps
-   * @param includeAllConsSymbols
+   * @param ignoreGaps
+   *          if true, normalise residue percentages ignoring gaps
+   * @param showSequenceLogo
    *          if true include all consensus symbols, else just show modal
    *          residue
-   * @param alphabet
    * @param nseq
    *          number of sequences
    */
   public static void completeConsensus(AlignmentAnnotation consensus,
-          Hashtable[] hconsensus, int iStart, int width,
-          boolean ignoreGapsInConsensusCalculation,
-          boolean includeAllConsSymbols, char[] alphabet, long nseq)
+          Profile[] profiles, int iStart, int width, boolean ignoreGaps,
+          boolean showSequenceLogo, long nseq)
   {
+    // long now = System.currentTimeMillis();
     if (consensus == null || consensus.annotations == null
             || consensus.annotations.length < width)
     {
-      // called with a bad alignment annotation row - wait for it to be
-      // initialised properly
+      /*
+       * called with a bad alignment annotation row 
+       * wait for it to be initialised properly
+       */
       return;
     }
 
-    final Format fmt = getPercentageFormat(nseq);
+    final int dp = getPercentageDp(nseq);
 
     for (int i = iStart; i < width; i++)
     {
-      Hashtable hci;
-      if (i >= hconsensus.length || ((hci = hconsensus[i]) == null))
-      {
-        // happens if sequences calculated over were shorter than alignment
-        // width
-        consensus.annotations[i] = null;
-        continue;
-      }
-      Float fv = (Float) hci
-              .get(ignoreGapsInConsensusCalculation ? PID_NOGAPS : PID_GAPS);
-      if (fv == null)
+      Profile profile;
+      if (i >= profiles.length || ((profile = profiles[i]) == null))
       {
+        /*
+         * happens if sequences calculated over were 
+         * shorter than alignment width
+         */
         consensus.annotations[i] = null;
-        // data has changed below us .. give up and
         continue;
       }
-      float value = fv.floatValue();
-      String maxRes = hci.get(AAFrequency.MAXRESIDUE).toString();
-      StringBuilder mouseOver = new StringBuilder(64);
-      if (maxRes.length() > 1)
-      {
-        mouseOver.append("[").append(maxRes).append("] ");
-        maxRes = "+";
-      }
-      else
-      {
-        mouseOver.append(hci.get(AAFrequency.MAXRESIDUE) + " ");
-      }
-      int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE);
-      if (profile != null && includeAllConsSymbols)
+
+      float value = profile.getPercentageIdentity(ignoreGaps);
+
+      String description = getTooltip(profile, value, showSequenceLogo,
+              ignoreGaps, dp);
+
+      String modalResidue = profile.getModalResidue();
+      if ("".equals(modalResidue))
       {
-        int sequenceCount = profile[1][0];
-        int nonGappedCount = profile[1][1];
-        int normalisedBy = ignoreGapsInConsensusCalculation ? nonGappedCount
-                : sequenceCount;
-        mouseOver.setLength(0);
-        if (alphabet != null)
-        {
-          for (int c = 0; c < alphabet.length; c++)
-          {
-            float tval = profile[0][alphabet[c]] * 100f / normalisedBy;
-            mouseOver
-                    .append(((c == 0) ? "" : "; "))
-                    .append(alphabet[c])
-                    .append(" ")
-                    .append(((fmt != null) ? fmt.form(tval) : ((int) tval)))
-                    .append("%");
-          }
-        }
-        else
-        {
-          // TODO do this sort once only in calculate()?
-          // char[][] ca = new char[profile[0].length][];
-          char[] ca = new char[profile[0].length];
-          float[] vl = new float[profile[0].length];
-          for (int c = 0; c < ca.length; c++)
-          {
-            ca[c] = (char) c;
-            // ca[c] = new char[]
-            // { (char) c };
-            vl[c] = profile[0][c];
-          }
-          QuickSort.sort(vl, ca);
-          for (int p = 0, c = ca.length - 1; profile[0][ca[c]] > 0; c--)
-          {
-            final char residue = ca[c];
-            if (residue != '-')
-            {
-              float tval = profile[0][residue] * 100f / normalisedBy;
-              mouseOver
-                      .append((((p == 0) ? "" : "; ")))
-                      .append(residue)
-                      .append(" ")
-                      .append(((fmt != null) ? fmt.form(tval)
-                              : ((int) tval))).append("%");
-              p++;
-            }
-          }
-        }
+        modalResidue = "-";
       }
-      else
+      else if (modalResidue.length() > 1)
       {
-        mouseOver.append(
-                (((fmt != null) ? fmt.form(value) : ((int) value))))
-                .append("%");
+        modalResidue = "+";
       }
-      consensus.annotations[i] = new Annotation(maxRes,
-              mouseOver.toString(), ' ', value);
+      consensus.annotations[i] = new Annotation(modalResidue,
+              description, ' ', value);
     }
+    // long elapsed = System.currentTimeMillis() - now;
+    // System.out.println(-elapsed);
   }
 
   /**
-   * Returns a Format designed to show all significant figures for profile
-   * percentages. For less than 100 sequences, returns null (the integer
-   * percentage value will be displayed). For 100-999 sequences, returns "%3.1f"
+   * Returns a tooltip showing either
+   * <ul>
+   * <li>the full profile (percentages of all residues present), if
+   * showSequenceLogo is true, or</li>
+   * <li>just the modal (most common) residue(s), if showSequenceLogo is false</li>
+   * </ul>
+   * Percentages are as a fraction of all sequence, or only ungapped sequences
+   * if ignoreGaps is true.
    * 
-   * @param nseq
+   * @param profile
+   * @param pid
+   * @param showSequenceLogo
+   * @param ignoreGaps
+   * @param dp
+   *          the number of decimal places to format percentages to
    * @return
    */
-  protected static Format getPercentageFormat(long nseq)
+  static String getTooltip(Profile profile, float pid,
+          boolean showSequenceLogo, boolean ignoreGaps, int dp)
   {
-    int scale = 0;
-    while (nseq >= 10)
+    ResidueCount counts = profile.getCounts();
+
+    String description = null;
+    if (counts != null && showSequenceLogo)
     {
-      scale++;
-      nseq /= 10;
+      int normaliseBy = ignoreGaps ? profile.getNonGapped() : profile
+              .getHeight();
+      description = counts.getTooltip(normaliseBy, dp);
+    }
+    else
+    {
+      StringBuilder sb = new StringBuilder(64);
+      String maxRes = profile.getModalResidue();
+      if (maxRes.length() > 1)
+      {
+        sb.append("[").append(maxRes).append("]");
+      }
+      else
+      {
+        sb.append(maxRes);
+      }
+      if (maxRes.length() > 0)
+      {
+        sb.append(" ");
+        Format.appendPercentage(sb, pid, dp);
+        sb.append("%");
+      }
+      description = sb.toString();
     }
-    return scale <= 1 ? null : new Format("%3." + (scale - 1) + "f");
+    return description;
   }
 
   /**
@@ -399,46 +343,46 @@ public class AAFrequency
    * in descending order of percentage value
    * </pre>
    * 
-   * @param hconsensus
-   *          the data table from which to extract and sort values
+   * @param profile
+   *          the data object from which to extract and sort values
    * @param ignoreGaps
    *          if true, only non-gapped values are included in percentage
    *          calculations
    * @return
    */
-  public static int[] extractProfile(Hashtable hconsensus,
+  public static int[] extractProfile(Profile profile,
           boolean ignoreGaps)
   {
     int[] rtnval = new int[64];
-    int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE);
-    if (profile == null)
+    ResidueCount counts = profile.getCounts();
+    if (counts == null)
     {
       return null;
     }
-    char[] ca = new char[profile[0].length];
-    float[] vl = new float[profile[0].length];
-    for (int c = 0; c < ca.length; c++)
-    {
-      ca[c] = (char) c;
-      vl[c] = profile[0][c];
-    }
-    QuickSort.sort(vl, ca);
+
+    SymbolCounts symbolCounts = counts.getSymbolCounts();
+    char[] symbols = symbolCounts.symbols;
+    int[] values = symbolCounts.values;
+    QuickSort.sort(values, symbols);
     int nextArrayPos = 2;
     int totalPercentage = 0;
-    int distinctValuesCount = 0;
-    final int divisor = profile[1][ignoreGaps ? 1 : 0];
-    for (int c = ca.length - 1; profile[0][ca[c]] > 0; c--)
+    final int divisor = ignoreGaps ? profile.getNonGapped() : profile
+            .getHeight();
+
+    /*
+     * traverse the arrays in reverse order (highest counts first)
+     */
+    for (int i = symbols.length - 1; i >= 0; i--)
     {
-      if (ca[c] != '-')
-      {
-        rtnval[nextArrayPos++] = ca[c];
-        final int percentage = (int) (profile[0][ca[c]] * 100f / divisor);
-        rtnval[nextArrayPos++] = percentage;
-        totalPercentage += percentage;
-        distinctValuesCount++;
-      }
+      int theChar = symbols[i];
+      int charCount = values[i];
+
+      rtnval[nextArrayPos++] = theChar;
+      final int percentage = (charCount * 100) / divisor;
+      rtnval[nextArrayPos++] = percentage;
+      totalPercentage += percentage;
     }
-    rtnval[0] = distinctValuesCount;
+    rtnval[0] = symbols.length;
     rtnval[1] = totalPercentage;
     int[] result = new int[rtnval.length + 1];
     result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
@@ -647,7 +591,7 @@ public class AAFrequency
       StringBuilder samePercent = new StringBuilder();
       String percent = null;
       String lastPercent = null;
-      Format fmt = getPercentageFormat(nseqs);
+      int percentDecPl = getPercentageDp(nseqs);
 
       for (int j = codons.length - 1; j >= 0; j--)
       {
@@ -669,7 +613,9 @@ public class AAFrequency
         final int pct = codonCount * 100 / totalCount;
         String codon = String
                 .valueOf(CodingUtils.decodeCodon(codonEncoded));
-        percent = fmt == null ? Integer.toString(pct) : fmt.form(pct);
+        StringBuilder sb = new StringBuilder();
+        Format.appendPercentage(sb, pct, percentDecPl);
+        percent = sb.toString();
         if (showProfileLogo || codonCount == modalCodonCount)
         {
           if (percent.equals(lastPercent) && j > 0)
@@ -695,4 +641,23 @@ public class AAFrequency
               mouseOver.toString(), ' ', pid);
     }
   }
+
+  /**
+   * Returns the number of decimal places to show for profile percentages. For
+   * less than 100 sequences, returns zero (the integer percentage value will be
+   * displayed). For 100-999 sequences, returns 1, for 1000-9999 returns 2, etc.
+   * 
+   * @param nseq
+   * @return
+   */
+  protected static int getPercentageDp(long nseq)
+  {
+    int scale = 0;
+    while (nseq >= 100)
+    {
+      scale++;
+      nseq /= 10;
+    }
+    return scale;
+  }
 }
index 75dec63..8127747 100755 (executable)
@@ -24,12 +24,13 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.ext.android.SparseIntArray;
 import jalview.schemes.ResidueProperties;
 
 import java.awt.Color;
-import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.Vector;
 
 /**
@@ -188,21 +189,22 @@ public class Conservation
    */
   public void calculate()
   {
-    int thresh, j, jSize = sequences.length;
-    int[] values; // Replaces residueHash
-    char c;
+    int jSize = sequences.length;
+    // int[] values; // Replaces residueHash
+    SparseIntArray values = new SparseIntArray();
 
-    total = new Hashtable[maxLength];
+    total = new Map[maxLength];
 
     for (int i = start; i <= end; i++)
     {
-      values = new int[255];
+      // values = new int[255];
+      values.clear();
 
-      for (j = 0; j < jSize; j++)
+      for (int j = 0; j < jSize; j++)
       {
         if (sequences[j].getLength() > i)
         {
-          c = sequences[j].getCharAt(i);
+          char c = sequences[j].getCharAt(i);
 
           if (canonicaliseAa)
           { // lookup the base aa code symbol
@@ -228,23 +230,29 @@ public class Conservation
 
             c = toUpperCase(c);
           }
-          values[c]++;
+          // values[c]++;
+          values.add(c, 1);
         }
         else
         {
-          values['-']++;
+          // values['-']++;
+          values.add('-', 1);
         }
       }
 
       // What is the count threshold to count the residues in residueHash()
-      thresh = (threshold * (jSize)) / 100;
+      int thresh = (threshold * jSize) / 100;
 
       // loop over all the found residues
-      Hashtable<String, Integer> resultHash = new Hashtable<String, Integer>();
-      for (char v = '-'; v < 'Z'; v++)
+      // Hashtable<String, Integer> resultHash = new Hashtable<String,
+      // Integer>();
+      Map<String, Integer> resultHash = new TreeMap<String, Integer>();
+      // for (char v = '-'; v < 'Z'; v++)
+      for (int key = 0; key < values.size(); key++)
       {
-
-        if (values[v] > thresh)
+        char v = (char) values.keyAt(key);
+        // if (values[v] > thresh)
+        if (values.valueAt(key) > thresh)
         {
           String res = String.valueOf(v);
 
@@ -359,7 +367,7 @@ public class Conservation
    */
   public void verdict(boolean consflag, float percentageGaps)
   {
-    StringBuffer consString = new StringBuffer();
+    StringBuilder consString = new StringBuilder(end);
 
     // NOTE THIS SHOULD CHECK IF THE CONSEQUENCE ALREADY
     // EXISTS AND NOT OVERWRITE WITH '-', BUT THIS CASE
@@ -374,7 +382,9 @@ public class Conservation
       int[] gapcons = countConsNGaps(i);
       int totGaps = gapcons[1];
       float pgaps = ((float) totGaps * 100) / sequences.length;
-      consSymbs[i - start] = new String();
+      StringBuilder positives = new StringBuilder(64);
+      StringBuilder negatives = new StringBuilder(32);
+      // consSymbs[i - start] = "";
 
       if (percentageGaps > pgaps)
       {
@@ -389,7 +399,9 @@ public class Conservation
           {
             if (result == 1)
             {
-              consSymbs[i - start] = type + " " + consSymbs[i - start];
+              // consSymbs[i - start] = type + " " + consSymbs[i - start];
+              positives.append(positives.length() == 0 ? "" : " ");
+              positives.append(type);
               count++;
             }
           }
@@ -399,16 +411,31 @@ public class Conservation
             {
               if (result == 0)
               {
-                consSymbs[i - start] = consSymbs[i - start] + " !" + type;
+                /*
+                 * add negatively conserved properties on the end
+                 */
+                // consSymbs[i - start] = consSymbs[i - start] + " !" + type;
+                negatives.append(negatives.length() == 0 ? "" : " ");
+                negatives.append("!").append(type);
               }
               else
               {
-                consSymbs[i - start] = type + " " + consSymbs[i - start];
+                /*
+                 * put positively conserved properties on the front
+                 */
+                // consSymbs[i - start] = type + " " + consSymbs[i - start];
+                positives.append(positives.length() == 0 ? "" : " ");
+                positives.append(type);
               }
               count++;
             }
           }
         }
+        if (negatives.length() > 0)
+        {
+          positives.append(" ").append(negatives);
+        }
+        consSymbs[i - start] = positives.toString();
 
         if (count < 10)
         {
diff --git a/src/jalview/analysis/Profile.java b/src/jalview/analysis/Profile.java
new file mode 100644 (file)
index 0000000..d94d031
--- /dev/null
@@ -0,0 +1,157 @@
+package jalview.analysis;
+
+
+/**
+ * A data bean to hold the result of computing a profile for a column of an
+ * alignment
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class Profile
+{
+  /*
+   * counts of keys (chars)
+   */
+  private ResidueCount counts;
+
+  /*
+   * the number of sequences in the profile
+   */
+  private int height;
+
+  /*
+   * the number of non-gapped sequences in the profile
+   */
+  private int gapped;
+
+  /*
+   * the highest count for any residue in the profile
+   */
+  private int maxCount;
+
+  /*
+   * the residue (e.g. K) or residues (e.g. KQW) with the
+   * highest count in the profile
+   */
+  private String modalResidue;
+
+  /**
+   * Constructor which allows derived data to be stored without having to store
+   * the full profile
+   * 
+   * @param seqCount
+   *          the number of sequences in the profile
+   * @param gaps
+   *          the number of gapped sequences
+   * @param max
+   *          the highest count for any residue
+   * @param modalres
+   *          the residue (or concatenated residues) with the highest count
+   */
+  public Profile(int seqCount, int gaps, int max, String modalRes)
+  {
+    this.height = seqCount;
+    this.gapped = gaps;
+    this.maxCount = max;
+    this.modalResidue = modalRes;
+  }
+
+  /**
+   * Set the full profile of counts
+   * 
+   * @param residueCounts
+   */
+  public void setCounts(ResidueCount residueCounts)
+  {
+    this.counts = residueCounts;
+  }
+
+  /**
+   * Returns the percentage identity of the profile, i.e. the highest proportion
+   * of conserved (equal) symbols. The percentage is as a fraction of all
+   * sequences, or only ungapped sequences if flag ignoreGaps is set true.
+   * 
+   * @param ignoreGaps
+   * @return
+   */
+  public float getPercentageIdentity(boolean ignoreGaps)
+  {
+    if (height == 0)
+    {
+      return 0f;
+    }
+    float pid = 0f;
+    if (ignoreGaps && gapped < height)
+    {
+      pid = (maxCount * 100f) / (height - gapped);
+    }
+    else
+    {
+      pid = (maxCount * 100f) / height;
+    }
+    return pid;
+  }
+
+  /**
+   * Returns the full symbol counts for this profile
+   * 
+   * @return
+   */
+  public ResidueCount getCounts()
+  {
+    return counts;
+  }
+
+  /**
+   * Returns the number of sequences in the profile
+   * 
+   * @return
+   */
+  public int getHeight()
+  {
+    return height;
+  }
+
+  /**
+   * Returns the number of sequences in the profile which had a gap character
+   * (or were too short to be included in this column's profile)
+   * 
+   * @return
+   */
+  public int getGapped()
+  {
+    return gapped;
+  }
+
+  /**
+   * Returns the highest count for any symbol(s) in the profile
+   * 
+   * @return
+   */
+  public int getMaxCount()
+  {
+    return maxCount;
+  }
+
+  /**
+   * Returns the symbol (or concatenated symbols) which have the highest count
+   * in the profile, or an empty string if there were no symbols counted
+   * 
+   * @return
+   */
+  public String getModalResidue()
+  {
+    return modalResidue;
+  }
+
+  /**
+   * Answers the number of non-gapped sequences in the profile
+   * 
+   * @return
+   */
+  public int getNonGapped()
+  {
+    return height - gapped;
+  }
+}
diff --git a/src/jalview/analysis/ResidueCount.java b/src/jalview/analysis/ResidueCount.java
new file mode 100644 (file)
index 0000000..75decf2
--- /dev/null
@@ -0,0 +1,621 @@
+package jalview.analysis;
+
+import jalview.util.Comparison;
+import jalview.util.Format;
+import jalview.util.QuickSort;
+import jalview.util.SparseCount;
+
+/**
+ * A class to count occurrences of residues in a profile, optimised for speed
+ * and memory footprint.
+ * @author gmcarstairs
+ *
+ */
+public class ResidueCount
+{
+  /**
+   * A data bean to hold the results of counting symbols
+   */
+  public class SymbolCounts
+  {
+    /**
+     * the symbols seen (as char values), in no particular order
+     */
+    public final char[] symbols;
+
+    /**
+     * the counts for each symbol, in the same order as the symbols
+     */
+    public final int[] values;
+
+    SymbolCounts(char[] s, int[] v)
+    {
+      symbols = s;
+      values = v;
+    }
+  }
+
+  private static final int TOUPPERCASE = 'A' - 'a';
+
+  /*
+   * nucleotide symbols to count (including N unknown)
+   */
+  private static final String NUCS = "ACGNTU";
+
+  /*
+   * amino acid symbols to count (including X unknown)
+   * NB we also include U so as to support counting of RNA bases
+   * in the "don't know" case of nucleotide / peptide
+   */
+  private static final String AAS = "ACDEFGHIKLMNPQRSTUVWXY";
+
+  private static final int GAP_COUNT = 0;
+
+  /*
+   * fast lookup tables holding the index into our count
+   * arrays of each symbol; index 0 is reserved for gap counting
+   */
+  private static int[] NUC_INDEX = new int[26];
+
+  private static int[] AA_INDEX = new int[26];
+  static
+  {
+    for (int i = 0; i < NUCS.length(); i++)
+    {
+      NUC_INDEX[NUCS.charAt(i) - 'A'] = i + 1;
+    }
+    for (int i = 0; i < AAS.length(); i++)
+    {
+      AA_INDEX[AAS.charAt(i) - 'A'] = i + 1;
+    }
+  }
+
+  /*
+   * counts array, just big enough for the nucleotide or peptide
+   * character set (plus gap counts in position 0)
+   */
+  private short[] counts;
+
+  /*
+   * alternative array of int counts for use if any count 
+   * exceeds the maximum value of short (32767)
+   */
+  private int[] intCounts;
+
+  /*
+   * flag set if we switch from short to int counts
+   */
+  private boolean useIntCounts;
+
+  /*
+   * general-purpose counter, only for use for characters
+   * that are not in the expected alphabet
+   */
+  private SparseCount otherData;
+
+  /*
+   * keeps track of the maximum count value recorded
+   * (if this class every allows decrements, would need to
+   * calculate this on request instead) 
+   */
+  int maxCount;
+
+  /*
+   * if we think we are counting nucleotide, can get by with smaller
+   * array to hold counts
+   */
+  private boolean isNucleotide;
+
+  /**
+   * Default constructor allocates arrays able to count either nucleotide or
+   * peptide bases. Use this constructor if not sure which the data is.
+   */
+  public ResidueCount()
+  {
+    this(false);
+  }
+
+  /**
+   * Constructor that allocates an array just big enough for the anticipated
+   * characters, plus one position to count gaps
+   */
+  public ResidueCount(boolean nucleotide)
+  {
+    isNucleotide = nucleotide;
+    int charsToCount = nucleotide ? NUCS.length() : AAS.length();
+    counts = new short[charsToCount + 1];
+  }
+
+  /**
+   * Increments the count for the given character. The supplied character may be
+   * upper or lower case but counts are for the upper case only. Gap characters
+   * (space, ., -) are all counted together.
+   * 
+   * @param c
+   * @return the new value of the count for the character
+   */
+  public int add(final char c)
+  {
+    char u = toUpperCase(c);
+    int newValue = 0;
+    int offset = getOffset(u);
+
+    /*
+     * offset 0 is reserved for gap counting, so 0 here means either
+     * an unexpected character, or a gap character passed in error
+     */
+    if (offset == 0)
+    {
+      if (Comparison.isGap(u))
+      {
+        newValue = addGap();
+      }
+      else
+      {
+        newValue = addOtherCharacter(u);
+      }
+    }
+    else
+    {
+      newValue = increment(offset);
+    }
+    return newValue;
+  }
+
+  /**
+   * Increment the count at the specified offset. If this would result in short
+   * overflow, promote to counting int values instead.
+   * 
+   * @param offset
+   * @return the new value of the count at this offset
+   */
+  int increment(int offset)
+  {
+    int newValue = 0;
+    if (useIntCounts)
+    {
+      newValue = intCounts[offset];
+      intCounts[offset] = ++newValue;
+    }
+    else
+    {
+      if (counts[offset] == Short.MAX_VALUE)
+      {
+        handleOverflow();
+        newValue = intCounts[offset];
+        intCounts[offset] = ++newValue;
+      }
+      else
+      {
+        newValue = counts[offset];
+        counts[offset] = (short) ++newValue;
+      }
+    }
+    maxCount = Math.max(maxCount, newValue);
+    return newValue;
+  }
+
+  /**
+   * Switch from counting in short to counting in int
+   */
+  synchronized void handleOverflow()
+  {
+    intCounts = new int[counts.length];
+    for (int i = 0; i < counts.length; i++)
+    {
+      intCounts[i] = counts[i];
+    }
+    counts = null;
+    useIntCounts = true;
+  }
+
+  /**
+   * Returns this character's offset in the count array
+   * 
+   * @param c
+   * @return
+   */
+  int getOffset(char c)
+  {
+    int offset = 0;
+    if ('A' <= c && c <= 'Z')
+    {
+      offset = isNucleotide ? NUC_INDEX[c - 'A'] : AA_INDEX[c - 'A'];
+    }
+    return offset;
+  }
+
+  /**
+   * @param c
+   * @return
+   */
+  protected char toUpperCase(final char c)
+  {
+    char u = c;
+    if ('a' <= c && c <= 'z')
+    {
+      u = (char) (c + TOUPPERCASE);
+    }
+    return u;
+  }
+
+  /**
+   * Increment count for some unanticipated character. The first time this
+   * called, a SparseCount is instantiated to hold these 'extra' counts.
+   * 
+   * @param c
+   * @return the new value of the count for the character
+   */
+  int addOtherCharacter(char c)
+  {
+    if (otherData == null)
+    {
+      otherData = new SparseCount();
+    }
+    int newValue = otherData.add(c, 1);
+    maxCount = Math.max(maxCount, newValue);
+    return newValue;
+  }
+
+  /**
+   * Set count for some unanticipated character. The first time this called, a
+   * SparseCount is instantiated to hold these 'extra' counts.
+   * 
+   * @param c
+   * @param value
+   */
+  void setOtherCharacter(char c, int value)
+  {
+    if (otherData == null)
+    {
+      otherData = new SparseCount();
+    }
+    otherData.put(c, value);
+  }
+
+  /**
+   * Increment count of gap characters
+   * 
+   * @return the new count of gaps
+   */
+  public int addGap()
+  {
+    int newValue;
+    if (useIntCounts)
+    {
+      newValue = ++intCounts[GAP_COUNT];
+    }
+    else
+    {
+      newValue = ++counts[GAP_COUNT];
+    }
+    return newValue;
+  }
+
+  /**
+   * Answers true if we are counting ints (only after overflow of short counts)
+   * 
+   * @return
+   */
+  boolean isCountingInts()
+  {
+    return useIntCounts;
+  }
+
+  /**
+   * Sets the count for the given character. The supplied character may be upper
+   * or lower case but counts are for the upper case only.
+   * 
+   * @param c
+   * @param count
+   */
+  public void put(char c, int count)
+  {
+    char u = toUpperCase(c);
+    int offset = getOffset(u);
+
+    /*
+     * offset 0 is reserved for gap counting, so 0 here means either
+     * an unexpected character, or a gap character passed in error
+     */
+    if (offset == 0)
+    {
+      if (Comparison.isGap(u))
+      {
+        set(0, count);
+      }
+      else
+      {
+        setOtherCharacter(u, count);
+        maxCount = Math.max(maxCount, count);
+      }
+    }
+    else
+    {
+      set(offset, count);
+      maxCount = Math.max(maxCount, count);
+    }
+  }
+
+  /**
+   * Sets the count at the specified offset. If this would result in short
+   * overflow, promote to counting int values instead.
+   * 
+   * @param offset
+   * @param value
+   */
+  void set(int offset, int value)
+  {
+    if (useIntCounts)
+    {
+      intCounts[offset] = value;
+    }
+    else
+    {
+      if (value > Short.MAX_VALUE || value < Short.MIN_VALUE)
+      {
+        handleOverflow();
+        intCounts[offset] = value;
+      }
+      else
+      {
+        counts[offset] = (short) value;
+      }
+    }
+  }
+
+  /**
+   * Returns the count for the given character, or zero if no count held
+   * 
+   * @param c
+   * @return
+   */
+  public int getCount(char c)
+  {
+    char u = toUpperCase(c);
+    int offset = getOffset(u);
+    if (offset == 0)
+    {
+      if (!Comparison.isGap(u))
+      {
+        // should have called getGapCount()
+        return otherData == null ? 0 : otherData.get(u);
+      }
+    }
+    return useIntCounts ? intCounts[offset] : counts[offset];
+  }
+
+  public int getGapCount()
+  {
+    return useIntCounts ? intCounts[0] : counts[0];
+  }
+
+  /**
+   * Answers true if this object wraps a counter for unexpected characters
+   * 
+   * @return
+   */
+  boolean isUsingOtherData()
+  {
+    return otherData != null;
+  }
+
+  /**
+   * Returns the character (or concatenated characters) for the symbol(s) with
+   * the given count in the profile. Can be used to get the modal residue by
+   * supplying the modal count value. Returns an empty string if no symbol has
+   * the given count. The symbols are in alphabetic order of standard peptide or
+   * nucleotide characters, followed by 'other' symbols if any.
+   * 
+   * @return
+   */
+  public String getResiduesForCount(int count)
+  {
+    if (count == 0)
+    {
+      return "";
+    }
+
+    /*
+     * find counts for the given value and append the
+     * corresponding symbol
+     */
+    StringBuilder modal = new StringBuilder();
+    if (useIntCounts)
+    {
+      for (int i = 1; i < intCounts.length; i++)
+      {
+        if (intCounts[i] == count)
+        {
+          modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS
+                  .charAt(i - 1));
+        }
+      }
+    }
+    else
+    {
+      for (int i = 1; i < counts.length; i++)
+      {
+        if (counts[i] == count)
+        {
+          modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS
+                  .charAt(i - 1));
+        }
+      }
+    }
+    if (otherData != null)
+    {
+      for (int i = 0; i < otherData.size(); i++)
+      {
+        if (otherData.valueAt(i) == count)
+        {
+          modal.append((char) otherData.keyAt(i));
+        }
+      }
+    }
+    return modal.toString();
+  }
+
+  /**
+   * Returns the highest count for any symbol(s) in the profile (excluding gap)
+   * 
+   * @return
+   */
+  public int getModalCount()
+  {
+    return maxCount;
+  }
+
+  /**
+   * Returns the number of distinct symbols with a non-zero count (excluding the
+   * gap symbol)
+   * 
+   * @return
+   */
+  public int size() {
+    int size = 0;
+    if (useIntCounts)
+    {
+      for (int i = 1; i < intCounts.length; i++)
+      {
+        if (intCounts[i] > 0)
+        {
+          size++;
+        }
+      }
+    }
+    else
+    {
+      for (int i = 1; i < counts.length; i++)
+      {
+        if (counts[i] > 0)
+        {
+          size++;
+        }
+      }
+    }
+
+    /*
+     * include 'other' characters recorded (even if count is zero
+     * though that would be a strange use case)
+     */
+    if (otherData != null)
+    {
+      size += otherData.size();
+    }
+
+    return size;
+  }
+
+  /**
+   * Returns a data bean holding those symbols that have a non-zero count
+   * (excluding the gap symbol), with their counts.
+   * 
+   * @return
+   */
+  public SymbolCounts getSymbolCounts()
+  {
+    int size = size();
+    char[] symbols = new char[size];
+    int[] values = new int[size];
+    int j = 0;
+
+    if (useIntCounts)
+    {
+      for (int i = 1; i < intCounts.length; i++)
+      {
+        if (intCounts[i] > 0)
+        {
+          char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS
+                  .charAt(i - 1);
+          symbols[j] = symbol;
+          values[j] = intCounts[i];
+          j++;
+        }
+      }
+    }
+    else
+    {
+      for (int i = 1; i < counts.length; i++)
+      {
+        if (counts[i] > 0)
+        {
+          char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS
+                  .charAt(i - 1);
+          symbols[j] = symbol;
+          values[j] = counts[i];
+          j++;
+        }
+      }
+    }
+    if (otherData != null)
+    {
+      for (int i = 0; i < otherData.size(); i++)
+      {
+        symbols[j] = (char) otherData.keyAt(i);
+        values[j] = otherData.valueAt(i);
+        j++;
+      }
+    }
+
+    return new SymbolCounts(symbols, values);
+  }
+
+  /**
+   * Returns a tooltip string showing residues in descending order of their
+   * percentage frequency in the profile
+   * 
+   * @param normaliseBy
+   *          the divisor for residue counts (may or may not include gapped
+   *          sequence count)
+   * @param percentageDecPl
+   *          the number of decimal places to show in percentages
+   * @return
+   */
+  public String getTooltip(int normaliseBy, int percentageDecPl)
+  {
+    SymbolCounts symbolCounts = getSymbolCounts();
+    char[] ca = symbolCounts.symbols;
+    int[] vl = symbolCounts.values;
+
+    /*
+     * sort characters into ascending order of their counts
+     */
+    QuickSort.sort(vl, ca);
+
+    /*
+     * traverse in reverse order (highest count first) to build tooltip
+     */
+    boolean first = true;
+    StringBuilder sb = new StringBuilder(64);
+    for (int c = ca.length - 1; c >= 0; c--)
+    {
+      final char residue = ca[c];
+      // TODO combine residues which share a percentage
+      // (see AAFrequency.completeCdnaConsensus)
+      float tval = (vl[c] * 100f) / normaliseBy;
+      sb.append(first ? "" : "; ").append(residue).append(" ");
+      Format.appendPercentage(sb, tval, percentageDecPl);
+      sb.append("%");
+      first = false;
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Returns a string representation of the symbol counts, for debug purposes.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append("[ ");
+    SymbolCounts sc = getSymbolCounts();
+    for (int i = 0; i < sc.symbols.length; i++)
+    {
+      sb.append(sc.symbols[i]).append(":").append(sc.values[i]).append(" ");
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+}
index bd7d53d..70463e7 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.api;
 
 import jalview.analysis.Conservation;
+import jalview.analysis.Profile;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
@@ -81,7 +82,7 @@ public interface AlignViewportI extends ViewStyleI
 
   ColumnSelection getColumnSelection();
 
-  Hashtable[] getSequenceConsensusHash();
+  Profile[] getSequenceConsensusHash();
 
   /**
    * Get consensus data table for the cDNA complement of this alignment (if any)
@@ -144,7 +145,7 @@ public interface AlignViewportI extends ViewStyleI
    * 
    * @param hconsensus
    */
-  void setSequenceConsensusHash(Hashtable[] hconsensus);
+  void setSequenceConsensusHash(Profile[] hconsensus);
 
   /**
    * Set the cDNA complement consensus for the viewport
index 9a408e3..98fd8f2 100755 (executable)
@@ -22,14 +22,13 @@ package jalview.datamodel;
 
 import jalview.analysis.AAFrequency;
 import jalview.analysis.Conservation;
+import jalview.analysis.Profile;
 import jalview.schemes.ColourSchemeI;
 
 import java.awt.Color;
 import java.util.ArrayList;
-import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
-import java.util.Vector;
 
 /**
  * Collects a set contiguous ranges on a set of sequences
@@ -45,8 +44,6 @@ public class SequenceGroup implements AnnotatedCollectionI
 
   Conservation conserve;
 
-  Vector aaFrequency;
-
   boolean displayBoxes = true;
 
   boolean displayText = true;
@@ -531,7 +528,7 @@ public class SequenceGroup implements AnnotatedCollectionI
     boolean upd = false;
     try
     {
-      Hashtable cnsns[] = AAFrequency.calculate(sequences, startRes,
+      Profile[] cnsns = AAFrequency.calculate(sequences, startRes,
               endRes + 1, showSequenceLogo);
       if (consensus != null)
       {
@@ -603,9 +600,9 @@ public class SequenceGroup implements AnnotatedCollectionI
     c.completeAnnotations(conservation, null, startRes, endRes + 1);
   }
 
-  public Hashtable[] consensusData = null;
+  public Profile[] consensusData = null;
 
-  private void _updateConsensusRow(Hashtable[] cnsns, long nseq)
+  private void _updateConsensusRow(Profile[] cnsns, long nseq)
   {
     if (consensus == null)
     {
diff --git a/src/jalview/ext/android/ContainerHelpers.java b/src/jalview/ext/android/ContainerHelpers.java
new file mode 100644 (file)
index 0000000..f536819
--- /dev/null
@@ -0,0 +1,102 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class ContainerHelpers
+{
+  static final boolean[] EMPTY_BOOLEANS = new boolean[0];
+
+  static final int[] EMPTY_INTS = new int[0];
+
+  static final long[] EMPTY_LONGS = new long[0];
+
+  static final Object[] EMPTY_OBJECTS = new Object[0];
+
+  // This is Arrays.binarySearch(), but doesn't do any argument validation.
+  static int binarySearch(int[] array, int size, int value)
+  {
+    int lo = 0;
+    int hi = size - 1;
+    while (lo <= hi)
+    {
+      final int mid = (lo + hi) >>> 1;
+      final int midVal = array[mid];
+      if (midVal < value)
+      {
+        lo = mid + 1;
+      }
+      else if (midVal > value)
+      {
+        hi = mid - 1;
+      }
+      else
+      {
+        return mid; // value found
+      }
+    }
+    return ~lo; // value not present
+  }
+
+  static int binarySearch(long[] array, int size, long value)
+  {
+    int lo = 0;
+    int hi = size - 1;
+    while (lo <= hi)
+    {
+      final int mid = (lo + hi) >>> 1;
+      final long midVal = array[mid];
+      if (midVal < value)
+      {
+        lo = mid + 1;
+      }
+      else if (midVal > value)
+      {
+        hi = mid - 1;
+      }
+      else
+      {
+        return mid; // value found
+      }
+    }
+    return ~lo; // value not present
+  }
+
+  // This is Arrays.binarySearch(), but doesn't do any argument validation.
+  static int binarySearch(short[] array, int size, short value)
+  {
+    int lo = 0;
+    int hi = size - 1;
+    while (lo <= hi)
+    {
+      final int mid = (lo + hi) >>> 1;
+      final int midVal = array[mid];
+      if (midVal < value)
+      {
+        lo = mid + 1;
+      }
+      else if (midVal > value)
+      {
+        hi = mid - 1;
+      }
+      else
+      {
+        return mid; // value found
+      }
+    }
+    return ~lo; // value not present
+  }
+}
diff --git a/src/jalview/ext/android/SparseIntArray.java b/src/jalview/ext/android/SparseIntArray.java
new file mode 100644 (file)
index 0000000..2b9c4af
--- /dev/null
@@ -0,0 +1,425 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * SparseIntArrays map integers to integers. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Integers, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra
+ * entry object for each mapping.
+ *
+ * <p>
+ * Note that this container keeps its mappings in an array data structure, using
+ * a binary search to find keys. The implementation is not intended to be
+ * appropriate for data structures that may contain large numbers of items. It
+ * is generally slower than a traditional HashMap, since lookups require a
+ * binary search and adds and removes require inserting and deleting entries in
+ * the array. For containers holding up to hundreds of items, the performance
+ * difference is not significant, less than 50%.
+ * </p>
+ *
+ * <p>
+ * It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)<code>.
+ * </p>
+ */
+public class SparseIntArray implements Cloneable
+{
+  private int[] mKeys;
+
+  private int[] mValues;
+
+  private int mSize;
+
+  /**
+   * Creates a new SparseIntArray containing no mappings.
+   */
+  public SparseIntArray()
+  {
+    this(10);
+  }
+
+  /**
+   * Creates a new SparseIntArray containing no mappings that will not require
+   * any additional memory allocation to store the specified number of mappings.
+   * If you supply an initial capacity of 0, the sparse array will be
+   * initialized with a light-weight representation not requiring any additional
+   * array allocations.
+   */
+  public SparseIntArray(int initialCapacity)
+  {
+    if (initialCapacity == 0)
+    {
+      mKeys = ContainerHelpers.EMPTY_INTS;
+      mValues = ContainerHelpers.EMPTY_INTS;
+    }
+    else
+    {
+      initialCapacity = idealIntArraySize(initialCapacity);
+      mKeys = new int[initialCapacity];
+      mValues = new int[initialCapacity];
+    }
+    mSize = 0;
+  }
+
+  @Override
+  public SparseIntArray clone()
+  {
+    SparseIntArray clone = null;
+    try
+    {
+      clone = (SparseIntArray) super.clone();
+      clone.mKeys = mKeys.clone();
+      clone.mValues = mValues.clone();
+    } catch (CloneNotSupportedException cnse)
+    {
+      /* ignore */
+    }
+    return clone;
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or <code>0</code> if no such
+   * mapping has been made.
+   */
+  public int get(int key)
+  {
+    return get(key, 0);
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or the specified value if no
+   * such mapping has been made.
+   */
+  public int get(int key, int valueIfKeyNotFound)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i < 0)
+    {
+      return valueIfKeyNotFound;
+    }
+    else
+    {
+      return mValues[i];
+    }
+  }
+
+  /**
+   * Removes the mapping from the specified key, if there was any.
+   */
+  public void delete(int key)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      removeAt(i);
+    }
+  }
+
+  /**
+   * Removes the mapping at the given index.
+   */
+  public void removeAt(int index)
+  {
+    System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+    System.arraycopy(mValues, index + 1, mValues, index, mSize
+            - (index + 1));
+    mSize--;
+  }
+
+  /**
+   * Adds a mapping from the specified key to the specified value, replacing the
+   * previous mapping from the specified key if there was one.
+   */
+  public void put(int key, int value)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] = value;
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealIntArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        int[] nvalues = new int[n];
+        // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        // Log.e("SparseIntArray", "move " + (mSize - i));
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = key;
+      mValues[i] = value;
+      mSize++;
+    }
+  }
+
+  /**
+   * Returns the number of key-value mappings that this SparseIntArray currently
+   * stores.
+   */
+  public int size()
+  {
+    return mSize;
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the key from
+   * the <code>index</code>th key-value mapping that this SparseIntArray stores.
+   *
+   * <p>
+   * The keys corresponding to indices in ascending order are guaranteed to be
+   * in ascending order, e.g., <code>keyAt(0)</code> will return the smallest
+   * key and <code>keyAt(size()-1)</code> will return the largest key.
+   * </p>
+   */
+  public int keyAt(int index)
+  {
+    return mKeys[index];
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the value
+   * from the <code>index</code>th key-value mapping that this SparseIntArray
+   * stores.
+   *
+   * <p>
+   * The values corresponding to indices in ascending order are guaranteed to be
+   * associated with keys in ascending order, e.g., <code>valueAt(0)</code> will
+   * return the value associated with the smallest key and
+   * <code>valueAt(size()-1)</code> will return the value associated with the
+   * largest key.
+   * </p>
+   */
+  public int valueAt(int index)
+  {
+    return mValues[index];
+  }
+
+  /**
+   * Returns the index for which {@link #keyAt} would return the specified key,
+   * or a negative number if the specified key is not mapped.
+   */
+  public int indexOfKey(int key)
+  {
+    return ContainerHelpers.binarySearch(mKeys, mSize, key);
+  }
+
+  /**
+   * Returns an index for which {@link #valueAt} would return the specified key,
+   * or a negative number if no keys map to the specified value. Beware that
+   * this is a linear search, unlike lookups by key, and that multiple keys can
+   * map to the same value and this will find only one of them.
+   */
+  public int indexOfValue(int value)
+  {
+    for (int i = 0; i < mSize; i++)
+    {
+      if (mValues[i] == value)
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Removes all key-value mappings from this SparseIntArray.
+   */
+  public void clear()
+  {
+    mSize = 0;
+  }
+
+  /**
+   * Puts a key/value pair into the array, optimizing for the case where the key
+   * is greater than all existing keys in the array.
+   */
+  public void append(int key, int value)
+  {
+    if (mSize != 0 && key <= mKeys[mSize - 1])
+    {
+      put(key, value);
+      return;
+    }
+    int pos = mSize;
+    if (pos >= mKeys.length)
+    {
+      int n = idealIntArraySize(pos + 1);
+      int[] nkeys = new int[n];
+      int[] nvalues = new int[n];
+      // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+      System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+      System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+      mKeys = nkeys;
+      mValues = nvalues;
+    }
+    mKeys[pos] = key;
+    mValues[pos] = value;
+    mSize = pos + 1;
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealIntArraySize(int need)
+  {
+    return idealByteArraySize(need * 4) / 4;
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealByteArraySize(int need)
+  {
+    for (int i = 4; i < 32; i++)
+    {
+      if (need <= (1 << i) - 12)
+      {
+        return (1 << i) - 12;
+      }
+    }
+
+    return need;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>
+   * This implementation composes a string by iterating over its mappings.
+   */
+  @Override
+  public String toString()
+  {
+    if (size() <= 0)
+    {
+      return "{}";
+    }
+    StringBuilder buffer = new StringBuilder(mSize * 28);
+    buffer.append('{');
+    for (int i = 0; i < mSize; i++)
+    {
+      if (i > 0)
+      {
+        buffer.append(", ");
+      }
+      int key = keyAt(i);
+      buffer.append(key);
+      buffer.append('=');
+      int value = valueAt(i);
+      buffer.append(value);
+    }
+    buffer.append('}');
+    return buffer.toString();
+  }
+
+  /**
+   * Method (copied from put) added for Jalview to efficiently increment a key's
+   * value if present, else add it with the given value. This avoids a double
+   * binary search (once to get the value, again to put the updated value).
+   * 
+   * @param key
+   * @oparam toAdd
+   * @return the new value of the count for the key
+   * @throw ArithmeticException if the result would exceed the maximum value of
+   *        an int
+   */
+  public int add(int key, int toAdd)
+  {
+    int newValue = toAdd;
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      checkOverflow(mValues[i], toAdd);
+      mValues[i] += toAdd;
+      newValue = mValues[i];
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealIntArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        int[] nvalues = new int[n];
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = key;
+      mValues[i] = toAdd;
+      mSize++;
+    }
+    return newValue;
+  }
+
+  /**
+   * Throws ArithmeticException if adding addend to value would exceed the range
+   * of int
+   * 
+   * @param value
+   * @param addend
+   */
+  static void checkOverflow(int value, int addend)
+  {
+    /*
+     * test cases being careful to avoid overflow while testing!
+     */
+    if (addend > 0)
+    {
+      if (value > 0 && Integer.MAX_VALUE - value < addend)
+      {
+        throw new ArithmeticException("Integer overflow adding " + addend
+                + " to  " + value);
+      }
+    }
+    else if (addend < 0)
+    {
+      if (value < 0 && Integer.MIN_VALUE - value > addend)
+      {
+        throw new ArithmeticException("Integer underflow adding " + addend
+                + " to  " + value);
+      }
+    }
+  }
+}
diff --git a/src/jalview/ext/android/SparseShortArray.java b/src/jalview/ext/android/SparseShortArray.java
new file mode 100644 (file)
index 0000000..375d745
--- /dev/null
@@ -0,0 +1,439 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * SparseShortArrays map shorts to shorts. Unlike a normal array of shorts,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Shorts to Shorts, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra
+ * entry object for each mapping.
+ *
+ * <p>
+ * Note that this container keeps its mappings in an array data structure, using
+ * a binary search to find keys. The implementation is not intended to be
+ * appropriate for data structures that may contain large numbers of items. It
+ * is generally slower than a traditional HashMap, since lookups require a
+ * binary search and adds and removes require inserting and deleting entries in
+ * the array. For containers holding up to hundreds of items, the performance
+ * difference is not significant, less than 50%.
+ * </p>
+ *
+ * <p>
+ * It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)<code>.
+ * </p>
+ */
+/**
+ * A copy of SparseShortArray designed to store short values (to minimise space
+ * usage).
+ * <p>
+ * Note that operations append, put, add throw ArithmeticException if the
+ * resulting value overflows the range of a short.
+ */
+public class SparseShortArray implements Cloneable
+{
+  private short[] mKeys;
+
+  private short[] mValues;
+
+  private int mSize;
+
+  /**
+   * Creates a new SparseShortArray containing no mappings.
+   */
+  public SparseShortArray()
+  {
+    this(10);
+  }
+
+  /**
+   * Creates a new SparseShortArray containing no mappings that will not require
+   * any additional memory allocation to store the specified number of mappings.
+   * If you supply an initial capacity of 0, the sparse array will be
+   * initialized with a light-weight representation not requiring any additional
+   * array allocations.
+   */
+  public SparseShortArray(int initialCapacity)
+  {
+    if (initialCapacity == 0)
+    {
+      mKeys = new short[0];
+      mValues = new short[0];
+    }
+    else
+    {
+      initialCapacity = idealShortArraySize(initialCapacity);
+      mKeys = new short[initialCapacity];
+      mValues = new short[initialCapacity];
+    }
+    mSize = 0;
+  }
+
+  @Override
+  public SparseShortArray clone()
+  {
+    SparseShortArray clone = null;
+    try
+    {
+      clone = (SparseShortArray) super.clone();
+      clone.mKeys = mKeys.clone();
+      clone.mValues = mValues.clone();
+    } catch (CloneNotSupportedException cnse)
+    {
+      /* ignore */
+    }
+    return clone;
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or <code>0</code> if no such
+   * mapping has been made.
+   */
+  public int get(int key)
+  {
+    return get(key, 0);
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or the specified value if no
+   * such mapping has been made.
+   * 
+   * @throws ArithmeticException
+   *           if key is outside the range of a short value
+   */
+  public int get(int key, int valueIfKeyNotFound)
+  {
+    checkOverflow(key);
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key);
+    if (i < 0)
+    {
+      return valueIfKeyNotFound;
+    }
+    else
+    {
+      return mValues[i];
+    }
+  }
+
+  /**
+   * Removes the mapping from the specified key, if there was any.
+   * 
+   * @throws ArithmeticException
+   *           if key is outside the range of a short value
+   */
+  public void delete(int key)
+  {
+    checkOverflow(key);
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key);
+    if (i >= 0)
+    {
+      removeAt(i);
+    }
+  }
+
+  /**
+   * Removes the mapping at the given index.
+   */
+  public void removeAt(int index)
+  {
+    System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+    System.arraycopy(mValues, index + 1, mValues, index, mSize
+            - (index + 1));
+    mSize--;
+  }
+
+  /**
+   * Adds a mapping from the specified key to the specified value, replacing the
+   * previous mapping from the specified key if there was one.
+   * 
+   * @throws ArithmeticException
+   *           if either argument is outside the range of a short value
+   */
+  public void put(int key, int value)
+  {
+    checkOverflow(key);
+    checkOverflow(value);
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key);
+    if (i >= 0)
+    {
+      mValues[i] = (short) value;
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealShortArraySize(mSize + 1);
+        short[] nkeys = new short[n];
+        short[] nvalues = new short[n];
+        // Log.e("SparseShortArray", "grow " + mKeys.length + " to " + n);
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        // Log.e("SparseShortArray", "move " + (mSize - i));
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = (short) key;
+      mValues[i] = (short) value;
+      mSize++;
+    }
+  }
+
+  /**
+   * Returns the number of key-value mappings that this SparseShortArray
+   * currently stores.
+   */
+  public int size()
+  {
+    return mSize;
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the key from
+   * the <code>index</code>th key-value mapping that this SparseShortArray
+   * stores.
+   *
+   * <p>
+   * The keys corresponding to indices in ascending order are guaranteed to be
+   * in ascending order, e.g., <code>keyAt(0)</code> will return the smallest
+   * key and <code>keyAt(size()-1)</code> will return the largest key.
+   * </p>
+   */
+  public short keyAt(int index)
+  {
+    return mKeys[index];
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the value
+   * from the <code>index</code>th key-value mapping that this SparseShortArray
+   * stores.
+   *
+   * <p>
+   * The values corresponding to indices in ascending order are guaranteed to be
+   * associated with keys in ascending order, e.g., <code>valueAt(0)</code> will
+   * return the value associated with the smallest key and
+   * <code>valueAt(size()-1)</code> will return the value associated with the
+   * largest key.
+   * </p>
+   */
+  public short valueAt(int index)
+  {
+    return mValues[index];
+  }
+
+  /**
+   * Returns the index for which {@link #keyAt} would return the specified key,
+   * or a negative number if the specified key is not mapped.
+   * 
+   * @throws ArithmeticException
+   *           if key is outside the range of a short value
+   */
+  public int indexOfKey(int key)
+  {
+    checkOverflow(key);
+    return ContainerHelpers.binarySearch(mKeys, mSize, (short) key);
+  }
+
+  /**
+   * Returns an index for which {@link #valueAt} would return the specified key,
+   * or a negative number if no keys map to the specified value. Beware that
+   * this is a linear search, unlike lookups by key, and that multiple keys can
+   * map to the same value and this will find only one of them.
+   */
+  public int indexOfValue(int value)
+  {
+    for (int i = 0; i < mSize; i++)
+    {
+      if (mValues[i] == value)
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Removes all key-value mappings from this SparseShortArray.
+   */
+  public void clear()
+  {
+    mSize = 0;
+  }
+
+  /**
+   * Puts a key/value pair into the array, optimizing for the case where the key
+   * is greater than all existing keys in the array.
+   */
+  public void append(int key, int value)
+  {
+    if (mSize != 0 && key <= mKeys[mSize - 1])
+    {
+      put(key, value);
+      return;
+    }
+    int pos = mSize;
+    if (pos >= mKeys.length)
+    {
+      int n = idealShortArraySize(pos + 1);
+      short[] nkeys = new short[n];
+      short[] nvalues = new short[n];
+      // Log.e("SparseShortArray", "grow " + mKeys.length + " to " + n);
+      System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+      System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+      mKeys = nkeys;
+      mValues = nvalues;
+    }
+    checkOverflow(key);
+    checkOverflow(value);
+    mKeys[pos] = (short) key;
+    mValues[pos] = (short) value;
+    mSize = pos + 1;
+  }
+
+  /**
+   * Throws an exception if the value is outside the range of a short.
+   * 
+   * @param value
+   * @throws ArithmeticException
+   */
+  public static void checkOverflow(int value)
+  {
+    if (value > Short.MAX_VALUE || value < Short.MIN_VALUE)
+    {
+      throw new ArithmeticException(String.valueOf(value));
+    }
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealShortArraySize(int need)
+  {
+    return idealByteArraySize(need * 2) / 2;
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealByteArraySize(int need)
+  {
+    for (int i = 4; i < 32; i++)
+    {
+      if (need <= (1 << i) - 12)
+      {
+        return (1 << i) - 12;
+      }
+    }
+
+    return need;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>
+   * This implementation composes a string by iterating over its mappings.
+   */
+  @Override
+  public String toString()
+  {
+    if (size() <= 0)
+    {
+      return "{}";
+    }
+    StringBuilder buffer = new StringBuilder(mSize * 28);
+    buffer.append('{');
+    for (int i = 0; i < mSize; i++)
+    {
+      if (i > 0)
+      {
+        buffer.append(", ");
+      }
+      int key = keyAt(i);
+      buffer.append(key);
+      buffer.append('=');
+      int value = valueAt(i);
+      buffer.append(value);
+    }
+    buffer.append('}');
+    return buffer.toString();
+  }
+
+  /**
+   * Method (copied from put) added for Jalview to efficiently increment a key's
+   * value if present, else add it with the given value. This avoids a double
+   * binary search (once to get the value, again to put the updated value).
+   * 
+   * @param key
+   * @oparam toAdd
+   * @return the new value of the count for the key
+   * @throws ArithmeticException
+   *           if key, or result of adding toAdd, is outside the range of a
+   *           short value
+   */
+  public int add(int key, int toAdd)
+  {
+    int newValue = toAdd;
+    checkOverflow(key);
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key);
+    if (i >= 0)
+    {
+      checkOverflow(toAdd + mValues[i]);
+      mValues[i] += (short) toAdd;
+      newValue = mValues[i];
+    }
+    else
+    {
+      checkOverflow(toAdd);
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealShortArraySize(mSize + 1);
+        short[] nkeys = new short[n];
+        short[] nvalues = new short[n];
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = (short) key;
+      mValues[i] = (short) toAdd;
+      mSize++;
+    }
+    return newValue;
+  }
+}
index 3fdcb3b..ed758c5 100644 (file)
@@ -22,6 +22,7 @@ package jalview.renderer;
 
 import jalview.analysis.AAFrequency;
 import jalview.analysis.CodingUtils;
+import jalview.analysis.Profile;
 import jalview.analysis.Rna;
 import jalview.analysis.StructureFrequency;
 import jalview.api.AlignViewportI;
@@ -73,7 +74,7 @@ public class AnnotationRenderer
 
   private ColumnSelection columnSelection;
 
-  private Hashtable[] hconsensus;
+  private Profile[] hconsensus;
 
   private Hashtable[] complementConsensus;
 
index 9d09259..37c31f9 100755 (executable)
  */
 package jalview.schemes;
 
-import jalview.analysis.AAFrequency;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 
 import java.awt.Color;
 import java.util.Map;
 
 public class Blosum62ColourScheme extends ResidueColourScheme
 {
+  private static final Color LIGHT_BLUE = new Color(204, 204, 255);
+  private static final Color DARK_BLUE = new Color(154, 154, 255);
+
   public Blosum62ColourScheme()
   {
     super();
@@ -52,14 +55,16 @@ public class Blosum62ColourScheme extends ResidueColourScheme
 
     Color currentColour;
 
-    if (!jalview.util.Comparison.isGap(res))
+    if (!Comparison.isGap(res))
     {
-      String max = (String) consensus[j].get(AAFrequency.MAXRESIDUE);
+      /*
+       * test if this is the consensus (or joint consensus) residue
+       */
+      String max = consensus[j].getModalResidue();
 
       if (max.indexOf(res) > -1)
       {
-        // TODO use a constant here?
-        currentColour = new Color(154, 154, 255);
+        currentColour = DARK_BLUE;
       }
       else
       {
@@ -74,8 +79,7 @@ public class Blosum62ColourScheme extends ResidueColourScheme
 
         if (c > 0)
         {
-          // TODO use a constant here?
-          currentColour = new Color(204, 204, 255);
+          currentColour = LIGHT_BLUE;
         }
         else
         {
index effdf59..165ece9 100755 (executable)
@@ -20,6 +20,7 @@
  */
 package jalview.schemes;
 
+import jalview.analysis.Profile;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
@@ -52,7 +53,7 @@ public interface ColourSchemeI
   /**
    * assign the given consensus profile for the colourscheme
    */
-  public void setConsensus(java.util.Hashtable[] h);
+  public void setConsensus(Profile[] hconsensus);
 
   /**
    * assign the given conservation to the colourscheme
index eac467a..0dcf960 100644 (file)
@@ -21,8 +21,7 @@
 package jalview.schemes;
 
 import jalview.analysis.Conservation;
-
-import java.util.Hashtable;
+import jalview.analysis.Profile;
 
 /**
  * Colourscheme that takes its colours from some other colourscheme
@@ -41,7 +40,7 @@ public class FollowerColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public void setConsensus(Hashtable[] consensus)
+  public void setConsensus(Profile[] consensus)
   {
     if (colourScheme != null)
     {
index 9dd763d..ccc69c2 100755 (executable)
@@ -20,9 +20,9 @@
  */
 package jalview.schemes;
 
-import jalview.analysis.AAFrequency;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 
 import java.awt.Color;
 
@@ -67,20 +67,22 @@ public class PIDColourScheme extends ResidueColourScheme
       return Color.white;
     }
 
-    if ((Integer
-            .parseInt(consensus[j].get(AAFrequency.MAXCOUNT).toString()) != -1)
-            && consensus[j].contains(String.valueOf(c)))
+    /*
+     * test whether this is the consensus (or joint consensus) residue
+     */
+    boolean matchesConsensus = consensus[j].getModalResidue().contains(
+            String.valueOf(c));
+    if (matchesConsensus)
     {
-      sc = ((Float) consensus[j].get(ignoreGaps)).floatValue();
+      sc = consensus[j].getPercentageIdentity(ignoreGaps);
 
-      if (!jalview.util.Comparison.isGap(c))
+      if (!Comparison.isGap(c))
       {
         for (int i = 0; i < thresholds.length; i++)
         {
           if (sc > thresholds[i])
           {
             currentColour = pidColours[i];
-
             break;
           }
         }
index bca98cf..56c573c 100755 (executable)
  */
 package jalview.schemes;
 
-import jalview.analysis.AAFrequency;
 import jalview.analysis.Conservation;
+import jalview.analysis.Profile;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 import jalview.util.MessageManager;
 
 import java.awt.Color;
-import java.util.Hashtable;
 import java.util.Map;
 
 /**
@@ -48,17 +48,21 @@ public class ResidueColourScheme implements ColourSchemeI
   int threshold = 0;
 
   /* Set when threshold colouring to either pid_gaps or pid_nogaps */
-  protected String ignoreGaps = AAFrequency.PID_GAPS;
+  protected boolean ignoreGaps = false;
 
-  /** Consenus as a hashtable array */
-  Hashtable[] consensus;
+  /*
+   * Consensus data indexed by column
+   */
+  Profile[] consensus;
 
-  /** Conservation string as a char array */
+  /*
+   * Conservation string as a char array 
+   */
   char[] conservation;
 
-  int conservationLength = 0;
-
-  /** DOCUMENT ME!! */
+  /*
+   * The conservation slider percentage setting 
+   */
   int inc = 30;
 
   /**
@@ -100,6 +104,7 @@ public class ResidueColourScheme implements ColourSchemeI
   /**
    * Find a colour without an index in a sequence
    */
+  @Override
   public Color findColour(char c)
   {
     return colors == null ? Color.white : colors[symbolIndex[c]];
@@ -133,58 +138,62 @@ public class ResidueColourScheme implements ColourSchemeI
    * 
    * @return Returns the percentage threshold
    */
+  @Override
   public int getThreshold()
   {
     return threshold;
   }
 
   /**
-   * DOCUMENT ME!
+   * Sets the percentage consensus threshold value, and whether gaps are ignored
+   * in percentage identity calculation
    * 
-   * @param ct
-   *          DOCUMENT ME!
+   * @param consensusThreshold
+   * @param ignoreGaps
    */
-  public void setThreshold(int ct, boolean ignoreGaps)
+  @Override
+  public void setThreshold(int consensusThreshold, boolean ignoreGaps)
   {
-    threshold = ct;
-    if (ignoreGaps)
-    {
-      this.ignoreGaps = AAFrequency.PID_NOGAPS;
-    }
-    else
-    {
-      this.ignoreGaps = AAFrequency.PID_GAPS;
-    }
+    threshold = consensusThreshold;
+    this.ignoreGaps = ignoreGaps;
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers true if there is a consensus profile for the specified column, and
+   * the given residue matches the consensus (or joint consensus) residue for
+   * the column, and the percentage identity for the profile is equal to or
+   * greater than the current threshold; else answers false. The percentage
+   * calculation depends on whether or not we are ignoring gapped sequences.
    * 
-   * @param s
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
+   * @param residue
+   * @param column
+   *          (index into consensus profiles)
    * 
-   * @return DOCUMENT ME!
+   * @return
+   * @see #setThreshold(int, boolean)
    */
-  public boolean aboveThreshold(char c, int j)
+  public boolean aboveThreshold(char residue, int column)
   {
-    if ('a' <= c && c <= 'z')
+    if ('a' <= residue && residue <= 'z')
     {
       // TO UPPERCASE !!!
       // Faster than toUpperCase
-      c -= ('a' - 'A');
+      residue -= ('a' - 'A');
     }
 
-    if (consensus == null || consensus.length < j || consensus[j] == null)
+    if (consensus == null || consensus.length < column
+            || consensus[column] == null)
     {
       return false;
     }
 
-    if ((((Integer) consensus[j].get(AAFrequency.MAXCOUNT)).intValue() != -1)
-            && consensus[j].contains(String.valueOf(c)))
+    /*
+     * test whether this is the consensus (or joint consensus) residue
+     */
+    if (consensus[column].getModalResidue().contains(
+            String.valueOf(residue)))
     {
-      if (((Float) consensus[j].get(ignoreGaps)).floatValue() >= threshold)
+      if (consensus[column].getPercentageIdentity(ignoreGaps) >= threshold)
       {
         return true;
       }
@@ -193,6 +202,7 @@ public class ResidueColourScheme implements ColourSchemeI
     return false;
   }
 
+  @Override
   public boolean conservationApplied()
   {
     return conservationColouring;
@@ -204,11 +214,13 @@ public class ResidueColourScheme implements ColourSchemeI
     conservationColouring = conservationApplied;
   }
 
+  @Override
   public void setConservationInc(int i)
   {
     inc = i;
   }
 
+  @Override
   public int getConservationInc()
   {
     return inc;
@@ -220,7 +232,8 @@ public class ResidueColourScheme implements ColourSchemeI
    * @param consensus
    *          DOCUMENT ME!
    */
-  public void setConsensus(Hashtable[] consensus)
+  @Override
+  public void setConsensus(Profile[] consensus)
   {
     if (consensus == null)
     {
@@ -230,6 +243,7 @@ public class ResidueColourScheme implements ColourSchemeI
     this.consensus = consensus;
   }
 
+  @Override
   public void setConservation(Conservation cons)
   {
     if (cons == null)
@@ -240,71 +254,88 @@ public class ResidueColourScheme implements ColourSchemeI
     else
     {
       conservationColouring = true;
-      int i, iSize = cons.getConsSequence().getLength();
+      int iSize = cons.getConsSequence().getLength();
       conservation = new char[iSize];
-      for (i = 0; i < iSize; i++)
+      for (int i = 0; i < iSize; i++)
       {
         conservation[i] = cons.getConsSequence().getCharAt(i);
       }
-      conservationLength = conservation.length;
     }
 
   }
 
   /**
-   * DOCUMENT ME!
+   * Applies a combination of column conservation score, and conservation
+   * percentage slider, to 'bleach' out the residue colours towards white.
+   * <p>
+   * If a column is fully conserved (identical residues, conservation score 11,
+   * shown as *), or all 10 physico-chemical properties are conserved
+   * (conservation score 10, shown as +), then the colour is left unchanged.
+   * <p>
+   * Otherwise a 'bleaching' factor is computed and applied to the colour. This
+   * is designed to fade colours for scores of 0-9 completely to white at slider
+   * positions ranging from 18% - 100% respectively.
    * 
-   * @param s
-   *          DOCUMENT ME!
-   * @param i
-   *          DOCUMENT ME!
+   * @param currentColour
+   * @param column
    * 
-   * @return DOCUMENT ME!
+   * @return bleached (or unmodified) colour
    */
-
-  Color applyConservation(Color currentColour, int i)
+  Color applyConservation(Color currentColour, int column)
   {
+    if (conservation == null || conservation.length <= column)
+    {
+      return currentColour;
+    }
+    char conservationScore = conservation[column];
+
+    /*
+     * if residues are fully conserved (* or 11), or all properties
+     * are conserved (+ or 10), leave colour unchanged
+     */
+    if (conservationScore == '*' || conservationScore == '+'
+            || conservationScore == (char) 10
+            || conservationScore == (char) 11)
+    {
+      return currentColour;
+    }
 
-    if ((conservationLength > i) && (conservation[i] != '*')
-            && (conservation[i] != '+'))
+    if (Comparison.isGap(conservationScore))
     {
-      if (jalview.util.Comparison.isGap(conservation[i]))
-      {
-        currentColour = Color.white;
-      }
-      else
-      {
-        float t = 11 - (conservation[i] - '0');
-        if (t == 0)
-        {
-          return Color.white;
-        }
-
-        int red = currentColour.getRed();
-        int green = currentColour.getGreen();
-        int blue = currentColour.getBlue();
-
-        int dr = 255 - red;
-        int dg = 255 - green;
-        int db = 255 - blue;
-
-        dr *= t / 10f;
-        dg *= t / 10f;
-        db *= t / 10f;
-
-        red += (inc / 20f) * dr;
-        green += (inc / 20f) * dg;
-        blue += (inc / 20f) * db;
-
-        if (red > 255 || green > 255 || blue > 255)
-        {
-          currentColour = Color.white;
-        }
-        else
-        {
-          currentColour = new Color(red, green, blue);
-        }
-      }
+      return Color.white;
+    }
+
+    /*
+     * convert score 0-9 to a bleaching factor 1.1 - 0.2
+     */
+    float bleachFactor = (11 - (conservationScore - '0')) / 10f;
+
+    /*
+     * scale this by the percentage slider / 20
+     */
+    bleachFactor *= (inc / 20f);
+
+    int red = currentColour.getRed();
+    int green = currentColour.getGreen();
+    int blue = currentColour.getBlue();
+
+    /*
+     * bleach colours towards white (255, 255, 255),
+     * depending on the consensus score and the conservation slider value
+     * scores of:                      0  1  2  3  4  5  6  7  8  9
+     * fade to white at slider value: 18 20 22 25 29 33 40 50 67 100%
+     */
+    red += (255 - red) * bleachFactor;
+    green += (255 - green) * bleachFactor;
+    blue += (255 - blue) * bleachFactor;
+
+    if (red > 255 || green > 255 || blue > 255)
+    {
+      currentColour = Color.white;
+    }
+    else
+    {
+      currentColour = new Color(red, green, blue);
     }
     return currentColour;
   }
index d14e4ad..7121985 100755 (executable)
@@ -947,4 +947,29 @@ public class Format
   {
     return formatString;
   }
+
+  /**
+   * Bespoke method to format percentage float value to the specified number of
+   * decimal places. Avoids use of general-purpose format parsers as a
+   * processing hotspot.
+   * 
+   * @param sb
+   * @param value
+   * @param dp
+   */
+  public static void appendPercentage(StringBuilder sb, float value, int dp)
+  {
+    sb.append((int) value);
+    if (dp > 0)
+    {
+      sb.append(".");
+      while (dp > 0)
+      {
+        value = value - (int) value;
+        value *= 10;
+        sb.append((int) value);
+        dp--;
+      }
+    }
+  }
 }
diff --git a/src/jalview/util/SparseCount.java b/src/jalview/util/SparseCount.java
new file mode 100644 (file)
index 0000000..e6b45f2
--- /dev/null
@@ -0,0 +1,154 @@
+package jalview.util;
+
+import jalview.ext.android.SparseIntArray;
+import jalview.ext.android.SparseShortArray;
+
+/**
+ * A class to count occurrences of characters with minimal memory footprint.
+ * Sparse arrays of short values are used to hold the counts, with automatic
+ * promotion to arrays of int if any count exceeds the maximum value for a
+ * short.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class SparseCount
+{
+  private static final int DEFAULT_PROFILE_SIZE = 2;
+
+  /*
+   * array of keys (chars) and values (counts)
+   * held either as shorts or (if shorts overflow) as ints 
+   */
+  private SparseShortArray shortProfile;
+
+  private SparseIntArray intProfile;
+
+  /*
+   * flag is set true after short overflow occurs
+   */
+  private boolean useInts;
+
+  /**
+   * Constructor which initially creates a new sparse array of short values to
+   * hold counts.
+   * 
+   * @param profileSize
+   */
+  public SparseCount(int profileSize)
+  {
+    this.shortProfile = new SparseShortArray(profileSize);
+  }
+
+  /**
+   * Constructor which allocates an initial count array for only two distinct
+   * values (the array will grow if needed)
+   */
+  public SparseCount()
+  {
+    this(DEFAULT_PROFILE_SIZE);
+  }
+
+  /**
+   * Adds the given value for the given key (or sets the initial value), and
+   * returns the new value
+   * 
+   * @param key
+   * @param value
+   */
+  public int add(int key, int value)
+  {
+    int newValue = 0;
+    if (useInts)
+    {
+      newValue = intProfile.add(key, value);
+    }
+    else
+    {
+      try {
+        newValue = shortProfile.add(key, value);
+      } catch (ArithmeticException e) {
+        handleOverflow();
+        newValue = intProfile.add(key, value);
+      }
+    }
+    return newValue;
+  }
+
+  /**
+   * Switch from counting shorts to counting ints
+   */
+  synchronized void handleOverflow()
+  {
+    int size = shortProfile.size();
+    intProfile = new SparseIntArray(size);
+    for (int i = 0; i < size; i++)
+    {
+      short key = shortProfile.keyAt(i);
+      short value = shortProfile.valueAt(i);
+      intProfile.put(key, value);
+    }
+    shortProfile = null;
+    useInts = true;
+  }
+
+  /**
+   * Returns the size of the profile (number of distinct items counted)
+   * 
+   * @return
+   */
+  public int size()
+  {
+    return useInts ? intProfile.size() : shortProfile.size();
+  }
+
+  /**
+   * Returns the value for the key (zero if no such key)
+   * 
+   * @param key
+   * @return
+   */
+  public int get(int key)
+  {
+    return useInts ? intProfile.get(key) : shortProfile.get(key);
+  }
+
+  /**
+   * Sets the value for the given key
+   * 
+   * @param key
+   * @param value
+   */
+  public void put(int key, int value)
+  {
+    if (useInts)
+    {
+      intProfile.put(key, value);
+    }
+    else
+    {
+      shortProfile.put(key, value);
+    }
+  }
+
+  public int keyAt(int k)
+  {
+    return useInts ? intProfile.keyAt(k) : shortProfile.keyAt(k);
+  }
+
+  public int valueAt(int k)
+  {
+    return useInts ? intProfile.valueAt(k) : shortProfile.valueAt(k);
+  }
+
+  /**
+   * Answers true if this object wraps arrays of int values, false if using
+   * short values
+   * 
+   * @return
+   */
+  boolean isUsingInt()
+  {
+    return useInts;
+  }
+}
index c1c88c1..c01be4e 100644 (file)
@@ -22,6 +22,7 @@ package jalview.viewmodel;
 
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.Conservation;
+import jalview.analysis.Profile;
 import jalview.api.AlignCalcManagerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
@@ -700,7 +701,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   /**
    * results of alignment consensus analysis for visible portion of view
    */
-  protected Hashtable[] hconsensus = null;
+  protected Profile[] hconsensus = null;
 
   /**
    * results of cDNA complement consensus visible portion of view
@@ -734,7 +735,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   }
 
   @Override
-  public void setSequenceConsensusHash(Hashtable[] hconsensus)
+  public void setSequenceConsensusHash(Profile[] hconsensus)
   {
     this.hconsensus = hconsensus;
   }
@@ -746,7 +747,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   }
 
   @Override
-  public Hashtable[] getSequenceConsensusHash()
+  public Profile[] getSequenceConsensusHash()
   {
     return hconsensus;
   }
@@ -939,7 +940,9 @@ public abstract class AlignmentViewport implements AlignViewportI,
     groupConservation = null;
     hconsensus = null;
     hcomplementConsensus = null;
-    // TODO removed listeners from changeSupport?
+    // colour scheme may hold reference to consensus
+    globalColourScheme = null;
+    // TODO remove listeners from changeSupport?
     changeSupport = null;
     setAlignment(null);
   }
index 529df6f..431fbec 100644 (file)
@@ -96,7 +96,6 @@ public class ComplementConsensusThread extends ConsensusThread
    * @param consensusData
    *          the computed consensus data
    */
-  @Override
   protected void deriveConsensus(AlignmentAnnotation consensusAnnotation,
           Hashtable[] consensusData)
   {
@@ -104,4 +103,16 @@ public class ComplementConsensusThread extends ConsensusThread
             alignViewport.isShowSequenceLogo(), getSequences().length);
   }
 
+  @Override
+  public void updateResultAnnotation(boolean immediate)
+  {
+    AlignmentAnnotation consensus = getConsensusAnnotation();
+    Hashtable[] hconsensus = getViewportConsensus();
+    if (immediate || !calcMan.isWorking(this) && consensus != null
+            && hconsensus != null)
+    {
+      deriveConsensus(consensus, hconsensus);
+    }
+  }
+
 }
index 5f0ec84..c88a24a 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.workers;
 
 import jalview.analysis.AAFrequency;
+import jalview.analysis.Profile;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentAnnotation;
@@ -29,8 +30,6 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.SequenceI;
 import jalview.schemes.ColourSchemeI;
 
-import java.util.Hashtable;
-
 public class ConsensusThread extends AlignCalcWorker
 {
   public ConsensusThread(AlignViewportI alignViewport,
@@ -125,7 +124,7 @@ public class ConsensusThread extends AlignCalcWorker
    */
   protected void computeConsensus(AlignmentI alignment)
   {
-    Hashtable[] hconsensus = new Hashtable[alignment.getWidth()];
+    Profile[] hconsensus = new Profile[alignment.getWidth()];
 
     SequenceI[] aseqs = getSequences();
     AAFrequency.calculate(aseqs, 0, alignment.getWidth(), hconsensus, true);
@@ -145,7 +144,7 @@ public class ConsensusThread extends AlignCalcWorker
   /**
    * @param hconsensus
    */
-  protected void setColourSchemeConsensus(Hashtable[] hconsensus)
+  protected void setColourSchemeConsensus(Profile[] hconsensus)
   {
     ColourSchemeI globalColourScheme = alignViewport
             .getGlobalColourScheme();
@@ -178,7 +177,7 @@ public class ConsensusThread extends AlignCalcWorker
   public void updateResultAnnotation(boolean immediate)
   {
     AlignmentAnnotation consensus = getConsensusAnnotation();
-    Hashtable[] hconsensus = getViewportConsensus();
+    Profile[] hconsensus = (Profile[]) getViewportConsensus();
     if (immediate || !calcMan.isWorking(this) && consensus != null
             && hconsensus != null)
     {
@@ -192,15 +191,15 @@ public class ConsensusThread extends AlignCalcWorker
    * 
    * @param consensusAnnotation
    *          the annotation to be populated
-   * @param consensusData
+   * @param hconsensus
    *          the computed consensus data
    */
   protected void deriveConsensus(AlignmentAnnotation consensusAnnotation,
-          Hashtable[] consensusData)
+          Profile[] hconsensus)
   {
     long nseq = getSequences().length;
-    AAFrequency.completeConsensus(consensusAnnotation, consensusData, 0,
-            consensusData.length, alignViewport.isIgnoreGapsConsensus(),
+    AAFrequency.completeConsensus(consensusAnnotation, hconsensus, 0,
+            hconsensus.length, alignViewport.isIgnoreGapsConsensus(),
             alignViewport.isShowSequenceLogo(), nseq);
   }
 
@@ -209,8 +208,9 @@ public class ConsensusThread extends AlignCalcWorker
    * 
    * @return
    */
-  protected Hashtable[] getViewportConsensus()
+  protected Object[] getViewportConsensus()
   {
+    // TODO convert ComplementConsensusThread to use Profile
     return alignViewport.getSequenceConsensusHash();
   }
 }
index eff6bbf..0ddbddc 100644 (file)
@@ -23,65 +23,62 @@ package jalview.analysis;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertNull;
 
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 
-import java.util.Hashtable;
-
 import org.testng.annotations.Test;
 
 public class AAFrequencyTest
 {
-  private static final String C = AAFrequency.MAXCOUNT;
-
-  private static final String R = AAFrequency.MAXRESIDUE;
-
-  private static final String G = AAFrequency.PID_GAPS;
-
-  private static final String N = AAFrequency.PID_NOGAPS;
-
-  private static final String P = AAFrequency.PROFILE;
-
   @Test(groups = { "Functional" })
   public void testCalculate_noProfile()
   {
-    SequenceI seq1 = new Sequence("Seq1", "CAGT");
-    SequenceI seq2 = new Sequence("Seq2", "CACT");
-    SequenceI seq3 = new Sequence("Seq3", "C--G");
-    SequenceI seq4 = new Sequence("Seq4", "CA-t");
+    SequenceI seq1 = new Sequence("Seq1", "CAG-T");
+    SequenceI seq2 = new Sequence("Seq2", "CAC-T");
+    SequenceI seq3 = new Sequence("Seq3", "C---G");
+    SequenceI seq4 = new Sequence("Seq4", "CA--t");
     SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
-    Hashtable[] result = new Hashtable[seq1.getLength()];
+    Profile[] result = new Profile[seq1.getLength()];
 
     AAFrequency.calculate(seqs, 0, seq1.getLength(), result, false);
 
     // col 0 is 100% C
-    Hashtable col = result[0];
-    assertEquals(100f, (Float) col.get(G), 0.0001f);
-    assertEquals(100f, (Float) col.get(N), 0.0001f);
-    assertEquals(4, col.get(C));
-    assertEquals("C", col.get(R));
-    assertNull(col.get(P));
+    Profile col = result[0];
+    assertEquals(100f, col.getPercentageIdentity(false));
+    assertEquals(100f, col.getPercentageIdentity(true));
+    assertEquals(4, col.getMaxCount());
+    assertEquals("C", col.getModalResidue());
+    assertNull(col.getCounts());
 
     // col 1 is 75% A
     col = result[1];
-    assertEquals(75f, (Float) col.get(G), 0.0001f);
-    assertEquals(100f, (Float) col.get(N), 0.0001f);
-    assertEquals(3, col.get(C));
-    assertEquals("A", col.get(R));
+    assertEquals(75f, col.getPercentageIdentity(false));
+    assertEquals(100f, col.getPercentageIdentity(true));
+    assertEquals(3, col.getMaxCount());
+    assertEquals("A", col.getModalResidue());
 
     // col 2 is 50% G 50% C or 25/25 counting gaps
     col = result[2];
-    assertEquals(25f, (Float) col.get(G), 0.0001f);
-    assertEquals(50f, (Float) col.get(N), 0.0001f);
-    assertEquals(1, col.get(C));
-    assertEquals("CG", col.get(R));
+    assertEquals(25f, col.getPercentageIdentity(false));
+    assertEquals(50f, col.getPercentageIdentity(true));
+    assertEquals(1, col.getMaxCount());
+    assertEquals("CG", col.getModalResidue());
 
-    // col 3 is 75% T 25% G
+    // col 3 is all gaps
     col = result[3];
-    assertEquals(75f, (Float) col.get(G), 0.0001f);
-    assertEquals(75f, (Float) col.get(N), 0.0001f);
-    assertEquals(3, col.get(C));
-    assertEquals("T", col.get(R));
+    assertEquals(0f, col.getPercentageIdentity(false));
+    assertEquals(0f, col.getPercentageIdentity(true));
+    assertEquals(0, col.getMaxCount());
+    assertEquals("", col.getModalResidue());
+
+    // col 4 is 75% T 25% G
+    col = result[4];
+    assertEquals(75f, col.getPercentageIdentity(false));
+    assertEquals(75f, col.getPercentageIdentity(true));
+    assertEquals(3, col.getMaxCount());
+    assertEquals("T", col.getModalResidue());
   }
 
   @Test(groups = { "Functional" })
@@ -92,33 +89,33 @@ public class AAFrequencyTest
     SequenceI seq3 = new Sequence("Seq3", "C--G");
     SequenceI seq4 = new Sequence("Seq4", "CA-t");
     SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
-    Hashtable[] result = new Hashtable[seq1.getLength()];
+    Profile[] result = new Profile[seq1.getLength()];
 
     AAFrequency.calculate(seqs, 0, seq1.getLength(), result, true);
-    int[][] profile = (int[][]) result[0].get(P);
-    assertEquals(4, profile[0]['C']);
-    assertEquals(4, profile[1][0]); // no of seqs
-    assertEquals(4, profile[1][1]); // nongapped in column
-
-    profile = (int[][]) result[1].get(P);
-    assertEquals(3, profile[0]['A']);
-    assertEquals(4, profile[1][0]);
-    assertEquals(3, profile[1][1]);
-
-    profile = (int[][]) result[2].get(P);
-    assertEquals(1, profile[0]['G']);
-    assertEquals(1, profile[0]['C']);
-    assertEquals(4, profile[1][0]);
-    assertEquals(2, profile[1][1]);
-
-    profile = (int[][]) result[3].get(P);
-    assertEquals(3, profile[0]['T']);
-    assertEquals(1, profile[0]['G']);
-    assertEquals(4, profile[1][0]);
-    assertEquals(4, profile[1][1]);
+    Profile profile = result[0];
+    assertEquals(4, profile.getCounts().getCount('C'));
+    assertEquals(4, profile.getHeight());
+    assertEquals(4, profile.getNonGapped());
+
+    profile = result[1];
+    assertEquals(3, profile.getCounts().getCount('A'));
+    assertEquals(4, profile.getHeight());
+    assertEquals(3, profile.getNonGapped());
+
+    profile = result[2];
+    assertEquals(1, profile.getCounts().getCount('C'));
+    assertEquals(1, profile.getCounts().getCount('G'));
+    assertEquals(4, profile.getHeight());
+    assertEquals(2, profile.getNonGapped());
+
+    profile = result[3];
+    assertEquals(3, profile.getCounts().getCount('T'));
+    assertEquals(1, profile.getCounts().getCount('G'));
+    assertEquals(4, profile.getHeight());
+    assertEquals(4, profile.getNonGapped());
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Functional" }, enabled = false)
   public void testCalculate_withProfileTiming()
   {
     SequenceI seq1 = new Sequence("Seq1", "CAGT");
@@ -126,7 +123,7 @@ public class AAFrequencyTest
     SequenceI seq3 = new Sequence("Seq3", "C--G");
     SequenceI seq4 = new Sequence("Seq4", "CA-t");
     SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
-    Hashtable[] result = new Hashtable[seq1.getLength()];
+    Profile[] result = new Profile[seq1.getLength()];
 
     // ensure class loaded and initialized
     AAFrequency.calculate(seqs, 0, seq1.getLength(), result, true);
@@ -139,14 +136,86 @@ public class AAFrequencyTest
     System.out.println(System.currentTimeMillis() - start);
   }
 
+  /**
+   * Test generation of consensus annotation with options 'include gaps'
+   * (profile percentages are of all sequences, whether gapped or not), and
+   * 'show logo' (the full profile with all residue percentages is reported in
+   * the description for the tooltip)
+   */
+  @Test(groups = { "Functional" })
+  public void testCompleteConsensus_includeGaps_showLogo()
+  {
+    /*
+     * first compute the profiles
+     */
+    SequenceI seq1 = new Sequence("Seq1", "CAG-T");
+    SequenceI seq2 = new Sequence("Seq2", "CAC-T");
+    SequenceI seq3 = new Sequence("Seq3", "C---G");
+    SequenceI seq4 = new Sequence("Seq4", "CA--t");
+    SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
+    Profile[] profiles = new Profile[seq1.getLength()];
+    AAFrequency.calculate(seqs, 0, seq1.getLength(), profiles, true);
+
+    AlignmentAnnotation consensus = new AlignmentAnnotation("Consensus",
+            "PID", new Annotation[seq1.getLength()]);
+    AAFrequency
+            .completeConsensus(consensus, profiles, 0, 5, false, true, 4);
+
+    Annotation ann = consensus.annotations[0];
+    assertEquals("C 100%", ann.description);
+    assertEquals("C", ann.displayCharacter);
+    ann = consensus.annotations[1];
+    assertEquals("A 75%", ann.description);
+    assertEquals("A", ann.displayCharacter);
+    ann = consensus.annotations[2];
+    assertEquals("C 25%; G 25%", ann.description);
+    assertEquals("+", ann.displayCharacter);
+    ann = consensus.annotations[3];
+    assertEquals("", ann.description);
+    assertEquals("-", ann.displayCharacter);
+    ann = consensus.annotations[4];
+    assertEquals("T 75%; G 25%", ann.description);
+    assertEquals("T", ann.displayCharacter);
+  }
+
+  /**
+   * Test generation of consensus annotation with options 'ignore gaps' (profile
+   * percentages are of the non-gapped sequences) and 'no logo' (only the modal
+   * residue[s] percentage is reported in the description for the tooltip)
+   */
   @Test(groups = { "Functional" })
-  public void testGetPercentageFormat()
+  public void testCompleteConsensus_ignoreGaps_noLogo()
   {
-    assertNull(AAFrequency.getPercentageFormat(0));
-    assertNull(AAFrequency.getPercentageFormat(99));
-    assertEquals("%3.1f", AAFrequency.getPercentageFormat(100).toString());
-    assertEquals("%3.1f", AAFrequency.getPercentageFormat(999).toString());
-    assertEquals("%3.2f", AAFrequency.getPercentageFormat(1000).toString());
-    assertEquals("%3.2f", AAFrequency.getPercentageFormat(9999).toString());
+    /*
+     * first compute the profiles
+     */
+    SequenceI seq1 = new Sequence("Seq1", "CAG-T");
+    SequenceI seq2 = new Sequence("Seq2", "CAC-T");
+    SequenceI seq3 = new Sequence("Seq3", "C---G");
+    SequenceI seq4 = new Sequence("Seq4", "CA--t");
+    SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
+    Profile[] profiles = new Profile[seq1.getLength()];
+    AAFrequency.calculate(seqs, 0, seq1.getLength(), profiles, true);
+  
+    AlignmentAnnotation consensus = new AlignmentAnnotation("Consensus",
+            "PID", new Annotation[seq1.getLength()]);
+    AAFrequency
+            .completeConsensus(consensus, profiles, 0, 5, true, false, 4);
+  
+    Annotation ann = consensus.annotations[0];
+    assertEquals("C 100%", ann.description);
+    assertEquals("C", ann.displayCharacter);
+    ann = consensus.annotations[1];
+    assertEquals("A 100%", ann.description);
+    assertEquals("A", ann.displayCharacter);
+    ann = consensus.annotations[2];
+    assertEquals("[CG] 50%", ann.description);
+    assertEquals("+", ann.displayCharacter);
+    ann = consensus.annotations[3];
+    assertEquals("", ann.description);
+    assertEquals("-", ann.displayCharacter);
+    ann = consensus.annotations[4];
+    assertEquals("T 75%", ann.description);
+    assertEquals("T", ann.displayCharacter);
   }
 }
diff --git a/test/jalview/analysis/ResidueCountTest.java b/test/jalview/analysis/ResidueCountTest.java
new file mode 100644 (file)
index 0000000..4a71f89
--- /dev/null
@@ -0,0 +1,404 @@
+package jalview.analysis;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.analysis.ResidueCount.SymbolCounts;
+
+import org.junit.Assert;
+import org.testng.annotations.Test;
+
+public class ResidueCountTest
+{
+  /**
+   * Test a mix of add and put for nucleotide counting
+   */
+  @Test(groups = "Functional")
+  public void test_countNucleotide()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    assertEquals(rc.getCount('A'), 0);
+    assertEquals(rc.getGapCount(), 0);
+    // add then add
+    assertEquals(rc.add('A'), 1);
+    assertEquals(rc.add('a'), 2);
+    // put then add
+    rc.put('g', 3);
+    assertEquals(rc.add('G'), 4);
+    // add then put
+    assertEquals(rc.add('c'), 1);
+    rc.put('C', 4);
+    assertEquals(rc.add('N'), 1);
+
+    assertEquals(rc.getCount('a'), 2);
+    assertEquals(rc.getCount('A'), 2);
+    assertEquals(rc.getCount('G'), 4);
+    assertEquals(rc.getCount('c'), 4);
+    assertEquals(rc.getCount('T'), 0); // never seen
+    assertEquals(rc.getCount('N'), 1);
+    assertEquals(rc.getCount('?'), 0);
+    assertEquals(rc.getCount('-'), 0);
+
+    assertFalse(rc.isCountingInts());
+    assertFalse(rc.isUsingOtherData());
+  }
+
+  /**
+   * Test adding to gap count (either using addGap or add)
+   */
+  @Test(groups = "Functional")
+  public void testAddGap()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    rc.addGap();
+    rc.add('-');
+    rc.add('.');
+    rc.add(' ');
+    
+    assertEquals(rc.getGapCount(), 4);
+    assertEquals(rc.getCount(' '), 4);
+    assertEquals(rc.getCount('-'), 4);
+    assertEquals(rc.getCount('.'), 4);
+    assertFalse(rc.isUsingOtherData());
+    assertFalse(rc.isCountingInts());
+  }
+
+  @Test(groups = "Functional")
+  public void testOverflow()
+  {
+    /*
+     * overflow from add
+     */
+    ResidueCount rc = new ResidueCount(true);
+    rc.addGap();
+    rc.put('A', Short.MAX_VALUE - 1);
+    assertFalse(rc.isCountingInts());
+    rc.add('A');
+    assertFalse(rc.isCountingInts());
+    rc.add('A');
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getCount('a'), Short.MAX_VALUE + 1);
+    rc.add('A');
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getCount('a'), Short.MAX_VALUE + 2);
+    assertEquals(rc.getGapCount(), 1);
+    rc.addGap();
+    assertEquals(rc.getGapCount(), 2);
+
+    /*
+     * overflow from put
+     */
+    rc = new ResidueCount(true);
+    rc.put('G', Short.MAX_VALUE + 1);
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getCount('g'), Short.MAX_VALUE + 1);
+    rc.put('G', 1);
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getCount('g'), 1);
+
+    /*
+     * underflow from put
+     */
+    rc = new ResidueCount(true);
+    rc.put('G', Short.MIN_VALUE - 1);
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getCount('g'), Short.MIN_VALUE - 1);
+  }
+
+  /**
+   * Test a mix of add and put for peptide counting
+   */
+  @Test(groups = "Functional")
+  public void test_countPeptide()
+  {
+    ResidueCount rc = new ResidueCount(false);
+    rc.put('q', 4);
+    rc.add('Q');
+    rc.add('X');
+    rc.add('x');
+    rc.add('W');
+    rc.put('w', 7);
+    rc.put('m', 12);
+    rc.put('M', 13);
+
+    assertEquals(rc.getCount('q'), 5);
+    assertEquals(rc.getCount('X'), 2);
+    assertEquals(rc.getCount('W'), 7);
+    assertEquals(rc.getCount('m'), 13);
+    assertEquals(rc.getCount('G'), 0);
+    assertEquals(rc.getCount('-'), 0);
+
+    assertFalse(rc.isCountingInts());
+    assertFalse(rc.isUsingOtherData());
+  }
+
+  @Test(groups = "Functional")
+  public void test_unexpectedPeptide()
+  {
+    ResidueCount rc = new ResidueCount(false);
+    // expected characters (upper or lower case):
+    String aas = "ACDEFGHIKLMNPQRSTVWXY";
+    String lower = aas.toLowerCase();
+    for (int i = 0; i < aas.length(); i++)
+    {
+      rc.put(aas.charAt(i), i);
+      rc.add(lower.charAt(i));
+    }
+    for (int i = 0; i < aas.length(); i++)
+    {
+      assertEquals(rc.getCount(aas.charAt(i)), i + 1);
+    }
+    assertFalse(rc.isUsingOtherData());
+
+    rc.put('J', 4);
+    assertTrue(rc.isUsingOtherData());
+    assertEquals(rc.getCount('J'), 4);
+    rc.add('j');
+    assertEquals(rc.getCount('J'), 5);
+  }
+
+  @Test(groups = "Functional")
+  public void test_unexpectedNucleotide()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    // expected characters (upper or lower case):
+    String nucs = "ACGTUN";
+    String lower = nucs.toLowerCase();
+    for (int i = 0; i < nucs.length(); i++)
+    {
+      rc.put(nucs.charAt(i), i);
+      rc.add(lower.charAt(i));
+    }
+    for (int i = 0; i < nucs.length(); i++)
+    {
+      assertEquals(rc.getCount(nucs.charAt(i)), i + 1);
+    }
+    assertFalse(rc.isUsingOtherData());
+
+    rc.add('J');
+    assertTrue(rc.isUsingOtherData());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModalCount()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    rc.add('c');
+    rc.add('g');
+    rc.add('c');
+    assertEquals(rc.getModalCount(), 2);
+
+    // modal count is in the 'short overflow' counts
+    rc = new ResidueCount();
+    rc.add('c');
+    rc.put('g', Short.MAX_VALUE);
+    rc.add('G');
+    assertEquals(rc.getModalCount(), Short.MAX_VALUE + 1);
+
+    // modal count is in the 'other data' counts
+    rc = new ResidueCount(false);
+    rc.add('Q');
+    rc.add('{');
+    rc.add('{');
+    assertEquals(rc.getModalCount(), 2);
+
+    // verify modal count excludes gap
+    rc = new ResidueCount();
+    rc.add('Q');
+    rc.add('P');
+    rc.add('Q');
+    rc.addGap();
+    rc.addGap();
+    rc.addGap();
+    assertEquals(rc.getModalCount(), 2);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResiduesForCount()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    rc.add('c');
+    rc.add('g');
+    rc.add('c');
+    assertEquals(rc.getResiduesForCount(2), "C");
+    assertEquals(rc.getResiduesForCount(1), "G");
+    assertEquals(rc.getResiduesForCount(3), "");
+    assertEquals(rc.getResiduesForCount(0), "");
+    assertEquals(rc.getResiduesForCount(-1), "");
+
+    // modal count is in the 'short overflow' counts
+    rc = new ResidueCount(true);
+    rc.add('c');
+    rc.put('g', Short.MAX_VALUE);
+    rc.add('G');
+    assertEquals(rc.getResiduesForCount(Short.MAX_VALUE + 1), "G");
+    assertEquals(rc.getResiduesForCount(1), "C");
+
+    // peptide modal count is in the 'short overflow' counts
+    rc = new ResidueCount(false);
+    rc.add('c');
+    rc.put('p', Short.MAX_VALUE);
+    rc.add('P');
+    assertEquals(rc.getResiduesForCount(Short.MAX_VALUE + 1), "P");
+    assertEquals(rc.getResiduesForCount(1), "C");
+  
+    // modal count is in the 'other data' counts
+    rc = new ResidueCount();
+    rc.add('Q');
+    rc.add('{');
+    rc.add('{');
+    assertEquals(rc.getResiduesForCount(1), "Q");
+    assertEquals(rc.getResiduesForCount(2), "{");
+
+    // residues share modal count
+    rc = new ResidueCount();
+    rc.add('G');
+    rc.add('G');
+    rc.add('c');
+    rc.add('C');
+    rc.add('U');
+    assertEquals(rc.getResiduesForCount(1), "U");
+    assertEquals(rc.getResiduesForCount(2), "CG");
+
+    // expected and unexpected symbols share modal count
+    rc = new ResidueCount();
+    rc.add('G');
+    rc.add('t');
+    rc.add('[');
+    rc.add('[');
+    rc.add('t');
+    rc.add('G');
+    rc.add('c');
+    rc.add('C');
+    rc.add('U');
+    assertEquals(rc.getResiduesForCount(1), "U");
+    assertEquals(rc.getResiduesForCount(2), "CGT[");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSymbolCounts_nucleotide()
+  {
+    ResidueCount rc = new ResidueCount(true);
+    rc.add('g');
+    rc.add('c');
+    rc.add('G');
+    rc.add('J'); // 'otherData'
+    rc.add('g');
+    rc.add('N');
+    rc.put('[', 0); // 'otherdata'
+
+    SymbolCounts sc = rc.getSymbolCounts();
+    Assert.assertArrayEquals(new char[] { 'C', 'G', 'N', 'J', '[' },
+            sc.symbols);
+    Assert.assertArrayEquals(new int[] { 1, 3, 1, 1, 0 }, sc.values);
+
+    // now with overflow to int counts
+    rc.put('U', Short.MAX_VALUE);
+    rc.add('u');
+    sc = rc.getSymbolCounts();
+    Assert.assertArrayEquals(new char[] { 'C', 'G', 'N', 'U', 'J', '[' },
+            sc.symbols);
+    Assert.assertArrayEquals(new int[] { 1, 3, 1, 32768, 1, 0 }, sc.values);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSymbolCounts_peptide()
+  {
+    ResidueCount rc = new ResidueCount(false);
+    rc.add('W');
+    rc.add('q');
+    rc.add('W');
+    rc.add('Z'); // 'otherData'
+    rc.add('w');
+    rc.add('L');
+
+    SymbolCounts sc = rc.getSymbolCounts();
+    Assert.assertArrayEquals(new char[] { 'L', 'Q', 'W', 'Z' }, sc.symbols);
+    Assert.assertArrayEquals(new int[] { 1, 1, 3, 1 }, sc.values);
+
+    // now with overflow to int counts
+    rc.put('W', Short.MAX_VALUE);
+    rc.add('W');
+    sc = rc.getSymbolCounts();
+    Assert.assertArrayEquals(new char[] { 'L', 'Q', 'W', 'Z' }, sc.symbols);
+    Assert.assertArrayEquals(new int[] { 1, 1, 32768, 1 }, sc.values);
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    ResidueCount rc = new ResidueCount();
+    rc.add('q');
+    rc.add('c');
+    rc.add('Q');
+    assertEquals(rc.toString(), "[ C:1 Q:2 ]");
+
+    // add 'other data'
+    rc.add('{');
+    assertEquals(rc.toString(), "[ C:1 Q:2 {:1 ]");
+
+    // switch from short to int counting:
+    rc.put('G', Short.MAX_VALUE);
+    rc.add('g');
+    assertEquals(rc.toString(), "[ C:1 G:32768 Q:2 {:1 ]");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTooltip()
+  {
+    ResidueCount rc = new ResidueCount();
+
+    // no counts!
+    assertEquals(rc.getTooltip(20, 1), "");
+
+    /*
+     * count 7 C, 6 K, 7 Q, 10 P, 9 W, 1 F (total 40)
+     */
+    for (int i = 0; i < 7; i++)
+    {
+      rc.add('c');
+      rc.add('q');
+    }
+    for (int i = 0; i < 10; i++)
+    {
+      rc.add('p');
+    }
+    for (int i = 0; i < 9; i++)
+    {
+      rc.add('W');
+    }
+    for (int i = 0; i < 6; i++)
+    {
+      rc.add('K');
+    }
+    rc.add('F');
+    
+    assertEquals(rc.getTooltip(40, 0),
+            "P 25%; W 22%; C 17%; Q 17%; K 15%; F 2%");
+
+    assertEquals(rc.getTooltip(30, 1),
+            "P 33.3%; W 30.0%; C 23.3%; Q 23.3%; K 20.0%; F 3.3%");
+  }
+
+  @Test(groups = "Functional")
+  public void testPut()
+  {
+    ResidueCount rc = new ResidueCount();
+    rc.put('q', 3);
+    assertEquals(rc.getCount('Q'), 3);
+    rc.put(' ', 4);
+    assertEquals(rc.getGapCount(), 4);
+    rc.put('.', 5);
+    assertEquals(rc.getGapCount(), 5);
+    rc.put('-', 6);
+    assertEquals(rc.getGapCount(), 6);
+
+    rc.put('?', 5);
+    assertEquals(rc.getCount('?'), 5);
+    rc.put('?', 6);
+    rc.put('!', 7);
+    assertEquals(rc.getCount('?'), 6);
+    assertEquals(rc.getCount('!'), 7);
+  }
+}
diff --git a/test/jalview/ext/android/SparseIntArrayTest.java b/test/jalview/ext/android/SparseIntArrayTest.java
new file mode 100644 (file)
index 0000000..0ce0467
--- /dev/null
@@ -0,0 +1,124 @@
+package jalview.ext.android;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import org.testng.annotations.Test;
+
+/*
+ * Tests for SparseIntArray. Unlike SparseShortArray, SparseIntArray does not throw
+ * any exception for overflow.
+ */
+public class SparseIntArrayTest
+{
+  @Test(groups = "Functional")
+  public void testPut()
+  {
+    SparseIntArray counter = new SparseIntArray();
+
+    /*
+     * either key or value may be in the range of int
+     */
+    counter.put(Integer.MAX_VALUE, Integer.MIN_VALUE);
+    counter.put(Integer.MIN_VALUE, Integer.MAX_VALUE);
+    assertEquals(counter.get(Integer.MAX_VALUE), Integer.MIN_VALUE);
+    assertEquals(counter.get(Integer.MIN_VALUE), Integer.MAX_VALUE);
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd()
+  {
+    SparseIntArray counter = new SparseIntArray();
+  
+    assertEquals(counter.add('P', 2), 2);
+    assertEquals(counter.add('P', 3), 5);
+    counter.put('Q', 7);
+    assertEquals(counter.add('Q', 4), 11);
+
+    counter.put('x', Integer.MAX_VALUE);
+    try
+    {
+      counter.add('x', 1);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+  
+    counter.put('y', Integer.MIN_VALUE);
+    try
+    {
+      counter.add('y', -1);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testCheckOverflow()
+  {
+    // things that don't overflow:
+    SparseIntArray.checkOverflow(Integer.MAX_VALUE, 0);
+    SparseIntArray.checkOverflow(Integer.MAX_VALUE, -1);
+    SparseIntArray.checkOverflow(Integer.MAX_VALUE, Integer.MIN_VALUE);
+    SparseIntArray.checkOverflow(Integer.MAX_VALUE, -Integer.MAX_VALUE);
+    SparseIntArray.checkOverflow(0, -Integer.MAX_VALUE);
+    SparseIntArray.checkOverflow(0, Integer.MIN_VALUE);
+    SparseIntArray.checkOverflow(Integer.MIN_VALUE, 0);
+    SparseIntArray.checkOverflow(Integer.MIN_VALUE, 1);
+    SparseIntArray.checkOverflow(Integer.MIN_VALUE, Integer.MAX_VALUE);
+
+    // and some that do
+    try
+    {
+      SparseIntArray.checkOverflow(Integer.MAX_VALUE, 1);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+    try
+    {
+      SparseIntArray.checkOverflow(Integer.MAX_VALUE - 1, 2);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+    try
+    {
+      SparseIntArray.checkOverflow(1, Integer.MAX_VALUE);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+    try
+    {
+      SparseIntArray.checkOverflow(Integer.MIN_VALUE, -1);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+    try
+    {
+      SparseIntArray.checkOverflow(Integer.MIN_VALUE + 1, -2);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+    try
+    {
+      SparseIntArray.checkOverflow(-1, Integer.MIN_VALUE);
+      fail("expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected
+    }
+  }
+
+}
diff --git a/test/jalview/ext/android/SparseShortArrayTest.java b/test/jalview/ext/android/SparseShortArrayTest.java
new file mode 100644 (file)
index 0000000..351f640
--- /dev/null
@@ -0,0 +1,94 @@
+package jalview.ext.android;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import org.testng.annotations.Test;
+
+public class SparseShortArrayTest
+{
+  @Test(groups = "Functional")
+  public void testPut()
+  {
+    SparseShortArray counter = new SparseShortArray();
+
+    /*
+     * either key or value may be in the range of short
+     */
+    counter.put(Short.MAX_VALUE, Short.MIN_VALUE);
+    counter.put(Short.MIN_VALUE, Short.MAX_VALUE);
+
+    // put a too large value
+    try
+    {
+      counter.put(0, Short.MAX_VALUE + 1);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+
+    // put a too small value
+    try
+    {
+      counter.put(1, Short.MIN_VALUE - 1);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+
+    // put a too large key
+    try
+    {
+      counter.put(Short.MAX_VALUE + 1, 0);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+
+    // put a too small key
+    try
+    {
+      counter.put(Short.MIN_VALUE - 1, 2);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd()
+  {
+    SparseShortArray counter = new SparseShortArray();
+  
+    assertEquals(counter.add('P', 2), 2);
+    assertEquals(counter.add('P', 3), 5);
+    counter.put('Q', 7);
+    assertEquals(counter.add('Q', 4), 11);
+
+    // increment giving overflow
+    counter.put('x', Short.MAX_VALUE);
+    try
+    {
+      counter.add('x', 1);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+  
+    // decrement giving underflow
+    counter.put('y', Short.MIN_VALUE);
+    try
+    {
+      counter.add('y', -1);
+      fail("Expected exception");
+    } catch (ArithmeticException e)
+    {
+      // expected;
+    }
+  }
+}
diff --git a/test/jalview/schemes/ResidueColourSchemeTest.java b/test/jalview/schemes/ResidueColourSchemeTest.java
new file mode 100644 (file)
index 0000000..8173dd1
--- /dev/null
@@ -0,0 +1,147 @@
+package jalview.schemes;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.analysis.Profile;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+
+public class ResidueColourSchemeTest
+{
+  @Test(groups = "Functional")
+  public void testAboveThreshold()
+  {
+    /*
+     * make up profiles for this alignment:
+     * AR-Q
+     * AR--
+     * SR-T
+     * SR-T
+     */
+    Profile[] profiles = new Profile[4]; 
+    profiles[0] = new Profile(4, 0, 2, "AS");
+    profiles[1] = new Profile(4, 0, 4, "R");
+    profiles[2] = new Profile(4, 4, 0, "");
+    profiles[3] = new Profile(4, 1, 2, "T");
+    ResidueColourScheme rcs = new ResidueColourScheme();
+    rcs.setConsensus(profiles);
+    
+    /*
+     * no threshold
+     */
+    rcs.setThreshold(0, true);
+    assertTrue(rcs.aboveThreshold('a', 0));
+    assertTrue(rcs.aboveThreshold('S', 0));
+    assertFalse(rcs.aboveThreshold('W', 0));
+    assertTrue(rcs.aboveThreshold('R', 1));
+    assertFalse(rcs.aboveThreshold('W', 2));
+    assertTrue(rcs.aboveThreshold('t', 3));
+    assertFalse(rcs.aboveThreshold('Q', 3));
+
+    /*
+     * with threshold, include gaps
+     */
+    rcs.setThreshold(60, false);
+    assertFalse(rcs.aboveThreshold('a', 0));
+    assertFalse(rcs.aboveThreshold('S', 0));
+    assertTrue(rcs.aboveThreshold('R', 1));
+    assertFalse(rcs.aboveThreshold('W', 2));
+    assertFalse(rcs.aboveThreshold('t', 3)); // 50% < 60%
+
+    /*
+     * with threshold, ignore gaps
+     */
+    rcs.setThreshold(60, true);
+    assertFalse(rcs.aboveThreshold('a', 0));
+    assertFalse(rcs.aboveThreshold('S', 0));
+    assertTrue(rcs.aboveThreshold('R', 1));
+    assertFalse(rcs.aboveThreshold('W', 2));
+    assertTrue(rcs.aboveThreshold('t', 3)); // 67% > 60%
+  }
+
+  /**
+   * Test colour bleaching based on conservation score and conservation slider.
+   * Scores of 10 or 11 should leave colours unchanged. Gap is always white.
+   */
+  @Test(groups = "Functional")
+  public void testApplyConservation()
+  {
+    ResidueColourScheme rcs = new ResidueColourScheme();
+
+    // no conservation present - no fading
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 12));
+    
+    // cheat by setting conservation sequence directly
+    // rather than calculating it - good enough for this test
+    String consensus = "0123456789+*-";
+    rcs.conservation = consensus.toCharArray();
+
+    // column out of range:
+    assertEquals(Color.RED,
+            rcs.applyConservation(Color.RED, consensus.length()));
+
+    /*
+     * with 100% threshold, 'fade factor' is 
+     * (11-score)/10 * 100/20 = (11-score)/2
+     * which is >= 1 for all scores i.e. all fade to white except +, *
+     */
+    rcs.setConservationInc(100);
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 0));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 1));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 2));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 3));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 4));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 5));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 6));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 7));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 8));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 9));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12));
+
+    /*
+     * with 0% threshold, there should be no fading
+     */
+    rcs.setConservationInc(0);
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 0));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 1));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 2));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 3));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 4));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 5));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 6));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 7));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 8));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 9));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10));
+    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11));
+    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12)); // gap
+
+    /*
+     * with 40% threshold, 'fade factor' is 
+     * (11-score)/10 * 40/20 = (11-score)/5
+     * which is {>1, >1, >1, >1, >1, >1, 1, 0.8, 0.6, 0.4} for score 0-9
+     * e.g. score 7 colour fades 80% of the way to white (255, 255, 255)
+     */
+    rcs.setConservationInc(40);
+    Color colour = new Color(155, 105, 55);
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 0));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 1));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 2));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 3));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 4));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 5));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 6));
+    assertEquals(new Color(235, 225, 215), rcs.applyConservation(colour, 7));
+    assertEquals(new Color(215, 195, 175), rcs.applyConservation(colour, 8));
+    assertEquals(new Color(195, 165, 135), rcs.applyConservation(colour, 9));
+    assertEquals(colour, rcs.applyConservation(colour, 10));
+    assertEquals(colour, rcs.applyConservation(colour, 11));
+    assertEquals(Color.WHITE, rcs.applyConservation(colour, 12));
+  }
+}
diff --git a/test/jalview/util/FormatTest.java b/test/jalview/util/FormatTest.java
new file mode 100644 (file)
index 0000000..18199f9
--- /dev/null
@@ -0,0 +1,32 @@
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class FormatTest
+{
+  @Test(groups = "Functional")
+  public void testAppendPercentage()
+  {
+    StringBuilder sb = new StringBuilder();
+    Format.appendPercentage(sb, 123.456f, 0);
+    assertEquals(sb.toString(), "123");
+
+    sb.setLength(0);
+    Format.appendPercentage(sb, 123.456f, 1);
+    assertEquals(sb.toString(), "123.4");
+
+    sb.setLength(0);
+    Format.appendPercentage(sb, 123.456f, 2);
+    assertEquals(sb.toString(), "123.45");
+
+    sb.setLength(0);
+    Format.appendPercentage(sb, 123.456f, 3);
+    assertEquals(sb.toString(), "123.456");
+
+    sb.setLength(0);
+    Format.appendPercentage(sb, 123.456f, 4);
+    assertEquals(sb.toString(), "123.4560");
+  }
+}
diff --git a/test/jalview/util/SparseCountTest.java b/test/jalview/util/SparseCountTest.java
new file mode 100644 (file)
index 0000000..c9a07a1
--- /dev/null
@@ -0,0 +1,79 @@
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+public class SparseCountTest
+{
+  @Test(groups = "Functional")
+  public void testAdd()
+  {
+    SparseCount p = new SparseCount(8);
+    p.add('a', 1);
+    p.add('b', 2);
+    p.add('a', 3);
+    p.add('b', -4);
+    assertEquals(p.size(), 2);
+    assertEquals(p.get('a'), 4);
+    assertEquals(p.get('b'), -2);
+  }
+
+  @Test(groups = "Functional")
+  public void testPut()
+  {
+    SparseCount p = new SparseCount(8);
+    p.put('a', 3);
+    p.add('b', 2);
+    p.put('b', 4);
+    assertEquals(p.size(), 2);
+    assertEquals(p.get('a'), 3);
+    assertEquals(p.get('b'), 4);
+  }
+
+  /**
+   * Test handling overflow of short by switching to counting ints
+   */
+  @Test(groups = "Functional")
+  public void testOverflow()
+  {
+    SparseCount p = new SparseCount(8);
+    p.put('a', Short.MAX_VALUE - 1);
+    p.add('a', 1);
+    assertFalse(p.isUsingInt());
+    p.add('a', 1);
+    assertTrue(p.isUsingInt());
+  }
+
+  /**
+   * Test handling underflow of short by switching to counting ints
+   */
+  @Test(groups = "Functional")
+  public void testUnderflow()
+  {
+    SparseCount p = new SparseCount(8);
+    p.put('a', Short.MIN_VALUE + 1);
+    p.add('a', -1);
+    assertFalse(p.isUsingInt());
+    p.add('a', -1);
+    assertTrue(p.isUsingInt());
+  }
+
+  @Test(groups = "Functional")
+  public void testKeyAt_ValueAt()
+  {
+    SparseCount p = new SparseCount(8);
+    p.put('W', 12);
+    p.put('K', 9);
+    p.put('R', 6);
+    assertEquals(p.size(), 3);
+    assertEquals(p.keyAt(0), 'K');
+    assertEquals(p.valueAt(0), 9);
+    assertEquals(p.keyAt(1), 'R');
+    assertEquals(p.valueAt(1), 6);
+    assertEquals(p.keyAt(2), 'W');
+    assertEquals(p.valueAt(2), 12);
+  }
+
+}