JAL-2541 revised cut with features (work in progress)
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 15 Jun 2017 13:06:15 +0000 (14:06 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 14 Sep 2017 09:36:21 +0000 (10:36 +0100)
Conflicts:
src/jalview/commands/EditCommand.java

src/jalview/commands/EditCommand.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/features/FeatureStore.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/datamodel/features/SequenceFeaturesI.java
src/jalview/ws/DBRefFetcher.java
test/jalview/commands/EditCommandTest.java
test/jalview/datamodel/features/FeatureStoreTest.java
test/jalview/datamodel/features/SequenceFeaturesTest.java

index cac843f..5248dc9 100644 (file)
  */
 package jalview.commands;
 
+import jalview.analysis.AlignSeq;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 import jalview.util.ReverseListIterator;
 import jalview.util.StringUtils;
 
@@ -559,12 +561,21 @@ public class EditCommand implements CommandI
             }
             command.oldds[i] = oldds;
             // FIXME JAL-2541 JAL-2526 get correct positions if on a gap
-            adjustFeatures(
-                    command,
-                    i,
-                    sequence.findPosition(command.position),
-                    sequence.findPosition(command.position + command.number),
-                    false);
+            List<SequenceFeature[]> amendedFeatures = sequence
+                    .adjustFeatures(command.position, command.position
+                            + command.number - 1);
+            if (command.editedFeatures == null)
+            {
+              command.editedFeatures = new HashMap<>();
+            }
+            command.editedFeatures.put(sequence, amendedFeatures);
+            //
+            // adjustFeatures(
+            // command,
+            // i,
+            // sequence.findPosition(command.position),
+            // sequence.findPosition(command.position + command.number),
+            // false);
           }
         }
       }
@@ -599,7 +610,8 @@ public class EditCommand implements CommandI
     {
       newDSNeeded = false;
       newDSWasNeeded = command.oldds != null && command.oldds[i] != null;
-      if (command.seqs[i].getLength() < 1)
+      SequenceI sequence = command.seqs[i];
+      if (sequence.getLength() < 1)
       {
         // ie this sequence was deleted, we need to
         // readd it to the alignment
@@ -610,21 +622,21 @@ public class EditCommand implements CommandI
           {
             if (!(command.alIndex[i] < 0))
             {
-              sequences.add(command.alIndex[i], command.seqs[i]);
+              sequences.add(command.alIndex[i], sequence);
             }
           }
         }
         else
         {
-          command.al.addSequence(command.seqs[i]);
+          command.al.addSequence(sequence);
         }
         seqWasDeleted = true;
       }
-      newstart = command.seqs[i].getStart();
-      newend = command.seqs[i].getEnd();
+      newstart = sequence.getStart();
+      newend = sequence.getEnd();
 
       tmp = new StringBuffer();
-      tmp.append(command.seqs[i].getSequence());
+      tmp.append(sequence.getSequence());
       // Undo of a delete does not replace original dataset sequence on to
       // alignment sequence.
 
@@ -644,16 +656,18 @@ public class EditCommand implements CommandI
         tmp.insert(command.position, command.string[i]);
         for (int s = 0; s < command.string[i].length; s++)
         {
-          if (jalview.schemes.ResidueProperties.aaIndex[command.string[i][s]] != 23)
+          // if (jalview.schemes.ResidueProperties.aaIndex[command.string[i][s]]
+          // != 23)
+          if (!Comparison.isGap(command.string[i][s]))
           {
             if (!newDSNeeded)
             {
               newDSNeeded = true;
-              start = command.seqs[i].findPosition(command.position);
-              end = command.seqs[i]
-                      .findPosition(command.position + command.number);
+              start = sequence.findPosition(command.position);
+              end = sequence.findPosition(command.position
+                      + command.number);
             }
-            if (command.seqs[i].getStart() == start)
+            if (sequence.getStart() == start)
             {
               newstart--;
             }
@@ -666,12 +680,12 @@ public class EditCommand implements CommandI
         command.string[i] = null;
       }
 
-      command.seqs[i].setSequence(tmp.toString());
-      command.seqs[i].setStart(newstart);
-      command.seqs[i].setEnd(newend);
+      sequence.setSequence(tmp.toString());
+      sequence.setStart(newstart);
+      sequence.setEnd(newend);
       if (newDSNeeded)
       {
-        if (command.seqs[i].getDatasetSequence() != null)
+        if (sequence.getDatasetSequence() != null)
         {
           SequenceI ds;
           if (newDSWasNeeded)
@@ -682,21 +696,20 @@ public class EditCommand implements CommandI
           {
             // make a new DS sequence
             // use new ds mechanism here
-            ds = new Sequence(command.seqs[i].getName(),
-                    jalview.analysis.AlignSeq.extractGaps(
-                            jalview.util.Comparison.GapChars,
-                            command.seqs[i].getSequenceAsString()),
-                    command.seqs[i].getStart(), command.seqs[i].getEnd());
-            ds.setDescription(command.seqs[i].getDescription());
+            String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
+                    sequence.getSequenceAsString());
+            ds = new Sequence(sequence.getName(), ungapped,
+                    sequence.getStart(), sequence.getEnd());
+            ds.setDescription(sequence.getDescription());
           }
           if (command.oldds == null)
           {
             command.oldds = new SequenceI[command.seqs.length];
           }
-          command.oldds[i] = command.seqs[i].getDatasetSequence();
-          command.seqs[i].setDatasetSequence(ds);
+          command.oldds[i] = sequence.getDatasetSequence();
+          sequence.setDatasetSequence(ds);
         }
-        adjustFeatures(command, i, start, end, true);
+        undoCutFeatures(command, i, start, end);
       }
     }
     adjustAnnotations(command, true, seqWasDeleted, views);
