Merge develop to Release_2_8_3_Branch
[jalview.git] / src / jalview / commands / EditCommand.java
index db837db..2d3a8d8 100644 (file)
@@ -26,11 +26,16 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.util.ReverseListIterator;
+import jalview.util.StringUtils;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Map;
 
 /**
  * 
@@ -58,7 +63,55 @@ public class EditCommand implements CommandI
 {
   public enum Action
   {
-    INSERT_GAP, DELETE_GAP, CUT, PASTE, REPLACE, INSERT_NUC
+    INSERT_GAP
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return DELETE_GAP;
+      }
+    },
+    DELETE_GAP
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return INSERT_GAP;
+      }
+    },
+    CUT
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return PASTE;
+      }
+    },
+    PASTE
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return CUT;
+      }
+    },
+    REPLACE
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return REPLACE;
+      }
+    },
+    INSERT_NUC
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return null;
+      }
+    };
+    public abstract Action getUndoAction();
   };
 
   private List<Edit> edits = new ArrayList<Edit>();
@@ -110,13 +163,88 @@ public class EditCommand implements CommandI
   }
 
   /**
-   * Add the given edit command to the stored list of commands.
+   * Add the given edit command to the stored list of commands. If simply
+   * expanding the range of the last command added, then modify it instead of
+   * adding a new command.
    * 
    * @param e
    */
