X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fdatamodel%2Ffeatures%2FFeatureStore.java;h=c6a49dbbc56b3a39d26cf26f3ea2080d23953d64;hb=c8863c027ddace3961405ed0051f104f1ec99581;hp=721ac18511118cff0d36b0b88d1f75d3e090e613;hpb=289a9b77b55b7a82f3fcc29bde33b23e3e80e1d4;p=jalview.git diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index 721ac18..c6a49db 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -72,10 +72,6 @@ public class FeatureStore } } - Comparator startOrdering = new RangeComparator(true); - - Comparator endOrdering = new RangeComparator(false); - /* * Non-positional features have no (zero) start/end position. * Kept as a separate list in case this criterion changes in future. @@ -121,6 +117,20 @@ 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; + + float positionalMinScore; + + float positionalMaxScore; + + float nonPositionalMinScore; + + float nonPositionalMaxScore; + /** * Constructor */ @@ -128,6 +138,11 @@ public class FeatureStore { nonNestedFeatures = new ArrayList(); 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 @@ -163,7 +178,7 @@ public class FeatureStore } else { - if (!nonNestedFeatures.contains(feature)) + if (!contains(nonNestedFeatures, feature)) { added = addNonNestedFeature(feature); if (!added) @@ -176,10 +191,59 @@ public class FeatureStore } } + 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 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. If the * non-positional features already include the new feature (by equality test), @@ -193,7 +257,6 @@ public class FeatureStore if (nonPositionalFeatures == null) { nonPositionalFeatures = new ArrayList(); - nonPositionalFeatureGroups = new HashSet(); } if (nonPositionalFeatures.contains(feature)) { @@ -241,7 +304,7 @@ public class FeatureStore * find the first stored feature which doesn't precede the new one */ int insertPosition = binarySearch(nonNestedFeatures, - SearchCriterion.byFeature(feature, startOrdering)); + SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); /* * fail if we detect feature enclosure - of the new feature by @@ -315,21 +378,70 @@ public class FeatureStore contactFeatureEnds = new ArrayList(); } - // TODO binary search for insertion points! - if (contactFeatureStarts.contains(feature)) + if (contains(contactFeatureStarts, feature)) { return false; } - contactFeatureStarts.add(feature); - Collections.sort(contactFeatureStarts, startOrdering); + /* + * 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, endOrdering); + Collections.sort(contactFeatureEnds, RangeComparator.BY_END_POSITION); 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. + * + * @param features + * @param feature + * @return + */ + protected static boolean contains(List features, + SequenceFeature feature) + { + if (features == null || feature == null) + { + return false; + } + + /* + * locate the first entry in the list which does not precede the feature + */ + int pos = binarySearch(features, + SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); + int len = features.size(); + while (pos < len) + { + SequenceFeature sf = features.get(pos); + if (sf.getBegin() > feature.getBegin()) + { + return false; // no match found + } + if (sf.equals(feature)) + { + return true; + } + pos++; + } + 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. @@ -621,44 +733,88 @@ public class FeatureStore if (removed) { - rebuildFeatureGroups(sf.getFeatureGroup(), removedNonPositional); + rescanAfterDelete(); } return removed; } /** - * Check whether the given feature group is still represented, in either - * positional or non-positional features, and if not, remove it from the set - * of feature groups + * 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 + */ + for (SequenceFeature sf : getNonPositionalFeatures()) + { + nonPositionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + + /* + * 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); + } + } + + /** + * 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 featureGroup - * @param nonPositional + * @param f1 + * @param f2 */ - protected void rebuildFeatureGroups(String featureGroup, - boolean nonPositional) + protected static float min(float f1, float f2) { - if (nonPositional && nonPositionalFeatures != null) + if (Float.isNaN(f1)) { - boolean found = false; - for (SequenceFeature sf : nonPositionalFeatures) - { - String group = sf.getFeatureGroup(); - if (featureGroup == group - || (featureGroup != null && featureGroup.equals(group))) - { - found = true; - break; - } - } - if (!found) - { - nonPositionalFeatureGroups.remove(featureGroup); - } + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.min(f1, f2); + } + } + + /** + * 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 Float.isNaN(f2) ? f1 : f2; } - else if (!findFeatureGroup(featureGroup)) + else { - positionalFeatureGroups.remove(featureGroup); + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); } } @@ -731,7 +887,7 @@ public class FeatureStore * @param sc * @return */ - protected int binarySearch(List features, + protected static int binarySearch(List features, SearchCriterion sc) { int start = 0; @@ -756,4 +912,109 @@ public class FeatureStore return matched; } + + /** + * Answers the number of positional (or non-positional) features stored. + * Contact features count as 1. + * + * @param positional + * @return + */ + public int getFeatureCount(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 total length of positional features (or zero if there are + * none). Contact features contribute a value of 1 to the total. + * + * @return + */ + public int getTotalFeatureLength() + { + return totalExtent; + } + + /** + * Answers the minimum 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 float getMinimumScore(boolean positional) + { + return positional ? positionalMinScore : nonPositionalMinScore; + } + + /** + * 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 float getMaximumScore(boolean positional) + { + return positional ? positionalMaxScore : nonPositionalMaxScore; + } + + /** + * 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 List getFeaturesForGroup(boolean positional, + String group) + { + 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 result; + } + + List sfs = positional ? getPositionalFeatures() + : getNonPositionalFeatures(); + for (SequenceFeature sf : sfs) + { + String featureGroup = sf.getFeatureGroup(); + if (group == null && featureGroup == null || group != null + && group.equals(featureGroup)) + { + result.add(sf); + } + } + return result; + } }