Merge branch 'JAL-2075_features_hiddeng' into develop
authorJim Procter <jprocter@issues.jalview.org>
Sun, 24 Apr 2016 10:28:48 +0000 (11:28 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Sun, 24 Apr 2016 10:28:48 +0000 (11:28 +0100)
23 files changed:
examples/groovy/featureCounter.groovy [new file with mode: 0644]
examples/groovy/multipleFeatureAnnotations.groovy [new file with mode: 0644]
help/helpTOC.xml
resources/lang/Messages.properties
src/jalview/api/AlignCalcWorkerI.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/Desktop.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/util/ColorUtils.java
src/jalview/workers/AlignCalcWorker.java
src/jalview/workers/AlignmentAnnotationFactory.java [new file with mode: 0644]
src/jalview/workers/AnnotationProviderI.java [new file with mode: 0644]
src/jalview/workers/AnnotationWorker.java [new file with mode: 0644]
src/jalview/workers/ColumnCounterWorker.java [new file with mode: 0644]
src/jalview/workers/ConsensusThread.java
src/jalview/workers/ConservationThread.java
src/jalview/workers/FeatureCounterI.java [new file with mode: 0644]
src/jalview/workers/StrucConsensusThread.java
src/jalview/ws/jws2/AADisorderClient.java
src/jalview/ws/jws2/JPred301Client.java
src/jalview/ws/jws2/RNAalifoldClient.java
test/jalview/analysis/scoremodels/FeatureScoreModelTest.java
test/jalview/util/ColorUtilsTest.java

diff --git a/examples/groovy/featureCounter.groovy b/examples/groovy/featureCounter.groovy
new file mode 100644 (file)
index 0000000..08d038d
--- /dev/null
@@ -0,0 +1,87 @@
+import jalview.workers.FeatureCounterI;
+import jalview.workers.AlignmentAnnotationFactory;
+
+/*
+ * Example script that registers two alignment annotation calculators
+ * - one that counts residues in a column with Pfam annotation
+ * - one that counts only charged residues with Pfam annotation
+ * Modify this example as required to count by column any desired value that can be 
+ * derived from the residue and sequence features at each position of an alignment.
+ */
+
+/*
+ * A closure that returns true for any Charged residue
+ */
+def isCharged = { residue ->
+    switch(residue) {
+        case ['D', 'd', 'E', 'e', 'H', 'h', 'K', 'k', 'R', 'r']:
+            return true
+    }
+    false
+} 
+
+/*
+ * A closure that returns 1 if sequence features include type 'Pfam', else 0
+ * Argument should be a list of SequenceFeature 
+ */
+def hasPfam = { features -> 
+    for (sf in features)
+    {
+        /*
+         * Here we inspect the type of the sequence feature.
+         * You can also test sf.description, sf.score, sf.featureGroup,
+         * sf.strand, sf.phase, sf.begin, sf.end
+         * or sf.getValue(attributeName) for GFF 'column 9' properties
+         */
+        if ("Pfam".equals(sf.type))
+        {
+            return true
+        }
+    }
+    false
+}
+
+/*
+ * Closure that counts residues with a Pfam feature annotation
+ * Parameters are
+ * - the name (label) for the alignment annotation
+ * - the description (tooltip) for the annotation
+ * - a closure (groovy function) that tests whether to include a residue
+ * - a closure that tests whether to increment count based on sequence features  
+ */
+def getColumnCounter = { name, desc, residueTester, featureCounter ->
+    [
+     getName: { name }, 
+     getDescription: { desc },
+     getMinColour: { [0, 255, 255] }, // cyan
+     getMaxColour: { [0, 0, 255] }, // blue
+     count: 
+         { res, feats -> 
+            def c = 0
+            if (residueTester.call(res))
+            {
+                if (featureCounter.call(feats))
+                {
+                    c++
+                }
+            }
+            c
+         }
+     ] as FeatureCounterI
+}
+
+/*
+ * Define annotation that counts any residue with Pfam domain annotation
+ */
+def pfamAnnotation = getColumnCounter("Pfam", "Count of residues with Pfam domain annotation", {true}, hasPfam)
+
+/*
+ * Define annotation that counts charged residues with Pfam domain annotation
+ */
+def chargedPfamAnnotation = getColumnCounter("Pfam charged", "Count of charged residues with Pfam domain annotation", isCharged, hasPfam)
+
+/*
+ * Register the annotations
+ */
+AlignmentAnnotationFactory.newCalculator(pfamAnnotation) 
+AlignmentAnnotationFactory.newCalculator(chargedPfamAnnotation)
diff --git a/examples/groovy/multipleFeatureAnnotations.groovy b/examples/groovy/multipleFeatureAnnotations.groovy
new file mode 100644 (file)
index 0000000..592c7f5
--- /dev/null
@@ -0,0 +1,110 @@
+import jalview.workers.AlignmentAnnotationFactory;
+import jalview.workers.AnnotationProviderI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
+import jalview.util.ColorUtils;
+import jalview.util.Comparison;
+import java.awt.Color;
+
+/*
+ * Example script to compute two alignment annotations
+ * - count of Phosphorylation features
+ * - count of Turn features
+ * To try this, first load example file uniref50.fa and load on features file
+ * exampleFeatures.txt, before running this script
+ *
+ * The script only needs to be run once - it will be registered by Jalview
+ * and recalculated automatically when the alignment changes.
+ */
+
+/*
+ * A closure that returns true if value includes "PHOSPHORYLATION"
+ */
+def phosCounter = { type ->    type.contains("PHOSPHORYLATION") }
+
+/*
+ * A closure that returns true if value includes "TURN"
+ */
+def turnCounter = { type ->    type.contains("TURN") }
+
+/*
+ * A closure that computes and returns an array of Annotation values,
+ * one for each column of the alignment
+ */
+def getAnnotations(al, fr, counter) 
+{
+    def width = al.width
+    def counts = new int[width] 
+    def max = 0
+    
+    /*
+     * count features in each column, record the maximum value
+     */
+    for (col = 0 ; col < width ; col++)
+    {
+        def count = 0
+        for (row = 0 ; row < al.height ; row++)
+        {
+            seq = al.getSequenceAt(row)
+            if (seq != null && col < seq.getLength())
+            {
+                def res = seq.getCharAt(col)
+                if (!Comparison.isGap(res))
+                {
+                    pos = seq.findPosition(col)
+                    features = fr.findFeaturesAtRes(seq, pos)
+                    for (feature in features)
+                    {
+                        if (counter.call(feature.type))
+                        {
+                            count++
+                        }
+                    }
+                }
+            }
+        }
+        counts[col] = count
+        if (count > max)
+        {
+            max = count
+        }
+    }
+    
+    /*
+     * make the Annotation objects, with a graduated colour scale 
+     * (from min value to max value) for the histogram bars 
+     */
+    def zero = '0' as char
+    def anns = new Annotation[width] 
+    for (col = 0 ; col < width ; col++)
+    {
+        def c = counts[col]
+        if (c > 0)
+        {
+            Color color = ColorUtils.getGraduatedColour(c, 0, Color.cyan,
+                max, Color.blue)
+            anns[col] = AlignmentAnnotationFactory.newAnnotation(String.valueOf(c),
+                String.valueOf(c), zero, c, color)
+        }
+    }
+    anns
+}
+
+/*
+ * Define the method that performs the calculations, and builds two
+ * AlignmentAnnotation objects
+ */
+def annotator = 
+    [ calculateAnnotation: { al, fr ->
+        def phosAnns = getAnnotations(al, fr, phosCounter)
+        def ann1 = AlignmentAnnotationFactory.newAlignmentAnnotation("Phosphorylation", "Count of Phosphorylation features", phosAnns)
+        def turnAnns = getAnnotations(al, fr, turnCounter)
+        def ann2 = AlignmentAnnotationFactory.newAlignmentAnnotation("Turn", "Count of Turn features", turnAnns)
+        return [ann1, ann2]
+      } 
+    ] as AnnotationProviderI
+    
+/*
+ * Register the annotation calculator with Jalview
+ */
+AlignmentAnnotationFactory.newCalculator(annotator) 
index 2594738..fe8e1a9 100755 (executable)
@@ -27,7 +27,7 @@
                                <tocitem text="PDB Sequence Fetcher" target="pdbfetcher" />
                                <tocitem text="PDB Structure Chooser" target="pdbchooser" />
                                <tocitem text="Chimera Viewer" target="chimera" />
-                               <tocitem text="Select columns by annotation" target="selectcolbyannot" />
+                               <tocitem text="Select Columns by Annotation" target="selectcolbyannot" />
                                <tocitem text="Latest Release Notes" target="release"/>
                </tocitem>
                
@@ -49,7 +49,7 @@
                <tocitem text="Viewing Trees" target="treeviewer" expand="false" />
                <tocitem text="Fetching Sequences" target="seqfetch" />         
                
-               <tocitem text="Select columns by annotation" target="selectcolbyannot" />
+               <tocitem text="Select Columns by Annotation" target="selectcolbyannot" />
                
                <tocitem text="Nucleic Acid Support" target="nucleicAcids" expand="false">
                        <tocitem text="Viewing RNA structure" target="varna" />
                        <tocitem text="Consensus" target="calcs.consensus" />
                        <tocitem text="RNA Structure Consensus" target="calcs.alstrconsensus" />
                        <tocitem text="Annotations File Format" target="annotations.fileformat" />
-                       <tocitem text="Select Column by Annotation" target="calcs.annotation" />
+                       <tocitem text="Select Columns by Annotation" target="selectcolbyannot" />
                </tocitem>
                <tocitem text="3D Structure Data" target="viewingpdbs" expand="false">
                        <tocitem text="PDB Sequence Fetcher" target="pdbfetcher" />
index 05505a4..bc6def9 100644 (file)
@@ -1292,3 +1292,6 @@ status.cancelled_image_export_operation = Cancelled {0} export operation.
 info.error_creating_file = Error creating {0} file.
 exception.outofmemory_loading_mmcif_file = Out of memory loading mmCIF File
 info.error_creating_file = Error creating {0} file.
+label.run_groovy = Run Groovy console script
+label.run_groovy_tip = Run the script in the Groovy console over this alignment
+label.couldnt_run_groovy_script = Failed to run Groovy script
index 872528b..06dc054 100644 (file)
@@ -22,12 +22,30 @@ package jalview.api;
 
 import jalview.datamodel.AlignmentAnnotation;
 
+/**
+ * Interface describing a worker that calculates alignment annotation(s). The
+ * main (re-)calculation should be performed by the inherited run() method.
+ */
 public interface AlignCalcWorkerI extends Runnable
 {
-
+  /**
+   * Answers true if this worker updates the given annotation (regardless of its
+   * current state)
+   * 
+   * @param annot
+   * @return
+   */
   public boolean involves(AlignmentAnnotation annot);
 
+  /**
+   * Updates the display of calculated annotation values (does not recalculate
+   * the values). This allows for quick redraw of annotations when display
+   * settings are changed.
+   */
   public void updateAnnotation();
 
-  void removeOurAnnotation();
+  /**
+   * Removes any annotation managed by this worker from the alignment
+   */
+  void removeAnnotation();
 }
index 28ff0a1..67152f2 100644 (file)
@@ -119,6 +119,8 @@ import java.awt.dnd.DropTargetEvent;
 import java.awt.dnd.DropTargetListener;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.KeyAdapter;
@@ -129,6 +131,7 @@ import java.awt.print.PageFormat;
 import java.awt.print.PrinterJob;
 import java.beans.PropertyChangeEvent;
 import java.io.File;
+import java.lang.reflect.Method;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -466,6 +469,14 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       formatMenu.add(vsel);
     }
