JAL-1681 show cDNA consensus on protein alignment - first version
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 10 Mar 2015 11:44:42 +0000 (11:44 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 10 Mar 2015 11:44:42 +0000 (11:44 +0000)
20 files changed:
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/CodingUtils.java [new file with mode: 0644]
src/jalview/analysis/StructureFrequency.java
src/jalview/api/AlignViewportI.java
src/jalview/datamodel/AlignedCodonFrame.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/SplitFrame.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/util/Format.java
src/jalview/util/MappingUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/AlignCalcManager.java
src/jalview/workers/ComplementConsensusThread.java [new file with mode: 0644]
src/jalview/workers/ConsensusThread.java
test/jalview/analysis/AAFrequencyTest.java
test/jalview/analysis/CodingUtilsTest.java [new file with mode: 0644]
test/jalview/datamodel/AlignedCodonFrameTest.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/util/MappingUtilsTest.java

index 8dfda39..b206f67 100755 (executable)
  */
 package jalview.analysis;
 
+import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.SequenceI;
 import jalview.util.Format;
+import jalview.util.MappingUtils;
+import jalview.util.QuickSort;
 
+import java.util.Arrays;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Takes in a vector or array of sequences and column start and column end and
@@ -51,6 +57,8 @@ public class AAFrequency
 
   public static final String PROFILE = "P";
 
+  public static final String ENCODED_CHARS = "E";
+
   /*
    * Quick look-up of String value of char 'A' to 'Z'
    */
@@ -166,7 +174,8 @@ public class AAFrequency
       {
         for (v = 'A'; v <= 'Z'; v++)
         {
-          if (values[v] < 2 || values[v] < maxCount)
+          // TODO why ignore values[v] == 1?
+          if (values[v] < 1 /* 2 */|| values[v] < maxCount)
           {
             continue;
           }
@@ -188,6 +197,7 @@ public class AAFrequency
       }
       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 } });
@@ -232,12 +242,34 @@ public class AAFrequency
             nseq);
   }
 
