Merge branch 'develop' into bug/JAL-2541cutRelocateFeatures
authorJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 11:54:21 +0000 (11:54 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 11:54:21 +0000 (11:54 +0000)
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/SequenceTest.java
test/jalview/datamodel/features/FeatureStoreTest.java
test/jalview/datamodel/features/SequenceFeaturesTest.java

index cac843f..1a227c5 100644 (file)
  */
 package jalview.commands;
 
+import jalview.analysis.AlignSeq;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.Range;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.Comparison;
 import jalview.util.ReverseListIterator;
 import jalview.util.StringUtils;
 
@@ -114,7 +118,7 @@ public class EditCommand implements CommandI
     public abstract Action getUndoAction();
   };
 
-  private List<Edit> edits = new ArrayList<Edit>();
+  private List<Edit> edits = new ArrayList<>();
 
   String description;
 
@@ -330,20 +334,8 @@ public class EditCommand implements CommandI
           int position, int number, AlignmentI al, boolean performEdit,
           AlignmentI[] views)
   {
-    Edit edit = new Edit(command, seqs, position, number,
-            al.getGapCharacter());
-    if (al.getHeight() == seqs.length)
-    {
-      edit.al = al;
-      edit.fullAlignmentHeight = true;
-    }
-
-    addEdit(edit);
-
-    if (performEdit)
-    {
-      performEdit(edit, views);
-    }
+    Edit edit = new Edit(command, seqs, position, number, al);
+    appendEdit(edit, al, performEdit, views);
   }
 
   /**
@@ -534,39 +526,62 @@ public class EditCommand implements CommandI
         command.string[i] = sequence.getSequence(command.position,
                 command.position + command.number);
         SequenceI oldds = sequence.getDatasetSequence();
-        if (command.oldds != null && command.oldds[i] != null)
-        {
-          // we are redoing an undone cut.
-          sequence.setDatasetSequence(null);
-        }
-        sequence.deleteChars(command.position,
+        Range cutPositions = sequence.findPositions(command.position + 1,
                 command.position + command.number);
+        boolean cutIsInternal = cutPositions != null
+                && sequence.getStart() != cutPositions
+                .getBegin() && sequence.getEnd() != cutPositions.getEnd();
+
+        /*
+         * perform the cut; if this results in a new dataset sequence, add
+         * that to the alignment dataset
+         */
+        SequenceI ds = sequence.getDatasetSequence();
+        sequence.deleteChars(command.position, command.position
+                + command.number);
+
         if (command.oldds != null && command.oldds[i] != null)
         {
-          // oldds entry contains the cut dataset sequence.
+          /*
+           * we are Redoing a Cut, or Undoing a Paste - so
+           * oldds entry contains the cut dataset sequence,
+           * with sequence features in expected place
+           */
           sequence.setDatasetSequence(command.oldds[i]);
           command.oldds[i] = oldds;
         }
         else
         {
-          // modify the oldds if necessary
+          /* 
+           * new cut operation: save the dataset sequence 
+           * so it can be restored in an Undo
+           */
+          if (command.oldds == null)
+          {
+            command.oldds = new SequenceI[command.seqs.length];
+          }
+          command.oldds[i] = oldds;// todo not if !cutIsInternal?
+
+          // do we need to edit sequence features for new sequence ?
           if (oldds != sequence.getDatasetSequence()
-                  || sequence.getFeatures().hasFeatures())
+                  || (cutIsInternal
+                          && sequence.getFeatures().hasFeatures()))
+          // todo or just test cutIsInternal && cutPositions != null ?
           {
-            if (command.oldds == null)
+            if (cutPositions != null)
             {
-              command.oldds = new SequenceI[command.seqs.length];
+              cutFeatures(command, sequence, cutPositions.getBegin(),
+                              cutPositions.getEnd(), cutIsInternal);
             }
-            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);
           }
         }
+        SequenceI newDs = sequence.getDatasetSequence();
+        if (newDs != ds && command.al != null
+                && command.al.getDataset() != null
+                && !command.al.getDataset().getSequences().contains(newDs))
+        {
+          command.al.getDataset().addSequence(newDs);
+        }
       }
 
       if (sequence.getLength() < 1)
