X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fcommands%2FEditCommand.java;h=b813dcfa13cc6d6a551ff7cb8dd96cf2ef35563e;hb=0ddbb5cb5c0ae3e53d344c624c717d5ca3085d5f;hp=82de3b2f96b022efff0ea0b9a25718cfc2d9d375;hpb=2273eba5668e5340354da60fed329c6c716cc439;p=jalview.git diff --git a/src/jalview/commands/EditCommand.java b/src/jalview/commands/EditCommand.java index 82de3b2..b813dcf 100644 --- a/src/jalview/commands/EditCommand.java +++ b/src/jalview/commands/EditCommand.java @@ -1,6 +1,6 @@ /* - * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2) - * 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,10 +63,58 @@ 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 edits = new ArrayList(); + private List edits = new ArrayList<>(); String description; @@ -69,15 +122,15 @@ public class EditCommand implements CommandI { } - public EditCommand(String description) + public EditCommand(String desc) { - this.description = description; + this.description = desc; } - public EditCommand(String description, Action command, SequenceI[] seqs, + public EditCommand(String desc, Action command, SequenceI[] seqs, int position, int number, AlignmentI al) { - this.description = description; + this.description = desc; if (command == Action.CUT || command == Action.PASTE) { setEdit(new Edit(command, seqs, position, number, al)); @@ -86,10 +139,10 @@ public class EditCommand implements CommandI performEdit(0, null); } - public EditCommand(String description, Action command, String replace, + public EditCommand(String desc, Action command, String replace, SequenceI[] seqs, int position, int number, AlignmentI al) { - this.description = description; + this.description = desc; if (command == Action.REPLACE) { setEdit(new Edit(command, seqs, position, number, al, replace)); @@ -110,13 +163,90 @@ 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 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 + *
    + *
  • a new Insert positions to the right of the last , + * or
  • + *
  • a new Delete gaps which is positions to the left of the last + * delete.
  • + *
+ */ + 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 +309,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 +327,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 +338,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 +377,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 iterator = edits.listIterator(commandIndex); while (iterator.hasNext()) @@ -239,7 +393,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 +433,7 @@ public class EditCommand implements CommandI */ @Override final public void undoCommand(AlignmentI[] views) - { + { ListIterator iterator = edits.listIterator(edits.size()); while (iterator.hasPrevious()) { @@ -316,14 +470,15 @@ 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); - // System.out.println("pos: "+command.position+" number: "+command.number); + // System.out.println("pos: "+command.position+" number: + // "+command.number); } adjustAnnotations(command, true, false, null); @@ -348,12 +503,12 @@ 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++) { - command.seqs[s].deleteChars(command.position, command.position - + command.number); + command.seqs[s].deleteChars(command.position, + command.position + command.number); } adjustAnnotations(command, false, false, null); @@ -366,7 +521,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][]; @@ -384,8 +539,8 @@ public class EditCommand implements CommandI // we are redoing an undone cut. sequence.setDatasetSequence(null); } - sequence.deleteChars(command.position, command.position - + command.number); + sequence.deleteChars(command.position, + command.position + command.number); if (command.oldds != null && command.oldds[i] != null) { // oldds entry contains the cut dataset sequence. @@ -396,19 +551,20 @@ public class EditCommand implements CommandI { // modify the oldds if necessary if (oldds != sequence.getDatasetSequence() - || sequence.getSequenceFeatures() != null) + || sequence.getFeatures().hasFeatures()) { if (command.oldds == null) { command.oldds = new SequenceI[command.seqs.length]; } 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); + sequence.findPosition(command.position + command.number), + false); } } } @@ -430,7 +586,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,11 +602,11 @@ 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 sequences; - synchronized (sequences = command.al.getSequences()) + List sequences = command.al.getSequences(); + synchronized (sequences) { if (!(command.alIndex[i] < 0)) { @@ -494,8 +650,8 @@ public class EditCommand implements CommandI { newDSNeeded = true; start = command.seqs[i].findPosition(command.position); - end = command.seqs[i].findPosition(command.position - + command.number); + end = command.seqs[i] + .findPosition(command.position + command.number); } if (command.seqs[i].getStart() == start) { @@ -548,7 +704,7 @@ public class EditCommand implements CommandI command.string = null; } - void replace(Edit command) + static void replace(Edit command) { StringBuffer tmp; String oldstring; @@ -579,16 +735,16 @@ public class EditCommand implements CommandI tmp = new StringBuffer(oldstring.substring(0, start)); tmp.append(command.string[i]); String nogaprep = jalview.analysis.AlignSeq.extractGaps( - jalview.util.Comparison.GapChars, new String( - command.string[i])); + jalview.util.Comparison.GapChars, + new String(command.string[i])); int ipos = command.seqs[i].findPosition(start) - command.seqs[i].getStart(); 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, new String( - command.string[i])); + jalview.util.Comparison.GapChars, + new String(command.string[i])); if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase())) { if (newDSWasNeeded) @@ -625,7 +781,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; @@ -633,7 +789,7 @@ public class EditCommand implements CommandI if (modifyVisibility && !insert) { // only occurs if a sequence was added or deleted. - command.deletedAnnotationRows = new Hashtable(); + command.deletedAnnotationRows = new Hashtable<>(); } if (command.fullAlignmentHeight) { @@ -645,6 +801,8 @@ public class EditCommand implements CommandI AlignmentAnnotation[] tmp; for (int s = 0; s < command.seqs.length; s++) { + command.seqs[s].sequenceChanged(); + if (modifyVisibility) { // Rows are only removed or added to sequence object. @@ -681,7 +839,8 @@ public class EditCommand implements CommandI tmp = saved; command.deletedAnnotationRows.put(command.seqs[s], saved); // and then remove any annotation in the other views - for (int alview = 0; views != null && alview < views.length; alview++) + for (int alview = 0; views != null + && alview < views.length; alview++) { if (views[alview] != command.al) { @@ -733,7 +892,8 @@ 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) { @@ -788,7 +948,7 @@ public class EditCommand implements CommandI if (!insert) { - command.deletedAnnotations = new Hashtable(); + command.deletedAnnotations = new Hashtable<>(); } int aSize; @@ -860,8 +1020,8 @@ public class EditCommand implements CommandI } System.arraycopy(annotations[a].annotations, command.position, - temp, command.position + command.number, aSize - - command.position); + temp, command.position + command.number, + aSize - command.position); } else { @@ -892,7 +1052,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); } @@ -904,8 +1064,8 @@ public class EditCommand implements CommandI annotations[a].annotations.length - command.position); if (copylen > 0) { - System.arraycopy(annotations[a].annotations, - command.position, deleted, 0, copylen); // command.number); + System.arraycopy(annotations[a].annotations, command.position, + deleted, 0, copylen); // command.number); } } @@ -914,10 +1074,10 @@ public class EditCommand implements CommandI if (annotations[a].annotations.length > command.position + command.number) { - System.arraycopy(annotations[a].annotations, command.position - + command.number, temp, command.position, - annotations[a].annotations.length - command.position - - command.number); // aSize + System.arraycopy(annotations[a].annotations, + command.position + command.number, temp, + command.position, annotations[a].annotations.length + - command.position - command.number); // aSize } } else @@ -949,8 +1109,8 @@ public class EditCommand implements CommandI } } - final void adjustFeatures(Edit command, int index, int i, int j, - boolean insert) + final static void adjustFeatures(Edit command, int index, final int i, + final int j, boolean insert) { SequenceI seq = command.seqs[index]; SequenceI sequence = seq.getDatasetSequence(); @@ -964,70 +1124,196 @@ 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; } - SequenceFeature[] sf = sequence.getSequenceFeatures(); + List sf = sequence.getFeatures() + .getPositionalFeatures(); - if (sf == null) + if (sf.isEmpty()) { return; } - SequenceFeature[] oldsf = new SequenceFeature[sf.length]; + List oldsf = new ArrayList<>(); int cSize = j - i; - for (int s = 0; s < sf.length; s++) + for (SequenceFeature feature : sf) { - SequenceFeature copy = new SequenceFeature(sf[s]); + SequenceFeature copy = new SequenceFeature(feature); - oldsf[s] = copy; + oldsf.add(copy); - if (sf[s].getEnd() < i) + if (feature.getEnd() < i) { continue; } - if (sf[s].getBegin() > j) + if (feature.getBegin() > j) { - sf[s].setBegin(copy.getBegin() - cSize); - sf[s].setEnd(copy.getEnd() - cSize); + 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; } - if (sf[s].getBegin() >= i) + int newBegin = feature.getBegin(); + int newEnd = feature.getEnd(); + if (newBegin >= i) { - sf[s].setBegin(i); + newBegin = i; + // feature.setBegin(i); } - if (sf[s].getEnd() < j) + if (newEnd < j) { - sf[s].setEnd(j - 1); + newEnd = j - 1; + // feature.setEnd(j - 1); } + newEnd = newEnd - cSize; + // feature.setEnd(feature.getEnd() - (cSize)); - sf[s].setEnd(sf[s].getEnd() - (cSize)); - - if (sf[s].getBegin() > sf[s].getEnd()) + sequence.deleteFeature(feature); + if (newEnd >= newBegin) { - sequence.deleteFeature(sf[s]); + 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(); + command.editedFeatures = new Hashtable<>(); } command.editedFeatures.put(seq, oldsf); } - class Edit + /** + * Returns the list of edit commands wrapped by this object. + * + * @return + */ + public List 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 priorState(boolean forUndo) + { + Map result = new HashMap<>(); + 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 editList = new ReverseListIterator<>(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; @@ -1037,7 +1323,7 @@ public class EditCommand implements CommandI Hashtable deletedAnnotations; - Hashtable editedFeatures; + Hashtable> editedFeatures; AlignmentI al; @@ -1053,51 +1339,95 @@ public class EditCommand implements CommandI char gapChar; - Edit(Action command, SequenceI[] seqs, int position, int number, - char gapChar) + public Edit(Action cmd, SequenceI[] sqs, int pos, int count, + char gap) { - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.gapChar = gapChar; + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.gapChar = gap; } - Edit(Action command, SequenceI[] seqs, int position, int number, - AlignmentI al) + Edit(Action cmd, SequenceI[] sqs, int pos, int count, + AlignmentI align) { - this.gapChar = al.getGapCharacter(); - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.al = al; - - alIndex = new int[seqs.length]; - for (int i = 0; i < seqs.length; i++) + this.gapChar = align.getGapCharacter(); + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.al = align; + + alIndex = new int[sqs.length]; + for (int i = 0; i < sqs.length; i++) { - alIndex[i] = al.findIndex(seqs[i]); + alIndex[i] = align.findIndex(sqs[i]); } - fullAlignmentHeight = (al.getHeight() == seqs.length); + fullAlignmentHeight = (align.getHeight() == sqs.length); } - Edit(Action command, SequenceI[] seqs, int position, int number, - AlignmentI al, String replace) + Edit(Action cmd, SequenceI[] sqs, int pos, int count, + AlignmentI align, String replace) { - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.al = al; - this.gapChar = al.getGapCharacter(); - string = new char[seqs.length][]; - for (int i = 0; i < seqs.length; i++) + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.al = align; + this.gapChar = align.getGapCharacter(); + string = new char[sqs.length][]; + for (int i = 0; i < sqs.length; i++) { string[i] = replace.toCharArray(); } - fullAlignmentHeight = (al.getHeight() == seqs.length); + fullAlignmentHeight = (align.getHeight() == sqs.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 getEditIterator(boolean forwards) + { + if (forwards) + { + return getEdits().iterator(); + } + else + { + return new ReverseListIterator<>(getEdits()); } } }