+  /**
+   * 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.
+   * 
+   * @param consensus
+   *          the annotation row to add annotations to
+   * @param hconsensus
+   *          the source consensus data
+   * @param iStart
+   *          start column
+   * @param width
+   *          end column
+   * @param ignoreGapsInConsensusCalculation
+   *          if true, use the consensus calculated ignoring gaps
+   * @param includeAllConsSymbols
+   *          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)
   {
-    float tval, value;
     if (consensus == null || consensus.annotations == null
             || consensus.annotations.length < width)
     {
@@ -245,26 +277,9 @@ public class AAFrequency
       // initialised properly
       return;
     }
-    String fmtstr = "%3.1f";
-    int precision = 0;
-    while (nseq >= 10)
-    {
-      precision++;
-      nseq /= 10;
-    }
-    final Format fmt;
-    if (precision > 1)
-    {
-      // if (precision>2)
-      {
-        fmtstr = "%" + (2 + precision) + "." + (precision) + "f";
-      }
-      fmt = new Format(fmtstr);
-    }
-    else
-    {
-      fmt = null;
-    }
+
+    final Format fmt = getPercentageFormat(nseq);
+
     for (int i = iStart; i < width; i++)
     {
       Hashtable hci;
@@ -275,92 +290,127 @@ public class AAFrequency
         consensus.annotations[i] = null;
         continue;
       }
-      value = 0;
-      Float fv;
-      if (ignoreGapsInConsensusCalculation)
-      {
-        fv = (Float) hci.get(AAFrequency.PID_NOGAPS);
-      }
-      else
-      {
-        fv = (Float) hci.get(AAFrequency.PID_GAPS);
-      }
+      Float fv = (Float) hci
+              .get(ignoreGapsInConsensusCalculation ? PID_NOGAPS : PID_GAPS);
       if (fv == null)
       {
         consensus.annotations[i] = null;
         // data has changed below us .. give up and
         continue;
       }
-      value = fv.floatValue();
+      float value = fv.floatValue();
       String maxRes = hci.get(AAFrequency.MAXRESIDUE).toString();
-      String mouseOver = hci.get(AAFrequency.MAXRESIDUE) + " ";
+      StringBuilder mouseOver = new StringBuilder(64);
       if (maxRes.length() > 1)
       {
-        mouseOver = "[" + maxRes + "] ";
+        mouseOver.append("[").append(maxRes).append("] ");
         maxRes = "+";
       }
+      else
+      {
+        mouseOver.append(hci.get(AAFrequency.MAXRESIDUE) + " ");
+      }
       int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE);
+      int sequenceCount = profile[1][0];
+      int nonGappedCount = profile[1][1];
+      int normalisedBy = ignoreGapsInConsensusCalculation ? nonGappedCount
+              : sequenceCount;
       if (profile != null && includeAllConsSymbols)
       {
-        mouseOver = "";
+        mouseOver.setLength(0);
         if (alphabet != null)
         {
           for (int c = 0; c < alphabet.length; c++)
           {
-            tval = profile[0][alphabet[c]] * 100f
-                    / profile[1][ignoreGapsInConsensusCalculation ? 1 : 0];
-            mouseOver += ((c == 0) ? "" : "; ") + alphabet[c] + " "
-                    + ((fmt != null) ? fmt.form(tval) : ((int) tval)) + "%";
+            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
         {
-          Object[] ca = new Object[profile[0].length];
+          // 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] = new char[]
-            { (char) c };
+            ca[c] = (char) c;
+            // ca[c] = new char[]
+            // { (char) c };
             vl[c] = profile[0][c];
           }
-          ;
-          jalview.util.QuickSort.sort(vl, ca);
-          for (int p = 0, c = ca.length - 1; profile[0][((char[]) ca[c])[0]] > 0; c--)
+          QuickSort.sort(vl, ca);
+          for (int p = 0, c = ca.length - 1; profile[0][ca[c]] > 0; c--)
           {
-            if (((char[]) ca[c])[0] != '-')
+            final char residue = ca[c];
+            if (residue != '-')
             {
-              tval = profile[0][((char[]) ca[c])[0]]
-                      * 100f
-                      / profile[1][ignoreGapsInConsensusCalculation ? 1 : 0];
-              mouseOver += ((p == 0) ? "" : "; ") + ((char[]) ca[c])[0]
-                      + " "
-                      + ((fmt != null) ? fmt.form(tval) : ((int) tval))
-                      + "%";
+              float tval = profile[0][residue] * 100f / normalisedBy;
+              mouseOver
+                      .append((((p == 0) ? "" : "; ")))
+                      .append(residue)
+                      .append(" ")
+                      .append(((fmt != null) ? fmt.form(tval)
+                              : ((int) tval))).append("%");
               p++;
-
             }
           }
-
         }
       }
       else
       {
-        mouseOver += ((fmt != null) ? fmt.form(value) : ((int) value))
-                + "%";
+        mouseOver.append(
+                (((fmt != null) ? fmt.form(value) : ((int) value))))
+                .append("%");
       }
-      consensus.annotations[i] = new Annotation(maxRes, mouseOver, ' ',
+      consensus.annotations[i] = new Annotation(maxRes,
+              mouseOver.toString(), ' ',
               value);
     }
   }
 
   /**
-   * get the sorted profile for the given position of the consensus
+   * 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"
+   * 
+   * @param nseq
+   * @return
+   */
+  protected static Format getPercentageFormat(long nseq)
+  {
+    int scale = 0;
+    while (nseq >= 10)
+    {
+      scale++;
+      nseq /= 10;
+    }
+    return scale <= 1 ? null : new Format("%3." + (scale - 1) + "f");
+  }
+
+  /**
+   * Returns the sorted profile for the given consensus data. The returned array
+   * contains
+   * 
+   * <pre>
+   *    [profileType, numberOfValues, nonGapCount, charValue1, percentage1, charValue2, percentage2, ...]
+   * in descending order of percentage value
+   * </pre>
    * 
    * @param hconsensus
+   *          the data table 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,
-          boolean ignoreGapsInConsensusCalculation)
+          boolean ignoreGaps)
   {
     int[] rtnval = new int[64];
     int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE);
@@ -368,27 +418,247 @@ public class AAFrequency
     {
       return null;
     }
-    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] = new char[]
-      { (char) c };
+      ca[c] = (char) c;
       vl[c] = profile[0][c];
     }
-    jalview.util.QuickSort.sort(vl, ca);
-    rtnval[0] = 2;
-    rtnval[1] = 0;
-    for (int c = ca.length - 1; profile[0][ca[c][0]] > 0; c--)
+    QuickSort.sort(vl, ca);
+    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--)
     {
-      if (ca[c][0] != '-')
+      if (ca[c] != '-')
       {
-        rtnval[rtnval[0]++] = ca[c][0];
-        rtnval[rtnval[0]] = (int) (profile[0][ca[c][0]] * 100f / profile[1][ignoreGapsInConsensusCalculation ? 1
-                : 0]);
-        rtnval[1] += rtnval[rtnval[0]++];
+        rtnval[nextArrayPos++] = ca[c];
+        final int percentage = (int) (profile[0][ca[c]] * 100f / divisor);
+        rtnval[nextArrayPos++] = percentage;
+        totalPercentage += percentage;
+        distinctValuesCount++;
       }
     }
-    return rtnval;
+    rtnval[0] = distinctValuesCount;
+    rtnval[1] = totalPercentage;
+    int[] result = new int[rtnval.length + 1];
+    result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
+    System.arraycopy(rtnval, 0, result, 1, rtnval.length);
+
+    return result;
+  }
+
+  /**
+   * Extract a sorted extract of cDNA codon profile data. The returned array
+   * contains
+   * 
+   * <pre>
+   *    [profileType, numberOfValues, totalCount, charValue1, percentage1, charValue2, percentage2, ...]
+   * in descending order of percentage value, where the character values encode codon triplets
+   * </pre>
+   * 
+   * @param hashtable
+   * @return
+   */
+  public static int[] extractCdnaProfile(Hashtable hashtable, boolean ignoreGaps)
+  {
+    // this holds #seqs, #ungapped, and then codon count, indexed by encoded
+    // codon triplet
+    int[] codonCounts = (int[]) hashtable.get(PROFILE);
+    int[] sortedCounts = new int[codonCounts.length - 2];
+    System.arraycopy(codonCounts, 2, sortedCounts, 0,
+            codonCounts.length - 2);
+
+    int[] result = new int[3 + 2 * sortedCounts.length];
+    // first value is just the type of profile data
+    result[0] = AlignmentAnnotation.CDNA_PROFILE;
+
+    char[] codons = new char[sortedCounts.length];
+    for (int i = 0; i < codons.length; i++)
+    {
+      codons[i] = (char) i;
+    }
+    QuickSort.sort(sortedCounts, codons);
+    int totalPercentage = 0;
+    int distinctValuesCount = 0;
+    int j = 3;
+    int divisor = ignoreGaps ? codonCounts[1] : codonCounts[0];
+    for (int i = codons.length - 1; i >= 0; i--)
+    {
+      final int codonCount = sortedCounts[i];
+      if (codonCount == 0)
+      {
+        break; // nothing else of interest here
+      }
+      distinctValuesCount++;
+      result[j++] = codons[i];
+      final int percentage = codonCount * 100 / divisor;
+      result[j++] = percentage;
+      totalPercentage += percentage;
+    }
+    result[2] = totalPercentage;
+
+    /*
+     * Just return the non-zero values
+     */
+    // todo next value is redundant if we limit the array to non-zero counts
+    result[1] = distinctValuesCount;
+    return Arrays.copyOfRange(result, 0, j);
+  }
+    
+  /**
+   * Compute a consensus for the cDNA coding for a protein alignment.
+   * 
+   * @param alignment
+   *          the protein alignment (which should hold mappings to cDNA
+   *          sequences)
+   * @param hconsensus
+   *          the consensus data stores to be populated (one per column)
+   */
+  public static void calculateCdna(AlignmentI alignment,
+          Hashtable[] hconsensus)
+  {
+    final char gapCharacter = alignment.getGapCharacter();
+    Set<AlignedCodonFrame> mappings = alignment.getCodonFrames();
+    if (mappings == null || mappings.isEmpty())
+    {
+      return;
+    }
+
+    int cols = alignment.getWidth();
+    for (int col = 0; col < cols; col++)
+    {
+      // todo would prefer a Java bean for consensus data
+      Hashtable<String, int[]> columnHash = new Hashtable<String, int[]>();
+      // #seqs, #ungapped seqs, counts indexed by (codon encoded + 1)
+      int[] codonCounts = new int[66];
+      codonCounts[0] = alignment.getSequences().size();
+      int ungappedCount = 0;
+      for (SequenceI seq : alignment.getSequences())
+      {
+        if (seq.getCharAt(col) == gapCharacter)
+        {
+          continue;
+        }
+        char[] codon = MappingUtils.findCodonFor(seq, col, mappings);
+        int codonEncoded = CodingUtils.encodeCodon(codon);
+        if (codonEncoded >= 0)
+        {
+          codonCounts[codonEncoded + 2]++;
+          ungappedCount++;
+        }
+      }
+      codonCounts[1] = ungappedCount;
+      // todo: sort values here, save counts and codons?
+      columnHash.put(PROFILE, codonCounts);
+      hconsensus[col] = columnHash;
+    }
+  }
+
+  /**
+   * Derive displayable cDNA consensus annotation from computed consensus data.
+   * 
+   * @param consensusAnnotation
+   *          the annotation row to be populated for display
+   * @param consensusData
+   *          the computed consensus data
+   * @param showProfileLogo
+   *          if true show all symbols present at each position, else only the
+   *          modal value
+   * @param nseqs
+   *          the number of sequences in the alignment
+   */
+  public static void completeCdnaConsensus(
+          AlignmentAnnotation consensusAnnotation,
+          Hashtable[] consensusData, boolean showProfileLogo, int nseqs)
+  {
+    if (consensusAnnotation == null
+            || consensusAnnotation.annotations == null
+            || consensusAnnotation.annotations.length < consensusData.length)
+    {
+      // called with a bad alignment annotation row - wait for it to be
+      // initialised properly
+      return;
+    }
+
+    for (int col = 0; col < consensusData.length; col++)
+    {
+      Hashtable hci = consensusData[col];
+      if (hci == null)
+      {
+        // gapped protein column?
+        continue;
+      }
+      // array holds #seqs, #ungapped, then codon counts indexed by codon
+      final int[] codonCounts = (int[]) hci.get(PROFILE);
+      int totalCount = 0;
+      StringBuilder mouseOver = new StringBuilder(32);
+
+      /*
+       * First pass - get total count and find the highest
+       */
+      final char[] codons = new char[codonCounts.length - 2];
+      for (int j = 2; j < codonCounts.length; j++)
+      {
+        final int codonCount = codonCounts[j];
+        codons[j - 2] = (char) (j - 2);
+        totalCount += codonCount;
+      }
+
+      /*
+       * Sort array of encoded codons by count ascending - so the modal value
+       * goes to the end; start by copying the count (dropping the first value)
+       */
+      int[] sortedCodonCounts = new int[codonCounts.length - 2];
+      System.arraycopy(codonCounts, 2, sortedCodonCounts, 0,
+              codonCounts.length - 2);
+      QuickSort.sort(sortedCodonCounts, codons);
+
+      int modalCodonEncoded = codons[codons.length - 1];
+      int modalCodonCount = sortedCodonCounts[codons.length - 1];
+      String modalCodon = String.valueOf(CodingUtils
+              .decodeCodon(modalCodonEncoded));
+      if (sortedCodonCounts.length > 1
+              && sortedCodonCounts[codons.length - 2] == modalCodonEncoded)
+      {
+        modalCodon = "+";
+      }
+      float pid = sortedCodonCounts[sortedCodonCounts.length - 1] * 100
+              / (float) totalCount;
+
+      /*
+       * todo ? Replace consensus hashtable with sorted arrays of codons and
+       * counts (non-zero only). Include total count in count array [0].
+       */
+
+      /*
+       * Scan sorted array backwards for most frequent values first.
+       */
+      for (int j = codons.length - 1; j >= 0; j--)
+      {
+        int codonCount = sortedCodonCounts[j];
+        if (codonCount == 0)
+        {
+          break;
+        }
+        int codonEncoded = codons[j];
+        final int pct = codonCount * 100 / totalCount;
+        String codon = String
+                .valueOf(CodingUtils.decodeCodon(codonEncoded));
+        Format fmt = getPercentageFormat(nseqs);
+        String formatted = fmt == null ? Integer.toString(pct) : fmt
+                .form(pct);
+        if (showProfileLogo || codonCount == modalCodonCount)
+        {
+          mouseOver.append(codon).append(": ").append(formatted)
+                  .append("% ");
+        }
+      }
+
+      consensusAnnotation.annotations[col] = new Annotation(modalCodon,
+              mouseOver.toString(), ' ', pid);
+    }
   }
 }