@@ -588,21 +603,19 @@ public class EditCommand implements CommandI
    */
   static void paste(Edit command, AlignmentI[] views)
   {
-    StringBuffer tmp;
-    boolean newDSNeeded;
-    boolean newDSWasNeeded;
-    int newstart, newend;
     boolean seqWasDeleted = false;
-    int start = 0, end = 0;
 
     for (int i = 0; i < command.seqs.length; i++)
     {
-      newDSNeeded = false;
-      newDSWasNeeded = command.oldds != null && command.oldds[i] != null;
-      if (command.seqs[i].getLength() < 1)
+      boolean newDSNeeded = false;
+      boolean newDSWasNeeded = command.oldds != null
+              && command.oldds[i] != null;
+      SequenceI sequence = command.seqs[i];
+      if (sequence.getLength() < 1)
       {
-        // ie this sequence was deleted, we need to
-        // readd it to the alignment
+        /*
+         * sequence was deleted; re-add it to the alignment
+         */
         if (command.alIndex[i] < command.al.getHeight())
         {
           List<SequenceI> sequences;
@@ -610,68 +623,78 @@ 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();
+      int newStart = sequence.getStart();
+      int newEnd = sequence.getEnd();
 
-      tmp = new StringBuffer();
-      tmp.append(command.seqs[i].getSequence());
+      StringBuilder tmp = new StringBuilder();
+      tmp.append(sequence.getSequence());
       // Undo of a delete does not replace original dataset sequence on to
       // alignment sequence.
 
+      int start = 0;
+      int length = 0;
+
       if (command.string != null && command.string[i] != null)
       {
         if (command.position >= tmp.length())
         {
           // This occurs if padding is on, and residues
           // are removed from end of alignment
-          int length = command.position - tmp.length();
-          while (length > 0)
+          int len = command.position - tmp.length();
+          while (len > 0)
           {
             tmp.append(command.gapChar);
-            length--;
+            len--;
           }
         }
         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 (!Comparison.isGap(command.string[i][s]))
           {
+            length++;
             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--;
+              newStart--;
             }
             else
             {
-              newend++;
+              newEnd++;
             }
           }
         }
         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);
+
+      /*
+       * command and Undo share the same dataset sequence if cut was
+       * at start or end of sequence
+       */
+      boolean sameDatasetSequence = false;
       if (newDSNeeded)
       {
-        if (command.seqs[i].getDatasetSequence() != null)
+        if (sequence.getDatasetSequence() != null)
         {
           SequenceI ds;
           if (newDSWasNeeded)
@@ -682,21 +705,29 @@ 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();
+          sameDatasetSequence = ds == sequence.getDatasetSequence();
+          ds.setSequenceFeatures(sequence.getSequenceFeatures());
+          if (!sameDatasetSequence && command.al.getDataset() != null)
+          {
+            // delete 'undone' sequence from alignment dataset
+            command.al.getDataset()
+                    .deleteSequence(sequence.getDatasetSequence());
+          }
+          sequence.setDatasetSequence(ds);
         }
-        adjustFeatures(command, i, start, end, true);
+        undoCutFeatures(command, command.seqs[i], start, length,
+                sameDatasetSequence);
       }
     }
     adjustAnnotations(command, true, seqWasDeleted, views);
@@ -706,7 +737,7 @@ public class EditCommand implements CommandI
 
   static void replace(Edit command)
   {
-    StringBuffer tmp;
+    StringBuilder tmp;
     String oldstring;
     int start = command.position;
     int end = command.number;
@@ -719,6 +750,7 @@ public class EditCommand implements CommandI
     {
       boolean newDSWasNeeded = command.oldds != null
               && command.oldds[i] != null;
+      boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
 
       /**
        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
@@ -731,49 +763,147 @@ public class EditCommand implements CommandI
        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
        * 
        */
+
+      Range beforeEditedPositions = command.seqs[i].findPositions(1, start);
+      Range afterEditedPositions = command.seqs[i]
+              .findPositions(end + 1, command.seqs[i].getLength());
+      
       oldstring = command.seqs[i].getSequenceAsString();
-      tmp = new StringBuffer(oldstring.substring(0, start));
+      tmp = new StringBuilder(oldstring.substring(0, start));
       tmp.append(command.string[i]);
-      String nogaprep = jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars,
+      String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
               new String(command.string[i]));
-      int ipos = command.seqs[i].findPosition(start)
-              - command.seqs[i].getStart();
-      tmp.append(oldstring.substring(end));
+      if (end < oldstring.length())
+      {
+        tmp.append(oldstring.substring(end));
+      }
+      // stash end prior to updating the sequence object so we can save it if
+      // need be.
+      Range oldstartend = new Range(command.seqs[i].getStart(),
+              command.seqs[i].getEnd());
       command.seqs[i].setSequence(tmp.toString());
-      command.string[i] = oldstring.substring(start, end).toCharArray();
-      String nogapold = jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars,
+      command.string[i] = oldstring
+              .substring(start, Math.min(end, oldstring.length()))
+              .toCharArray();
+      String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
               new String(command.string[i]));
+
       if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
       {
-        if (newDSWasNeeded)
+        // we may already have dataset and limits stashed...
+        if (newDSWasNeeded || newStartEndWasNeeded)
         {
+          if (newDSWasNeeded)
+          {
+          // then just switch the dataset sequence
           SequenceI oldds = command.seqs[i].getDatasetSequence();
           command.seqs[i].setDatasetSequence(command.oldds[i]);
           command.oldds[i] = oldds;
+          }
+          if (newStartEndWasNeeded)
+          {
+            Range newStart = command.oldStartEnd[i];
+            command.oldStartEnd[i] = oldstartend;
+            command.seqs[i].setStart(newStart.getBegin());
+            command.seqs[i].setEnd(newStart.getEnd());
+          }
         }
-        else
+        else         
         {
-          if (command.oldds == null)
+          // decide if we need a new dataset sequence or modify start/end
+          // first edit the original dataset sequence string
+          SequenceI oldds = command.seqs[i].getDatasetSequence();
+          String osp = oldds.getSequenceAsString();
+          int beforeStartOfEdit = -oldds.getStart() + 1
+                  + (beforeEditedPositions == null
+                          ? ((afterEditedPositions != null)
+                                  ? afterEditedPositions.getBegin() - 1
+                                  : oldstartend.getBegin()
+                                          + nogapold.length())
+                          : beforeEditedPositions.getEnd()
+                  );
+          int afterEndOfEdit = -oldds.getStart() + 1
+                  + ((afterEditedPositions == null)
+                  ? oldstartend.getEnd()
+                          : afterEditedPositions.getBegin() - 1);
+          String fullseq = osp.substring(0,
+                  beforeStartOfEdit)
+                  + nogaprep
+                  + osp.substring(afterEndOfEdit);
+
+          // and check if new sequence data is different..
+          if (!fullseq.equalsIgnoreCase(osp))
           {
-            command.oldds = new SequenceI[command.seqs.length];
-          }
-          command.oldds[i] = command.seqs[i].getDatasetSequence();
-          SequenceI newds = new Sequence(
-                  command.seqs[i].getDatasetSequence());
-          String fullseq, osp = newds.getSequenceAsString();
-          fullseq = osp.substring(0, ipos) + nogaprep
-                  + osp.substring(ipos + nogaprep.length());
-          newds.setSequence(fullseq.toUpperCase());
-          // TODO: JAL-1131 ensure newly created dataset sequence is added to
-          // the set of
-          // dataset sequences associated with the alignment.
-          // TODO: JAL-1131 fix up any annotation associated with new dataset
-          // sequence to ensure that original sequence/annotation relationships
-          // are preserved.
-          command.seqs[i].setDatasetSequence(newds);
+            // old ds and edited ds are different, so
+            // create the new dataset sequence
+            SequenceI newds = new Sequence(oldds);
+            newds.setSequence(fullseq);
+
+            if (command.oldds == null)
+            {
+              command.oldds = new SequenceI[command.seqs.length];
+            }
+            command.oldds[i] = command.seqs[i].getDatasetSequence();
+
+            // And preserve start/end for good-measure
 
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = oldstartend;
+            // TODO: JAL-1131 ensure newly created dataset sequence is added to
+            // the set of
+            // dataset sequences associated with the alignment.
+            // TODO: JAL-1131 fix up any annotation associated with new dataset
+            // sequence to ensure that original sequence/annotation
+            // relationships
+            // are preserved.
+            command.seqs[i].setDatasetSequence(newds);
+          }
+          else
+          {
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
+                    command.seqs[i].getEnd());
+            if (beforeEditedPositions != null
+                    && afterEditedPositions == null)
+            {
+              // modification at end
+              command.seqs[i].setEnd(
+                      beforeEditedPositions.getEnd() + nogaprep.length()
+                              - nogapold.length());
+            }
+            else if (afterEditedPositions != null
+                    && beforeEditedPositions == null)
+            {
+              // modification at start
+              command.seqs[i].setStart(
+                      afterEditedPositions.getBegin() - nogaprep.length());
+            }
+            else
+            {
+              // edit covered both start and end. Here we can only guess the
+              // new
+              // start/end
+              String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
+                      command.seqs[i].getSequenceAsString().toUpperCase());
+              int newStart = command.seqs[i].getDatasetSequence()
+                      .getSequenceAsString().indexOf(nogapalseq);
+              if (newStart == -1)
+              {
+                throw new Error(
+                        "Implementation Error: could not locate start/end "
+                                + "in dataset sequence after an edit of the sequence string");
+              }
+              int newEnd = newStart + nogapalseq.length() - 1;
+              command.seqs[i].setStart(newStart);
+              command.seqs[i].setEnd(newEnd);
+            }
+          }
         }
       }
       tmp = null;
