From a5f232621e8d15888530a7819da2fb774549ee21 Mon Sep 17 00:00:00 2001 From: Renia Correya Date: Tue, 30 Apr 2024 15:37:13 +0100 Subject: [PATCH] JAL-4392 Merging consensus secondary structure --- resources/lang/Messages.properties | 3 + src/jalview/analysis/AAFrequency.java | 239 +++++++- src/jalview/api/AlignViewportI.java | 13 + src/jalview/bin/Cache.java | 2 + src/jalview/datamodel/Profile.java | 57 ++ src/jalview/datamodel/ProfileI.java | 7 + src/jalview/datamodel/SecondaryStructureCount.java | 612 ++++++++++++++++++++ src/jalview/datamodel/SequenceGroup.java | 98 ++++ src/jalview/gui/AlignFrame.java | 8 + src/jalview/gui/AlignViewport.java | 5 +- src/jalview/gui/PopupMenu.java | 22 + src/jalview/jbgui/GAlignFrame.java | 23 +- src/jalview/project/Jalview2XML.java | 2 + src/jalview/renderer/AnnotationRenderer.java | 21 +- src/jalview/renderer/ResidueShader.java | 48 ++ src/jalview/renderer/ResidueShaderI.java | 4 + src/jalview/viewmodel/AlignmentViewport.java | 146 ++++- src/jalview/workers/AlignCalcManager.java | 8 +- .../workers/SecondaryStructureConsensusThread.java | 258 +++++++++ test/jalview/renderer/ResidueShaderTest.java | 37 ++ 20 files changed, 1599 insertions(+), 14 deletions(-) create mode 100644 src/jalview/datamodel/SecondaryStructureCount.java create mode 100644 src/jalview/workers/SecondaryStructureConsensusThread.java diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index c3c8589..195eb1f 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -277,7 +277,9 @@ label.show_selected_annotations = Show selected annotations label.group_consensus = Group Consensus label.group_conservation = Group Conservation label.show_consensus_histogram = Show Consensus Histogram +label.show_ssconsensus_histogram = Show SS Consensus Histogram label.show_consensus_logo = Show Consensus Logo +label.show_ssconsensus_logo = Show SS Consensus Logo label.norm_consensus_logo = Normalise Consensus Logo label.apply_all_groups = Apply to all groups label.autocalculated_annotation = Autocalculated Annotation @@ -1288,6 +1290,7 @@ label.togglehidden = Show hidden regions label.quality_descr = Alignment Quality based on Blosum62 scores label.conservation_descr = Conservation of total alignment less than {0}% gaps label.consensus_descr = PID +label.ssconsensus_descr = SS Consensus label.complement_consensus_descr = PID for cDNA label.strucconsensus_descr = PID for base pairs label.occupancy_descr = Number of aligned positions diff --git a/src/jalview/analysis/AAFrequency.java b/src/jalview/analysis/AAFrequency.java index 6967885..796625a 100755 --- a/src/jalview/analysis/AAFrequency.java +++ b/src/jalview/analysis/AAFrequency.java @@ -30,6 +30,7 @@ import jalview.datamodel.Profiles; import jalview.datamodel.ProfilesI; import jalview.datamodel.ResidueCount; import jalview.datamodel.ResidueCount.SymbolCounts; +import jalview.datamodel.SecondaryStructureCount; import jalview.datamodel.SequenceI; import jalview.ext.android.SparseIntArray; import jalview.util.Comparison; @@ -54,6 +55,8 @@ import java.util.List; public class AAFrequency { public static final String PROFILE = "P"; + private static final String SS_ANNOTATION_LABEL = "Secondary Structure"; + private static final char COIL = 'C'; /* * Quick look-up of String value of char 'A' to 'Z' @@ -191,6 +194,130 @@ public class AAFrequency // jalview.bin.Console.outPrintln(elapsed); } + + public static final ProfilesI calculateSS(List list, int start, + int end) + { + return calculateSS(list, start, end, false); + } + + public static final ProfilesI calculateSS(List sequences, + int start, int end, boolean profile) + { + SequenceI[] seqs = new SequenceI[sequences.size()]; + int width = 0; + synchronized (sequences) + { + for (int i = 0; i < sequences.size(); i++) + { + seqs[i] = sequences.get(i); + int length = seqs[i].getLength(); + if (length > width) + { + width = length; + } + } + + if (end >= width) + { + end = width; + } + + ProfilesI reply = calculateSS(seqs, width, start, end, profile); + return reply; + } + } + + public static final ProfilesI calculateSS(final SequenceI[] sequences, + int width, int start, int end, boolean saveFullProfile) + { + // long now = System.currentTimeMillis(); + int seqCount = sequences.length; + + ProfileI[] result = new ProfileI[width]; + + for (int column = start; column < end; column++) + { + /* + * Apply a heuristic to detect nucleotide data (which can + * be counted in more compact arrays); here we test for + * more than 90% nucleotide; recheck every 10 columns in case + * of misleading data e.g. highly conserved Alanine in peptide! + * Mistakenly guessing nucleotide has a small performance cost, + * as it will result in counting in sparse arrays. + * Mistakenly guessing peptide has a small space cost, + * as it will use a larger than necessary array to hold counts. + */ + + int ssCount = 0; + + SecondaryStructureCount ssCounts = new SecondaryStructureCount(); + + for (int row = 0; row < seqCount; row++) + { + if (sequences[row] == null) + { + jalview.bin.Console.errPrintln( + "WARNING: Consensus skipping null sequence - possible race condition."); + continue; + } + + char c = sequences[row].getCharAt(column); + + if (sequences[row].getLength() > column && !Comparison.isGap(c)) + { + + AlignmentAnnotation[] aa = sequences[row].getAnnotation(SS_ANNOTATION_LABEL); + if(aa == null) { + continue; + } + int seqPosition = sequences[row].findPosition(column); + char ss; + if (aa[0].getAnnotationForPosition(seqPosition) != null) { + ss = aa[0].getAnnotationForPosition(seqPosition).secondaryStructure; + + //There is no representation for coil and it can be either ' ' or null. + if (ss == ' ') { + ss = COIL; + } + } + else { + ss = COIL; + } + + //secondaryStructures[row][column] = ss; + + ssCounts.add(ss); + ssCount++; + + } + else + { + /* + * count a gap if the sequence doesn't reach this column + */ + ssCounts.addGap(); + } + } + + int maxSSCount = ssCounts.getModalCount(); + String maxSS = ssCounts.getSSForCount(maxSSCount); + int gapCount = ssCounts.getGapCount(); + ProfileI profile = new Profile(maxSS, ssCount, gapCount, + maxSSCount); + + if (saveFullProfile) + { + profile.setSSCounts(ssCounts); + } + + result[column] = profile; + } + return new Profiles(result); + // long elapsed = System.currentTimeMillis() - now; + // jalview.bin.Console.outPrintln(elapsed); + } + /** * Make an estimate of the profile size we are going to compute i.e. how many * different characters may be present in it. Overestimating has a cost of @@ -287,6 +414,58 @@ public class AAFrequency // long elapsed = System.currentTimeMillis() - now; // jalview.bin.Console.outPrintln(-elapsed); } + + + public static void completeSSConsensus(AlignmentAnnotation ssConsensus, + ProfilesI profiles, int startCol, int endCol, boolean ignoreGaps, + boolean showSequenceLogo, long nseq) + { + // long now = System.currentTimeMillis(); + if (ssConsensus == null || ssConsensus.annotations == null + || ssConsensus.annotations.length < endCol) + { + /* + * called with a bad alignment annotation row + * wait for it to be initialised properly + */ + return; + } + + for (int i = startCol; i < endCol; i++) + { + ProfileI profile = profiles.get(i); + if (profile == null) + { + /* + * happens if sequences calculated over were + * shorter than alignment width + */ + ssConsensus.annotations[i] = null; + return; + } + + final int dp = getPercentageDp(nseq); + + float value = profile.getSSPercentageIdentity(ignoreGaps); + + String description = getSSTooltip(profile, value, showSequenceLogo, + ignoreGaps, dp); + + String modalSS = profile.getModalSS(); + if ("".equals(modalSS)) + { + modalSS = "-"; + } + else if (modalSS.length() > 1) + { + modalSS = "+"; + } + ssConsensus.annotations[i] = new Annotation(modalSS, description, + ' ', value); + } + // long elapsed = System.currentTimeMillis() - now; + // jalview.bin.Console.outPrintln(-elapsed); + } /** * Derive the gap count annotation row. @@ -392,6 +571,41 @@ public class AAFrequency } return description; } + + static String getSSTooltip(ProfileI profile, float pid, + boolean showSequenceLogo, boolean ignoreGaps, int dp) + { + SecondaryStructureCount counts = profile.getSSCounts(); + + String description = null; + if (counts != null && showSequenceLogo) + { + int normaliseBy = ignoreGaps ? profile.getNonGapped() + : profile.getHeight(); + description = counts.getTooltip(normaliseBy, dp); + } + else + { + StringBuilder sb = new StringBuilder(64); + String maxSS = profile.getModalSS(); + if (maxSS.length() > 1) + { + sb.append("[").append(maxSS).append("]"); + } + else + { + sb.append(maxSS); + } + if (maxSS.length() > 0) + { + sb.append(" "); + Format.appendPercentage(sb, pid, dp); + sb.append("%"); + } + description = sb.toString(); + } + return description; + } /** * Returns the sorted profile for the given consensus data. The returned array @@ -411,15 +625,30 @@ public class AAFrequency */ public static int[] extractProfile(ProfileI profile, boolean ignoreGaps) { - ResidueCount counts = profile.getCounts(); - if (counts == null) + char[] symbols; + int[] values; + + if (profile.getCounts() != null) { + ResidueCount counts = profile.getCounts(); + SymbolCounts symbolCounts = counts.getSymbolCounts(); + symbols = symbolCounts.symbols; + values = symbolCounts.values; + + } + else if(profile.getSSCounts() != null) + { + SecondaryStructureCount counts = profile.getSSCounts(); + // to do + SecondaryStructureCount.SymbolCounts symbolCounts = counts.getSymbolCounts(); + symbols = symbolCounts.symbols; + values = symbolCounts.values; + } + else { return null; } + - SymbolCounts symbolCounts = counts.getSymbolCounts(); - char[] symbols = symbolCounts.symbols; - int[] values = symbolCounts.values; QuickSort.sort(values, symbols); int totalPercentage = 0; final int divisor = ignoreGaps ? profile.getNonGapped() diff --git a/src/jalview/api/AlignViewportI.java b/src/jalview/api/AlignViewportI.java index 0cfd03d..b7747f5 100644 --- a/src/jalview/api/AlignViewportI.java +++ b/src/jalview/api/AlignViewportI.java @@ -85,8 +85,12 @@ public interface AlignViewportI extends ViewStyleI boolean isValidCharWidth(); boolean isShowConsensusHistogram(); + + boolean isShowSSConsensusHistogram(); boolean isShowSequenceLogo(); + + boolean isShowSequenceSSLogo(); boolean isNormaliseSequenceLogo(); @@ -129,6 +133,9 @@ public interface AlignViewportI extends ViewStyleI * @return */ AlignmentAnnotation getAlignmentConsensusAnnotation(); + + AlignmentAnnotation getAlignmentSecondaryStructureConsensusAnnotation(); + /** * get the container for alignment gap annotation @@ -175,6 +182,9 @@ public interface AlignViewportI extends ViewStyleI * @param hconsensus */ void setSequenceConsensusHash(ProfilesI hconsensus); + + void setSequenceSSConsensusHash(ProfilesI hSSConsensus); + /** * Set the cDNA complement consensus for the viewport @@ -567,4 +577,7 @@ public interface AlignViewportI extends ViewStyleI Iterator getViewAsVisibleContigs(boolean selectedRegionOnly); ContactMatrixI getContactMatrix(AlignmentAnnotation alignmentAnnotation); + + ProfilesI getSequenceSSConsensusHash(); + } diff --git a/src/jalview/bin/Cache.java b/src/jalview/bin/Cache.java index a64e869..558b859 100755 --- a/src/jalview/bin/Cache.java +++ b/src/jalview/bin/Cache.java @@ -177,6 +177,8 @@ import jalview.ws.sifts.SiftsSettings; * in the alignment. *
  • SHOW_CONSENSUS_HISTOGRAM (false) Show consensus annotation row's * histogram.
  • + *
  • SHOW_SSCONSENSUS_HISTOGRAM (false) Show secondary structure consensus annotation row's + * histogram.
  • *
  • SHOW_CONSENSUS_LOGO (false) Show consensus annotation row's sequence * logo.
  • *
  • NORMALISE_CONSENSUS_LOGO (false) Show consensus annotation row's sequence diff --git a/src/jalview/datamodel/Profile.java b/src/jalview/datamodel/Profile.java index 8638896..35f1429 100644 --- a/src/jalview/datamodel/Profile.java +++ b/src/jalview/datamodel/Profile.java @@ -32,6 +32,8 @@ public class Profile implements ProfileI * an object holding counts of symbols in the profile */ private ResidueCount counts; + + private SecondaryStructureCount ssCounts; /* * the number of sequences (gapped or not) in the profile @@ -47,12 +49,16 @@ public class Profile implements ProfileI * the highest count for any residue in the profile */ private int maxCount; + private int maxSSCount; + /* * the residue (e.g. K) or residues (e.g. KQW) with the * highest count in the profile */ private String modalResidue; + + private String modalSS; /** * Constructor which allows derived data to be stored without having to store @@ -74,6 +80,14 @@ public class Profile implements ProfileI this.maxCount = max; this.modalResidue = modalRes; } + + public Profile(String modalSS, int ssCount, int gaps, int maxSSCount) + { + this.height = ssCount; + this.gapped = gaps; + this.maxSSCount = maxSSCount; + this.modalSS = modalSS; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#setCounts(jalview.datamodel.ResidueCount) @@ -83,6 +97,12 @@ public class Profile implements ProfileI { this.counts = residueCounts; } + + @Override + public void setSSCounts(SecondaryStructureCount secondaryStructureCount) + { + this.ssCounts = secondaryStructureCount; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#getPercentageIdentity(boolean) @@ -105,6 +125,25 @@ public class Profile implements ProfileI } return pid; } + + @Override + public float getSSPercentageIdentity(boolean ignoreGaps) + { + if (height == 0) + { + return 0f; + } + float ssPid = 0f; + if (ignoreGaps && gapped < height) + { + ssPid = (maxSSCount * 100f) / (height - gapped); + } + else + { + ssPid = (maxSSCount * 100f) / height; + } + return ssPid; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#getCounts() @@ -114,6 +153,12 @@ public class Profile implements ProfileI { return counts; } + + @Override + public SecondaryStructureCount getSSCounts() + { + return ssCounts; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#getHeight() @@ -141,6 +186,12 @@ public class Profile implements ProfileI { return maxCount; } + + @Override + public int getMaxSSCount() + { + return maxSSCount; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#getModalResidue() @@ -150,6 +201,12 @@ public class Profile implements ProfileI { return modalResidue; } + + @Override + public String getModalSS() + { + return modalSS; + } /* (non-Javadoc) * @see jalview.datamodel.ProfileI#getNonGapped() diff --git a/src/jalview/datamodel/ProfileI.java b/src/jalview/datamodel/ProfileI.java index 65a5c0d..511095e 100644 --- a/src/jalview/datamodel/ProfileI.java +++ b/src/jalview/datamodel/ProfileI.java @@ -29,6 +29,8 @@ public interface ProfileI * @param residueCounts */ public abstract void setCounts(ResidueCount residueCounts); + public abstract void setSSCounts(SecondaryStructureCount secondaryStructureCount); + /** * Returns the percentage identity of the profile, i.e. the highest proportion @@ -39,6 +41,8 @@ public interface ProfileI * @return */ public abstract float getPercentageIdentity(boolean ignoreGaps); + + public abstract float getSSPercentageIdentity(boolean ignoreGaps); /** * Returns the full symbol counts for this profile @@ -68,6 +72,7 @@ public interface ProfileI * @return */ public abstract int getMaxCount(); + public abstract int getMaxSSCount(); /** * Returns the symbol (or concatenated symbols) which have the highest count @@ -76,6 +81,7 @@ public interface ProfileI * @return */ public abstract String getModalResidue(); + public abstract String getModalSS(); /** * Answers the number of non-gapped sequences in the profile @@ -83,5 +89,6 @@ public interface ProfileI * @return */ public abstract int getNonGapped(); + SecondaryStructureCount getSSCounts(); } \ No newline at end of file diff --git a/src/jalview/datamodel/SecondaryStructureCount.java b/src/jalview/datamodel/SecondaryStructureCount.java new file mode 100644 index 0000000..52e9746 --- /dev/null +++ b/src/jalview/datamodel/SecondaryStructureCount.java @@ -0,0 +1,612 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +package jalview.datamodel; + +import jalview.util.Comparison; +import jalview.util.Format; +import jalview.util.QuickSort; +import jalview.util.SparseCount; + +/** + * A class to count occurrences of residues in a profile, optimised for speed + * and memory footprint. + * + * @author gmcarstairs + * + */ +public class SecondaryStructureCount +{ + /** + * A data bean to hold the results of counting symbols + */ + public class SymbolCounts + { + /** + * the symbols seen (as char values), in no particular order + */ + public final char[] symbols; + + /** + * the counts for each symbol, in the same order as the symbols + */ + public final int[] values; + + SymbolCounts(char[] s, int[] v) + { + symbols = s; + values = v; + } + } + + private static final int TOUPPERCASE = 'A' - 'a'; + + /* + * nucleotide symbols to count (including N unknown) + */ + private static final String SS_SYMBOLS = "HEC"; + + + static final int GAP_COUNT = 0; + + /* + * fast lookup tables holding the index into our count + * arrays of each symbol; index 0 is reserved for gap counting + */ + private static int[] SS_INDEX = new int[26]; + + static + { + for (int i = 0; i < SS_SYMBOLS.length(); i++) + { + SS_INDEX[SS_SYMBOLS.charAt(i) - 'A'] = i + 1; + } + } + + /* + * counts array, just big enough for the nucleotide or peptide + * character set (plus gap counts in position 0) + */ + private short[] counts; + + /* + * alternative array of int counts for use if any count + * exceeds the maximum value of short (32767) + */ + private int[] intCounts; + + /* + * flag set if we switch from short to int counts + */ + private boolean useIntCounts; + + /* + * general-purpose counter, only for use for characters + * that are not in the expected alphabet + */ + private SparseCount otherData; + + /* + * keeps track of the maximum count value recorded + * (if this class ever allows decrements, would need to + * calculate this on request instead) + */ + int maxCount; + + /** + * Constructor that allocates an array just big enough for the anticipated + * characters, plus one position to count gaps + */ + public SecondaryStructureCount() + { + //isSS = true; + int charsToCount = SS_SYMBOLS.length(); + counts = new short[charsToCount + 1]; + } + + /** + * Increments the count for the given character. The supplied character may be + * upper or lower case but counts are for the upper case only. Gap characters + * (space, ., -) are all counted together. + * + * @param c + * @return the new value of the count for the character + */ + public int add(final char c) + { + char u = toUpperCase(c); + int newValue = 0; + int offset = getOffset(u); + + /* + * offset 0 is reserved for gap counting, so 0 here means either + * an unexpected character, or a gap character passed in error + */ + if (offset == 0) + { + if (Comparison.isGap(u)) + { + newValue = addGap(); + } + else + { + newValue = addOtherCharacter(u); + } + } + else + { + newValue = increment(offset); + } + return newValue; + } + + /** + * Increment the count at the specified offset. If this would result in short + * overflow, promote to counting int values instead. + * + * @param offset + * @return the new value of the count at this offset + */ + int increment(int offset) + { + int newValue = 0; + if (useIntCounts) + { + newValue = intCounts[offset]; + intCounts[offset] = ++newValue; + } + else + { + if (counts[offset] == Short.MAX_VALUE) + { + handleOverflow(); + newValue = intCounts[offset]; + intCounts[offset] = ++newValue; + } + else + { + newValue = counts[offset]; + counts[offset] = (short) ++newValue; + } + } + + if (offset != GAP_COUNT) + { + // update modal residue count + maxCount = Math.max(maxCount, newValue); + } + return newValue; + } + + /** + * Switch from counting in short to counting in int + */ + synchronized void handleOverflow() + { + intCounts = new int[counts.length]; + for (int i = 0; i < counts.length; i++) + { + intCounts[i] = counts[i]; + } + counts = null; + useIntCounts = true; + } + + /** + * Returns this character's offset in the count array + * + * @param c + * @return + */ + int getOffset(char c) + { + int offset = 0; + if ('A' <= c && c <= 'Z') + { + offset = SS_INDEX[c - 'A']; + } + return offset; + } + + /** + * @param c + * @return + */ + protected char toUpperCase(final char c) + { + char u = c; + if ('a' <= c && c <= 'z') + { + u = (char) (c + TOUPPERCASE); + } + return u; + } + + /** + * Increment count for some unanticipated character. The first time this + * called, a SparseCount is instantiated to hold these 'extra' counts. + * + * @param c + * @return the new value of the count for the character + */ + int addOtherCharacter(char c) + { + if (otherData == null) + { + otherData = new SparseCount(); + } + int newValue = otherData.add(c, 1); + maxCount = Math.max(maxCount, newValue); + return newValue; + } + + /** + * Set count for some unanticipated character. The first time this called, a + * SparseCount is instantiated to hold these 'extra' counts. + * + * @param c + * @param value + */ + void setOtherCharacter(char c, int value) + { + if (otherData == null) + { + otherData = new SparseCount(); + } + otherData.put(c, value); + } + + /** + * Increment count of gap characters + * + * @return the new count of gaps + */ + public int addGap() + { + int newValue = increment(GAP_COUNT); + return newValue; + } + + /** + * Answers true if we are counting ints (only after overflow of short counts) + * + * @return + */ + boolean isCountingInts() + { + return useIntCounts; + } + + /** + * Sets the count for the given character. The supplied character may be upper + * or lower case but counts are for the upper case only. + * + * @param c + * @param count + */ + public void put(char c, int count) + { + char u = toUpperCase(c); + int offset = getOffset(u); + + /* + * offset 0 is reserved for gap counting, so 0 here means either + * an unexpected character, or a gap character passed in error + */ + if (offset == 0) + { + if (Comparison.isGap(u)) + { + set(0, count); + } + else + { + setOtherCharacter(u, count); + maxCount = Math.max(maxCount, count); + } + } + else + { + set(offset, count); + maxCount = Math.max(maxCount, count); + } + } + + /** + * Sets the count at the specified offset. If this would result in short + * overflow, promote to counting int values instead. + * + * @param offset + * @param value + */ + void set(int offset, int value) + { + if (useIntCounts) + { + intCounts[offset] = value; + } + else + { + if (value > Short.MAX_VALUE || value < Short.MIN_VALUE) + { + handleOverflow(); + intCounts[offset] = value; + } + else + { + counts[offset] = (short) value; + } + } + } + + /** + * Returns the count for the given character, or zero if no count held + * + * @param c + * @return + */ + public int getCount(char c) + { + char u = toUpperCase(c); + int offset = getOffset(u); + if (offset == 0) + { + if (!Comparison.isGap(u)) + { + // should have called getGapCount() + return otherData == null ? 0 : otherData.get(u); + } + } + return useIntCounts ? intCounts[offset] : counts[offset]; + } + + public int getGapCount() + { + return useIntCounts ? intCounts[0] : counts[0]; + } + + /** + * Answers true if this object wraps a counter for unexpected characters + * + * @return + */ + boolean isUsingOtherData() + { + return otherData != null; + } + + /** + * Returns the character (or concatenated characters) for the symbol(s) with + * the given count in the profile. Can be used to get the modal residue by + * supplying the modal count value. Returns an empty string if no symbol has + * the given count. The symbols are in alphabetic order of standard peptide or + * nucleotide characters, followed by 'other' symbols if any. + * + * @return + */ + public String getSSForCount(int count) + { + if (count == 0) + { + return ""; + } + + /* + * find counts for the given value and append the + * corresponding symbol + */ + StringBuilder modal = new StringBuilder(); + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] == count) + { + modal.append( + SS_SYMBOLS.charAt(i - 1)); + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] == count) + { + modal.append( + SS_SYMBOLS.charAt(i - 1)); + } + } + } + if (otherData != null) + { + for (int i = 0; i < otherData.size(); i++) + { + if (otherData.valueAt(i) == count) + { + modal.append((char) otherData.keyAt(i)); + } + } + } + return modal.toString(); + } + + /** + * Returns the highest count for any symbol(s) in the profile (excluding gap) + * + * @return + */ + public int getModalCount() + { + return maxCount; + } + + /** + * Returns the number of distinct symbols with a non-zero count (excluding the + * gap symbol) + * + * @return + */ + public int size() + { + int size = 0; + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] > 0) + { + size++; + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] > 0) + { + size++; + } + } + } + + /* + * include 'other' characters recorded (even if count is zero + * though that would be a strange use case) + */ + if (otherData != null) + { + size += otherData.size(); + } + + return size; + } + + /** + * Returns a data bean holding those symbols that have a non-zero count + * (excluding the gap symbol), with their counts. + * + * @return + */ + public SymbolCounts getSymbolCounts() + { + int size = size(); + char[] symbols = new char[size]; + int[] values = new int[size]; + int j = 0; + + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] > 0) + { + char symbol = SS_SYMBOLS.charAt(i - 1); + symbols[j] = symbol; + values[j] = intCounts[i]; + j++; + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] > 0) + { + char symbol = SS_SYMBOLS.charAt(i - 1); + symbols[j] = symbol; + values[j] = counts[i]; + j++; + } + } + } + if (otherData != null) + { + for (int i = 0; i < otherData.size(); i++) + { + symbols[j] = (char) otherData.keyAt(i); + values[j] = otherData.valueAt(i); + j++; + } + } + + return new SymbolCounts(symbols, values); + } + + /** + * Returns a tooltip string showing residues in descending order of their + * percentage frequency in the profile + * + * @param normaliseBy + * the divisor for residue counts (may or may not include gapped + * sequence count) + * @param percentageDecPl + * the number of decimal places to show in percentages + * @return + */ + public String getTooltip(int normaliseBy, int percentageDecPl) + { + SymbolCounts symbolCounts = getSymbolCounts(); + char[] ca = symbolCounts.symbols; + int[] vl = symbolCounts.values; + + /* + * sort characters into ascending order of their counts + */ + QuickSort.sort(vl, ca); + + /* + * traverse in reverse order (highest count first) to build tooltip + */ + boolean first = true; + StringBuilder sb = new StringBuilder(64); + for (int c = ca.length - 1; c >= 0; c--) + { + final char residue = ca[c]; + // TODO combine residues which share a percentage + // (see AAFrequency.completeCdnaConsensus) + float tval = (vl[c] * 100f) / normaliseBy; + sb.append(first ? "" : "; ").append(residue).append(" "); + Format.appendPercentage(sb, tval, percentageDecPl); + sb.append("%"); + first = false; + } + return sb.toString(); + } + + /** + * Returns a string representation of the symbol counts, for debug purposes. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("[ "); + SymbolCounts sc = getSymbolCounts(); + for (int i = 0; i < sc.symbols.length; i++) + { + sb.append(sc.symbols[i]).append(":").append(sc.values[i]).append(" "); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/jalview/datamodel/SequenceGroup.java b/src/jalview/datamodel/SequenceGroup.java index 7e53c46..b5048a0 100755 --- a/src/jalview/datamodel/SequenceGroup.java +++ b/src/jalview/datamodel/SequenceGroup.java @@ -132,6 +132,9 @@ public class SequenceGroup implements AnnotatedCollectionI * consensus calculation property */ private boolean showSequenceLogo = false; + + + private boolean showSequenceSSLogo = false; /** * flag indicating if logo should be rendered normalised @@ -149,10 +152,15 @@ public class SequenceGroup implements AnnotatedCollectionI private boolean hidecols = false; AlignmentAnnotation consensus = null; + + + AlignmentAnnotation ssConsensus = null; AlignmentAnnotation conservation = null; private boolean showConsensusHistogram; + + private boolean showSSConsensusHistogram; private AnnotatedCollectionI context; @@ -229,6 +237,7 @@ public class SequenceGroup implements AnnotatedCollectionI showSequenceLogo = seqsel.showSequenceLogo; normaliseSequenceLogo = seqsel.normaliseSequenceLogo; showConsensusHistogram = seqsel.showConsensusHistogram; + showSSConsensusHistogram = seqsel.showSSConsensusHistogram; idColour = seqsel.idColour; outlineColour = seqsel.outlineColour; seqrep = seqsel.seqrep; @@ -610,6 +619,21 @@ public class SequenceGroup implements AnnotatedCollectionI cs.setConsensus(cnsns); upd = true; } + + + ProfilesI ssCnsns = AAFrequency.calculateSS(sequences, startRes, + endRes + 1, showSequenceLogo); + if (ssConsensus != null) + { + _updateSSConsensusRow(ssCnsns, sequences.size()); + upd = true; + } + if (cs != null) + { + cs.setSsConsensus(ssCnsns); + upd = true; + } + if ((conservation != null) || (cs != null && cs.conservationApplied())) @@ -699,6 +723,33 @@ public class SequenceGroup implements AnnotatedCollectionI // for // ignoreGapsInConsensusCalculation); } + + public ProfilesI ssConsensusData = null; + + private void _updateSSConsensusRow(ProfilesI ssCnsns, long nseq) + { + if (ssConsensus == null) + { + getSSConsensus(); + } + ssConsensus.label = "Sec Str Consensus for " + getName(); + ssConsensus.description = "Percent Identity"; + ssConsensusData = ssCnsns; + // preserve width if already set + int aWidth = (ssConsensus.annotations != null) + ? (endRes < ssConsensus.annotations.length + ? ssConsensus.annotations.length + : endRes + 1) + : endRes + 1; + ssConsensus.annotations = null; + ssConsensus.annotations = new Annotation[aWidth]; // should be alignment width + + AAFrequency.completeSSConsensus(ssConsensus, ssCnsns, startRes, endRes + 1, + ignoreGapsInConsensus, showSequenceLogo, nseq); // TODO: setting + // container + // for + // ignoreGapsInConsensusCalculation); + } /** * @param s @@ -1154,6 +1205,30 @@ public class SequenceGroup implements AnnotatedCollectionI } return consensus; } + + public AlignmentAnnotation getSSConsensus() + { + // TODO get or calculate and get consensus annotation row for this group + int aWidth = this.getWidth(); + // pointer + // possibility + // here. + if (aWidth < 0) + { + return null; + } + if (ssConsensus == null) + { + ssConsensus = new AlignmentAnnotation("", "", new Annotation[1], 0f, + 100f, AlignmentAnnotation.BAR_GRAPH); + ssConsensus.hasText = true; + ssConsensus.autoCalculated = true; + ssConsensus.groupRef = this; + ssConsensus.label = "Sec Str Consensus for " + getName(); + ssConsensus.description = "Percent Identity"; + } + return ssConsensus; + } /** * set this alignmentAnnotation object as the one used to render consensus @@ -1256,6 +1331,18 @@ public class SequenceGroup implements AnnotatedCollectionI } this.showSequenceLogo = showSequenceLogo; } + + + public void setshowSequenceSSLogo(boolean showSequenceSSLogo) + { + // TODO: decouple calculation from settings update + if (this.showSequenceSSLogo != showSequenceSSLogo && ssConsensus != null) + { + this.showSequenceSSLogo = showSequenceSSLogo; + recalcConservation(); + } + this.showSequenceSSLogo = showSequenceSSLogo; + } /** * @@ -1273,6 +1360,17 @@ public class SequenceGroup implements AnnotatedCollectionI } this.showConsensusHistogram = showConsHist; } + + public void setShowSSConsensusHistogram(boolean showSSConsHist) + { + + if (showSSConsensusHistogram != showSSConsHist && consensus != null) + { + this.showSSConsensusHistogram = showSSConsHist; + recalcConservation(); + } + this.showSSConsensusHistogram = showSSConsHist; + } /** * @return the showConsensusHistogram diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index c49626b..1bf2529 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -802,6 +802,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, } ap.av.updateConservation(ap); ap.av.updateConsensus(ap); + ap.av.updateSecondaryStructureConsensus(ap); ap.av.updateStrucConsensus(ap); } } @@ -5717,6 +5718,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, viewport.setShowConsensusHistogram(showConsensusHistogram.getState()); alignPanel.updateAnnotation(applyAutoAnnotationSettings.getState()); } + + @Override + protected void showSSConsensusHistogram_actionPerformed(ActionEvent e) + { + viewport.setShowSSConsensusHistogram(showSSConsensusHistogram.getState()); + alignPanel.updateAnnotation(applyAutoAnnotationSettings.getState()); + } /* * (non-Javadoc) diff --git a/src/jalview/gui/AlignViewport.java b/src/jalview/gui/AlignViewport.java index a8bc815..037f120 100644 --- a/src/jalview/gui/AlignViewport.java +++ b/src/jalview/gui/AlignViewport.java @@ -281,7 +281,9 @@ public class AlignViewport extends AlignmentViewport } showConsensusHistogram = Cache.getDefault("SHOW_CONSENSUS_HISTOGRAM", true); - showSequenceLogo = Cache.getDefault("SHOW_CONSENSUS_LOGO", false); + showSSConsensusHistogram = Cache.getDefault("SHOW_SSCONSENSUS_HISTOGRAM", + true); + showSequenceLogo = Cache.getDefault("SHOW_CONSENSUS_LOGO", true); normaliseSequenceLogo = Cache.getDefault("NORMALISE_CONSENSUS_LOGO", false); showGroupConsensus = Cache.getDefault("SHOW_GROUP_CONSENSUS", false); @@ -314,6 +316,7 @@ public class AlignViewport extends AlignmentViewport if (residueShading != null) { residueShading.setConsensus(hconsensus); + residueShading.setSsConsensus(hSSConsensus); } setColourAppliesToAllGroups(true); } diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 8875288..d2c00eb 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -54,6 +54,7 @@ import jalview.analysis.AAFrequency; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.AlignmentUtils; import jalview.analysis.Conservation; +import jalview.api.AlignCalcWorkerI; import jalview.api.AlignViewportI; import jalview.bin.Console; import jalview.commands.ChangeCaseCommand; @@ -87,6 +88,7 @@ import jalview.util.Platform; import jalview.util.StringUtils; import jalview.util.UrlLink; import jalview.viewmodel.seqfeatures.FeatureRendererModel; +import jalview.workers.SecondaryStructureConsensusThread; /** * The popup menu that is displayed on right-click on a sequence id, or in the @@ -1737,8 +1739,28 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { final AlignmentI alignment = this.ap.getAlignment(); AlignmentUtils.addReferenceAnnotations(candidates, alignment, null); + + if(AlignmentUtils.isSSAnnotationPresent(candidates)) { + restartSSConsensusWorker(); + } + refresh(); } + + + private void restartSSConsensusWorker() { + + List workers = ap.alignFrame.getViewport().getCalcManager() + .getRegisteredWorkersOfClass(SecondaryStructureConsensusThread.class); + if (!workers.isEmpty()) { + + ap.alignFrame.getViewport().getCalcManager().startWorker(workers.remove(0)); + + } + + } + + protected void makeReferenceSeq_actionPerformed(ActionEvent actionEvent) { diff --git a/src/jalview/jbgui/GAlignFrame.java b/src/jalview/jbgui/GAlignFrame.java index 7daeb37..da35bd4 100755 --- a/src/jalview/jbgui/GAlignFrame.java +++ b/src/jalview/jbgui/GAlignFrame.java @@ -198,7 +198,9 @@ public class GAlignFrame extends JInternalFrame protected JCheckBoxMenuItem showGroupConservation = new JCheckBoxMenuItem(); - protected JCheckBoxMenuItem showConsensusHistogram = new JCheckBoxMenuItem(); + protected JCheckBoxMenuItem showConsensusHistogram = new JCheckBoxMenuItem(); + + protected JCheckBoxMenuItem showSSConsensusHistogram = new JCheckBoxMenuItem(); protected JCheckBoxMenuItem showSequenceLogo = new JCheckBoxMenuItem(); @@ -873,6 +875,18 @@ public class GAlignFrame extends JInternalFrame showConsensusHistogram_actionPerformed(e); } + }); + showSSConsensusHistogram.setText( + MessageManager.getString("label.show_ssconsensus_histogram")); + showSSConsensusHistogram.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + showConsensusHistogram_actionPerformed(e); + } + }); showSequenceLogo .setText(MessageManager.getString("label.show_consensus_logo")); @@ -1890,6 +1904,7 @@ public class GAlignFrame extends JInternalFrame autoAnnMenu.addSeparator(); autoAnnMenu.add(applyAutoAnnotationSettings); autoAnnMenu.add(showConsensusHistogram); + autoAnnMenu.add(showSSConsensusHistogram); autoAnnMenu.add(showSequenceLogo); autoAnnMenu.add(normaliseSequenceLogo); autoAnnMenu.addSeparator(); @@ -2224,6 +2239,12 @@ public class GAlignFrame extends JInternalFrame // TODO Auto-generated method stub } + + protected void showSSConsensusHistogram_actionPerformed(ActionEvent e) + { + // TODO Auto-generated method stub + + } protected void showSequenceLogo_actionPerformed(ActionEvent e) { diff --git a/src/jalview/project/Jalview2XML.java b/src/jalview/project/Jalview2XML.java index 983b512..2f4052a 100644 --- a/src/jalview/project/Jalview2XML.java +++ b/src/jalview/project/Jalview2XML.java @@ -5309,6 +5309,8 @@ public class Jalview2XML view.isIgnoreGapsinConsensus()); viewport.getResidueShading() .setConsensus(viewport.getSequenceConsensusHash()); + viewport.getResidueShading() + .setSsConsensus(viewport.getSequenceSSConsensusHash()); if (safeBoolean(view.isConservationSelected()) && cs != null) { viewport.getResidueShading() diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index b5dac0d..015db5c 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -88,6 +88,8 @@ public class AnnotationRenderer private HiddenColumns hiddenColumns; private ProfilesI hconsensus; + + private ProfilesI hSSconsensus; private Hashtable[] complementConsensus; @@ -164,6 +166,7 @@ public class AnnotationRenderer { hiddenColumns = null; hconsensus = null; + hSSconsensus = null; complementConsensus = null; hStrucConsensus = null; fadedImage = null; @@ -377,6 +380,7 @@ public class AnnotationRenderer columnSelection = av.getColumnSelection(); hiddenColumns = av.getAlignment().getHiddenColumns(); hconsensus = av.getSequenceConsensusHash(); + hSSconsensus = av.getSequenceSSConsensusHash(); complementConsensus = av.getComplementConsensusHash(); hStrucConsensus = av.getRnaStructureConsensusHash(); av_ignoreGapsConsensus = av.isIgnoreGapsConsensus(); @@ -425,6 +429,15 @@ public class AnnotationRenderer } } } + + else if(aa.autoCalculated && aa.label.startsWith("SecondaryStructureConsensus")) + { + return AAFrequency.extractProfile( + hSSconsensus.get(column), + av_ignoreGapsConsensus); + + } + else { if (aa.autoCalculated && aa.label.startsWith("StrucConsensus")) @@ -514,6 +527,8 @@ public class AnnotationRenderer .getAlignmentStrucConsensusAnnotation(); final AlignmentAnnotation complementConsensusAnnot = av .getComplementConsensusAnnotation(); + final AlignmentAnnotation ssConsensusAnnot = av + .getAlignmentSecondaryStructureConsensusAnnotation(); BitSet graphGroupDrawn = new BitSet(); int charOffset = 0; // offset for a label @@ -540,7 +555,7 @@ public class AnnotationRenderer normaliseProfile = row.groupRef.isNormaliseSequenceLogo(); } else if (row == consensusAnnot || row == structConsensusAnnot - || row == complementConsensusAnnot) + || row == complementConsensusAnnot || row == ssConsensusAnnot) { renderHistogram = av_renderHistogram; renderProfile = av_renderProfile; @@ -1519,6 +1534,8 @@ public class AnnotationRenderer * {profile type, #values, total count, char1, pct1, char2, pct2...} */ int profl[] = getProfileFor(_aa, column); + + // just try to draw the logo if profl is not null if (profl != null && profl[2] != 0) @@ -1602,7 +1619,9 @@ public class AnnotationRenderer } else { + colour = profcolour.findColour(dc[0], column, null); + } g.setColor(colour == Color.white ? Color.lightGray : colour); diff --git a/src/jalview/renderer/ResidueShader.java b/src/jalview/renderer/ResidueShader.java index c031170..b914c65 100644 --- a/src/jalview/renderer/ResidueShader.java +++ b/src/jalview/renderer/ResidueShader.java @@ -61,6 +61,22 @@ public class ResidueShader implements ResidueShaderI * the consensus data for each column */ private ProfilesI consensus; + + /* + * the consensus data for each column + */ + private ProfilesI ssConsensus; + + + public ProfilesI getSsConsensus() + { + return ssConsensus; + } + + public void setSsConsensus(ProfilesI ssConsensus) + { + this.ssConsensus = ssConsensus; + } /* * if true, apply shading of colour by conservation @@ -128,6 +144,7 @@ public class ResidueShader implements ResidueShaderI this.conservationIncrement = rs.conservationIncrement; this.ignoreGaps = rs.ignoreGaps; this.pidThreshold = rs.pidThreshold; + this.ssConsensus = rs.ssConsensus; } /** @@ -137,6 +154,7 @@ public class ResidueShader implements ResidueShaderI public void setConsensus(ProfilesI cons) { consensus = cons; + } /** @@ -261,6 +279,36 @@ public class ResidueShader implements ResidueShaderI return colour; } + + @Override + public Color findSSColour(char symbol, int position, SequenceI seq) + { + if (colourScheme == null) + { + return Color.white; // Colour is 'None' + } + + /* + * get 'base' colour + */ + ProfileI profile = ssConsensus == null ? null : ssConsensus.get(position); + String modalSS = profile == null ? null + : profile.getModalSS(); + float pid = profile == null ? 0f + : profile.getSSPercentageIdentity(ignoreGaps); + Color colour = colourScheme.findColour(symbol, position, seq, + modalSS, pid); + + /* + * apply PID threshold and consensus fading if in force + */ + if (!Comparison.isGap(symbol)) + { + colour = adjustColour(symbol, position, colour); + } + + return colour; + } /** * Adjusts colour by applying thresholding or conservation shading, if in diff --git a/src/jalview/renderer/ResidueShaderI.java b/src/jalview/renderer/ResidueShaderI.java index 4d97171..0412d21 100644 --- a/src/jalview/renderer/ResidueShaderI.java +++ b/src/jalview/renderer/ResidueShaderI.java @@ -34,6 +34,8 @@ public interface ResidueShaderI { public abstract void setConsensus(ProfilesI cons); + + public abstract void setSsConsensus(ProfilesI cons); public abstract boolean conservationApplied(); @@ -82,4 +84,6 @@ public interface ResidueShaderI public abstract void setColourScheme(ColourSchemeI cs); + Color findSSColour(char symbol, int position, SequenceI seq); + } \ No newline at end of file diff --git a/src/jalview/viewmodel/AlignmentViewport.java b/src/jalview/viewmodel/AlignmentViewport.java index 3e1bc63..ccf6379 100644 --- a/src/jalview/viewmodel/AlignmentViewport.java +++ b/src/jalview/viewmodel/AlignmentViewport.java @@ -76,6 +76,7 @@ import jalview.viewmodel.styles.ViewStyle; import jalview.workers.AlignCalcManager; import jalview.workers.ComplementConsensusThread; import jalview.workers.ConsensusThread; +import jalview.workers.SecondaryStructureConsensusThread; import jalview.workers.StrucConsensusThread; /** @@ -697,6 +698,8 @@ public abstract class AlignmentViewport } protected AlignmentAnnotation consensus; + + protected AlignmentAnnotation secondaryStructureConsensus; protected AlignmentAnnotation complementConsensus; @@ -709,6 +712,8 @@ public abstract class AlignmentViewport protected AlignmentAnnotation quality; protected AlignmentAnnotation[] groupConsensus; + + protected AlignmentAnnotation[] groupSSConsensus; protected AlignmentAnnotation[] groupConservation; @@ -716,6 +721,10 @@ public abstract class AlignmentViewport * results of alignment consensus analysis for visible portion of view */ protected ProfilesI hconsensus = null; + + protected ProfilesI hSSConsensus = null; + + /** * results of cDNA complement consensus visible portion of view @@ -753,7 +762,13 @@ public abstract class AlignmentViewport { this.hconsensus = hconsensus; } - + + @Override + public void setSequenceSSConsensusHash(ProfilesI hSSConsensus) + { + this.hSSConsensus = hSSConsensus; + } + @Override public void setComplementConsensusHash( Hashtable[] hconsensus) @@ -766,6 +781,12 @@ public abstract class AlignmentViewport { return hconsensus; } + + @Override + public ProfilesI getSequenceSSConsensusHash() + { + return hSSConsensus; + } @Override public Hashtable[] getComplementConsensusHash() @@ -805,6 +826,14 @@ public abstract class AlignmentViewport return consensus; } + + @Override + public AlignmentAnnotation getAlignmentSecondaryStructureConsensusAnnotation() + { + return secondaryStructureConsensus; + } + + @Override public AlignmentAnnotation getAlignmentGapAnnotation() { @@ -896,6 +925,26 @@ public abstract class AlignmentViewport } } } + + + + + /** + * trigger update of consensus annotation + */ + public void updateSecondaryStructureConsensus(final AlignmentViewPanel ap) + { + // see note in mantis : issue number 8585 + if (secondaryStructureConsensus == null || !autoCalculateConsensus) + { + return; + } + if (calculator + .getRegisteredWorkersOfClass(SecondaryStructureConsensusThread.class) == null) + { + calculator.registerWorker(new SecondaryStructureConsensusThread(this, ap)); + } + } // --------START Structure Conservation public void updateStrucConsensus(final AlignmentViewPanel ap) @@ -959,6 +1008,7 @@ public abstract class AlignmentViewport consensus = null; complementConsensus = null; strucConsensus = null; + secondaryStructureConsensus = null; conservation = null; quality = null; groupConsensus = null; @@ -1005,6 +1055,8 @@ public abstract class AlignmentViewport * should consensus profile be rendered by default */ protected boolean showSequenceLogo = false; + + protected boolean showSequenceSSLogo = false; /** * should consensus profile be rendered normalised to row height @@ -1015,6 +1067,13 @@ public abstract class AlignmentViewport * should consensus histograms be rendered by default */ protected boolean showConsensusHistogram = true; + + protected boolean showSSConsensusHistogram = true; + + public void setShowSSConsensusHistogram(boolean showSSConsensusHistogram) + { + this.showSSConsensusHistogram = showSSConsensusHistogram; + } /** * @return the showConsensusProfile @@ -1024,6 +1083,12 @@ public abstract class AlignmentViewport { return showSequenceLogo; } + + @Override + public boolean isShowSequenceSSLogo() + { + return showSequenceSSLogo; + } /** * @param showSequenceLogo @@ -1042,6 +1107,18 @@ public abstract class AlignmentViewport } this.showSequenceLogo = showSequenceLogo; } + + public void setShowSequenceSSLogo(boolean showSequenceSSLogo) + { + if (showSequenceSSLogo != this.showSequenceSSLogo) + { + // TODO: decouple settings setting from calculation when refactoring + // annotation update method from alignframe to viewport + this.showSequenceSSLogo = showSequenceSSLogo; + calculator.updateAnnotationFor(SecondaryStructureConsensusThread.class); + } + this.showSequenceSSLogo = showSequenceSSLogo; + } /** * @param showConsensusHistogram @@ -1096,6 +1173,12 @@ public abstract class AlignmentViewport { return this.showConsensusHistogram; } + + @Override + public boolean isShowSSConsensusHistogram() + { + return this.showSSConsensusHistogram; + } /** * when set, updateAlignment will always ensure sequences are of equal length @@ -1247,6 +1330,7 @@ public abstract class AlignmentViewport if (ap != null) { updateConsensus(ap); + updateSecondaryStructureConsensus(ap); if (residueShading != null) { residueShading.setThreshold(residueShading.getThreshold(), @@ -1872,6 +1956,7 @@ public abstract class AlignmentViewport if (autoCalculateConsensus) { updateConsensus(ap); + updateSecondaryStructureConsensus(ap); } if (hconsensus != null && autoCalculateConsensus) { @@ -1955,13 +2040,19 @@ public abstract class AlignmentViewport consensus = new AlignmentAnnotation("Consensus", MessageManager.getString("label.consensus_descr"), new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH); + + secondaryStructureConsensus = new AlignmentAnnotation("SecondaryStructureConsensus", + MessageManager.getString("label.ssconsensus_descr"), + new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH); + initConsensus(consensus); + initSSConsensus(secondaryStructureConsensus); initGapCounts(); - initComplementConsensus(); } } + /** * If this is a protein alignment and there are mappings to cDNA, adds the * cDNA consensus annotation and returns true, else returns false. @@ -2012,6 +2103,17 @@ public abstract class AlignmentViewport alignment.addAnnotation(aa); } } + + private void initSSConsensus(AlignmentAnnotation aa) + { + aa.hasText = true; + aa.autoCalculated = true; + + if (showConsensus) + { + alignment.addAnnotation(aa); + } + } // these should be extracted from the view model - style and settings for // derived annotation @@ -2160,7 +2262,9 @@ public abstract class AlignmentViewport boolean conv = isShowGroupConservation(); boolean cons = isShowGroupConsensus(); boolean showprf = isShowSequenceLogo(); + boolean showSSprf = isShowSequenceSSLogo(); boolean showConsHist = isShowConsensusHistogram(); + boolean showSSConsHist = isShowSSConsensusHistogram(); boolean normLogo = isNormaliseSequenceLogo(); /** @@ -2197,7 +2301,9 @@ public abstract class AlignmentViewport { // set defaults for this group's conservation/consensus sg.setshowSequenceLogo(showprf); + sg.setshowSequenceSSLogo(showSSprf); sg.setShowConsensusHistogram(showConsHist); + sg.setShowSSConsensusHistogram(showSSConsHist); sg.setNormaliseSequenceLogo(normLogo); } if (conv) @@ -2209,6 +2315,7 @@ public abstract class AlignmentViewport { updateCalcs = true; alignment.addAnnotation(sg.getConsensus(), 0); + alignment.addAnnotation(sg.getSSConsensus(), 0); } // refresh the annotation rows if (updateCalcs) @@ -2998,6 +3105,41 @@ public abstract class AlignmentViewport + ((ignoreGapsInConsensusCalculation) ? " without gaps" : "")); return sq; } + + public SequenceI getSSConsensusSeq() + { + if (secondaryStructureConsensus == null) + { + updateSecondaryStructureConsensus(null); + } + if (secondaryStructureConsensus == null) + { + return null; + } + StringBuffer seqs = new StringBuffer(); + for (int i = 0; i < secondaryStructureConsensus.annotations.length; i++) + { + Annotation annotation = secondaryStructureConsensus.annotations[i]; + if (annotation != null) + { + String description = annotation.description; + if (description != null && description.startsWith("[")) + { + // consensus is a tie - just pick the first one + seqs.append(description.charAt(1)); + } + else + { + seqs.append(annotation.displayCharacter); + } + } + } + + SequenceI sq = new Sequence("Sec Str Consensus", seqs.toString()); + sq.setDescription("Percentage Identity Sec Str Consensus " + + ((ignoreGapsInConsensusCalculation) ? " without gaps" : "")); + return sq; + } @Override public void setCurrentTree(TreeModel tree) diff --git a/src/jalview/workers/AlignCalcManager.java b/src/jalview/workers/AlignCalcManager.java index cdf8ea4..d64bf4d 100644 --- a/src/jalview/workers/AlignCalcManager.java +++ b/src/jalview/workers/AlignCalcManager.java @@ -20,10 +20,6 @@ */ package jalview.workers; -import jalview.api.AlignCalcManagerI; -import jalview.api.AlignCalcWorkerI; -import jalview.datamodel.AlignmentAnnotation; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -33,6 +29,10 @@ 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 { /* diff --git a/src/jalview/workers/SecondaryStructureConsensusThread.java b/src/jalview/workers/SecondaryStructureConsensusThread.java new file mode 100644 index 0000000..04cefe5 --- /dev/null +++ b/src/jalview/workers/SecondaryStructureConsensusThread.java @@ -0,0 +1,258 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +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.Annotation; +import jalview.datamodel.ProfilesI; +import jalview.datamodel.SequenceI; +import jalview.renderer.ResidueShaderI; + +public class SecondaryStructureConsensusThread extends AlignCalcWorker +{ + public SecondaryStructureConsensusThread(AlignViewportI alignViewport, + AlignmentViewPanel alignPanel) + { + super(alignViewport, alignPanel); + } + + @Override + public void run() + { + if (calcMan.isPending(this)) + { + return; + } + calcMan.notifyStart(this); + // long started = System.currentTimeMillis(); + try + { + AlignmentAnnotation ssConsensus = getSSConsensusAnnotation(); + AlignmentAnnotation gap = getGapAnnotation(); + if ((ssConsensus == null && gap == null) || calcMan.isPending(this)) + { + calcMan.workerComplete(this); + return; + } + while (!calcMan.notifyWorking(this)) + { + try + { + if (ap != null) + { + ap.paintAlignment(false, false); + } + Thread.sleep(200); + } catch (Exception ex) + { + ex.printStackTrace(); + } + } + if (alignViewport.isClosed()) + { + abortAndDestroy(); + return; + } + AlignmentI alignment = alignViewport.getAlignment(); + + int aWidth = -1; + + if (alignment == null || (aWidth = alignment.getWidth()) < 0) + { + calcMan.workerComplete(this); + return; + } + + eraseSSConsensus(aWidth); + computeSSConsensus(alignment); + updateResultAnnotation(true); + + if (ap != null) + { + ap.paintAlignment(true, true); + } + } catch (OutOfMemoryError error) + { + calcMan.disableWorker(this); + 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 eraseSSConsensus(int aWidth) + { + AlignmentAnnotation ssConsensus = getSSConsensusAnnotation(); + if (ssConsensus != null) + { + ssConsensus.annotations = new Annotation[aWidth]; + } + AlignmentAnnotation gap = getGapAnnotation(); + if (gap != null) + { + gap.annotations = new Annotation[aWidth]; + } + } + + /** + * @param alignment + */ + protected void computeSSConsensus(AlignmentI alignment) + { + + SequenceI[] aseqs = getSequences(); + int width = alignment.getWidth(); + ProfilesI hSSConsensus = AAFrequency.calculateSS(aseqs, width, 0, width, + true); + + alignViewport.setSequenceSSConsensusHash(hSSConsensus); + setColourSchemeConsensus(hSSConsensus); + } + + /** + * @return + */ + protected SequenceI[] getSequences() + { + return alignViewport.getAlignment().getSequencesArray(); + } + + /** + * @param hconsensus + */ + protected void setColourSchemeConsensus(ProfilesI hSSconsensus) + { + ResidueShaderI cs = alignViewport.getResidueShading(); + if (cs != null) + { + cs.setSsConsensus(hSSconsensus); + } + } + + /** + * Get the Consensus annotation for the alignment + * + * @return + */ + protected AlignmentAnnotation getSSConsensusAnnotation() + { + return alignViewport.getAlignmentSecondaryStructureConsensusAnnotation(); + } + + /** + * Get the Gap annotation for the alignment + * + * @return + */ + protected AlignmentAnnotation getGapAnnotation() + { + return alignViewport.getAlignmentGapAnnotation(); + } + + /** + * update the consensus annotation from the sequence profile data using + * current visualization settings. + */ + @Override + public void updateAnnotation() + { + updateResultAnnotation(false); + } + + public void updateResultAnnotation(boolean immediate) + { + AlignmentAnnotation ssConsensus = getSSConsensusAnnotation(); + ProfilesI hSSConsensus = (ProfilesI) getViewportSSConsensus(); + if (immediate || !calcMan.isWorking(this) && ssConsensus != null + && hSSConsensus != null) + { + deriveSSConsensus(ssConsensus, hSSConsensus); + AlignmentAnnotation gap = getGapAnnotation(); + if (gap != null) + { + deriveGap(gap, hSSConsensus); + } + } + } + + /** + * Convert the computed consensus data into the desired annotation for + * display. + * + * @param consensusAnnotation + * the annotation to be populated + * @param hconsensus + * the computed consensus data + */ + protected void deriveSSConsensus(AlignmentAnnotation ssConsensusAnnotation, + ProfilesI hSSConsensus) + { + + long nseq = getSequences().length; + AAFrequency.completeSSConsensus(ssConsensusAnnotation, hSSConsensus, + hSSConsensus.getStartColumn(), hSSConsensus.getEndColumn() + 1, + alignViewport.isIgnoreGapsConsensus(), + alignViewport.isShowSequenceLogo(), nseq); + } + + /** + * Convert the computed consensus data into a gap annotation row for display. + * + * @param gapAnnotation + * the annotation to be populated + * @param hconsensus + * the computed consensus data + */ + protected void deriveGap(AlignmentAnnotation gapAnnotation, + ProfilesI hconsensus) + { + long nseq = getSequences().length; + AAFrequency.completeGapAnnot(gapAnnotation, hconsensus, + hconsensus.getStartColumn(), hconsensus.getEndColumn() + 1, + nseq); + } + + /** + * Get the consensus data stored on the viewport. + * + * @return + */ + protected Object getViewportSSConsensus() + { + // TODO convert ComplementConsensusThread to use Profile + return alignViewport.getSequenceSSConsensusHash(); + } +} diff --git a/test/jalview/renderer/ResidueShaderTest.java b/test/jalview/renderer/ResidueShaderTest.java index 78f00c8..081272a 100644 --- a/test/jalview/renderer/ResidueShaderTest.java +++ b/test/jalview/renderer/ResidueShaderTest.java @@ -30,6 +30,7 @@ import jalview.datamodel.ProfileI; import jalview.datamodel.Profiles; import jalview.datamodel.ProfilesI; import jalview.datamodel.ResidueCount; +import jalview.datamodel.SecondaryStructureCount; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; import jalview.schemes.ColourSchemeI; @@ -338,6 +339,42 @@ public class ResidueShaderTest { return 0; } + + @Override + public void setSSCounts( + SecondaryStructureCount secondaryStructureCount) + { + // TODO Auto-generated method stub + + } + + @Override + public float getSSPercentageIdentity(boolean ignoreGaps) + { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getMaxSSCount() + { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getModalSS() + { + // TODO Auto-generated method stub + return null; + } + + @Override + public SecondaryStructureCount getSSCounts() + { + // TODO Auto-generated method stub + return null; + } }; } -- 1.7.10.2