JAL-4392 Merging consensus secondary structure
authorRenia Correya <rcorreya001@dundee.ac.uk>
Tue, 30 Apr 2024 14:37:13 +0000 (15:37 +0100)
committerRenia Correya <rcorreya001@dundee.ac.uk>
Tue, 30 Apr 2024 14:37:13 +0000 (15:37 +0100)
20 files changed:
resources/lang/Messages.properties
src/jalview/analysis/AAFrequency.java
src/jalview/api/AlignViewportI.java
src/jalview/bin/Cache.java
src/jalview/datamodel/Profile.java
src/jalview/datamodel/ProfileI.java
src/jalview/datamodel/SecondaryStructureCount.java [new file with mode: 0644]
src/jalview/datamodel/SequenceGroup.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/PopupMenu.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/renderer/ResidueShader.java
src/jalview/renderer/ResidueShaderI.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/AlignCalcManager.java
src/jalview/workers/SecondaryStructureConsensusThread.java [new file with mode: 0644]
test/jalview/renderer/ResidueShaderTest.java

index c3c8589..195eb1f 100644 (file)
@@ -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 
index 6967885..796625a 100755 (executable)
@@ -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<SequenceI> list, int start,
+          int end)
+  {
+    return calculateSS(list, start, end, false);
+  }
+  
+  public static final ProfilesI calculateSS(List<SequenceI> 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()
index 0cfd03d..b7747f5 100644 (file)
@@ -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<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly);
 
   ContactMatrixI getContactMatrix(AlignmentAnnotation alignmentAnnotation);
+
+  ProfilesI getSequenceSSConsensusHash();
+
 }
index a64e869..558b859 100755 (executable)
@@ -177,6 +177,8 @@ import jalview.ws.sifts.SiftsSettings;
  * in the alignment.</li>
  * <li>SHOW_CONSENSUS_HISTOGRAM (false) Show consensus annotation row's
  * histogram.</li>
+ * <li>SHOW_SSCONSENSUS_HISTOGRAM (false) Show secondary structure consensus annotation row's
+ * histogram.</li>
  * <li>SHOW_CONSENSUS_LOGO (false) Show consensus annotation row's sequence
  * logo.</li>
  * <li>NORMALISE_CONSENSUS_LOGO (false) Show consensus annotation row's sequence
index 8638896..35f1429 100644 (file)
@@ -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()
index 65a5c0d..511095e 100644 (file)
@@ -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 (file)
index 0000000..52e9746
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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();
+  }
+}
index 7e53c46..b5048a0 100755 (executable)
@@ -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
index c49626b..1bf2529 100644 (file)
@@ -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)
index a8bc815..037f120 100644 (file)
@@ -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);
   }
index 8875288..d2c00eb 100644 (file)
@@ -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<AlignCalcWorkerI> 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)
   {
index 7daeb37..da35bd4 100755 (executable)
@@ -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)
   {
index 983b512..2f4052a 100644 (file)
@@ -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()
index b5dac0d..015db5c 100644 (file)
@@ -88,6 +88,8 @@ public class AnnotationRenderer
   private HiddenColumns hiddenColumns;
 
   private ProfilesI hconsensus;
+  
+  private ProfilesI hSSconsensus;
 
   private Hashtable<String, Object>[] 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);
 
index c031170..b914c65 100644 (file)
@@ -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
index 4d97171..0412d21 100644 (file)
@@ -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
index 3e1bc63..ccf6379 100644 (file)
@@ -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<String, Object>[] hconsensus)
@@ -766,6 +781,12 @@ public abstract class AlignmentViewport
   {
     return hconsensus;
   }
+  
+  @Override
+  public ProfilesI getSequenceSSConsensusHash()
+  {
+    return hSSConsensus;
+  }
 
   @Override
   public Hashtable<String, Object>[] 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)
index cdf8ea4..d64bf4d 100644 (file)
  */
 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 (file)
index 0000000..04cefe5
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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();
+  }
+}
index 78f00c8..081272a 100644 (file)
@@ -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;
+          }
         };
       }