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.Range;
28 import jalview.datamodel.Sequence;
29 import jalview.datamodel.SequenceFeature;
30 import jalview.datamodel.SequenceI;
31 import jalview.datamodel.features.SequenceFeaturesI;
32 import jalview.util.Comparison;
33 import jalview.util.ReverseListIterator;
34 import jalview.util.StringUtils;
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.Hashtable;
39 import java.util.Iterator;
40 import java.util.List;
41 import java.util.ListIterator;
51 * Description: Essential information for performing undo and redo for cut/paste
52 * insert/delete gap which can be stored in the HistoryList
56 * Copyright: Copyright (c) 2006
60 * Company: Dundee University
63 * @author not attributable
66 public class EditCommand implements CommandI
73 public Action getUndoAction()
81 public Action getUndoAction()
89 public Action getUndoAction()
97 public Action getUndoAction()
105 public Action getUndoAction()
113 public Action getUndoAction()
118 public abstract Action getUndoAction();
121 private List<Edit> edits = new ArrayList<Edit>();
129 public EditCommand(String desc)
131 this.description = desc;
134 public EditCommand(String desc, Action command, SequenceI[] seqs,
135 int position, int number, AlignmentI al)
137 this.description = desc;
138 if (command == Action.CUT || command == Action.PASTE)
140 setEdit(new Edit(command, seqs, position, number, al));
143 performEdit(0, null);
146 public EditCommand(String desc, Action command, String replace,
147 SequenceI[] seqs, int position, int number, AlignmentI al)
149 this.description = desc;
150 if (command == Action.REPLACE)
152 setEdit(new Edit(command, seqs, position, number, al, replace));
155 performEdit(0, null);
159 * Set the list of edits to the specified item (only).
163 protected void setEdit(Edit e)
170 * Add the given edit command to the stored list of commands. If simply
171 * expanding the range of the last command added, then modify it instead of
172 * adding a new command.
176 public void addEdit(Edit e)
178 if (!expandEdit(edits, e))
185 * Returns true if the new edit is incorporated by updating (expanding the
186 * range of) the last edit on the list, else false. We can 'expand' the last
187 * edit if the new one is the same action, on the same sequences, and acts on
188 * a contiguous range. This is the case where a mouse drag generates a series
189 * of contiguous gap insertions or deletions.
195 protected static boolean expandEdit(List<Edit> edits, Edit e)
197 if (edits == null || edits.isEmpty())
201 Edit lastEdit = edits.get(edits.size() - 1);
202 Action action = e.command;
203 if (lastEdit.command != action)
209 * Both commands must act on the same sequences - compare the underlying
210 * dataset sequences, rather than the aligned sequences, which change as
213 if (lastEdit.seqs.length != e.seqs.length)
217 for (int i = 0; i < e.seqs.length; i++)
219 if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
220 .getDatasetSequence())
227 * Check a contiguous edit; either
229 * <li>a new Insert <n> positions to the right of the last <insert n>,
231 * <li>a new Delete <n> gaps which is <n> positions to the left of the last
235 boolean contiguous = (action == Action.INSERT_GAP
236 && e.position == lastEdit.position + lastEdit.number)
237 || (action == Action.DELETE_GAP
238 && e.position + e.number == lastEdit.position);
242 * We are just expanding the range of the last edit. For delete gap, also
243 * moving the start position left.
245 lastEdit.number += e.number;
246 lastEdit.seqs = e.seqs;
247 if (action == Action.DELETE_GAP)
257 * Clear the list of stored edit commands.
260 protected void clearEdits()
266 * Returns the i'th stored Edit command.
271 protected Edit getEdit(int i)
273 if (i >= 0 && i < edits.size())
281 final public String getDescription()
293 * Return the alignment for the first edit (or null if no edit).
297 final public AlignmentI getAlignment()
299 return (edits.isEmpty() ? null : edits.get(0).al);
303 * append a new editCommand Note. this shouldn't be called if the edit is an
304 * operation affects more alignment objects than the one referenced in al (for
305 * example, cut or pasting whole sequences). Use the form with an additional
306 * AlignmentI[] views parameter.
315 final public void appendEdit(Action command, SequenceI[] seqs,
316 int position, int number, AlignmentI al, boolean performEdit)
318 appendEdit(command, seqs, position, number, al, performEdit, null);
322 * append a new edit command with a set of alignment views that may be
333 final public void appendEdit(Action command, SequenceI[] seqs,
334 int position, int number, AlignmentI al, boolean performEdit,
337 Edit edit = new Edit(command, seqs, position, number, al);
338 appendEdit(edit, al, performEdit, views);
342 * Overloaded method that accepts an Edit object with additional parameters.
349 final public void appendEdit(Edit edit, AlignmentI al,
350 boolean performEdit, AlignmentI[] views)
352 if (al.getHeight() == edit.seqs.length)
355 edit.fullAlignmentHeight = true;
362 performEdit(edit, views);
367 * Execute all the edit commands, starting at the given commandIndex
369 * @param commandIndex
372 public final void performEdit(int commandIndex, AlignmentI[] views)
374 ListIterator<Edit> iterator = edits.listIterator(commandIndex);
375 while (iterator.hasNext())
377 Edit edit = iterator.next();
378 performEdit(edit, views);
383 * Execute one edit command in all the specified alignment views
388 protected static void performEdit(Edit edit, AlignmentI[] views)
390 switch (edit.command)
408 // TODO:add deleteNuc for UNDO
410 // insertNuc(edits[e]);
418 final public void doCommand(AlignmentI[] views)
420 performEdit(0, views);
424 * Undo the stored list of commands, in reverse order.
427 final public void undoCommand(AlignmentI[] views)
429 ListIterator<Edit> iterator = edits.listIterator(edits.size());
430 while (iterator.hasPrevious())
432 Edit e = iterator.previous();
460 * Insert gap(s) in sequences as specified by the command, and adjust
465 final private static void insertGap(Edit command)
468 for (int s = 0; s < command.seqs.length; s++)
470 command.seqs[s].insertCharAt(command.position, command.number,
472 // System.out.println("pos: "+command.position+" number:
473 // "+command.number);
476 adjustAnnotations(command, true, false, null);
480 // final void insertNuc(Edit command)
483 // for (int s = 0; s < command.seqs.length; s++)
485 // System.out.println("pos: "+command.position+" number: "+command.number);
486 // command.seqs[s].insertCharAt(command.position, command.number,'A');
489 // adjustAnnotations(command, true, false, null);
493 * Delete gap(s) in sequences as specified by the command, and adjust
498 final static private void deleteGap(Edit command)
500 for (int s = 0; s < command.seqs.length; s++)
502 command.seqs[s].deleteChars(command.position,
503 command.position + command.number);
506 adjustAnnotations(command, false, false, null);
510 * Carry out a Cut action. The cut characters are saved in case Undo is
516 static void cut(Edit command, AlignmentI[] views)
518 boolean seqDeleted = false;
519 command.string = new char[command.seqs.length][];
521 for (int i = 0; i < command.seqs.length; i++)
523 final SequenceI sequence = command.seqs[i];
524 if (sequence.getLength() > command.position)
526 command.string[i] = sequence.getSequence(command.position,
527 command.position + command.number);
528 SequenceI oldds = sequence.getDatasetSequence();
529 if (command.oldds != null && command.oldds[i] != null)
531 // we are redoing an undone cut.
532 sequence.setDatasetSequence(null);
534 Range cutPositions = sequence.findPositions(command.position + 1,
535 command.position + command.number);
536 boolean cutIsInternal = cutPositions != null
537 && sequence.getStart() != cutPositions
538 .getBegin() && sequence.getEnd() != cutPositions.getEnd();
541 * perform the cut; if this results in a new dataset sequence, add
542 * that to the alignment dataset
544 SequenceI ds = sequence.getDatasetSequence();
545 sequence.deleteChars(command.position, command.position
547 SequenceI newDs = sequence.getDatasetSequence();
548 if (newDs != ds && command.al != null
549 && command.al.getDataset() != null
550 && !command.al.getDataset().getSequences().contains(newDs))
552 command.al.getDataset().addSequence(newDs);
555 if (command.oldds != null && command.oldds[i] != null)
557 // Undoing previous Paste - so
558 // oldds entry contains the cut dataset sequence,
559 // with sequence features in expected place.
560 sequence.setDatasetSequence(command.oldds[i]);
561 command.oldds[i] = oldds;
566 // We always keep track of the dataset sequence so we can safely
567 // restore it during the Undo
568 if (command.oldds == null)
570 command.oldds = new SequenceI[command.seqs.length];
572 command.oldds[i] = oldds;// todo not if !cutIsInternal?
574 // do we need to edit sequence features for new sequence ?
575 if (oldds != sequence.getDatasetSequence()
577 && sequence.getFeatures().hasFeatures()))
578 // todo or just test cutIsInternal && cutPositions != null ?
580 if (cutPositions != null)
582 cutFeatures(command, sequence, cutPositions.getBegin(),
583 cutPositions.getEnd(), cutIsInternal);
589 if (sequence.getLength() < 1)
591 command.al.deleteSequence(sequence);
596 adjustAnnotations(command, false, seqDeleted, views);
600 * Perform the given Paste command. This may be to add cut or copied sequences
601 * to an alignment, or to undo a 'Cut' action on a region of the alignment.
606 static void paste(Edit command, AlignmentI[] views)
608 boolean seqWasDeleted = false;
610 for (int i = 0; i < command.seqs.length; i++)
612 boolean newDSNeeded = false;
613 boolean newDSWasNeeded = command.oldds != null
614 && command.oldds[i] != null;
615 SequenceI sequence = command.seqs[i];
616 if (sequence.getLength() < 1)
619 * sequence was deleted; re-add it to the alignment
621 if (command.alIndex[i] < command.al.getHeight())
623 List<SequenceI> sequences;
624 synchronized (sequences = command.al.getSequences())
626 if (!(command.alIndex[i] < 0))
628 sequences.add(command.alIndex[i], sequence);
634 command.al.addSequence(sequence);
636 seqWasDeleted = true;
638 int newStart = sequence.getStart();
639 int newEnd = sequence.getEnd();
641 StringBuilder tmp = new StringBuilder();
642 tmp.append(sequence.getSequence());
643 // Undo of a delete does not replace original dataset sequence on to
644 // alignment sequence.
649 if (command.string != null && command.string[i] != null)
651 if (command.position >= tmp.length())
653 // This occurs if padding is on, and residues
654 // are removed from end of alignment
655 int len = command.position - tmp.length();
658 tmp.append(command.gapChar);
662 tmp.insert(command.position, command.string[i]);
663 for (int s = 0; s < command.string[i].length; s++)
665 if (!Comparison.isGap(command.string[i][s]))
671 start = sequence.findPosition(command.position);
673 // .findPosition(command.position + command.number);
675 if (sequence.getStart() == start)
685 command.string[i] = null;
688 sequence.setSequence(tmp.toString());
689 sequence.setStart(newStart);
690 sequence.setEnd(newEnd);
693 * command and Undo share the same dataset sequence if cut was
694 * at start or end of sequence
696 boolean sameDatasetSequence = false;
699 if (sequence.getDatasetSequence() != null)
704 ds = command.oldds[i];
708 // make a new DS sequence
709 // use new ds mechanism here
710 String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
711 sequence.getSequenceAsString());
712 ds = new Sequence(sequence.getName(), ungapped,
713 sequence.getStart(), sequence.getEnd());
714 ds.setDescription(sequence.getDescription());
716 if (command.oldds == null)
718 command.oldds = new SequenceI[command.seqs.length];
720 command.oldds[i] = sequence.getDatasetSequence();
721 sameDatasetSequence = ds == sequence.getDatasetSequence();
722 ds.setSequenceFeatures(sequence.getSequenceFeatures());
723 sequence.setDatasetSequence(ds);
725 undoCutFeatures(command, command.seqs[i], start, length,
726 sameDatasetSequence);
729 adjustAnnotations(command, true, seqWasDeleted, views);
731 command.string = null;
734 static void replace(Edit command)
738 int start = command.position;
739 int end = command.number;
740 // TODO TUTORIAL - Fix for replacement with different length of sequence (or
742 // TODO Jalview 2.4 bugfix change to an aggregate command - original
743 // sequence string is cut, new string is pasted in.
744 command.number = start + command.string[0].length;
745 for (int i = 0; i < command.seqs.length; i++)
747 boolean newDSWasNeeded = command.oldds != null
748 && command.oldds[i] != null;
749 boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
752 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
753 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
754 * viewport.alignment));
758 * then addHistoryItem(new EditCommand( "Add sequences",
759 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
763 Range beforeEditedPositions = command.seqs[i].findPositions(1, start);
764 Range afterEditedPositions = command.seqs[i]
765 .findPositions(end + 1, command.seqs[i].getLength());
767 oldstring = command.seqs[i].getSequenceAsString();
768 tmp = new StringBuffer(oldstring.substring(0, start));
769 tmp.append(command.string[i]);
770 String nogaprep = jalview.analysis.AlignSeq.extractGaps(
771 jalview.util.Comparison.GapChars,
772 new String(command.string[i]));
773 if (end < oldstring.length())
775 tmp.append(oldstring.substring(end));
777 // stash end prior to updating the sequence object so we can save it if
779 Range oldstartend = new Range(command.seqs[i].getStart(),
780 command.seqs[i].getEnd());
781 command.seqs[i].setSequence(tmp.toString());
782 command.string[i] = oldstring
783 .substring(start, Math.min(end, oldstring.length()))
785 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
786 new String(command.string[i]));
788 if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
790 // we may already have dataset and limits stashed...
791 if (newDSWasNeeded || newStartEndWasNeeded)
795 // then just switch the dataset sequence
796 SequenceI oldds = command.seqs[i].getDatasetSequence();
797 command.seqs[i].setDatasetSequence(command.oldds[i]);
798 command.oldds[i] = oldds;
800 if (newStartEndWasNeeded)
802 Range newStart = command.oldStartEnd[i];
803 command.oldStartEnd[i] = oldstartend;
804 command.seqs[i].setStart(newStart.getBegin());
805 command.seqs[i].setEnd(newStart.getEnd());
810 // decide if we need a new dataset sequence or modify start/end
811 // first edit the original dataset sequence string
812 SequenceI oldds = command.seqs[i].getDatasetSequence();
813 String osp = oldds.getSequenceAsString();
814 int beforeStartOfEdit = -oldds.getStart() + 1
815 + (beforeEditedPositions == null
816 ? ((afterEditedPositions != null)
817 ? afterEditedPositions.getBegin() - 1
818 : oldstartend.getBegin()
820 : beforeEditedPositions.getEnd()
822 int afterEndOfEdit = -oldds.getStart() + 1
823 + ((afterEditedPositions == null)
824 ? oldstartend.getEnd()
825 : afterEditedPositions.getBegin() - 1);
826 String fullseq = osp.substring(0,
829 + osp.substring(afterEndOfEdit);
831 // and check if new sequence data is different..
832 if (!fullseq.equalsIgnoreCase(osp))
834 // old ds and edited ds are different, so
835 // create the new dataset sequence
836 SequenceI newds = new Sequence(oldds);
837 newds.setSequence(fullseq.toUpperCase());
839 if (command.oldds == null)
841 command.oldds = new SequenceI[command.seqs.length];
843 command.oldds[i] = command.seqs[i].getDatasetSequence();
845 // And preserve start/end for good-measure
847 if (command.oldStartEnd == null)
849 command.oldStartEnd = new Range[command.seqs.length];
851 command.oldStartEnd[i] = oldstartend;
852 // TODO: JAL-1131 ensure newly created dataset sequence is added to
854 // dataset sequences associated with the alignment.
855 // TODO: JAL-1131 fix up any annotation associated with new dataset
856 // sequence to ensure that original sequence/annotation
859 command.seqs[i].setDatasetSequence(newds);
863 if (command.oldStartEnd == null)
865 command.oldStartEnd = new Range[command.seqs.length];
867 command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
868 command.seqs[i].getEnd());
869 if (beforeEditedPositions != null
870 && afterEditedPositions == null)
872 // modification at end
873 command.seqs[i].setEnd(
874 beforeEditedPositions.getEnd() + nogaprep.length()
875 - nogapold.length());
877 else if (afterEditedPositions != null
878 && beforeEditedPositions == null)
880 // modification at start
881 command.seqs[i].setStart(
882 afterEditedPositions.getBegin() - nogaprep.length());
886 // edit covered both start and end. Here we can only guess the
889 String nogapalseq = jalview.analysis.AlignSeq.extractGaps(
890 jalview.util.Comparison.GapChars,
891 command.seqs[i].getSequenceAsString().toUpperCase());
892 int newStart = command.seqs[i].getDatasetSequence()
893 .getSequenceAsString().indexOf(nogapalseq);
897 "Implementation Error: could not locate start/end "
898 + "in dataset sequence after an edit of the sequence string");
900 int newEnd = newStart + nogapalseq.length() - 1;
901 command.seqs[i].setStart(newStart);
902 command.seqs[i].setEnd(newEnd);
912 final static void adjustAnnotations(Edit command, boolean insert,
913 boolean modifyVisibility, AlignmentI[] views)
915 AlignmentAnnotation[] annotations = null;
917 if (modifyVisibility && !insert)
919 // only occurs if a sequence was added or deleted.
920 command.deletedAnnotationRows = new Hashtable<SequenceI, AlignmentAnnotation[]>();
922 if (command.fullAlignmentHeight)
924 annotations = command.al.getAlignmentAnnotation();
929 AlignmentAnnotation[] tmp;
930 for (int s = 0; s < command.seqs.length; s++)
932 command.seqs[s].sequenceChanged();
934 if (modifyVisibility)
936 // Rows are only removed or added to sequence object.
940 tmp = command.seqs[s].getAnnotation();
943 int alen = tmp.length;
944 for (int aa = 0; aa < tmp.length; aa++)
946 if (!command.al.deleteAnnotation(tmp[aa]))
948 // strip out annotation not in the current al (will be put
949 // back on insert in all views)
954 command.seqs[s].setAlignmentAnnotation(null);
955 if (alen != tmp.length)
957 // save the non-null annotation references only
958 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
959 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
963 saved[aapos++] = tmp[aa];
968 command.deletedAnnotationRows.put(command.seqs[s], saved);
969 // and then remove any annotation in the other views
970 for (int alview = 0; views != null
971 && alview < views.length; alview++)
973 if (views[alview] != command.al)
975 AlignmentAnnotation[] toremove = views[alview]
976 .getAlignmentAnnotation();
977 if (toremove == null || toremove.length == 0)
981 // remove any alignment annotation on this sequence that's
982 // on that alignment view.
983 for (int aa = 0; aa < toremove.length; aa++)
985 if (toremove[aa].sequenceRef == command.seqs[s])
987 views[alview].deleteAnnotation(toremove[aa]);
995 // save all the annotation
996 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1003 if (command.deletedAnnotationRows != null
1004 && command.deletedAnnotationRows
1005 .containsKey(command.seqs[s]))
1007 AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1008 .get(command.seqs[s]);
1009 command.seqs[s].setAlignmentAnnotation(revealed);
1010 if (revealed != null)
1012 for (int aa = 0; aa < revealed.length; aa++)
1014 // iterate through al adding original annotation
1015 command.al.addAnnotation(revealed[aa]);
1017 for (int aa = 0; aa < revealed.length; aa++)
1019 command.al.setAnnotationIndex(revealed[aa], aa);
1021 // and then duplicate added annotation on every other alignment
1023 for (int vnum = 0; views != null && vnum < views.length; vnum++)
1025 if (views[vnum] != command.al)
1027 int avwidth = views[vnum].getWidth() + 1;
1028 // duplicate in this view
1029 for (int a = 0; a < revealed.length; a++)
1031 AlignmentAnnotation newann = new AlignmentAnnotation(
1033 command.seqs[s].addAlignmentAnnotation(newann);
1034 newann.padAnnotation(avwidth);
1035 views[vnum].addAnnotation(newann);
1036 views[vnum].setAnnotationIndex(newann, a);
1046 if (command.seqs[s].getAnnotation() == null)
1053 annotations = command.seqs[s].getAnnotation();
1057 tmp = new AlignmentAnnotation[aSize
1058 + command.seqs[s].getAnnotation().length];
1060 System.arraycopy(annotations, 0, tmp, 0, aSize);
1062 System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1063 command.seqs[s].getAnnotation().length);
1067 aSize = annotations.length;
1071 if (annotations == null)
1078 command.deletedAnnotations = new Hashtable<String, Annotation[]>();
1083 for (int a = 0; a < annotations.length; a++)
1085 if (annotations[a].autoCalculated
1086 || annotations[a].annotations == null)
1093 aSize = annotations[a].annotations.length;
1096 temp = new Annotation[aSize + command.number];
1097 if (annotations[a].padGaps)
1099 for (int aa = 0; aa < temp.length; aa++)
1101 temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1107 if (command.position < aSize)
1109 if (command.position + command.number >= aSize)
1115 tSize = aSize - command.number;
1127 temp = new Annotation[tSize];
1132 if (command.position < annotations[a].annotations.length)
1134 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1137 if (command.deletedAnnotations != null
1138 && command.deletedAnnotations
1139 .containsKey(annotations[a].annotationId))
1141 Annotation[] restore = command.deletedAnnotations
1142 .get(annotations[a].annotationId);
1144 System.arraycopy(restore, 0, temp, command.position,
1149 System.arraycopy(annotations[a].annotations, command.position,
1150 temp, command.position + command.number,
1151 aSize - command.position);
1155 if (command.deletedAnnotations != null
1156 && command.deletedAnnotations
1157 .containsKey(annotations[a].annotationId))
1159 Annotation[] restore = command.deletedAnnotations
1160 .get(annotations[a].annotationId);
1162 temp = new Annotation[annotations[a].annotations.length
1164 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1165 annotations[a].annotations.length);
1166 System.arraycopy(restore, 0, temp,
1167 annotations[a].annotations.length, restore.length);
1171 temp = annotations[a].annotations;
1177 if (tSize != aSize || command.position < 2)
1179 int copylen = Math.min(command.position,
1180 annotations[a].annotations.length);
1183 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1184 copylen); // command.position);
1187 Annotation[] deleted = new Annotation[command.number];
1188 if (copylen >= command.position)
1190 copylen = Math.min(command.number,
1191 annotations[a].annotations.length - command.position);
1194 System.arraycopy(annotations[a].annotations, command.position,
1195 deleted, 0, copylen); // command.number);
1199 command.deletedAnnotations.put(annotations[a].annotationId,
1201 if (annotations[a].annotations.length > command.position
1204 System.arraycopy(annotations[a].annotations,
1205 command.position + command.number, temp,
1206 command.position, annotations[a].annotations.length
1207 - command.position - command.number); // aSize
1212 int dSize = aSize - command.position;
1216 Annotation[] deleted = new Annotation[command.number];
1217 System.arraycopy(annotations[a].annotations, command.position,
1220 command.deletedAnnotations.put(annotations[a].annotationId,
1223 tSize = Math.min(annotations[a].annotations.length,
1225 temp = new Annotation[tSize];
1226 System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1230 temp = annotations[a].annotations;
1235 annotations[a].annotations = temp;
1240 * Restores features to the state before a Cut.
1242 * <li>re-add any features deleted by the cut</li>
1243 * <li>remove any truncated features created by the cut</li>
1244 * <li>shift right any features to the right of the cut</li>
1250 * the sequence the Cut applied to
1252 * the start residue position of the cut
1254 * the number of residues cut
1255 * @param sameDatasetSequence
1256 * true if dataset sequence and frame of reference were left
1257 * unchanged by the Cut
1259 final static void undoCutFeatures(Edit command, SequenceI seq,
1260 final int start, final int length, boolean sameDatasetSequence)
1262 SequenceI sequence = seq.getDatasetSequence();
1263 if (sequence == null)
1269 * shift right features that lie to the right of the restored cut (but not
1270 * if dataset sequence unchanged - so coordinates were changed by Cut)
1272 if (!sameDatasetSequence)
1275 * shift right all features right of and not
1276 * contiguous with the cut position
1278 seq.getFeatures().shiftFeatures(start + 1, length);
1281 * shift right any features that start at the cut position,
1282 * unless they were truncated
1284 List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1286 for (SequenceFeature sf : sfs)
1288 if (sf.getBegin() == start)
1290 if (!command.truncatedFeatures.containsKey(seq)
1291 || !command.truncatedFeatures.get(seq).contains(sf))
1294 * feature was shifted left to cut position (not truncated),
1295 * so shift it back right
1297 SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
1298 + length, sf.getEnd() + length, sf.getFeatureGroup(),
1300 seq.addSequenceFeature(shifted);
1301 seq.deleteFeature(sf);
1308 * restore any features that were deleted or truncated
1310 if (command.deletedFeatures != null
1311 && command.deletedFeatures.containsKey(seq))
1313 for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1315 sequence.addSequenceFeature(deleted);
1320 * delete any truncated features
1322 if (command.truncatedFeatures != null
1323 && command.truncatedFeatures.containsKey(seq))
1325 for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1327 sequence.deleteFeature(amended);
1333 * Returns the list of edit commands wrapped by this object.
1337 public List<Edit> getEdits()
1343 * Returns a map whose keys are the dataset sequences, and values their
1344 * aligned sequences before the command edit list was applied. The aligned
1345 * sequences are copies, which may be updated without affecting the originals.
1347 * The command holds references to the aligned sequences (after editing). If
1348 * the command is an 'undo',then the prior state is simply the aligned state.
1349 * Otherwise, we have to derive the prior state by working backwards through
1350 * the edit list to infer the aligned sequences before editing.
1352 * Note: an alternative solution would be to cache the 'before' state of each
1353 * edit, but this would be expensive in space in the common case that the
1354 * original is never needed (edits are not mirrored).
1357 * @throws IllegalStateException
1358 * on detecting an edit command of a type that can't be unwound
1360 public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1362 Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
1363 if (getEdits() == null)
1369 for (Edit e : getEdits())
1371 for (SequenceI seq : e.getSequences())
1373 SequenceI ds = seq.getDatasetSequence();
1374 // SequenceI preEdit = result.get(ds);
1375 if (!result.containsKey(ds))
1378 * copy sequence including start/end (but don't use copy constructor
1379 * as we don't need annotations)
1381 SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1382 seq.getStart(), seq.getEnd());
1383 preEdit.setDatasetSequence(ds);
1384 result.put(ds, preEdit);
1392 * Work backwards through the edit list, deriving the sequences before each
1393 * was applied. The final result is the sequence set before any edits.
1395 Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
1396 while (editList.hasNext())
1398 Edit oldEdit = editList.next();
1399 Action action = oldEdit.getAction();
1400 int position = oldEdit.getPosition();
1401 int number = oldEdit.getNumber();
1402 final char gap = oldEdit.getGapCharacter();
1403 for (SequenceI seq : oldEdit.getSequences())
1405 SequenceI ds = seq.getDatasetSequence();
1406 SequenceI preEdit = result.get(ds);
1407 if (preEdit == null)
1409 preEdit = new Sequence("", seq.getSequenceAsString(),
1410 seq.getStart(), seq.getEnd());
1411 preEdit.setDatasetSequence(ds);
1412 result.put(ds, preEdit);
1415 * 'Undo' this edit action on the sequence (updating the value in the
1420 if (action == Action.DELETE_GAP)
1422 preEdit.setSequence(new String(StringUtils.insertCharAt(
1423 preEdit.getSequence(), position, number, gap)));
1425 else if (action == Action.INSERT_GAP)
1427 preEdit.setSequence(new String(StringUtils.deleteChars(
1428 preEdit.getSequence(), position, position + number)));
1432 System.err.println("Can't undo edit action " + action);
1433 // throw new IllegalStateException("Can't undo edit action " +
1444 private SequenceI[] oldds;
1447 * start and end of sequence prior to edit
1449 private Range[] oldStartEnd;
1451 private boolean fullAlignmentHeight = false;
1453 private Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1455 private Map<String, Annotation[]> deletedAnnotations;
1458 * features deleted by the cut (re-add on Undo)
1459 * (including the original of any shortened features)
1461 private Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1464 * shortened features added by the cut (delete on Undo)
1466 private Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1468 private AlignmentI al;
1470 final private Action command;
1476 private int[] alIndex;
1478 private int position;
1482 private char gapChar;
1484 public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1489 this.position = pos;
1490 this.number = count;
1494 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1497 this(cmd, sqs, pos, count, align.getGapCharacter());
1501 alIndex = new int[sqs.length];
1502 for (int i = 0; i < sqs.length; i++)
1504 alIndex[i] = align.findIndex(sqs[i]);
1507 fullAlignmentHeight = (align.getHeight() == sqs.length);
1511 * Constructor given a REPLACE command and the replacement string
1520 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1521 AlignmentI align, String replace)
1523 this(cmd, sqs, pos, count, align);
1525 string = new char[sqs.length][];
1526 for (int i = 0; i < sqs.length; i++)
1528 string[i] = replace.toCharArray();
1532 public SequenceI[] getSequences()
1537 public int getPosition()
1542 public Action getAction()
1547 public int getNumber()
1552 public char getGapCharacter()
1559 * Returns an iterator over the list of edit commands which traverses the list
1560 * either forwards or backwards.
1565 public Iterator<Edit> getEditIterator(boolean forwards)
1569 return getEdits().iterator();
1573 return new ReverseListIterator<Edit>(getEdits());
1578 * Adjusts features for Cut, and saves details of changes made to allow Undo
1580 * <li>features left of the cut are unchanged</li>
1581 * <li>features right of the cut are shifted left</li>
1582 * <li>features internal to the cut region are deleted</li>
1583 * <li>features that overlap or span the cut are shortened</li>
1584 * <li>the originals of any deleted or shortened features are saved, to re-add
1586 * <li>any added (shortened) features are saved, to delete on Undo</li>
1591 * @param fromPosition
1593 * @param cutIsInternal
1595 protected static void cutFeatures(Edit command, SequenceI seq,
1596 int fromPosition, int toPosition, boolean cutIsInternal)
1599 * if the cut is at start or end of sequence
1600 * then we don't modify the sequence feature store
1606 List<SequenceFeature> added = new ArrayList<>();
1607 List<SequenceFeature> removed = new ArrayList<>();
1609 SequenceFeaturesI featureStore = seq.getFeatures();
1610 if (toPosition < fromPosition || featureStore == null)
1615 int cutStartPos = fromPosition;
1616 int cutEndPos = toPosition;
1617 int cutWidth = cutEndPos - cutStartPos + 1;
1619 synchronized (featureStore)
1622 * get features that overlap the cut region
1624 List<SequenceFeature> toAmend = featureStore.findFeatures(
1625 cutStartPos, cutEndPos);
1628 * add any contact features that span the cut region
1629 * (not returned by findFeatures)
1631 for (SequenceFeature contact : featureStore.getContactFeatures())
1633 if (contact.getBegin() < cutStartPos
1634 && contact.getEnd() > cutEndPos)
1636 toAmend.add(contact);
1641 * adjust start-end of overlapping features;
1642 * delete features enclosed by the cut;
1643 * delete partially overlapping contact features
1645 for (SequenceFeature sf : toAmend)
1647 int sfBegin = sf.getBegin();
1648 int sfEnd = sf.getEnd();
1649 int newBegin = sfBegin;
1651 boolean toDelete = false;
1652 boolean follows = false;
1654 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1657 * feature lies within cut region - delete it
1661 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1664 * feature spans cut region - left-shift the end
1668 else if (sfEnd <= cutEndPos)
1671 * feature overlaps left of cut region - truncate right
1673 newEnd = cutStartPos - 1;
1674 if (sf.isContactFeature())
1679 else if (sfBegin >= cutStartPos)
1682 * remaining case - feature overlaps right
1683 * truncate left, adjust end of feature
1685 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1686 newEnd = newBegin + sfEnd - cutEndPos - 1;
1687 if (sf.isContactFeature())
1693 seq.deleteFeature(sf);
1700 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1701 sf.getFeatureGroup(), sf.getScore());
1702 seq.addSequenceFeature(copy);
1711 * and left shift any features lying to the right of the cut region
1714 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1718 * save deleted and amended features, so that Undo can
1719 * re-add or delete them respectively
1721 if (command.deletedFeatures == null)
1723 command.deletedFeatures = new HashMap<>();
1725 if (command.truncatedFeatures == null)
1727 command.truncatedFeatures = new HashMap<>();
1729 command.deletedFeatures.put(seq, removed);
1730 command.truncatedFeatures.put(seq, added);