@@ -789,7 +919,7 @@ public class EditCommand implements CommandI
     if (modifyVisibility && !insert)
     {
       // only occurs if a sequence was added or deleted.
-      command.deletedAnnotationRows = new Hashtable<SequenceI, AlignmentAnnotation[]>();
+      command.deletedAnnotationRows = new Hashtable<>();
     }
     if (command.fullAlignmentHeight)
     {
@@ -892,8 +1022,7 @@ public class EditCommand implements CommandI
                 }
                 // and then duplicate added annotation on every other alignment
                 // view
-                for (int vnum = 0; views != null
-                        && vnum < views.length; vnum++)
+                for (int vnum = 0; views != null && vnum < views.length; vnum++)
                 {
                   if (views[vnum] != command.al)
                   {
@@ -948,7 +1077,7 @@ public class EditCommand implements CommandI
 
     if (!insert)
     {
-      command.deletedAnnotations = new Hashtable<String, Annotation[]>();
+      command.deletedAnnotations = new Hashtable<>();
     }
 
     int aSize;
@@ -1109,98 +1238,97 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void adjustFeatures(Edit command, int index, final int i,
-          final int j, boolean insert)
+  /**
+   * Restores features to the state before a Cut.
+   * <ul>
+   * <li>re-add any features deleted by the cut</li>
+   * <li>remove any truncated features created by the cut</li>
+   * <li>shift right any features to the right of the cut</li>
+   * </ul>
+   * 
+   * @param command
+   *          the Cut command
+   * @param seq
+   *          the sequence the Cut applied to
+   * @param start
+   *          the start residue position of the cut
+   * @param length
+   *          the number of residues cut
+   * @param sameDatasetSequence
+   *          true if dataset sequence and frame of reference were left
+   *          unchanged by the Cut
+   */
+  final static void undoCutFeatures(Edit command, SequenceI seq,
+          final int start, final int length, boolean sameDatasetSequence)
   {
-    SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
     if (sequence == null)
     {
       sequence = seq;
     }
 
-    if (insert)
+    /*
+     * shift right features that lie to the right of the restored cut (but not 
+     * if dataset sequence unchanged - so coordinates were changed by Cut)
+     */
+    if (!sameDatasetSequence)
     {
-      if (command.editedFeatures != null
-              && command.editedFeatures.containsKey(seq))
+      /*
+       * shift right all features right of and not 
+       * contiguous with the cut position
+       */
+      seq.getFeatures().shiftFeatures(start + 1, length);
+
+      /*
+       * shift right any features that start at the cut position,
+       * unless they were truncated
+       */
+      List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
+              start);
+      for (SequenceFeature sf : sfs)
       {
-        sequence.setSequenceFeatures(command.editedFeatures.get(seq));
+        if (sf.getBegin() == start)
+        {
+          if (!command.truncatedFeatures.containsKey(seq)
+                  || !command.truncatedFeatures.get(seq).contains(sf))
+          {
+            /*
+             * feature was shifted left to cut position (not truncated),
+             * so shift it back right
+             */
+            SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
+                    + length, sf.getEnd() + length, sf.getFeatureGroup(),
+                    sf.getScore());
+            seq.addSequenceFeature(shifted);
+            seq.deleteFeature(sf);
+          }
+        }
       }
-
-      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)
+    /*
+     * restore any features that were deleted or truncated
+     */
+    if (command.deletedFeatures != null
+            && command.deletedFeatures.containsKey(seq))
     {
-      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)
+      for (SequenceFeature deleted : command.deletedFeatures.get(seq))
       {
-        newEnd = j - 1;
-        // feature.setEnd(j - 1);
+        sequence.addSequenceFeature(deleted);
       }
-      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)
+    /*
+     * delete any truncated features
+     */
+    if (command.truncatedFeatures != null
+            && command.truncatedFeatures.containsKey(seq))
     {
-      command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
+      for (SequenceFeature amended : command.truncatedFeatures.get(seq))
+      {
+        sequence.deleteFeature(amended);
+      }
     }
