From 32cb25af86f1710205b6fdca0df00aed0a0211bb Mon Sep 17 00:00:00 2001 From: gmungoc Date: Fri, 22 Apr 2016 16:03:28 +0100 Subject: [PATCH] JAL-2068 framework and example scripts for pluggable alignment annotation workers --- examples/groovy/featureCounter.groovy | 87 ++++++++ examples/groovy/multipleFeatureAnnotations.groovy | 110 ++++++++++ src/jalview/workers/AlignCalcWorker.java | 8 +- .../workers/AlignmentAnnotationFactory.java | 117 ++++++++++ src/jalview/workers/AnnotationProviderI.java | 17 ++ src/jalview/workers/AnnotationWorker.java | 136 ++++++++++++ src/jalview/workers/ColumnCounterWorker.java | 225 ++++++++++++++++++++ src/jalview/workers/FeatureCounterI.java | 63 ++++++ 8 files changed, 760 insertions(+), 3 deletions(-) create mode 100644 examples/groovy/featureCounter.groovy create mode 100644 examples/groovy/multipleFeatureAnnotations.groovy create mode 100644 src/jalview/workers/AlignmentAnnotationFactory.java create mode 100644 src/jalview/workers/AnnotationProviderI.java create mode 100644 src/jalview/workers/AnnotationWorker.java create mode 100644 src/jalview/workers/ColumnCounterWorker.java create mode 100644 src/jalview/workers/FeatureCounterI.java 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/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/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(); +} -- 1.7.10.2