From d08df378179f85c2142f69ce63380baad803a0ab Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 18 Apr 2017 09:42:45 +0100 Subject: [PATCH] JAL-2480 SequenceFeaturesI encapsulates features api --- src/jalview/datamodel/Sequence.java | 7 ++ src/jalview/datamodel/SequenceI.java | 9 ++ src/jalview/datamodel/features/FeatureStore.java | 61 ++++++++++ .../datamodel/features/SequenceFeatures.java | 127 +++++++++----------- .../datamodel/features/SequenceFeaturesI.java | 125 +++++++++++++++++++ src/jalview/gui/FeatureSettings.java | 52 +++----- .../datamodel/features/FeatureStoreTest.java | 41 +++++++ .../datamodel/features/SequenceFeaturesTest.java | 18 +-- 8 files changed, 323 insertions(+), 117 deletions(-) create mode 100644 src/jalview/datamodel/features/SequenceFeaturesI.java diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index e1c8566..b93cae0 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -23,6 +23,7 @@ package jalview.datamodel; import jalview.analysis.AlignSeq; import jalview.api.DBRefEntryI; import jalview.datamodel.features.SequenceFeatures; +import jalview.datamodel.features.SequenceFeaturesI; import jalview.util.Comparison; import jalview.util.DBRefUtils; import jalview.util.MapList; @@ -426,6 +427,12 @@ public class Sequence extends ASequence implements SequenceI } @Override + public SequenceFeaturesI getFeatures() + { + return sequenceFeatureStore; + } + + @Override public boolean addPDBId(PDBEntry entry) { if (pdbIds == null) diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index 7ecb4ed..605f682 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -20,6 +20,8 @@ */ package jalview.datamodel; +import jalview.datamodel.features.SequenceFeaturesI; + import java.util.List; import java.util.Vector; @@ -267,6 +269,13 @@ public interface SequenceI extends ASequenceI public SequenceFeature[] getSequenceFeatures(); /** + * Answers the object holding features for the sequence + * + * @return + */ + SequenceFeaturesI getFeatures(); + + /** * Replaces the array of sequence features associated with this sequence with * a new array reference. If this sequence has a dataset sequence, then this * method will update the dataset sequence's feature array diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index 721ac18..d6a94e2 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -121,6 +121,12 @@ public class FeatureStore */ Set nonPositionalFeatureGroups; + /* + * the total length of all positional features; contact features count 1 to + * the total and 1 to size(), consistent with an average 'feature length' of 1 + */ + int totalExtent; + /** * Constructor */ @@ -176,6 +182,18 @@ public class FeatureStore } } + /* + * record the total extent of positional features, to make + * getAverageFeatureLength possible; we count the length of a + * contact feature as 1 + */ + if (added && !feature.isNonPositional()) + { + int featureLength = feature.isContactFeature() ? 1 : 1 + + feature.getEnd() - feature.getBegin(); + totalExtent += featureLength; + } + return added; } @@ -622,6 +640,7 @@ public class FeatureStore if (removed) { rebuildFeatureGroups(sf.getFeatureGroup(), removedNonPositional); + // TODO and recalculate totalExtent (feature may have changed length!) } return removed; @@ -756,4 +775,46 @@ public class FeatureStore return matched; } + + /** + * Answers the number of positional (or non-positional) features stored + * + * @param positional + * @return + */ + public int size(boolean positional) + { + if (!positional) + { + return nonPositionalFeatures == null ? 0 : nonPositionalFeatures + .size(); + } + + int size = nonNestedFeatures.size(); + + if (contactFeatureStarts != null) + { + // note a contact feature (start/end) counts as one + size += contactFeatureStarts.size(); + } + + if (nestedFeatures != null) + { + size += nestedFeatures.size(); + } + + return size; + } + + /** + * Answers the average length of positional features (or zero if there are + * none). Contact features contribute a value of 1 to the average. + * + * @return + */ + public float getAverageFeatureLength() + { + int d = size(true); + return d == 0 ? 0f : (float) totalExtent / d; + } } diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java index 6165d0a..4489b1d 100644 --- a/src/jalview/datamodel/features/SequenceFeatures.java +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -4,6 +4,7 @@ import jalview.datamodel.SequenceFeature; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -19,7 +20,7 @@ import java.util.Set; * @author gmcarstairs * */ -public class SequenceFeatures +public class SequenceFeatures implements SequenceFeaturesI { /* @@ -36,14 +37,10 @@ public class SequenceFeatures featureStore = new HashMap(); } - /** - * Adds one sequence feature to the store, and returns true, unless the - * feature is already contained in the store, in which case this method - * returns false. Containment is determined by SequenceFeature.equals() - * comparison. - * - * @param sf + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#add(jalview.datamodel.SequenceFeature) */ + @Override public boolean add(SequenceFeature sf) { String type = sf.getType(); @@ -55,16 +52,10 @@ public class SequenceFeatures return featureStore.get(type).addFeature(sf); } - /** - * Returns a (possibly empty) list of features, optionally restricted to - * specified types, which overlap the given (inclusive) sequence position - * range - * - * @param from - * @param to - * @param type - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#findFeatures(int, int, java.lang.String) */ + @Override public List findFeatures(int from, int to, String... type) { @@ -82,13 +73,10 @@ public class SequenceFeatures return result; } - /** - * Answers a list of all features stored, optionally restricted to specified - * types, in no particular guaranteed order - * - * @param type - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getAllFeatures(java.lang.String) */ + @Override public List getAllFeatures(String... type) { List result = new ArrayList(); @@ -100,13 +88,25 @@ public class SequenceFeatures return result; } - /** - * Answers a list of all positional features, optionally restricted to - * specified types, in no particular guaranteed order - * - * @param type - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getFeatureCount(boolean, java.lang.String) */ + @Override + public int getFeatureCount(boolean positional, String... type) + { + int result = 0; + for (FeatureStore fs : featureStore.values()) + { + result += fs.size(positional); + } + + return result; + } + + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getPositionalFeatures(java.lang.String) + */ + @Override public List getPositionalFeatures(String... type) { List result = new ArrayList(); @@ -136,12 +136,10 @@ public class SequenceFeatures .keySet() : Arrays.asList(type); } - /** - * Answers a list of all contact features, optionally restricted to specified - * types, in no particular guaranteed order - * - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getContactFeatures(java.lang.String) */ + @Override public List getContactFeatures(String... type) { List result = new ArrayList(); @@ -157,14 +155,10 @@ public class SequenceFeatures return result; } - /** - * Answers a list of all non-positional features, optionally restricted to - * specified types, in no particular guaranteed order - * - * @param type - * if no type is specified, all are returned - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getNonPositionalFeatures(java.lang.String) */ + @Override public List getNonPositionalFeatures(String... type) { List result = new ArrayList(); @@ -180,14 +174,10 @@ public class SequenceFeatures return result; } - /** - * Deletes the given feature from the store, returning true if it was found - * (and deleted), else false. This method makes no assumption that the feature - * is in the 'expected' place in the store, in case it has been modified since - * it was added. - * - * @param sf + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#delete(jalview.datamodel.SequenceFeature) */ + @Override public boolean delete(SequenceFeature sf) { for (FeatureStore featureSet : featureStore.values()) @@ -200,11 +190,10 @@ public class SequenceFeatures return false; } - /** - * Answers true if this store contains at least one feature, else false - * - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#hasFeatures() */ + @Override public boolean hasFeatures() { for (FeatureStore featureSet : featureStore.values()) @@ -217,17 +206,10 @@ public class SequenceFeatures return false; } - /** - * Returns a set of the distinct feature groups present in the collection. The - * set may include null. The boolean parameter determines whether the groups - * for positional or for non-positional features are returned. The optional - * type parameter may be used to restrict to groups for specified feature - * types. - * - * @param positionalFeatures - * @param type - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getFeatureGroups(boolean, java.lang.String) */ + @Override public Set getFeatureGroups(boolean positionalFeatures, String... type) { @@ -247,16 +229,10 @@ public class SequenceFeatures return groups; } - /** - * Answers the set of distinct feature types for which there is at least one - * feature with one of the given feature group(s). The parameter determines - * whether the groups for positional or for non-positional features are - * returned. - * - * @param positionalFeatures - * @param groups - * @return + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getFeatureTypesForGroups(boolean, java.lang.String) */ + @Override public Set getFeatureTypesForGroups(boolean positionalFeatures, String... groups) { @@ -281,4 +257,13 @@ public class SequenceFeatures return result; } + + /* (non-Javadoc) + * @see jalview.datamodel.features.SequenceFeaturesI#getFeatureTypes() + */ + @Override + public Set getFeatureTypes() + { + return Collections.unmodifiableSet(featureStore.keySet()); + } } diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java new file mode 100644 index 0000000..b6cc3ad --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeaturesI.java @@ -0,0 +1,125 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; + +import java.util.List; +import java.util.Set; + +public interface SequenceFeaturesI +{ + + /** + * Adds one sequence feature to the store, and returns true, unless the + * feature is already contained in the store, in which case this method + * returns false. Containment is determined by SequenceFeature.equals() + * comparison. + * + * @param sf + */ + public abstract boolean add(SequenceFeature sf); + + /** + * Returns a (possibly empty) list of features, optionally restricted to + * specified types, which overlap the given (inclusive) sequence position + * range + * + * @param from + * @param to + * @param type + * @return + */ + public abstract List findFeatures(int from, int to, + String... type); + + /** + * Answers a list of all features stored, optionally restricted to specified + * types, in no particular guaranteed order + * + * @param type + * @return + */ + public abstract List getAllFeatures(String... type); + + public abstract int getFeatureCount(boolean positional, String... type); + + /** + * Answers a list of all positional features, optionally restricted to + * specified types, in no particular guaranteed order + * + * @param type + * @return + */ + public abstract List getPositionalFeatures( + String... type); + + /** + * Answers a list of all contact features, optionally restricted to specified + * types, in no particular guaranteed order + * + * @return + */ + public abstract List getContactFeatures(String... type); + + /** + * Answers a list of all non-positional features, optionally restricted to + * specified types, in no particular guaranteed order + * + * @param type + * if no type is specified, all are returned + * @return + */ + public abstract List getNonPositionalFeatures( + String... type); + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + public abstract boolean delete(SequenceFeature sf); + + /** + * Answers true if this store contains at least one feature, else false + * + * @return + */ + public abstract boolean hasFeatures(); + + /** + * Returns a set of the distinct feature groups present in the collection. The + * set may include null. The boolean parameter determines whether the groups + * for positional or for non-positional features are returned. The optional + * type parameter may be used to restrict to groups for specified feature + * types. + * + * @param positionalFeatures + * @param type + * @return + */ + public abstract Set getFeatureGroups(boolean positionalFeatures, + String... type); + + /** + * Answers the set of distinct feature types for which there is at least one + * feature with one of the given feature group(s). The boolean parameter + * determines whether the groups for positional or for non-positional features + * are returned. + * + * @param positionalFeatures + * @param groups + * @return + */ + public abstract Set getFeatureTypesForGroups( + boolean positionalFeatures, String... groups); + + /** + * Answers an immutable set of the distinct feature types for which a feature + * is stored + * @return + */ + public abstract Set getFeatureTypes(); + +} \ No newline at end of file diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 26f9964..067d8ae 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -23,6 +23,7 @@ package jalview.gui; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsControllerI; import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.Help.HelpId; @@ -60,6 +61,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Arrays; +import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -470,43 +472,19 @@ public class FeatureSettings extends JPanel implements @Override synchronized public void discoverAllFeatureData() { - Vector allFeatures = new Vector(); - Vector allGroups = new Vector(); - SequenceFeature[] tmpfeatures; - String group; - for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++) - { - tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i) - .getSequenceFeatures(); - if (tmpfeatures == null) - { - continue; - } + Set allGroups = new HashSet(); + AlignmentI alignment = af.getViewport().getAlignment(); - int index = 0; - while (index < tmpfeatures.length) + for (int i = 0; i < alignment.getHeight(); i++) + { + SequenceI seq = alignment.getSequenceAt(i); + for (String group : seq.getFeatures().getFeatureGroups(true)) { - if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0) + if (group != null && !allGroups.contains(group)) { - index++; - continue; - } - - if (tmpfeatures[index].getFeatureGroup() != null) - { - group = tmpfeatures[index].featureGroup; - if (!allGroups.contains(group)) - { - allGroups.addElement(group); - checkGroupState(group); - } + allGroups.add(group); + checkGroupState(group); } - - if (!allFeatures.contains(tmpfeatures[index].getType())) - { - allFeatures.addElement(tmpfeatures[index].getType()); - } - index++; } } @@ -572,7 +550,7 @@ public class FeatureSettings extends JPanel implements synchronized void resetTable(String[] groupChanged) { - if (resettingTable == true) + if (resettingTable) { return; } @@ -591,13 +569,13 @@ public class FeatureSettings extends JPanel implements for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++) { - tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i) - .getSequenceFeatures(); + SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i); + tmpfeatures = seq.getSequenceFeatures(); if (tmpfeatures == null) { continue; } - + Set types = seq.getFeatures().getFeatureTypes(); int index = 0; while (index < tmpfeatures.length) { diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java index 8aabc56..b04f810 100644 --- a/test/jalview/datamodel/features/FeatureStoreTest.java +++ b/test/jalview/datamodel/features/FeatureStoreTest.java @@ -365,12 +365,16 @@ public class FeatureStoreTest Float.NaN, null); assertTrue(fs.addFeature(sf1)); + assertEquals(fs.size(true), 1); // positional + assertEquals(fs.size(false), 0); // non-positional /* * re-adding the same or an identical feature should fail */ assertFalse(fs.addFeature(sf1)); + assertEquals(fs.size(true), 1); assertFalse(fs.addFeature(sf2)); + assertEquals(fs.size(true), 1); } @Test(groups = "Functional") @@ -378,6 +382,7 @@ public class FeatureStoreTest { FeatureStore fs = new FeatureStore(); assertTrue(fs.isEmpty()); + assertEquals(fs.size(true), 0); /* * non-nested feature @@ -386,8 +391,10 @@ public class FeatureStoreTest Float.NaN, null); fs.addFeature(sf1); assertFalse(fs.isEmpty()); + assertEquals(fs.size(true), 1); fs.delete(sf1); assertTrue(fs.isEmpty()); + assertEquals(fs.size(true), 0); /* * non-positional feature @@ -395,8 +402,11 @@ public class FeatureStoreTest sf1 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, null); fs.addFeature(sf1); assertFalse(fs.isEmpty()); + assertEquals(fs.size(false), 1); // non-positional + assertEquals(fs.size(true), 0); // positional fs.delete(sf1); assertTrue(fs.isEmpty()); + assertEquals(fs.size(false), 0); /* * contact feature @@ -404,8 +414,10 @@ public class FeatureStoreTest sf1 = new SequenceFeature("Disulfide bond", "", 19, 49, Float.NaN, null); fs.addFeature(sf1); assertFalse(fs.isEmpty()); + assertEquals(fs.size(true), 1); fs.delete(sf1); assertTrue(fs.isEmpty()); + assertEquals(fs.size(true), 0); /* * sf2, sf3 added as nested features @@ -418,14 +430,18 @@ public class FeatureStoreTest fs.addFeature(sf1); fs.addFeature(sf2); fs.addFeature(sf3); + assertEquals(fs.size(true), 3); assertTrue(fs.delete(sf1)); + assertEquals(fs.size(true), 2); // FeatureStore should now only contain features in the NCList assertTrue(fs.nonNestedFeatures.isEmpty()); assertEquals(fs.nestedFeatures.size(), 2); assertFalse(fs.isEmpty()); assertTrue(fs.delete(sf2)); + assertEquals(fs.size(true), 1); assertFalse(fs.isEmpty()); assertTrue(fs.delete(sf3)); + assertEquals(fs.size(true), 0); assertTrue(fs.isEmpty()); // all gone } @@ -481,4 +497,29 @@ public class FeatureStoreTest groups = fs.getFeatureGroups(true); assertTrue(groups.isEmpty()); } + + @Test(groups = "Functional") + public void testGetAverageFeatureLength() + { + FeatureStore fs = new FeatureStore(); + assertEquals(fs.getAverageFeatureLength(), 0f); + + addFeature(fs, 10, 20); // 11 + assertEquals(fs.getAverageFeatureLength(), 11f); + addFeature(fs, 17, 37); // 21 + addFeature(fs, 14, 74); // 61 + assertEquals(fs.getAverageFeatureLength(), 31f); + + // non-positional features don't count + SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 0, 0, 1f, + "group1"); + fs.addFeature(sf1); + assertEquals(fs.getAverageFeatureLength(), 31f); + + // contact features count 1 + SequenceFeature sf2 = new SequenceFeature("disulphide bond", "desc", + 15, 35, 1f, "group1"); + fs.addFeature(sf2); + assertEquals(fs.getAverageFeatureLength(), 94f / 4f); + } } diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java index 84a07b4..f9b9b6e 100644 --- a/test/jalview/datamodel/features/SequenceFeaturesTest.java +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -16,7 +16,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testGetPositionalFeatures() { - SequenceFeatures store = new SequenceFeatures(); + SequenceFeaturesI store = new SequenceFeatures(); SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, Float.NaN, null); store.add(sf1); @@ -96,7 +96,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testGetContactFeatures() { - SequenceFeatures store = new SequenceFeatures(); + SequenceFeaturesI store = new SequenceFeatures(); // non-contact SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, Float.NaN, null); @@ -148,7 +148,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testGetNonPositionalFeatures() { - SequenceFeatures store = new SequenceFeatures(); + SequenceFeaturesI store = new SequenceFeatures(); // positional SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, Float.NaN, null); @@ -211,7 +211,7 @@ public class SequenceFeaturesTest * @param to * @return */ - SequenceFeature addFeature(SequenceFeatures sf, String type, int from, + SequenceFeature addFeature(SequenceFeaturesI sf, String type, int from, int to) { SequenceFeature sf1 = new SequenceFeature(type, "", from, to, @@ -224,7 +224,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testFindFeatures() { - SequenceFeatures sf = new SequenceFeatures(); + SequenceFeaturesI sf = new SequenceFeatures(); SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); SequenceFeature sf2 = addFeature(sf, "Pfam", 1, 15); SequenceFeature sf3 = addFeature(sf, "Pfam", 20, 30); @@ -288,7 +288,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testDelete() { - SequenceFeatures sf = new SequenceFeatures(); + SequenceFeaturesI sf = new SequenceFeatures(); SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); assertTrue(sf.getPositionalFeatures().contains(sf1)); @@ -302,7 +302,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testHasFeatures() { - SequenceFeatures sf = new SequenceFeatures(); + SequenceFeaturesI sf = new SequenceFeatures(); assertFalse(sf.hasFeatures()); SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); @@ -319,7 +319,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testGetFeatureGroups() { - SequenceFeatures sf = new SequenceFeatures(); + SequenceFeaturesI sf = new SequenceFeatures(); assertTrue(sf.getFeatureGroups(true).isEmpty()); assertTrue(sf.getFeatureGroups(false).isEmpty()); @@ -412,7 +412,7 @@ public class SequenceFeaturesTest @Test(groups = "Functional") public void testGetFeatureTypesForGroups() { - SequenceFeatures sf = new SequenceFeatures(); + SequenceFeaturesI sf = new SequenceFeatures(); assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty()); /* -- 1.7.10.2