-
-    command.editedFeatures.put(seq, oldsf);
-
   }
 
   /**
@@ -1233,7 +1361,7 @@ public class EditCommand implements CommandI
    */
   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
   {
-    Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
+    Map<SequenceI, SequenceI> result = new HashMap<>();
     if (getEdits() == null)
     {
       return result;
@@ -1266,7 +1394,7 @@ public class EditCommand implements CommandI
      * Work backwards through the edit list, deriving the sequences before each
      * was applied. The final result is the sequence set before any edits.
      */
-    Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
+    Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
     while (editList.hasNext())
     {
       Edit oldEdit = editList.next();
@@ -1315,29 +1443,45 @@ public class EditCommand implements CommandI
 
   public class Edit
   {
-    public SequenceI[] oldds;
+    private SequenceI[] oldds;
+
+    /**
+     * start and end of sequence prior to edit
+     */
+    private Range[] oldStartEnd;
+
+    private boolean fullAlignmentHeight = false;
 
-    boolean fullAlignmentHeight = false;
+    private Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
 
-    Hashtable<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
+    private Map<String, Annotation[]> deletedAnnotations;
 
-    Hashtable<String, Annotation[]> deletedAnnotations;
+    /*
+     * features deleted by the cut (re-add on Undo)
+     * (including the original of any shortened features)
+     */
+    private Map<SequenceI, List<SequenceFeature>> deletedFeatures;
 
-    Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
+    /*
+     * shortened features added by the cut (delete on Undo)
+     */
+    private Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
 
-    AlignmentI al;
+    private AlignmentI al;
 
-    Action command;
+    final private Action command;
 
     char[][] string;
 
     SequenceI[] seqs;
 
-    int[] alIndex;
+    private int[] alIndex;
 
-    int position, number;
+    private int position;
 
-    char gapChar;
+    private int number;
+
+    private char gapChar;
 
     public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
             char gap)
@@ -1352,11 +1496,8 @@ public class EditCommand implements CommandI
     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
             AlignmentI align)
     {
-      this.gapChar = align.getGapCharacter();
-      this.command = cmd;
-      this.seqs = sqs;
-      this.position = pos;
-      this.number = count;
+      this(cmd, sqs, pos, count, align.getGapCharacter());
+
       this.al = align;
 
       alIndex = new int[sqs.length];
@@ -1368,22 +1509,26 @@ public class EditCommand implements CommandI
       fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
+    /**
+     * Constructor given a REPLACE command and the replacement string
+     * 
+     * @param cmd
+     * @param sqs
+     * @param pos
+     * @param count
+     * @param align
+     * @param replace
+     */
     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
             AlignmentI align, String replace)
     {
-      this.command = cmd;
-      this.seqs = sqs;
-      this.position = pos;
-      this.number = count;
-      this.al = align;
-      this.gapChar = align.getGapCharacter();
+      this(cmd, sqs, pos, count, align);
+
       string = new char[sqs.length][];
       for (int i = 0; i < sqs.length; i++)
       {
         string[i] = replace.toCharArray();
       }
-
-      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
     public SequenceI[] getSequences()
@@ -1427,7 +1572,163 @@ public class EditCommand implements CommandI
     }
     else
     {
-      return new ReverseListIterator<Edit>(getEdits());
+      return new ReverseListIterator<>(getEdits());
+    }
+  }
+
+  /**
+   * Adjusts features for Cut, and saves details of changes made to allow Undo
+   * <ul>
+   * <li>features left of the cut are unchanged</li>
+   * <li>features right of the cut are shifted left</li>
+   * <li>features internal to the cut region are deleted</li>
+   * <li>features that overlap or span the cut are shortened</li>
+   * <li>the originals of any deleted or shortened features are saved, to re-add
+   * on Undo</li>
+   * <li>any added (shortened) features are saved, to delete on Undo</li>
+   * </ul>
+   * 
+   * @param command
+   * @param seq
+   * @param fromPosition
+   * @param toPosition
+   * @param cutIsInternal
+   */
+  protected static void cutFeatures(Edit command, SequenceI seq,
+          int fromPosition, int toPosition, boolean cutIsInternal)
+  {
+    /* 
+     * if the cut is at start or end of sequence
+     * then we don't modify the sequence feature store
+     */
+    if (!cutIsInternal)
+    {
+      return;
+    }
+    List<SequenceFeature> added = new ArrayList<>();
+    List<SequenceFeature> removed = new ArrayList<>();
+  
+    SequenceFeaturesI featureStore = seq.getFeatures();
+    if (toPosition < fromPosition || featureStore == null)
+    {
+      return;
+    }
+  
+    int cutStartPos = fromPosition;
+    int cutEndPos = toPosition;
+    int cutWidth = cutEndPos - cutStartPos + 1;
+  
+    synchronized (featureStore)
+    {
+      /*
+       * get features that overlap the cut region
+       */
+      List<SequenceFeature> toAmend = featureStore.findFeatures(
+              cutStartPos, cutEndPos);
+  
+      /*
+       * add any contact features that span the cut region
+       * (not returned by findFeatures)
+       */
+      for (SequenceFeature contact : featureStore.getContactFeatures())
+      {
+        if (contact.getBegin() < cutStartPos
+                && contact.getEnd() > cutEndPos)
+        {
+          toAmend.add(contact);
+        }
+      }
+
+      /*
+       * adjust start-end of overlapping features;
+       * delete features enclosed by the cut;
+       * delete partially overlapping contact features
+       */
+      for (SequenceFeature sf : toAmend)
+      {
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int newBegin = sfBegin;
+        int newEnd = sfEnd;
+        boolean toDelete = false;
+        boolean follows = false;
+        
+        if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
+        {
+          /*
+           * feature lies within cut region - delete it
+           */
+          toDelete = true;
+        }
+        else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
+        {
+          /*
+           * feature spans cut region - left-shift the end
+           */
+          newEnd -= cutWidth;
+        }
+        else if (sfEnd <= cutEndPos)
+        {
+          /*
+           * feature overlaps left of cut region - truncate right
+           */
+          newEnd = cutStartPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+        else if (sfBegin >= cutStartPos)
+        {
+          /*
+           * remaining case - feature overlaps right
+           * truncate left, adjust end of feature
+           */
+          newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
+          newEnd = newBegin + sfEnd - cutEndPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+  
+        seq.deleteFeature(sf);
+        if (!follows)
+        {
+          removed.add(sf);
+        }
+        if (!toDelete)
+        {
+          SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+                  sf.getFeatureGroup(), sf.getScore());
+          seq.addSequenceFeature(copy);
+          if (!follows)
+          {
+            added.add(copy);
+          }
+        }
+      }
+  
+      /*
+       * and left shift any features lying to the right of the cut region
+       */
+
+      featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
+    }
+
+    /*
+     * save deleted and amended features, so that Undo can 
+     * re-add or delete them respectively
+     */
+    if (command.deletedFeatures == null)
+    {
+      command.deletedFeatures = new HashMap<>();
+    }
+    if (command.truncatedFeatures == null)
+    {
+      command.truncatedFeatures = new HashMap<>();
     }
+    command.deletedFeatures.put(seq, removed);
+    command.truncatedFeatures.put(seq, added);
   }
 }
index 33de452..f555855 100755 (executable)
@@ -43,10 +43,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
 {
@@ -797,6 +794,7 @@ public class Sequence extends ASequence implements SequenceI
      * preserve end residue column provided cursor was valid
      */
     int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
+
     if (residuePos == this.end)
     {
       endColumn = column;
@@ -833,8 +831,7 @@ public class Sequence extends ASequence implements SequenceI
     /*
      * move left or right to find pos from hint.position
      */
-    int col = curs.columnPosition - 1; // convert from base 1 to 0-based array
-                                       // index
+    int col = curs.columnPosition - 1; // convert from base 1 to base 0
     int newPos = curs.residuePosition;
     int delta = newPos > pos ? -1 : 1;
 
@@ -1263,12 +1260,13 @@ public class Sequence extends ASequence implements SequenceI
     boolean createNewDs = false;
     // TODO: take a (second look) at the dataset creation validation method for
     // the very large sequence case
+
     int startIndex = findIndex(start) - 1;
     int endIndex = findIndex(end) - 1;
     int startDeleteColumn = -1; // for dataset sequence deletions
     int deleteCount = 0;
 
-    for (int s = i; s < j; s++)
+    for (int s = i; s < j && s < sequence.length; s++)
     {
       if (Comparison.isGap(sequence[s]))
       {
@@ -1913,6 +1911,15 @@ public class Sequence extends ASequence implements SequenceI
 
     List<SequenceFeature> result = getFeatures().findFeatures(startPos,
             endPos, types);
+    if (datasetSequence != null)
+    {
+      result = datasetSequence.getFeatures().findFeatures(startPos, endPos,
+              types);
+    }
+    else
+    {
+      result = sequenceFeatureStore.findFeatures(startPos, endPos, types);
+    }
 
     /*
      * if end column is gapped, endPos may be to the right, 
index 8dce31e..48615f0 100755 (executable)
@@ -206,11 +206,14 @@ public interface SequenceI extends ASequenceI
   public int findPosition(int i);
 
   /**
-   * Returns the from-to sequence positions (start..) for the given column
-   * positions (1..), or null if no residues are included in the range
+   * Returns the sequence positions for first and last residues lying within the
+   * given column positions [fromColum,toColumn] (where columns are numbered
+   * from 1), or null if no residues are included in the range
    * 
    * @param fromColum
+   *          - first column base 1
    * @param toColumn
+   *          - last column, base 1
    * @return
    */
   public Range findPositions(int fromColum, int toColumn);
index 02ce1c5..951b7a5 100644 (file)
@@ -1047,13 +1047,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
@@ -1063,21 +1065,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 727d3ef..8f965b4 100644 (file)
@@ -472,13 +472,22 @@ 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
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void deleteAll()
+  {
+    featureStore.clear();
+  }
+}
index 31712b9..ca0283e 100644 (file)
@@ -215,10 +215,17 @@ 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);
-}
\ No newline at end of file
+  boolean shiftFeatures(int fromPosition, int shiftBy);
+
+  /**
+   * Deletes all positional and non-positional features
+   */
+  void deleteAll();
+}
index 061e70c..41e4d0d 100644 (file)
@@ -670,7 +670,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..2160657 100644 (file)
@@ -21,8 +21,8 @@
 package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
@@ -34,6 +34,8 @@ import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
@@ -50,6 +52,22 @@ import org.testng.annotations.Test;
  */
 public class EditCommandTest
 {
+  private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
+  {
+
+    @Override
+    public int compare(SequenceFeature o1, SequenceFeature o2)
+    {
+      return o1.getDescription().compareTo(o2.getDescription());
+    }
+  };
+
+  private EditCommand testee;
+
+  private SequenceI[] seqs;
+
+  private Alignment al;
+
   /*
    * compute n(n+1)/2 e.g. 
    * func(5) = 5 + 4 + 3 + 2 + 1 = 15
@@ -66,12 +84,6 @@ public class EditCommandTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  private EditCommand testee;
-
-  private SequenceI[] seqs;
-
-  private Alignment al;
-
   @BeforeMethod(alwaysRun = true)
   public void setUp()
   {
@@ -141,18 +153,23 @@ public class EditCommandTest
   }
 
   /**
-   * Test a Paste action, where this adds sequences to an alignment.
+   * Test a Paste action, followed by Undo and Redo
    */
   @Test(groups = { "Functional" }, enabled = false)
-  // TODO fix so it works
-  public void testPaste_addToAlignment()
+  public void testPaste_undo_redo()
   {
+    // TODO code this test properly, bearing in mind that:
+    // Paste action requires something on the clipboard (Cut/Copy)
+    // - EditCommand.paste doesn't add sequences to the alignment
+    // ... that is done in AlignFrame.paste()
+    // ... unless as a Redo
+    // ...
+
     SequenceI[] newSeqs = new SequenceI[2];
     newSeqs[0] = new Sequence("newseq0", "ACEFKL");
     newSeqs[1] = new Sequence("newseq1", "JWMPDH");
 
-    Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al);
-    EditCommand.paste(ec, new AlignmentI[] { al });
+    new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
     assertEquals(6, al.getSequences().size());
     assertEquals("1234567890", seqs[3].getSequenceAsString());
     assertEquals("ACEFKL", seqs[4].getSequenceAsString());
@@ -262,12 +279,123 @@ public class EditCommandTest
   {
     // seem to need a dataset sequence on the edited sequence here
     seqs[1].createDatasetSequence();
-    new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
+    assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
+    // NB command.number holds end position for a Replace command
+    new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
             4, 8, al);
     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
+    assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
+    assertEquals("fghjZxYopq",
+            seqs[1].getDatasetSequence().getSequenceAsString());
     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
     assertEquals("1234567890", seqs[3].getSequenceAsString());
-    seqs[1] = new Sequence("seq1", "fghjZXYnopq");
+  }
+
+  /**
+   * Test the replace command (used to manually edit a sequence)
+   */
+  @Test(groups = { "Functional" })
+  public void testReplace_withGaps()
+  {
+    SequenceI seq = new Sequence("seq", "ABC--DEF");
+    seq.createDatasetSequence();
+    assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(6, seq.getEnd());
+
+    /*
+     * replace C- with XYZ
+     * NB arg4 = start column of selection for edit (base 0)
+     * arg5 = column after end of selection for edit
+     */
+    EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
+            new SequenceI[]
+            { seq }, 2,
+            4, al);
+    assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(8, seq.getEnd());
+    assertEquals("ABxyZDEF",
+            seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, seq.getDatasetSequence().getEnd());
+
+    /*
+     * undo the edit
+     */
+    AlignmentI[] views = new AlignmentI[]
+    { new Alignment(new SequenceI[] { seq }) };
+    edit.undoCommand(views);
+
+    assertEquals("ABC--DEF", seq.getSequenceAsString());
+    assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(6, seq.getEnd());
+    assertEquals(6, seq.getDatasetSequence().getEnd());
+
+    /*
+     * redo the edit
+     */
+    edit.doCommand(views);
+
+    assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(8, seq.getEnd());
+    assertEquals("ABxyZDEF",
+            seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, seq.getDatasetSequence().getEnd());
+
+  }
+
+  /**
+   * Test replace command when it doesn't cause a sequence edit (see comment in
+   */
+  @Test(groups = { "Functional" })
+  public void testReplaceFirstResiduesWithGaps()
+  {
+    // test replace when gaps are inserted at start. Start/end should change
+    // w.r.t. original edited sequence.
+    SequenceI dsseq = seqs[1].getDatasetSequence();
+    EditCommand edit = new EditCommand("", Action.REPLACE, "----",
+            new SequenceI[]
+            { seqs[1] }, 0, 4, al);
+
+    // trimmed start
+    assertEquals("----klmnopq", seqs[1].getSequenceAsString());
+    // and ds is preserved
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // and it is unchanged
+    assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
+    // and that alignment sequence start has been adjusted
+    assertEquals(5, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
+    AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
+    // and undo
+    edit.undoCommand(views);
+
+    // dataset sequence unchanged
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // restore sequence
+    assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
+    // and start/end numbering also restored
+    assertEquals(1, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
+    // now redo
+    edit.undoCommand(views);
+
+    // and repeat asserts for the original edit
+
+    // trimmed start
+    assertEquals("----klmnopq", seqs[1].getSequenceAsString());
+    // and ds is preserved
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // and it is unchanged
+    assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
+    // and that alignment sequence start has been adjusted
+    assertEquals(5, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
   }
 
   /**
@@ -663,7 +791,7 @@ public class EditCommandTest
      * create sequence features before, after and overlapping
      * a cut of columns/residues 4-7
      */
-    SequenceI seq0 = seqs[0];
+    SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
     seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
             null));
     seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
@@ -675,29 +803,52 @@ public class EditCommandTest
     seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
             null));
 
