JAL-2480 move ContiguousI and Range to jalview.datamodel
[jalview.git] / src / jalview / datamodel / features / FeatureStore.java
index cd7d055..1b6277c 100644 (file)
@@ -1,5 +1,6 @@
 package jalview.datamodel.features;
 
+import jalview.datamodel.ContiguousI;
 import jalview.datamodel.SequenceFeature;
 
 import java.util.ArrayList;
@@ -72,10 +73,6 @@ public class FeatureStore
     }
   }
 
-  Comparator<ContiguousI> startOrdering = new RangeComparator(true);
-
-  Comparator<ContiguousI> 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.
@@ -182,7 +179,7 @@ public class FeatureStore
     }
     else
     {
-      if (!nonNestedFeatures.contains(feature))
+      if (!contains(nonNestedFeatures, feature))
       {
         added = addNonNestedFeature(feature);
         if (!added)
@@ -308,7 +305,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
@@ -382,21 +379,70 @@ public class FeatureStore
       contactFeatureEnds = new ArrayList<SequenceFeature>();
     }
 
-    // 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<SequenceFeature> 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.
@@ -696,7 +742,8 @@ public class FeatureStore
 
   /**
    * Rescan all features to recompute any cached values after an entry has been
-   * deleted
+   * deleted. This is expected to be an infrequent event, so performance here is
+   * not critical.
    */
   protected synchronized void rescanAfterDelete()
   {
@@ -841,7 +888,7 @@ public class FeatureStore
    * @param sc
    * @return
    */
-  protected int binarySearch(List<SequenceFeature> features,
+  protected static int binarySearch(List<SequenceFeature> features,
           SearchCriterion sc)
   {
     int start = 0;
@@ -934,4 +981,77 @@ public class FeatureStore
   {
     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<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    /*
+     * 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<SequenceFeature> 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;
+  }
+
+  /**
+   * Adds the shift value to the start and end of all positional features.
+   * Returns true if at least one feature was updated, else false.
+   * 
+   * @param shift
+   * @return
+   */
+  public synchronized boolean shiftFeatures(int shift)
+  {
+    /*
+     * 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())
+    {
+      modified = true;
+      int newBegin = sf.getBegin() + shift;
+      int newEnd = sf.getEnd() + shift;
+
+      /*
+       * 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;
+  }
 }