JAL-2541 it works!! it works!!!
[jalview.git] / src / jalview / commands / EditCommand.java
index 5248dc9..9be8d3f 100644 (file)
@@ -24,9 +24,11 @@ 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;
@@ -332,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);
   }
 
   /**
@@ -541,8 +531,14 @@ public class EditCommand implements CommandI
           // 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();
+        sequence.deleteChars(command.position, command.position
+                + command.number);
+
         if (command.oldds != null && command.oldds[i] != null)
         {
           // oldds entry contains the cut dataset sequence.
@@ -560,22 +556,16 @@ public class EditCommand implements CommandI
               command.oldds = new SequenceI[command.seqs.length];
             }
             command.oldds[i] = oldds;
-            // FIXME JAL-2541 JAL-2526 get correct positions if on a gap
-            List<SequenceFeature[]> amendedFeatures = sequence
-                    .adjustFeatures(command.position, command.position
-                            + command.number - 1);
-            if (command.editedFeatures == null)
+            if (oldds != sequence.getDatasetSequence())
             {
-              command.editedFeatures = new HashMap<>();
+              oldds.getFeatures().deleteAll();
+            }
+
+            if (cutPositions != null)
+            {
+              cutFeatures(command, sequence, cutPositions.getBegin(),
+                              cutPositions.getEnd(), cutIsInternal);
             }
-            command.editedFeatures.put(sequence, amendedFeatures);
-            //
-            // adjustFeatures(
-            // command,
-            // i,
-            // sequence.findPosition(command.position),
-            // sequence.findPosition(command.position + command.number),
-            // false);
           }
         }
       }
@@ -599,22 +589,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;
+      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;
@@ -632,48 +619,50 @@ public class EditCommand implements CommandI
         }
         seqWasDeleted = true;
       }
-      newstart = sequence.getStart();
-      newend = sequence.getEnd();
+      int newStart = sequence.getStart();
+      int newEnd = sequence.getEnd();
 
-      tmp = new StringBuffer();
+      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 = sequence.findPosition(command.position);
-              end = sequence.findPosition(command.position
-                      + command.number);
+              // end = sequence
+              // .findPosition(command.position + command.number);
             }
             if (sequence.getStart() == start)
             {
-              newstart--;
+              newStart--;
             }
             else
             {
-              newend++;
+              newEnd++;
             }
           }
         }
@@ -681,8 +670,14 @@ public class EditCommand implements CommandI
       }
 
       sequence.setSequence(tmp.toString());
-      sequence.setStart(newstart);
-      sequence.setEnd(newend);
+      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 (sequence.getDatasetSequence() != null)
@@ -707,9 +702,12 @@ public class EditCommand implements CommandI
             command.oldds = new SequenceI[command.seqs.length];
           }
           command.oldds[i] = sequence.getDatasetSequence();
+          sameDatasetSequence = ds == sequence.getDatasetSequence();
+          ds.setSequenceFeatures(sequence.getSequenceFeatures());
           sequence.setDatasetSequence(ds);
         }
-        undoCutFeatures(command, i, start, end);
+        undoCutFeatures(command, command.seqs[i], start, length,
+                sameDatasetSequence);
       }
     }
     adjustAnnotations(command, true, seqWasDeleted, views);
@@ -755,8 +753,7 @@ public class EditCommand implements CommandI
       tmp.append(oldstring.substring(end));
       command.seqs[i].setSequence(tmp.toString());
       command.string[i] = oldstring.substring(start, end).toCharArray();
-      String nogapold = jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars,
+      String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
               new String(command.string[i]));
       if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
       {
@@ -905,8 +902,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)
                   {
@@ -1122,10 +1118,29 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void undoCutFeatures(Edit command, int index, final int i,
-          final int j)
+  /**
+   * 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)
     {
@@ -1133,101 +1148,66 @@ public class EditCommand implements CommandI
     }
 
     /*
-     * insert == true for an Undo of a Cut; restore the original features
-     * and delete the amended ones
+     * 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 (true)
+    if (!sameDatasetSequence)
     {
-      // TODO shift right features that lie to the right of the restored cut
-      // (add a start position parameter to SequenceFeatures.shift)
+      /*
+       * shift right all features right of and not 
+       * contiguous with the cut position
+       */
+      seq.getFeatures().shiftFeatures(start + 1, length);
 
-      if (command.editedFeatures != null
-              && command.editedFeatures.containsKey(seq))
+      /*
+       * shift right any features that start at the cut position,
+       * unless they were truncated
+       */
+      List<SequenceFeature> sfs = seq.findFeatures(start, start);
+      for (SequenceFeature sf : sfs)
       {
-        for (SequenceFeature[] toRestore : command.editedFeatures.get(seq))
+        if (sf.getBegin() == start)
         {
-          sequence.addSequenceFeature(toRestore[0]);
-          if (toRestore[1] != null)
+          if (!command.truncatedFeatures.containsKey(seq)
+                  || !command.truncatedFeatures.get(seq).contains(sf))
           {
-            sequence.deleteFeature(toRestore[1]);
+            /*
+             * 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)
-    // {
-    // 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);
+    /*
+     * restore any features that were deleted or truncated
+     */
+    if (command.deletedFeatures != null
+            && command.deletedFeatures.containsKey(seq))
+    {
+      for (SequenceFeature deleted : command.deletedFeatures.get(seq))
+      {
+        sequence.addSequenceFeature(deleted);
+      }
+    }
 
+    /*
+     * delete any truncated features
+     */
+    if (command.truncatedFeatures != null
+            && command.truncatedFeatures.containsKey(seq))
+    {
+      for (SequenceFeature amended : command.truncatedFeatures.get(seq))
+      {
+        sequence.deleteFeature(amended);
+      }
+    }
   }
 
   /**
@@ -1350,7 +1330,16 @@ public class EditCommand implements CommandI
 
     Map<String, Annotation[]> deletedAnnotations;
 
-    Map<SequenceI, List<SequenceFeature[]>> editedFeatures;
+    /*
+     * features deleted by the cut (re-add on Undo)
+     * (including the original of any shortened features)
+     */
+    Map<SequenceI, List<SequenceFeature>> deletedFeatures;
+
+    /*
+     * shortened features added by the cut (delete on Undo)
+     */
+    Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
 
     AlignmentI al;
 
@@ -1379,11 +1368,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];
@@ -1398,19 +1384,13 @@ public class EditCommand implements CommandI
     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()
@@ -1457,4 +1437,159 @@ public class EditCommand implements CommandI
       return new ReverseListIterator<Edit>(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 shorted 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 (!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
+       * (but not if the cut is at start or end of sequence)
+       */
+      if (cutIsInternal)
+      {
+        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);
+  }
 }