@@ -1109,8 +1122,8 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void adjustFeatures(Edit command, int index, final int i,
-          final int j, boolean insert)
+  final static void undoCutFeatures(Edit command, int index, final int i,
+          final int j)
   {
     SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
@@ -1119,87 +1132,101 @@ public class EditCommand implements CommandI
       sequence = seq;
     }
 
-    if (insert)
+    /*
+     * insert == true for an Undo of a Cut; restore the original features
+     * and delete the amended ones
+     */
+    if (true)
     {
+      // TODO shift right features that lie to the right of the restored cut
+      // (add a start position parameter to SequenceFeatures.shift)
+
       if (command.editedFeatures != null
               && command.editedFeatures.containsKey(seq))
       {
-        sequence.setSequenceFeatures(command.editedFeatures.get(seq));
+        for (SequenceFeature[] toRestore : command.editedFeatures.get(seq))
+        {
+          sequence.addSequenceFeature(toRestore[0]);
+          if (toRestore[1] != null)
+          {
+            sequence.deleteFeature(toRestore[1]);
+          }
+        }
       }
-
       return;
     }
 
-    List<SequenceFeature> sf = sequence.getFeatures()
-            .getPositionalFeatures();
-
-    if (sf.isEmpty())
-    {
-      return;
-    }
-
-    List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
-
-    int cSize = j - i;
-
-    for (SequenceFeature feature : sf)
-    {
-      SequenceFeature copy = new SequenceFeature(feature);
-
-      oldsf.add(copy);
-
-      if (feature.getEnd() < i)
-      {
-        continue;
-      }
-
-      if (feature.getBegin() > j)
-      {
-        int newBegin = copy.getBegin() - cSize;
-        int newEnd = copy.getEnd() - cSize;
-        SequenceFeature newSf = new SequenceFeature(feature, newBegin,
-                newEnd, feature.getFeatureGroup(), feature.getScore());
-        sequence.deleteFeature(feature);
-        sequence.addSequenceFeature(newSf);
-        // feature.setBegin(newBegin);
-        // feature.setEnd(newEnd);
-        continue;
-      }
-
-      int newBegin = feature.getBegin();
-      int newEnd = feature.getEnd();
-      if (newBegin >= i)
-      {
-        newBegin = i;
-        // feature.setBegin(i);
-      }
-
-      if (newEnd < j)
-      {
-        newEnd = j - 1;
-        // feature.setEnd(j - 1);
-      }
-      newEnd = newEnd - cSize;
-      // feature.setEnd(feature.getEnd() - (cSize));
-
-      sequence.deleteFeature(feature);
-      if (newEnd >= newBegin)
-      {
-        sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
-                newEnd, feature.getFeatureGroup(), feature.getScore()));
-      }
-      // if (feature.getBegin() > feature.getEnd())
-      // {
-      // sequence.deleteFeature(feature);
-      // }
-    }
-
-    if (command.editedFeatures == null)
-    {
-      command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
-    }
-
-    command.editedFeatures.put(seq, oldsf);
+    // List<SequenceFeature> sf = sequence.getFeatures()
+    // .getPositionalFeatures();
+    //
+    // if (sf.isEmpty())
+    // {
+    // return;
+    // }
+    //
+    // List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
+    //
+    // int cSize = j - i;
+    //
+    // for (SequenceFeature feature : sf)
+    // {
+    // SequenceFeature copy = new SequenceFeature(feature);
+    //
+    // oldsf.add(copy);
+    //
+    // if (feature.getEnd() < i)
+    // {
+    // continue;
+    // }
+    //
+    // if (feature.getBegin() > j)
+    // {
+    // int newBegin = copy.getBegin() - cSize;
+    // int newEnd = copy.getEnd() - cSize;
+    // SequenceFeature newSf = new SequenceFeature(feature, newBegin,
+    // newEnd, feature.getFeatureGroup(), feature.getScore());
+    // sequence.deleteFeature(feature);
+    // sequence.addSequenceFeature(newSf);
+    // // feature.setBegin(newBegin);
+    // // feature.setEnd(newEnd);
+    // continue;
+    // }
+    //
+    // int newBegin = feature.getBegin();
+    // int newEnd = feature.getEnd();
+    // if (newBegin >= i)
+    // {
+    // newBegin = i;
+    // // feature.setBegin(i);
+    // }
+    //
+    // if (newEnd < j)
+    // {
+    // newEnd = j - 1;
+    // // feature.setEnd(j - 1);
+    // }
+    // newEnd = newEnd - cSize;
+    // // feature.setEnd(feature.getEnd() - (cSize));
+    //
+    // sequence.deleteFeature(feature);
+    // if (newEnd >= newBegin)
+    // {
+    // sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
+    // newEnd, feature.getFeatureGroup(), feature.getScore()));
+    // }
+    // // if (feature.getBegin() > feature.getEnd())
+    // // {
+    // // sequence.deleteFeature(feature);
+    // // }
+    // }
+    //
+    // if (command.editedFeatures == null)
+    // {
+    // command.editedFeatures = new Hashtable<SequenceI,
+    // List<SequenceFeature>>();
+    // }
+    //
+    // command.editedFeatures.put(seq, oldsf);
 
   }
 
