JAL-2089 patch broken merge to master for Release 2.10.0b1
[jalview.git] / src / jalview / commands / EditCommand.java
index aff8595..21ff841 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2b1)
- * Copyright (C) 2014 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
@@ -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;
   }
 
   /**
@@ -179,8 +307,7 @@ public class EditCommand implements CommandI
    * @param performEdit
    */
   final public void appendEdit(Action command, SequenceI[] seqs,
-          int position,
-          int number, AlignmentI al, boolean performEdit)
+          int position, int number, AlignmentI al, boolean performEdit)
   {
     appendEdit(command, seqs, position, number, al, performEdit, null);
   }
@@ -198,8 +325,8 @@ public class EditCommand implements CommandI
    * @param views
    */
   final public void appendEdit(Action command, SequenceI[] seqs,
-          int position,
-          int number, AlignmentI al, boolean performEdit, AlignmentI[] views)
+          int position, int number, AlignmentI al, boolean performEdit,
+          AlignmentI[] views)
   {
     Edit edit = new Edit(command, seqs, position, number,
             al.getGapCharacter());
@@ -209,7 +336,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 +375,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 +391,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)
     {
@@ -279,7 +431,7 @@ public class EditCommand implements CommandI
    */
   @Override
   final public void undoCommand(AlignmentI[] views)
-  { 
+  {
     ListIterator<Edit> iterator = edits.listIterator(edits.size());
     while (iterator.hasPrevious())
     {
@@ -316,7 +468,7 @@ 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++)
@@ -348,7 +500,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 +518,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][];
@@ -407,8 +559,8 @@ public class EditCommand implements CommandI
                     command,
                     i,
                     sequence.findPosition(command.position),
-                    sequence.findPosition(command.position
-                            + command.number), false);
+                    sequence.findPosition(command.position + command.number),
+                    false);
           }
         }
       }
@@ -430,7 +582,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 +598,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 +700,7 @@ public class EditCommand implements CommandI
     command.string = null;
   }
 
-  void replace(Edit command)
+  static void replace(Edit command)
   {
     StringBuffer tmp;
     String oldstring;
@@ -625,7 +777,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;
@@ -892,7 +1044,7 @@ public class EditCommand implements CommandI
           int copylen = Math.min(command.position,
                   annotations[a].annotations.length);
           if (copylen > 0)
-           {
+          {
             System.arraycopy(annotations[a].annotations, 0, temp, 0,
                     copylen); // command.position);
           }
@@ -949,7 +1101,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];
@@ -964,8 +1116,7 @@ public class EditCommand implements CommandI
       if (command.editedFeatures != null
               && command.editedFeatures.containsKey(seq))
       {
-        sequence.setSequenceFeatures(command.editedFeatures
-                .get(seq));
+        sequence.setSequenceFeatures(command.editedFeatures.get(seq));
       }
 
       return;
@@ -1027,7 +1178,117 @@ 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 (!result.containsKey(ds))
+          {
+            /*
+             * copy sequence including start/end (but don't use copy constructor
+             * as we don't need annotations)
+             */
+            SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
+                    seq.getStart(), seq.getEnd());
+            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> editList = new ReverseListIterator<Edit>(getEdits());
+    while (editList.hasNext())
+    {
+      Edit oldEdit = editList.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(),
+                  seq.getStart(), seq.getEnd());
+          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 +1314,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 +1360,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());
+    }
   }
 }