From: Jim Procter Date: Sun, 24 Apr 2016 10:28:48 +0000 (+0100) Subject: Merge branch 'JAL-2075_features_hiddeng' into develop X-Git-Tag: Release_2_10_0~249^2~7 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=8e86de369f964f94c2342a9cc64b0e4f346cfd02;hp=4a621265c074df3b90126a453bed7a0e7bed02c6;p=jalview.git Merge branch 'JAL-2075_features_hiddeng' into develop --- diff --git a/examples/groovy/featureCounter.groovy b/examples/groovy/featureCounter.groovy new file mode 100644 index 0000000..08d038d --- /dev/null +++ b/examples/groovy/featureCounter.groovy @@ -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 index 0000000..592c7f5 --- /dev/null +++ b/examples/groovy/multipleFeatureAnnotations.groovy @@ -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) diff --git a/help/helpTOC.xml b/help/helpTOC.xml index 2594738..fe8e1a9 100755 --- a/help/helpTOC.xml +++ b/help/helpTOC.xml @@ -27,7 +27,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -127,7 +127,7 @@ - + diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 05505a4..bc6def9 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -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 diff --git a/src/jalview/api/AlignCalcWorkerI.java b/src/jalview/api/AlignCalcWorkerI.java index 872528b..06dc054 100644 --- a/src/jalview/api/AlignCalcWorkerI.java +++ b/src/jalview/api/AlignCalcWorkerI.java @@ -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(); } diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 28ff0a1..67152f2 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -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 diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 27f60ea..b7c4098 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -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; + } + } diff --git a/src/jalview/jbgui/GAlignFrame.java b/src/jalview/jbgui/GAlignFrame.java index 205b9c6..1eb558e 100755 --- a/src/jalview/jbgui/GAlignFrame.java +++ b/src/jalview/jbgui/GAlignFrame.java @@ -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. * diff --git a/src/jalview/util/ColorUtils.java b/src/jalview/util/ColorUtils.java index b8cd563..31d1ded 100644 --- a/src/jalview/util/ColorUtils.java +++ b/src/jalview/util/ColorUtils.java @@ -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); + } } diff --git a/src/jalview/workers/AlignCalcWorker.java b/src/jalview/workers/AlignCalcWorker.java index bca3145..48e3604 100644 --- a/src/jalview/workers/AlignCalcWorker.java +++ b/src/jalview/workers/AlignCalcWorker.java @@ -46,7 +46,7 @@ public abstract class AlignCalcWorker implements AlignCalcWorkerI protected AlignmentViewPanel ap; - protected List ourAnnots = null; + protected List 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 index 0000000..37f3ca5 --- /dev/null +++ b/src/jalview/workers/AlignmentAnnotationFactory.java @@ -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.
+ * Currently supports two flavours of calculator: + *
    + *
  • a 'feature counter' which can count any desired property derivable from + * residue value and any sequence features at each position of the alignment
  • + *
  • a 'general purpose' calculator which computes one more complete + * AlignmentAnnotation objects
  • + *
+ */ +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 index 0000000..653ff04 --- /dev/null +++ b/src/jalview/workers/AnnotationProviderI.java @@ -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 calculateAnnotation(AlignmentI al, + FeatureRenderer fr); +} diff --git a/src/jalview/workers/AnnotationWorker.java b/src/jalview/workers/AnnotationWorker.java new file mode 100644 index 0000000..fbf7531 --- /dev/null +++ b/src/jalview/workers/AnnotationWorker.java @@ -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 . + * 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(); + 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 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 index 0000000..6f4a4f3 --- /dev/null +++ b/src/jalview/workers/ColumnCounterWorker.java @@ -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 . + * 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.
+ * 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(); + 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 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 + } +} diff --git a/src/jalview/workers/ConsensusThread.java b/src/jalview/workers/ConsensusThread.java index a8698e6..14e2a31 100644 --- a/src/jalview/workers/ConsensusThread.java +++ b/src/jalview/workers/ConsensusThread.java @@ -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) diff --git a/src/jalview/workers/ConservationThread.java b/src/jalview/workers/ConservationThread.java index 236bccf..1075e4d 100644 --- a/src/jalview/workers/ConservationThread.java +++ b/src/jalview/workers/ConservationThread.java @@ -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 index 0000000..aa4a283 --- /dev/null +++ b/src/jalview/workers/FeatureCounterI.java @@ -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 + *
    + *
  • the number of variant features at the position
  • + *
  • the number of Cath features of status 'True Positive'
  • + *
  • 1 if the residue is hydrophobic, else 0
  • + *
  • etc
  • + *
+ * + * @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 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(); +} diff --git a/src/jalview/workers/StrucConsensusThread.java b/src/jalview/workers/StrucConsensusThread.java index e0b3833..3483dac 100644 --- a/src/jalview/workers/StrucConsensusThread.java +++ b/src/jalview/workers/StrucConsensusThread.java @@ -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) diff --git a/src/jalview/ws/jws2/AADisorderClient.java b/src/jalview/ws/jws2/AADisorderClient.java index f929b1e..f4b1c31 100644 --- a/src/jalview/ws/jws2/AADisorderClient.java +++ b/src/jalview/ws/jws2/AADisorderClient.java @@ -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"; diff --git a/src/jalview/ws/jws2/JPred301Client.java b/src/jalview/ws/jws2/JPred301Client.java index e4d6329..c15f256 100644 --- a/src/jalview/ws/jws2/JPred301Client.java +++ b/src/jalview/ws/jws2/JPred301Client.java @@ -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) diff --git a/src/jalview/ws/jws2/RNAalifoldClient.java b/src/jalview/ws/jws2/RNAalifoldClient.java index 41aa223..9ca6d2e 100644 --- a/src/jalview/ws/jws2/RNAalifoldClient.java +++ b/src/jalview/ws/jws2/RNAalifoldClient.java @@ -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; diff --git a/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java b/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java index 2dc2ac3..029483f 100644 --- a/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java +++ b/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java @@ -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)"); + } } } diff --git a/test/jalview/util/ColorUtilsTest.java b/test/jalview/util/ColorUtilsTest.java index 2b12a72..a82b9c0 100644 --- a/test/jalview/util/ColorUtilsTest.java +++ b/test/jalview/util/ColorUtilsTest.java @@ -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); + } }