@@ -1319,11 +1346,11 @@ public class EditCommand implements CommandI
 
     boolean fullAlignmentHeight = false;
 
-    Hashtable<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
+    Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
 
-    Hashtable<String, Annotation[]> deletedAnnotations;
+    Map<String, Annotation[]> deletedAnnotations;
 
-    Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
+    Map<SequenceI, List<SequenceFeature[]>> editedFeatures;
 
     AlignmentI al;
 
index 2f1da7f..b758d51 100755 (executable)
@@ -44,10 +44,7 @@ import fr.orsay.lri.varna.models.rna.RNA;
 
 /**
  * 
- * Implements the SequenceI interface for a char[] based sequence object.
- * 
- * @author $author$
- * @version $Revision$
+ * Implements the SequenceI interface for a char[] based sequence object
  */
 public class Sequence extends ASequence implements SequenceI
 {
@@ -1901,4 +1898,75 @@ public class Sequence extends ASequence implements SequenceI
 
     return count;
   }
+
+  @Override
+  public List<SequenceFeature[]> adjustFeatures(int fromColumn, int toColumn)
+  {
+    List<SequenceFeature[]> amended = new ArrayList<>();
+
+    if (toColumn < fromColumn)
+    {
+      return amended;
+    }
+
+    synchronized (sequenceFeatureStore)
+    {
+      /*
+       * get features that overlap or span the cut region
+       */
+      List<SequenceFeature> overlaps = findFeatures(fromColumn, toColumn);
+      int cutWidth = toColumn - fromColumn + 1;
+
+      /*
+       * get features that strictly follow the cut region,
+       *  and shift them left by the width of the cut
+       */
+      List<SequenceFeature> follow = findFeatures(toColumn + 1,
+              Integer.MAX_VALUE);
+      follow.removeAll(overlaps);
+      for (SequenceFeature sf : follow)
+      {
+        SequenceFeature copy = new SequenceFeature(sf, sf.getBegin()
+                - cutWidth, sf.getEnd() - cutWidth, sf.getFeatureGroup(),
+                sf.getScore());
+        deleteFeature(sf);
+        addSequenceFeature(copy);
+      }
+
+      /*
+       * adjust start-end of overlapping features, and delete if enclosed by
+       * the cut, or a partially overlapping contact feature
+       */
+      for (SequenceFeature sf : overlaps)
+      {
+        // TODO recode to compute newBegin, newEnd, isDelete
+        // then perform the action
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int startCol = findIndex(sfBegin);
+        int endCol = findIndex(sfEnd);
+        if (startCol >= fromColumn && endCol <= toColumn)
+        {
+          // within cut region - delete feature
+          deleteFeature(sf);
+          amended.add(new SequenceFeature[] { sf, null });
+          continue;
+        }
+        if (startCol < fromColumn && endCol > toColumn)
+        {
+          // feature spans cut region - shift end left
+          SequenceFeature copy = new SequenceFeature(sf, sf.getBegin(),
+                  sf.getEnd() - cutWidth, sf.getFeatureGroup(),
+                  sf.getScore());
+          deleteFeature(sf);
+          addSequenceFeature(copy);
+          amended.add(new SequenceFeature[] { sf, copy });
+          continue;
+        }
+        // todo partial overlap - delete if contact feature
+      }
+    }
+
+    return amended;
+  }
 }