+    addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusGained(FocusEvent e)
+      {
+        Desktop.setCurrentAlignFrame(AlignFrame.this);
+      }
+    });
 
   }
 
@@ -911,10 +922,21 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             .setSelected(av.getGlobalColourScheme() instanceof jalview.schemes.RNAHelicesColour);
 
     showProducts.setEnabled(canShowProducts());
+    setGroovyEnabled(Desktop.getGroovyConsole() != null);
 
     updateEditMenuBar();
   }
 
+  /**
+   * Set the enabled state of the 'Run Groovy' option in the Calculate menu
+   * 
+   * @param b
+   */
+  public void setGroovyEnabled(boolean b)
+  {
+    runGroovy.setEnabled(b);
+  }
+
   private IProgressIndicator progressBar;
 
   /*
@@ -6090,6 +6112,45 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       return;
     }
   }
+
+  /**
+   * Try to run a script in the Groovy console, having first ensured that this
+   * AlignFrame is set as currentAlignFrame in Desktop, to allow the script to
+   * be targeted at this alignment.
+   */
+  @Override
+  protected void runGroovy_actionPerformed()
+  {
+    Desktop.setCurrentAlignFrame(this);
+    Object console = Desktop.instance.getGroovyConsole();
+    if (console != null)
+    {
+      /*
+       * use reflection here to avoid compile-time dependency
+       * on Groovy libraries
+       */
+      try
+      {
+        Class<?> gcClass = getClass().getClassLoader().loadClass(
+                "groovy.ui.Console");
+        Method runScript = gcClass.getMethod("runScript");
+        runScript.invoke(console);
+      } catch (Exception ex)
+      {
+        System.err.println((ex.toString()));
+        JOptionPane
+                .showInternalMessageDialog(Desktop.desktop, MessageManager
+                        .getString("label.couldnt_run_groovy_script"),
+                        MessageManager
+                                .getString("label.groovy_support_failed"),
+                        JOptionPane.ERROR_MESSAGE);
+      }
+    }
+    else
+    {
+      System.err.println("Can't run Groovy script as console not found");
+    }
+  }
 }
 
 class PrintThread extends Thread
