X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fdatamodel%2Ffeatures%2FFeatureStore.java;h=ccd8b66af9774e88b13d58b6720b3f6feb91484a;hb=8406f2ced29d2c04dbabaf1cde80b1a7fa8ce8ce;hp=951b7a5e0859b384e2b3dd9346123550506d58e5;hpb=5929abf0b458a833c936a2f34895e29600354a54;p=jalview.git diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index 951b7a5..ccd8b66 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -20,97 +20,114 @@ */ package jalview.datamodel.features; -import jalview.datamodel.ContiguousI; import jalview.datamodel.SequenceFeature; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; -/** - * A data store for a set of sequence features that supports efficient lookup of - * features overlapping a given range. Intended for (but not limited to) storage - * of features for one sequence and feature type. - * - * @author gmcarstairs - * - */ -public class FeatureStore +public abstract class FeatureStore implements FeatureStoreI { + /** - * a class providing criteria for performing a binary search of a list + * Answers the 'length' of the feature, counting 0 for non-positional features + * and 1 for contact features + * + * @param feature + * @return */ - abstract static class SearchCriterion + protected static int getFeatureLength(SequenceFeature feature) { - /** - * Answers true if the entry passes the search criterion test - * - * @param entry - * @return - */ - abstract boolean compare(SequenceFeature entry); - - /** - * serves a search condition for finding the first feature whose start - * position follows a given target location - * - * @param target - * @return - */ - static SearchCriterion byStart(final long target) + if (feature.isNonPositional()) + { + return 0; + } + if (feature.isContactFeature()) { - return new SearchCriterion() { + return 1; + } + return 1 + feature.getEnd() - feature.getBegin(); + } - @Override - boolean compare(SequenceFeature entry) - { - return entry.getBegin() >= target; - } - }; + /** + * Answers true if the list contains the feature, else false. This method is + * optimised for the condition that the list is sorted on feature start + * position ascending, and will give unreliable results if this does not hold. + * + * @param features + * @param feature + * @return + */ + @Override + public boolean listContains(List features, + SequenceFeature feature) + { + if (features == null || feature == null) + { + return false; } - /** - * serves a search condition for finding the first feature whose end - * position is at or follows a given target location - * - * @param target - * @return + /* + * locate the first entry in the list which does not precede the feature */ - static SearchCriterion byEnd(final long target) + int pos = findFirstBegin(features, feature.begin); + int len = features.size(); + while (pos < len) { - return new SearchCriterion() + SequenceFeature sf = features.get(pos); + if (sf.getBegin() > feature.getBegin()) { - - @Override - boolean compare(SequenceFeature entry) - { - return entry.getEnd() >= target; - } - }; + return false; // no match found + } + if (sf.equals(feature)) + { + return true; + } + pos++; } + return false; + } - /** - * serves a search condition for finding the first feature which follows the - * given range as determined by a supplied comparator - * - * @param target - * @return - */ - static SearchCriterion byFeature(final ContiguousI to, - final Comparator rc) + /** + * A helper method to return the maximum of two floats, where a non-NaN value + * is treated as 'greater than' a NaN value (unlike Math.max which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float max(float f1, float f2) + { + if (Float.isNaN(f1)) { - return new SearchCriterion() - { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); + } + } - @Override - boolean compare(SequenceFeature entry) - { - return rc.compare(entry, to) >= 0; - } - }; + /** + * A helper method to return the minimum of two floats, where a non-NaN value + * is treated as 'less than' a NaN value (unlike Math.min which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float min(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.min(f1, f2); } } @@ -121,14 +138,6 @@ public class FeatureStore List nonPositionalFeatures; /* - * An ordered list of features, with the promise that no feature in the list - * properly contains any other. This constraint allows bounded linear search - * of the list for features overlapping a region. - * Contact features are not included in this list. - */ - List nonNestedFeatures; - - /* * contact features ordered by first contact position */ List contactFeatureStarts; @@ -139,13 +148,10 @@ public class FeatureStore List contactFeatureEnds; /* - * Nested Containment List is used to hold any features that are nested - * within (properly contained by) any other feature. This is a recursive tree - * which supports depth-first scan for features overlapping a range. - * It is used here as a 'catch-all' fallback for features that cannot be put - * into a simple ordered list without invalidating the search methods. + * IntervalStore holds remaining features and provides efficient + * query for features overlapping any given interval */ - NCList nestedFeatures; + Collection features; /* * Feature groups represented in stored positional features @@ -178,254 +184,14 @@ public class FeatureStore */ public FeatureStore() { - nonNestedFeatures = new ArrayList(); - positionalFeatureGroups = new HashSet(); - nonPositionalFeatureGroups = new HashSet(); + positionalFeatureGroups = new HashSet<>(); + nonPositionalFeatureGroups = new HashSet<>(); positionalMinScore = Float.NaN; positionalMaxScore = Float.NaN; nonPositionalMinScore = Float.NaN; nonPositionalMaxScore = Float.NaN; - // we only construct nonPositionalFeatures, contactFeatures - // or the NCList if we need to - } - - /** - * 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 feature - */ - public boolean addFeature(SequenceFeature feature) - { - if (contains(feature)) - { - return false; - } - - /* - * keep a record of feature groups - */ - if (!feature.isNonPositional()) - { - positionalFeatureGroups.add(feature.getFeatureGroup()); - } - - boolean added = false; - - if (feature.isContactFeature()) - { - added = addContactFeature(feature); - } - else if (feature.isNonPositional()) - { - added = addNonPositionalFeature(feature); - } - else - { - added = addNonNestedFeature(feature); - if (!added) - { - /* - * detected a nested feature - put it in the NCList structure - */ - added = addNestedFeature(feature); - } - } - - if (added) - { - /* - * record the total extent of positional features, to make - * getTotalFeatureLength possible; we count the length of a - * contact feature as 1 - */ - totalExtent += getFeatureLength(feature); - - /* - * record the minimum and maximum score for positional - * and non-positional features - */ - float score = feature.getScore(); - if (!Float.isNaN(score)) - { - if (feature.isNonPositional()) - { - nonPositionalMinScore = min(nonPositionalMinScore, score); - nonPositionalMaxScore = max(nonPositionalMaxScore, score); - } - else - { - positionalMinScore = min(positionalMinScore, score); - positionalMaxScore = max(positionalMaxScore, score); - } - } - } - - return added; - } - - /** - * Answers true if this store contains the given feature (testing by - * SequenceFeature.equals), else false - * - * @param feature - * @return - */ - public boolean contains(SequenceFeature feature) - { - if (feature.isNonPositional()) - { - return nonPositionalFeatures == null ? false : nonPositionalFeatures - .contains(feature); - } - - if (feature.isContactFeature()) - { - return contactFeatureStarts == null ? false : listContains( - contactFeatureStarts, feature); - } - - if (listContains(nonNestedFeatures, feature)) - { - return true; - } - - return nestedFeatures == null ? false : nestedFeatures - .contains(feature); - } - - /** - * Answers the 'length' of the feature, counting 0 for non-positional features - * and 1 for contact features - * - * @param feature - * @return - */ - protected static int getFeatureLength(SequenceFeature feature) - { - if (feature.isNonPositional()) - { - return 0; - } - if (feature.isContactFeature()) - { - return 1; - } - return 1 + feature.getEnd() - feature.getBegin(); - } - - /** - * Adds the feature to the list of non-positional features (with lazy - * instantiation of the list if it is null), and returns true. The feature - * group is added to the set of distinct feature groups for non-positional - * features. This method allows duplicate features, so test before calling to - * prevent this. - * - * @param feature - */ - protected boolean addNonPositionalFeature(SequenceFeature feature) - { - if (nonPositionalFeatures == null) - { - nonPositionalFeatures = new ArrayList(); - } - - nonPositionalFeatures.add(feature); - - nonPositionalFeatureGroups.add(feature.getFeatureGroup()); - - return true; - } - - /** - * Adds one feature to the NCList that can manage nested features (creating - * the NCList if necessary), and returns true. If the feature is already - * stored in the NCList (by equality test), then it is not added, and this - * method returns false. - */ - protected synchronized boolean addNestedFeature(SequenceFeature feature) - { - if (nestedFeatures == null) - { - nestedFeatures = new NCList<>(feature); - return true; - } - return nestedFeatures.add(feature, false); - } - - /** - * Add a feature to the list of non-nested features, maintaining the ordering - * of the list. A check is made for whether the feature is nested in (properly - * contained by) an existing feature. If there is no nesting, the feature is - * added to the list and the method returns true. If nesting is found, the - * feature is not added and the method returns false. - * - * @param feature - * @return - */ - protected boolean addNonNestedFeature(SequenceFeature feature) - { - synchronized (nonNestedFeatures) - { - /* - * find the first stored feature which doesn't precede the new one - */ - int insertPosition = binarySearch(nonNestedFeatures, - SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); - - /* - * fail if we detect feature enclosure - of the new feature by - * the one preceding it, or of the next feature by the new one - */ - if (insertPosition > 0) - { - if (encloses(nonNestedFeatures.get(insertPosition - 1), feature)) - { - return false; - } - } - if (insertPosition < nonNestedFeatures.size()) - { - if (encloses(feature, nonNestedFeatures.get(insertPosition))) - { - return false; - } - } - - /* - * checks passed - add the feature - */ - nonNestedFeatures.add(insertPosition, feature); - - return true; - } - } - - /** - * Answers true if range1 properly encloses range2, else false - * - * @param range1 - * @param range2 - * @return - */ - protected boolean encloses(ContiguousI range1, ContiguousI range2) - { - int begin1 = range1.getBegin(); - int begin2 = range2.getBegin(); - int end1 = range1.getEnd(); - int end2 = range2.getEnd(); - if (begin1 == begin2 && end1 > end2) - { - return true; - } - if (begin1 < begin2 && end1 >= end2) - { - return true; - } - return false; + // we only construct nonPositionalFeatures, contactFeatures if we need to } /** @@ -441,230 +207,105 @@ public class FeatureStore { if (contactFeatureStarts == null) { - contactFeatureStarts = new ArrayList(); - } - if (contactFeatureEnds == null) - { - contactFeatureEnds = new ArrayList(); + contactFeatureStarts = new ArrayList<>(); + contactFeatureEnds = new ArrayList<>(); } /* + * insert into list sorted by start (first contact position): + * binary search the sorted list to find the insertion point + */ + contactFeatureStarts.add( + findFirstBegin(contactFeatureStarts, feature.begin), feature); + /* + * insert into list sorted by end (second contact position): * binary search the sorted list to find the insertion point */ - int insertPosition = binarySearch(contactFeatureStarts, - SearchCriterion.byFeature(feature, - RangeComparator.BY_START_POSITION)); - contactFeatureStarts.add(insertPosition, feature); - // and resort to mak siccar...just in case insertion point not quite right - Collections.sort(contactFeatureStarts, RangeComparator.BY_START_POSITION); - - insertPosition = binarySearch(contactFeatureStarts, - SearchCriterion.byFeature(feature, - RangeComparator.BY_END_POSITION)); - contactFeatureEnds.add(feature); - Collections.sort(contactFeatureEnds, RangeComparator.BY_END_POSITION); + contactFeatureEnds.add(findFirstEnd(contactFeatureEnds, feature.end), + feature); return true; } /** - * Answers true if the list contains the feature, else false. This method is - * optimised for the condition that the list is sorted on feature start - * position ascending, and will give unreliable results if this does not hold. + * 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 features * @param feature - * @return */ - protected static boolean listContains(List features, - SequenceFeature feature) + + @Override + public boolean addFeature(SequenceFeature feature) { - if (features == null || feature == null) + if (contains(feature)) { return false; } /* - * locate the first entry in the list which does not precede the feature + * keep a record of feature groups */ - int pos = binarySearch(features, - SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); - int len = features.size(); - while (pos < len) + if (!feature.isNonPositional()) { - SequenceFeature sf = features.get(pos); - if (sf.getBegin() > feature.getBegin()) - { - return false; // no match found - } - if (sf.equals(feature)) - { - return true; - } - pos++; + positionalFeatureGroups.add(feature.getFeatureGroup()); } - return false; - } - /** - * Returns a (possibly empty) list of features whose extent overlaps the given - * range. The returned list is not ordered. Contact features are included if - * either of the contact points lies within the range. - * - * @param start - * start position of overlap range (inclusive) - * @param end - * end position of overlap range (inclusive) - * @return - */ - public List findOverlappingFeatures(long start, long end) - { - List result = new ArrayList<>(); - - findNonNestedFeatures(start, end, result); - - findContactFeatures(start, end, result); - - if (nestedFeatures != null) + if (feature.isContactFeature()) { - result.addAll(nestedFeatures.findOverlaps(start, end)); + addContactFeature(feature); } - - return result; - } - - /** - * Adds contact features to the result list where either the second or the - * first contact position lies within the target range - * - * @param from - * @param to - * @param result - */ - protected void findContactFeatures(long from, long to, - List result) - { - if (contactFeatureStarts != null) + else if (feature.isNonPositional()) { - findContactStartFeatures(from, to, result); + addNonPositionalFeature(feature); } - if (contactFeatureEnds != null) + else { - findContactEndFeatures(from, to, result); + addNestedFeature(feature); } - } - /** - * Adds to the result list any contact features whose end (second contact - * point), but not start (first contact point), lies in the query from-to - * range - * - * @param from - * @param to - * @param result - */ - protected void findContactEndFeatures(long from, long to, - List result) - { /* - * find the first contact feature (if any) that does not lie - * entirely before the target range + * record the total extent of positional features, to make + * getTotalFeatureLength possible; we count the length of a + * contact feature as 1 */ - int startPosition = binarySearch(contactFeatureEnds, - SearchCriterion.byEnd(from)); - for (; startPosition < contactFeatureEnds.size(); startPosition++) - { - SequenceFeature sf = contactFeatureEnds.get(startPosition); - if (!sf.isContactFeature()) - { - System.err.println("Error! non-contact feature type " - + sf.getType() + " in contact features list"); - continue; - } - - int begin = sf.getBegin(); - if (begin >= from && begin <= to) - { - /* - * this feature's first contact position lies in the search range - * so we don't include it in results a second time - */ - continue; - } - - int end = sf.getEnd(); - if (end >= from && end <= to) - { - result.add(sf); - } - if (end > to) - { - break; - } - } - } + totalExtent += getFeatureLength(feature); - /** - * Adds non-nested features to the result list that lie within the target - * range. Non-positional features (start=end=0), contact features and nested - * features are excluded. - * - * @param from - * @param to - * @param result - */ - protected void findNonNestedFeatures(long from, long to, - List result) - { /* - * find the first feature whose end position is - * after the target range start + * record the minimum and maximum score for positional + * and non-positional features */ - int startIndex = binarySearch(nonNestedFeatures, - SearchCriterion.byEnd(from)); - - final int startIndex1 = startIndex; - int i = startIndex1; - while (i < nonNestedFeatures.size()) + float score = feature.getScore(); + if (!Float.isNaN(score)) { - SequenceFeature sf = nonNestedFeatures.get(i); - if (sf.getBegin() > to) + if (feature.isNonPositional()) { - break; + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); } - if (sf.getBegin() <= to && sf.getEnd() >= from) + else { - result.add(sf); + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); } - i++; } + + return true; } - /** - * Adds contact features whose start position lies in the from-to range to the - * result list - * - * @param from - * @param to - * @param result - */ - protected void findContactStartFeatures(long from, long to, - List result) + private void addFeaturesForGroup(String group, + Collection sfs, List result) { - int startPosition = binarySearch(contactFeatureStarts, - SearchCriterion.byStart(from)); - - for (; startPosition < contactFeatureStarts.size(); startPosition++) + if (sfs == null) { - SequenceFeature sf = contactFeatureStarts.get(startPosition); - if (!sf.isContactFeature()) - { - System.err.println("Error! non-contact feature type " - + sf.getType() + " in contact features list"); - continue; - } - int begin = sf.getBegin(); - if (begin >= from && begin <= to) + return; + } + for (SequenceFeature sf : sfs) + { + String featureGroup = sf.getFeatureGroup(); + if (group == null && featureGroup == null + || group != null && group.equals(featureGroup)) { result.add(sf); } @@ -672,67 +313,62 @@ public class FeatureStore } /** - * Answers a list of all positional features stored, in no guaranteed order + * Adds one feature to the IntervalStore that can manage nested features + * (creating the IntervalStore if necessary) + */ + abstract protected void addNestedFeature(SequenceFeature feature); + + /** + * Adds the feature to the list of non-positional features (with lazy + * instantiation of the list if it is null), and returns true. The feature + * group is added to the set of distinct feature groups for non-positional + * features. This method allows duplicate features, so test before calling to + * prevent this. * - * @return + * @param feature */ - public List getPositionalFeatures() + protected boolean addNonPositionalFeature(SequenceFeature feature) { - /* - * add non-nested features (may be all features for many cases) - */ - List result = new ArrayList<>(); - result.addAll(nonNestedFeatures); - - /* - * add any contact features - from the list by start position - */ - if (contactFeatureStarts != null) + if (nonPositionalFeatures == null) { - result.addAll(contactFeatureStarts); + nonPositionalFeatures = new ArrayList<>(); } - /* - * add any nested features - */ - if (nestedFeatures != null) - { - result.addAll(nestedFeatures.getEntries()); - } + nonPositionalFeatures.add(feature); - return result; + nonPositionalFeatureGroups.add(feature.getFeatureGroup()); + + return true; } /** - * Answers a list of all contact features. If there are none, returns an - * immutable empty list. + * Answers true if this store contains the given feature (testing by + * SequenceFeature.equals), else false * + * @param feature * @return */ - public List getContactFeatures() + @Override + public boolean contains(SequenceFeature feature) { - if (contactFeatureStarts == null) + if (feature.isNonPositional()) { - return Collections.emptyList(); + return nonPositionalFeatures == null ? false + : nonPositionalFeatures.contains(feature); } - return new ArrayList<>(contactFeatureStarts); - } - /** - * Answers a list of all non-positional features. If there are none, returns - * an immutable empty list. - * - * @return - */ - public List getNonPositionalFeatures() - { - if (nonPositionalFeatures == null) + if (feature.isContactFeature()) { - return Collections.emptyList(); + return contactFeatureStarts != null + && listContains(contactFeatureStarts, feature); } - return new ArrayList<>(nonPositionalFeatures); + + return features == null ? false : containsFeature(feature); } + + abstract protected boolean containsFeature(SequenceFeature feature); + /** * 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 @@ -741,15 +377,14 @@ public class FeatureStore * * @param sf */ + + @Override public synchronized boolean delete(SequenceFeature sf) { - /* - * try the non-nested positional features first - */ - boolean removed = nonNestedFeatures.remove(sf); + boolean removed = false; /* - * if not found, try contact positions (and if found, delete + * try contact positions (and if found, delete * from both lists of contact positions) */ if (!removed && contactFeatureStarts != null) @@ -775,9 +410,9 @@ public class FeatureStore /* * if not found, try nested features */ - if (!removed && nestedFeatures != null) + if (!removed && features != null) { - removed = nestedFeatures.delete(sf); + removed = features.remove(sf); } if (removed) @@ -788,100 +423,64 @@ public class FeatureStore return removed; } - /** - * Rescan all features to recompute any cached values after an entry has been - * deleted. This is expected to be an infrequent event, so performance here is - * not critical. - */ - protected synchronized void rescanAfterDelete() - { - positionalFeatureGroups.clear(); - nonPositionalFeatureGroups.clear(); - totalExtent = 0; - positionalMinScore = Float.NaN; - positionalMaxScore = Float.NaN; - nonPositionalMinScore = Float.NaN; - nonPositionalMaxScore = Float.NaN; + abstract protected void findContactFeatures(long from, long to, + List result); - /* - * scan non-positional features for groups and scores - */ - for (SequenceFeature sf : getNonPositionalFeatures()) - { - nonPositionalFeatureGroups.add(sf.getFeatureGroup()); - float score = sf.getScore(); - nonPositionalMinScore = min(nonPositionalMinScore, score); - nonPositionalMaxScore = max(nonPositionalMaxScore, score); - } + abstract protected int findFirstBegin(List list, + long pos); - /* - * scan positional features for groups, scores and extents - */ - for (SequenceFeature sf : getPositionalFeatures()) - { - positionalFeatureGroups.add(sf.getFeatureGroup()); - float score = sf.getScore(); - positionalMinScore = min(positionalMinScore, score); - positionalMaxScore = max(positionalMaxScore, score); - totalExtent += getFeatureLength(sf); - } + abstract protected int findFirstEnd(List list, long pos); + + @Override + public List findOverlappingFeatures(long start, long end) + { + return findOverlappingFeatures(start, end, null); } - /** - * A helper method to return the minimum of two floats, where a non-NaN value - * is treated as 'less than' a NaN value (unlike Math.min which does the - * opposite) - * - * @param f1 - * @param f2 - */ - protected static float min(float f1, float f2) + @Override + public List getContactFeatures() { - if (Float.isNaN(f1)) - { - return Float.isNaN(f2) ? f1 : f2; - } - else - { - return Float.isNaN(f2) ? f1 : Math.min(f1, f2); - } + return getContactFeatures(new ArrayList<>()); } /** - * A helper method to return the maximum of two floats, where a non-NaN value - * is treated as 'greater than' a NaN value (unlike Math.max which does the - * opposite) + * Answers a list of all contact features. If there are none, returns an + * immutable empty list. * - * @param f1 - * @param f2 + * @return */ - protected static float max(float f1, float f2) + + @Override + public List getContactFeatures( + List result) { - if (Float.isNaN(f1)) - { - return Float.isNaN(f2) ? f1 : f2; - } - else + if (contactFeatureStarts != null) { - return Float.isNaN(f2) ? f1 : Math.max(f1, f2); + result.addAll(contactFeatureStarts); } + return result; } /** - * Answers true if this store has no features, else false + * Answers the number of positional (or non-positional) features stored. + * Contact features count as 1. * + * @param positional * @return */ - public boolean isEmpty() + + @Override + public int getFeatureCount(boolean positional) { - boolean hasFeatures = !nonNestedFeatures.isEmpty() - || (contactFeatureStarts != null && !contactFeatureStarts - .isEmpty()) - || (nonPositionalFeatures != null && !nonPositionalFeatures - .isEmpty()) - || (nestedFeatures != null && nestedFeatures.size() > 0); + if (!positional) + { + return nonPositionalFeatures == null ? 0 + : nonPositionalFeatures.size(); + } + + return (contactFeatureStarts == null ? 0 : contactFeatureStarts.size()) + + features.size(); - return !hasFeatures; } /** @@ -892,6 +491,8 @@ public class FeatureStore * @param positionalFeatures * @return */ + + @Override public Set getFeatureGroups(boolean positionalFeatures) { if (positionalFeatures) @@ -900,87 +501,68 @@ public class FeatureStore } else { - return nonPositionalFeatureGroups == null ? Collections - . emptySet() : Collections - .unmodifiableSet(nonPositionalFeatureGroups); + return nonPositionalFeatureGroups == null + ? Collections. emptySet() + : Collections.unmodifiableSet(nonPositionalFeatureGroups); } } - /** - * Performs a binary search of the (sorted) list to find the index of the - * first entry which returns true for the given comparator function. Returns - * the length of the list if there is no such entry. - * - * @param features - * @param sc - * @return - */ - protected static int binarySearch(List features, - SearchCriterion sc) + @Override + public Collection getFeatures() { - int start = 0; - int end = features.size() - 1; - int matched = features.size(); - - while (start <= end) - { - int mid = (start + end) / 2; - SequenceFeature entry = features.get(mid); - boolean compare = sc.compare(entry); - if (compare) - { - matched = mid; - end = mid - 1; - } - else - { - start = mid + 1; - } - } - - return matched; + return features; } /** - * Answers the number of positional (or non-positional) features stored. - * Contact features count as 1. + * Answers a list of all either positional or non-positional features whose + * feature group matches the given group (which may be null) * * @param positional + * @param group * @return */ - public int getFeatureCount(boolean positional) + + @Override + public List getFeaturesForGroup(boolean positional, + String group) { - if (!positional) + List result = new ArrayList<>(); + + /* + * if we know features don't include the target group, no need + * to inspect them for matches + */ + if (positional && !positionalFeatureGroups.contains(group) + || !positional && !nonPositionalFeatureGroups.contains(group)) { - return nonPositionalFeatures == null ? 0 : nonPositionalFeatures - .size(); + return result; } - int size = nonNestedFeatures.size(); - - if (contactFeatureStarts != null) + if (positional) { - // note a contact feature (start/end) counts as one - size += contactFeatureStarts.size(); + addFeaturesForGroup(group, contactFeatureStarts, result); + addFeaturesForGroup(group, features, result); } - - if (nestedFeatures != null) + else { - size += nestedFeatures.size(); + addFeaturesForGroup(group, nonPositionalFeatures, result); } - - return size; + return result; } /** - * Answers the total length of positional features (or zero if there are - * none). Contact features contribute a value of 1 to the total. + * Answers the maximum score held for positional or non-positional features. + * This may be Float.NaN if there are no features, are none has a non-NaN + * score. * + * @param positional * @return */ - public int getTotalFeatureLength() + + @Override + public float getMaximumScore(boolean positional) { - return totalExtent; + return positional ? positionalMaxScore : nonPositionalMaxScore; } /** @@ -991,59 +573,154 @@ public class FeatureStore * @param positional * @return */ + + @Override public float getMinimumScore(boolean positional) { return positional ? positionalMinScore : nonPositionalMinScore; } + @Override + public List getNonPositionalFeatures() + { + return getNonPositionalFeatures(new ArrayList<>()); + } + /** - * Answers the maximum score held for positional or non-positional features. - * This may be Float.NaN if there are no features, are none has a non-NaN - * score. + * Answers a list of all non-positional features. If there are none, returns + * an immutable empty list. * - * @param positional * @return */ - public float getMaximumScore(boolean positional) + + @Override + public List getNonPositionalFeatures( + List result) { - return positional ? positionalMaxScore : nonPositionalMaxScore; + if (nonPositionalFeatures != null) + { + result.addAll(nonPositionalFeatures); + } + return result; + } + + @Override + public List getPositionalFeatures() + { + return getPositionalFeatures(new ArrayList<>()); } /** - * Answers a list of all either positional or non-positional features whose - * feature group matches the given group (which may be null) + * Answers a list of all positional features stored, in no guaranteed order * - * @param positional - * @param group * @return */ - public List getFeaturesForGroup(boolean positional, - String group) + + @Override + public List getPositionalFeatures( + List result) { - List result = new ArrayList<>(); /* - * if we know features don't include the target group, no need - * to inspect them for matches + * add any contact features - from the list by start position */ - if (positional && !positionalFeatureGroups.contains(group) - || !positional && !nonPositionalFeatureGroups.contains(group)) + if (contactFeatureStarts != null) { - return result; + result.addAll(contactFeatureStarts); } - List sfs = positional ? getPositionalFeatures() - : getNonPositionalFeatures(); - for (SequenceFeature sf : sfs) + /* + * add any nested features + */ + if (features != null) { - String featureGroup = sf.getFeatureGroup(); - if (group == null && featureGroup == null || group != null - && group.equals(featureGroup)) + result.addAll(features); + } + + return result; + } + + /** + * Answers the total length of positional features (or zero if there are + * none). Contact features contribute a value of 1 to the total. + * + * @return + */ + + @Override + public int getTotalFeatureLength() + { + return totalExtent; + } + + /** + * Answers true if this store has no features, else false + * + * @return + */ + + @Override + public boolean isEmpty() + { + boolean hasFeatures = (contactFeatureStarts != null + && !contactFeatureStarts.isEmpty()) + || (nonPositionalFeatures != null + && !nonPositionalFeatures.isEmpty()) + || features.size() > 0; + + return !hasFeatures; + } + + /** + * Rescan all features to recompute any cached values after an entry has been + * deleted. This is expected to be an infrequent event, so performance here is + * not critical. + */ + protected synchronized void rescanAfterDelete() + { + positionalFeatureGroups.clear(); + nonPositionalFeatureGroups.clear(); + totalExtent = 0; + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + /* + * scan non-positional features for groups and scores + */ + if (nonPositionalFeatures != null) + { + for (SequenceFeature sf : nonPositionalFeatures) { - result.add(sf); + nonPositionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); } } - return result; + + /* + * scan positional features for groups, scores and extents + */ + + rescanPositional(contactFeatureStarts); + rescanPositional(features); + } + + private void rescanPositional(Collection sfs) + { + if (sfs == null) + { + return; + } + for (SequenceFeature sf : sfs) + { + positionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + totalExtent += getFeatureLength(sf); + } } /** @@ -1055,6 +732,8 @@ public class FeatureStore * @param shiftBy * @return */ + + @Override public synchronized boolean shiftFeatures(int fromPosition, int shiftBy) { /* @@ -1086,4 +765,5 @@ public class FeatureStore } return modified; } + }