index 6e6d1aa..538b791 100755 (executable)
@@ -533,5 +533,23 @@ public interface SequenceI extends ASequenceI
    * @param c1
    * @param c2
    */
-  public int replace(char c1, char c2);
+  int replace(char c1, char c2);
+
+  /**
+   * Adjusts position and extent of features to allow for cut of the specified
+   * (inclusive) column range. Returns a list of {originalFeature,
+   * amendedFeature} for
+   * <ul>
+   * <li>features that have been deleted (as within the cut) - amendedFeature is
+   * null</li>
+   * <li>truncated features (as overlapping or spanning the cut)</li>
+   * </ul>
+   * Contact features that overlap the cut region are deleted. Contact features
+   * that enclose the cut region are shortened.
+   * 
+   * @param fromColumn
+   * @param toColumn
+   * @return
+   */
+  List<SequenceFeature[]> adjustFeatures(int fromColumn, int toColumn);
 }
index 51bee57..8f01065 100644 (file)
@@ -1027,13 +1027,15 @@ public class FeatureStore
   }
 
   /**
-   * Adds the shift value to the start and end of all positional features.
-   * Returns true if at least one feature was updated, else false.
+   * 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 shift
+   * @param fromPosition
+   * @param shiftBy
    * @return
    */
-  public synchronized boolean shiftFeatures(int shift)
+  public synchronized boolean shiftFeatures(int fromPosition, int shiftBy)
   {
     /*
      * Because begin and end are final fields (to ensure the data store's
@@ -1043,21 +1045,24 @@ public class FeatureStore
     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)
+      if (sf.getBegin() >= fromPosition)
       {
-        newBegin = Math.max(1, newBegin);
-        SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd,
-                sf.getFeatureGroup(), sf.getScore());
-        addFeature(sf2);
+        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);
       }
-      delete(sf);
     }
     return modified;
   }
index 52da8c7..24f2081 100644 (file)
@@ -443,13 +443,13 @@ public class SequenceFeatures implements SequenceFeaturesI
    * {@inheritDoc}
    */
   @Override
-  public boolean shiftFeatures(int shift)
+  public boolean shiftFeatures(int fromPosition, int shiftBy)
   {
     boolean modified = false;
     for (FeatureStore fs : featureStore.values())
     {
-      modified |= fs.shiftFeatures(shift);
+      modified |= fs.shiftFeatures(fromPosition, shiftBy);
     }
     return modified;
   }
-}
\ No newline at end of file
+}
index 58beca2..40beae3 100644 (file)
@@ -195,10 +195,12 @@ public interface SequenceFeaturesI
   float getMaximumScore(String type, boolean positional);
 
   /**
-   * Adds the shift amount to the start and end of all positional features,
-   * returning true if at least one feature was shifted, else false
+   * 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 shift
+   * @param fromPosition
+   * @param shiftBy
    */
-  abstract boolean shiftFeatures(int shift);
+  boolean shiftFeatures(int fromPosition, int shiftBy);
 }
\ No newline at end of file
index fb8864d..5dd8bf3 100644 (file)
@@ -711,7 +711,8 @@ public class DBRefFetcher implements Runnable
             int startShift = absStart - sequenceStart + 1;
             if (startShift != 0)
             {
-              modified |= sequence.getFeatures().shiftFeatures(startShift);
+              modified |= sequence.getFeatures().shiftFeatures(1,
+                      startShift);
             }
           }
         }
index 155f00e..7f1a432 100644 (file)
@@ -736,13 +736,9 @@ public class EditCommandTest
      */
     SequenceI[] sqs = new SequenceI[] { seq0 };
 
