Merge branch 'bug/JAL-4353_cannot_output_multiple_different_structure_images_for_one_...
[jalview.git] / src / jalview / commands / EditCommand.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
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.
11  *  
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.
16  * 
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.
20  */
21 package jalview.commands;
22
23 import java.util.Locale;
24
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;
38
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;
45 import java.util.Map;
46
47 /**
48  * 
49  * <p>
50  * Title: EditCommmand
51  * </p>
52  * 
53  * <p>
54  * Description: Essential information for performing undo and redo for cut/paste
55  * insert/delete gap which can be stored in the HistoryList
56  * </p>
57  * 
58  * <p>
59  * Copyright: Copyright (c) 2006
60  * </p>
61  * 
62  * <p>
63  * Company: Dundee University
64  * </p>
65  * 
66  * @author not attributable
67  * @version 1.0
68  */
69 public class EditCommand implements CommandI
70 {
71   public enum Action
72   {
73     INSERT_GAP
74     {
75       @Override
76       public Action getUndoAction()
77       {
78         return DELETE_GAP;
79       }
80     },
81     DELETE_GAP
82     {
83       @Override
84       public Action getUndoAction()
85       {
86         return INSERT_GAP;
87       }
88     },
89     CUT
90     {
91       @Override
92       public Action getUndoAction()
93       {
94         return PASTE;
95       }
96     },
97     PASTE
98     {
99       @Override
100       public Action getUndoAction()
101       {
102         return CUT;
103       }
104     },
105     REPLACE
106     {
107       @Override
108       public Action getUndoAction()
109       {
110         return REPLACE;
111       }
112     },
113     INSERT_NUC
114     {
115       @Override
116       public Action getUndoAction()
117       {
118         return null;
119       }
120     };
121
122     public abstract Action getUndoAction();
123   };
124
125   private List<Edit> edits = new ArrayList<>();
126
127   String description;
128
129   public EditCommand()
130   {
131   }
132
133   public EditCommand(String desc)
134   {
135     this.description = desc;
136   }
137
138   public EditCommand(String desc, Action command, SequenceI[] seqs,
139           int position, int number, AlignmentI al)
140   {
141     this.description = desc;
142     if (command == Action.CUT || command == Action.PASTE)
143     {
144       setEdit(new Edit(command, seqs, position, number, al));
145     }
146
147     performEdit(0, null);
148   }
149
150   public EditCommand(String desc, Action command, String replace,
151           SequenceI[] seqs, int position, int number, AlignmentI al)
152   {
153     this.description = desc;
154     if (command == Action.REPLACE)
155     {
156       setEdit(new Edit(command, seqs, position, number, al, replace));
157     }
158
159     performEdit(0, null);
160   }
161
162   /**
163    * Set the list of edits to the specified item (only).
164    * 
165    * @param e
166    */
167   protected void setEdit(Edit e)
168   {
169     edits.clear();
170     edits.add(e);
171   }
172
173   /**
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.
177    * 
178    * @param e
179    */
180   public void addEdit(Edit e)
181   {
182     if (!expandEdit(edits, e))
183     {
184       edits.add(e);
185     }
186   }
187
188   /**
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.
194    * 
195    * @param edits
196    * @param e
197    * @return
198    */
199   protected static boolean expandEdit(List<Edit> edits, Edit e)
200   {
201     if (edits == null || edits.isEmpty())
202     {
203       return false;
204     }
205     Edit lastEdit = edits.get(edits.size() - 1);
206     Action action = e.command;
207     if (lastEdit.command != action)
208     {
209       return false;
210     }
211
212     /*
213      * Both commands must act on the same sequences - compare the underlying
214      * dataset sequences, rather than the aligned sequences, which change as
215      * they are edited.
216      */
217     if (lastEdit.seqs.length != e.seqs.length)
218     {
219       return false;
220     }
221     for (int i = 0; i < e.seqs.length; i++)
222     {
223       if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
224               .getDatasetSequence())
225       {
226         return false;
227       }
228     }
229
230     /**
231      * Check a contiguous edit; either
232      * <ul>
233      * <li>a new Insert <n> positions to the right of the last <insert n>,
234      * or</li>
235      * <li>a new Delete <n> gaps which is <n> positions to the left of the last
236      * delete.</li>
237      * </ul>
238      */
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);
243     if (contiguous)
244     {
245       /*
246        * We are just expanding the range of the last edit. For delete gap, also
247        * moving the start position left.
248        */
249       lastEdit.number += e.number;
250       lastEdit.seqs = e.seqs;
251       if (action == Action.DELETE_GAP)
252       {
253         lastEdit.position--;
254       }
255       return true;
256     }
257     return false;
258   }
259
260   /**
261    * Clear the list of stored edit commands.
262    * 
263    */
264   protected void clearEdits()
265   {
266     edits.clear();
267   }
268
269   /**
270    * Returns the i'th stored Edit command.
271    * 
272    * @param i
273    * @return
274    */
275   protected Edit getEdit(int i)
276   {
277     if (i >= 0 && i < edits.size())
278     {
279       return edits.get(i);
280     }
281     return null;
282   }
283
284   @Override
285   final public String getDescription()
286   {
287     return description;
288   }
289
290   @Override
291   public int getSize()
292   {
293     return edits.size();
294   }
295
296   /**
297    * Return the alignment for the first edit (or null if no edit).
298    * 
299    * @return
300    */
301   final public AlignmentI getAlignment()
302   {
303     return (edits.isEmpty() ? null : edits.get(0).al);
304   }
305
306   /**
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.
311    * 
312    * @param command
313    * @param seqs
314    * @param position
315    * @param number
316    * @param al
317    * @param performEdit
318    */
319   final public void appendEdit(Action command, SequenceI[] seqs,
320           int position, int number, AlignmentI al, boolean performEdit)
321   {
322     appendEdit(command, seqs, position, number, al, performEdit, null);
323   }
324
325   /**
326    * append a new edit command with a set of alignment views that may be
327    * operated on
328    * 
329    * @param command
330    * @param seqs
331    * @param position
332    * @param number
333    * @param al
334    * @param performEdit
335    * @param views
336    */
337   final public void appendEdit(Action command, SequenceI[] seqs,
338           int position, int number, AlignmentI al, boolean performEdit,
339           AlignmentI[] views)
340   {
341     Edit edit = new Edit(command, seqs, position, number, al);
342     appendEdit(edit, al, performEdit, views);
343   }
344
345   /**
346    * Overloaded method that accepts an Edit object with additional parameters.
347    * 
348    * @param edit
349    * @param al
350    * @param performEdit
351    * @param views
352    */
353   final public void appendEdit(Edit edit, AlignmentI al,
354           boolean performEdit, AlignmentI[] views)
355   {
356     if (al.getHeight() == edit.seqs.length)
357     {
358       edit.al = al;
359       edit.fullAlignmentHeight = true;
360     }
361
362     addEdit(edit);
363
364     if (performEdit)
365     {
366       performEdit(edit, views);
367     }
368   }
369
370   /**
371    * Execute all the edit commands, starting at the given commandIndex
372    * 
373    * @param commandIndex
374    * @param views
375    */
376   public final void performEdit(int commandIndex, AlignmentI[] views)
377   {
378     ListIterator<Edit> iterator = edits.listIterator(commandIndex);
379     while (iterator.hasNext())
380     {
381       Edit edit = iterator.next();
382       performEdit(edit, views);
383     }
384   }
385
386   /**
387    * Execute one edit command in all the specified alignment views
388    * 
389    * @param edit
390    * @param views
391    */
392   protected static void performEdit(Edit edit, AlignmentI[] views)
393   {
394     switch (edit.command)
395     {
396     case INSERT_GAP:
397       insertGap(edit);
398       break;
399     case DELETE_GAP:
400       deleteGap(edit);
401       break;
402     case CUT:
403       cut(edit, views);
404       break;
405     case PASTE:
406       paste(edit, views);
407       break;
408     case REPLACE:
409       replace(edit);
410       break;
411     case INSERT_NUC:
412       // TODO:add deleteNuc for UNDO
413       // case INSERT_NUC:
414       // insertNuc(edits[e]);
415       break;
416     default:
417       break;
418     }
419   }
420
421   @Override
422   final public void doCommand(AlignmentI[] views)
423   {
424     performEdit(0, views);
425   }
426
427   /**
428    * Undo the stored list of commands, in reverse order.
429    */
430   @Override
431   final public void undoCommand(AlignmentI[] views)
432   {
433     ListIterator<Edit> iterator = edits.listIterator(edits.size());
434     while (iterator.hasPrevious())
435     {
436       Edit e = iterator.previous();
437       switch (e.command)
438       {
439       case INSERT_GAP:
440         deleteGap(e);
441         break;
442       case DELETE_GAP:
443         insertGap(e);
444         break;
445       case CUT:
446         paste(e, views);
447         break;
448       case PASTE:
449         cut(e, views);
450         break;
451       case REPLACE:
452         replace(e);
453         break;
454       case INSERT_NUC:
455         // not implemented
456         break;
457       default:
458         break;
459       }
460     }
461   }
462
463   /**
464    * Insert gap(s) in sequences as specified by the command, and adjust
465    * annotations.
466    * 
467    * @param command
468    */
469   final private static void insertGap(Edit command)
470   {
471
472     for (int s = 0; s < command.seqs.length; s++)
473     {
474       command.seqs[s].insertCharAt(command.position, command.number,
475               command.gapChar);
476       // jalview.bin.Console.outPrintln("pos: "+command.position+" number:
477       // "+command.number);
478     }
479
480     adjustAnnotations(command, true, false, null);
481   }
482
483   //
484   // final void insertNuc(Edit command)
485   // {
486   //
487   // for (int s = 0; s < command.seqs.length; s++)
488   // {
489   // jalview.bin.Console.outPrintln("pos: "+command.position+" number:
490   // "+command.number);
491   // command.seqs[s].insertCharAt(command.position, command.number,'A');
492   // }
493   //
494   // adjustAnnotations(command, true, false, null);
495   // }
496
497   /**
498    * Delete gap(s) in sequences as specified by the command, and adjust
499    * annotations.
500    * 
501    * @param command
502    */
503   final static private void deleteGap(Edit command)
504   {
505     for (int s = 0; s < command.seqs.length; s++)
506     {
507       command.seqs[s].deleteChars(command.position,
508               command.position + command.number);
509     }
510
511     adjustAnnotations(command, false, false, null);
512   }
513
514   /**
515    * Carry out a Cut action. The cut characters are saved in case Undo is
516    * requested.
517    * 
518    * @param command
519    * @param views
520    */
521   static void cut(Edit command, AlignmentI[] views)
522   {
523     boolean seqDeleted = false;
524     command.string = new char[command.seqs.length][];
525
526     for (int i = 0; i < command.seqs.length; i++)
527     {
528       final SequenceI sequence = command.seqs[i];
529       if (sequence.getLength() > command.position)
530       {
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();
539
540         /*
541          * perform the cut; if this results in a new dataset sequence, add
542          * that to the alignment dataset
543          */
544         SequenceI ds = sequence.getDatasetSequence();
545         sequence.deleteChars(command.position,
546                 command.position + command.number);
547
548         if (command.oldds != null && command.oldds[i] != null)
549         {
550           /*
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
554            */
555           sequence.setDatasetSequence(command.oldds[i]);
556           command.oldds[i] = oldds;
557         }
558         else
559         {
560           /* 
561            * new cut operation: save the dataset sequence 
562            * so it can be restored in an Undo
563            */
564           if (command.oldds == null)
565           {
566             command.oldds = new SequenceI[command.seqs.length];
567           }
568           command.oldds[i] = oldds;// todo not if !cutIsInternal?
569
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 ?
574           {
575             if (cutPositions != null)
576             {
577               cutFeatures(command, sequence, cutPositions.getBegin(),
578                       cutPositions.getEnd(), cutIsInternal);
579             }
580           }
581         }
582         SequenceI newDs = sequence.getDatasetSequence();
583         if (newDs != ds && command.al != null
584                 && command.al.getDataset() != null
585                 && !command.al.getDataset().getSequences().contains(newDs))
586         {
587           command.al.getDataset().addSequence(newDs);
588         }
589       }
590
591       if (sequence.getLength() < 1)
592       {
593         command.al.deleteSequence(sequence);
594         seqDeleted = true;
595       }
596     }
597
598     adjustAnnotations(command, false, seqDeleted, views);
599   }
600
601   /**
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.
604    * 
605    * @param command
606    * @param views
607    */
608   static void paste(Edit command, AlignmentI[] views)
609   {
610     boolean seqWasDeleted = false;
611
612     for (int i = 0; i < command.seqs.length; i++)
613     {
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)
619       {
620         /*
621          * sequence was deleted; re-add it to the alignment
622          */
623         if (command.alIndex[i] < command.al.getHeight())
624         {
625           List<SequenceI> sequences = command.al.getSequences();
626           synchronized (sequences)
627           {
628             if (!(command.alIndex[i] < 0))
629             {
630               sequences.add(command.alIndex[i], sequence);
631             }
632           }
633         }
634         else
635         {
636           command.al.addSequence(sequence);
637         }
638         seqWasDeleted = true;
639       }
640       int newStart = sequence.getStart();
641       int newEnd = sequence.getEnd();
642
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.
647
648       int start = 0;
649       int length = 0;
650
651       if (command.string != null && command.string[i] != null)
652       {
653         if (command.position >= tmp.length())
654         {
655           // This occurs if padding is on, and residues
656           // are removed from end of alignment
657           int len = command.position - tmp.length();
658           while (len > 0)
659           {
660             tmp.append(command.gapChar);
661             len--;
662           }
663         }
664         tmp.insert(command.position, command.string[i]);
665         for (int s = 0; s < command.string[i].length; s++)
666         {
667           if (!Comparison.isGap(command.string[i][s]))
668           {
669             length++;
670             if (!newDSNeeded)
671             {
672               newDSNeeded = true;
673               start = sequence.findPosition(command.position);
674               // end = sequence
675               // .findPosition(command.position + command.number);
676             }
677             if (sequence.getStart() == start)
678             {
679               newStart--;
680             }
681             else
682             {
683               newEnd++;
684             }
685           }
686         }
687         command.string[i] = null;
688       }
689
690       sequence.setSequence(tmp.toString());
691       sequence.setStart(newStart);
692       sequence.setEnd(newEnd);
693
694       /*
695        * command and Undo share the same dataset sequence if cut was
696        * at start or end of sequence
697        */
698       boolean sameDatasetSequence = false;
699       if (newDSNeeded)
700       {
701         if (sequence.getDatasetSequence() != null)
702         {
703           SequenceI ds;
704           if (newDSWasNeeded)
705           {
706             ds = command.oldds[i];
707           }
708           else
709           {
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());
717           }
718           if (command.oldds == null)
719           {
720             command.oldds = new SequenceI[command.seqs.length];
721           }
722           command.oldds[i] = sequence.getDatasetSequence();
723           sameDatasetSequence = ds == sequence.getDatasetSequence();
724           ds.setSequenceFeatures(sequence.getSequenceFeatures());
725           if (!sameDatasetSequence && command.al.getDataset() != null)
726           {
727             // delete 'undone' sequence from alignment dataset
728             command.al.getDataset()
729                     .deleteSequence(sequence.getDatasetSequence());
730           }
731           sequence.setDatasetSequence(ds);
732         }
733         undoCutFeatures(command, command.seqs[i], start, length,
734                 sameDatasetSequence);
735       }
736     }
737     adjustAnnotations(command, true, seqWasDeleted, views);
738
739     command.string = null;
740   }
741
742   static void replace(Edit command)
743   {
744     StringBuilder tmp;
745     String oldstring;
746     int start = command.position;
747     int end = command.number;
748     // TODO TUTORIAL - Fix for replacement with different length of sequence (or
749     // whole sequence)
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++)
754     {
755       boolean newDSWasNeeded = command.oldds != null
756               && command.oldds[i] != null;
757       boolean newStartEndWasNeeded = command.oldStartEnd != null
758               && command.oldStartEnd[i] != null;
759
760       /**
761        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
762        * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
763        * viewport.alignment));
764        * 
765        */
766       /**
767        * then addHistoryItem(new EditCommand( "Add sequences",
768        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
769        * 
770        */
771       ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
772               start);
773       ContiguousI afterEditedPositions = command.seqs[i]
774               .findPositions(end + 1, command.seqs[i].getLength());
775
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())
782       {
783         tmp.append(oldstring.substring(end));
784       }
785       // stash end prior to updating the sequence object so we can save it if
786       // need be.
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()))
792               .toCharArray();
793       String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
794               new String(command.string[i]));
795
796       if (!nogaprep.toLowerCase(Locale.ROOT)
797               .equals(nogapold.toLowerCase(Locale.ROOT)))
798       {
799         // we may already have dataset and limits stashed...
800         if (newDSWasNeeded || newStartEndWasNeeded)
801         {
802           if (newDSWasNeeded)
803           {
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;
808           }
809           if (newStartEndWasNeeded)
810           {
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());
815           }
816         }
817         else
818         {
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()
828                                           + nogapold.length())
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);
835
836           // and check if new sequence data is different..
837           if (!fullseq.equalsIgnoreCase(osp))
838           {
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));
843
844             if (command.oldds == null)
845             {
846               command.oldds = new SequenceI[command.seqs.length];
847             }
848             command.oldds[i] = command.seqs[i].getDatasetSequence();
849
850             // And preserve start/end for good-measure
851
852             if (command.oldStartEnd == null)
853             {
854               command.oldStartEnd = new Range[command.seqs.length];
855             }
856             command.oldStartEnd[i] = oldstartend;
857             // TODO: JAL-1131 ensure newly created dataset sequence is added to
858             // the set of
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
862             // relationships
863             // are preserved.
864             command.seqs[i].setDatasetSequence(newds);
865           }
866           else
867           {
868             if (command.oldStartEnd == null)
869             {
870               command.oldStartEnd = new Range[command.seqs.length];
871             }
872             command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
873                     command.seqs[i].getEnd());
874             if (beforeEditedPositions != null
875                     && afterEditedPositions == null)
876             {
877               // modification at end
878               command.seqs[i].setEnd(beforeEditedPositions.getEnd()
879                       + nogaprep.length() - nogapold.length());
880             }
881             else if (afterEditedPositions != null
882                     && beforeEditedPositions == null)
883             {
884               // modification at start
885               command.seqs[i].setStart(
886                       afterEditedPositions.getBegin() - nogaprep.length());
887             }
888             else
889             {
890               // edit covered both start and end. Here we can only guess the
891               // new
892               // start/end
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);
898               if (newStart == -1)
899               {
900                 throw new Error(
901                         "Implementation Error: could not locate start/end "
902                                 + "in dataset sequence after an edit of the sequence string");
903               }
904               int newEnd = newStart + nogapalseq.length() - 1;
905               command.seqs[i].setStart(newStart);
906               command.seqs[i].setEnd(newEnd);
907             }
908           }
909         }
910       }
911       tmp = null;
912       oldstring = null;
913     }
914   }
915
916   final static void adjustAnnotations(Edit command, boolean insert,
917           boolean modifyVisibility, AlignmentI[] views)
918   {
919     AlignmentAnnotation[] annotations = null;
920
921     if (modifyVisibility && !insert)
922     {
923       // only occurs if a sequence was added or deleted.
924       command.deletedAnnotationRows = new Hashtable<>();
925     }
926     if (command.fullAlignmentHeight)
927     {
928       annotations = command.al.getAlignmentAnnotation();
929     }
930     else
931     {
932       int aSize = 0;
933       AlignmentAnnotation[] tmp;
934       for (int s = 0; s < command.seqs.length; s++)
935       {
936         command.seqs[s].sequenceChanged();
937
938         if (modifyVisibility)
939         {
940           // Rows are only removed or added to sequence object.
941           if (!insert)
942           {
943             // remove rows
944             tmp = command.seqs[s].getAnnotation();
945             if (tmp != null)
946             {
947               int alen = tmp.length;
948               for (int aa = 0; aa < tmp.length; aa++)
949               {
950                 if (!command.al.deleteAnnotation(tmp[aa]))
951                 {
952                   // strip out annotation not in the current al (will be put
953                   // back on insert in all views)
954                   tmp[aa] = null;
955                   alen--;
956                 }
957               }
958               command.seqs[s].setAlignmentAnnotation(null);
959               if (alen != tmp.length)
960               {
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++)
964                 {
965                   if (tmp[aa] != null)
966                   {
967                     saved[aapos++] = tmp[aa];
968                     tmp[aa] = null;
969                   }
970                 }
971                 tmp = saved;
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++)
976                 {
977                   if (views[alview] != command.al)
978                   {
979                     AlignmentAnnotation[] toremove = views[alview]
980                             .getAlignmentAnnotation();
981                     if (toremove == null || toremove.length == 0)
982                     {
983                       continue;
984                     }
985                     // remove any alignment annotation on this sequence that's
986                     // on that alignment view.
987                     for (int aa = 0; aa < toremove.length; aa++)
988                     {
989                       if (toremove[aa].sequenceRef == command.seqs[s])
990                       {
991                         views[alview].deleteAnnotation(toremove[aa]);
992                       }
993                     }
994                   }
995                 }
996               }
997               else
998               {
999                 // save all the annotation
1000                 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1001               }
1002             }
1003           }
1004           else
1005           {
1006             // recover rows
1007             if (command.deletedAnnotationRows != null
1008                     && command.deletedAnnotationRows
1009                             .containsKey(command.seqs[s]))
1010             {
1011               AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1012                       .get(command.seqs[s]);
1013               command.seqs[s].setAlignmentAnnotation(revealed);
1014               if (revealed != null)
1015               {
1016                 for (int aa = 0; aa < revealed.length; aa++)
1017                 {
1018                   // iterate through al adding original annotation
1019                   command.al.addAnnotation(revealed[aa]);
1020                 }
1021                 for (int aa = 0; aa < revealed.length; aa++)
1022                 {
1023                   command.al.setAnnotationIndex(revealed[aa], aa);
1024                 }
1025                 // and then duplicate added annotation on every other alignment
1026                 // view
1027                 for (int vnum = 0; views != null
1028                         && vnum < views.length; vnum++)
1029                 {
1030                   if (views[vnum] != command.al)
1031                   {
1032                     int avwidth = views[vnum].getWidth() + 1;
1033                     // duplicate in this view
1034                     for (int a = 0; a < revealed.length; a++)
1035                     {
1036                       AlignmentAnnotation newann = new AlignmentAnnotation(
1037                               revealed[a]);
1038                       command.seqs[s].addAlignmentAnnotation(newann);
1039                       newann.padAnnotation(avwidth);
1040                       views[vnum].addAnnotation(newann);
1041                       views[vnum].setAnnotationIndex(newann, a);
1042                     }
1043                   }
1044                 }
1045               }
1046             }
1047           }
1048           continue;
1049         }
1050
1051         if (command.seqs[s].getAnnotation() == null)
1052         {
1053           continue;
1054         }
1055
1056         if (aSize == 0)
1057         {
1058           annotations = command.seqs[s].getAnnotation();
1059         }
1060         else
1061         {
1062           tmp = new AlignmentAnnotation[aSize
1063                   + command.seqs[s].getAnnotation().length];
1064
1065           System.arraycopy(annotations, 0, tmp, 0, aSize);
1066
1067           System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
1068                   command.seqs[s].getAnnotation().length);
1069
1070           annotations = tmp;
1071         }
1072         aSize = annotations.length;
1073       }
1074     }
1075
1076     if (annotations == null)
1077     {
1078       return;
1079     }
1080
1081     if (!insert)
1082     {
1083       command.deletedAnnotations = new Hashtable<>();
1084     }
1085
1086     int aSize;
1087     Annotation[] temp;
1088     for (int a = 0; a < annotations.length; a++)
1089     {
1090       if (annotations[a].autoCalculated
1091               || annotations[a].annotations == null)
1092       {
1093         continue;
1094       }
1095
1096       int tSize = 0;
1097
1098       aSize = annotations[a].annotations.length;
1099       if (insert)
1100       {
1101         temp = new Annotation[aSize + command.number];
1102         if (annotations[a].padGaps)
1103         {
1104           for (int aa = 0; aa < temp.length; aa++)
1105           {
1106             temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
1107           }
1108         }
1109       }
1110       else
1111       {
1112         if (command.position < aSize)
1113         {
1114           if (command.position + command.number >= aSize)
1115           {
1116             tSize = aSize;
1117           }
1118           else
1119           {
1120             tSize = aSize - command.number;
1121           }
1122         }
1123         else
1124         {
1125           tSize = aSize;
1126         }
1127
1128         if (tSize < 0)
1129         {
1130           tSize = aSize;
1131         }
1132         temp = new Annotation[tSize];
1133       }
1134
1135       if (insert)
1136       {
1137         if (command.position < annotations[a].annotations.length)
1138         {
1139           System.arraycopy(annotations[a].annotations, 0, temp, 0,
1140                   command.position);
1141
1142           if (command.deletedAnnotations != null
1143                   && command.deletedAnnotations
1144                           .containsKey(annotations[a].annotationId))
1145           {
1146             Annotation[] restore = command.deletedAnnotations
1147                     .get(annotations[a].annotationId);
1148
1149             System.arraycopy(restore, 0, temp, command.position,
1150                     command.number);
1151
1152           }
1153
1154           System.arraycopy(annotations[a].annotations, command.position,
1155                   temp, command.position + command.number,
1156                   aSize - command.position);
1157         }
1158         else
1159         {
1160           if (command.deletedAnnotations != null
1161                   && command.deletedAnnotations
1162                           .containsKey(annotations[a].annotationId))
1163           {
1164             Annotation[] restore = command.deletedAnnotations
1165                     .get(annotations[a].annotationId);
1166
1167             temp = new Annotation[annotations[a].annotations.length
1168                     + restore.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);
1173           }
1174           else
1175           {
1176             temp = annotations[a].annotations;
1177           }
1178         }
1179       }
1180       else
1181       {
1182         if (tSize != aSize || command.position < 2)
1183         {
1184           int copylen = Math.min(command.position,
1185                   annotations[a].annotations.length);
1186           if (copylen > 0)
1187           {
1188             System.arraycopy(annotations[a].annotations, 0, temp, 0,
1189                     copylen); // command.position);
1190           }
1191
1192           Annotation[] deleted = new Annotation[command.number];
1193           if (copylen >= command.position)
1194           {
1195             copylen = Math.min(command.number,
1196                     annotations[a].annotations.length - command.position);
1197             if (copylen > 0)
1198             {
1199               System.arraycopy(annotations[a].annotations, command.position,
1200                       deleted, 0, copylen); // command.number);
1201             }
1202           }
1203
1204           command.deletedAnnotations.put(annotations[a].annotationId,
1205                   deleted);
1206           if (annotations[a].annotations.length > command.position
1207                   + command.number)
1208           {
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
1213           }
1214         }
1215         else
1216         {
1217           int dSize = aSize - command.position;
1218
1219           if (dSize > 0)
1220           {
1221             Annotation[] deleted = new Annotation[command.number];
1222             System.arraycopy(annotations[a].annotations, command.position,
1223                     deleted, 0, dSize);
1224
1225             command.deletedAnnotations.put(annotations[a].annotationId,
1226                     deleted);
1227
1228             tSize = Math.min(annotations[a].annotations.length,
1229                     command.position);
1230             temp = new Annotation[tSize];
1231             System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1232           }
1233           else
1234           {
1235             temp = annotations[a].annotations;
1236           }
1237         }
1238       }
1239
1240       annotations[a].annotations = temp;
1241     }
1242   }
1243
1244   /**
1245    * Restores features to the state before a Cut.
1246    * <ul>
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>
1250    * </ul>
1251    * 
1252    * @param command
1253    *          the Cut command
1254    * @param seq
1255    *          the sequence the Cut applied to
1256    * @param start
1257    *          the start residue position of the cut
1258    * @param length
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
1263    */
1264   final static void undoCutFeatures(Edit command, SequenceI seq,
1265           final int start, final int length, boolean sameDatasetSequence)
1266   {
1267     SequenceI sequence = seq.getDatasetSequence();
1268     if (sequence == null)
1269     {
1270       sequence = seq;
1271     }
1272
1273     /*
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)
1276      */
1277     if (!sameDatasetSequence)
1278     {
1279       /*
1280        * shift right all features right of and not 
1281        * contiguous with the cut position
1282        */
1283       seq.getFeatures().shiftFeatures(start + 1, length);
1284
1285       /*
1286        * shift right any features that start at the cut position,
1287        * unless they were truncated
1288        */
1289       List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
1290               start);
1291       for (SequenceFeature sf : sfs)
1292       {
1293         if (sf.getBegin() == start)
1294         {
1295           if (!command.truncatedFeatures.containsKey(seq)
1296                   || !command.truncatedFeatures.get(seq).contains(sf))
1297           {
1298             /*
1299              * feature was shifted left to cut position (not truncated),
1300              * so shift it back right
1301              */
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);
1307           }
1308         }
1309       }
1310     }
1311
1312     /*
1313      * restore any features that were deleted or truncated
1314      */
1315     if (command.deletedFeatures != null
1316             && command.deletedFeatures.containsKey(seq))
1317     {
1318       for (SequenceFeature deleted : command.deletedFeatures.get(seq))
1319       {
1320         sequence.addSequenceFeature(deleted);
1321       }
1322     }
1323
1324     /*
1325      * delete any truncated features
1326      */
1327     if (command.truncatedFeatures != null
1328             && command.truncatedFeatures.containsKey(seq))
1329     {
1330       for (SequenceFeature amended : command.truncatedFeatures.get(seq))
1331       {
1332         sequence.deleteFeature(amended);
1333       }
1334     }
1335   }
1336
1337   /**
1338    * Returns the list of edit commands wrapped by this object.
1339    * 
1340    * @return
1341    */
1342   public List<Edit> getEdits()
1343   {
1344     return this.edits;
1345   }
1346
1347   /**
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.
1351    * 
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.
1356    * 
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).
1360    * 
1361    * @return
1362    * @throws IllegalStateException
1363    *           on detecting an edit command of a type that can't be unwound
1364    */
1365   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1366   {
1367     Map<SequenceI, SequenceI> result = new HashMap<>();
1368     if (getEdits() == null)
1369     {
1370       return result;
1371     }
1372     if (forUndo)
1373     {
1374       for (Edit e : getEdits())
1375       {
1376         for (SequenceI seq : e.getSequences())
1377         {
1378           SequenceI ds = seq.getDatasetSequence();
1379           // SequenceI preEdit = result.get(ds);
1380           if (!result.containsKey(ds))
1381           {
1382             /*
1383              * copy sequence including start/end (but don't use copy constructor
1384              * as we don't need annotations)
1385              */
1386             SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1387                     seq.getStart(), seq.getEnd());
1388             preEdit.setDatasetSequence(ds);
1389             result.put(ds, preEdit);
1390           }
1391         }
1392       }
1393       return result;
1394     }
1395
1396     /*
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.
1399      */
1400     Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
1401     while (editList.hasNext())
1402     {
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())
1409       {
1410         SequenceI ds = seq.getDatasetSequence();
1411         SequenceI preEdit = result.get(ds);
1412         if (preEdit == null)
1413         {
1414           preEdit = new Sequence("", seq.getSequenceAsString(),
1415                   seq.getStart(), seq.getEnd());
1416           preEdit.setDatasetSequence(ds);
1417           result.put(ds, preEdit);
1418         }
1419         /*
1420          * 'Undo' this edit action on the sequence (updating the value in the
1421          * map).
1422          */
1423         if (ds != null)
1424         {
1425           if (action == Action.DELETE_GAP)
1426           {
1427             preEdit.setSequence(new String(StringUtils.insertCharAt(
1428                     preEdit.getSequence(), position, number, gap)));
1429           }
1430           else if (action == Action.INSERT_GAP)
1431           {
1432             preEdit.setSequence(new String(StringUtils.deleteChars(
1433                     preEdit.getSequence(), position, position + number)));
1434           }
1435           else
1436           {
1437             jalview.bin.Console
1438                     .errPrintln("Can't undo edit action " + action);
1439             // throw new IllegalStateException("Can't undo edit action " +
1440             // action);
1441           }
1442         }
1443       }
1444     }
1445     return result;
1446   }
1447
1448   public class Edit
1449   {
1450     SequenceI[] oldds;
1451
1452     /**
1453      * start and end of sequence prior to edit
1454      */
1455     Range[] oldStartEnd;
1456
1457     boolean fullAlignmentHeight = false;
1458
1459     Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1460
1461     Map<String, Annotation[]> deletedAnnotations;
1462
1463     /*
1464      * features deleted by the cut (re-add on Undo)
1465      * (including the original of any shortened features)
1466      */
1467     Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1468
1469     /*
1470      * shortened features added by the cut (delete on Undo)
1471      */
1472     Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1473
1474     AlignmentI al;
1475
1476     final Action command;
1477
1478     char[][] string;
1479
1480     SequenceI[] seqs;
1481
1482     int[] alIndex;
1483
1484     int position;
1485
1486     int number;
1487
1488     char gapChar;
1489
1490     /*
1491      * flag that identifies edits inserted to balance 
1492      * user edits in a 'locked editing' region
1493      */
1494     private boolean systemGenerated;
1495
1496     public Edit(Action cmd, SequenceI[] sqs, int pos, int count, char gap)
1497     {
1498       this.command = cmd;
1499       this.seqs = sqs;
1500       this.position = pos;
1501       this.number = count;
1502       this.gapChar = gap;
1503     }
1504
1505     Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align)
1506     {
1507       this(cmd, sqs, pos, count, align.getGapCharacter());
1508
1509       this.al = align;
1510
1511       alIndex = new int[sqs.length];
1512       for (int i = 0; i < sqs.length; i++)
1513       {
1514         alIndex[i] = align.findIndex(sqs[i]);
1515       }
1516
1517       fullAlignmentHeight = (align.getHeight() == sqs.length);
1518     }
1519
1520     /**
1521      * Constructor given a REPLACE command and the replacement string
1522      * 
1523      * @param cmd
1524      * @param sqs
1525      * @param pos
1526      * @param count
1527      * @param align
1528      * @param replace
1529      */
1530     Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align,
1531             String replace)
1532     {
1533       this(cmd, sqs, pos, count, align);
1534
1535       string = new char[sqs.length][];
1536       for (int i = 0; i < sqs.length; i++)
1537       {
1538         string[i] = replace.toCharArray();
1539       }
1540     }
1541
1542     public SequenceI[] getSequences()
1543     {
1544       return seqs;
1545     }
1546
1547     public int getPosition()
1548     {
1549       return position;
1550     }
1551
1552     public Action getAction()
1553     {
1554       return command;
1555     }
1556
1557     public int getNumber()
1558     {
1559       return number;
1560     }
1561
1562     public char getGapCharacter()
1563     {
1564       return gapChar;
1565     }
1566
1567     public void setSystemGenerated(boolean b)
1568     {
1569       systemGenerated = b;
1570     }
1571
1572     public boolean isSystemGenerated()
1573     {
1574       return systemGenerated;
1575     }
1576   }
1577
1578   /**
1579    * Returns an iterator over the list of edit commands which traverses the list
1580    * either forwards or backwards.
1581    * 
1582    * @param forwards
1583    * @return
1584    */
1585   public Iterator<Edit> getEditIterator(boolean forwards)
1586   {
1587     if (forwards)
1588     {
1589       return getEdits().iterator();
1590     }
1591     else
1592     {
1593       return new ReverseListIterator<>(getEdits());
1594     }
1595   }
1596
1597   /**
1598    * Adjusts features for Cut, and saves details of changes made to allow Undo
1599    * <ul>
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
1605    * on Undo</li>
1606    * <li>any added (shortened) features are saved, to delete on Undo</li>
1607    * </ul>
1608    * 
1609    * @param command
1610    * @param seq
1611    * @param fromPosition
1612    * @param toPosition
1613    * @param cutIsInternal
1614    */
1615   protected static void cutFeatures(Edit command, SequenceI seq,
1616           int fromPosition, int toPosition, boolean cutIsInternal)
1617   {
1618     /* 
1619      * if the cut is at start or end of sequence
1620      * then we don't modify the sequence feature store
1621      */
1622     if (!cutIsInternal)
1623     {
1624       return;
1625     }
1626     List<SequenceFeature> added = new ArrayList<>();
1627     List<SequenceFeature> removed = new ArrayList<>();
1628
1629     SequenceFeaturesI featureStore = seq.getFeatures();
1630     if (toPosition < fromPosition || featureStore == null)
1631     {
1632       return;
1633     }
1634
1635     int cutStartPos = fromPosition;
1636     int cutEndPos = toPosition;
1637     int cutWidth = cutEndPos - cutStartPos + 1;
1638
1639     synchronized (featureStore)
1640     {
1641       /*
1642        * get features that overlap the cut region
1643        */
1644       List<SequenceFeature> toAmend = featureStore.findFeatures(cutStartPos,
1645               cutEndPos);
1646
1647       /*
1648        * add any contact features that span the cut region
1649        * (not returned by findFeatures)
1650        */
1651       for (SequenceFeature contact : featureStore.getContactFeatures())
1652       {
1653         if (contact.getBegin() < cutStartPos
1654                 && contact.getEnd() > cutEndPos)
1655         {
1656           toAmend.add(contact);
1657         }
1658       }
1659
1660       /*
1661        * adjust start-end of overlapping features;
1662        * delete features enclosed by the cut;
1663        * delete partially overlapping contact features
1664        */
1665       for (SequenceFeature sf : toAmend)
1666       {
1667         int sfBegin = sf.getBegin();
1668         int sfEnd = sf.getEnd();
1669         int newBegin = sfBegin;
1670         int newEnd = sfEnd;
1671         boolean toDelete = false;
1672         boolean follows = false;
1673
1674         if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1675         {
1676           /*
1677            * feature lies within cut region - delete it
1678            */
1679           toDelete = true;
1680         }
1681         else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1682         {
1683           /*
1684            * feature spans cut region - left-shift the end
1685            */
1686           newEnd -= cutWidth;
1687         }
1688         else if (sfEnd <= cutEndPos)
1689         {
1690           /*
1691            * feature overlaps left of cut region - truncate right
1692            */
1693           newEnd = cutStartPos - 1;
1694           if (sf.isContactFeature())
1695           {
1696             toDelete = true;
1697           }
1698         }
1699         else if (sfBegin >= cutStartPos)
1700         {
1701           /*
1702            * remaining case - feature overlaps right
1703            * truncate left, adjust end of feature
1704            */
1705           newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1706           newEnd = newBegin + sfEnd - cutEndPos - 1;
1707           if (sf.isContactFeature())
1708           {
1709             toDelete = true;
1710           }
1711         }
1712
1713         seq.deleteFeature(sf);
1714         if (!follows)
1715         {
1716           removed.add(sf);
1717         }
1718         if (!toDelete)
1719         {
1720           SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1721                   sf.getFeatureGroup(), sf.getScore());
1722           seq.addSequenceFeature(copy);
1723           if (!follows)
1724           {
1725             added.add(copy);
1726           }
1727         }
1728       }
1729
1730       /*
1731        * and left shift any features lying to the right of the cut region
1732        */
1733
1734       featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1735     }
1736
1737     /*
1738      * save deleted and amended features, so that Undo can 
1739      * re-add or delete them respectively
1740      */
1741     if (command.deletedFeatures == null)
1742     {
1743       command.deletedFeatures = new HashMap<>();
1744     }
1745     if (command.truncatedFeatures == null)
1746     {
1747       command.truncatedFeatures = new HashMap<>();
1748     }
1749     command.deletedFeatures.put(seq, removed);
1750     command.truncatedFeatures.put(seq, added);
1751   }
1752 }