diff --git a/src/jalview/analysis/CodingUtils.java b/src/jalview/analysis/CodingUtils.java
new file mode 100644 (file)
index 0000000..a434465
--- /dev/null
@@ -0,0 +1,119 @@
+package jalview.analysis;
+
+/**
+ * A utility class to provide encoding/decoding schemes for data.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class CodingUtils
+{
+
+  /*
+   * Number of bits used when encoding codon characters. 2 is enough for ACGT.
+   * To accommodate more (e.g. ambiguity codes), simply increase this number
+   * (and adjust unit tests to match).
+   */
+  private static final int CODON_ENCODING_BITSHIFT = 2;
+
+  /**
+   * Encode a codon from e.g. ['A', 'G', 'C'] to a number in the range 0 - 63.
+   * Converts lower to upper case, U to T, then assembles a binary value by
+   * encoding A/C/G/T as 00/01/10/11 respectively and shifting.
+   * 
+   * @param codon
+   * @return the encoded codon, or a negative number if unexpected characters
+   *         found
+   */
+  public static int encodeCodon(char[] codon)
+  {
+    if (codon == null)
+    {
+      return -1;
+    }
+    return encodeCodon(codon[2])
+            + (encodeCodon(codon[1]) << CODON_ENCODING_BITSHIFT)
+            + (encodeCodon(codon[0]) << (2 * CODON_ENCODING_BITSHIFT));
+  }
+
+  /**
+   * Encodes aA/cC/gG/tTuU as 0/1/2/3 respectively. Returns Integer.MIN_VALUE (a
+   * large negative value) for any other character.
+   * 
+   * @param c
+   * @return
+   */
+  public static int encodeCodon(char c)
+  {
+    int result = Integer.MIN_VALUE;
+    switch (c)
+    {
+    case 'A':
+    case 'a':
+      result = 0;
+      break;
+    case 'C':
+    case 'c':
+      result = 1;
+      break;
+    case 'G':
+    case 'g':
+      result = 2;
+      break;
+    case 'T':
+    case 't':
+    case 'U':
+    case 'u':
+      result = 3;
+      break;
+    }
+    return result;
+  }
+
+  /**
+   * Converts a binary encoded codon into an ['A', 'C', 'G'] (or 'T') triplet.
+   * 
+   * The two low-order bits encode for A/C/G/T as 0/1/2/3, etc.
+   * 
+   * @param encoded
+   * @return
+   */
+  public static char[] decodeCodon(int encoded)
+  {
+    char[] result = new char[3];
+    result[2] = decodeNucleotide(encoded & 3);
+    encoded = encoded >>> CODON_ENCODING_BITSHIFT;
+    result[1] = decodeNucleotide(encoded & 3);
+    encoded = encoded >>> CODON_ENCODING_BITSHIFT;
+    result[0] = decodeNucleotide(encoded & 3);
+    return result;
+  }
+
+  /**
+   * Convert value 0/1/2/3 to 'A'/'C'/'G'/'T'
+   * 
+   * @param i
+   * @return
+   */
+  public static char decodeNucleotide(int i)
+  {
+    char result = '0';
+    switch (i)
+    {
+    case 0:
+      result = 'A';
+      break;
+    case 1:
+      result = 'C';
+      break;
+    case 2:
+      result = 'G';
+      break;
+    case 3:
+      result = 'T';
+      break;
+    }
+    return result;
+  }
+
+}
index dc0212e..cb78b04 100644 (file)
  */
 package jalview.analysis;
 
-import java.util.*;
-
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.util.Format;
-import jalview.datamodel.*;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
 
 /**
  * Takes in a vector or array of sequences and column start and column end and
@@ -36,6 +40,8 @@ import jalview.datamodel.*;
  */
 public class StructureFrequency
 {
+  public static final int STRUCTURE_PROFILE_LENGTH = 74;
+
   // No need to store 1000s of strings which are not
   // visible to the user.
   public static final String MAXCOUNT = "C";
@@ -187,6 +193,7 @@ public class StructureFrequency
       // UPDATE this for new values
       if (profile)
       {
+        // TODO 1-dim array with jsize in [0], nongapped in [1]; or Pojo
         residueHash.put(PROFILE, new int[][]
         { values, new int[]
         { jSize, (jSize - values['-']) } });
@@ -463,16 +470,19 @@ public class StructureFrequency
   public static int[] extractProfile(Hashtable hconsensus,
           boolean ignoreGapsInConsensusCalculation)
   {
-    int[] rtnval = new int[74]; // 2*(5*5)+2
+    int[] rtnval = new int[STRUCTURE_PROFILE_LENGTH]; // 2*(5*5)+2
     int[][] profile = (int[][]) hconsensus.get(StructureFrequency.PROFILE);
     int[][] pairs = (int[][]) hconsensus
             .get(StructureFrequency.PAIRPROFILE);
 
     if (profile == null)
+    {
       return null;
+    }
 
     // TODO fix the object length, also do it in completeConsensus
-    Object[] ca = new Object[625];
+    // Object[] ca = new Object[625];
+    int[][] ca = new int[625][];
     float[] vl = new float[625];
     int x = 0;
     for (int c = 65; c < 90; c++)
@@ -487,21 +497,28 @@ public class StructureFrequency
     }
     jalview.util.QuickSort.sort(vl, ca);
 
-    rtnval[0] = 2;
+    int valuesCount = 0;
     rtnval[1] = 0;
+    int offset = 2;
     for (int c = 624; c > 0; c--)
     {
       if (vl[c] > 0)
       {
-        rtnval[rtnval[0]++] = ((int[]) ca[c])[0];
-        rtnval[rtnval[0]++] = ((int[]) ca[c])[1];
-        rtnval[rtnval[0]] = (int) (vl[c] * 100f / profile[1][ignoreGapsInConsensusCalculation ? 1
+        rtnval[offset++] = ca[c][0];
+        rtnval[offset++] = ca[c][1];
+        rtnval[offset] = (int) (vl[c] * 100f / profile[1][ignoreGapsInConsensusCalculation ? 1
                 : 0]);
-        rtnval[1] += rtnval[rtnval[0]++];
+        rtnval[1] += rtnval[offset++];
+        valuesCount++;
       }
     }
+    rtnval[0] = valuesCount;
 
-    return rtnval;
+    // insert profile type code in position 0
+    int[] result = new int[rtnval.length + 1];
+    result[0] = AlignmentAnnotation.STRUCTURE_PROFILE;
+    System.arraycopy(rtnval, 0, result, 1, rtnval.length);
+    return result;
   }
 
   public static void main(String args[])
index 037f19e..374e0be 100644 (file)
@@ -75,6 +75,13 @@ public interface AlignViewportI extends ViewStyleI
 
   Hashtable[] getSequenceConsensusHash();
 
+  /**
+   * Get consensus data table for the cDNA complement of this alignment (if any)
+   * 
+   * @return
+   */
+  Hashtable[] getComplementConsensusHash();
+
   Hashtable[] getRnaStructureConsensusHash();
 
   boolean isIgnoreGapsConsensus();
@@ -93,6 +100,13 @@ public interface AlignViewportI extends ViewStyleI
   AlignmentAnnotation getAlignmentConsensusAnnotation();
 
   /**
+   * get the container for cDNA complement consensus annotation
+   * 
+   * @return
+   */
+  AlignmentAnnotation getComplementConsensusAnnotation();
+
+  /**
    * Test to see if viewport is still open and active
    * 
    * @return true indicates that all references to viewport should be dropped
@@ -120,6 +134,13 @@ public interface AlignViewportI extends ViewStyleI
   void setSequenceConsensusHash(Hashtable[] hconsensus);
 
   /**
+   * Set the cDNA complement consensus for the viewport
+   * 
+   * @param hconsensus
+   */
+  void setComplementConsensusHash(Hashtable[] hconsensus);
+
+  /**
    * 
    * @return the alignment annotatino row for the structure consensus
    *         calculation
index cbddf1c..b174c31 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.datamodel;
 
 import jalview.util.MapList;
+import jalview.util.MappingUtils;
 
 /**
  * Stores mapping between the columns of a protein alignment and a DNA alignment
@@ -376,4 +377,50 @@ public class AlignedCodonFrame
     }
     return null;
   }
+
+  /**
+   * Returns the DNA codon for the given position (base 1) in a mapped protein
+   * sequence, or null if no mapping is found.
+   * 
+   * @param protein
+   *          the peptide dataset sequence
+   * @param aaPos
+   *          residue position (base 1) in the peptide sequence
+   * @return
+   */
+  public char[] getMappedCodon(SequenceI protein, int aaPos)
+  {
+    if (dnaToProt == null)
+    {
+      return null;
+    }
+    MapList ml = null;
+    char[] dnaSeq = null;
+    for (int i = 0; i < dnaToProt.length; i++)
+    {
+      if (dnaToProt[i].to == protein)
+      {
+        ml = getdnaToProt()[i];
+        dnaSeq = dnaSeqs[i].getSequence();
+        break;
+      }
+    }
+    if (ml == null)
+    {
+      return null;
+    }
+    int[] codonPos = ml.locateInFrom(aaPos, aaPos);
+    if (codonPos == null)
+    {
+      return null;
+    }
+
+    /*
+     * Read off the mapped nucleotides (converting to position base 0)
+     */
+    codonPos = MappingUtils.flattenRanges(codonPos);
+    return new char[]
+    { dnaSeq[codonPos[0] - 1], dnaSeq[codonPos[1] - 1],
+        dnaSeq[codonPos[2] - 1] };
+  }
 }
index 0d99155..f8782e7 100755 (executable)
@@ -40,6 +40,15 @@ import java.util.Map.Entry;
  */
 public class AlignmentAnnotation
 {
+  /*
+   * Identifers for different types of profile data
+   */
+  public static final int SEQUENCE_PROFILE = 0;
+
+  public static final int STRUCTURE_PROFILE = 1;
+
+  public static final int CDNA_PROFILE = 2;
+
   /**
    * If true, this annotations is calculated every edit, eg consensus, quality
    * or conservation graphs
index e7a4735..2f43e82 100644 (file)
@@ -4796,8 +4796,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             boolean asSplitFrame = true;
             if (asSplitFrame)
             {
-              AlignFrame copyThis = new AlignFrame(
-                      AlignFrame.this.viewport.getAlignment(),
+              final Alignment copyAlignment = new Alignment(
+                      AlignFrame.this.viewport.getAlignment());
+              AlignFrame copyThis = new AlignFrame(copyAlignment,
                       AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
               copyThis.setTitle(AlignFrame.this.getTitle());
               // SplitFrame with dna above, protein below
index 9cbded9..d594051 100644 (file)
@@ -575,5 +575,25 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
     }
     return null;
   }
+
+  /**
+   * Set the 'other half' to hidden / revealed.
+   */
+  @Override
+  public void setComplementVisible(Object alignFrame, boolean show)
+  {
+    /*
+     * Hiding the AlignPanel suppresses unnecessary repaints
+     */
+    if (alignFrame == getTopFrame())
+    {
+      ((AlignFrame) getBottomFrame()).alignPanel.setVisible(show);
+    }
+    else if (alignFrame == getBottomFrame())
+    {
+      ((AlignFrame) getTopFrame()).alignPanel.setVisible(show);
+    }
+    super.setComplementVisible(alignFrame, show);
+  }
 }
 
index 448bcd2..2e40416 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.renderer;
 
 import jalview.analysis.AAFrequency;
+import jalview.analysis.CodingUtils;
 import jalview.analysis.StructureFrequency;
 import jalview.api.AlignViewportI;
 import jalview.datamodel.AlignmentAnnotation;
@@ -142,6 +143,8 @@ public class AnnotationRenderer
 
   private Hashtable[] hconsensus;
 
+  private Hashtable[] complementConsensus;
+
   private Hashtable[] hStrucConsensus;
 
   private boolean av_ignoreGapsConsensus;
@@ -298,24 +301,37 @@ public class AnnotationRenderer
       profcolour = av.getAlignment().isNucleotide() ? new jalview.schemes.NucleotideColourScheme()
               : new jalview.schemes.ZappoColourScheme();
     }
-    boolean rna = av.getAlignment().isNucleotide();
     columnSelection = av.getColumnSelection();
-    hconsensus = av.getSequenceConsensusHash();// hconsensus;
-    hStrucConsensus = av.getRnaStructureConsensusHash(); // hStrucConsensus;
+    hconsensus = av.getSequenceConsensusHash();
+    complementConsensus = av.getComplementConsensusHash();
+    hStrucConsensus = av.getRnaStructureConsensusHash();
     av_ignoreGapsConsensus = av.isIgnoreGapsConsensus();
   }
 