-    // goal is to have this passing for all from/to values!!
-    // for (int from = 0; from < seq0.getLength(); from++)
-    // {
-    // for (int to = from; to < seq0.getLength(); to++)
-    for (int from = 1; from < 3; from++)
+    for (int from = 0; from < seq0.getLength(); from++)
     {
-      for (int to = 2; to < 3; to++)
+      for (int to = from; to < seq0.getLength(); to++)
       {
         testee.appendEdit(Action.CUT, sqs, from, (to - from + 1),
                 alignment, true);
index db21c2f..6e7dd02 100644 (file)
@@ -2,6 +2,7 @@ package jalview.datamodel.features;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
 import jalview.datamodel.SequenceFeature;
@@ -774,7 +775,7 @@ public class FeatureStoreTest
   public void testShiftFeatures()
   {
     FeatureStore fs = new FeatureStore();
-    assertFalse(fs.shiftFeatures(1));
+    assertFalse(fs.shiftFeatures(0, 1)); // nothing to do
 
     SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
     fs.addFeature(sf1);
@@ -790,9 +791,9 @@ public class FeatureStoreTest
     fs.addFeature(sf4);
 
     /*
-     * shift features right by 5
+     * shift all features right by 5
      */
-    assertTrue(fs.shiftFeatures(5));
+    assertTrue(fs.shiftFeatures(0, 5));
 
     // non-positional features untouched:
     List<SequenceFeature> nonPos = fs.getNonPositionalFeatures();
@@ -818,7 +819,7 @@ public class FeatureStoreTest
      * feature at [7-10] should be removed
      * feature at [13-19] should become [1-4] 
      */
-    assertTrue(fs.shiftFeatures(-15));
+    assertTrue(fs.shiftFeatures(0, -15));
     pos = fs.getPositionalFeatures();
     assertEquals(pos.size(), 2);
     SequenceFeatures.sortFeatures(pos, true);
@@ -826,6 +827,32 @@ public class FeatureStoreTest
     assertEquals(pos.get(0).getEnd(), 4);
     assertEquals(pos.get(1).getBegin(), 13);
     assertEquals(pos.get(1).getEnd(), 22);
+
+    /*
+     * shift right by 4 from position 2 onwards
+     * feature at [1-4] unchanged, feature at [13-22] shifts
+     */
+    assertTrue(fs.shiftFeatures(2, 4));
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(1).getBegin(), 17);
+    assertEquals(pos.get(1).getEnd(), 26);
+
+    /*
+     * shift right by 4 from position 18 onwards
+     * should be no change
+     */
+    SequenceFeature f1 = pos.get(0);
+    SequenceFeature f2 = pos.get(1);
+    assertFalse(fs.shiftFeatures(18, 4)); // no update
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertSame(pos.get(0), f1);
+    assertSame(pos.get(1), f2);
   }
 
   @Test(groups = "Functional")
index a144f03..3f0eb9a 100644 (file)
@@ -1147,7 +1147,7 @@ public class SequenceFeaturesTest
   public void testShiftFeatures()
   {
     SequenceFeatures store = new SequenceFeatures();
-    assertFalse(store.shiftFeatures(1));
+    assertFalse(store.shiftFeatures(0, 1));
 
     SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
     store.add(sf1);
@@ -1165,7 +1165,7 @@ public class SequenceFeaturesTest
     /*
      * shift features right by 5
      */
-    assertTrue(store.shiftFeatures(5));
+    assertTrue(store.shiftFeatures(0, 5));
   
     // non-positional features untouched:
     List<SequenceFeature> nonPos = store.getNonPositionalFeatures();
@@ -1194,7 +1194,7 @@ public class SequenceFeaturesTest
      * feature at [7-10] should be removed
      * feature at [13-19] should become [1-4] 
      */
-    assertTrue(store.shiftFeatures(-15));
+    assertTrue(store.shiftFeatures(0, -15));
     pos = store.getPositionalFeatures();
     assertEquals(pos.size(), 2);
     SequenceFeatures.sortFeatures(pos, true);
@@ -1204,6 +1204,35 @@ public class SequenceFeaturesTest
     assertEquals(pos.get(1).getBegin(), 13);
     assertEquals(pos.get(1).getEnd(), 22);
     assertEquals(pos.get(1).getType(), "Disulfide bond");
+
+    /*
+     * shift right by 4 from column 2
+     * feature at [1-4] should be unchanged
+     * feature at [13-22] should become [17-26] 
+     */
+    assertTrue(store.shiftFeatures(2, 4));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(0).getType(), "Metal");
+    assertEquals(pos.get(1).getBegin(), 17);
+    assertEquals(pos.get(1).getEnd(), 26);
+    assertEquals(pos.get(1).getType(), "Disulfide bond");
+
+    /*
+     * shift right from column 18
+     * should be no updates
+     */
+    SequenceFeature f1 = pos.get(0);
+    SequenceFeature f2 = pos.get(1);
+    assertFalse(store.shiftFeatures(18, 6));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertSame(pos.get(0), f1);
+    assertSame(pos.get(1), f2);
   }
 
   @Test(groups = "Functional")