+    /*
+     * add some contact features
+     */
+    SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
+            6, 0f, null);
+    seq0.addSequenceFeature(internalContact); // should get deleted
+    SequenceFeature overlapLeftContact = new SequenceFeature(
+            "disulphide bond", "", 2, 6, 0f, null);
+    seq0.addSequenceFeature(overlapLeftContact); // should get deleted
+    SequenceFeature overlapRightContact = new SequenceFeature(
+            "disulphide bond", "", 5, 8, 0f, null);
+    seq0.addSequenceFeature(overlapRightContact); // should get deleted
+    SequenceFeature spanningContact = new SequenceFeature(
+            "disulphide bond", "", 2, 9, 0f, null);
+    seq0.addSequenceFeature(spanningContact); // should get shortened 3'
+
+    /*
+     * cut columns 3-6 (base 0), residues d-g 4-7
+     */
     Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
     EditCommand.cut(ec, new AlignmentI[] { al });
 
     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
     SequenceFeatures.sortFeatures(sfs, true);
 
-    assertEquals(4, sfs.size()); // feature internal to cut has been deleted
+    assertEquals(5, sfs.size()); // features internal to cut were deleted
     SequenceFeature sf = sfs.get(0);
     assertEquals("before", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(3, sf.getEnd());
     sf = sfs.get(1);
+    assertEquals("disulphide bond", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(5, sf.getEnd()); // truncated by cut
+    sf = sfs.get(2);
     assertEquals("overlap left", sf.getType());
     assertEquals(2, sf.getBegin());
     assertEquals(3, sf.getEnd()); // truncated by cut
-    sf = sfs.get(2);
-    assertEquals("overlap right", sf.getType());
-    assertEquals(4, sf.getBegin()); // shifted left by cut
-    assertEquals(5, sf.getEnd()); // truncated by cut
     sf = sfs.get(3);
     assertEquals("after", sf.getType());
     assertEquals(4, sf.getBegin()); // shifted left by cut
     assertEquals(6, sf.getEnd()); // shifted left by cut
+    sf = sfs.get(4);
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(4, sf.getEnd()); // truncated by cut
   }
 
   /**
@@ -711,16 +862,28 @@ public class EditCommandTest
      * create a sequence features on each subrange of 1-5
      */
     SequenceI seq0 = new Sequence("seq", "ABCDE");
+    int start = 8;
+    int end = 12;
+    seq0.setStart(start);
+    seq0.setEnd(end);
     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
     alignment.setDataset(null);
-    for (int from = 1; from <= seq0.getLength(); from++)
+
+    /*
+     * create a new alignment with shared dataset sequence
+     */
+    AlignmentI copy = new Alignment(
+            new SequenceI[]
+            { alignment.getDataset().getSequenceAt(0).deriveSequence() });
+    SequenceI copySeq0 = copy.getSequenceAt(0);
+
+    for (int from = start; from <= end; from++)
     {
-      for (int to = from; to <= seq0.getLength(); to++)
+      for (int to = from; to <= end; to++)
       {
         String desc = String.format("%d-%d", from, to);
         SequenceFeature sf = new SequenceFeature("test", desc, from, to,
-                0f,
-                null);
+                0f, null);
         sf.setValue("from", Integer.valueOf(from));
         sf.setValue("to", Integer.valueOf(to));
         seq0.addSequenceFeature(sf);
@@ -729,109 +892,234 @@ public class EditCommandTest
     // sanity check
     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
     assertEquals(func(5), sfs.size());
+    assertEquals(sfs, copySeq0.getSequenceFeatures());
+    String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
 
     /*
-     * now perform all possible cuts of subranges of 1-5 (followed by Undo)
+     * now perform all possible cuts of subranges of columns 1-5
      * and validate the resulting remaining sequence features!
      */
     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);
+        EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
+                - from + 1), alignment);
+        final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
+        boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
+                .getDatasetSequence();
+
+        verifyCut(seq0, from, to, msg, start);
+
+        /*
+         * verify copy alignment dataset sequence unaffected
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
 
+        /*
+         * verify any new dataset sequence was added to the
+         * alignment dataset
+         */
+        assertEquals("Wrong Dataset size after " + msg,
+                newDatasetSequence ? 2 : 1,
+                alignment.getDataset().getHeight());
+
+        /*
+         * undo and verify all restored
+         */
+        AlignmentI[] views = new AlignmentI[] { alignment };
+        ec.undoCommand(views);
         sfs = seq0.getSequenceFeatures();
