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()
121 public abstract Action getUndoAction();
124 private List<Edit> edits = new ArrayList<>();
132 public EditCommand(String desc)
134 this.description = desc;
137 public EditCommand(String desc, Action command, SequenceI[] seqs,
138 int position, int number, AlignmentI al)
140 this.description = desc;
141 if (command == Action.CUT || command == Action.PASTE)
143 setEdit(new Edit(command, seqs, position, number, al));
146 performEdit(0, null);
149 public EditCommand(String desc, Action command, String replace,
150 SequenceI[] seqs, int position, int number, AlignmentI al)
152 this.description = desc;
153 if (command == Action.REPLACE)
155 setEdit(new Edit(command, seqs, position, number, al, replace));
158 performEdit(0, null);
162 * Set the list of edits to the specified item (only).
166 protected void setEdit(Edit e)
173 * Add the given edit command to the stored list of commands. If simply
174 * expanding the range of the last command added, then modify it instead of
175 * adding a new command.
179 public void addEdit(Edit e)
181 if (!expandEdit(edits, e))
188 * Returns true if the new edit is incorporated by updating (expanding the
189 * range of) the last edit on the list, else false. We can 'expand' the last
190 * edit if the new one is the same action, on the same sequences, and acts on
191 * a contiguous range. This is the case where a mouse drag generates a series
192 * of contiguous gap insertions or deletions.
198 protected static boolean expandEdit(List<Edit> edits, Edit e)
200 if (edits == null || edits.isEmpty())
204 Edit lastEdit = edits.get(edits.size() - 1);
205 Action action = e.command;
206 if (lastEdit.command != action)
212 * Both commands must act on the same sequences - compare the underlying
213 * dataset sequences, rather than the aligned sequences, which change as
216 if (lastEdit.seqs.length != e.seqs.length)
220 for (int i = 0; i < e.seqs.length; i++)
222 if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
223 .getDatasetSequence())
230 * Check a contiguous edit; either
232 * <li>a new Insert <n> positions to the right of the last <insert n>,
234 * <li>a new Delete <n> gaps which is <n> positions to the left of the last
238 boolean contiguous = (action == Action.INSERT_GAP
239 && e.position == lastEdit.position + lastEdit.number)
240 || (action == Action.DELETE_GAP
241 && e.position + e.number == lastEdit.position);
245 * We are just expanding the range of the last edit. For delete gap, also
246 * moving the start position left.
248 lastEdit.number += e.number;
249 lastEdit.seqs = e.seqs;
250 if (action == Action.DELETE_GAP)
260 * Clear the list of stored edit commands.
263 protected void clearEdits()
269 * Returns the i'th stored Edit command.
274 protected Edit getEdit(int i)
276 if (i >= 0 && i < edits.size())
284 final public String getDescription()
296 * Return the alignment for the first edit (or null if no edit).
300 final public AlignmentI getAlignment()
302 return (edits.isEmpty() ? null : edits.get(0).al);
306 * append a new editCommand Note. this shouldn't be called if the edit is an
307 * operation affects more alignment objects than the one referenced in al (for
308 * example, cut or pasting whole sequences). Use the form with an additional
309 * AlignmentI[] views parameter.
318 final public void appendEdit(Action command, SequenceI[] seqs,
319 int position, int number, AlignmentI al, boolean performEdit)
321 appendEdit(command, seqs, position, number, al, performEdit, null);
325 * append a new edit command with a set of alignment views that may be
336 final public void appendEdit(Action command, SequenceI[] seqs,
337 int position, int number, AlignmentI al, boolean performEdit,
340 Edit edit = new Edit(command, seqs, position, number, al);
341 appendEdit(edit, al, performEdit, views);
345 * Overloaded method that accepts an Edit object with additional parameters.
352 final public void appendEdit(Edit edit, AlignmentI al,
353 boolean performEdit, AlignmentI[] views)
355 if (al.getHeight() == edit.seqs.length)
358 edit.fullAlignmentHeight = true;
365 performEdit(edit, views);
370 * Execute all the edit commands, starting at the given commandIndex
372 * @param commandIndex
375 public final void performEdit(int commandIndex, AlignmentI[] views)
377 ListIterator<Edit> iterator = edits.listIterator(commandIndex);
378 while (iterator.hasNext())
380 Edit edit = iterator.next();
381 performEdit(edit, views);
386 * Execute one edit command in all the specified alignment views
391 protected static void performEdit(Edit edit, AlignmentI[] views)
393 switch (edit.command)
411 // TODO:add deleteNuc for UNDO
413 // insertNuc(edits[e]);
421 final public void doCommand(AlignmentI[] views)
423 performEdit(0, views);
427 * Undo the stored list of commands, in reverse order.
430 final public void undoCommand(AlignmentI[] views)
432 ListIterator<Edit> iterator = edits.listIterator(edits.size());
433 while (iterator.hasPrevious())
435 Edit e = iterator.previous();
463 * Insert gap(s) in sequences as specified by the command, and adjust
468 final private static void insertGap(Edit command)
471 for (int s = 0; s < command.seqs.length; s++)
473 command.seqs[s].insertCharAt(command.position, command.number,
475 // System.out.println("pos: "+command.position+" number:
476 // "+command.number);
479 adjustAnnotations(command, true, false, null);
483 // final void insertNuc(Edit command)
486 // for (int s = 0; s < command.seqs.length; s++)
488 // System.out.println("pos: "+command.position+" number: "+command.number);
489 // command.seqs[s].insertCharAt(command.position, command.number,'A');
492 // adjustAnnotations(command, true, false, null);
496 * Delete gap(s) in sequences as specified by the command, and adjust
501 final static private void deleteGap(Edit command)
503 for (int s = 0; s < command.seqs.length; s++)
505 command.seqs[s].deleteChars(command.position,
506 command.position + command.number);
509 adjustAnnotations(command, false, false, null);
513 * Carry out a Cut action. The cut characters are saved in case Undo is
519 static void cut(Edit command, AlignmentI[] views)
521 boolean seqDeleted = false;
522 command.string = new char[command.seqs.length][];
524 for (int i = 0; i < command.seqs.length; i++)
526 final SequenceI sequence = command.seqs[i];
527 if (sequence.getLength() > command.position)
529 command.string[i] = sequence.getSequence(command.position,
530 command.position + command.number);
531 SequenceI oldds = sequence.getDatasetSequence();
532 ContiguousI cutPositions = sequence.findPositions(
533 command.position + 1, command.position + command.number);
534 boolean cutIsInternal = cutPositions != null
535 && sequence.getStart() != cutPositions
536 .getBegin() && sequence.getEnd() != cutPositions.getEnd();
539 * perform the cut; if this results in a new dataset sequence, add
540 * that to the alignment dataset
542 SequenceI ds = sequence.getDatasetSequence();
543 sequence.deleteChars(command.position, command.position
546 if (command.oldds != null && command.oldds[i] != null)
549 * we are Redoing a Cut, or Undoing a Paste - so
550 * oldds entry contains the cut dataset sequence,
551 * with sequence features in expected place
553 sequence.setDatasetSequence(command.oldds[i]);
554 command.oldds[i] = oldds;
559 * new cut operation: save the dataset sequence
560 * so it can be restored in an Undo
562 if (command.oldds == null)
564 command.oldds = new SequenceI[command.seqs.length];
566 command.oldds[i] = oldds;// todo not if !cutIsInternal?
568 // do we need to edit sequence features for new sequence ?
569 if (oldds != sequence.getDatasetSequence()
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 && command.oldStartEnd[i]!=null;
759 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
760 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
761 * viewport.alignment));
765 * then addHistoryItem(new EditCommand( "Add sequences",
766 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
769 ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
771 ContiguousI afterEditedPositions = command.seqs[i]
772 .findPositions(end + 1, command.seqs[i].getLength());
774 oldstring = command.seqs[i].getSequenceAsString();
775 tmp = new StringBuilder(oldstring.substring(0, start));
776 tmp.append(command.string[i]);
777 String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
778 new String(command.string[i]));
779 if (end < oldstring.length())
781 tmp.append(oldstring.substring(end));
783 // stash end prior to updating the sequence object so we can save it if
785 Range oldstartend = new Range(command.seqs[i].getStart(),
786 command.seqs[i].getEnd());
787 command.seqs[i].setSequence(tmp.toString());
788 command.string[i] = oldstring
789 .substring(start, Math.min(end, oldstring.length()))
791 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
792 new String(command.string[i]));
794 if (!nogaprep.toLowerCase(Locale.ROOT).equals(nogapold.toLowerCase(Locale.ROOT)))
796 // we may already have dataset and limits stashed...
797 if (newDSWasNeeded || newStartEndWasNeeded)
801 // then just switch the dataset sequence
802 SequenceI oldds = command.seqs[i].getDatasetSequence();
803 command.seqs[i].setDatasetSequence(command.oldds[i]);
804 command.oldds[i] = oldds;
806 if (newStartEndWasNeeded)
808 Range newStart = command.oldStartEnd[i];
809 command.oldStartEnd[i] = oldstartend;
810 command.seqs[i].setStart(newStart.getBegin());
811 command.seqs[i].setEnd(newStart.getEnd());
816 // decide if we need a new dataset sequence or modify start/end
817 // first edit the original dataset sequence string
818 SequenceI oldds = command.seqs[i].getDatasetSequence();
819 String osp = oldds.getSequenceAsString();
820 int beforeStartOfEdit = -oldds.getStart() + 1
821 + (beforeEditedPositions == null
822 ? ((afterEditedPositions != null)
823 ? afterEditedPositions.getBegin() - 1
824 : oldstartend.getBegin()
826 : beforeEditedPositions.getEnd()
828 int afterEndOfEdit = -oldds.getStart() + 1
829 + ((afterEditedPositions == null)
830 ? oldstartend.getEnd()
831 : afterEditedPositions.getBegin() - 1);
832 String fullseq = osp.substring(0,
835 + osp.substring(afterEndOfEdit);
837 // and check if new sequence data is different..
838 if (!fullseq.equalsIgnoreCase(osp))
840 // old ds and edited ds are different, so
841 // create the new dataset sequence
842 SequenceI newds = new Sequence(oldds);
843 newds.setSequence(fullseq.toUpperCase(Locale.ROOT));
845 if (command.oldds == null)
847 command.oldds = new SequenceI[command.seqs.length];
849 command.oldds[i] = command.seqs[i].getDatasetSequence();
851 // And preserve start/end for good-measure
853 if (command.oldStartEnd == null)
855 command.oldStartEnd = new Range[command.seqs.length];
857 command.oldStartEnd[i] = oldstartend;
858 // TODO: JAL-1131 ensure newly created dataset sequence is added to
860 // dataset sequences associated with the alignment.
861 // TODO: JAL-1131 fix up any annotation associated with new dataset
862 // sequence to ensure that original sequence/annotation
865 command.seqs[i].setDatasetSequence(newds);
869 if (command.oldStartEnd == null)
871 command.oldStartEnd = new Range[command.seqs.length];
873 command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
874 command.seqs[i].getEnd());
875 if (beforeEditedPositions != null
876 && afterEditedPositions == null)
878 // modification at end
879 command.seqs[i].setEnd(
880 beforeEditedPositions.getEnd() + nogaprep.length()
881 - nogapold.length());
883 else if (afterEditedPositions != null
884 && beforeEditedPositions == null)
886 // modification at start
887 command.seqs[i].setStart(
888 afterEditedPositions.getBegin() - nogaprep.length());
892 // edit covered both start and end. Here we can only guess the
895 String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
896 command.seqs[i].getSequenceAsString().toUpperCase(Locale.ROOT));
897 int newStart = command.seqs[i].getDatasetSequence()
898 .getSequenceAsString().indexOf(nogapalseq);
902 "Implementation Error: could not locate start/end "
903 + "in dataset sequence after an edit of the sequence string");
905 int newEnd = newStart + nogapalseq.length() - 1;
906 command.seqs[i].setStart(newStart);
907 command.seqs[i].setEnd(newEnd);
917 final static void adjustAnnotations(Edit command, boolean insert,
918 boolean modifyVisibility, AlignmentI[] views)
920 AlignmentAnnotation[] annotations = null;
922 if (modifyVisibility && !insert)
924 // only occurs if a sequence was added or deleted.
925 command.deletedAnnotationRows = new Hashtable<>();
927 if (command.fullAlignmentHeight)
929 annotations = command.al.getAlignmentAnnotation();
934 AlignmentAnnotation[] tmp;
935 for (int s = 0; s < command.seqs.length; s++)
937 command.seqs[s].sequenceChanged();
939 if (modifyVisibility)
941 // Rows are only removed or added to sequence object.
945 tmp = command.seqs[s].getAnnotation();
948 int alen = tmp.length;
949 for (int aa = 0; aa < tmp.length; aa++)
951 if (!command.al.deleteAnnotation(tmp[aa]))
953 // strip out annotation not in the current al (will be put
954 // back on insert in all views)
959 command.seqs[s].setAlignmentAnnotation(null);
960 if (alen != tmp.length)
962 // save the non-null annotation references only
963 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
964 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
968 saved[aapos++] = tmp[aa];
973 command.deletedAnnotationRows.put(command.seqs[s], saved);
974 // and then remove any annotation in the other views
975 for (int alview = 0; views != null
976 && alview < views.length; alview++)
978 if (views[alview] != command.al)
980 AlignmentAnnotation[] toremove = views[alview]
981 .getAlignmentAnnotation();
982 if (toremove == null || toremove.length == 0)
986 // remove any alignment annotation on this sequence that's
987 // on that alignment view.
988 for (int aa = 0; aa < toremove.length; aa++)
990 if (toremove[aa].sequenceRef == command.seqs[s])
992 views[alview].deleteAnnotation(toremove[aa]);
1000 // save all the annotation
1001 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1008 if (command.deletedAnnotationRows != null
1009 && command.deletedAnnotationRows
1010 .containsKey(command.seqs[s]))
1012 AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1013 .get(command.seqs[s]);
1014 command.seqs[s].setAlignmentAnnotation(revealed);
1015 if (revealed != null)
1017 for (int aa = 0; aa < revealed.length; aa++)
1019 // iterate through al adding original annotation
1020 command.al.addAnnotation(revealed[aa]);
1022 for (int aa = 0; aa < revealed.length; aa++)
1024 command.al.setAnnotationIndex(revealed[aa], aa);
1026 // and then duplicate added annotation on every other alignment
1028 for (int vnum = 0; views != null && vnum < views.length; vnum++)
1030 if (views[vnum] != command.al)
1032 int avwidth = views[vnum].getWidth() + 1;
1033 // duplicate in this view
1034 for (int a = 0; a < revealed.length; a++)
1036 AlignmentAnnotation newann = new AlignmentAnnotation(
1038 command.seqs[s].addAlignmentAnnotation(newann);
1039 newann.padAnnotation(avwidth);
1040 views[vnum].addAnnotation(newann);
1041 views[vnum].setAnnotationIndex(newann, a);
1051 if (command.seqs[s].getAnnotation() == null)
1058 annotations = command.seqs[s].getAnnotation();
1062 tmp = new AlignmentAnnotation[aSize
1063 + command.seqs[s].getAnnotation().length];
1065 System.arraycopy(annotations, 0, tmp, 0, aSize);
1067 System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1068 command.seqs[s].getAnnotation().length);
1072 aSize = annotations.length;
1076 if (annotations == null)
1083 command.deletedAnnotations = new Hashtable<>();
1088 for (int a = 0; a < annotations.length; a++)
1090 if (annotations[a].autoCalculated
1091 || annotations[a].annotations == null)
1098 aSize = annotations[a].annotations.length;
1101 temp = new Annotation[aSize + command.number];
1102 if (annotations[a].padGaps)
1104 for (int aa = 0; aa < temp.length; aa++)
1106 temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1112 if (command.position < aSize)
1114 if (command.position + command.number >= aSize)
1120 tSize = aSize - command.number;
1132 temp = new Annotation[tSize];
1137 if (command.position < annotations[a].annotations.length)
1139 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1142 if (command.deletedAnnotations != null
1143 && command.deletedAnnotations
1144 .containsKey(annotations[a].annotationId))
1146 Annotation[] restore = command.deletedAnnotations
1147 .get(annotations[a].annotationId);
1149 System.arraycopy(restore, 0, temp, command.position,
1154 System.arraycopy(annotations[a].annotations, command.position,
1155 temp, command.position + command.number,
1156 aSize - command.position);
1160 if (command.deletedAnnotations != null
1161 && command.deletedAnnotations
1162 .containsKey(annotations[a].annotationId))
1164 Annotation[] restore = command.deletedAnnotations
1165 .get(annotations[a].annotationId);
1167 temp = new Annotation[annotations[a].annotations.length
1169 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1170 annotations[a].annotations.length);
1171 System.arraycopy(restore, 0, temp,
1172 annotations[a].annotations.length, restore.length);
1176 temp = annotations[a].annotations;
1182 if (tSize != aSize || command.position < 2)
1184 int copylen = Math.min(command.position,
1185 annotations[a].annotations.length);
1188 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1189 copylen); // command.position);
1192 Annotation[] deleted = new Annotation[command.number];
1193 if (copylen >= command.position)
1195 copylen = Math.min(command.number,
1196 annotations[a].annotations.length - command.position);
1199 System.arraycopy(annotations[a].annotations, command.position,
1200 deleted, 0, copylen); // command.number);
1204 command.deletedAnnotations.put(annotations[a].annotationId,
1206 if (annotations[a].annotations.length > command.position
1209 System.arraycopy(annotations[a].annotations,
1210 command.position + command.number, temp,
1211 command.position, annotations[a].annotations.length
1212 - command.position - command.number); // aSize
1217 int dSize = aSize - command.position;
1221 Annotation[] deleted = new Annotation[command.number];
1222 System.arraycopy(annotations[a].annotations, command.position,
1225 command.deletedAnnotations.put(annotations[a].annotationId,
1228 tSize = Math.min(annotations[a].annotations.length,
1230 temp = new Annotation[tSize];
1231 System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1235 temp = annotations[a].annotations;
1240 annotations[a].annotations = temp;
1245 * Restores features to the state before a Cut.
1247 * <li>re-add any features deleted by the cut</li>
1248 * <li>remove any truncated features created by the cut</li>
1249 * <li>shift right any features to the right of the cut</li>
1255 * the sequence the Cut applied to
1257 * the start residue position of the cut
1259 * the number of residues cut
1260 * @param sameDatasetSequence
1261 * true if dataset sequence and frame of reference were left
1262 * unchanged by the Cut
1264 final static void undoCutFeatures(Edit command, SequenceI seq,
1265 final int start, final int length, boolean sameDatasetSequence)
1267 SequenceI sequence = seq.getDatasetSequence();
1268 if (sequence == null)
1274 * shift right features that lie to the right of the restored cut (but not
1275 * if dataset sequence unchanged - so coordinates were changed by Cut)
1277 if (!sameDatasetSequence)
1280 * shift right all features right of and not
1281 * contiguous with the cut position
1283 seq.getFeatures().shiftFeatures(start + 1, length);
1286 * shift right any features that start at the cut position,
1287 * unless they were truncated
1289 List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1291 for (SequenceFeature sf : sfs)
1293 if (sf.getBegin() == start)
1295 if (!command.truncatedFeatures.containsKey(seq)
1296 || !command.truncatedFeatures.get(seq).contains(sf))
1299 * feature was shifted left to cut position (not truncated),
1300 * so shift it back right
1302 SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
1303 + length, sf.getEnd() + length, sf.getFeatureGroup(),
1305 seq.addSequenceFeature(shifted);
1306 seq.deleteFeature(sf);
1313 * restore any features that were deleted or truncated
1315 if (command.deletedFeatures != null
1316 && command.deletedFeatures.containsKey(seq))
1318 for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1320 sequence.addSequenceFeature(deleted);
1325 * delete any truncated features
1327 if (command.truncatedFeatures != null
1328 && command.truncatedFeatures.containsKey(seq))
1330 for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1332 sequence.deleteFeature(amended);
1338 * Returns the list of edit commands wrapped by this object.
1342 public List<Edit> getEdits()
1348 * Returns a map whose keys are the dataset sequences, and values their
1349 * aligned sequences before the command edit list was applied. The aligned
1350 * sequences are copies, which may be updated without affecting the originals.
1352 * The command holds references to the aligned sequences (after editing). If
1353 * the command is an 'undo',then the prior state is simply the aligned state.
1354 * Otherwise, we have to derive the prior state by working backwards through
1355 * the edit list to infer the aligned sequences before editing.
1357 * Note: an alternative solution would be to cache the 'before' state of each
1358 * edit, but this would be expensive in space in the common case that the
1359 * original is never needed (edits are not mirrored).
1362 * @throws IllegalStateException
1363 * on detecting an edit command of a type that can't be unwound
1365 public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1367 Map<SequenceI, SequenceI> result = new HashMap<>();
1368 if (getEdits() == null)
1374 for (Edit e : getEdits())
1376 for (SequenceI seq : e.getSequences())
1378 SequenceI ds = seq.getDatasetSequence();
1379 // SequenceI preEdit = result.get(ds);
1380 if (!result.containsKey(ds))
1383 * copy sequence including start/end (but don't use copy constructor
1384 * as we don't need annotations)
1386 SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1387 seq.getStart(), seq.getEnd());
1388 preEdit.setDatasetSequence(ds);
1389 result.put(ds, preEdit);
1397 * Work backwards through the edit list, deriving the sequences before each
1398 * was applied. The final result is the sequence set before any edits.
1400 Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
1401 while (editList.hasNext())
1403 Edit oldEdit = editList.next();
1404 Action action = oldEdit.getAction();
1405 int position = oldEdit.getPosition();
1406 int number = oldEdit.getNumber();
1407 final char gap = oldEdit.getGapCharacter();
1408 for (SequenceI seq : oldEdit.getSequences())
1410 SequenceI ds = seq.getDatasetSequence();
1411 SequenceI preEdit = result.get(ds);
1412 if (preEdit == null)
1414 preEdit = new Sequence("", seq.getSequenceAsString(),
1415 seq.getStart(), seq.getEnd());
1416 preEdit.setDatasetSequence(ds);
1417 result.put(ds, preEdit);
1420 * 'Undo' this edit action on the sequence (updating the value in the
1425 if (action == Action.DELETE_GAP)
1427 preEdit.setSequence(new String(StringUtils.insertCharAt(
1428 preEdit.getSequence(), position, number, gap)));
1430 else if (action == Action.INSERT_GAP)
1432 preEdit.setSequence(new String(StringUtils.deleteChars(
1433 preEdit.getSequence(), position, position + number)));
1437 System.err.println("Can't undo edit action " + action);
1438 // throw new IllegalStateException("Can't undo edit action " +
1452 * start and end of sequence prior to edit
1454 Range[] oldStartEnd;
1456 boolean fullAlignmentHeight = false;
1458 Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1460 Map<String, Annotation[]> deletedAnnotations;
1463 * features deleted by the cut (re-add on Undo)
1464 * (including the original of any shortened features)
1466 Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1469 * shortened features added by the cut (delete on Undo)
1471 Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1475 final Action command;
1490 * flag that identifies edits inserted to balance
1491 * user edits in a 'locked editing' region
1493 private boolean systemGenerated;
1495 public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1500 this.position = pos;
1501 this.number = count;
1505 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1508 this(cmd, sqs, pos, count, align.getGapCharacter());
1512 alIndex = new int[sqs.length];
1513 for (int i = 0; i < sqs.length; i++)
1515 alIndex[i] = align.findIndex(sqs[i]);
1518 fullAlignmentHeight = (align.getHeight() == sqs.length);
1522 * Constructor given a REPLACE command and the replacement string
1531 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1532 AlignmentI align, String replace)
1534 this(cmd, sqs, pos, count, align);
1536 string = new char[sqs.length][];
1537 for (int i = 0; i < sqs.length; i++)
1539 string[i] = replace.toCharArray();
1543 public SequenceI[] getSequences()
1548 public int getPosition()
1553 public Action getAction()
1558 public int getNumber()
1563 public char getGapCharacter()
1568 public void setSystemGenerated(boolean b)
1570 systemGenerated = b;
1573 public boolean isSystemGenerated()
1575 return systemGenerated;
1580 * Returns an iterator over the list of edit commands which traverses the list
1581 * either forwards or backwards.
1586 public Iterator<Edit> getEditIterator(boolean forwards)
1590 return getEdits().iterator();
1594 return new ReverseListIterator<>(getEdits());
1599 * Adjusts features for Cut, and saves details of changes made to allow Undo
1601 * <li>features left of the cut are unchanged</li>
1602 * <li>features right of the cut are shifted left</li>
1603 * <li>features internal to the cut region are deleted</li>
1604 * <li>features that overlap or span the cut are shortened</li>
1605 * <li>the originals of any deleted or shortened features are saved, to re-add
1607 * <li>any added (shortened) features are saved, to delete on Undo</li>
1612 * @param fromPosition
1614 * @param cutIsInternal
1616 protected static void cutFeatures(Edit command, SequenceI seq,
1617 int fromPosition, int toPosition, boolean cutIsInternal)
1620 * if the cut is at start or end of sequence
1621 * then we don't modify the sequence feature store
1627 List<SequenceFeature> added = new ArrayList<>();
1628 List<SequenceFeature> removed = new ArrayList<>();
1630 SequenceFeaturesI featureStore = seq.getFeatures();
1631 if (toPosition < fromPosition || featureStore == null)
1636 int cutStartPos = fromPosition;
1637 int cutEndPos = toPosition;
1638 int cutWidth = cutEndPos - cutStartPos + 1;
1640 synchronized (featureStore)
1643 * get features that overlap the cut region
1645 List<SequenceFeature> toAmend = featureStore.findFeatures(
1646 cutStartPos, cutEndPos);
1649 * add any contact features that span the cut region
1650 * (not returned by findFeatures)
1652 for (SequenceFeature contact : featureStore.getContactFeatures())
1654 if (contact.getBegin() < cutStartPos
1655 && contact.getEnd() > cutEndPos)
1657 toAmend.add(contact);
1662 * adjust start-end of overlapping features;
1663 * delete features enclosed by the cut;
1664 * delete partially overlapping contact features
1666 for (SequenceFeature sf : toAmend)
1668 int sfBegin = sf.getBegin();
1669 int sfEnd = sf.getEnd();
1670 int newBegin = sfBegin;
1672 boolean toDelete = false;
1673 boolean follows = false;
1675 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1678 * feature lies within cut region - delete it
1682 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1685 * feature spans cut region - left-shift the end
1689 else if (sfEnd <= cutEndPos)
1692 * feature overlaps left of cut region - truncate right
1694 newEnd = cutStartPos - 1;
1695 if (sf.isContactFeature())
1700 else if (sfBegin >= cutStartPos)
1703 * remaining case - feature overlaps right
1704 * truncate left, adjust end of feature
1706 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1707 newEnd = newBegin + sfEnd - cutEndPos - 1;
1708 if (sf.isContactFeature())
1714 seq.deleteFeature(sf);
1721 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1722 sf.getFeatureGroup(), sf.getScore());
1723 seq.addSequenceFeature(copy);
1732 * and left shift any features lying to the right of the cut region
1735 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1739 * save deleted and amended features, so that Undo can
1740 * re-add or delete them respectively
1742 if (command.deletedFeatures == null)
1744 command.deletedFeatures = new HashMap<>();
1746 if (command.truncatedFeatures == null)
1748 command.truncatedFeatures = new HashMap<>();
1750 command.deletedFeatures.put(seq, removed);
1751 command.truncatedFeatures.put(seq, added);