2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
21 package jalview.commands;
23 import jalview.analysis.AlignSeq;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.Annotation;
27 import jalview.datamodel.ContiguousI;
28 import jalview.datamodel.Range;
29 import jalview.datamodel.Sequence;
30 import jalview.datamodel.SequenceFeature;
31 import jalview.datamodel.SequenceI;
32 import jalview.datamodel.features.SequenceFeaturesI;
33 import jalview.util.Comparison;
34 import jalview.util.ReverseListIterator;
35 import jalview.util.StringUtils;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.Hashtable;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.ListIterator;
52 * Description: Essential information for performing undo and redo for cut/paste
53 * insert/delete gap which can be stored in the HistoryList
57 * Copyright: Copyright (c) 2006
61 * Company: Dundee University
64 * @author not attributable
67 public class EditCommand implements CommandI
74 public Action getUndoAction()
82 public Action getUndoAction()
90 public Action getUndoAction()
98 public Action getUndoAction()
106 public Action getUndoAction()
114 public Action getUndoAction()
119 public abstract Action getUndoAction();
122 private List<Edit> edits = new ArrayList<>();
130 public EditCommand(String desc)
132 this.description = desc;
135 public EditCommand(String desc, Action command, SequenceI[] seqs,
136 int position, int number, AlignmentI al)
138 this.description = desc;
139 if (command == Action.CUT || command == Action.PASTE)
141 setEdit(new Edit(command, seqs, position, number, al));
144 performEdit(0, null);
147 public EditCommand(String desc, Action command, String replace,
148 SequenceI[] seqs, int position, int number, AlignmentI al)
150 this.description = desc;
151 if (command == Action.REPLACE)
153 setEdit(new Edit(command, seqs, position, number, al, replace));
156 performEdit(0, null);
160 * Set the list of edits to the specified item (only).
164 protected void setEdit(Edit e)
171 * Add the given edit command to the stored list of commands. If simply
172 * expanding the range of the last command added, then modify it instead of
173 * adding a new command.
177 public void addEdit(Edit e)
179 if (!expandEdit(edits, e))
186 * Returns true if the new edit is incorporated by updating (expanding the
187 * range of) the last edit on the list, else false. We can 'expand' the last
188 * edit if the new one is the same action, on the same sequences, and acts on
189 * a contiguous range. This is the case where a mouse drag generates a series
190 * of contiguous gap insertions or deletions.
196 protected static boolean expandEdit(List<Edit> edits, Edit e)
198 if (edits == null || edits.isEmpty())
202 Edit lastEdit = edits.get(edits.size() - 1);
203 Action action = e.command;
204 if (lastEdit.command != action)
210 * Both commands must act on the same sequences - compare the underlying
211 * dataset sequences, rather than the aligned sequences, which change as
214 if (lastEdit.seqs.length != e.seqs.length)
218 for (int i = 0; i < e.seqs.length; i++)
220 if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
221 .getDatasetSequence())
228 * Check a contiguous edit; either
230 * <li>a new Insert <n> positions to the right of the last <insert n>,
232 * <li>a new Delete <n> gaps which is <n> positions to the left of the last
236 boolean contiguous = (action == Action.INSERT_GAP
237 && e.position == lastEdit.position + lastEdit.number)
238 || (action == Action.DELETE_GAP
239 && e.position + e.number == lastEdit.position);
243 * We are just expanding the range of the last edit. For delete gap, also
244 * moving the start position left.
246 lastEdit.number += e.number;
247 lastEdit.seqs = e.seqs;
248 if (action == Action.DELETE_GAP)
258 * Clear the list of stored edit commands.
261 protected void clearEdits()
267 * Returns the i'th stored Edit command.
272 protected Edit getEdit(int i)
274 if (i >= 0 && i < edits.size())
282 final public String getDescription()
294 * Return the alignment for the first edit (or null if no edit).
298 final public AlignmentI getAlignment()
300 return (edits.isEmpty() ? null : edits.get(0).al);
304 * append a new editCommand Note. this shouldn't be called if the edit is an
305 * operation affects more alignment objects than the one referenced in al (for
306 * example, cut or pasting whole sequences). Use the form with an additional
307 * AlignmentI[] views parameter.
316 final public void appendEdit(Action command, SequenceI[] seqs,
317 int position, int number, AlignmentI al, boolean performEdit)
319 appendEdit(command, seqs, position, number, al, performEdit, null);
323 * append a new edit command with a set of alignment views that may be
334 final public void appendEdit(Action command, SequenceI[] seqs,
335 int position, int number, AlignmentI al, boolean performEdit,
338 Edit edit = new Edit(command, seqs, position, number, al);
339 appendEdit(edit, al, performEdit, views);
343 * Overloaded method that accepts an Edit object with additional parameters.
350 final public void appendEdit(Edit edit, AlignmentI al,
351 boolean performEdit, AlignmentI[] views)
353 if (al.getHeight() == edit.seqs.length)
356 edit.fullAlignmentHeight = true;
363 performEdit(edit, views);
368 * Execute all the edit commands, starting at the given commandIndex
370 * @param commandIndex
373 public final void performEdit(int commandIndex, AlignmentI[] views)
375 ListIterator<Edit> iterator = edits.listIterator(commandIndex);
376 while (iterator.hasNext())
378 Edit edit = iterator.next();
379 performEdit(edit, views);
384 * Execute one edit command in all the specified alignment views
389 protected static void performEdit(Edit edit, AlignmentI[] views)
391 switch (edit.command)
409 // TODO:add deleteNuc for UNDO
411 // insertNuc(edits[e]);
419 final public void doCommand(AlignmentI[] views)
421 performEdit(0, views);
425 * Undo the stored list of commands, in reverse order.
428 final public void undoCommand(AlignmentI[] views)
430 ListIterator<Edit> iterator = edits.listIterator(edits.size());
431 while (iterator.hasPrevious())
433 Edit e = iterator.previous();
461 * Insert gap(s) in sequences as specified by the command, and adjust
466 final private static void insertGap(Edit command)
469 for (int s = 0; s < command.seqs.length; s++)
471 command.seqs[s].insertCharAt(command.position, command.number,
473 // System.out.println("pos: "+command.position+" number:
474 // "+command.number);
477 adjustAnnotations(command, true, false, null);
481 // final void insertNuc(Edit command)
484 // for (int s = 0; s < command.seqs.length; s++)
486 // System.out.println("pos: "+command.position+" number: "+command.number);
487 // command.seqs[s].insertCharAt(command.position, command.number,'A');
490 // adjustAnnotations(command, true, false, null);
494 * Delete gap(s) in sequences as specified by the command, and adjust
499 final static private void deleteGap(Edit command)
501 for (int s = 0; s < command.seqs.length; s++)
503 command.seqs[s].deleteChars(command.position,
504 command.position + command.number);
507 adjustAnnotations(command, false, false, null);
511 * Carry out a Cut action. The cut characters are saved in case Undo is
517 static void cut(Edit command, AlignmentI[] views)
519 boolean seqDeleted = false;
520 command.string = new char[command.seqs.length][];
522 for (int i = 0; i < command.seqs.length; i++)
524 final SequenceI sequence = command.seqs[i];
525 if (sequence.getLength() > command.position)
527 command.string[i] = sequence.getSequence(command.position,
528 command.position + command.number);
529 SequenceI oldds = sequence.getDatasetSequence();
530 ContiguousI cutPositions = sequence.findPositions(
531 command.position + 1, command.position + command.number);
532 boolean cutIsInternal = cutPositions != null
533 && sequence.getStart() != cutPositions
534 .getBegin() && sequence.getEnd() != cutPositions.getEnd();
537 * perform the cut; if this results in a new dataset sequence, add
538 * that to the alignment dataset
540 SequenceI ds = sequence.getDatasetSequence();
541 sequence.deleteChars(command.position, command.position
544 if (command.oldds != null && command.oldds[i] != null)
547 * we are Redoing a Cut, or Undoing a Paste - so
548 * oldds entry contains the cut dataset sequence,
549 * with sequence features in expected place
551 sequence.setDatasetSequence(command.oldds[i]);
552 command.oldds[i] = oldds;
557 * new cut operation: save the dataset sequence
558 * so it can be restored in an Undo
560 if (command.oldds == null)
562 command.oldds = new SequenceI[command.seqs.length];
564 command.oldds[i] = oldds;// todo not if !cutIsInternal?
566 // do we need to edit sequence features for new sequence ?
567 if (oldds != sequence.getDatasetSequence()
569 && sequence.getFeatures().hasFeatures()))
570 // todo or just test cutIsInternal && cutPositions != null ?
572 if (cutPositions != null)
574 cutFeatures(command, sequence, cutPositions.getBegin(),
575 cutPositions.getEnd(), cutIsInternal);
579 SequenceI newDs = sequence.getDatasetSequence();
580 if (newDs != ds && command.al != null
581 && command.al.getDataset() != null
582 && !command.al.getDataset().getSequences().contains(newDs))
584 command.al.getDataset().addSequence(newDs);
588 if (sequence.getLength() < 1)
590 command.al.deleteSequence(sequence);
595 adjustAnnotations(command, false, seqDeleted, views);
599 * Perform the given Paste command. This may be to add cut or copied sequences
600 * to an alignment, or to undo a 'Cut' action on a region of the alignment.
605 static void paste(Edit command, AlignmentI[] views)
607 boolean seqWasDeleted = false;
609 for (int i = 0; i < command.seqs.length; i++)
611 boolean newDSNeeded = false;
612 boolean newDSWasNeeded = command.oldds != null
613 && command.oldds[i] != null;
614 SequenceI sequence = command.seqs[i];
615 if (sequence.getLength() < 1)
618 * sequence was deleted; re-add it to the alignment
620 if (command.alIndex[i] < command.al.getHeight())
622 List<SequenceI> sequences;
623 synchronized (sequences = command.al.getSequences())
625 if (!(command.alIndex[i] < 0))
627 sequences.add(command.alIndex[i], sequence);
633 command.al.addSequence(sequence);
635 seqWasDeleted = true;
637 int newStart = sequence.getStart();
638 int newEnd = sequence.getEnd();
640 StringBuilder tmp = new StringBuilder();
641 tmp.append(sequence.getSequence());
642 // Undo of a delete does not replace original dataset sequence on to
643 // alignment sequence.
648 if (command.string != null && command.string[i] != null)
650 if (command.position >= tmp.length())
652 // This occurs if padding is on, and residues
653 // are removed from end of alignment
654 int len = command.position - tmp.length();
657 tmp.append(command.gapChar);
661 tmp.insert(command.position, command.string[i]);
662 for (int s = 0; s < command.string[i].length; s++)
664 if (!Comparison.isGap(command.string[i][s]))
670 start = sequence.findPosition(command.position);
672 // .findPosition(command.position + command.number);
674 if (sequence.getStart() == start)
684 command.string[i] = null;
687 sequence.setSequence(tmp.toString());
688 sequence.setStart(newStart);
689 sequence.setEnd(newEnd);
692 * command and Undo share the same dataset sequence if cut was
693 * at start or end of sequence
695 boolean sameDatasetSequence = false;
698 if (sequence.getDatasetSequence() != null)
703 ds = command.oldds[i];
707 // make a new DS sequence
708 // use new ds mechanism here
709 String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
710 sequence.getSequenceAsString());
711 ds = new Sequence(sequence.getName(), ungapped,
712 sequence.getStart(), sequence.getEnd());
713 ds.setDescription(sequence.getDescription());
715 if (command.oldds == null)
717 command.oldds = new SequenceI[command.seqs.length];
719 command.oldds[i] = sequence.getDatasetSequence();
720 sameDatasetSequence = ds == sequence.getDatasetSequence();
721 ds.setSequenceFeatures(sequence.getSequenceFeatures());
722 if (!sameDatasetSequence && command.al.getDataset() != null)
724 // delete 'undone' sequence from alignment dataset
725 command.al.getDataset()
726 .deleteSequence(sequence.getDatasetSequence());
728 sequence.setDatasetSequence(ds);
730 undoCutFeatures(command, command.seqs[i], start, length,
731 sameDatasetSequence);
734 adjustAnnotations(command, true, seqWasDeleted, views);
736 command.string = null;
739 static void replace(Edit command)
743 int start = command.position;
744 int end = command.number;
745 // TODO TUTORIAL - Fix for replacement with different length of sequence (or
747 // TODO Jalview 2.4 bugfix change to an aggregate command - original
748 // sequence string is cut, new string is pasted in.
749 command.number = start + command.string[0].length;
750 for (int i = 0; i < command.seqs.length; i++)
752 boolean newDSWasNeeded = command.oldds != null
753 && command.oldds[i] != null;
754 boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
757 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
758 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
759 * viewport.alignment));
763 * then addHistoryItem(new EditCommand( "Add sequences",
764 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
767 ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
769 ContiguousI afterEditedPositions = command.seqs[i]
770 .findPositions(end + 1, command.seqs[i].getLength());
772 oldstring = command.seqs[i].getSequenceAsString();
773 tmp = new StringBuilder(oldstring.substring(0, start));
774 tmp.append(command.string[i]);
775 String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
776 new String(command.string[i]));
777 if (end < oldstring.length())
779 tmp.append(oldstring.substring(end));
781 // stash end prior to updating the sequence object so we can save it if
783 Range oldstartend = new Range(command.seqs[i].getStart(),
784 command.seqs[i].getEnd());
785 command.seqs[i].setSequence(tmp.toString());
786 command.string[i] = oldstring
787 .substring(start, Math.min(end, oldstring.length()))
789 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
790 new String(command.string[i]));
792 if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
794 // we may already have dataset and limits stashed...
795 if (newDSWasNeeded || newStartEndWasNeeded)
799 // then just switch the dataset sequence
800 SequenceI oldds = command.seqs[i].getDatasetSequence();
801 command.seqs[i].setDatasetSequence(command.oldds[i]);
802 command.oldds[i] = oldds;
804 if (newStartEndWasNeeded)
806 Range newStart = command.oldStartEnd[i];
807 command.oldStartEnd[i] = oldstartend;
808 command.seqs[i].setStart(newStart.getBegin());
809 command.seqs[i].setEnd(newStart.getEnd());
814 // decide if we need a new dataset sequence or modify start/end
815 // first edit the original dataset sequence string
816 SequenceI oldds = command.seqs[i].getDatasetSequence();
817 String osp = oldds.getSequenceAsString();
818 int beforeStartOfEdit = -oldds.getStart() + 1
819 + (beforeEditedPositions == null
820 ? ((afterEditedPositions != null)
821 ? afterEditedPositions.getBegin() - 1
822 : oldstartend.getBegin()
824 : beforeEditedPositions.getEnd()
826 int afterEndOfEdit = -oldds.getStart() + 1
827 + ((afterEditedPositions == null)
828 ? oldstartend.getEnd()
829 : afterEditedPositions.getBegin() - 1);
830 String fullseq = osp.substring(0,
833 + osp.substring(afterEndOfEdit);
835 // and check if new sequence data is different..
836 if (!fullseq.equalsIgnoreCase(osp))
838 // old ds and edited ds are different, so
839 // create the new dataset sequence
840 SequenceI newds = new Sequence(oldds);
841 newds.setSequence(fullseq);
843 if (command.oldds == null)
845 command.oldds = new SequenceI[command.seqs.length];
847 command.oldds[i] = command.seqs[i].getDatasetSequence();
849 // And preserve start/end for good-measure
851 if (command.oldStartEnd == null)
853 command.oldStartEnd = new Range[command.seqs.length];
855 command.oldStartEnd[i] = oldstartend;
856 // TODO: JAL-1131 ensure newly created dataset sequence is added to
858 // dataset sequences associated with the alignment.
859 // TODO: JAL-1131 fix up any annotation associated with new dataset
860 // sequence to ensure that original sequence/annotation
863 command.seqs[i].setDatasetSequence(newds);
867 if (command.oldStartEnd == null)
869 command.oldStartEnd = new Range[command.seqs.length];
871 command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
872 command.seqs[i].getEnd());
873 if (beforeEditedPositions != null
874 && afterEditedPositions == null)
876 // modification at end
877 command.seqs[i].setEnd(
878 beforeEditedPositions.getEnd() + nogaprep.length()
879 - nogapold.length());
881 else if (afterEditedPositions != null
882 && beforeEditedPositions == null)
884 // modification at start
885 command.seqs[i].setStart(
886 afterEditedPositions.getBegin() - nogaprep.length());
890 // edit covered both start and end. Here we can only guess the
893 String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
894 command.seqs[i].getSequenceAsString().toUpperCase());
895 int newStart = command.seqs[i].getDatasetSequence()
896 .getSequenceAsString().indexOf(nogapalseq);
900 "Implementation Error: could not locate start/end "
901 + "in dataset sequence after an edit of the sequence string");
903 int newEnd = newStart + nogapalseq.length() - 1;
904 command.seqs[i].setStart(newStart);
905 command.seqs[i].setEnd(newEnd);
915 final static void adjustAnnotations(Edit command, boolean insert,
916 boolean modifyVisibility, AlignmentI[] views)
918 AlignmentAnnotation[] annotations = null;
920 if (modifyVisibility && !insert)
922 // only occurs if a sequence was added or deleted.
923 command.deletedAnnotationRows = new Hashtable<>();
925 if (command.fullAlignmentHeight)
927 annotations = command.al.getAlignmentAnnotation();
932 AlignmentAnnotation[] tmp;
933 for (int s = 0; s < command.seqs.length; s++)
935 command.seqs[s].sequenceChanged();
937 if (modifyVisibility)
939 // Rows are only removed or added to sequence object.
943 tmp = command.seqs[s].getAnnotation();
946 int alen = tmp.length;
947 for (int aa = 0; aa < tmp.length; aa++)
949 if (!command.al.deleteAnnotation(tmp[aa]))
951 // strip out annotation not in the current al (will be put
952 // back on insert in all views)
957 command.seqs[s].setAlignmentAnnotation(null);
958 if (alen != tmp.length)
960 // save the non-null annotation references only
961 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
962 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
966 saved[aapos++] = tmp[aa];
971 command.deletedAnnotationRows.put(command.seqs[s], saved);
972 // and then remove any annotation in the other views
973 for (int alview = 0; views != null
974 && alview < views.length; alview++)
976 if (views[alview] != command.al)
978 AlignmentAnnotation[] toremove = views[alview]
979 .getAlignmentAnnotation();
980 if (toremove == null || toremove.length == 0)
984 // remove any alignment annotation on this sequence that's
985 // on that alignment view.
986 for (int aa = 0; aa < toremove.length; aa++)
988 if (toremove[aa].sequenceRef == command.seqs[s])
990 views[alview].deleteAnnotation(toremove[aa]);
998 // save all the annotation
999 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1006 if (command.deletedAnnotationRows != null
1007 && command.deletedAnnotationRows
1008 .containsKey(command.seqs[s]))
1010 AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1011 .get(command.seqs[s]);
1012 command.seqs[s].setAlignmentAnnotation(revealed);
1013 if (revealed != null)
1015 for (int aa = 0; aa < revealed.length; aa++)
1017 // iterate through al adding original annotation
1018 command.al.addAnnotation(revealed[aa]);
1020 for (int aa = 0; aa < revealed.length; aa++)
1022 command.al.setAnnotationIndex(revealed[aa], aa);
1024 // and then duplicate added annotation on every other alignment
1026 for (int vnum = 0; views != null && vnum < views.length; vnum++)
1028 if (views[vnum] != command.al)
1030 int avwidth = views[vnum].getWidth() + 1;
1031 // duplicate in this view
1032 for (int a = 0; a < revealed.length; a++)
1034 AlignmentAnnotation newann = new AlignmentAnnotation(
1036 command.seqs[s].addAlignmentAnnotation(newann);
1037 newann.padAnnotation(avwidth);
1038 views[vnum].addAnnotation(newann);
1039 views[vnum].setAnnotationIndex(newann, a);
1049 if (command.seqs[s].getAnnotation() == null)
1056 annotations = command.seqs[s].getAnnotation();
1060 tmp = new AlignmentAnnotation[aSize
1061 + command.seqs[s].getAnnotation().length];
1063 System.arraycopy(annotations, 0, tmp, 0, aSize);
1065 System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1066 command.seqs[s].getAnnotation().length);
1070 aSize = annotations.length;
1074 if (annotations == null)
1081 command.deletedAnnotations = new Hashtable<>();
1086 for (int a = 0; a < annotations.length; a++)
1088 if (annotations[a].autoCalculated
1089 || annotations[a].annotations == null)
1096 aSize = annotations[a].annotations.length;
1099 temp = new Annotation[aSize + command.number];
1100 if (annotations[a].padGaps)
1102 for (int aa = 0; aa < temp.length; aa++)
1104 temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1110 if (command.position < aSize)
1112 if (command.position + command.number >= aSize)
1118 tSize = aSize - command.number;
1130 temp = new Annotation[tSize];
1135 if (command.position < annotations[a].annotations.length)
1137 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1140 if (command.deletedAnnotations != null
1141 && command.deletedAnnotations
1142 .containsKey(annotations[a].annotationId))
1144 Annotation[] restore = command.deletedAnnotations
1145 .get(annotations[a].annotationId);
1147 System.arraycopy(restore, 0, temp, command.position,
1152 System.arraycopy(annotations[a].annotations, command.position,
1153 temp, command.position + command.number,
1154 aSize - command.position);
1158 if (command.deletedAnnotations != null
1159 && command.deletedAnnotations
1160 .containsKey(annotations[a].annotationId))
1162 Annotation[] restore = command.deletedAnnotations
1163 .get(annotations[a].annotationId);
1165 temp = new Annotation[annotations[a].annotations.length
1167 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1168 annotations[a].annotations.length);
1169 System.arraycopy(restore, 0, temp,
1170 annotations[a].annotations.length, restore.length);
1174 temp = annotations[a].annotations;
1180 if (tSize != aSize || command.position < 2)
1182 int copylen = Math.min(command.position,
1183 annotations[a].annotations.length);
1186 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1187 copylen); // command.position);
1190 Annotation[] deleted = new Annotation[command.number];
1191 if (copylen >= command.position)
1193 copylen = Math.min(command.number,
1194 annotations[a].annotations.length - command.position);
1197 System.arraycopy(annotations[a].annotations, command.position,
1198 deleted, 0, copylen); // command.number);
1202 command.deletedAnnotations.put(annotations[a].annotationId,
1204 if (annotations[a].annotations.length > command.position
1207 System.arraycopy(annotations[a].annotations,
1208 command.position + command.number, temp,
1209 command.position, annotations[a].annotations.length
1210 - command.position - command.number); // aSize
1215 int dSize = aSize - command.position;
1219 Annotation[] deleted = new Annotation[command.number];
1220 System.arraycopy(annotations[a].annotations, command.position,
1223 command.deletedAnnotations.put(annotations[a].annotationId,
1226 tSize = Math.min(annotations[a].annotations.length,
1228 temp = new Annotation[tSize];
1229 System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1233 temp = annotations[a].annotations;
1238 annotations[a].annotations = temp;
1243 * Restores features to the state before a Cut.
1245 * <li>re-add any features deleted by the cut</li>
1246 * <li>remove any truncated features created by the cut</li>
1247 * <li>shift right any features to the right of the cut</li>
1253 * the sequence the Cut applied to
1255 * the start residue position of the cut
1257 * the number of residues cut
1258 * @param sameDatasetSequence
1259 * true if dataset sequence and frame of reference were left
1260 * unchanged by the Cut
1262 final static void undoCutFeatures(Edit command, SequenceI seq,
1263 final int start, final int length, boolean sameDatasetSequence)
1265 SequenceI sequence = seq.getDatasetSequence();
1266 if (sequence == null)
1272 * shift right features that lie to the right of the restored cut (but not
1273 * if dataset sequence unchanged - so coordinates were changed by Cut)
1275 if (!sameDatasetSequence)
1278 * shift right all features right of and not
1279 * contiguous with the cut position
1281 seq.getFeatures().shiftFeatures(start + 1, length);
1284 * shift right any features that start at the cut position,
1285 * unless they were truncated
1287 List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1289 for (SequenceFeature sf : sfs)
1291 if (sf.getBegin() == start)
1293 if (!command.truncatedFeatures.containsKey(seq)
1294 || !command.truncatedFeatures.get(seq).contains(sf))
1297 * feature was shifted left to cut position (not truncated),
1298 * so shift it back right
1300 SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
1301 + length, sf.getEnd() + length, sf.getFeatureGroup(),
1303 seq.addSequenceFeature(shifted);
1304 seq.deleteFeature(sf);
1311 * restore any features that were deleted or truncated
1313 if (command.deletedFeatures != null
1314 && command.deletedFeatures.containsKey(seq))
1316 for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1318 sequence.addSequenceFeature(deleted);
1323 * delete any truncated features
1325 if (command.truncatedFeatures != null
1326 && command.truncatedFeatures.containsKey(seq))
1328 for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1330 sequence.deleteFeature(amended);
1336 * Returns the list of edit commands wrapped by this object.
1340 public List<Edit> getEdits()
1346 * Returns a map whose keys are the dataset sequences, and values their
1347 * aligned sequences before the command edit list was applied. The aligned
1348 * sequences are copies, which may be updated without affecting the originals.
1350 * The command holds references to the aligned sequences (after editing). If
1351 * the command is an 'undo',then the prior state is simply the aligned state.
1352 * Otherwise, we have to derive the prior state by working backwards through
1353 * the edit list to infer the aligned sequences before editing.
1355 * Note: an alternative solution would be to cache the 'before' state of each
1356 * edit, but this would be expensive in space in the common case that the
1357 * original is never needed (edits are not mirrored).
1360 * @throws IllegalStateException
1361 * on detecting an edit command of a type that can't be unwound
1363 public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1365 Map<SequenceI, SequenceI> result = new HashMap<>();
1366 if (getEdits() == null)
1372 for (Edit e : getEdits())
1374 for (SequenceI seq : e.getSequences())
1376 SequenceI ds = seq.getDatasetSequence();
1377 // SequenceI preEdit = result.get(ds);
1378 if (!result.containsKey(ds))
1381 * copy sequence including start/end (but don't use copy constructor
1382 * as we don't need annotations)
1384 SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1385 seq.getStart(), seq.getEnd());
1386 preEdit.setDatasetSequence(ds);
1387 result.put(ds, preEdit);
1395 * Work backwards through the edit list, deriving the sequences before each
1396 * was applied. The final result is the sequence set before any edits.
1398 Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
1399 while (editList.hasNext())
1401 Edit oldEdit = editList.next();
1402 Action action = oldEdit.getAction();
1403 int position = oldEdit.getPosition();
1404 int number = oldEdit.getNumber();
1405 final char gap = oldEdit.getGapCharacter();
1406 for (SequenceI seq : oldEdit.getSequences())
1408 SequenceI ds = seq.getDatasetSequence();
1409 SequenceI preEdit = result.get(ds);
1410 if (preEdit == null)
1412 preEdit = new Sequence("", seq.getSequenceAsString(),
1413 seq.getStart(), seq.getEnd());
1414 preEdit.setDatasetSequence(ds);
1415 result.put(ds, preEdit);
1418 * 'Undo' this edit action on the sequence (updating the value in the
1423 if (action == Action.DELETE_GAP)
1425 preEdit.setSequence(new String(StringUtils.insertCharAt(
1426 preEdit.getSequence(), position, number, gap)));
1428 else if (action == Action.INSERT_GAP)
1430 preEdit.setSequence(new String(StringUtils.deleteChars(
1431 preEdit.getSequence(), position, position + number)));
1435 System.err.println("Can't undo edit action " + action);
1436 // throw new IllegalStateException("Can't undo edit action " +
1447 private SequenceI[] oldds;
1450 * start and end of sequence prior to edit
1452 private Range[] oldStartEnd;
1454 private boolean fullAlignmentHeight = false;
1456 private Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1458 private Map<String, Annotation[]> deletedAnnotations;
1461 * features deleted by the cut (re-add on Undo)
1462 * (including the original of any shortened features)
1464 private Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1467 * shortened features added by the cut (delete on Undo)
1469 private Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1471 private AlignmentI al;
1473 final private Action command;
1479 private int[] alIndex;
1481 private int position;
1485 private char gapChar;
1488 * flag that identifies edits inserted to balance
1489 * user edits in a 'locked editing' region
1491 private boolean systemGenerated;
1493 public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1498 this.position = pos;
1499 this.number = count;
1503 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1506 this(cmd, sqs, pos, count, align.getGapCharacter());
1510 alIndex = new int[sqs.length];
1511 for (int i = 0; i < sqs.length; i++)
1513 alIndex[i] = align.findIndex(sqs[i]);
1516 fullAlignmentHeight = (align.getHeight() == sqs.length);
1520 * Constructor given a REPLACE command and the replacement string
1529 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1530 AlignmentI align, String replace)
1532 this(cmd, sqs, pos, count, align);
1534 string = new char[sqs.length][];
1535 for (int i = 0; i < sqs.length; i++)
1537 string[i] = replace.toCharArray();
1541 public SequenceI[] getSequences()
1546 public int getPosition()
1551 public Action getAction()
1556 public int getNumber()
1561 public char getGapCharacter()
1566 public void setSystemGenerated(boolean b)
1568 systemGenerated = b;
1571 public boolean isSystemGenerated()
1573 return systemGenerated;
1578 * Returns an iterator over the list of edit commands which traverses the list
1579 * either forwards or backwards.
1584 public Iterator<Edit> getEditIterator(boolean forwards)
1588 return getEdits().iterator();
1592 return new ReverseListIterator<>(getEdits());
1597 * Adjusts features for Cut, and saves details of changes made to allow Undo
1599 * <li>features left of the cut are unchanged</li>
1600 * <li>features right of the cut are shifted left</li>
1601 * <li>features internal to the cut region are deleted</li>
1602 * <li>features that overlap or span the cut are shortened</li>
1603 * <li>the originals of any deleted or shortened features are saved, to re-add
1605 * <li>any added (shortened) features are saved, to delete on Undo</li>
1610 * @param fromPosition
1612 * @param cutIsInternal
1614 protected static void cutFeatures(Edit command, SequenceI seq,
1615 int fromPosition, int toPosition, boolean cutIsInternal)
1618 * if the cut is at start or end of sequence
1619 * then we don't modify the sequence feature store
1625 List<SequenceFeature> added = new ArrayList<>();
1626 List<SequenceFeature> removed = new ArrayList<>();
1628 SequenceFeaturesI featureStore = seq.getFeatures();
1629 if (toPosition < fromPosition || featureStore == null)
1634 int cutStartPos = fromPosition;
1635 int cutEndPos = toPosition;
1636 int cutWidth = cutEndPos - cutStartPos + 1;
1638 synchronized (featureStore)
1641 * get features that overlap the cut region
1643 List<SequenceFeature> toAmend = featureStore.findFeatures(
1644 cutStartPos, cutEndPos);
1647 * add any contact features that span the cut region
1648 * (not returned by findFeatures)
1650 for (SequenceFeature contact : featureStore.getContactFeatures())
1652 if (contact.getBegin() < cutStartPos
1653 && contact.getEnd() > cutEndPos)
1655 toAmend.add(contact);
1660 * adjust start-end of overlapping features;
1661 * delete features enclosed by the cut;
1662 * delete partially overlapping contact features
1664 for (SequenceFeature sf : toAmend)
1666 int sfBegin = sf.getBegin();
1667 int sfEnd = sf.getEnd();
1668 int newBegin = sfBegin;
1670 boolean toDelete = false;
1671 boolean follows = false;
1673 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1676 * feature lies within cut region - delete it
1680 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1683 * feature spans cut region - left-shift the end
1687 else if (sfEnd <= cutEndPos)
1690 * feature overlaps left of cut region - truncate right
1692 newEnd = cutStartPos - 1;
1693 if (sf.isContactFeature())
1698 else if (sfBegin >= cutStartPos)
1701 * remaining case - feature overlaps right
1702 * truncate left, adjust end of feature
1704 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1705 newEnd = newBegin + sfEnd - cutEndPos - 1;
1706 if (sf.isContactFeature())
1712 seq.deleteFeature(sf);
1719 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1720 sf.getFeatureGroup(), sf.getScore());
1721 seq.addSequenceFeature(copy);
1730 * and left shift any features lying to the right of the cut region
1733 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1737 * save deleted and amended features, so that Undo can
1738 * re-add or delete them respectively
1740 if (command.deletedFeatures == null)
1742 command.deletedFeatures = new HashMap<>();
1744 if (command.truncatedFeatures == null)
1746 command.truncatedFeatures = new HashMap<>();
1748 command.deletedFeatures.put(seq, removed);
1749 command.truncatedFeatures.put(seq, added);