From bfb4b56a77352694f4bc445ec31851bbdcdce140 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 10 Mar 2015 11:44:42 +0000 Subject: [PATCH] JAL-1681 show cDNA consensus on protein alignment - first version --- src/jalview/analysis/AAFrequency.java | 418 ++++++++++++++++---- src/jalview/analysis/CodingUtils.java | 119 ++++++ src/jalview/analysis/StructureFrequency.java | 39 +- src/jalview/api/AlignViewportI.java | 21 + src/jalview/datamodel/AlignedCodonFrame.java | 47 +++ src/jalview/datamodel/AlignmentAnnotation.java | 9 + src/jalview/gui/AlignFrame.java | 5 +- src/jalview/gui/SplitFrame.java | 20 + src/jalview/renderer/AnnotationRenderer.java | 210 ++++++---- src/jalview/util/Format.java | 9 + src/jalview/util/MappingUtils.java | 56 +++ src/jalview/viewmodel/AlignmentViewport.java | 106 +++-- src/jalview/workers/AlignCalcManager.java | 12 +- src/jalview/workers/ComplementConsensusThread.java | 68 ++++ src/jalview/workers/ConsensusThread.java | 147 ++++--- test/jalview/analysis/AAFrequencyTest.java | 29 +- test/jalview/analysis/CodingUtilsTest.java | 78 ++++ test/jalview/datamodel/AlignedCodonFrameTest.java | 29 ++ test/jalview/datamodel/AlignmentTest.java | 16 +- test/jalview/util/MappingUtilsTest.java | 26 ++ 20 files changed, 1215 insertions(+), 249 deletions(-) create mode 100644 src/jalview/analysis/CodingUtils.java create mode 100644 src/jalview/workers/ComplementConsensusThread.java create mode 100644 test/jalview/analysis/CodingUtilsTest.java diff --git a/src/jalview/analysis/AAFrequency.java b/src/jalview/analysis/AAFrequency.java index 8dfda39..b206f67 100755 --- a/src/jalview/analysis/AAFrequency.java +++ b/src/jalview/analysis/AAFrequency.java @@ -20,13 +20,19 @@ */ 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 + * + *
+   *    [profileType, numberOfValues, nonGapCount, charValue1, percentage1, charValue2, percentage2, ...]
+   * in descending order of percentage value
+   * 
* * @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 + * + *
+   *    [profileType, numberOfValues, totalCount, charValue1, percentage1, charValue2, percentage2, ...]
+   * in descending order of percentage value, where the character values encode codon triplets
+   * 
+ * + * @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 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 columnHash = new Hashtable(); + // #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 index 0000000..a434465 --- /dev/null +++ b/src/jalview/analysis/CodingUtils.java @@ -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; + } + +} diff --git a/src/jalview/analysis/StructureFrequency.java b/src/jalview/analysis/StructureFrequency.java index dc0212e..cb78b04 100644 --- a/src/jalview/analysis/StructureFrequency.java +++ b/src/jalview/analysis/StructureFrequency.java @@ -20,10 +20,14 @@ */ 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[]) diff --git a/src/jalview/api/AlignViewportI.java b/src/jalview/api/AlignViewportI.java index 037f19e..374e0be 100644 --- a/src/jalview/api/AlignViewportI.java +++ b/src/jalview/api/AlignViewportI.java @@ -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 diff --git a/src/jalview/datamodel/AlignedCodonFrame.java b/src/jalview/datamodel/AlignedCodonFrame.java index cbddf1c..b174c31 100644 --- a/src/jalview/datamodel/AlignedCodonFrame.java +++ b/src/jalview/datamodel/AlignedCodonFrame.java @@ -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] }; + } } diff --git a/src/jalview/datamodel/AlignmentAnnotation.java b/src/jalview/datamodel/AlignmentAnnotation.java index 0d99155..f8782e7 100755 --- a/src/jalview/datamodel/AlignmentAnnotation.java +++ b/src/jalview/datamodel/AlignmentAnnotation.java @@ -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 diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index e7a4735..2f43e82 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -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 diff --git a/src/jalview/gui/SplitFrame.java b/src/jalview/gui/SplitFrame.java index 9cbded9..d594051 100644 --- a/src/jalview/gui/SplitFrame.java +++ b/src/jalview/gui/SplitFrame.java @@ -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); + } } diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index 448bcd2..2e40416 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -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); } diff --git a/src/jalview/util/Format.java b/src/jalview/util/Format.java index a7b311b..f1dd359 100755 --- a/src/jalview/util/Format.java +++ b/src/jalview/util/Format.java @@ -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; + } } diff --git a/src/jalview/util/MappingUtils.java b/src/jalview/util/MappingUtils.java index eebc539..4cfb49e 100644 --- a/src/jalview/util/MappingUtils.java +++ b/src/jalview/util/MappingUtils.java @@ -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 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; + } } diff --git a/src/jalview/viewmodel/AlignmentViewport.java b/src/jalview/viewmodel/AlignmentViewport.java index d504093..df08682 100644 --- a/src/jalview/viewmodel/AlignmentViewport.java +++ b/src/jalview/viewmodel/AlignmentViewport.java @@ -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 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) diff --git a/src/jalview/workers/AlignCalcManager.java b/src/jalview/workers/AlignCalcManager.java index 24de71e..800b3c3 100644 --- a/src/jalview/workers/AlignCalcManager.java +++ b/src/jalview/workers/AlignCalcManager.java @@ -20,6 +20,10 @@ */ 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 restartable = Collections @@ -256,10 +256,12 @@ public class AlignCalcManager implements AlignCalcManagerI for (List 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 index 0000000..fcc5a82 --- /dev/null +++ b/src/jalview/workers/ComplementConsensusThread.java @@ -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); + } + +} diff --git a/src/jalview/workers/ConsensusThread.java b/src/jalview/workers/ConsensusThread.java index f940450..f0c320b 100644 --- a/src/jalview/workers/ConsensusThread.java +++ b/src/jalview/workers/ConsensusThread.java @@ -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(); + } } diff --git a/test/jalview/analysis/AAFrequencyTest.java b/test/jalview/analysis/AAFrequencyTest.java index 1c30c79..788e742 100644 --- a/test/jalview/analysis/AAFrequencyTest.java +++ b/test/jalview/analysis/AAFrequencyTest.java @@ -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 index 0000000..0f235fb --- /dev/null +++ b/test/jalview/analysis/CodingUtilsTest.java @@ -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' })); + } + +} diff --git a/test/jalview/datamodel/AlignedCodonFrameTest.java b/test/jalview/datamodel/AlignedCodonFrameTest.java index 25d0155..0e24bf6 100644 --- a/test/jalview/datamodel/AlignedCodonFrameTest.java +++ b/test/jalview/datamodel/AlignedCodonFrameTest.java @@ -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))); + } } diff --git a/test/jalview/datamodel/AlignmentTest.java b/test/jalview/datamodel/AlignmentTest.java index 3b3d926..df98af9 100644 --- a/test/jalview/datamodel/AlignmentTest.java +++ b/test/jalview/datamodel/AlignmentTest.java @@ -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()); } /** diff --git a/test/jalview/util/MappingUtilsTest.java b/test/jalview/util/MappingUtilsTest.java index f0f3be7..f1ea01c 100644 --- a/test/jalview/util/MappingUtilsTest.java +++ b/test/jalview/util/MappingUtilsTest.java @@ -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 }))); + } } -- 1.7.10.2