+        assertEquals("After undo of " + msg, func(5), sfs.size());
+        verifyUndo(from, to, sfs);
+
+        /*
+         * verify copy alignment dataset sequence still unaffected
+         * and alignment dataset has shrunk (if it was added to)
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after Undo of " + msg, 1,
+                alignment.getDataset().getHeight());
 
         /*
-         * confirm the number of features has reduced by the
-         * number of features within the cut region i.e. by
-         * func(length of cut)
+         * redo and verify
          */
-        String msg = String.format("Cut %d-%d ", from, to);
-        if (to - from == 4)
-        {
-          // all columns cut
-          assertNull(sfs);
-        }
-        else
-        {
-          assertEquals(msg + "wrong number of features left", func(5)
-                  - func(to - from + 1), sfs.size());
-        }
+        ec.doCommand(views);
+        verifyCut(seq0, from, to, msg, start);
 
         /*
-         * inspect individual features
+         * verify copy alignment dataset sequence unaffected
+         * and any new dataset sequence readded to alignment dataset
          */
-        if (sfs != null)
-        {
-          for (SequenceFeature sf : sfs)
-          {
-            checkFeatureRelocation(sf, from + 1, to + 1);
-          }
-        }
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after Redo of " + msg,
+                newDatasetSequence ? 2 : 1,
+                alignment.getDataset().getHeight());
+
         /*
          * undo ready for next cut
          */