index 27f60ea..b7c4098 100644 (file)
@@ -2507,9 +2507,21 @@ public class Desktop extends jalview.jbgui.GDesktop implements
       java.lang.reflect.Method setvar = gcClass.getMethod("setVariable",
               new Class[] { String.class, Object.class });
       java.lang.reflect.Method run = gcClass.getMethod("run");
-      Object gc = gccons.newInstance();
-      setvar.invoke(gc, new Object[] { "Jalview", this });
-      run.invoke(gc);
+      groovyConsole = gccons.newInstance();
+      setvar.invoke(groovyConsole, new Object[] { "Jalview", this });
+      run.invoke(groovyConsole);
+      /*
+       * and rebuild alignframe menus to enable 'Run Groovy'
+       */
+
+      AlignFrame[] alignFrames = getAlignFrames();
+      if (alignFrames != null)
+      {
+        for (AlignFrame af : alignFrames)
+        {
+          af.setGroovyEnabled(true);
+        }
+      }
     } catch (Exception ex)
     {
       jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex);
@@ -2917,6 +2929,13 @@ public class Desktop extends jalview.jbgui.GDesktop implements
    */
   private java.util.concurrent.Semaphore block = new Semaphore(0);
 
+  /*
+   * groovy.ui.Console object - if Groovy jars are present and the 
+   * user has activated the Groovy console. Use via reflection to
+   * avoid compile-time dependency on Groovy libraries.
+   */
+  private static Object groovyConsole;
+
   /**
    * add another dialog thread to the queue
    * 
@@ -3145,4 +3164,9 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     Desktop.currentAlignFrame = currentAlignFrame;
   }
 
+  public static Object getGroovyConsole()
+  {
+    return groovyConsole;
+  }
+
 }
index 205b9c6..1eb558e 100755 (executable)
@@ -179,6 +179,8 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenu showProducts = new JMenu();
 
+  protected JMenuItem runGroovy = new JMenuItem();
+
   protected JMenuItem rnahelicesColour = new JMenuItem();
 
   protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
@@ -1726,6 +1728,17 @@ public class GAlignFrame extends JInternalFrame
     // for show products actions see AlignFrame.canShowProducts
     showProducts.setText(MessageManager.getString("label.get_cross_refs"));
 
+    runGroovy.setText(MessageManager.getString("label.run_groovy"));
+    runGroovy.setToolTipText(MessageManager.getString("label.run_groovy_tip"));
+    runGroovy.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        runGroovy_actionPerformed();
+      }
+    });
+
     JMenuItem openFeatureSettings = new JMenuItem(
             MessageManager.getString("action.feature_settings"));
     openFeatureSettings.addActionListener(new ActionListener()
@@ -2160,6 +2173,7 @@ public class GAlignFrame extends JInternalFrame
     alignFrameMenuBar.add(colourMenu);
     alignFrameMenuBar.add(calculateMenu);
     alignFrameMenuBar.add(webService);
+
     fileMenu.add(fetchSequence);
     fileMenu.add(addSequenceMenu);
     fileMenu.add(reload);
@@ -2178,6 +2192,9 @@ public class GAlignFrame extends JInternalFrame
     fileMenu.add(associatedData);
     fileMenu.addSeparator();
     fileMenu.add(closeMenuItem);
+
+    pasteMenu.add(pasteNew);
+    pasteMenu.add(pasteThis);
     editMenu.add(undoMenuItem);
     editMenu.add(redoMenuItem);
     editMenu.add(cut);
@@ -2198,6 +2215,13 @@ public class GAlignFrame extends JInternalFrame
     // editMenu.addSeparator();
     editMenu.add(padGapsMenuitem);
 
+    showMenu.add(showAllColumns);
+    showMenu.add(showAllSeqs);
+    showMenu.add(showAllhidden);
+    hideMenu.add(hideSelColumns);
+    hideMenu.add(hideSelSequences);
+    hideMenu.add(hideAllSelection);
+    hideMenu.add(hideAllButSelection);
     viewMenu.add(newView);
     viewMenu.add(expandViews);
     viewMenu.add(gatherViews);
@@ -2268,6 +2292,12 @@ public class GAlignFrame extends JInternalFrame
     colourMenu.add(modifyPID);
     colourMenu.add(annotationColour);
     colourMenu.add(rnahelicesColour);
+
+    sort.add(sortIDMenuItem);
+    sort.add(sortLengthMenuItem);
+    sort.add(sortGroupMenuItem);
+    sort.add(sortPairwiseMenuItem);
+    sort.add(sortByTreeMenu);
     calculateMenu.add(sort);
     calculateMenu.add(calculateTree);
     calculateMenu.addSeparator();
@@ -2281,17 +2311,14 @@ public class GAlignFrame extends JInternalFrame
     calculateMenu.add(autoCalculate);
     calculateMenu.add(sortByTree);
     calculateMenu.addSeparator();
+    calculateMenu.add(expandAlignment);
     calculateMenu.add(extractScores);
+    calculateMenu.addSeparator();
+    calculateMenu.add(runGroovy);
+
     webServiceNoServices = new JMenuItem(
             MessageManager.getString("label.no_services"));
     webService.add(webServiceNoServices);
-    pasteMenu.add(pasteNew);
-    pasteMenu.add(pasteThis);
-    sort.add(sortIDMenuItem);
-    sort.add(sortLengthMenuItem);
-    sort.add(sortGroupMenuItem);
-    sort.add(sortPairwiseMenuItem);
-    sort.add(sortByTreeMenu);
     exportImageMenu.add(htmlMenuItem);
     exportImageMenu.add(epsFile);
     exportImageMenu.add(createPNG);
@@ -2303,13 +2330,6 @@ public class GAlignFrame extends JInternalFrame
     this.getContentPane().add(statusPanel, java.awt.BorderLayout.SOUTH);
     statusPanel.add(statusBar, null);
     this.getContentPane().add(tabbedPane, java.awt.BorderLayout.CENTER);
-    showMenu.add(showAllColumns);
-    showMenu.add(showAllSeqs);
-    showMenu.add(showAllhidden);
-    hideMenu.add(hideSelColumns);
-    hideMenu.add(hideSelSequences);
-    hideMenu.add(hideAllSelection);
-    hideMenu.add(hideAllButSelection);
 
     formatMenu.add(font);
     formatMenu.addSeparator();
@@ -2337,7 +2357,6 @@ public class GAlignFrame extends JInternalFrame
     selectMenu.add(grpsFromSelection);
     selectMenu.add(deleteGroups);
     selectMenu.add(annotationColumn);
-    calculateMenu.add(expandAlignment);
     // TODO - determine if the listenToViewSelections button is needed : see bug
     // JAL-574
     // selectMenu.addSeparator();
@@ -2355,6 +2374,14 @@ public class GAlignFrame extends JInternalFrame
   }
 
   /**
+   * Try to run script in a Groovy console, having first ensured that this
+   * alignframe is set as currentAlignFrame in Desktop
+   */
+  protected void runGroovy_actionPerformed()
+  {
+
+  }
+  /**
    * Adds the given action listener and key accelerator to the given menu item.
    * Also saves in a lookup table to support lookup of action by key stroke.
    * 
index b8cd563..31d1ded 100644 (file)
@@ -103,4 +103,43 @@ public class ColorUtils
     return col == null ? null : col.brighter().brighter().brighter();
   }
 
+  /**
+   * Returns a color between minColour and maxColour; the RGB values are in
+   * proportion to where 'value' lies between minValue and maxValue
+   * 
+   * @param value
+   * @param minValue
+   * @param minColour
+   * @param maxValue
+   * @param maxColour
+   * @return
+   */
+  public static Color getGraduatedColour(float value, float minValue,
+          Color minColour, float maxValue, Color maxColour)
+  {
+    if (minValue == maxValue)
+    {
+      return minColour;
+    }
+    if (value < minValue)
+    {
+      value = minValue;
+    }
+    if (value > maxValue)
+    {
+      value = maxValue;
+    }
+
+    /*
+     * prop = proportion of the way value is from minValue to maxValue
+     */
+    float prop = (value - minValue) / (maxValue - minValue);
+    float r = minColour.getRed() + prop
+            * (maxColour.getRed() - minColour.getRed());
+    float g = minColour.getGreen() + prop
+            * (maxColour.getGreen() - minColour.getGreen());
+    float b = minColour.getBlue() + prop
+            * (maxColour.getBlue() - minColour.getBlue());
+    return new Color(r / 255, g / 255, b / 255);
+  }
 }
