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 // jalview.bin.Console.outPrintln("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 // jalview.bin.Console.outPrintln("pos: "+command.position+" number:
490 // "+command.number);
491 // command.seqs[s].insertCharAt(command.position, command.number,'A');
494 // adjustAnnotations(command, true, false, null);
498 * Delete gap(s) in sequences as specified by the command, and adjust
503 final static private void deleteGap(Edit command)
505 for (int s = 0; s < command.seqs.length; s++)
507 command.seqs[s].deleteChars(command.position,
508 command.position + command.number);
511 adjustAnnotations(command, false, false, null);
515 * Carry out a Cut action. The cut characters are saved in case Undo is
521 static void cut(Edit command, AlignmentI[] views)
523 boolean seqDeleted = false;
524 command.string = new char[command.seqs.length][];
526 for (int i = 0; i < command.seqs.length; i++)
528 final SequenceI sequence = command.seqs[i];
529 if (sequence.getLength() > command.position)
531 command.string[i] = sequence.getSequence(command.position,
532 command.position + command.number);
533 SequenceI oldds = sequence.getDatasetSequence();
534 ContiguousI cutPositions = sequence.findPositions(
535 command.position + 1, command.position + command.number);
536 boolean cutIsInternal = cutPositions != null
537 && sequence.getStart() != cutPositions.getBegin()
538 && 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,
546 command.position + command.number);
548 if (command.oldds != null && command.oldds[i] != null)
551 * we are Redoing a Cut, or Undoing a Paste - so
552 * oldds entry contains the cut dataset sequence,
553 * with sequence features in expected place
555 sequence.setDatasetSequence(command.oldds[i]);
556 command.oldds[i] = oldds;
561 * new cut operation: save the dataset sequence
562 * so it can be restored in an Undo
564 if (command.oldds == null)
566 command.oldds = new SequenceI[command.seqs.length];
568 command.oldds[i] = oldds;// todo not if !cutIsInternal?
570 // do we need to edit sequence features for new sequence ?
571 if (oldds != sequence.getDatasetSequence() || (cutIsInternal
572 && sequence.getFeatures().hasFeatures()))
573 // todo or just test cutIsInternal && cutPositions != null ?
575 if (cutPositions != null)
577 cutFeatures(command, sequence, cutPositions.getBegin(),
578 cutPositions.getEnd(), cutIsInternal);
582 SequenceI newDs = sequence.getDatasetSequence();
583 if (newDs != ds && command.al != null
584 && command.al.getDataset() != null
585 && !command.al.getDataset().getSequences().contains(newDs))
587 command.al.getDataset().addSequence(newDs);
591 if (sequence.getLength() < 1)
593 command.al.deleteSequence(sequence);
598 adjustAnnotations(command, false, seqDeleted, views);
602 * Perform the given Paste command. This may be to add cut or copied sequences
603 * to an alignment, or to undo a 'Cut' action on a region of the alignment.
608 static void paste(Edit command, AlignmentI[] views)
610 boolean seqWasDeleted = false;
612 for (int i = 0; i < command.seqs.length; i++)
614 boolean newDSNeeded = false;
615 boolean newDSWasNeeded = command.oldds != null
616 && command.oldds[i] != null;
617 SequenceI sequence = command.seqs[i];
618 if (sequence.getLength() < 1)
621 * sequence was deleted; re-add it to the alignment
623 if (command.alIndex[i] < command.al.getHeight())
625 List<SequenceI> sequences = command.al.getSequences();
626 synchronized (sequences)
628 if (!(command.alIndex[i] < 0))
630 sequences.add(command.alIndex[i], sequence);
636 command.al.addSequence(sequence);
638 seqWasDeleted = true;
640 int newStart = sequence.getStart();
641 int newEnd = sequence.getEnd();
643 StringBuilder tmp = new StringBuilder();
644 tmp.append(sequence.getSequence());
645 // Undo of a delete does not replace original dataset sequence on to
646 // alignment sequence.
651 if (command.string != null && command.string[i] != null)
653 if (command.position >= tmp.length())
655 // This occurs if padding is on, and residues
656 // are removed from end of alignment
657 int len = command.position - tmp.length();
660 tmp.append(command.gapChar);
664 tmp.insert(command.position, command.string[i]);
665 for (int s = 0; s < command.string[i].length; s++)
667 if (!Comparison.isGap(command.string[i][s]))
673 start = sequence.findPosition(command.position);
675 // .findPosition(command.position + command.number);
677 if (sequence.getStart() == start)
687 command.string[i] = null;
690 sequence.setSequence(tmp.toString());
691 sequence.setStart(newStart);
692 sequence.setEnd(newEnd);
695 * command and Undo share the same dataset sequence if cut was
696 * at start or end of sequence
698 boolean sameDatasetSequence = false;
701 if (sequence.getDatasetSequence() != null)
706 ds = command.oldds[i];
710 // make a new DS sequence
711 // use new ds mechanism here
712 String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
713 sequence.getSequenceAsString());
714 ds = new Sequence(sequence.getName(), ungapped,
715 sequence.getStart(), sequence.getEnd());
716 ds.setDescription(sequence.getDescription());
718 if (command.oldds == null)
720 command.oldds = new SequenceI[command.seqs.length];
722 command.oldds[i] = sequence.getDatasetSequence();
723 sameDatasetSequence = ds == sequence.getDatasetSequence();
724 ds.setSequenceFeatures(sequence.getSequenceFeatures());
725 if (!sameDatasetSequence && command.al.getDataset() != null)
727 // delete 'undone' sequence from alignment dataset
728 command.al.getDataset()
729 .deleteSequence(sequence.getDatasetSequence());
731 sequence.setDatasetSequence(ds);
733 undoCutFeatures(command, command.seqs[i], start, length,
734 sameDatasetSequence);
737 adjustAnnotations(command, true, seqWasDeleted, views);
739 command.string = null;
742 static void replace(Edit command)
746 int start = command.position;
747 int end = command.number;
748 // TODO TUTORIAL - Fix for replacement with different length of sequence (or
750 // TODO Jalview 2.4 bugfix change to an aggregate command - original
751 // sequence string is cut, new string is pasted in.
752 command.number = start + command.string[0].length;
753 for (int i = 0; i < command.seqs.length; i++)
755 boolean newDSWasNeeded = command.oldds != null
756 && command.oldds[i] != null;
757 boolean newStartEndWasNeeded = command.oldStartEnd != null
758 && command.oldStartEnd[i] != null;
761 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
762 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
763 * viewport.alignment));
767 * then addHistoryItem(new EditCommand( "Add sequences",
768 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
771 ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
773 ContiguousI afterEditedPositions = command.seqs[i]
774 .findPositions(end + 1, command.seqs[i].getLength());
776 oldstring = command.seqs[i].getSequenceAsString();
777 tmp = new StringBuilder(oldstring.substring(0, start));
778 tmp.append(command.string[i]);
779 String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
780 new String(command.string[i]));
781 if (end < oldstring.length())
783 tmp.append(oldstring.substring(end));
785 // stash end prior to updating the sequence object so we can save it if
787 Range oldstartend = new Range(command.seqs[i].getStart(),
788 command.seqs[i].getEnd());
789 command.seqs[i].setSequence(tmp.toString());
790 command.string[i] = oldstring
791 .substring(start, Math.min(end, oldstring.length()))
793 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
794 new String(command.string[i]));
796 if (!nogaprep.toLowerCase(Locale.ROOT)
797 .equals(nogapold.toLowerCase(Locale.ROOT)))
799 // we may already have dataset and limits stashed...
800 if (newDSWasNeeded || newStartEndWasNeeded)
804 // then just switch the dataset sequence
805 SequenceI oldds = command.seqs[i].getDatasetSequence();
806 command.seqs[i].setDatasetSequence(command.oldds[i]);
807 command.oldds[i] = oldds;
809 if (newStartEndWasNeeded)
811 Range newStart = command.oldStartEnd[i];
812 command.oldStartEnd[i] = oldstartend;
813 command.seqs[i].setStart(newStart.getBegin());
814 command.seqs[i].setEnd(newStart.getEnd());
819 // decide if we need a new dataset sequence or modify start/end
820 // first edit the original dataset sequence string
821 SequenceI oldds = command.seqs[i].getDatasetSequence();
822 String osp = oldds.getSequenceAsString();
823 int beforeStartOfEdit = -oldds.getStart() + 1
824 + (beforeEditedPositions == null
825 ? ((afterEditedPositions != null)
826 ? afterEditedPositions.getBegin() - 1
827 : oldstartend.getBegin()
829 : beforeEditedPositions.getEnd());
830 int afterEndOfEdit = -oldds.getStart() + 1
831 + ((afterEditedPositions == null) ? oldstartend.getEnd()
832 : afterEditedPositions.getBegin() - 1);
833 String fullseq = osp.substring(0, beforeStartOfEdit) + nogaprep
834 + osp.substring(afterEndOfEdit);
836 // and check if new sequence data is different..
837 if (!fullseq.equalsIgnoreCase(osp))
839 // old ds and edited ds are different, so
840 // create the new dataset sequence
841 SequenceI newds = new Sequence(oldds);
842 newds.setSequence(fullseq.toUpperCase(Locale.ROOT));
844 if (command.oldds == null)
846 command.oldds = new SequenceI[command.seqs.length];
848 command.oldds[i] = command.seqs[i].getDatasetSequence();
850 // And preserve start/end for good-measure
852 if (command.oldStartEnd == null)
854 command.oldStartEnd = new Range[command.seqs.length];
856 command.oldStartEnd[i] = oldstartend;
857 // TODO: JAL-1131 ensure newly created dataset sequence is added to
859 // dataset sequences associated with the alignment.
860 // TODO: JAL-1131 fix up any annotation associated with new dataset
861 // sequence to ensure that original sequence/annotation
864 command.seqs[i].setDatasetSequence(newds);
868 if (command.oldStartEnd == null)
870 command.oldStartEnd = new Range[command.seqs.length];
872 command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
873 command.seqs[i].getEnd());
874 if (beforeEditedPositions != null
875 && afterEditedPositions == null)
877 // modification at end
878 command.seqs[i].setEnd(beforeEditedPositions.getEnd()
879 + nogaprep.length() - nogapold.length());
881 else if (afterEditedPositions != null
882 && beforeEditedPositions == null)
884 // modification at start
885 command.seqs[i].setStart(
886 afterEditedPositions.getBegin() - nogaprep.length());
890 // edit covered both start and end. Here we can only guess the
893 String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
894 command.seqs[i].getSequenceAsString()
895 .toUpperCase(Locale.ROOT));
896 int newStart = command.seqs[i].getDatasetSequence()
897 .getSequenceAsString().indexOf(nogapalseq);
901 "Implementation Error: could not locate start/end "
902 + "in dataset sequence after an edit of the sequence string");
904 int newEnd = newStart + nogapalseq.length() - 1;
905 command.seqs[i].setStart(newStart);
906 command.seqs[i].setEnd(newEnd);
916 final static void adjustAnnotations(Edit command, boolean insert,
917 boolean modifyVisibility, AlignmentI[] views)
919 AlignmentAnnotation[] annotations = null;
921 if (modifyVisibility && !insert)
923 // only occurs if a sequence was added or deleted.
924 command.deletedAnnotationRows = new Hashtable<>();
926 if (command.fullAlignmentHeight)
928 annotations = command.al.getAlignmentAnnotation();
933 AlignmentAnnotation[] tmp;
934 for (int s = 0; s < command.seqs.length; s++)
936 command.seqs[s].sequenceChanged();
938 if (modifyVisibility)
940 // Rows are only removed or added to sequence object.
944 tmp = command.seqs[s].getAnnotation();
947 int alen = tmp.length;
948 for (int aa = 0; aa < tmp.length; aa++)
950 if (!command.al.deleteAnnotation(tmp[aa]))
952 // strip out annotation not in the current al (will be put
953 // back on insert in all views)
958 command.seqs[s].setAlignmentAnnotation(null);
959 if (alen != tmp.length)
961 // save the non-null annotation references only
962 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
963 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
967 saved[aapos++] = tmp[aa];
972 command.deletedAnnotationRows.put(command.seqs[s], saved);
973 // and then remove any annotation in the other views
974 for (int alview = 0; views != null
975 && alview < views.length; alview++)
977 if (views[alview] != command.al)
979 AlignmentAnnotation[] toremove = views[alview]
980 .getAlignmentAnnotation();
981 if (toremove == null || toremove.length == 0)
985 // remove any alignment annotation on this sequence that's
986 // on that alignment view.
987 for (int aa = 0; aa < toremove.length; aa++)
989 if (toremove[aa].sequenceRef == command.seqs[s])
991 views[alview].deleteAnnotation(toremove[aa]);
999 // save all the annotation
1000 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1007 if (command.deletedAnnotationRows != null
1008 && command.deletedAnnotationRows
1009 .containsKey(command.seqs[s]))
1011 AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1012 .get(command.seqs[s]);
1013 command.seqs[s].setAlignmentAnnotation(revealed);
1014 if (revealed != null)
1016 for (int aa = 0; aa < revealed.length; aa++)
1018 // iterate through al adding original annotation
1019 command.al.addAnnotation(revealed[aa]);
1021 for (int aa = 0; aa < revealed.length; aa++)
1023 command.al.setAnnotationIndex(revealed[aa], aa);
1025 // and then duplicate added annotation on every other alignment
1027 for (int vnum = 0; views != null
1028 && 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,
1303 sf.getBegin() + length, sf.getEnd() + length,
1304 sf.getFeatureGroup(), sf.getScore());
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)));
1438 .errPrintln("Can't undo edit action " + action);
1439 // throw new IllegalStateException("Can't undo edit action " +
1453 * start and end of sequence prior to edit
1455 Range[] oldStartEnd;
1457 boolean fullAlignmentHeight = false;
1459 Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1461 Map<String, Annotation[]> deletedAnnotations;
1464 * features deleted by the cut (re-add on Undo)
1465 * (including the original of any shortened features)
1467 Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1470 * shortened features added by the cut (delete on Undo)
1472 Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1476 final Action command;
1491 * flag that identifies edits inserted to balance
1492 * user edits in a 'locked editing' region
1494 private boolean systemGenerated;
1496 public Edit(Action cmd, SequenceI[] sqs, int pos, int count, char gap)
1500 this.position = pos;
1501 this.number = count;
1505 Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align)
1507 this(cmd, sqs, pos, count, align.getGapCharacter());
1511 alIndex = new int[sqs.length];
1512 for (int i = 0; i < sqs.length; i++)
1514 alIndex[i] = align.findIndex(sqs[i]);
1517 fullAlignmentHeight = (align.getHeight() == sqs.length);
1521 * Constructor given a REPLACE command and the replacement string
1530 Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align,
1533 this(cmd, sqs, pos, count, align);
1535 string = new char[sqs.length][];
1536 for (int i = 0; i < sqs.length; i++)
1538 string[i] = replace.toCharArray();
1542 public SequenceI[] getSequences()
1547 public int getPosition()
1552 public Action getAction()
1557 public int getNumber()
1562 public char getGapCharacter()
1567 public void setSystemGenerated(boolean b)
1569 systemGenerated = b;
1572 public boolean isSystemGenerated()
1574 return systemGenerated;
1579 * Returns an iterator over the list of edit commands which traverses the list
1580 * either forwards or backwards.
1585 public Iterator<Edit> getEditIterator(boolean forwards)
1589 return getEdits().iterator();
1593 return new ReverseListIterator<>(getEdits());
1598 * Adjusts features for Cut, and saves details of changes made to allow Undo
1600 * <li>features left of the cut are unchanged</li>
1601 * <li>features right of the cut are shifted left</li>
1602 * <li>features internal to the cut region are deleted</li>
1603 * <li>features that overlap or span the cut are shortened</li>
1604 * <li>the originals of any deleted or shortened features are saved, to re-add
1606 * <li>any added (shortened) features are saved, to delete on Undo</li>
1611 * @param fromPosition
1613 * @param cutIsInternal
1615 protected static void cutFeatures(Edit command, SequenceI seq,
1616 int fromPosition, int toPosition, boolean cutIsInternal)
1619 * if the cut is at start or end of sequence
1620 * then we don't modify the sequence feature store
1626 List<SequenceFeature> added = new ArrayList<>();
1627 List<SequenceFeature> removed = new ArrayList<>();
1629 SequenceFeaturesI featureStore = seq.getFeatures();
1630 if (toPosition < fromPosition || featureStore == null)
1635 int cutStartPos = fromPosition;
1636 int cutEndPos = toPosition;
1637 int cutWidth = cutEndPos - cutStartPos + 1;
1639 synchronized (featureStore)
1642 * get features that overlap the cut region
1644 List<SequenceFeature> toAmend = featureStore.findFeatures(cutStartPos,
1648 * add any contact features that span the cut region
1649 * (not returned by findFeatures)
1651 for (SequenceFeature contact : featureStore.getContactFeatures())
1653 if (contact.getBegin() < cutStartPos
1654 && contact.getEnd() > cutEndPos)
1656 toAmend.add(contact);
1661 * adjust start-end of overlapping features;
1662 * delete features enclosed by the cut;
1663 * delete partially overlapping contact features
1665 for (SequenceFeature sf : toAmend)
1667 int sfBegin = sf.getBegin();
1668 int sfEnd = sf.getEnd();
1669 int newBegin = sfBegin;
1671 boolean toDelete = false;
1672 boolean follows = false;
1674 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1677 * feature lies within cut region - delete it
1681 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1684 * feature spans cut region - left-shift the end
1688 else if (sfEnd <= cutEndPos)
1691 * feature overlaps left of cut region - truncate right
1693 newEnd = cutStartPos - 1;
1694 if (sf.isContactFeature())
1699 else if (sfBegin >= cutStartPos)
1702 * remaining case - feature overlaps right
1703 * truncate left, adjust end of feature
1705 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1706 newEnd = newBegin + sfEnd - cutEndPos - 1;
1707 if (sf.isContactFeature())
1713 seq.deleteFeature(sf);
1720 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1721 sf.getFeatureGroup(), sf.getScore());
1722 seq.addSequenceFeature(copy);
1731 * and left shift any features lying to the right of the cut region
1734 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1738 * save deleted and amended features, so that Undo can
1739 * re-add or delete them respectively
1741 if (command.deletedFeatures == null)
1743 command.deletedFeatures = new HashMap<>();
1745 if (command.truncatedFeatures == null)
1747 command.truncatedFeatures = new HashMap<>();
1749 command.deletedFeatures.put(seq, removed);
1750 command.truncatedFeatures.put(seq, added);