X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fdatamodel%2Ffeatures%2FFeatureStore.java;h=7651016f6283a390fe21934ae627a4ef750f7b93;hb=af259f508805faf2da90585ee9a67cd7853bf5aa;hp=bd94c8a5dea1411bcf83d42a3e8ade101493fa8e;hpb=51728d3951398f9c12d7017c776953f17322cc68;p=jalview.git diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index bd94c8a..7651016 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -1,32 +1,52 @@ +/* + * 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.datamodel.features; -import jalview.datamodel.SequenceFeature; - import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import intervalstore.api.IntervalStoreI; +import intervalstore.impl.BinarySearcher; +import intervalstore.impl.BinarySearcher.Compare; +import intervalstore.impl.IntervalStore; +import jalview.datamodel.SequenceFeature; /** * A data store for a set of sequence features that supports efficient lookup of - * features overlapping a given range. + * 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 { - Comparator startOrdering = new RangeComparator(true); - - Comparator endOrdering = new RangeComparator(false); - /* - * 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. + * Non-positional features have no (zero) start/end position. + * Kept as a separate list in case this criterion changes in future. */ - List nonNestedFeatures; + List nonPositionalFeatures; /* * contact features ordered by first contact position @@ -39,190 +59,282 @@ 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; + IntervalStoreI features; + + /* + * Feature groups represented in stored positional features + * (possibly including null) + */ + Set positionalFeatureGroups; + + /* + * Feature groups represented in stored non-positional features + * (possibly including null) + */ + 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 */ public FeatureStore() { - nonNestedFeatures = new ArrayList(); - // we only construct contactFeatures and the NCList if we need to + features = new IntervalStore<>(); + positionalFeatureGroups = new HashSet<>(); + nonPositionalFeatureGroups = new HashSet<>(); + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + + // we only construct nonPositionalFeatures, contactFeatures if we need to } /** - * Add one entry to the data store + * 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 void addFeature(SequenceFeature feature) + public boolean addFeature(SequenceFeature feature) { + if (contains(feature)) + { + return false; + } + + /* + * keep a record of feature groups + */ + if (!feature.isNonPositional()) + { + positionalFeatureGroups.add(feature.getFeatureGroup()); + } + if (feature.isContactFeature()) { addContactFeature(feature); } + else if (feature.isNonPositional()) + { + addNonPositionalFeature(feature); + } else { - boolean added = addNonNestedFeature(feature); - if (!added) + addNestedFeature(feature); + } + + /* + * 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()) { - /* - * detected a nested feature - put it in the NCList structure - */ - addNestedFeature(feature); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + else + { + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); } } + + return true; } /** - * Adds one feature to the NCList that can manage nested features (creating - * the NCList if necessary) + * Answers true if this store contains the given feature (testing by + * SequenceFeature.equals), else false + * + * @param feature + * @return */ - protected synchronized void addNestedFeature(SequenceFeature feature) + public boolean contains(SequenceFeature feature) { - if (nestedFeatures == null) + if (feature.isNonPositional()) { - nestedFeatures = new NCList(feature); + return nonPositionalFeatures == null ? false : nonPositionalFeatures + .contains(feature); } - else + + if (feature.isContactFeature()) { - nestedFeatures.add(feature); + return contactFeatureStarts == null ? false : listContains( + contactFeatureStarts, feature); } + + return features == null ? false : features + .contains(feature); } /** - * 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. - *

- * Contact features are added at the position of their first contact point + * Answers the 'length' of the feature, counting 0 for non-positional features + * and 1 for contact features * * @param feature * @return */ - protected boolean addNonNestedFeature(SequenceFeature feature) + protected static int getFeatureLength(SequenceFeature feature) { - synchronized (nonNestedFeatures) + if (feature.isNonPositional()) { - int insertPosition = binarySearchForAdd(nonNestedFeatures, feature); - - /* - * 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 or append the feature - */ - if (insertPosition == nonNestedFeatures.size()) - { - nonNestedFeatures.add(feature); - } - else - { - nonNestedFeatures.add(insertPosition, feature); - } - return true; + return 0; + } + if (feature.isContactFeature()) + { + return 1; } + return 1 + feature.getEnd() - feature.getBegin(); } /** - * Answers true if range1 properly encloses range2, else false + * 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 range1 - * @param range2 - * @return + * @param feature */ - protected boolean encloses(ContiguousI range1, ContiguousI range2) + protected boolean addNonPositionalFeature(SequenceFeature feature) { - int begin1 = range1.getBegin(); - int begin2 = range2.getBegin(); - int end1 = range1.getEnd(); - int end2 = range2.getEnd(); - if (begin1 == begin2 && end1 > end2) + if (nonPositionalFeatures == null) { - return true; + nonPositionalFeatures = new ArrayList<>(); } - if (begin1 < begin2 && end1 >= end2) + + nonPositionalFeatures.add(feature); + + nonPositionalFeatureGroups.add(feature.getFeatureGroup()); + + return true; + } + + /** + * Adds one feature to the IntervalStore that can manage nested features + * (creating the IntervalStore if necessary) + */ + protected synchronized void addNestedFeature(SequenceFeature feature) + { + if (features == null) { - return true; + features = new IntervalStore<>(); } - return false; + features.add(feature); } /** - * Answers the index of the first element in the given list which follows or - * matches the given feature in the sort order. If no such element, answers - * the length of the list. + * Add a contact feature to the lists that hold them ordered by start (first + * contact) and by end (second contact) position, ensuring the lists remain + * ordered, and returns true. This method allows duplicate features to be + * added, so test before calling to avoid this. * - * @param list * @param feature - * * @return */ - protected int binarySearchForAdd(List list, SequenceFeature feature) + protected synchronized boolean addContactFeature(SequenceFeature feature) { - // TODO binary search! - int i = 0; - while (i < list.size()) + if (contactFeatureStarts == null) { - if (startOrdering.compare(nonNestedFeatures.get(i), feature) >= 0) - { - break; - } - i++; + contactFeatureStarts = new ArrayList<>(); } - return i; + if (contactFeatureEnds == null) + { + contactFeatureEnds = new ArrayList<>(); + } + + /* + * insert into list sorted by start (first contact position): + * binary search the sorted list to find the insertion point + */ + int insertPosition = BinarySearcher.findFirst(contactFeatureStarts, + true, Compare.GE, feature.getBegin()); + contactFeatureStarts.add(insertPosition, feature); + + + /* + * insert into list sorted by end (second contact position): + * binary search the sorted list to find the insertion point + */ + insertPosition = BinarySearcher.findFirst(contactFeatureEnds, + false, Compare.GE, feature.getEnd()); + contactFeatureEnds.add(insertPosition, feature); + + return true; } /** - * Add a contact feature to the lists that hold them ordered by start (first - * contact) and by end (second contact) position, ensuring the lists remain - * ordered + * 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 synchronized void addContactFeature(SequenceFeature feature) + protected static boolean listContains(List features, + SequenceFeature feature) { - // TODO binary search for insertion points! - if (contactFeatureStarts == null) + if (features == null || feature == null) { - contactFeatureStarts = new ArrayList(); + return false; } - if (contactFeatureEnds == null) + + /* + * 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 pos = BinarySearcher.findFirst(features, true, Compare.GE, + feature.getBegin()); + int len = features.size(); + while (pos < len) { - contactFeatureEnds = new ArrayList(); + SequenceFeature sf = features.get(pos); + if (sf.getBegin() > feature.getBegin()) + { + return false; // no match found + } + if (sf.equals(feature)) + { + return true; + } + pos++; } - contactFeatureStarts.add(feature); - Collections.sort(contactFeatureStarts, startOrdering); - contactFeatureEnds.add(feature); - Collections.sort(contactFeatureEnds, endOrdering); + return false; } /** - * Returns a (possibly empty) list of entries whose range overlaps the given + * 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. * @@ -234,15 +346,13 @@ public class FeatureStore */ public List findOverlappingFeatures(long start, long end) { - List result = new ArrayList(); - - findNonNestedFeatures(start, end, result); + List result = new ArrayList<>(); findContactFeatures(start, end, result); - if (nestedFeatures != null) + if (features != null) { - result.addAll(nestedFeatures.findOverlaps(start, end)); + result.addAll(features.findOverlaps(start, end)); } return result; @@ -250,7 +360,7 @@ public class FeatureStore /** * Adds contact features to the result list where either the second or the - * first contact position lies within the target range. + * first contact position lies within the target range * * @param from * @param to @@ -261,32 +371,44 @@ public class FeatureStore { if (contactFeatureStarts != null) { - findContactStartFeatures(from, to, result); + findContactStartOverlaps(from, to, result); } if (contactFeatureEnds != null) { - findContactEndFeatures(from, to, result); + findContactEndOverlaps(from, to, result); } } /** + * 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, + protected void findContactEndOverlaps(long from, long to, List result) { - // TODO binary search for startPosition - for (int startPosition = 0; startPosition < contactFeatureEnds.size(); startPosition++) + /* + * find the first contact feature (if any) + * whose end point is not before the target range + */ + int index = BinarySearcher.findFirst(contactFeatureEnds, + false, Compare.GE, (int) from); + + while (index < contactFeatureEnds.size()) { - SequenceFeature sf = contactFeatureEnds.get(startPosition); + SequenceFeature sf = contactFeatureEnds.get(index); if (!sf.isContactFeature()) { System.err.println("Error! non-contact feature type " + sf.getType() + " in contact features list"); + index++; continue; } + int begin = sf.getBegin(); if (begin >= from && begin <= to) { @@ -294,152 +416,439 @@ public class FeatureStore * this feature's first contact position lies in the search range * so we don't include it in results a second time */ + index++; continue; } - int end = sf.getEnd(); - if (end >= from && end <= to) + + if (sf.getEnd() > to) { - result.add(sf); + /* + * this feature (and all following) has end point after the target range + */ + break; } + + /* + * feature has end >= from and end <= to + * i.e. contact end point lies within overlap search range + */ + result.add(sf); + index++; } } /** - * Returns the index of the first contact feature found whose end (second - * contact position) is not before the given start position. If no such - * feature is found, returns the length of the contact features list. + * Adds contact features whose start position lies in the from-to range to the + * result list * - * @param start - * @return + * @param from + * @param to + * @param result */ - protected int contactsBinarySearch(long start) + protected void findContactStartOverlaps(long from, long to, + List result) { - // TODO binary search!! - int i = 0; - while (i < contactFeatureEnds.size()) + int index = BinarySearcher.findFirst(contactFeatureStarts, + true, Compare.GE, (int) from); + + while (index < contactFeatureStarts.size()) { - if (contactFeatureEnds.get(i).getEnd() >= start) + SequenceFeature sf = contactFeatureStarts.get(index); + if (!sf.isContactFeature()) + { + System.err.println("Error! non-contact feature " + sf.toString() + + " in contact features list"); + index++; + continue; + } + if (sf.getBegin() > to) { + /* + * this feature's start (and all following) follows the target range + */ break; } - i++; + + /* + * feature has begin >= from and begin <= to + * i.e. contact start point lies within overlap search range + */ + result.add(sf); + index++; } + } + + /** + * Answers a list of all positional features stored, in no guaranteed order + * + * @return + */ + public List getPositionalFeatures() + { + List result = new ArrayList<>(); - return i; + /* + * add any contact features - from the list by start position + */ + if (contactFeatureStarts != null) + { + result.addAll(contactFeatureStarts); + } + + /* + * add any nested features + */ + if (features != null) + { + result.addAll(features); + } + + return result; } /** - * Adds features to the result list that are at a single position which lies - * within the target range. Non-positional features (start=end=0) and contact - * features are excluded. + * Answers a list of all contact features. If there are none, returns an + * immutable empty list. * - * @param from - * @param to - * @param result + * @return */ - protected void findNonNestedFeatures(long from, long to, - List result) + public List getContactFeatures() { - int startIndex = binarySearch(nonNestedFeatures, from); - findNonNestedFeatures(startIndex, from, to, result); + if (contactFeatureStarts == null) + { + return Collections.emptyList(); + } + return new ArrayList<>(contactFeatureStarts); } /** - * Scans the list of non-nested features, starting from startIndex, to find - * those that overlap the from-to range, and adds them to the result list. - * Returns the index of the first feature whose start position is after the - * target range (or the length of the whole list if none such feature exists). + * Answers a list of all non-positional features. If there are none, returns + * an immutable empty list. * - * @param startIndex - * @param from - * @param to - * @param result * @return */ - protected int findNonNestedFeatures(final int startIndex, long from, - long to, - List result) + public List getNonPositionalFeatures() { - int i = startIndex; - while (i < nonNestedFeatures.size()) + if (nonPositionalFeatures == null) { - SequenceFeature sf = nonNestedFeatures.get(i); - if (sf.getBegin() > to) - { - break; - } - int start = sf.getBegin(); - int end = sf.getEnd(); - if (sf.isContactFeature()) - { - end = start; - } - if (start <= to && end >= from) + return Collections.emptyList(); + } + return new ArrayList<>(nonPositionalFeatures); + } + + /** + * 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 synchronized boolean delete(SequenceFeature sf) + { + boolean removed = false; + + /* + * try contact positions (and if found, delete + * from both lists of contact positions) + */ + if (!removed && contactFeatureStarts != null) + { + removed = contactFeatureStarts.remove(sf); + if (removed) { - result.add(sf); + contactFeatureEnds.remove(sf); } - i++; } - return i; + + boolean removedNonPositional = false; + + /* + * if not found, try non-positional features + */ + if (!removed && nonPositionalFeatures != null) + { + removedNonPositional = nonPositionalFeatures.remove(sf); + removed = removedNonPositional; + } + + /* + * if not found, try nested features + */ + if (!removed && features != null) + { + removed = features.remove(sf); + } + + if (removed) + { + rescanAfterDelete(); + } + + 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; + + /* + * 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); + } } /** - * Performs a binary search of the (sorted) list to find the index of the - * first entry whose end position is not less than the target position (i.e. - * skip all features that properly precede the given position) + * 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); + } + } + + /** + * 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 + { + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); + } + } + + /** + * Answers true if this store has no features, else false * - * @param features - * @param target * @return */ - protected int binarySearch(List features, long target) + public boolean isEmpty() { - int width = features.size() / 2; - int lastpos = width; - while (width > 0) + boolean hasFeatures = (contactFeatureStarts != null + && !contactFeatureStarts + .isEmpty()) + || (nonPositionalFeatures != null && !nonPositionalFeatures + .isEmpty()) + || (features != null && features.size() > 0); + + return !hasFeatures; + } + + /** + * Answers the set of distinct feature groups stored, possibly including null, + * as an unmodifiable view of the set. The parameter determines whether the + * groups for positional or for non-positional features are returned. + * + * @param positionalFeatures + * @return + */ + public Set getFeatureGroups(boolean positionalFeatures) + { + if (positionalFeatures) { - int end = features.get(lastpos).getEnd(); - width = width / 2; - if (end > target) - { - lastpos -= width; - } - else - { - lastpos += width; - } + return Collections.unmodifiableSet(positionalFeatureGroups); + } + else + { + return nonPositionalFeatureGroups == null ? Collections + . emptySet() : Collections + .unmodifiableSet(nonPositionalFeatureGroups); } - // todo correct binary search - return lastpos > 1 ? lastpos - 2 : 0; - // return lastpos; } /** - * Adds contact features whose start position lies in the from-to range to the - * result list + * Answers the number of positional (or non-positional) features stored. + * Contact features count as 1. * - * @param from - * @param to - * @param result + * @param positional + * @return */ - protected void findContactStartFeatures(long from, long to, - List result) + public int getFeatureCount(boolean positional) { - // TODO binary search for startPosition - for (int startPosition = 0; startPosition < contactFeatureStarts.size(); startPosition++) + if (!positional) { - SequenceFeature sf = contactFeatureStarts.get(startPosition); - if (!sf.isContactFeature()) + return nonPositionalFeatures == null ? 0 : nonPositionalFeatures + .size(); + } + + int size = 0; + + if (contactFeatureStarts != null) + { + // note a contact feature (start/end) counts as one + size += contactFeatureStarts.size(); + } + + if (features != null) + { + size += features.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)) { - System.err.println("Error! non-contact feature type " - + sf.getType() + " in contact features list"); - continue; + result.add(sf); } - int begin = sf.getBegin(); - if (begin >= from && begin <= to) + } + return result; + } + + /** + * Adds the shift amount to the start and end of all positional features whose + * start position is at or after fromPosition. Returns true if at least one + * feature was shifted, else false. + * + * @param fromPosition + * @param shiftBy + * @return + */ + public synchronized boolean shiftFeatures(int fromPosition, int shiftBy) + { + /* + * Because begin and end are final fields (to ensure the data store's + * integrity), we have to delete each feature and re-add it as amended. + * (Although a simple shift of all values would preserve data integrity!) + */ + boolean modified = false; + for (SequenceFeature sf : getPositionalFeatures()) + { + if (sf.getBegin() >= fromPosition) { - result.add(sf); + modified = true; + int newBegin = sf.getBegin() + shiftBy; + int newEnd = sf.getEnd() + shiftBy; + + /* + * sanity check: don't shift left of the first residue + */ + if (newEnd > 0) + { + newBegin = Math.max(1, newBegin); + SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd, + sf.getFeatureGroup(), sf.getScore()); + addFeature(sf2); + } + delete(sf); } } + return modified; } }