-        testee.undoCommand(new AlignmentI[] { alignment });
-        assertEquals(func(5), seq0.getSequenceFeatures().size());
+        ec.undoCommand(views);
+
+        /*
+         * final verify that copy alignment dataset sequence is still unaffected
+         * and that alignment dataset has shrunk
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
+                alignment.getDataset().getHeight());
       }
     }
   }
 
   /**
+   * Verify by inspection that the sequence features left on the sequence after
+   * a cut match the expected results. The trick to this is that we can parse
+   * each feature's original start-end positions from its description.
+   * 
+   * @param seq0
+   * @param from
+   * @param to
+   * @param msg
+   * @param seqStart
+   */
+  protected void verifyCut(SequenceI seq0, int from, int to,
+          final String msg, int seqStart)
+  {
+    List<SequenceFeature> sfs;
+    sfs = seq0.getSequenceFeatures();
+
+    Collections.sort(sfs, BY_DESCRIPTION);
+
+    /*
+     * confirm the number of features has reduced by the
+     * number of features within the cut region i.e. by
+     * func(length of cut); exception is a cut at start or end of sequence, 
+     * which retains the original coordinates, dataset sequence 
+     * and all its features
+     */
+    boolean datasetRetained = from == 0 || to == 4;
+    if (datasetRetained)
+    {
+      // dataset and all features retained
+      assertEquals(msg, func(5), sfs.size());
+    }
+    else if (to - from == 4)
+    {
+      // all columns were cut
+      assertTrue(sfs.isEmpty());
+    }
+    else
+    {
+      // failure in checkFeatureRelocation is more informative!
+      assertEquals(msg + "wrong number of features left", func(5)
+              - func(to - from + 1), sfs.size());
+    }
+
+    /*
+     * inspect individual features
+     */
+    for (SequenceFeature sf : sfs)
+    {
+      verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
+              seqStart);
+    }
+  }
+
+  /**
+   * Check that after Undo, every feature has start/end that match its original
+   * "start" and "end" properties
+   * 
+   * @param from
+   * @param to
+   * @param sfs
+   */
+  protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
+  {
+    for (SequenceFeature sf : sfs)
+    {
+      final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+      final int oldTo = ((Integer) sf.getValue("to")).intValue();
+      String msg = String.format(
+              "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
+              oldFrom, oldTo);
+      assertEquals(msg + "start", oldFrom, sf.getBegin());
+      assertEquals(msg + "end", oldTo, sf.getEnd());
+    }
+  }
+
+  /**
    * Helper method to check a feature has been correctly relocated after a cut
    * 
    * @param sf
    * @param from
-   *          start of cut (first residue cut)
+   *          start of cut (first residue cut 1..)
    * @param to
-   *          end of cut (last residue cut)
+   *          end of cut (last residue cut 1..)
+   * @param newDataset
+   * @param seqStart
    */
-  private void checkFeatureRelocation(SequenceFeature sf, int from, int to)
+  private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
+ boolean newDataset, int seqStart)
   {
     // TODO handle the gapped sequence case as well
     int cutSize = to - from + 1;
-    int oldFrom = ((Integer) sf.getValue("from")).intValue();
-    int oldTo = ((Integer) sf.getValue("to")).intValue();
+    final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    final int oldTo = ((Integer) sf.getValue("to")).intValue();
+    final int oldFromPosition = oldFrom - seqStart + 1; // 1..
+    final int oldToPosition = oldTo - seqStart + 1; // 1..
 
     String msg = String.format(
             "Feature %s relocated to %d-%d after cut of %d-%d",
             sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
-    if (oldTo < from)
+    if (!newDataset)
+    {
+      // dataset retained with all features unchanged
+      assertEquals("0: " + msg, oldFrom, sf.getBegin());
+      assertEquals("0: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldToPosition < from)
     {
       // before cut region so unchanged
       assertEquals("1: " + msg, oldFrom, sf.getBegin());
       assertEquals("2: " + msg, oldTo, sf.getEnd());
     }
-    else if (oldFrom > to)
+    else if (oldFromPosition > to)
     {
       // follows cut region - shift by size of cut
-      assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin());
-      assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd());
+      assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
+              sf.getBegin());
+      assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
+              sf.getEnd());
     }
-    else if (oldFrom < from && oldTo > to)
+    else if (oldFromPosition < from && oldToPosition > to)
     {
       // feature encloses cut region - shrink it right
       assertEquals("5: " + msg, oldFrom, sf.getBegin());
       assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
     }
-    else if (oldFrom < from)
+    else if (oldFromPosition < from)
     {
       // feature overlaps left side of cut region - truncated right
-      assertEquals("7: " + msg, from - 1, sf.getEnd());
+      assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
     }
-    else if (oldTo > to)
+    else if (oldToPosition > to)
     {
       // feature overlaps right side of cut region - truncated left
-      assertEquals("8: " + msg, from, sf.getBegin());
-      assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd());
+      assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
+              sf.getBegin());
+      assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
+              sf.getEnd());
     }
     else
     {
@@ -844,30 +1132,32 @@ public class EditCommandTest
    * Test a cut action's relocation of sequence features
    */
   @Test(groups = { "Functional" })
-  public void testCut_gappedWithFeatures()
+  public void testCut_withFeatures5prime()
   {
+    SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
+    seq0.createDatasetSequence();
+    assertEquals(8, seq0.getStart());
+    seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
+            null));
+    SequenceI[] seqsArray = new SequenceI[] { seq0 };
+    AlignmentI alignment = new Alignment(seqsArray);
+
     /*
-     * create sequence features before, after and overlapping
-     * a cut of columns/residues 4-7
+     * cut columns of A-B; same dataset sequence is retained, aligned sequence
+     * start becomes 10
      */