index bca3145..48e3604 100644 (file)
@@ -46,7 +46,7 @@ public abstract class AlignCalcWorker implements AlignCalcWorkerI
 
   protected AlignmentViewPanel ap;
 
-  protected List<AlignmentAnnotation> ourAnnots = null;
+  protected List<AlignmentAnnotation> ourAnnots;
 
   public AlignCalcWorker(AlignViewportI alignViewport,
           AlignmentViewPanel alignPanel)
@@ -68,17 +68,18 @@ public abstract class AlignCalcWorker implements AlignCalcWorkerI
 
   }
 
+  @Override
   public boolean involves(AlignmentAnnotation i)
   {
     return ourAnnots != null && ourAnnots.contains(i);
   }
 
   /**
-   * permanently remove from the alignment all annotation rows managed by this
+   * Permanently removes from the alignment all annotation rows managed by this
    * worker
    */
   @Override
-  public void removeOurAnnotation()
+  public void removeAnnotation()
   {
     if (ourAnnots != null && alignViewport != null)
     {
@@ -90,6 +91,7 @@ public abstract class AlignCalcWorker implements AlignCalcWorkerI
           alignment.deleteAnnotation(aa, true);
         }
       }
+      ourAnnots.clear();
     }
   }
   // TODO: allow GUI to query workers associated with annotation to add items to