-  protected void addEdit(Edit e)
+  public void addEdit(Edit e)
   {
-    edits.add(e);
+    if (!expandEdit(edits, e))
+    {
+      edits.add(e);
+    }
+  }
+
+  /**
+   * Returns true if the new edit is incorporated by updating (expanding the
+   * range of) the last edit on the list, else false. We can 'expand' the last
+   * edit if the new one is the same action, on the same sequences, and acts on
+   * a contiguous range. This is the case where a mouse drag generates a series
+   * of contiguous gap insertions or deletions.
+   * 
+   * @param edits
+   * @param e
+   * @return
+   */
+  protected static boolean expandEdit(List<Edit> edits, Edit e)
+  {
+    if (edits == null || edits.isEmpty())
+    {
+      return false;
+    }
+    Edit lastEdit = edits.get(edits.size() - 1);
+    Action action = e.command;
+    if (lastEdit.command != action)
+    {
+      return false;
+    }
+
+    /*
+     * Both commands must act on the same sequences - compare the underlying
+     * dataset sequences, rather than the aligned sequences, which change as
+     * they are edited.
+     */
+    if (lastEdit.seqs.length != e.seqs.length)
+    {
+      return false;
+    }
+    for (int i = 0; i < e.seqs.length; i++)
+    {
+      if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
+              .getDatasetSequence())
+      {
+        return false;
+      }
+    }
+
+    /**
+     * Check a contiguous edit; either
+     * <ul>
+     * <li>a new Insert <n> positions to the right of the last <insert n>, or</li>
+     * <li>a new Delete <n> gaps which is <n> positions to the left of the last
+     * delete.</li>
+     * </ul>
+     */
+    boolean contiguous = (action == Action.INSERT_GAP && e.position == lastEdit.position
+            + lastEdit.number)
+            || (action == Action.DELETE_GAP && e.position + e.number == lastEdit.position);
+    if (contiguous)
+    {
+      /*
+       * We are just expanding the range of the last edit. For delete gap, also
+       * moving the start position left.
+       */
+      lastEdit.number += e.number;
+      lastEdit.seqs = e.seqs;
+      if (action == Action.DELETE_GAP)
+      {
+        lastEdit.position--;
+      }
+      return true;
+    }
+    return false;
   }
 
   /**
@@ -209,7 +337,32 @@ public class EditCommand implements CommandI
       edit.fullAlignmentHeight = true;
     }
 
-    edits.add(edit);
+    addEdit(edit);
+
+    if (performEdit)
+    {
+      performEdit(edit, views);
+    }
+  }
+
+  /**
+   * Overloaded method that accepts an Edit object with additional parameters.
+   * 
+   * @param edit
+   * @param al
+   * @param performEdit
+   * @param views
+   */
+  final public void appendEdit(Edit edit, AlignmentI al,
+          boolean performEdit, AlignmentI[] views)
+  {
+    if (al.getHeight() == edit.seqs.length)
+    {
+      edit.al = al;
+      edit.fullAlignmentHeight = true;
+    }
+
+    addEdit(edit);
 
     if (performEdit)
     {
@@ -223,7 +376,7 @@ public class EditCommand implements CommandI
    * @param commandIndex
    * @param views
    */
-  final void performEdit(int commandIndex, AlignmentI[] views)
+  public final void performEdit(int commandIndex, AlignmentI[] views)
   {
     ListIterator<Edit> iterator = edits.listIterator(commandIndex);
     while (iterator.hasNext())
@@ -239,7 +392,7 @@ public class EditCommand implements CommandI
    * @param edit
    * @param views
    */
-  protected void performEdit(Edit edit, AlignmentI[] views)
+  protected static void performEdit(Edit edit, AlignmentI[] views)
   {
     switch (edit.command)
     {
@@ -316,13 +469,13 @@ public class EditCommand implements CommandI
    * 
    * @param command
    */
-  final private void insertGap(Edit command)
+  final private static void insertGap(Edit command)
   {
 
     for (int s = 0; s < command.seqs.length; s++)
     {
-      command.seqs[s].insertCharAt(command.position, command.number,
-              command.gapChar);
+      command.seqs[s].insertCharAt(command.position,
+              command.number, command.gapChar);
       // System.out.println("pos: "+command.position+" number: "+command.number);
     }
 
@@ -348,7 +501,7 @@ public class EditCommand implements CommandI
    * 
    * @param command
    */
-  final private void deleteGap(Edit command)
+  final static private void deleteGap(Edit command)
   {
     for (int s = 0; s < command.seqs.length; s++)
     {
@@ -366,7 +519,7 @@ public class EditCommand implements CommandI
    * @param command
    * @param views
    */
-  void cut(Edit command, AlignmentI[] views)
+  static void cut(Edit command, AlignmentI[] views)
   {
     boolean seqDeleted = false;
     command.string = new char[command.seqs.length][];
@@ -430,7 +583,7 @@ public class EditCommand implements CommandI
    * @param command
    * @param views
    */
-  void paste(Edit command, AlignmentI[] views)
+  static void paste(Edit command, AlignmentI[] views)
   {
     StringBuffer tmp;
     boolean newDSNeeded;
@@ -446,7 +599,7 @@ public class EditCommand implements CommandI
       if (command.seqs[i].getLength() < 1)
       {
         // ie this sequence was deleted, we need to
-        // read it to the alignment
+        // readd it to the alignment
         if (command.alIndex[i] < command.al.getHeight())
         {
           List<SequenceI> sequences;
@@ -548,7 +701,7 @@ public class EditCommand implements CommandI
     command.string = null;
   }
 
-  void replace(Edit command)
+  static void replace(Edit command)
   {
     StringBuffer tmp;
     String oldstring;
@@ -625,7 +778,7 @@ public class EditCommand implements CommandI
     }
   }
 
-  final void adjustAnnotations(Edit command, boolean insert,
+  final static void adjustAnnotations(Edit command, boolean insert,
           boolean modifyVisibility, AlignmentI[] views)
   {
     AlignmentAnnotation[] annotations = null;
@@ -949,7 +1102,7 @@ public class EditCommand implements CommandI
     }
   }
 
-  final void adjustFeatures(Edit command, int index, int i, int j,
+  final static void adjustFeatures(Edit command, int index, int i, int j,
           boolean insert)
   {
     SequenceI seq = command.seqs[index];
@@ -1027,7 +1180,112 @@ public class EditCommand implements CommandI
 
   }
 
-  class Edit
+  /**
+   * Returns the list of edit commands wrapped by this object.
+   * 
+   * @return
+   */
+  public List<Edit> getEdits()
+  {
+    return this.edits;
+  }
+
+  /**
+   * Returns a map whose keys are the dataset sequences, and values their
+   * aligned sequences before the command edit list was applied. The aligned
+   * sequences are copies, which may be updated without affecting the originals.
+   * 
+   * The command holds references to the aligned sequences (after editing). If
+   * the command is an 'undo',then the prior state is simply the aligned state.
+   * Otherwise, we have to derive the prior state by working backwards through
+   * the edit list to infer the aligned sequences before editing.
+   * 
+   * Note: an alternative solution would be to cache the 'before' state of each
+   * edit, but this would be expensive in space in the common case that the
+   * original is never needed (edits are not mirrored).
+   * 
+   * @return
+   * @throws IllegalStateException
+   *           on detecting an edit command of a type that can't be unwound
+   */
+  public Map<SequenceI, SequenceI> priorState(boolean forUndo)
+  {
+    Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
+    if (getEdits() == null)
+    {
+      return result;
+    }
+    if (forUndo)
+    {
+      for (Edit e : getEdits())
+      {
+        for (SequenceI seq : e.getSequences())
+        {
+          SequenceI ds = seq.getDatasetSequence();
+          SequenceI preEdit = result.get(ds);
+          if (preEdit == null)
+          {
+            preEdit = new Sequence("", seq.getSequenceAsString());
+            preEdit.setDatasetSequence(ds);
+            result.put(ds, preEdit);
+          }
+        }
+      }
+      return result;
+    }
+
+    /*
+     * 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> edits = new ReverseListIterator<Edit>(getEdits());
+    while (edits.hasNext())
+    {
+      Edit oldEdit = edits.next();
+      Action action = oldEdit.getAction();
+      int position = oldEdit.getPosition();
+      int number = oldEdit.getNumber();
+      final char gap = oldEdit.getGapCharacter();
+      for (SequenceI seq : oldEdit.getSequences())
+      {
+        SequenceI ds = seq.getDatasetSequence();
+        SequenceI preEdit = result.get(ds);
+        if (preEdit == null)
+        {
+          preEdit = new Sequence("", seq.getSequenceAsString());
+          preEdit.setDatasetSequence(ds);
+          result.put(ds, preEdit);
+        }
+        /*
+         * 'Undo' this edit action on the sequence (updating the value in the
+         * map).
+         */
+        if (ds != null)
+        {
+          if (action == Action.DELETE_GAP)
+          {
+            preEdit.setSequence(new String(StringUtils.insertCharAt(
+                    preEdit.getSequence(), position,
+                    number, gap)));
+          }
+          else if (action == Action.INSERT_GAP)
+          {
+            preEdit.setSequence(new String(StringUtils.deleteChars(
+                    preEdit.getSequence(), position, position + number)));
+          }
+          else
+          {
+            System.err.println("Can't undo edit action " + action);
+            // throw new IllegalStateException("Can't undo edit action " +
+            // action);
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  public class Edit
   {
     public SequenceI[] oldds;
 
@@ -1053,7 +1311,7 @@ public class EditCommand implements CommandI
 
     char gapChar;
 
-    Edit(Action command, SequenceI[] seqs, int position, int number,
+    public Edit(Action command, SequenceI[] seqs, int position, int number,
             char gapChar)
     {
       this.command = command;
@@ -1099,5 +1357,49 @@ public class EditCommand implements CommandI
 
       fullAlignmentHeight = (al.getHeight() == seqs.length);
     }
+
+    public SequenceI[] getSequences()
+    {
+      return seqs;
+    }
+
+    public int getPosition()
+    {
+      return position;
+    }
+
+    public Action getAction()
+    {
+      return command;
+    }
+
+    public int getNumber()
+    {
+      return number;
+    }
+
+    public char getGapCharacter()
+    {
+      return gapChar;
+    }
+  }
+
+  /**
+   * Returns an iterator over the list of edit commands which traverses the list
+   * either forwards or backwards.
+   * 
+   * @param forwards
+   * @return
+   */
+  public Iterator<Edit> getEditIterator(boolean forwards)
+  {
+    if (forwards)
+    {
+      return getEdits().iterator();
+    }
+    else
+    {
+      return new ReverseListIterator<Edit>(getEdits());
+    }
   }
 }