From 60e252f7e0084336b0a85620842ce3db8f63e5b0 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 23 May 2017 17:10:23 +0100 Subject: [PATCH] JAL-2490 find features for export as GFF, JAL-2548 respect group visibility --- src/jalview/analysis/AlignmentSorter.java | 5 + src/jalview/api/FeatureRenderer.java | 4 +- src/jalview/appletgui/AlignFrame.java | 17 +- src/jalview/gui/AnnotationExporter.java | 7 +- src/jalview/io/FeaturesFile.java | 295 +++++++++++--------- .../seqfeatures/FeatureRendererModel.java | 11 - test/jalview/io/FeaturesFileTest.java | 62 ++-- 7 files changed, 230 insertions(+), 171 deletions(-) diff --git a/src/jalview/analysis/AlignmentSorter.java b/src/jalview/analysis/AlignmentSorter.java index f228350..e7733e9 100755 --- a/src/jalview/analysis/AlignmentSorter.java +++ b/src/jalview/analysis/AlignmentSorter.java @@ -783,8 +783,13 @@ public class AlignmentSorter continue; } + /* + * accept all features with null or empty group, otherwise + * check group is one of the currently visible groups + */ String featureGroup = sf.getFeatureGroup(); if (groups != null && featureGroup != null + && !"".equals(featureGroup) && !groups.contains(featureGroup)) { it.remove(); diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 7123b8c..edd236b 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -165,9 +165,9 @@ public interface FeatureRenderer List getDisplayedFeatureTypes(); /** - * get current displayed groups + * Returns a (possibly empty) list of currently visible feature groups * - * @return a (possibly empty) list of feature groups + * @return */ List getDisplayedFeatureGroups(); diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index b8700e1..2dde2ab 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -1440,6 +1440,17 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, return null; } + private List getDisplayedFeatureGroups() + { + if (alignPanel.getFeatureRenderer() != null + && viewport.getFeaturesDisplayed() != null) + { + return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups(); + + } + return null; + } + public String outputFeatures(boolean displayTextbox, String format) { String features; @@ -1447,12 +1458,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, if (format.equalsIgnoreCase("Jalview")) { features = formatter.printJalviewFormat(viewport.getAlignment() - .getSequencesArray(), getDisplayedFeatureCols(), true); + .getSequencesArray(), getDisplayedFeatureCols(), + getDisplayedFeatureGroups(), true); } else { features = formatter.printGffFormat(viewport.getAlignment() - .getSequencesArray(), getDisplayedFeatureCols(), true); + .getSequencesArray(), getDisplayedFeatureCols(), + getDisplayedFeatureGroups(), true); } if (displayTextbox) diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index a48c030..42913de 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -34,6 +34,7 @@ import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.List; import java.util.Map; import javax.swing.BorderFactory; @@ -160,16 +161,18 @@ public class AnnotationExporter extends JPanel SequenceI[] sequences = ap.av.getAlignment().getSequencesArray(); Map featureColours = ap.getFeatureRenderer() .getDisplayedFeatureCols(); + List featureGroups = ap.getFeatureRenderer() + .getDisplayedFeatureGroups(); boolean includeNonPositional = ap.av.isShowNPFeats(); if (GFFFormat.isSelected()) { text = formatter.printGffFormat(sequences, featureColours, - includeNonPositional); + featureGroups, includeNonPositional); } else { text = formatter.printJalviewFormat(sequences, featureColours, - includeNonPositional); + featureGroups, includeNonPositional); } } else diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 869b18b..afc00ee 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -47,11 +47,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; /** * Parses and writes features files, which may be in Jalview, GFF2 or GFF3 @@ -500,19 +498,24 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns contents of a Jalview format features file + * Returns contents of a Jalview format features file, for visible features, + * as filtered by type and group. Features with a null group are displayed if + * their feature type is visible. Non-positional features may optionally be + * included (with no check on type or group). * * @param sequences * source of features * @param visible * map of colour for each visible feature type + * @param visibleFeatureGroups * @param includeNonPositional * if true, include non-positional features (regardless of group or * type) * @return */ public String printJalviewFormat(SequenceI[] sequences, - Map visible, boolean includeNonPositional) + Map visible, + List visibleFeatureGroups, boolean includeNonPositional) { if (!includeNonPositional && (visible == null || visible.isEmpty())) { @@ -535,30 +538,36 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - // Work out which groups are both present and visible - Set groups = new HashSet(); String[] types = visible == null ? new String[0] : visible.keySet() .toArray(new String[visible.keySet().size()]); - for (int i = 0; i < sequences.length; i++) + /* + * sort groups alphabetically, and ensure that null group is output last + */ + List sortedGroups = new ArrayList(visibleFeatureGroups); + sortedGroups.remove(null); + Collections.sort(sortedGroups); + sortedGroups.add(null); + + boolean foundSome = false; + + /* + * first output any non-positional features + */ + if (includeNonPositional) { - groups.addAll(sequences[i].getFeatures() - .getFeatureGroups(true, types)); - if (includeNonPositional) + for (int i = 0; i < sequences.length; i++) { - groups.addAll(sequences[i].getFeatures().getFeatureGroups(false, - types)); + String sequenceName = sequences[i].getName(); + for (SequenceFeature feature : sequences[i].getFeatures() + .getNonPositionalFeatures()) + { + foundSome = true; + out.append(formatJalviewFeature(sequenceName, feature)); + } } } - /* - * sort distinct groups so null group is output last - */ - List sortedGroups = new ArrayList(groups); - Collections.sort(sortedGroups, SORT_NULL_LAST); - - // TODO check where null group should be output - boolean foundSome = false; for (String group : sortedGroups) { if (group != null) @@ -570,16 +579,12 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /* - * output features within groups (non-positional first if wanted) + * output positional features within groups */ for (int i = 0; i < sequences.length; i++) { + String sequenceName = sequences[i].getName(); List features = new ArrayList(); - if (includeNonPositional) - { - features.addAll(sequences[i].getFeatures().getFeaturesForGroup( - false, group, types)); - } if (types.length > 0) { features.addAll(sequences[i].getFeatures().getFeaturesForGroup( @@ -588,57 +593,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI for (SequenceFeature sequenceFeature : features) { - // we have features to output foundSome = true; - if (sequenceFeature.description == null - || sequenceFeature.description.equals("")) - { - out.append(sequenceFeature.type).append(TAB); - } - else - { - if (sequenceFeature.links != null - && sequenceFeature.getDescription().indexOf("") == -1) - { - out.append(""); - } - - out.append(sequenceFeature.description); - if (sequenceFeature.links != null) - { - for (int l = 0; l < sequenceFeature.links.size(); l++) - { - String label = sequenceFeature.links.elementAt(l); - String href = label.substring(label.indexOf("|") + 1); - label = label.substring(0, label.indexOf("|")); - - if (sequenceFeature.description.indexOf(href) == -1) - { - out.append(" " + label + ""); - } - } - - if (sequenceFeature.getDescription().indexOf("") == -1) - { - out.append(""); - } - } - - out.append(TAB); - } - out.append(sequences[i].getName()); - out.append("\t-1\t"); - out.append(sequenceFeature.begin); - out.append(TAB); - out.append(sequenceFeature.end); - out.append(TAB); - out.append(sequenceFeature.type); - if (!Float.isNaN(sequenceFeature.score)) - { - out.append(TAB); - out.append(sequenceFeature.score); - } - out.append(newline); + out.append(formatJalviewFeature(sequenceName, sequenceFeature)); } } @@ -654,6 +610,68 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * @param out + * @param sequenceName + * @param sequenceFeature + */ + protected String formatJalviewFeature( + String sequenceName, SequenceFeature sequenceFeature) + { + StringBuilder out = new StringBuilder(64); + if (sequenceFeature.description == null + || sequenceFeature.description.equals("")) + { + out.append(sequenceFeature.type).append(TAB); + } + else + { + if (sequenceFeature.links != null + && sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + + out.append(sequenceFeature.description); + if (sequenceFeature.links != null) + { + for (int l = 0; l < sequenceFeature.links.size(); l++) + { + String label = sequenceFeature.links.elementAt(l); + String href = label.substring(label.indexOf("|") + 1); + label = label.substring(0, label.indexOf("|")); + + if (sequenceFeature.description.indexOf(href) == -1) + { + out.append(" " + label + ""); + } + } + + if (sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + } + + out.append(TAB); + } + out.append(sequenceName); + out.append("\t-1\t"); + out.append(sequenceFeature.begin); + out.append(TAB); + out.append(sequenceFeature.end); + out.append(TAB); + out.append(sequenceFeature.type); + if (!Float.isNaN(sequenceFeature.score)) + { + out.append(TAB); + out.append(sequenceFeature.score); + } + out.append(newline); + + return out.toString(); + } + + /** * Parse method that is called when a GFF file is dragged to the desktop */ @Override @@ -712,76 +730,84 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * the sequences whose features are to be output * @param visible * a map whose keys are the type names of visible features + * @param visibleFeatureGroups * @param includeNonPositionalFeatures * @return */ public String printGffFormat(SequenceI[] sequences, Map visible, + List visibleFeatureGroups, boolean includeNonPositionalFeatures) { StringBuilder out = new StringBuilder(256); - int version = gffVersion == 0 ? 2 : gffVersion; - out.append(String.format("%s %d\n", GFF_VERSION, version)); - String source; - boolean isnonpos; + + out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); + + if (!includeNonPositionalFeatures + && (visible == null || visible.isEmpty())) + { + return out.toString(); + } + + String[] types = visible == null ? new String[0] : visible.keySet() + .toArray( + new String[visible.keySet().size()]); + for (SequenceI seq : sequences) { - SequenceFeature[] features = seq.getSequenceFeatures(); - if (features != null) + List features = new ArrayList(); + if (includeNonPositionalFeatures) { - for (SequenceFeature sf : features) - { - isnonpos = sf.begin == 0 && sf.end == 0; - if (!includeNonPositionalFeatures && isnonpos) - { - /* - * ignore non-positional features if not wanted - */ - continue; - } - if (!isnonpos && !visible.containsKey(sf.type)) - { - /* - * ignore not visible features if not wanted - */ - continue; - } + features.addAll(seq.getFeatures().getNonPositionalFeatures()); + } + if (visible != null && !visible.isEmpty()) + { + features.addAll(seq.getFeatures().getPositionalFeatures(types)); + } - source = sf.featureGroup; - if (source == null) - { - source = sf.getDescription(); - } + for (SequenceFeature sf : features) + { + String source = sf.featureGroup; + if (!sf.isNonPositional() && source != null + && !visibleFeatureGroups.contains(source)) + { + // group is not visible + continue; + } - out.append(seq.getName()); - out.append(TAB); - out.append(source); - out.append(TAB); - out.append(sf.type); - out.append(TAB); - out.append(sf.begin); - out.append(TAB); - out.append(sf.end); - out.append(TAB); - out.append(sf.score); - out.append(TAB); - - int strand = sf.getStrand(); - out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); - out.append(TAB); - - String phase = sf.getPhase(); - out.append(phase == null ? "." : phase); - - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) - { - out.append(TAB).append(attributes); - } + if (source == null) + { + source = sf.getDescription(); + } - out.append(newline); + out.append(seq.getName()); + out.append(TAB); + out.append(source); + out.append(TAB); + out.append(sf.type); + out.append(TAB); + out.append(sf.begin); + out.append(TAB); + out.append(sf.end); + out.append(TAB); + out.append(sf.score); + out.append(TAB); + + int strand = sf.getStrand(); + out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); + out.append(TAB); + + String phase = sf.getPhase(); + out.append(phase == null ? "." : phase); + + // miscellaneous key-values (GFF column 9) + String attributes = sf.getAttributes(); + if (attributes != null) + { + out.append(TAB).append(attributes); } + + out.append(newline); } } @@ -1042,10 +1068,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI // rename sequences if GFF handler requested this // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ? - SequenceFeature[] sfs = seq.getSequenceFeatures(); - if (sfs != null) + List sfs = seq.getFeatures().getPositionalFeatures(); + if (!sfs.isEmpty()) { - String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN); + String newName = (String) sfs.get(0).getValue( + GffHelperI.RENAME_TOKEN); if (newName != null) { seq.setName(newName); diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 284ee4f..a8e8989 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -980,23 +980,12 @@ public abstract class FeatureRendererModel implements public List getDisplayedFeatureGroups() { List _gps = new ArrayList(); - boolean valid = false; for (String gp : getFeatureGroups()) { if (checkGroupVisibility(gp, false)) { - valid = true; _gps.add(gp); } - if (!valid) - { - return null; - } - else - { - // gps = new String[_gps.size()]; - // _gps.toArray(gps); - } } return _gps; } diff --git a/test/jalview/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index 6429e16..d59c6bb 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -39,7 +39,10 @@ import jalview.gui.JvOptionPane; import java.awt.Color; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.testng.annotations.BeforeClass; @@ -421,17 +424,20 @@ public class FeaturesFileTest */ FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); Map visible = fr.getDisplayedFeatureCols(); + List visibleGroups = new ArrayList( + Arrays.asList(new String[] {})); String exported = featuresFile.printJalviewFormat( - al.getSequencesArray(), visible, false); + al.getSequencesArray(), visible, visibleGroups, false); String expected = "No Features Visible"; assertEquals(expected, exported); /* * include non-positional features */ + visibleGroups.add("uniprot"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, true); - expected = "\nSTARTGROUP\tuniprot\nCath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\nENDGROUP\tuniprot\n"; + visible, visibleGroups, true); + expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n\nSTARTGROUP\tuniprot\nENDGROUP\tuniprot\n"; assertEquals(expected, exported); /* @@ -441,7 +447,7 @@ public class FeaturesFileTest fr.setVisible("GAMMA-TURN"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, false); + visible, visibleGroups, false); expected = "METAL\tcc9900\n" + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" @@ -456,7 +462,7 @@ public class FeaturesFileTest fr.setVisible("Pfam"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, false); + visible, visibleGroups, false); /* * features are output within group, ordered by sequence and by type */ @@ -484,12 +490,14 @@ public class FeaturesFileTest FeaturesFile featuresFile = new FeaturesFile(); FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); Map visible = new HashMap(); - String exported = featuresFile.printGffFormat( - al.getSequencesArray(), visible, false); + List visibleGroups = new ArrayList( + Arrays.asList(new String[] {})); + String exported = featuresFile.printGffFormat(al.getSequencesArray(), + visible, visibleGroups, false); String gffHeader = "##gff-version 2\n"; assertEquals(gffHeader, exported); exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, - true); + visibleGroups, true); assertEquals(gffHeader, exported); /* @@ -503,7 +511,8 @@ public class FeaturesFileTest .addSequenceFeature( new SequenceFeature("GAMMA-TURN", "Turn", 36, 38, 2.1f, "s3dm")); - SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f, "Uniprot"); + SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f, + "Uniprot"); sf.setAttributes("x=y;black=white"); sf.setStrand("+"); sf.setPhase("2"); @@ -513,40 +522,53 @@ public class FeaturesFileTest * with no features displayed, exclude non-positional features */ exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, - false); + visibleGroups, false); assertEquals(gffHeader, exported); - + /* * include non-positional features */ - exported = featuresFile.printGffFormat(al.getSequencesArray(), - visible, true); + visibleGroups.add("Uniprot"); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, true); String expected = gffHeader + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n"; assertEquals(expected, exported); - + /* * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam + * only Uniprot group visible here... */ fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); visible = fr.getDisplayedFeatureCols(); - exported = featuresFile.printGffFormat(al.getSequencesArray(), - visible, false); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); + // METAL feature has null group: description used for column 2 + expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; + assertEquals(expected, exported); + + /* + * set s3dm group visible + */ + visibleGroups.add("s3dm"); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); // METAL feature has null group: description used for column 2 expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; assertEquals(expected, exported); - + /* * now set Pfam visible */ fr.setVisible("Pfam"); visible = fr.getDisplayedFeatureCols(); - exported = featuresFile.printGffFormat(al.getSequencesArray(), - visible, false); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); // Pfam feature columns include strand(+), phase(2), attributes - expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + expected = gffHeader + + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n" + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"; assertEquals(expected, exported); -- 1.7.10.2