diff --git a/src/jalview/workers/AlignmentAnnotationFactory.java b/src/jalview/workers/AlignmentAnnotationFactory.java
new file mode 100644 (file)
index 0000000..37f3ca5
--- /dev/null
@@ -0,0 +1,117 @@
+package jalview.workers;
+
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+
+import java.awt.Color;
+
+/**
+ * Factory class with methods which allow clients (including external scripts
+ * such as Groovy) to 'register and forget' an alignment annotation calculator. <br>
+ * Currently supports two flavours of calculator:
+ * <ul>
+ * <li>a 'feature counter' which can count any desired property derivable from
+ * residue value and any sequence features at each position of the alignment</li>
+ * <li>a 'general purpose' calculator which computes one more complete
+ * AlignmentAnnotation objects</li>
+ * </ul>
+ */
+public class AlignmentAnnotationFactory
+{
+  /**
+   * Constructs and registers a new alignment annotation worker
+   * 
+   * @param counter
+   *          provider of feature counts per alignment position
+   */
+  public static void newCalculator(FeatureCounterI counter)
+  {
+    if (Desktop.getCurrentAlignFrame() != null)
+    {
+      newCalculator(Desktop.getCurrentAlignFrame(), counter);
+    }
+    else
+    {
+      System.err
+              .println("Can't register calculator as no alignment window has focus");
+    }
+  }
+
+  /**
+   * Constructs and registers a new alignment annotation worker
+   * 
+   * @param af
+   *          the AlignFrame for which the annotation is to be calculated
+   * @param counter
+   *          provider of feature counts per alignment position
+   */
+  public static void newCalculator(AlignFrame af, FeatureCounterI counter)
+  {
+    new ColumnCounterWorker(af, counter);
+  }
+
+  /**
+   * Constructs and registers a new alignment annotation worker
+   * 
+   * @param calculator
+   *          provider of AlignmentAnnotation for the alignment
+   */
+  public static void newCalculator(AnnotationProviderI calculator)
+  {
+    if (Desktop.getCurrentAlignFrame() != null)
+    {
+      newCalculator(Desktop.getCurrentAlignFrame(), calculator);
+    }
+    else
+    {
+      System.err
+              .println("Can't register calculator as no alignment window has focus");
+    }
+  }
+
+  /**
+   * Constructs and registers a new alignment annotation worker
+   * 
+   * @param af
+   *          the AlignFrame for which the annotation is to be calculated
+   * @param calculator
+   *          provider of AlignmentAnnotation for the alignment
+   */
+  public static void newCalculator(AlignFrame af,
+          AnnotationProviderI calculator)
+  {
+    new AnnotationWorker(af, calculator);
+  }
+
+  /**
+   * Factory method to construct an Annotation object
+   * 
+   * @param displayChar
+   * @param desc
+   * @param secondaryStructure
+   * @param val
+   * @param color
+   * @return
+   */
+  public static Annotation newAnnotation(String displayChar, String desc,
+          char secondaryStructure, float val, Color color)
+  {
+    return new Annotation(displayChar, desc, secondaryStructure, val, color);
+  }
+
+  /**
+   * Factory method to construct an AlignmentAnnotation object
+   * 
+   * @param name
+   * @param desc
+   * @param anns
+   * @return
+   */
+  public static AlignmentAnnotation newAlignmentAnnotation(String name,
+          String desc, Annotation[] anns)
+  {
+    return new AlignmentAnnotation(name, desc, anns);
+  }
+}
diff --git a/src/jalview/workers/AnnotationProviderI.java b/src/jalview/workers/AnnotationProviderI.java
new file mode 100644 (file)
index 0000000..653ff04
--- /dev/null
@@ -0,0 +1,17 @@
+package jalview.workers;
+
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.gui.FeatureRenderer;
+
+import java.util.List;
+
+/**
+ * Interface to be satisfied by any class which computes one or more alignment
+ * annotations
+ */
+public interface AnnotationProviderI
+{
+  List<AlignmentAnnotation> calculateAnnotation(AlignmentI al,
+          FeatureRenderer fr);
+}
diff --git a/src/jalview/workers/AnnotationWorker.java b/src/jalview/workers/AnnotationWorker.java
new file mode 100644 (file)
index 0000000..fbf7531
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignmentPanel;
+import jalview.gui.FeatureRenderer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to create and update one or more alignment annotations, given a
+ * 'calculator'.
+ * 
+ */
+class AnnotationWorker extends AlignCalcWorker
+{
+  /*
+   * the provider of the annotation calculations
+   */
+  AnnotationProviderI counter;
+
+  /**
+   * Constructor
+   * 
+   * @param af
+   * @param counter
+   */
+  public AnnotationWorker(AlignFrame af, AnnotationProviderI counter)
+  {
+    super(af.getViewport(), af.alignPanel);
+    ourAnnots = new ArrayList<AlignmentAnnotation>();
+    this.counter = counter;
+    calcMan.registerWorker(this);
+  }
+
+  @Override
+  public void run()
+  {
+    try
+    {
+      calcMan.notifyStart(this);
+
+      while (!calcMan.notifyWorking(this))
+      {
+        try
+        {
+          Thread.sleep(200);
+        } catch (InterruptedException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+      if (alignViewport.isClosed())
+      {
+        abortAndDestroy();
+        return;
+      }
+
+      removeAnnotations();
+      AlignmentI alignment = alignViewport.getAlignment();
+      if (alignment != null)
+      {
+        try
+        {
+          List<AlignmentAnnotation> anns = counter.calculateAnnotation(
+                  alignment, new FeatureRenderer((AlignmentPanel) ap));
+          for (AlignmentAnnotation ann : anns)
+          {
+            ann.showAllColLabels = true;
+            ann.graph = AlignmentAnnotation.BAR_GRAPH;
+            ourAnnots.add(ann);
+            alignment.addAnnotation(ann);
+          }
+        } catch (IndexOutOfBoundsException x)
+        {
+          // probable race condition. just finish and return without any fuss.
+          return;
+        }
+      }
+    } catch (OutOfMemoryError error)
+    {
+      ap.raiseOOMWarning("calculating annotations", error);
+      calcMan.workerCannotRun(this);
+    } finally
+    {
+      calcMan.workerComplete(this);
+    }
+
+    if (ap != null)
+    {
+      ap.adjustAnnotationHeight();
+      ap.paintAlignment(true);
+    }
+
+  }
+
+  /**
+   * Remove all our annotations before re-calculating them
+   */
+  void removeAnnotations()
+  {
+    for (AlignmentAnnotation ann : ourAnnots)
+    {
+      alignViewport.getAlignment().deleteAnnotation(ann);
+    }
+    ourAnnots.clear();
+  }
+
+  @Override
+  public void updateAnnotation()
+  {
+    // do nothing
+  }
+}
diff --git a/src/jalview/workers/ColumnCounterWorker.java b/src/jalview/workers/ColumnCounterWorker.java
new file mode 100644 (file)
index 0000000..6f4a4f3
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * 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.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignmentPanel;
+import jalview.gui.FeatureRenderer;
+import jalview.util.ColorUtils;
+import jalview.util.Comparison;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to compute an alignment annotation with column counts of any
+ * properties of interest of positions in an alignment. <br>
+ * This is designed to be extensible, by supplying to the constructor an object
+ * that computes a count for each residue position, based on the residue value
+ * and any sequence features at that position.
+ * 
+ */
+class ColumnCounterWorker extends AlignCalcWorker
+{
+  FeatureCounterI counter;
+
+  /**
+   * Constructor registers the annotation for the given alignment frame
+   * 
+   * @param af
+   * @param counter
+   */
+  public ColumnCounterWorker(AlignFrame af, FeatureCounterI counter)
+  {
+    super(af.getViewport(), af.alignPanel);
+    ourAnnots = new ArrayList<AlignmentAnnotation>();
+    this.counter = counter;
+    calcMan.registerWorker(this);
+  }
+
+  /**
+   * method called under control of AlignCalcManager to recompute the annotation
+   * when the alignment changes
+   */
+  @Override
+  public void run()
+  {
+    try
+    {
+      calcMan.notifyStart(this);
+
+      while (!calcMan.notifyWorking(this))
+      {
+        try
+        {
+          Thread.sleep(200);
+        } catch (InterruptedException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+      if (alignViewport.isClosed())
+      {
+        abortAndDestroy();
+        return;
+      }
+
+      removeAnnotation();
+      if (alignViewport.getAlignment() != null)
+      {
+        try
+        {
+          computeAnnotations();
+        } catch (IndexOutOfBoundsException x)
+        {
+          // probable race condition. just finish and return without any fuss.
+          return;
+        }
+      }
+    } catch (OutOfMemoryError error)
+    {
+      ap.raiseOOMWarning("calculating feature counts", error);
+      calcMan.workerCannotRun(this);
+    } finally
+    {
+      calcMan.workerComplete(this);
+    }
+
+    if (ap != null)
+    {
+      ap.adjustAnnotationHeight();
+      ap.paintAlignment(true);
+    }
+
+  }
+
+  /**
+   * Scan each column of the alignment to calculate a count by feature type. Set
+   * the count as the value of the alignment annotation for that feature type.
+   */
+  void computeAnnotations()
+  {
+    FeatureRenderer fr = new FeatureRenderer((AlignmentPanel) ap);
+    // TODO use the commented out code once JAL-2075 is fixed
+    // to get adequate performance on genomic length sequence
+    AlignmentI alignment = alignViewport.getAlignment();
+    // AlignmentView alignmentView = alignViewport.getAlignmentView(false);
+    // AlignmentI alignment = alignmentView.getVisibleAlignment(' ');
+
+    // int width = alignmentView.getWidth();
+    int width = alignment.getWidth();
+    int height = alignment.getHeight();
+    int[] counts = new int[width];
+    int max = 0;
+
+    for (int col = 0; col < width; col++)
+    {
+      int count = 0;
+      for (int row = 0; row < height; row++)
+      {
+        count += countFeaturesAt(alignment, col, row, fr);
+      }
+      counts[col] = count;
+      max = Math.max(count, max);
+    }
+
+    Annotation[] anns = new Annotation[width];
+    /*
+     * add non-zero counts as annotations
+     */
+    for (int i = 0; i < counts.length; i++)
+    {
+      int count = counts[i];
+      if (count > 0)
+      {
+        Color color = ColorUtils.getGraduatedColour(count, 0, Color.cyan,
+                max, Color.blue);
+        anns[i] = new Annotation(String.valueOf(count),
+                String.valueOf(count), '0', count, color);
+      }
+    }
+
+    /*
+     * construct the annotation, save it and add it to the displayed alignment
+     */
+    AlignmentAnnotation ann = new AlignmentAnnotation(counter.getName(),
+            counter.getDescription(), anns);
+    ann.showAllColLabels = true;
+    ann.graph = AlignmentAnnotation.BAR_GRAPH;
+    ourAnnots.add(ann);
+    alignViewport.getAlignment().addAnnotation(ann);
+  }
+
+  /**
+   * Returns a count of any feature types present at the specified position of
+   * the alignment
+   * 
+   * @param alignment
+   * @param col
+   * @param row
+   * @param fr
+   */
+  int countFeaturesAt(AlignmentI alignment, int col, int row,
+          FeatureRenderer fr)
+  {
+    SequenceI seq = alignment.getSequenceAt(row);
+    if (seq == null)
+    {
+      return 0;
+    }
+    if (col >= seq.getLength())
+    {
+      return 0;// sequence doesn't extend this far
+    }
+    char res = seq.getCharAt(col);
+    if (Comparison.isGap(res))
+    {
+      return 0;
+    }
+    int pos = seq.findPosition(col);
+
+    /*
+     * compute a count for any displayed features at residue
+     */
+    // NB have to adjust pos if using AlignmentView.getVisibleAlignment
+    // see JAL-2075
+    List<SequenceFeature> features = fr.findFeaturesAtRes(seq, pos);
+    int count = this.counter.count(String.valueOf(res), features);
+    return count;
+  }
+
+  /**
+   * Method called when the user changes display options that may affect how the
+   * annotation is rendered, but do not change its values. Currently no such
+   * options affect user-defined annotation, so this method does nothing.
+   */
+  @Override
+  public void updateAnnotation()
+  {
+    // do nothing
+  }
+}
index a8698e6..14e2a31 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.workers;
 
 import jalview.analysis.AAFrequency;
-import jalview.api.AlignCalcWorkerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentAnnotation;
@@ -32,8 +31,7 @@ import jalview.schemes.ColourSchemeI;
 
 import java.util.Hashtable;
 
-public class ConsensusThread extends AlignCalcWorker implements
-        AlignCalcWorkerI
+public class ConsensusThread extends AlignCalcWorker
 {
   public ConsensusThread(AlignViewportI alignViewport,
           AlignmentViewPanel alignPanel)
index 236bccf..1075e4d 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.workers;
 
 import jalview.analysis.Conservation;
-import jalview.api.AlignCalcWorkerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentAnnotation;
@@ -30,8 +29,7 @@ import jalview.datamodel.AlignmentI;
 import java.util.ArrayList;
 import java.util.List;
 
-public class ConservationThread extends AlignCalcWorker implements
-        AlignCalcWorkerI
+public class ConservationThread extends AlignCalcWorker
 {
 
   private int ConsPercGaps = 25; // JBPNote : This should be a configurable
diff --git a/src/jalview/workers/FeatureCounterI.java b/src/jalview/workers/FeatureCounterI.java
new file mode 100644 (file)
index 0000000..aa4a283
--- /dev/null
@@ -0,0 +1,63 @@
+package jalview.workers;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.List;
+
+/**
+ * An interface for a type that returns counts of any value of interest at a
+ * sequence position that can be determined from the sequence character and any
+ * features present at that position
+ * 
+ */
+public interface FeatureCounterI
+{
+  /**
+   * Returns a count of some property of interest, for example
+   * <ul>
+   * <li>the number of variant features at the position</li>
+   * <li>the number of Cath features of status 'True Positive'</li>
+   * <li>1 if the residue is hydrophobic, else 0</li>
+   * <li>etc</li>
+   * </ul>
+   * 
+   * @param residue
+   *          the residue (or gap) at the position
+   * @param a
+   *          list of any sequence features which include the position
+   */
+  int count(String residue, List<SequenceFeature> features);
+
+  /**
+   * Returns a name for the annotation that this is counting, for use as the
+   * displayed label
+   * 
+   * @return
+   */
+  String getName();
+
+  /**
+   * Returns a description for the annotation, for display as a tooltip
+   * 
+   * @return
+   */
+  String getDescription();
+
+  /**
+   * Returns the colour (as [red, green, blue] values in the range 0-255) to use
+   * for the minimum value on histogram bars. If this is different to
+   * getMaxColour(), then bars will have a graduated colour.
+   * 
+   * @return
+   */
+  int[] getMinColour();
+
+  /**
+   * Returns the colour (as [red, green, blue] values in the range 0-255) to use
+   * for the maximum value on histogram bars. If this is the same as
+   * getMinColour(), then bars will have a single colour (not graduated).
+   * 
+   * @return
+   */
+  int[] getMaxColour();
+}
index e0b3833..3483dac 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.workers;
 
 import jalview.analysis.StructureFrequency;
-import jalview.api.AlignCalcWorkerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentAnnotation;
@@ -31,8 +30,7 @@ import jalview.datamodel.SequenceI;
 
 import java.util.Hashtable;
 
-public class StrucConsensusThread extends AlignCalcWorker implements
-        AlignCalcWorkerI
+public class StrucConsensusThread extends AlignCalcWorker
 {
   public StrucConsensusThread(AlignViewportI alignViewport,
           AlignmentViewPanel alignPanel)
index f929b1e..f4b1c31 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.ws.jws2;
 
-import jalview.api.AlignCalcWorkerI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.GraphLine;
@@ -46,8 +45,7 @@ import compbio.data.sequence.Score;
 import compbio.data.sequence.ScoreManager.ScoreHolder;
 import compbio.metadata.Argument;
 
-public class AADisorderClient extends JabawsCalcWorker implements
-        AlignCalcWorkerI
+public class AADisorderClient extends JabawsCalcWorker
 {
 
   private static final String THRESHOLD = "THRESHOLD";
index e4d6329..c15f256 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.ws.jws2;
 
-import jalview.api.AlignCalcWorkerI;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.gui.AlignFrame;
@@ -43,9 +42,7 @@ import compbio.data.sequence.JpredAlignment;
 import compbio.metadata.Argument;
 
 public class JPred301Client extends JabawsMsaInterfaceAlignCalcWorker
-        implements AlignCalcWorkerI
 {
-
   /**
    * 
    * @return default args for this service when run as dynamic web service
@@ -87,6 +84,7 @@ public class JPred301Client extends JabawsMsaInterfaceAlignCalcWorker
     return (seqs.size() > 1);
   }
 
+  @Override
   public String getServiceActionText()
   {
     return "calculating consensus secondary structure prediction using JPred service";
@@ -112,6 +110,7 @@ public class JPred301Client extends JabawsMsaInterfaceAlignCalcWorker
    * update the consensus annotation from the sequence profile data using
    * current visualization settings.
    */
+  @Override
   public void updateResultAnnotation(boolean immediate)
   {
     if (immediate || !calcMan.isWorking(this) && msascoreset != null)
index 41aa223..9ca6d2e 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.ws.jws2;
 
-import jalview.api.AlignCalcWorkerI;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.gui.AlignFrame;
@@ -50,8 +49,7 @@ import compbio.metadata.Argument;
  * 
  */
 
-public class RNAalifoldClient extends JabawsCalcWorker implements
-        AlignCalcWorkerI
+public class RNAalifoldClient extends JabawsCalcWorker
 {
 
   String methodName;
@@ -75,6 +73,7 @@ public class RNAalifoldClient extends JabawsCalcWorker implements
     initViewportParams();
   }
 
+  @Override
   public String getCalcId()
   {
     return CALC_ID;
index 2dc2ac3..029483f 100644 (file)
@@ -27,7 +27,7 @@ import jalview.gui.AlignFrame;
 import jalview.io.FileLoader;
 import jalview.io.FormatAdapter;
 
-import org.testng.AssertJUnit;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
 public class FeatureScoreModelTest
@@ -40,14 +40,13 @@ public class FeatureScoreModelTest
 
   int[] sf3 = new int[] { -1, -1, -1, -1, -1, -1, 76, 77 };
 
-  @Test(groups = { "Functional" })
-  public void testFeatureScoreModel() throws Exception
+  public AlignFrame getTestAlignmentFrame()
   {
     AlignFrame alf = new FileLoader(false).LoadFileWaitTillLoaded(
             alntestFile, FormatAdapter.PASTE);
     AlignmentI al = alf.getViewport().getAlignment();
-    AssertJUnit.assertEquals(4, al.getHeight());
-    AssertJUnit.assertEquals(5, al.getWidth());
+    Assert.assertEquals(al.getHeight(), 4);
+    Assert.assertEquals(al.getWidth(), 5);
     for (int i = 0; i < 4; i++)
     {
       SequenceI ds = al.getSequenceAt(i).getDatasetSequence();
@@ -72,21 +71,68 @@ public class FeatureScoreModelTest
     alf.getFeatureRenderer().setVisible("sf2");
     alf.getFeatureRenderer().setVisible("sf3");
     alf.getFeatureRenderer().findAllFeatures(true);
-    AssertJUnit.assertEquals("Number of feature types", 3, alf
-            .getFeatureRenderer().getDisplayedFeatureTypes().size());
-    AssertJUnit.assertTrue(alf.getCurrentView().areFeaturesDisplayed());
+    Assert.assertEquals(alf.getFeatureRenderer().getDisplayedFeatureTypes()
+            .size(), 3, "Number of feature types");
+    Assert.assertTrue(alf.getCurrentView().areFeaturesDisplayed());
+    return alf;
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
     FeatureScoreModel fsm = new FeatureScoreModel();
-    AssertJUnit.assertTrue(fsm.configureFromAlignmentView(alf
+    Assert.assertTrue(fsm.configureFromAlignmentView(alf
             .getCurrentView().getAlignPanel()));
     alf.selectAllSequenceMenuItem_actionPerformed(null);
     float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
             true));
-    AssertJUnit.assertTrue("FER1_MESCR should be identical with RAPSA (2)",
-            dm[0][2] == 0f);
-    AssertJUnit
-            .assertTrue(
-                    "FER1_MESCR should be further from SPIOL (1) than it is from RAPSA (2)",
-                    dm[0][1] > dm[0][2]);
+    Assert.assertTrue(dm[0][2] == 0f,
+            "FER1_MESCR (0) should be identical with RAPSA (2)");
+    Assert.assertTrue(dm[0][1] > dm[0][2],
+            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel_hiddenFirstColumn() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
+    // hiding first two columns shouldn't affect the tree
+    alf.getViewport().hideColumns(0, 1);
+    FeatureScoreModel fsm = new FeatureScoreModel();
+    Assert.assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
+            .getAlignPanel()));
+    alf.selectAllSequenceMenuItem_actionPerformed(null);
+    float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
+            true));
+    Assert.assertTrue(dm[0][2] == 0f,
+            "FER1_MESCR (0) should be identical with RAPSA (2)");
+    Assert.assertTrue(dm[0][1] > dm[0][2],
+            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
+  }
 
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel_HiddenColumns() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
+    // hide columns and check tree changes
+    alf.getViewport().hideColumns(3, 4);
+    alf.getViewport().hideColumns(0, 1);
+    FeatureScoreModel fsm = new FeatureScoreModel();
+    Assert.assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
+            .getAlignPanel()));
+    alf.selectAllSequenceMenuItem_actionPerformed(null);
+    float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
+            true));
+    Assert.assertTrue(dm[0][2] == 0f,
+            "After hiding last two columns FER1_MESCR (0) should still be identical with RAPSA (2)");
+    Assert.assertTrue(dm[0][1] == 0f,
+            "After hiding last two columns FER1_MESCR (0) should now also be identical with SPIOL (1)");
+    for (int s=0;s<3;s++)
+    {
+      Assert.assertTrue(dm[s][3] > 0f, "After hiding last two columns "
+              + alf.getViewport().getAlignment().getSequenceAt(s).getName()
+              + "(" + s + ") should still be distinct from FER1_MAIZE (3)");
+    }
   }
 }
index 2b12a72..a82b9c0 100644 (file)
@@ -72,4 +72,65 @@ public class ColorUtilsTest
     assertEquals("#800080", ColorUtils.toTkCode(new Color(128, 0, 128))); // purple
     assertEquals("#00ff00", ColorUtils.toTkCode(new Color(0, 255, 0))); // lime
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetGraduatedColour()
+  {
+    Color minColour = new Color(100, 100, 100);
+    Color maxColour = new Color(180, 200, 220);
+
+    /*
+     * value half-way between min and max
+     */
+    Color col = ColorUtils.getGraduatedColour(20f, 10f, minColour, 30f,
+            maxColour);
+    assertEquals(140, col.getRed());
+    assertEquals(150, col.getGreen());
+    assertEquals(160, col.getBlue());
+
+    /*
+     * value two-thirds of the way between min and max
+     */
+    col = ColorUtils
+            .getGraduatedColour(30f, 10f, minColour, 40f, maxColour);
+    assertEquals(153, col.getRed());
+    // Color constructor rounds float value to nearest int
+    assertEquals(167, col.getGreen());
+    assertEquals(180, col.getBlue());
+
+    /*
+     * value = min
+     */
+    col = ColorUtils
+            .getGraduatedColour(10f, 10f, minColour, 30f, maxColour);
+    assertEquals(minColour, col);
+
+    /*
+     * value = max
+     */
+    col = ColorUtils
+            .getGraduatedColour(30f, 10f, minColour, 30f, maxColour);
+    assertEquals(maxColour, col);
+
+    /*
+     * value < min
+     */
+    col = ColorUtils.getGraduatedColour(0f, 10f, minColour, 30f, maxColour);
+    assertEquals(minColour, col);
+
+    /*
+     * value > max
+     */
+    col = ColorUtils
+            .getGraduatedColour(40f, 10f, minColour, 30f,
+            maxColour);
+    assertEquals(maxColour, col);
+
+    /*
+     * min = max
+     */
+    col = ColorUtils
+            .getGraduatedColour(40f, 10f, minColour, 10f, maxColour);
+    assertEquals(minColour, col);
+  }
 }