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 java.util.Locale;
25 import jalview.analysis.AlignSeq;
26 import jalview.datamodel.AlignmentAnnotation;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.Annotation;
29 import jalview.datamodel.ContiguousI;
30 import jalview.datamodel.Range;
31 import jalview.datamodel.Sequence;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceI;
34 import jalview.datamodel.features.SequenceFeaturesI;
35 import jalview.util.Comparison;
36 import jalview.util.ReverseListIterator;
37 import jalview.util.StringUtils;
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.Hashtable;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.ListIterator;
54 * Description: Essential information for performing undo and redo for cut/paste
55 * insert/delete gap which can be stored in the HistoryList
59 * Copyright: Copyright (c) 2006
63 * Company: Dundee University
66 * @author not attributable
69 public class EditCommand implements CommandI
76 public Action getUndoAction()
84 public Action getUndoAction()
92 public Action getUndoAction()
100 public Action getUndoAction()
108 public Action getUndoAction()
116 public Action getUndoAction()
122 public abstract Action getUndoAction();
125 private List<Edit> edits = new ArrayList<>();
133 public EditCommand(String desc)
135 this.description = desc;
138 public EditCommand(String desc, Action command, SequenceI[] seqs,
139 int position, int number, AlignmentI al)
141 this.description = desc;
142 if (command == Action.CUT || command == Action.PASTE)
144 setEdit(new Edit(command, seqs, position, number, al));
147 performEdit(0, null);
150 public EditCommand(String desc, Action command, String replace,
151 SequenceI[] seqs, int position, int number, AlignmentI al)
153 this.description = desc;
154 if (command == Action.REPLACE)
156 setEdit(new Edit(command, seqs, position, number, al, replace));
159 performEdit(0, null);
163 * Set the list of edits to the specified item (only).
167 protected void setEdit(Edit e)
174 * Add the given edit command to the stored list of commands. If simply
175 * expanding the range of the last command added, then modify it instead of
176 * adding a new command.
180 public void addEdit(Edit e)
182 if (!expandEdit(edits, e))
189 * Returns true if the new edit is incorporated by updating (expanding the
190 * range of) the last edit on the list, else false. We can 'expand' the last
191 * edit if the new one is the same action, on the same sequences, and acts on
192 * a contiguous range. This is the case where a mouse drag generates a series
193 * of contiguous gap insertions or deletions.
199 protected static boolean expandEdit(List<Edit> edits, Edit e)
201 if (edits == null || edits.isEmpty())
205 Edit lastEdit = edits.get(edits.size() - 1);
206 Action action = e.command;
207 if (lastEdit.command != action)
213 * Both commands must act on the same sequences - compare the underlying
214 * dataset sequences, rather than the aligned sequences, which change as
217 if (lastEdit.seqs.length != e.seqs.length)
221 for (int i = 0; i < e.seqs.length; i++)
223 if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
224 .getDatasetSequence())
231 * Check a contiguous edit; either
233 * <li>a new Insert <n> positions to the right of the last <insert n>,
235 * <li>a new Delete <n> gaps which is <n> positions to the left of the last
239 boolean contiguous = (action == Action.INSERT_GAP
240 && e.position == lastEdit.position + lastEdit.number)
241 || (action == Action.DELETE_GAP
242 && e.position + e.number == lastEdit.position);
246 * We are just expanding the range of the last edit. For delete gap, also
247 * moving the start position left.
249 lastEdit.number += e.number;
250 lastEdit.seqs = e.seqs;
251 if (action == Action.DELETE_GAP)
261 * Clear the list of stored edit commands.
264 protected void clearEdits()
270 * Returns the i'th stored Edit command.
275 protected Edit getEdit(int i)
277 if (i >= 0 && i < edits.size())
285 final public String getDescription()
297 * Return the alignment for the first edit (or null if no edit).
301 final public AlignmentI getAlignment()
303 return (edits.isEmpty() ? null : edits.get(0).al);
307 * append a new editCommand Note. this shouldn't be called if the edit is an
308 * operation affects more alignment objects than the one referenced in al (for
309 * example, cut or pasting whole sequences). Use the form with an additional
310 * AlignmentI[] views parameter.
319 final public void appendEdit(Action command, SequenceI[] seqs,
320 int position, int number, AlignmentI al, boolean performEdit)
322 appendEdit(command, seqs, position, number, al, performEdit, null);
326 * append a new edit command with a set of alignment views that may be
337 final public void appendEdit(Action command, SequenceI[] seqs,
338 int position, int number, AlignmentI al, boolean performEdit,
341 Edit edit = new Edit(command, seqs, position, number, al);
342 appendEdit(edit, al, performEdit, views);
346 * Overloaded method that accepts an Edit object with additional parameters.
353 final public void appendEdit(Edit edit, AlignmentI al,
354 boolean performEdit, AlignmentI[] views)
356 if (al.getHeight() == edit.seqs.length)
359 edit.fullAlignmentHeight = true;
366 performEdit(edit, views);
371 * Execute all the edit commands, starting at the given commandIndex
373 * @param commandIndex
376 public final void performEdit(int commandIndex, AlignmentI[] views)
378 ListIterator<Edit> iterator = edits.listIterator(commandIndex);
379 while (iterator.hasNext())
381 Edit edit = iterator.next();
382 performEdit(edit, views);
387 * Execute one edit command in all the specified alignment views
392 protected static void performEdit(Edit edit, AlignmentI[] views)
394 switch (edit.command)
412 // TODO:add deleteNuc for UNDO
414 // insertNuc(edits[e]);
422 final public void doCommand(AlignmentI[] views)
424 performEdit(0, views);
428 * Undo the stored list of commands, in reverse order.
431 final public void undoCommand(AlignmentI[] views)
433 ListIterator<Edit> iterator = edits.listIterator(edits.size());
434 while (iterator.hasPrevious())
436 Edit e = iterator.previous();
464 * Insert gap(s) in sequences as specified by the command, and adjust
469 final private static void insertGap(Edit command)
472 for (int s = 0; s < command.seqs.length; s++)
474 command.seqs[s].insertCharAt(command.position, command.number,
476 // System.out.println("pos: "+command.position+" number:
477 // "+command.number);
480 adjustAnnotations(command, true, false, null);
484 // final void insertNuc(Edit command)
487 // for (int s = 0; s < command.seqs.length; s++)
489 // System.out.println("pos: "+command.position+" number: "+command.number);
490 // command.seqs[s].insertCharAt(command.position, command.number,'A');
493 // adjustAnnotations(command, true, false, null);
497 * Delete gap(s) in sequences as specified by the command, and adjust
502 final static private void deleteGap(Edit command)
504 for (int s = 0; s < command.seqs.length; s++)
506 command.seqs[s].deleteChars(command.position,
507 command.position + command.number);
510 adjustAnnotations(command, false, false, null);
514 * Carry out a Cut action. The cut characters are saved in case Undo is
520 static void cut(Edit command, AlignmentI[] views)
522 boolean seqDeleted = false;
523 command.string = new char[command.seqs.length][];
525 for (int i = 0; i < command.seqs.length; i++)
527 final SequenceI sequence = command.seqs[i];
528 if (sequence.getLength() > command.position)
530 command.string[i] = sequence.getSequence(command.position,
531 command.position + command.number);
532 SequenceI oldds = sequence.getDatasetSequence();
533 ContiguousI cutPositions = sequence.findPositions(
534 command.position + 1, command.position + command.number);
535 boolean cutIsInternal = cutPositions != null
536 && sequence.getStart() != cutPositions.getBegin()
537 && sequence.getEnd() != cutPositions.getEnd();
540 * perform the cut; if this results in a new dataset sequence, add
541 * that to the alignment dataset
543 SequenceI ds = sequence.getDatasetSequence();
544 sequence.deleteChars(command.position,
545 command.position + command.number);
547 if (command.oldds != null && command.oldds[i] != null)
550 * we are Redoing a Cut, or Undoing a Paste - so
551 * oldds entry contains the cut dataset sequence,
552 * with sequence features in expected place
554 sequence.setDatasetSequence(command.oldds[i]);
555 command.oldds[i] = oldds;
560 * new cut operation: save the dataset sequence
561 * so it can be restored in an Undo
563 if (command.oldds == null)
565 command.oldds = new SequenceI[command.seqs.length];
567 command.oldds[i] = oldds;// todo not if !cutIsInternal?
569 // do we need to edit sequence features for new sequence ?
570 if (oldds != sequence.getDatasetSequence() || (cutIsInternal
571 && sequence.getFeatures().hasFeatures()))
572 // todo or just test cutIsInternal && cutPositions != null ?
574 if (cutPositions != null)
576 cutFeatures(command, sequence, cutPositions.getBegin(),
577 cutPositions.getEnd(), cutIsInternal);
581 SequenceI newDs = sequence.getDatasetSequence();
582 if (newDs != ds && command.al != null
583 && command.al.getDataset() != null
584 && !command.al.getDataset().getSequences().contains(newDs))
586 command.al.getDataset().addSequence(newDs);
590 if (sequence.getLength() < 1)
592 command.al.deleteSequence(sequence);
597 adjustAnnotations(command, false, seqDeleted, views);
601 * Perform the given Paste command. This may be to add cut or copied sequences
602 * to an alignment, or to undo a 'Cut' action on a region of the alignment.
607 static void paste(Edit command, AlignmentI[] views)
609 boolean seqWasDeleted = false;
611 for (int i = 0; i < command.seqs.length; i++)
613 boolean newDSNeeded = false;
614 boolean newDSWasNeeded = command.oldds != null
615 && command.oldds[i] != null;
616 SequenceI sequence = command.seqs[i];
617 if (sequence.getLength() < 1)
620 * sequence was deleted; re-add it to the alignment
622 if (command.alIndex[i] < command.al.getHeight())
624 List<SequenceI> sequences = command.al.getSequences();
625 synchronized (sequences)
627 if (!(command.alIndex[i] < 0))
629 sequences.add(command.alIndex[i], sequence);
635 command.al.addSequence(sequence);
637 seqWasDeleted = true;
639 int newStart = sequence.getStart();
640 int newEnd = sequence.getEnd();
642 StringBuilder tmp = new StringBuilder();
643 tmp.append(sequence.getSequence());
644 // Undo of a delete does not replace original dataset sequence on to
645 // alignment sequence.
650 if (command.string != null && command.string[i] != null)
652 if (command.position >= tmp.length())
654 // This occurs if padding is on, and residues
655 // are removed from end of alignment
656 int len = command.position - tmp.length();
659 tmp.append(command.gapChar);
663 tmp.insert(command.position, command.string[i]);
664 for (int s = 0; s < command.string[i].length; s++)
666 if (!Comparison.isGap(command.string[i][s]))
672 start = sequence.findPosition(command.position);
674 // .findPosition(command.position + command.number);
676 if (sequence.getStart() == start)
686 command.string[i] = null;
689 sequence.setSequence(tmp.toString());
690 sequence.setStart(newStart);
691 sequence.setEnd(newEnd);
694 * command and Undo share the same dataset sequence if cut was
695 * at start or end of sequence
697 boolean sameDatasetSequence = false;
700 if (sequence.getDatasetSequence() != null)
705 ds = command.oldds[i];
709 // make a new DS sequence
710 // use new ds mechanism here
711 String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
712 sequence.getSequenceAsString());
713 ds = new Sequence(sequence.getName(), ungapped,
714 sequence.getStart(), sequence.getEnd());
715 ds.setDescription(sequence.getDescription());
717 if (command.oldds == null)
719 command.oldds = new SequenceI[command.seqs.length];
721 command.oldds[i] = sequence.getDatasetSequence();
722 sameDatasetSequence = ds == sequence.getDatasetSequence();
723 ds.setSequenceFeatures(sequence.getSequenceFeatures());
724 if (!sameDatasetSequence && command.al.getDataset() != null)
726 // delete 'undone' sequence from alignment dataset
727 command.al.getDataset()
728 .deleteSequence(sequence.getDatasetSequence());
730 sequence.setDatasetSequence(ds);
732 undoCutFeatures(command, command.seqs[i], start, length,
733 sameDatasetSequence);
736 adjustAnnotations(command, true, seqWasDeleted, views);
738 command.string = null;
741 static void replace(Edit command)
745 int start = command.position;
746 int end = command.number;
747 // TODO TUTORIAL - Fix for replacement with different length of sequence (or
749 // TODO Jalview 2.4 bugfix change to an aggregate command - original
750 // sequence string is cut, new string is pasted in.
751 command.number = start + command.string[0].length;
752 for (int i = 0; i < command.seqs.length; i++)
754 boolean newDSWasNeeded = command.oldds != null
755 && command.oldds[i] != null;
756 boolean newStartEndWasNeeded = command.oldStartEnd != null
757 && command.oldStartEnd[i] != null;
760 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
761 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
762 * viewport.alignment));
766 * then addHistoryItem(new EditCommand( "Add sequences",
767 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
770 ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
772 ContiguousI afterEditedPositions = command.seqs[i]
773 .findPositions(end + 1, command.seqs[i].getLength());
775 oldstring = command.seqs[i].getSequenceAsString();
776 tmp = new StringBuilder(oldstring.substring(0, start));
777 tmp.append(command.string[i]);
778 String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
779 new String(command.string[i]));
780 if (end < oldstring.length())
782 tmp.append(oldstring.substring(end));
784 // stash end prior to updating the sequence object so we can save it if
786 Range oldstartend = new Range(command.seqs[i].getStart(),
787 command.seqs[i].getEnd());
788 command.seqs[i].setSequence(tmp.toString());
789 command.string[i] = oldstring
790 .substring(start, Math.min(end, oldstring.length()))
792 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
793 new String(command.string[i]));
795 if (!nogaprep.toLowerCase(Locale.ROOT)
796 .equals(nogapold.toLowerCase(Locale.ROOT)))
798 // we may already have dataset and limits stashed...
799 if (newDSWasNeeded || newStartEndWasNeeded)
803 // then just switch the dataset sequence
804 SequenceI oldds = command.seqs[i].getDatasetSequence();
805 command.seqs[i].setDatasetSequence(command.oldds[i]);
806 command.oldds[i] = oldds;
808 if (newStartEndWasNeeded)
810 Range newStart = command.oldStartEnd[i];
811 command.oldStartEnd[i] = oldstartend;
812 command.seqs[i].setStart(newStart.getBegin());
813 command.seqs[i].setEnd(newStart.getEnd());
818 // decide if we need a new dataset sequence or modify start/end
819 // first edit the original dataset sequence string
820 SequenceI oldds = command.seqs[i].getDatasetSequence();
821 String osp = oldds.getSequenceAsString();
822 int beforeStartOfEdit = -oldds.getStart() + 1
823 + (beforeEditedPositions == null
824 ? ((afterEditedPositions != null)
825 ? afterEditedPositions.getBegin() - 1
826 : oldstartend.getBegin()
828 : beforeEditedPositions.getEnd());
829 int afterEndOfEdit = -oldds.getStart() + 1
830 + ((afterEditedPositions == null) ? oldstartend.getEnd()
831 : afterEditedPositions.getBegin() - 1);
832 String fullseq = osp.substring(0, beforeStartOfEdit) + nogaprep
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.toUpperCase(Locale.ROOT));
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(beforeEditedPositions.getEnd()
878 + nogaprep.length() - nogapold.length());
880 else if (afterEditedPositions != null
881 && beforeEditedPositions == null)
883 // modification at start
884 command.seqs[i].setStart(
885 afterEditedPositions.getBegin() - nogaprep.length());
889 // edit covered both start and end. Here we can only guess the
892 String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
893 command.seqs[i].getSequenceAsString()
894 .toUpperCase(Locale.ROOT));
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
1027 && vnum < views.length; vnum++)
1029 if (views[vnum] != command.al)
1031 int avwidth = views[vnum].getWidth() + 1;
1032 // duplicate in this view
1033 for (int a = 0; a < revealed.length; a++)
1035 AlignmentAnnotation newann = new AlignmentAnnotation(
1037 command.seqs[s].addAlignmentAnnotation(newann);
1038 newann.padAnnotation(avwidth);
1039 views[vnum].addAnnotation(newann);
1040 views[vnum].setAnnotationIndex(newann, a);
1050 if (command.seqs[s].getAnnotation() == null)
1057 annotations = command.seqs[s].getAnnotation();
1061 tmp = new AlignmentAnnotation[aSize
1062 + command.seqs[s].getAnnotation().length];
1064 System.arraycopy(annotations, 0, tmp, 0, aSize);
1066 System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1067 command.seqs[s].getAnnotation().length);
1071 aSize = annotations.length;
1075 if (annotations == null)
1082 command.deletedAnnotations = new Hashtable<>();
1087 for (int a = 0; a < annotations.length; a++)
1089 if (annotations[a].autoCalculated
1090 || annotations[a].annotations == null)
1097 aSize = annotations[a].annotations.length;
1100 temp = new Annotation[aSize + command.number];
1101 if (annotations[a].padGaps)
1103 for (int aa = 0; aa < temp.length; aa++)
1105 temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1111 if (command.position < aSize)
1113 if (command.position + command.number >= aSize)
1119 tSize = aSize - command.number;
1131 temp = new Annotation[tSize];
1136 if (command.position < annotations[a].annotations.length)
1138 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1141 if (command.deletedAnnotations != null
1142 && command.deletedAnnotations
1143 .containsKey(annotations[a].annotationId))
1145 Annotation[] restore = command.deletedAnnotations
1146 .get(annotations[a].annotationId);
1148 System.arraycopy(restore, 0, temp, command.position,
1153 System.arraycopy(annotations[a].annotations, command.position,
1154 temp, command.position + command.number,
1155 aSize - command.position);
1159 if (command.deletedAnnotations != null
1160 && command.deletedAnnotations
1161 .containsKey(annotations[a].annotationId))
1163 Annotation[] restore = command.deletedAnnotations
1164 .get(annotations[a].annotationId);
1166 temp = new Annotation[annotations[a].annotations.length
1168 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1169 annotations[a].annotations.length);
1170 System.arraycopy(restore, 0, temp,
1171 annotations[a].annotations.length, restore.length);
1175 temp = annotations[a].annotations;
1181 if (tSize != aSize || command.position < 2)
1183 int copylen = Math.min(command.position,
1184 annotations[a].annotations.length);
1187 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1188 copylen); // command.position);
1191 Annotation[] deleted = new Annotation[command.number];
1192 if (copylen >= command.position)
1194 copylen = Math.min(command.number,
1195 annotations[a].annotations.length - command.position);
1198 System.arraycopy(annotations[a].annotations, command.position,
1199 deleted, 0, copylen); // command.number);
1203 command.deletedAnnotations.put(annotations[a].annotationId,
1205 if (annotations[a].annotations.length > command.position
1208 System.arraycopy(annotations[a].annotations,
1209 command.position + command.number, temp,
1210 command.position, annotations[a].annotations.length
1211 - command.position - command.number); // aSize
1216 int dSize = aSize - command.position;
1220 Annotation[] deleted = new Annotation[command.number];
1221 System.arraycopy(annotations[a].annotations, command.position,
1224 command.deletedAnnotations.put(annotations[a].annotationId,
1227 tSize = Math.min(annotations[a].annotations.length,
1229 temp = new Annotation[tSize];
1230 System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1234 temp = annotations[a].annotations;
1239 annotations[a].annotations = temp;
1244 * Restores features to the state before a Cut.
1246 * <li>re-add any features deleted by the cut</li>
1247 * <li>remove any truncated features created by the cut</li>
1248 * <li>shift right any features to the right of the cut</li>
1254 * the sequence the Cut applied to
1256 * the start residue position of the cut
1258 * the number of residues cut
1259 * @param sameDatasetSequence
1260 * true if dataset sequence and frame of reference were left
1261 * unchanged by the Cut
1263 final static void undoCutFeatures(Edit command, SequenceI seq,
1264 final int start, final int length, boolean sameDatasetSequence)
1266 SequenceI sequence = seq.getDatasetSequence();
1267 if (sequence == null)
1273 * shift right features that lie to the right of the restored cut (but not
1274 * if dataset sequence unchanged - so coordinates were changed by Cut)
1276 if (!sameDatasetSequence)
1279 * shift right all features right of and not
1280 * contiguous with the cut position
1282 seq.getFeatures().shiftFeatures(start + 1, length);
1285 * shift right any features that start at the cut position,
1286 * unless they were truncated
1288 List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1290 for (SequenceFeature sf : sfs)
1292 if (sf.getBegin() == start)
1294 if (!command.truncatedFeatures.containsKey(seq)
1295 || !command.truncatedFeatures.get(seq).contains(sf))
1298 * feature was shifted left to cut position (not truncated),
1299 * so shift it back right
1301 SequenceFeature shifted = new SequenceFeature(sf,
1302 sf.getBegin() + length, sf.getEnd() + length,
1303 sf.getFeatureGroup(), sf.getScore());
1304 seq.addSequenceFeature(shifted);
1305 seq.deleteFeature(sf);
1312 * restore any features that were deleted or truncated
1314 if (command.deletedFeatures != null
1315 && command.deletedFeatures.containsKey(seq))
1317 for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1319 sequence.addSequenceFeature(deleted);
1324 * delete any truncated features
1326 if (command.truncatedFeatures != null
1327 && command.truncatedFeatures.containsKey(seq))
1329 for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1331 sequence.deleteFeature(amended);
1337 * Returns the list of edit commands wrapped by this object.
1341 public List<Edit> getEdits()
1347 * Returns a map whose keys are the dataset sequences, and values their
1348 * aligned sequences before the command edit list was applied. The aligned
1349 * sequences are copies, which may be updated without affecting the originals.
1351 * The command holds references to the aligned sequences (after editing). If
1352 * the command is an 'undo',then the prior state is simply the aligned state.
1353 * Otherwise, we have to derive the prior state by working backwards through
1354 * the edit list to infer the aligned sequences before editing.
1356 * Note: an alternative solution would be to cache the 'before' state of each
1357 * edit, but this would be expensive in space in the common case that the
1358 * original is never needed (edits are not mirrored).
1361 * @throws IllegalStateException
1362 * on detecting an edit command of a type that can't be unwound
1364 public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1366 Map<SequenceI, SequenceI> result = new HashMap<>();
1367 if (getEdits() == null)
1373 for (Edit e : getEdits())
1375 for (SequenceI seq : e.getSequences())
1377 SequenceI ds = seq.getDatasetSequence();
1378 // SequenceI preEdit = result.get(ds);
1379 if (!result.containsKey(ds))
1382 * copy sequence including start/end (but don't use copy constructor
1383 * as we don't need annotations)
1385 SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1386 seq.getStart(), seq.getEnd());
1387 preEdit.setDatasetSequence(ds);
1388 result.put(ds, preEdit);
1396 * Work backwards through the edit list, deriving the sequences before each
1397 * was applied. The final result is the sequence set before any edits.
1399 Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
1400 while (editList.hasNext())
1402 Edit oldEdit = editList.next();
1403 Action action = oldEdit.getAction();
1404 int position = oldEdit.getPosition();
1405 int number = oldEdit.getNumber();
1406 final char gap = oldEdit.getGapCharacter();
1407 for (SequenceI seq : oldEdit.getSequences())
1409 SequenceI ds = seq.getDatasetSequence();
1410 SequenceI preEdit = result.get(ds);
1411 if (preEdit == null)
1413 preEdit = new Sequence("", seq.getSequenceAsString(),
1414 seq.getStart(), seq.getEnd());
1415 preEdit.setDatasetSequence(ds);
1416 result.put(ds, preEdit);
1419 * 'Undo' this edit action on the sequence (updating the value in the
1424 if (action == Action.DELETE_GAP)
1426 preEdit.setSequence(new String(StringUtils.insertCharAt(
1427 preEdit.getSequence(), position, number, gap)));
1429 else if (action == Action.INSERT_GAP)
1431 preEdit.setSequence(new String(StringUtils.deleteChars(
1432 preEdit.getSequence(), position, position + number)));
1436 System.err.println("Can't undo edit action " + action);
1437 // throw new IllegalStateException("Can't undo edit action " +
1451 * start and end of sequence prior to edit
1453 Range[] oldStartEnd;
1455 boolean fullAlignmentHeight = false;
1457 Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1459 Map<String, Annotation[]> deletedAnnotations;
1462 * features deleted by the cut (re-add on Undo)
1463 * (including the original of any shortened features)
1465 Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1468 * shortened features added by the cut (delete on Undo)
1470 Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1474 final Action command;
1489 * flag that identifies edits inserted to balance
1490 * user edits in a 'locked editing' region
1492 private boolean systemGenerated;
1494 public Edit(Action cmd, SequenceI[] sqs, int pos, int count, char gap)
1498 this.position = pos;
1499 this.number = count;
1503 Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align)
1505 this(cmd, sqs, pos, count, align.getGapCharacter());
1509 alIndex = new int[sqs.length];
1510 for (int i = 0; i < sqs.length; i++)
1512 alIndex[i] = align.findIndex(sqs[i]);
1515 fullAlignmentHeight = (align.getHeight() == sqs.length);
1519 * Constructor given a REPLACE command and the replacement string
1528 Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align,
1531 this(cmd, sqs, pos, count, align);
1533 string = new char[sqs.length][];
1534 for (int i = 0; i < sqs.length; i++)
1536 string[i] = replace.toCharArray();
1540 public SequenceI[] getSequences()
1545 public int getPosition()
1550 public Action getAction()
1555 public int getNumber()
1560 public char getGapCharacter()
1565 public void setSystemGenerated(boolean b)
1567 systemGenerated = b;
1570 public boolean isSystemGenerated()
1572 return systemGenerated;
1577 * Returns an iterator over the list of edit commands which traverses the list
1578 * either forwards or backwards.
1583 public Iterator<Edit> getEditIterator(boolean forwards)
1587 return getEdits().iterator();
1591 return new ReverseListIterator<>(getEdits());
1596 * Adjusts features for Cut, and saves details of changes made to allow Undo
1598 * <li>features left of the cut are unchanged</li>
1599 * <li>features right of the cut are shifted left</li>
1600 * <li>features internal to the cut region are deleted</li>
1601 * <li>features that overlap or span the cut are shortened</li>
1602 * <li>the originals of any deleted or shortened features are saved, to re-add
1604 * <li>any added (shortened) features are saved, to delete on Undo</li>
1609 * @param fromPosition
1611 * @param cutIsInternal
1613 protected static void cutFeatures(Edit command, SequenceI seq,
1614 int fromPosition, int toPosition, boolean cutIsInternal)
1617 * if the cut is at start or end of sequence
1618 * then we don't modify the sequence feature store
1624 List<SequenceFeature> added = new ArrayList<>();
1625 List<SequenceFeature> removed = new ArrayList<>();
1627 SequenceFeaturesI featureStore = seq.getFeatures();
1628 if (toPosition < fromPosition || featureStore == null)
1633 int cutStartPos = fromPosition;
1634 int cutEndPos = toPosition;
1635 int cutWidth = cutEndPos - cutStartPos + 1;
1637 synchronized (featureStore)
1640 * get features that overlap the cut region
1642 List<SequenceFeature> toAmend = featureStore.findFeatures(cutStartPos,
1646 * add any contact features that span the cut region
1647 * (not returned by findFeatures)
1649 for (SequenceFeature contact : featureStore.getContactFeatures())
1651 if (contact.getBegin() < cutStartPos
1652 && contact.getEnd() > cutEndPos)
1654 toAmend.add(contact);
1659 * adjust start-end of overlapping features;
1660 * delete features enclosed by the cut;
1661 * delete partially overlapping contact features
1663 for (SequenceFeature sf : toAmend)
1665 int sfBegin = sf.getBegin();
1666 int sfEnd = sf.getEnd();
1667 int newBegin = sfBegin;
1669 boolean toDelete = false;
1670 boolean follows = false;
1672 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1675 * feature lies within cut region - delete it
1679 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1682 * feature spans cut region - left-shift the end
1686 else if (sfEnd <= cutEndPos)
1689 * feature overlaps left of cut region - truncate right
1691 newEnd = cutStartPos - 1;
1692 if (sf.isContactFeature())
1697 else if (sfBegin >= cutStartPos)
1700 * remaining case - feature overlaps right
1701 * truncate left, adjust end of feature
1703 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1704 newEnd = newBegin + sfEnd - cutEndPos - 1;
1705 if (sf.isContactFeature())
1711 seq.deleteFeature(sf);
1718 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1719 sf.getFeatureGroup(), sf.getScore());
1720 seq.addSequenceFeature(copy);
1729 * and left shift any features lying to the right of the cut region
1732 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1736 * save deleted and amended features, so that Undo can
1737 * re-add or delete them respectively
1739 if (command.deletedFeatures == null)
1741 command.deletedFeatures = new HashMap<>();
1743 if (command.truncatedFeatures == null)
1745 command.truncatedFeatures = new HashMap<>();
1747 command.deletedFeatures.put(seq, removed);
1748 command.truncatedFeatures.put(seq, added);