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<>();
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 Range cutPositions = sequence.findPositions(command.position + 1,
530 command.position + command.number);
531 boolean cutIsInternal = cutPositions != null
532 && sequence.getStart() != cutPositions
533 .getBegin() && sequence.getEnd() != cutPositions.getEnd();
536 * perform the cut; if this results in a new dataset sequence, add
537 * that to the alignment dataset
539 SequenceI ds = sequence.getDatasetSequence();
540 sequence.deleteChars(command.position, command.position
543 if (command.oldds != null && command.oldds[i] != null)
546 * we are Redoing a Cut, or Undoing a Paste - so
547 * oldds entry contains the cut dataset sequence,
548 * with sequence features in expected place
550 sequence.setDatasetSequence(command.oldds[i]);
551 command.oldds[i] = oldds;
556 * new cut operation: save the dataset sequence
557 * so it can be restored in an Undo
559 if (command.oldds == null)
561 command.oldds = new SequenceI[command.seqs.length];
563 command.oldds[i] = oldds;// todo not if !cutIsInternal?
565 // do we need to edit sequence features for new sequence ?
566 if (oldds != sequence.getDatasetSequence()
568 && sequence.getFeatures().hasFeatures()))
569 // todo or just test cutIsInternal && cutPositions != null ?
571 if (cutPositions != null)
573 cutFeatures(command, sequence, cutPositions.getBegin(),
574 cutPositions.getEnd(), cutIsInternal);
578 SequenceI newDs = sequence.getDatasetSequence();
579 if (newDs != ds && command.al != null
580 && command.al.getDataset() != null
581 && !command.al.getDataset().getSequences().contains(newDs))
583 command.al.getDataset().addSequence(newDs);
587 if (sequence.getLength() < 1)
589 command.al.deleteSequence(sequence);
594 adjustAnnotations(command, false, seqDeleted, views);
598 * Perform the given Paste command. This may be to add cut or copied sequences
599 * to an alignment, or to undo a 'Cut' action on a region of the alignment.
604 static void paste(Edit command, AlignmentI[] views)
606 boolean seqWasDeleted = false;
608 for (int i = 0; i < command.seqs.length; i++)
610 boolean newDSNeeded = false;
611 boolean newDSWasNeeded = command.oldds != null
612 && command.oldds[i] != null;
613 SequenceI sequence = command.seqs[i];
614 if (sequence.getLength() < 1)
617 * sequence was deleted; re-add it to the alignment
619 if (command.alIndex[i] < command.al.getHeight())
621 List<SequenceI> sequences = command.al.getSequences();
622 synchronized (sequences)
624 if (!(command.alIndex[i] < 0))
626 sequences.add(command.alIndex[i], sequence);
632 command.al.addSequence(sequence);
634 seqWasDeleted = true;
636 int newStart = sequence.getStart();
637 int newEnd = sequence.getEnd();
639 StringBuilder tmp = new StringBuilder();
640 tmp.append(sequence.getSequence());
641 // Undo of a delete does not replace original dataset sequence on to
642 // alignment sequence.
647 if (command.string != null && command.string[i] != null)
649 if (command.position >= tmp.length())
651 // This occurs if padding is on, and residues
652 // are removed from end of alignment
653 int len = command.position - tmp.length();
656 tmp.append(command.gapChar);
660 tmp.insert(command.position, command.string[i]);
661 for (int s = 0; s < command.string[i].length; s++)
663 if (!Comparison.isGap(command.string[i][s]))
669 start = sequence.findPosition(command.position);
671 // .findPosition(command.position + command.number);
673 if (sequence.getStart() == start)
683 command.string[i] = null;
686 sequence.setSequence(tmp.toString());
687 sequence.setStart(newStart);
688 sequence.setEnd(newEnd);
691 * command and Undo share the same dataset sequence if cut was
692 * at start or end of sequence
694 boolean sameDatasetSequence = false;
697 if (sequence.getDatasetSequence() != null)
702 ds = command.oldds[i];
706 // make a new DS sequence
707 // use new ds mechanism here
708 String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
709 sequence.getSequenceAsString());
710 ds = new Sequence(sequence.getName(), ungapped,
711 sequence.getStart(), sequence.getEnd());
712 ds.setDescription(sequence.getDescription());
714 if (command.oldds == null)
716 command.oldds = new SequenceI[command.seqs.length];
718 command.oldds[i] = sequence.getDatasetSequence();
719 sameDatasetSequence = ds == sequence.getDatasetSequence();
720 ds.setSequenceFeatures(sequence.getSequenceFeatures());
721 if (!sameDatasetSequence && command.al.getDataset() != null)
723 // delete 'undone' sequence from alignment dataset
724 command.al.getDataset()
725 .deleteSequence(sequence.getDatasetSequence());
727 sequence.setDatasetSequence(ds);
729 undoCutFeatures(command, command.seqs[i], start, length,
730 sameDatasetSequence);
733 adjustAnnotations(command, true, seqWasDeleted, views);
735 command.string = null;
738 static void replace(Edit command)
742 int start = command.position;
743 int end = command.number;
744 // TODO TUTORIAL - Fix for replacement with different length of sequence (or
746 // TODO Jalview 2.4 bugfix change to an aggregate command - original
747 // sequence string is cut, new string is pasted in.
748 command.number = start + command.string[0].length;
749 for (int i = 0; i < command.seqs.length; i++)
751 boolean newDSWasNeeded = command.oldds != null
752 && command.oldds[i] != null;
753 boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
756 * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
757 * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
758 * viewport.alignment));
762 * then addHistoryItem(new EditCommand( "Add sequences",
763 * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
767 Range beforeEditedPositions = command.seqs[i].findPositions(1, start);
768 Range afterEditedPositions = command.seqs[i]
769 .findPositions(end + 1, command.seqs[i].getLength());
771 oldstring = command.seqs[i].getSequenceAsString();
772 tmp = new StringBuilder(oldstring.substring(0, start));
773 tmp.append(command.string[i]);
774 String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
775 new String(command.string[i]));
776 if (end < oldstring.length())
778 tmp.append(oldstring.substring(end));
780 // stash end prior to updating the sequence object so we can save it if
782 Range oldstartend = new Range(command.seqs[i].getStart(),
783 command.seqs[i].getEnd());
784 command.seqs[i].setSequence(tmp.toString());
785 command.string[i] = oldstring
786 .substring(start, Math.min(end, oldstring.length()))
788 String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
789 new String(command.string[i]));
791 if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
793 // we may already have dataset and limits stashed...
794 if (newDSWasNeeded || newStartEndWasNeeded)
798 // then just switch the dataset sequence
799 SequenceI oldds = command.seqs[i].getDatasetSequence();
800 command.seqs[i].setDatasetSequence(command.oldds[i]);
801 command.oldds[i] = oldds;
803 if (newStartEndWasNeeded)
805 Range newStart = command.oldStartEnd[i];
806 command.oldStartEnd[i] = oldstartend;
807 command.seqs[i].setStart(newStart.getBegin());
808 command.seqs[i].setEnd(newStart.getEnd());
813 // decide if we need a new dataset sequence or modify start/end
814 // first edit the original dataset sequence string
815 SequenceI oldds = command.seqs[i].getDatasetSequence();
816 String osp = oldds.getSequenceAsString();
817 int beforeStartOfEdit = -oldds.getStart() + 1
818 + (beforeEditedPositions == null
819 ? ((afterEditedPositions != null)
820 ? afterEditedPositions.getBegin() - 1
821 : oldstartend.getBegin()
823 : beforeEditedPositions.getEnd()
825 int afterEndOfEdit = -oldds.getStart() + 1
826 + ((afterEditedPositions == null)
827 ? oldstartend.getEnd()
828 : afterEditedPositions.getBegin() - 1);
829 String fullseq = osp.substring(0,
832 + osp.substring(afterEndOfEdit);
834 // and check if new sequence data is different..
835 if (!fullseq.equalsIgnoreCase(osp))
837 // old ds and edited ds are different, so
838 // create the new dataset sequence
839 SequenceI newds = new Sequence(oldds);
840 newds.setSequence(fullseq);
842 if (command.oldds == null)
844 command.oldds = new SequenceI[command.seqs.length];
846 command.oldds[i] = command.seqs[i].getDatasetSequence();
848 // And preserve start/end for good-measure
850 if (command.oldStartEnd == null)
852 command.oldStartEnd = new Range[command.seqs.length];
854 command.oldStartEnd[i] = oldstartend;
855 // TODO: JAL-1131 ensure newly created dataset sequence is added to
857 // dataset sequences associated with the alignment.
858 // TODO: JAL-1131 fix up any annotation associated with new dataset
859 // sequence to ensure that original sequence/annotation
862 command.seqs[i].setDatasetSequence(newds);
866 if (command.oldStartEnd == null)
868 command.oldStartEnd = new Range[command.seqs.length];
870 command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
871 command.seqs[i].getEnd());
872 if (beforeEditedPositions != null
873 && afterEditedPositions == null)
875 // modification at end
876 command.seqs[i].setEnd(
877 beforeEditedPositions.getEnd() + nogaprep.length()
878 - nogapold.length());
880 else if (afterEditedPositions != null
881 && beforeEditedPositions == null)
883 // modification at start
884 command.seqs[i].setStart(
885 afterEditedPositions.getBegin() - nogaprep.length());
889 // edit covered both start and end. Here we can only guess the
892 String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
893 command.seqs[i].getSequenceAsString().toUpperCase());
894 int newStart = command.seqs[i].getDatasetSequence()
895 .getSequenceAsString().indexOf(nogapalseq);
899 "Implementation Error: could not locate start/end "
900 + "in dataset sequence after an edit of the sequence string");
902 int newEnd = newStart + nogapalseq.length() - 1;
903 command.seqs[i].setStart(newStart);
904 command.seqs[i].setEnd(newEnd);
914 final static void adjustAnnotations(Edit command, boolean insert,
915 boolean modifyVisibility, AlignmentI[] views)
917 AlignmentAnnotation[] annotations = null;
919 if (modifyVisibility && !insert)
921 // only occurs if a sequence was added or deleted.
922 command.deletedAnnotationRows = new Hashtable<>();
924 if (command.fullAlignmentHeight)
926 annotations = command.al.getAlignmentAnnotation();
931 AlignmentAnnotation[] tmp;
932 for (int s = 0; s < command.seqs.length; s++)
934 command.seqs[s].sequenceChanged();
936 if (modifyVisibility)
938 // Rows are only removed or added to sequence object.
942 tmp = command.seqs[s].getAnnotation();
945 int alen = tmp.length;
946 for (int aa = 0; aa < tmp.length; aa++)
948 if (!command.al.deleteAnnotation(tmp[aa]))
950 // strip out annotation not in the current al (will be put
951 // back on insert in all views)
956 command.seqs[s].setAlignmentAnnotation(null);
957 if (alen != tmp.length)
959 // save the non-null annotation references only
960 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
961 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
965 saved[aapos++] = tmp[aa];
970 command.deletedAnnotationRows.put(command.seqs[s], saved);
971 // and then remove any annotation in the other views
972 for (int alview = 0; views != null
973 && alview < views.length; alview++)
975 if (views[alview] != command.al)
977 AlignmentAnnotation[] toremove = views[alview]
978 .getAlignmentAnnotation();
979 if (toremove == null || toremove.length == 0)
983 // remove any alignment annotation on this sequence that's
984 // on that alignment view.
985 for (int aa = 0; aa < toremove.length; aa++)
987 if (toremove[aa].sequenceRef == command.seqs[s])
989 views[alview].deleteAnnotation(toremove[aa]);
997 // save all the annotation
998 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1005 if (command.deletedAnnotationRows != null
1006 && command.deletedAnnotationRows
1007 .containsKey(command.seqs[s]))
1009 AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1010 .get(command.seqs[s]);
1011 command.seqs[s].setAlignmentAnnotation(revealed);
1012 if (revealed != null)
1014 for (int aa = 0; aa < revealed.length; aa++)
1016 // iterate through al adding original annotation
1017 command.al.addAnnotation(revealed[aa]);
1019 for (int aa = 0; aa < revealed.length; aa++)
1021 command.al.setAnnotationIndex(revealed[aa], aa);
1023 // and then duplicate added annotation on every other alignment
1025 for (int vnum = 0; views != null && vnum < views.length; vnum++)
1027 if (views[vnum] != command.al)
1029 int avwidth = views[vnum].getWidth() + 1;
1030 // duplicate in this view
1031 for (int a = 0; a < revealed.length; a++)
1033 AlignmentAnnotation newann = new AlignmentAnnotation(
1035 command.seqs[s].addAlignmentAnnotation(newann);
1036 newann.padAnnotation(avwidth);
1037 views[vnum].addAnnotation(newann);
1038 views[vnum].setAnnotationIndex(newann, a);
1048 if (command.seqs[s].getAnnotation() == null)
1055 annotations = command.seqs[s].getAnnotation();
1059 tmp = new AlignmentAnnotation[aSize
1060 + command.seqs[s].getAnnotation().length];
1062 System.arraycopy(annotations, 0, tmp, 0, aSize);
1064 System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1065 command.seqs[s].getAnnotation().length);
1069 aSize = annotations.length;
1073 if (annotations == null)
1080 command.deletedAnnotations = new Hashtable<>();
1085 for (int a = 0; a < annotations.length; a++)
1087 if (annotations[a].autoCalculated
1088 || annotations[a].annotations == null)
1095 aSize = annotations[a].annotations.length;
1098 temp = new Annotation[aSize + command.number];
1099 if (annotations[a].padGaps)
1101 for (int aa = 0; aa < temp.length; aa++)
1103 temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1109 if (command.position < aSize)
1111 if (command.position + command.number >= aSize)
1117 tSize = aSize - command.number;
1129 temp = new Annotation[tSize];
1134 if (command.position < annotations[a].annotations.length)
1136 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1139 if (command.deletedAnnotations != null
1140 && command.deletedAnnotations
1141 .containsKey(annotations[a].annotationId))
1143 Annotation[] restore = command.deletedAnnotations
1144 .get(annotations[a].annotationId);
1146 System.arraycopy(restore, 0, temp, command.position,
1151 System.arraycopy(annotations[a].annotations, command.position,
1152 temp, command.position + command.number,
1153 aSize - command.position);
1157 if (command.deletedAnnotations != null
1158 && command.deletedAnnotations
1159 .containsKey(annotations[a].annotationId))
1161 Annotation[] restore = command.deletedAnnotations
1162 .get(annotations[a].annotationId);
1164 temp = new Annotation[annotations[a].annotations.length
1166 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1167 annotations[a].annotations.length);
1168 System.arraycopy(restore, 0, temp,
1169 annotations[a].annotations.length, restore.length);
1173 temp = annotations[a].annotations;
1179 if (tSize != aSize || command.position < 2)
1181 int copylen = Math.min(command.position,
1182 annotations[a].annotations.length);
1185 System.arraycopy(annotations[a].annotations, 0, temp, 0,
1186 copylen); // command.position);
1189 Annotation[] deleted = new Annotation[command.number];
1190 if (copylen >= command.position)
1192 copylen = Math.min(command.number,
1193 annotations[a].annotations.length - command.position);
1196 System.arraycopy(annotations[a].annotations, command.position,
1197 deleted, 0, copylen); // command.number);
1201 command.deletedAnnotations.put(annotations[a].annotationId,
1203 if (annotations[a].annotations.length > command.position
1206 System.arraycopy(annotations[a].annotations,
1207 command.position + command.number, temp,
1208 command.position, annotations[a].annotations.length
1209 - command.position - command.number); // aSize
1214 int dSize = aSize - command.position;
1218 Annotation[] deleted = new Annotation[command.number];
1219 System.arraycopy(annotations[a].annotations, command.position,
1222 command.deletedAnnotations.put(annotations[a].annotationId,
1225 tSize = Math.min(annotations[a].annotations.length,
1227 temp = new Annotation[tSize];
1228 System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1232 temp = annotations[a].annotations;
1237 annotations[a].annotations = temp;
1242 * Restores features to the state before a Cut.
1244 * <li>re-add any features deleted by the cut</li>
1245 * <li>remove any truncated features created by the cut</li>
1246 * <li>shift right any features to the right of the cut</li>
1252 * the sequence the Cut applied to
1254 * the start residue position of the cut
1256 * the number of residues cut
1257 * @param sameDatasetSequence
1258 * true if dataset sequence and frame of reference were left
1259 * unchanged by the Cut
1261 final static void undoCutFeatures(Edit command, SequenceI seq,
1262 final int start, final int length, boolean sameDatasetSequence)
1264 SequenceI sequence = seq.getDatasetSequence();
1265 if (sequence == null)
1271 * shift right features that lie to the right of the restored cut (but not
1272 * if dataset sequence unchanged - so coordinates were changed by Cut)
1274 if (!sameDatasetSequence)
1277 * shift right all features right of and not
1278 * contiguous with the cut position
1280 seq.getFeatures().shiftFeatures(start + 1, length);
1283 * shift right any features that start at the cut position,
1284 * unless they were truncated
1286 List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1288 for (SequenceFeature sf : sfs)
1290 if (sf.getBegin() == start)
1292 if (!command.truncatedFeatures.containsKey(seq)
1293 || !command.truncatedFeatures.get(seq).contains(sf))
1296 * feature was shifted left to cut position (not truncated),
1297 * so shift it back right
1299 SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
1300 + length, sf.getEnd() + length, sf.getFeatureGroup(),
1302 seq.addSequenceFeature(shifted);
1303 seq.deleteFeature(sf);
1310 * restore any features that were deleted or truncated
1312 if (command.deletedFeatures != null
1313 && command.deletedFeatures.containsKey(seq))
1315 for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1317 sequence.addSequenceFeature(deleted);
1322 * delete any truncated features
1324 if (command.truncatedFeatures != null
1325 && command.truncatedFeatures.containsKey(seq))
1327 for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1329 sequence.deleteFeature(amended);
1335 * Returns the list of edit commands wrapped by this object.
1339 public List<Edit> getEdits()
1345 * Returns a map whose keys are the dataset sequences, and values their
1346 * aligned sequences before the command edit list was applied. The aligned
1347 * sequences are copies, which may be updated without affecting the originals.
1349 * The command holds references to the aligned sequences (after editing). If
1350 * the command is an 'undo',then the prior state is simply the aligned state.
1351 * Otherwise, we have to derive the prior state by working backwards through
1352 * the edit list to infer the aligned sequences before editing.
1354 * Note: an alternative solution would be to cache the 'before' state of each
1355 * edit, but this would be expensive in space in the common case that the
1356 * original is never needed (edits are not mirrored).
1359 * @throws IllegalStateException
1360 * on detecting an edit command of a type that can't be unwound
1362 public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1364 Map<SequenceI, SequenceI> result = new HashMap<>();
1365 if (getEdits() == null)
1371 for (Edit e : getEdits())
1373 for (SequenceI seq : e.getSequences())
1375 SequenceI ds = seq.getDatasetSequence();
1376 // SequenceI preEdit = result.get(ds);
1377 if (!result.containsKey(ds))
1380 * copy sequence including start/end (but don't use copy constructor
1381 * as we don't need annotations)
1383 SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1384 seq.getStart(), seq.getEnd());
1385 preEdit.setDatasetSequence(ds);
1386 result.put(ds, preEdit);
1394 * Work backwards through the edit list, deriving the sequences before each
1395 * was applied. The final result is the sequence set before any edits.
1397 Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
1398 while (editList.hasNext())
1400 Edit oldEdit = editList.next();
1401 Action action = oldEdit.getAction();
1402 int position = oldEdit.getPosition();
1403 int number = oldEdit.getNumber();
1404 final char gap = oldEdit.getGapCharacter();
1405 for (SequenceI seq : oldEdit.getSequences())
1407 SequenceI ds = seq.getDatasetSequence();
1408 SequenceI preEdit = result.get(ds);
1409 if (preEdit == null)
1411 preEdit = new Sequence("", seq.getSequenceAsString(),
1412 seq.getStart(), seq.getEnd());
1413 preEdit.setDatasetSequence(ds);
1414 result.put(ds, preEdit);
1417 * 'Undo' this edit action on the sequence (updating the value in the
1422 if (action == Action.DELETE_GAP)
1424 preEdit.setSequence(new String(StringUtils.insertCharAt(
1425 preEdit.getSequence(), position, number, gap)));
1427 else if (action == Action.INSERT_GAP)
1429 preEdit.setSequence(new String(StringUtils.deleteChars(
1430 preEdit.getSequence(), position, position + number)));
1434 System.err.println("Can't undo edit action " + action);
1435 // throw new IllegalStateException("Can't undo edit action " +
1449 * start and end of sequence prior to edit
1451 Range[] oldStartEnd;
1453 boolean fullAlignmentHeight = false;
1455 Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1457 Map<String, Annotation[]> deletedAnnotations;
1460 * features deleted by the cut (re-add on Undo)
1461 * (including the original of any shortened features)
1463 Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1466 * shortened features added by the cut (delete on Undo)
1468 Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1472 final Action command;
1486 public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1491 this.position = pos;
1492 this.number = count;
1496 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1499 this(cmd, sqs, pos, count, align.getGapCharacter());
1503 alIndex = new int[sqs.length];
1504 for (int i = 0; i < sqs.length; i++)
1506 alIndex[i] = align.findIndex(sqs[i]);
1509 fullAlignmentHeight = (align.getHeight() == sqs.length);
1513 * Constructor given a REPLACE command and the replacement string
1522 Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1523 AlignmentI align, String replace)
1525 this(cmd, sqs, pos, count, align);
1527 string = new char[sqs.length][];
1528 for (int i = 0; i < sqs.length; i++)
1530 string[i] = replace.toCharArray();
1534 public SequenceI[] getSequences()
1539 public int getPosition()
1544 public Action getAction()
1549 public int getNumber()
1554 public char getGapCharacter()
1561 * Returns an iterator over the list of edit commands which traverses the list
1562 * either forwards or backwards.
1567 public Iterator<Edit> getEditIterator(boolean forwards)
1571 return getEdits().iterator();
1575 return new ReverseListIterator<>(getEdits());
1580 * Adjusts features for Cut, and saves details of changes made to allow Undo
1582 * <li>features left of the cut are unchanged</li>
1583 * <li>features right of the cut are shifted left</li>
1584 * <li>features internal to the cut region are deleted</li>
1585 * <li>features that overlap or span the cut are shortened</li>
1586 * <li>the originals of any deleted or shortened features are saved, to re-add
1588 * <li>any added (shortened) features are saved, to delete on Undo</li>
1593 * @param fromPosition
1595 * @param cutIsInternal
1597 protected static void cutFeatures(Edit command, SequenceI seq,
1598 int fromPosition, int toPosition, boolean cutIsInternal)
1601 * if the cut is at start or end of sequence
1602 * then we don't modify the sequence feature store
1608 List<SequenceFeature> added = new ArrayList<>();
1609 List<SequenceFeature> removed = new ArrayList<>();
1611 SequenceFeaturesI featureStore = seq.getFeatures();
1612 if (toPosition < fromPosition || featureStore == null)
1617 int cutStartPos = fromPosition;
1618 int cutEndPos = toPosition;
1619 int cutWidth = cutEndPos - cutStartPos + 1;
1621 synchronized (featureStore)
1624 * get features that overlap the cut region
1626 List<SequenceFeature> toAmend = featureStore.findFeatures(
1627 cutStartPos, cutEndPos);
1630 * add any contact features that span the cut region
1631 * (not returned by findFeatures)
1633 for (SequenceFeature contact : featureStore.getContactFeatures())
1635 if (contact.getBegin() < cutStartPos
1636 && contact.getEnd() > cutEndPos)
1638 toAmend.add(contact);
1643 * adjust start-end of overlapping features;
1644 * delete features enclosed by the cut;
1645 * delete partially overlapping contact features
1647 for (SequenceFeature sf : toAmend)
1649 int sfBegin = sf.getBegin();
1650 int sfEnd = sf.getEnd();
1651 int newBegin = sfBegin;
1653 boolean toDelete = false;
1654 boolean follows = false;
1656 if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1659 * feature lies within cut region - delete it
1663 else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1666 * feature spans cut region - left-shift the end
1670 else if (sfEnd <= cutEndPos)
1673 * feature overlaps left of cut region - truncate right
1675 newEnd = cutStartPos - 1;
1676 if (sf.isContactFeature())
1681 else if (sfBegin >= cutStartPos)
1684 * remaining case - feature overlaps right
1685 * truncate left, adjust end of feature
1687 newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1688 newEnd = newBegin + sfEnd - cutEndPos - 1;
1689 if (sf.isContactFeature())
1695 seq.deleteFeature(sf);
1702 SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1703 sf.getFeatureGroup(), sf.getScore());
1704 seq.addSequenceFeature(copy);
1713 * and left shift any features lying to the right of the cut region
1716 featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1720 * save deleted and amended features, so that Undo can
1721 * re-add or delete them respectively
1723 if (command.deletedFeatures == null)
1725 command.deletedFeatures = new HashMap<>();
1727 if (command.truncatedFeatures == null)
1729 command.truncatedFeatures = new HashMap<>();
1731 command.deletedFeatures.put(seq, removed);
1732 command.truncatedFeatures.put(seq, added);