-    SequenceI seq0 = new Sequence("seq", "A-BCC");
-    seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f,
-            null));
-    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
-    // cut columns of A-B
-    Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3
-                                                                  // base 0
+    Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
     EditCommand.cut(ec, new AlignmentI[] { alignment });
   
     /*
-     * feature on CC(3-4) should now be on CC(1-2)
+     * feature on CC(10-11) should still be on CC(10-11)
      */
+    assertSame(seq0, alignment.getSequenceAt(0));
+    assertEquals(10, seq0.getStart());
     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
     assertEquals(1, sfs.size());
     SequenceFeature sf = sfs.get(0);
-    assertEquals(1, sf.getBegin());
-    assertEquals(2, sf.getEnd());
-
-    // TODO add further cases including Undo - see JAL-2541
+    assertEquals(10, sf.getBegin());
+    assertEquals(11, sf.getEnd());
   }
 }
index 79bb2bb..9629b6f 100644 (file)
@@ -290,6 +290,61 @@ public class SequenceTest
     assertEquals(0, sq.findIndex(2));
   }
 
+  @Test(groups = { "Functional" })
+  public void testFindPositions()
+  {
+    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+
+    /*
+     * invalid inputs
+     */
+    assertNull(sq.findPositions(6, 5));
+    assertNull(sq.findPositions(0, 5));
+    assertNull(sq.findPositions(-1, 5));
+
+    /*
+     * all gapped ranges
+     */
+    assertNull(sq.findPositions(1, 1)); // 1-based columns
+    assertNull(sq.findPositions(5, 5));
+    assertNull(sq.findPositions(5, 6));
+    assertNull(sq.findPositions(5, 7));
+
+    /*
+     * all ungapped ranges
+     */
+    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
+    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
+    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
+    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
+
+    /*
+     * gap to ungapped range
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
+
+    /*
+     * ungapped to gapped range
+     */
+    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
+    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
+
+    /*
+     * ungapped to ungapped enclosing gaps
+     */
+    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
+    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
+
+    /*
+     * gapped to gapped enclosing ungapped
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
+    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
+    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
+  }
+
   /**
    * Tests for the method that returns a dataset sequence position (start..) for
    * an aligned column position (base 0).
@@ -465,8 +520,7 @@ public class SequenceTest
     assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0",
             PA.getValue(sq, "cursor").toString());
     sq.sequenceChanged();
-    assertEquals(12, sq.findPosition(8));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(12, sq.findPosition(8)); // E12
     // sequenceChanged() invalidates cursor.lastResidueColumn
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1",
@@ -505,6 +559,13 @@ public class SequenceTest
     assertEquals(6, sq.getEnd());
     assertNull(PA.getValue(sq, "datasetSequence"));
 
+    sq = new Sequence("test", "ABCDE");
+    sq.deleteChars(0, 3);
+    assertEquals("DE", sq.getSequenceAsString());
+    assertEquals(4, sq.getStart());
+    assertEquals(5, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
     /*
      * delete at end
      */
@@ -514,6 +575,21 @@ public class SequenceTest
     assertEquals(1, sq.getStart());
     assertEquals(4, sq.getEnd());
     assertNull(PA.getValue(sq, "datasetSequence"));
+
+    /*
+     * delete more positions than there are
+     */
+    sq = new Sequence("test/8-11", "ABCD");
+    sq.deleteChars(0, 99);
+    assertEquals("", sq.getSequenceAsString());
+    assertEquals(12, sq.getStart()); // = findPosition(99) ?!?
+    assertEquals(11, sq.getEnd());
+
+    sq = new Sequence("test/8-11", "----");
+    sq.deleteChars(0, 99); // ArrayIndexOutOfBoundsException <= 2.10.2
+    assertEquals("", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(11, sq.getEnd());
   }
 
   @Test(groups = { "Functional" })
@@ -1614,6 +1690,10 @@ public class SequenceTest
     // cursor should now be at [D 6]
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    assertEquals(0, cursor.lastColumnPosition); // not yet found
+    assertEquals(13, sq.findPosition(8)); // E13
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(9, cursor.lastColumnPosition); // found
 
     /*
      * deleteChars should invalidate the cached cursor
@@ -1693,61 +1773,6 @@ public class SequenceTest
   }
 
   @Test(groups = { "Functional" })
-  public void testFindPositions()
-  {
-    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
-
-    /*
-     * invalid inputs
-     */
-    assertNull(sq.findPositions(6, 5));
-    assertNull(sq.findPositions(0, 5));
-    assertNull(sq.findPositions(-1, 5));
-
-    /*
-     * all gapped ranges
-     */
-    assertNull(sq.findPositions(1, 1)); // 1-based columns
-    assertNull(sq.findPositions(5, 5));
-    assertNull(sq.findPositions(5, 6));
-    assertNull(sq.findPositions(5, 7));
-
-    /*
-     * all ungapped ranges
-     */
-    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
-    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
-    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
-    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
-
-    /*
-     * gap to ungapped range
-     */
-    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
-    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
-
-    /*
-     * ungapped to gapped range
-     */
-    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
-    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
-
-    /*
-     * ungapped to ungapped enclosing gaps
-     */
-    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
-    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
-
-    /*
-     * gapped to gapped enclosing ungapped
-     */
-    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
-    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
-    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
-    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
-  }
-
-  @Test(groups = { "Functional" })
   public void testGapBitset()
   {
     SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
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 32987b0..29e76bb 100644 (file)
@@ -1161,7 +1161,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);
@@ -1179,7 +1179,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();
@@ -1208,7 +1208,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);
@@ -1218,6 +1218,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")
@@ -1232,4 +1261,18 @@ public class SequenceFeaturesTest
     assertTrue(store.isOntologyTerm("junk", new String[] {}));
     assertTrue(store.isOntologyTerm("junk", (String[]) null));
   }
+
+  @Test(groups = "Functional")
+  public void testDeleteAll()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertFalse(store.hasFeatures());
+    store.deleteAll();
+    assertFalse(store.hasFeatures());
+    store.add(new SequenceFeature("Cath", "Desc", 12, 20, 0f, "Group"));
+    store.add(new SequenceFeature("Pfam", "Desc", 6, 12, 2f, "Group2"));
+    assertTrue(store.hasFeatures());
+    store.deleteAll();
+    assertFalse(store.hasFeatures());
+  }
 }