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