+  /**
+   * Returns profile data; the first element is the profile type, the second is
+   * the number of distinct values, the third the total count, and the remainder
+   * depend on the profile type.
+   * 
+   * @param aa
+   * @param column
+   * @return
+   */
   public int[] getProfileFor(AlignmentAnnotation aa, int column)
   {
     // TODO : consider refactoring the global alignment calculation
     // properties/rendering attributes as a global 'alignment group' which holds
     // all vis settings for the alignment as a whole rather than a subset
     //
-    if (aa.autoCalculated && aa.label.startsWith("Consensus"))
+    if (aa.autoCalculated
+            && (aa.label.startsWith("Consensus") || aa.label
+                    .startsWith("cDNA Consensus")))
     {
+      boolean forComplement = aa.label.startsWith("cDNA Consensus");
       if (aa.groupRef != null && aa.groupRef.consensusData != null
               && aa.groupRef.isShowSequenceLogo())
       {
+        // TODO? group consensus for cDNA complement
         return AAFrequency.extractProfile(
                 aa.groupRef.consensusData[column],
                 aa.groupRef.getIgnoreGapsConsensus());
@@ -324,8 +340,16 @@ public class AnnotationRenderer
       // be stored
       if (aa.groupRef == null && aa.sequenceRef == null)
       {
-        return AAFrequency.extractProfile(hconsensus[column],
-                av_ignoreGapsConsensus);
+        if (forComplement)
+        {
+          return AAFrequency.extractCdnaProfile(
+                  complementConsensus[column], av_ignoreGapsConsensus);
+        }
+        else
+        {
+          return AAFrequency.extractProfile(hconsensus[column],
+                  av_ignoreGapsConsensus);
+        }
       }
     }
     else
@@ -400,11 +424,15 @@ public class AnnotationRenderer
     boolean validRes = false;
     boolean validEnd = false;
     boolean labelAllCols = false;
-    boolean centreColLabels, centreColLabelsDef = av.isCentreColumnLabels();
+    boolean centreColLabels;
+    boolean centreColLabelsDef = av.isCentreColumnLabels();
     boolean scaleColLabel = false;
-    AlignmentAnnotation consensusAnnot = av
-            .getAlignmentConsensusAnnotation(), structConsensusAnnot = av
+    final AlignmentAnnotation consensusAnnot = av
+            .getAlignmentConsensusAnnotation();
+    final AlignmentAnnotation structConsensusAnnot = av
             .getAlignmentStrucConsensusAnnotation();
+    final AlignmentAnnotation complementConsensusAnnot = av
+            .getComplementConsensusAnnotation();
     boolean renderHistogram = true, renderProfile = true, normaliseProfile = false, isRNA = rna;
 
     BitSet graphGroupDrawn = new BitSet();
@@ -431,7 +459,8 @@ public class AnnotationRenderer
           renderProfile = row.groupRef.isShowSequenceLogo();
           normaliseProfile = row.groupRef.isNormaliseSequenceLogo();
         }
-        else if (row == consensusAnnot || row == structConsensusAnnot)
+        else if (row == consensusAnnot || row == structConsensusAnnot
+                || row == complementConsensusAnnot)
         {
           renderHistogram = av_renderHistogram;
           renderProfile = av_renderProfile;
@@ -555,6 +584,8 @@ public class AnnotationRenderer
           {
             validRes = true;
           }
+          final String displayChar = validRes ? row_annotations[column].displayCharacter
+                  : null;
           if (x > -1)
           {
             if (activeRow == i)
@@ -584,21 +615,19 @@ public class AnnotationRenderer
               g.setColor(Color.orange.darker());
               g.fillRect(x * charWidth, y, charWidth, charHeight);
             }
-            if (validCharWidth
-                    && validRes
-                    && row_annotations[column].displayCharacter != null
-                    && (row_annotations[column].displayCharacter.length() > 0))
+            if (validCharWidth && validRes && displayChar != null
+                    && (displayChar.length() > 0))
             {
 
-              if (centreColLabels || scaleColLabel)
+              fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
+                      displayChar.length());
+              if (/* centreColLabels || */scaleColLabel)
               {
-                fmWidth = fm.charsWidth(
-                        row_annotations[column].displayCharacter
-                                .toCharArray(), 0,
-                        row_annotations[column].displayCharacter.length());
-
-                if (scaleColLabel)
-                {
+                // fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
+                // displayChar.length());
+                //
+                // if (scaleColLabel)
+                // {
                   // justify the label and scale to fit in column
                   if (fmWidth > charWidth)
                   {
@@ -610,14 +639,13 @@ public class AnnotationRenderer
                     // and update the label's width to reflect the scaling.
                     fmWidth = charWidth;
                   }
-                }
-              }
-              else
-              {
-                fmWidth = fm
-                        .charWidth(row_annotations[column].displayCharacter
-                                .charAt(0));
+                // }
               }
+              // TODO is it ok to use width of / show all characters here?
+              // else
+              // {
+              // fmWidth = fm.charWidth(displayChar.charAt(0));
+              // }
               charOffset = (int) ((charWidth - fmWidth) / 2f);
 
               if (row_annotations[column].colour == null)
@@ -631,17 +659,17 @@ public class AnnotationRenderer
 
               if (column == 0 || row.graph > 0)
               {
-                g.drawString(row_annotations[column].displayCharacter,
-                        (x * charWidth) + charOffset, y + iconOffset);
+                g.drawString(displayChar, (x * charWidth) + charOffset, y
+                        + iconOffset);
               }
               else if (row_annotations[column - 1] == null
                       || (labelAllCols
-                              || !row_annotations[column].displayCharacter
-                                      .equals(row_annotations[column - 1].displayCharacter) || (row_annotations[column].displayCharacter
+                              || !displayChar
+                                      .equals(row_annotations[column - 1].displayCharacter) || (displayChar
                               .length() < 2 && row_annotations[column].secondaryStructure == ' ')))
               {
-                g.drawString(row_annotations[column].displayCharacter, x
-                        * charWidth + charOffset, y + iconOffset);
+                g.drawString(displayChar, x * charWidth + charOffset, y
+                        + iconOffset);
               }
               g.setFont(ofont);
             }
@@ -654,7 +682,7 @@ public class AnnotationRenderer
             if (ss == '(')
             {
               // distinguish between forward/backward base-pairing
-              if (row_annotations[column].displayCharacter.indexOf(')') > -1)
+              if (displayChar.indexOf(')') > -1)
               {
 
                 ss = ')';
@@ -663,7 +691,7 @@ public class AnnotationRenderer
             }
             if (ss == '[')
             {
-              if ((row_annotations[column].displayCharacter.indexOf(']') > -1))
+              if ((displayChar.indexOf(']') > -1))
               {
                 ss = ']';
 
@@ -672,7 +700,7 @@ public class AnnotationRenderer
             if (ss == '{')
             {
               // distinguish between forward/backward base-pairing
-              if (row_annotations[column].displayCharacter.indexOf('}') > -1)
+              if (displayChar.indexOf('}') > -1)
               {
                 ss = '}';
 
@@ -681,7 +709,7 @@ public class AnnotationRenderer
             if (ss == '<')
             {
               // distinguish between forward/backward base-pairing
-              if (row_annotations[column].displayCharacter.indexOf('<') > -1)
+              if (displayChar.indexOf('<') > -1)
               {
                 ss = '>';
 
@@ -690,7 +718,7 @@ public class AnnotationRenderer
             if (ss >= 65)
             {
               // distinguish between forward/backward base-pairing
-              if (row_annotations[column].displayCharacter.indexOf(ss + 32) > -1)
+              if (displayChar.indexOf(ss + 32) > -1)
               {
 
                 ss = (char) (ss + 32);
@@ -1302,9 +1330,16 @@ public class AnnotationRenderer
       if (renderProfile)
       {
 
+        /*
+         * {profile type, #values, total count, char1, pct1, char2, pct2...}
+         */
         int profl[] = getProfileFor(_aa, column);
+
+        boolean isStructureProfile = profl[0] == AlignmentAnnotation.STRUCTURE_PROFILE;
+        boolean isCdnaProfile = profl[0] == AlignmentAnnotation.CDNA_PROFILE;
+
         // just try to draw the logo if profl is not null
-        if (profl != null && profl[1] != 0)
+        if (profl != null && profl[2] != 0)
         {
           float ht = normaliseProfile ? y - _aa.graphHeight : y1;
           double htn = normaliseProfile ? _aa.graphHeight : (y2 - y1);// aa.graphHeight;
@@ -1314,55 +1349,80 @@ public class AnnotationRenderer
           char[] dc;
 
           /**
-           * profl.length == 74 indicates that the profile of a secondary
-           * structure conservation row was accesed. Therefore dc gets length 2,
-           * to have space for a basepair instead of just a single nucleotide
+           * Render a single base for a sequence profile, a base pair for
+           * structure profile, and a triplet for a cdna profile
            */
-          if (profl.length == 74)
-          {
-            dc = new char[2];
-          }
-          else
-          {
-            dc = new char[1];
-          }
+          dc = new char[isStructureProfile ? 2 : (isCdnaProfile ? 3 : 1)];
+
           LineMetrics lm = g.getFontMetrics(ofont).getLineMetrics("Q", g);
-          double scale = 1f / (normaliseProfile ? profl[1] : 100f);
+          double scale = 1f / (normaliseProfile ? profl[2] : 100f);
           float ofontHeight = 1f / lm.getAscent();// magnify to fill box
           double scl = 0.0;
-          for (int c = 2; c < profl[0];)
-          {
-            dc[0] = (char) profl[c++];
 
-            if (_aa.label.startsWith("StrucConsensus"))
+          /*
+           * Traverse the character(s)/percentage data in the array
+           */
+          int c = 3;
+          int valuesProcessed = 0;
+          // profl[1] is the number of values in the profile
+          while (valuesProcessed < profl[1])
+          {
+            if (isStructureProfile)
             {
+              // todo can we encode a structure pair as an int, like codons?
+              dc[0] = (char) profl[c++];
               dc[1] = (char) profl[c++];
             }
+            else if (isCdnaProfile)
+            {
+              dc = CodingUtils.decodeCodon(profl[c++]);
+            }
+            else
+            {
+              dc[0] = (char) profl[c++];
+            }
 
             wdth = charWidth;
             wdth /= fm.charsWidth(dc, 0, dc.length);
 
             ht += scl;
+            // next profl[] position is profile % for the character(s)
+            scl = htn * scale * profl[c++];
+            lm = ofont.getLineMetrics(dc, 0, 1, g.getFontMetrics()
+                    .getFontRenderContext());
+            g.setFont(ofont.deriveFont(AffineTransform.getScaleInstance(
+                    wdth, scl / lm.getAscent())));
+            lm = g.getFontMetrics().getLineMetrics(dc, 0, 1, g);
+
+            // Debug - render boxes around characters
+            // g.setColor(Color.red);
+            // g.drawRect(x*av.charWidth, (int)ht, av.charWidth,
+            // (int)(scl));
+            // g.setColor(profcolour.findColour(dc[0]).darker());
+
+            /*
+             * Set character colour as per alignment colour scheme; use the
+             * codon translation if a cDNA profile
+             */
+            Color colour = null;
+            if (isCdnaProfile)
             {
-              scl = htn * scale * profl[c++];
-              lm = ofont.getLineMetrics(dc, 0, 1, g.getFontMetrics()
-                      .getFontRenderContext());
-              g.setFont(ofont.deriveFont(AffineTransform.getScaleInstance(
-                      wdth, scl / lm.getAscent())));
-              lm = g.getFontMetrics().getLineMetrics(dc, 0, 1, g);
-
-              // Debug - render boxes around characters
-              // g.setColor(Color.red);
-              // g.drawRect(x*av.charWidth, (int)ht, av.charWidth,
-              // (int)(scl));
-              // g.setColor(profcolour.findColour(dc[0]).darker());
-              g.setColor(profcolour.findColour(dc[0], column, null));
-
-              hght = (ht + (scl - lm.getDescent() - lm.getBaselineOffsets()[lm
-                      .getBaselineIndex()]));
-
-              g.drawChars(dc, 0, dc.length, x * charWidth, (int) hght);
+              final String codonTranslation = ResidueProperties
+                      .codonTranslate(new String(dc));
+              colour = profcolour.findColour(codonTranslation.charAt(0),
+                      column, null);
             }
+            else
+            {
+              colour = profcolour.findColour(dc[0], column, null);
+            }
+            g.setColor(colour == Color.white ? Color.lightGray : colour);
+
+            hght = (ht + (scl - lm.getDescent() - lm.getBaselineOffsets()[lm
+                    .getBaselineIndex()]));
+
+            g.drawChars(dc, 0, dc.length, x * charWidth, (int) hght);
+            valuesProcessed++;
           }
           g.setFont(ofont);
         }
index a7b311b..f1dd359 100755 (executable)
@@ -54,6 +54,8 @@ public class Format
 
   private char fmt; // one of cdeEfgGiosxXos
 
+  private final String formatString;
+
   /**
    * Creates a new Format object.
    * 
@@ -62,6 +64,7 @@ public class Format
    */
   public Format(String s)
   {
+    formatString = s;
     width = 0;
     precision = -1;
     pre = "";
@@ -938,4 +941,10 @@ public class Format
 
     return f + p.substring(p.length() - 3, p.length());
   }
+
+  @Override
+  public String toString()
+  {
+    return formatString;
+  }
 }
index eebc539..4cfb49e 100644 (file)
@@ -486,4 +486,60 @@ public final class MappingUtils
     return mappedColumns;
   }
 
+  /**
+   * Returns the mapped codon for a given aligned sequence column position (base
+   * 0).
+   * 
+   * @param seq
+   *          an aligned peptide sequence
+   * @param col
+   *          an aligned column position (base 0)
+   * @param mappings
+   *          a set of codon mappings
+   * @return the bases of the mapped codon in the cDNA dataset sequence, or null
+   *         if not found
+   */
+  public static char[] findCodonFor(SequenceI seq, int col,
+          Set<AlignedCodonFrame> mappings)
+  {
+    int dsPos = seq.findPosition(col);
+    for (AlignedCodonFrame mapping : mappings)
+    {
+      if (mapping.involvesSequence(seq))
+      {
+        return mapping.getMappedCodon(seq.getDatasetSequence(), dsPos);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Converts a series of [start, end] ranges into an array of individual
+   * positions.
+   * 
+   * @param ranges
+   * @return
+   */
+  public static int[] flattenRanges(int[] ranges)
+  {
+    /*
+     * Count how many positions altogether
+     */
+    int count = 0;
+    for (int i = 0; i < ranges.length - 1; i += 2)
+    {
+      count += ranges[i + 1] - ranges[i] + 1;
+    }
+
+    int[] result = new int[count];
+    int k = 0;
+    for (int i = 0; i < ranges.length - 1; i += 2)
+    {
+      for (int j = ranges[i]; j <= ranges[i + 1]; j++)
+      {
+        result[k++] = j;
+      }
+    }
+    return result;
+  }
 }
index d504093..df08682 100644 (file)
@@ -27,6 +27,7 @@ import jalview.api.AlignmentViewPanel;
 import jalview.api.FeaturesDisplayedI;
 import jalview.api.ViewStyleI;
 import jalview.commands.CommandI;
+import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
@@ -46,6 +47,7 @@ import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
 import jalview.viewmodel.styles.ViewStyle;
 import jalview.workers.AlignCalcManager;
+import jalview.workers.ComplementConsensusThread;
 import jalview.workers.ConsensusThread;
 import jalview.workers.StrucConsensusThread;
 
@@ -58,6 +60,7 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * base class holding visualization and analysis attributes and common logic for
@@ -642,6 +645,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
   protected AlignmentAnnotation consensus;
 
+  protected AlignmentAnnotation complementConsensus;
+
   protected AlignmentAnnotation strucConsensus;
 
   protected AlignmentAnnotation conservation;
@@ -658,6 +663,11 @@ public abstract class AlignmentViewport implements AlignViewportI,
   protected Hashtable[] hconsensus = null;
 
   /**
+   * results of cDNA complement consensus visible portion of view
+   */
+  protected Hashtable[] hcomplementConsensus = null;
+
+  /**
    * results of secondary structure base pair consensus for visible portion of
    * view
    */
@@ -687,7 +697,12 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public void setSequenceConsensusHash(Hashtable[] hconsensus)
   {
     this.hconsensus = hconsensus;
+  }
 
+  @Override
+  public void setComplementConsensusHash(Hashtable[] hconsensus)
+  {
+    this.hcomplementConsensus = hconsensus;
   }
 
   @Override
@@ -697,6 +712,12 @@ public abstract class AlignmentViewport implements AlignViewportI,
   }
 
   @Override
+  public Hashtable[] getComplementConsensusHash()
+  {
+    return hcomplementConsensus;
+  }
+
+  @Override
   public Hashtable[] getRnaStructureConsensusHash()
   {
     return hStrucConsensus;
@@ -728,6 +749,12 @@ public abstract class AlignmentViewport implements AlignViewportI,
   }
 
   @Override
+  public AlignmentAnnotation getComplementConsensusAnnotation()
+  {
+    return complementConsensus;
+  }
+
+  @Override
   public AlignmentAnnotation getAlignmentStrucConsensusAnnotation()
   {
     return strucConsensus;
@@ -768,6 +795,20 @@ public abstract class AlignmentViewport implements AlignViewportI,
     {
       calculator.registerWorker(new ConsensusThread(this, ap));
     }
+
+    /*
+     * A separate thread to compute cDNA consensus for a protein alignment
+     */
+    final AlignmentI al = this.getAlignment();
+    if (!al.isNucleotide() && al.getCodonFrames() != null
+            && !al.getCodonFrames().isEmpty())
+    {
+      if (calculator
+              .getRegisteredWorkersOfClass(ComplementConsensusThread.class) == null)
+      {
+        calculator.registerWorker(new ComplementConsensusThread(this, ap));
+      }
+    }
   }
 
   // --------START Structure Conservation
@@ -872,6 +913,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
       // annotation update method from alignframe to viewport
       this.showSequenceLogo = showSequenceLogo;
       calculator.updateAnnotationFor(ConsensusThread.class);
+      calculator.updateAnnotationFor(ComplementConsensusThread.class);
       calculator.updateAnnotationFor(StrucConsensusThread.class);
     }
     this.showSequenceLogo = showSequenceLogo;
@@ -1672,21 +1714,33 @@ public abstract class AlignmentViewport implements AlignViewportI,
       {
         initRNAStructure();
       }
-      initConsensus();
+      consensus = new AlignmentAnnotation("Consensus", "PID",
+              new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
+      initConsensus(consensus);
+
+      if (!alignment.isNucleotide())
+      {
+        final Set<AlignedCodonFrame> codonMappings = alignment
+                .getCodonFrames();
+        if (codonMappings != null && !codonMappings.isEmpty())
+        {
+          complementConsensus = new AlignmentAnnotation("cDNA Consensus",
+                  "PID for cDNA", new Annotation[1], 0f, 100f,
+                  AlignmentAnnotation.BAR_GRAPH);
+          initConsensus(complementConsensus);
+        }
+      }
     }
   }
 
-  private void initConsensus()
+  private void initConsensus(AlignmentAnnotation aa)
   {
-
-    consensus = new AlignmentAnnotation("Consensus", "PID",
-            new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
-    consensus.hasText = true;
-    consensus.autoCalculated = true;
+    aa.hasText = true;
+    aa.autoCalculated = true;
 
     if (showConsensus)
     {
-      alignment.addAnnotation(consensus);
+      alignment.addAnnotation(aa);
     }
   }
 
@@ -1748,57 +1802,57 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public int calcPanelHeight()
   {
     // setHeight of panels
-    AlignmentAnnotation[] aa = getAlignment().getAlignmentAnnotation();
+    AlignmentAnnotation[] anns = getAlignment().getAlignmentAnnotation();
     int height = 0;
     int charHeight = getCharHeight();
-    if (aa != null)
+    if (anns != null)
     {
       BitSet graphgrp = new BitSet();
-      for (int i = 0; i < aa.length; i++)
+      for (AlignmentAnnotation aa : anns)
       {
-        if (aa[i] == null)
+        if (aa == null)
         {
           System.err.println("Null annotation row: ignoring.");
           continue;
         }
-        if (!aa[i].visible)
+        if (!aa.visible)
         {
           continue;
         }
-        if (aa[i].graphGroup > -1)
+        if (aa.graphGroup > -1)
         {
-          if (graphgrp.get(aa[i].graphGroup))
+          if (graphgrp.get(aa.graphGroup))
           {
             continue;
           }
           else
           {
-            graphgrp.set(aa[i].graphGroup);
+            graphgrp.set(aa.graphGroup);
           }
         }
-        aa[i].height = 0;
+        aa.height = 0;
 
-        if (aa[i].hasText)
+        if (aa.hasText)
         {
-          aa[i].height += charHeight;
+          aa.height += charHeight;
         }
 
-        if (aa[i].hasIcons)
+        if (aa.hasIcons)
         {
-          aa[i].height += 16;
+          aa.height += 16;
         }
 
-        if (aa[i].graph > 0)
+        if (aa.graph > 0)
         {
-          aa[i].height += aa[i].graphHeight;
+          aa.height += aa.graphHeight;
         }
 
-        if (aa[i].height == 0)
+        if (aa.height == 0)
         {
-          aa[i].height = 20;
+          aa.height = 20;
         }
 
-        height += aa[i].height;
+        height += aa.height;
       }
     }
     if (height == 0)
index 24de71e..800b3c3 100644 (file)
  */
 package jalview.workers;
 
+import jalview.api.AlignCalcManagerI;
+import jalview.api.AlignCalcWorkerI;
+import jalview.datamodel.AlignmentAnnotation;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -28,10 +32,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import jalview.api.AlignCalcManagerI;
-import jalview.api.AlignCalcWorkerI;
-import jalview.datamodel.AlignmentAnnotation;
-
 public class AlignCalcManager implements AlignCalcManagerI
 {
   private volatile List<AlignCalcWorkerI> restartable = Collections
@@ -256,10 +256,12 @@ public class AlignCalcManager implements AlignCalcManagerI
       for (List<AlignCalcWorkerI> workers : updating.values())
       {
         for (AlignCalcWorkerI worker : workers)
+        {
           if (worker.involves(alignmentAnnotation))
           {
             return true;
           }
+        }
       }
     }
     return false;
@@ -291,7 +293,7 @@ public class AlignCalcManager implements AlignCalcManagerI
     AlignCalcWorkerI[] workers;
     synchronized (canUpdate)
     {
-      workers = canUpdate.toArray(new AlignCalcWorkerI[0]);
+      workers = canUpdate.toArray(new AlignCalcWorkerI[canUpdate.size()]);
     }
     for (AlignCalcWorkerI worker : workers)
     {
diff --git a/src/jalview/workers/ComplementConsensusThread.java b/src/jalview/workers/ComplementConsensusThread.java
new file mode 100644 (file)
index 0000000..fcc5a82
--- /dev/null
@@ -0,0 +1,68 @@
+package jalview.workers;
+
+import jalview.analysis.AAFrequency;
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Hashtable;
+
+/**
+ * A thread to recompute the consensus of the cDNA complement for a linked
+ * protein alignment.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class ComplementConsensusThread extends ConsensusThread
+{
+
+  public ComplementConsensusThread(AlignViewportI alignViewport,
+          AlignmentViewPanel alignPanel)
+  {
+    super(alignViewport, alignPanel);
+  }
+
+  @Override
+  protected AlignmentAnnotation getConsensusAnnotation()
+  {
+    return alignViewport.getComplementConsensusAnnotation();
+  }
+
+  @Override
+  protected Hashtable[] getViewportConsensus()
+  {
+    return alignViewport.getComplementConsensusHash();
+  }
+
+  @Override
+  protected void computeConsensus(AlignmentI alignment)
+  {
+    Hashtable[] hconsensus = new Hashtable[alignment.getWidth()];
+
+    SequenceI[] aseqs = getSequences();
+    AAFrequency.calculateCdna(alignment, hconsensus);
+
+    alignViewport.setComplementConsensusHash(hconsensus);
+  }
+
+  /**
+   * Convert the computed consensus data into the desired annotation for
+   * display.
+   * 
+   * @param consensusAnnotation
+   *          the annotation to be populated
+   * @param consensusData
+   *          the computed consensus data
+   */
+  @Override
+  protected void deriveConsensus(AlignmentAnnotation consensusAnnotation,
+          Hashtable[] consensusData)
+  {
+    AAFrequency.completeCdnaConsensus(consensusAnnotation, consensusData,
+            alignViewport.isShowSequenceLogo(), getSequences().length);
+  }
+
+}
index f940450..f0c320b 100644 (file)
@@ -35,8 +35,6 @@ import java.util.Hashtable;
 public class ConsensusThread extends AlignCalcWorker implements
         AlignCalcWorkerI
 {
-  private long nseq = -1;
-
   public ConsensusThread(AlignViewportI alignViewport,
           AlignmentViewPanel alignPanel)
   {
@@ -54,8 +52,7 @@ public class ConsensusThread extends AlignCalcWorker implements
     long started = System.currentTimeMillis();
     try
     {
-      AlignmentAnnotation consensus = alignViewport
-              .getAlignmentConsensusAnnotation();
+      AlignmentAnnotation consensus = getConsensusAnnotation();
       if (consensus == null || calcMan.isPending(this))
       {
         calcMan.workerComplete(this);
@@ -88,58 +85,91 @@ public class ConsensusThread extends AlignCalcWorker implements
       if (alignment == null || (aWidth = alignment.getWidth()) < 0)
       {
         calcMan.workerComplete(this);
-        // .updatingConservation = false;
-        // AlignViewport.UPDATING_CONSERVATION = false;
-
-        return;
-      }
-      consensus = alignViewport.getAlignmentConsensusAnnotation();
-
-      consensus.annotations = null;
-      consensus.annotations = new Annotation[aWidth];
-      Hashtable[] hconsensus = alignViewport.getSequenceConsensusHash();
-      hconsensus = new Hashtable[aWidth];
-      try
-      {
-        SequenceI aseqs[] = alignment.getSequencesArray();
-        nseq = aseqs.length;
-        AAFrequency.calculate(aseqs, 0, alignment.getWidth(), hconsensus,
-                true);
-      } catch (ArrayIndexOutOfBoundsException x)
-      {
-        // this happens due to a race condition -
-        // alignment was edited at same time as calculation was running
-        //
-        // calcMan.workerCannotRun(this);
-        calcMan.workerComplete(this);
         return;
       }
-      alignViewport.setSequenceConsensusHash(hconsensus);
+      eraseConsensus(aWidth);
+      // long now = System.currentTimeMillis();
+      computeConsensus(alignment);
       updateResultAnnotation(true);
-      ColourSchemeI globalColourScheme = alignViewport
-              .getGlobalColourScheme();
-      if (globalColourScheme != null)
+      // System.out.println(System.currentTimeMillis() - now);
+
+      if (ap != null)
       {
-        globalColourScheme.setConsensus(hconsensus);
+        ap.paintAlignment(true);
       }
-
     } catch (OutOfMemoryError error)
     {
       calcMan.workerCannotRun(this);
-
-      // consensus = null;
-      // hconsensus = null;
       ap.raiseOOMWarning("calculating consensus", error);
+    } finally
+    {
+      /*
+       * e.g. ArrayIndexOutOfBoundsException can happen due to a race condition
+       * - alignment was edited at same time as calculation was running
+       */
+      calcMan.workerComplete(this);
     }
+  }
+
+  /**
+   * Clear out any existing consensus annotations
+   * 
+   * @param aWidth
+   *          the width (number of columns) of the annotated alignment
+   */
+  protected void eraseConsensus(int aWidth)
+  {
+    AlignmentAnnotation consensus = getConsensusAnnotation();
+    consensus.annotations = new Annotation[aWidth];
+  }
+
+  /**
+   * @param alignment
+   */
+  protected void computeConsensus(AlignmentI alignment)
+  {
+    Hashtable[] hconsensus = new Hashtable[alignment.getWidth()];
+
+    SequenceI[] aseqs = getSequences();
+    AAFrequency.calculate(aseqs, 0, alignment.getWidth(), hconsensus,
+            true);
+
+    alignViewport.setSequenceConsensusHash(hconsensus);
+    setColourSchemeConsensus(hconsensus);
+  }
+
+  /**
+   * @return
+   */
+  protected SequenceI[] getSequences()
+  {
+    return alignViewport.getAlignment().getSequencesArray();
+  }
 
-    calcMan.workerComplete(this);
-    if (ap != null)
+  /**
+   * @param hconsensus
+   */
+  protected void setColourSchemeConsensus(Hashtable[] hconsensus)
+  {
+    ColourSchemeI globalColourScheme = alignViewport
+            .getGlobalColourScheme();
+    if (globalColourScheme != null)
     {
-      ap.paintAlignment(true);
+      globalColourScheme.setConsensus(hconsensus);
     }
   }
 
   /**
+   * Get the Consensus annotation for the alignment
+   * 
+   * @return
+   */
+  protected AlignmentAnnotation getConsensusAnnotation()
+  {
+    return alignViewport.getAlignmentConsensusAnnotation();
+  }
+
+  /**
    * update the consensus annotation from the sequence profile data using
    * current visualization settings.
    */
@@ -151,15 +181,40 @@ public class ConsensusThread extends AlignCalcWorker implements
 
   public void updateResultAnnotation(boolean immediate)
   {
-    AlignmentAnnotation consensus = alignViewport
-            .getAlignmentConsensusAnnotation();
-    Hashtable[] hconsensus = alignViewport.getSequenceConsensusHash();
+    AlignmentAnnotation consensus = getConsensusAnnotation();
+    Hashtable[] hconsensus = getViewportConsensus();
     if (immediate || !calcMan.isWorking(this) && consensus != null
             && hconsensus != null)
     {
-      AAFrequency.completeConsensus(consensus, hconsensus, 0,
-              hconsensus.length, alignViewport.isIgnoreGapsConsensus(),
-              alignViewport.isShowSequenceLogo(), nseq);
+      deriveConsensus(consensus, hconsensus);
     }
   }
+
+  /**
+   * Convert the computed consensus data into the desired annotation for
+   * display.
+   * 
+   * @param consensusAnnotation
+   *          the annotation to be populated
+   * @param consensusData
+   *          the computed consensus data
+   */
+  protected void deriveConsensus(AlignmentAnnotation consensusAnnotation,
+          Hashtable[] consensusData)
+  {
+    long nseq = getSequences().length;
+    AAFrequency.completeConsensus(consensusAnnotation, consensusData, 0,
+            consensusData.length, alignViewport.isIgnoreGapsConsensus(),
+            alignViewport.isShowSequenceLogo(), nseq);
+  }
+
+  /**
+   * Get the consensus data stored on the viewport.
+   * 
+   * @return
+   */
+  protected Hashtable[] getViewportConsensus()
+  {
+    return alignViewport.getSequenceConsensusHash();
+  }
 }
index 1c30c79..788e742 100644 (file)
@@ -1,5 +1,5 @@
-package jalview.analysis;
 
+package jalview.analysis;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import jalview.datamodel.Sequence;
@@ -33,22 +33,30 @@ public class AAFrequencyTest
     Hashtable[] result = new Hashtable[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));
+
+    // 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));
+
+    // col 2 is 50% G 50% C or 25/25 counting gaps
     col = result[2];
-    assertEquals(0f, (Float) col.get(G), 0.0001f);
-    assertEquals(0f, (Float) col.get(N), 0.0001f);
-    assertEquals(0, col.get(C));
-    assertEquals("-", col.get(R));
+    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));
+
+    // col 3 is 75% T 25% G
     col = result[3];
     assertEquals(75f, (Float) col.get(G), 0.0001f);
     assertEquals(75f, (Float) col.get(N), 0.0001f);
@@ -112,4 +120,15 @@ public class AAFrequencyTest
     }
     System.out.println(System.currentTimeMillis() - start);
   }
+
+  @Test
+  public void testGetPercentageFormat()
+  {
+    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());
+  }
 }
diff --git a/test/jalview/analysis/CodingUtilsTest.java b/test/jalview/analysis/CodingUtilsTest.java
new file mode 100644 (file)
index 0000000..0f235fb
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.analysis;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class CodingUtilsTest
+{
+
+  @Test
+  public void testDecodeCodon()
+  {
+    assertTrue(Arrays.equals(new char[]
+    { 'A', 'A', 'A' }, CodingUtils.decodeCodon(0)));
+    assertTrue(Arrays.equals(new char[]
+    { 'A', 'A', 'C' }, CodingUtils.decodeCodon(1)));
+    assertTrue(Arrays.equals(new char[]
+    { 'A', 'A', 'G' }, CodingUtils.decodeCodon(2)));
+    assertTrue(Arrays.equals(new char[]
+    { 'A', 'A', 'T' }, CodingUtils.decodeCodon(3)));
+    assertTrue(Arrays.equals(new char[]
+    { 'A', 'C', 'A' }, CodingUtils.decodeCodon(4)));
+    assertTrue(Arrays.equals(new char[]
+    { 'C', 'A', 'A' }, CodingUtils.decodeCodon(16)));
+    assertTrue(Arrays.equals(new char[]
+    { 'G', 'G', 'G' }, CodingUtils.decodeCodon(42)));
+    assertTrue(Arrays.equals(new char[]
+    { 'T', 'T', 'T' }, CodingUtils.decodeCodon(63)));
+  }
+
+  @Test
+  public void testDecodeNucleotide()
+  {
+    assertEquals('A', CodingUtils.decodeNucleotide(0));
+    assertEquals('C', CodingUtils.decodeNucleotide(1));
+    assertEquals('G', CodingUtils.decodeNucleotide(2));
+    assertEquals('T', CodingUtils.decodeNucleotide(3));
+    assertEquals('0', CodingUtils.decodeNucleotide(4));
+  }
+
+  @Test
+  public void testEncodeCodon()
+  {
+    assertTrue(CodingUtils.encodeCodon('Z') < 0);
+    assertEquals(0, CodingUtils.encodeCodon('a'));
+    assertEquals(0, CodingUtils.encodeCodon('A'));
+    assertEquals(1, CodingUtils.encodeCodon('c'));
+    assertEquals(1, CodingUtils.encodeCodon('C'));
+    assertEquals(2, CodingUtils.encodeCodon('g'));
+    assertEquals(2, CodingUtils.encodeCodon('G'));
+    assertEquals(3, CodingUtils.encodeCodon('t'));
+    assertEquals(3, CodingUtils.encodeCodon('T'));
+    assertEquals(3, CodingUtils.encodeCodon('u'));
+    assertEquals(3, CodingUtils.encodeCodon('U'));
+    
+    assertEquals(-1, CodingUtils.encodeCodon(null));
+    assertEquals(0, CodingUtils.encodeCodon(new char[]
+    { 'A', 'A', 'A' }));
+    assertEquals(1, CodingUtils.encodeCodon(new char[]
+    { 'A', 'A', 'C' }));
+    assertEquals(2, CodingUtils.encodeCodon(new char[]
+    { 'A', 'A', 'G' }));
+    assertEquals(3, CodingUtils.encodeCodon(new char[]
+    { 'A', 'A', 'T' }));
+    assertEquals(4, CodingUtils.encodeCodon(new char[]
+    { 'A', 'C', 'A' }));
+    assertEquals(16, CodingUtils.encodeCodon(new char[]
+    { 'C', 'A', 'A' }));
+    assertEquals(42, CodingUtils.encodeCodon(new char[]
+    { 'G', 'G', 'G' }));
+    assertEquals(63, CodingUtils.encodeCodon(new char[]
+    { 'T', 'T', 'T' }));
+  }
+
+}
index 25d0155..0e24bf6 100644 (file)
@@ -109,4 +109,33 @@ public class AlignedCodonFrameTest
      */
     assertNull(acf.getMappedRegion(seq1, aseq2, 1));
   }
+
+  @Test
+  public void testGetMappedCodon()
+  {
+    final Sequence seq1 = new Sequence("Seq1", "c-G-TA-gC-gT-T");
+    seq1.createDatasetSequence();
+    final Sequence aseq1 = new Sequence("Seq1", "-P-R");
+    aseq1.createDatasetSequence();
+
+    /*
+     * First with no mappings
+     */
+    AlignedCodonFrame acf = new AlignedCodonFrame();
+
+    assertNull(acf.getMappedCodon(seq1.getDatasetSequence(), 0));
+
+    /*
+     * Set up the mappings for the exons (upper-case bases)
+     */
+    MapList map = new MapList(new int[]
+    { 2, 4, 6, 6, 8, 9 }, new int[]
+    { 1, 2 }, 3, 1);
+    acf.addMap(seq1.getDatasetSequence(), aseq1.getDatasetSequence(), map);
+    
+    assertEquals("[G, T, A]", Arrays.toString(acf.getMappedCodon(
+            aseq1.getDatasetSequence(), 1)));
+    assertEquals("[C, T, T]", Arrays.toString(acf.getMappedCodon(
+            aseq1.getDatasetSequence(), 2)));
+  }
 }
index 3b3d926..df98af9 100644 (file)
@@ -171,21 +171,27 @@ public class AlignmentTest
   }
 
   /**
-   * Aligning protein from cDNA yet to be implemented, does nothing.
+   * Aligning protein from cDNA.
    * 
    * @throws IOException
    */
   @Test
   public void testAlignAs_proteinAsCdna() throws IOException
   {
+    // see also AlignmentUtilsTests
     AlignmentI al1 = loadAlignment(CDNA_SEQS_1, "FASTA");
     AlignmentI al2 = loadAlignment(AA_SEQS_1, "FASTA");
-    String before0 = al2.getSequenceAt(0).getSequenceAsString();
-    String before1 = al2.getSequenceAt(1).getSequenceAsString();
+    AlignedCodonFrame acf = new AlignedCodonFrame();
+    MapList ml = new MapList(new int[]
+    { 1, 12 }, new int[]
+    { 1, 4 }, 3, 1);
+    acf.addMap(al1.getSequenceAt(0), al2.getSequenceAt(0), ml);
+    acf.addMap(al1.getSequenceAt(1), al2.getSequenceAt(1), ml);
+    al2.addCodonFrame(acf);
 
     ((Alignment) al2).alignAs(al1, false, true);
-    assertEquals(before0, al2.getSequenceAt(0).getSequenceAsString());
-    assertEquals(before1, al2.getSequenceAt(1).getSequenceAsString());
+    assertEquals("K-Q-Y-L-", al2.getSequenceAt(0).getSequenceAsString());
+    assertEquals("-R-F-P-W", al2.getSequenceAt(1).getSequenceAsString());
   }
 
   /**
index f0f3be7..f1ea01c 100644 (file)
@@ -19,6 +19,7 @@ import jalview.io.FormatAdapter;
 
 import java.awt.Color;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Set;
 
@@ -397,4 +398,29 @@ public class MappingUtilsTest
     cs = MappingUtils.mapColumnSelection(colsel, dnaView, proteinView);
     assertEquals("[0, 1, 3]", cs.getSelected().toString());
   }
+
+  /**
+   * Tests for the method that converts a series of [start, end] ranges to
+   * single positions
+   */
+  @Test
+  public void testFlattenRanges()
+  {
+    assertEquals("[1, 2, 3, 4]",
+            Arrays.toString(MappingUtils.flattenRanges(new int[]
+            { 1, 4 })));
+    assertEquals("[1, 2, 3, 4]",
+            Arrays.toString(MappingUtils.flattenRanges(new int[]
+            { 1, 2, 3, 4 })));
+    assertEquals("[1, 2, 3, 4]",
+            Arrays.toString(MappingUtils.flattenRanges(new int[]
+            { 1, 1, 2, 2, 3, 3, 4, 4 })));
+    assertEquals("[1, 2, 3, 4, 7, 8, 9, 12]",
+            Arrays.toString(MappingUtils.flattenRanges(new int[]
+            { 1, 4, 7, 9, 12, 12 })));
+    // unpaired start position is ignored:
+    assertEquals("[1, 2, 3, 4, 7, 8, 9, 12]",
+            Arrays.toString(MappingUtils.flattenRanges(new int[]
+            { 1, 4, 7, 9, 12, 12, 15 })));
+  }
 }