30595bce2afcec658ce343e5ee7067bcd3310c03
[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     public abstract Action getUndoAction();
122   };
123
124   private List<Edit> edits = new ArrayList<>();
125
126   String description;
127
128   public EditCommand()
129   {
130   }
131
132   public EditCommand(String desc)
133   {
134     this.description = desc;
135   }
136
137   public EditCommand(String desc, Action command, SequenceI[] seqs,
138           int position, int number, AlignmentI al)
139   {
140     this.description = desc;
141     if (command == Action.CUT || command == Action.PASTE)
142     {
143       setEdit(new Edit(command, seqs, position, number, al));
144     }
145
146     performEdit(0, null);
147   }
148
149   public EditCommand(String desc, Action command, String replace,
150           SequenceI[] seqs, int position, int number, AlignmentI al)
151   {
152     this.description = desc;
153     if (command == Action.REPLACE)
154     {
155       setEdit(new Edit(command, seqs, position, number, al, replace));
156     }
157
158     performEdit(0, null);
159   }
160
161   /**
162    * Set the list of edits to the specified item (only).
163    * 
164    * @param e
165    */
166   protected void setEdit(Edit e)
167   {
168     edits.clear();
169     edits.add(e);
170   }
171
172   /**
173    * Add the given edit command to the stored list of commands. If simply
174    * expanding the range of the last command added, then modify it instead of
175    * adding a new command.
176    * 
177    * @param e
178    */
179   public void addEdit(Edit e)
180   {
181     if (!expandEdit(edits, e))
182     {
183       edits.add(e);
184     }
185   }
186
187   /**
188    * Returns true if the new edit is incorporated by updating (expanding the
189    * range of) the last edit on the list, else false. We can 'expand' the last
190    * edit if the new one is the same action, on the same sequences, and acts on
191    * a contiguous range. This is the case where a mouse drag generates a series
192    * of contiguous gap insertions or deletions.
193    * 
194    * @param edits
195    * @param e
196    * @return
197    */
198   protected static boolean expandEdit(List<Edit> edits, Edit e)
199   {
200     if (edits == null || edits.isEmpty())
201     {
202       return false;
203     }
204     Edit lastEdit = edits.get(edits.size() - 1);
205     Action action = e.command;
206     if (lastEdit.command != action)
207     {
208       return false;
209     }
210
211     /*
212      * Both commands must act on the same sequences - compare the underlying
213      * dataset sequences, rather than the aligned sequences, which change as
214      * they are edited.
215      */
216     if (lastEdit.seqs.length != e.seqs.length)
217     {
218       return false;
219     }
220     for (int i = 0; i < e.seqs.length; i++)
221     {
222       if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
223               .getDatasetSequence())
224       {
225         return false;
226       }
227     }
228
229     /**
230      * Check a contiguous edit; either
231      * <ul>
232      * <li>a new Insert <n> positions to the right of the last <insert n>,
233      * or</li>
234      * <li>a new Delete <n> gaps which is <n> positions to the left of the last
235      * delete.</li>
236      * </ul>
237      */
238     boolean contiguous = (action == Action.INSERT_GAP
239             && e.position == lastEdit.position + lastEdit.number)
240             || (action == Action.DELETE_GAP
241                     && e.position + e.number == lastEdit.position);
242     if (contiguous)
243     {
244       /*
245        * We are just expanding the range of the last edit. For delete gap, also
246        * moving the start position left.
247        */
248       lastEdit.number += e.number;
249       lastEdit.seqs = e.seqs;
250       if (action == Action.DELETE_GAP)
251       {
252         lastEdit.position--;
253       }
254       return true;
255     }
256     return false;
257   }
258
259   /**
260    * Clear the list of stored edit commands.
261    * 
262    */
263   protected void clearEdits()
264   {
265     edits.clear();
266   }
267
268   /**
269    * Returns the i'th stored Edit command.
270    * 
271    * @param i
272    * @return
273    */
274   protected Edit getEdit(int i)
275   {
276     if (i >= 0 && i < edits.size())
277     {
278       return edits.get(i);
279     }
280     return null;
281   }
282
283   @Override
284   final public String getDescription()
285   {
286     return description;
287   }
288
289   @Override
290   public int getSize()
291   {
292     return edits.size();
293   }
294
295   /**
296    * Return the alignment for the first edit (or null if no edit).
297    * 
298    * @return
299    */
300   final public AlignmentI getAlignment()
301   {
302     return (edits.isEmpty() ? null : edits.get(0).al);
303   }
304
305   /**
306    * append a new editCommand Note. this shouldn't be called if the edit is an
307    * operation affects more alignment objects than the one referenced in al (for
308    * example, cut or pasting whole sequences). Use the form with an additional
309    * AlignmentI[] views parameter.
310    * 
311    * @param command
312    * @param seqs
313    * @param position
314    * @param number
315    * @param al
316    * @param performEdit
317    */
318   final public void appendEdit(Action command, SequenceI[] seqs,
319           int position, int number, AlignmentI al, boolean performEdit)
320   {
321     appendEdit(command, seqs, position, number, al, performEdit, null);
322   }
323
324   /**
325    * append a new edit command with a set of alignment views that may be
326    * operated on
327    * 
328    * @param command
329    * @param seqs
330    * @param position
331    * @param number
332    * @param al
333    * @param performEdit
334    * @param views
335    */
336   final public void appendEdit(Action command, SequenceI[] seqs,
337           int position, int number, AlignmentI al, boolean performEdit,
338           AlignmentI[] views)
339   {
340     Edit edit = new Edit(command, seqs, position, number, al);
341     appendEdit(edit, al, performEdit, views);
342   }
343
344   /**
345    * Overloaded method that accepts an Edit object with additional parameters.
346    * 
347    * @param edit
348    * @param al
349    * @param performEdit
350    * @param views
351    */
352   final public void appendEdit(Edit edit, AlignmentI al,
353           boolean performEdit, AlignmentI[] views)
354   {
355     if (al.getHeight() == edit.seqs.length)
356     {
357       edit.al = al;
358       edit.fullAlignmentHeight = true;
359     }
360
361     addEdit(edit);
362
363     if (performEdit)
364     {
365       performEdit(edit, views);
366     }
367   }
368
369   /**
370    * Execute all the edit commands, starting at the given commandIndex
371    * 
372    * @param commandIndex
373    * @param views
374    */
375   public final void performEdit(int commandIndex, AlignmentI[] views)
376   {
377     ListIterator<Edit> iterator = edits.listIterator(commandIndex);
378     while (iterator.hasNext())
379     {
380       Edit edit = iterator.next();
381       performEdit(edit, views);
382     }
383   }
384
385   /**
386    * Execute one edit command in all the specified alignment views
387    * 
388    * @param edit
389    * @param views
390    */
391   protected static void performEdit(Edit edit, AlignmentI[] views)
392   {
393     switch (edit.command)
394     {
395     case INSERT_GAP:
396       insertGap(edit);
397       break;
398     case DELETE_GAP:
399       deleteGap(edit);
400       break;
401     case CUT:
402       cut(edit, views);
403       break;
404     case PASTE:
405       paste(edit, views);
406       break;
407     case REPLACE:
408       replace(edit);
409       break;
410     case INSERT_NUC:
411       // TODO:add deleteNuc for UNDO
412       // case INSERT_NUC:
413       // insertNuc(edits[e]);
414       break;
415     default:
416       break;
417     }
418   }
419
420   @Override
421   final public void doCommand(AlignmentI[] views)
422   {
423     performEdit(0, views);
424   }
425
426   /**
427    * Undo the stored list of commands, in reverse order.
428    */
429   @Override
430   final public void undoCommand(AlignmentI[] views)
431   {
432     ListIterator<Edit> iterator = edits.listIterator(edits.size());
433     while (iterator.hasPrevious())
434     {
435       Edit e = iterator.previous();
436       switch (e.command)
437       {
438       case INSERT_GAP:
439         deleteGap(e);
440         break;
441       case DELETE_GAP:
442         insertGap(e);
443         break;
444       case CUT:
445         paste(e, views);
446         break;
447       case PASTE:
448         cut(e, views);
449         break;
450       case REPLACE:
451         replace(e);
452         break;
453       case INSERT_NUC:
454         // not implemented
455         break;
456       default:
457         break;
458       }
459     }
460   }
461
462   /**
463    * Insert gap(s) in sequences as specified by the command, and adjust
464    * annotations.
465    * 
466    * @param command
467    */
468   final private static void insertGap(Edit command)
469   {
470
471     for (int s = 0; s < command.seqs.length; s++)
472     {
473       command.seqs[s].insertCharAt(command.position, command.number,
474               command.gapChar);
475       // System.out.println("pos: "+command.position+" number:
476       // "+command.number);
477     }
478
479     adjustAnnotations(command, true, false, null);
480   }
481
482   //
483   // final void insertNuc(Edit command)
484   // {
485   //
486   // for (int s = 0; s < command.seqs.length; s++)
487   // {
488   // System.out.println("pos: "+command.position+" number: "+command.number);
489   // command.seqs[s].insertCharAt(command.position, command.number,'A');
490   // }
491   //
492   // adjustAnnotations(command, true, false, null);
493   // }
494
495   /**
496    * Delete gap(s) in sequences as specified by the command, and adjust
497    * annotations.
498    * 
499    * @param command
500    */
501   final static private void deleteGap(Edit command)
502   {
503     for (int s = 0; s < command.seqs.length; s++)
504     {
505       command.seqs[s].deleteChars(command.position,
506               command.position + command.number);
507     }
508
509     adjustAnnotations(command, false, false, null);
510   }
511
512   /**
513    * Carry out a Cut action. The cut characters are saved in case Undo is
514    * requested.
515    * 
516    * @param command
517    * @param views
518    */
519   static void cut(Edit command, AlignmentI[] views)
520   {
521     boolean seqDeleted = false;
522     command.string = new char[command.seqs.length][];
523
524     for (int i = 0; i < command.seqs.length; i++)
525     {
526       final SequenceI sequence = command.seqs[i];
527       if (sequence.getLength() > command.position)
528       {
529         command.string[i] = sequence.getSequence(command.position,
530                 command.position + command.number);
531         SequenceI oldds = sequence.getDatasetSequence();
532         ContiguousI cutPositions = sequence.findPositions(
533                 command.position + 1, command.position + command.number);
534         boolean cutIsInternal = cutPositions != null
535                 && sequence.getStart() != cutPositions
536                 .getBegin() && sequence.getEnd() != cutPositions.getEnd();
537
538         /*
539          * perform the cut; if this results in a new dataset sequence, add
540          * that to the alignment dataset
541          */
542         SequenceI ds = sequence.getDatasetSequence();
543         sequence.deleteChars(command.position, command.position
544                 + command.number);
545
546         if (command.oldds != null && command.oldds[i] != null)
547         {
548           /*
549            * we are Redoing a Cut, or Undoing a Paste - so
550            * oldds entry contains the cut dataset sequence,
551            * with sequence features in expected place
552            */
553           sequence.setDatasetSequence(command.oldds[i]);
554           command.oldds[i] = oldds;
555         }
556         else
557         {
558           /* 
559            * new cut operation: save the dataset sequence 
560            * so it can be restored in an Undo
561            */
562           if (command.oldds == null)
563           {
564             command.oldds = new SequenceI[command.seqs.length];
565           }
566           command.oldds[i] = oldds;// todo not if !cutIsInternal?
567
568           // do we need to edit sequence features for new sequence ?
569           if (oldds != sequence.getDatasetSequence()
570                   || (cutIsInternal
571                           && sequence.getFeatures().hasFeatures()))
572           // todo or just test cutIsInternal && cutPositions != null ?
573           {
574             if (cutPositions != null)
575             {
576               cutFeatures(command, sequence, cutPositions.getBegin(),
577                               cutPositions.getEnd(), cutIsInternal);
578             }
579           }
580         }
581         SequenceI newDs = sequence.getDatasetSequence();
582         if (newDs != ds && command.al != null
583                 && command.al.getDataset() != null
584                 && !command.al.getDataset().getSequences().contains(newDs))
585         {
586           command.al.getDataset().addSequence(newDs);
587         }
588       }
589
590       if (sequence.getLength() < 1)
591       {
592         command.al.deleteSequence(sequence);
593         seqDeleted = true;
594       }
595     }
596
597     adjustAnnotations(command, false, seqDeleted, views);
598   }
599
600   /**
601    * Perform the given Paste command. This may be to add cut or copied sequences
602    * to an alignment, or to undo a 'Cut' action on a region of the alignment.
603    * 
604    * @param command
605    * @param views
606    */
607   static void paste(Edit command, AlignmentI[] views)
608   {
609     boolean seqWasDeleted = false;
610
611     for (int i = 0; i < command.seqs.length; i++)
612     {
613       boolean newDSNeeded = false;
614       boolean newDSWasNeeded = command.oldds != null
615               && command.oldds[i] != null;
616       SequenceI sequence = command.seqs[i];
617       if (sequence.getLength() < 1)
618       {
619         /*
620          * sequence was deleted; re-add it to the alignment
621          */
622         if (command.alIndex[i] < command.al.getHeight())
623         {
624           List<SequenceI> sequences = command.al.getSequences();
625           synchronized (sequences)
626           {
627             if (!(command.alIndex[i] < 0))
628             {
629               sequences.add(command.alIndex[i], sequence);
630             }
631           }
632         }
633         else
634         {
635           command.al.addSequence(sequence);
636         }
637         seqWasDeleted = true;
638       }
639       int newStart = sequence.getStart();
640       int newEnd = sequence.getEnd();
641
642       StringBuilder tmp = new StringBuilder();
643       tmp.append(sequence.getSequence());
644       // Undo of a delete does not replace original dataset sequence on to
645       // alignment sequence.
646
647       int start = 0;
648       int length = 0;
649
650       if (command.string != null && command.string[i] != null)
651       {
652         if (command.position >= tmp.length())
653         {
654           // This occurs if padding is on, and residues
655           // are removed from end of alignment
656           int len = command.position - tmp.length();
657           while (len > 0)
658           {
659             tmp.append(command.gapChar);
660             len--;
661           }
662         }
663         tmp.insert(command.position, command.string[i]);
664         for (int s = 0; s < command.string[i].length; s++)
665         {
666           if (!Comparison.isGap(command.string[i][s]))
667           {
668             length++;
669             if (!newDSNeeded)
670             {
671               newDSNeeded = true;
672               start = sequence.findPosition(command.position);
673               // end = sequence
674               // .findPosition(command.position + command.number);
675             }
676             if (sequence.getStart() == start)
677             {
678               newStart--;
679             }
680             else
681             {
682               newEnd++;
683             }
684           }
685         }
686         command.string[i] = null;
687       }
688
689       sequence.setSequence(tmp.toString());
690       sequence.setStart(newStart);
691       sequence.setEnd(newEnd);
692
693       /*
694        * command and Undo share the same dataset sequence if cut was
695        * at start or end of sequence
696        */
697       boolean sameDatasetSequence = false;
698       if (newDSNeeded)
699       {
700         if (sequence.getDatasetSequence() != null)
701         {
702           SequenceI ds;
703           if (newDSWasNeeded)
704           {
705             ds = command.oldds[i];
706           }
707           else
708           {
709             // make a new DS sequence
710             // use new ds mechanism here
711             String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
712                     sequence.getSequenceAsString());
713             ds = new Sequence(sequence.getName(), ungapped,
714                     sequence.getStart(), sequence.getEnd());
715             ds.setDescription(sequence.getDescription());
716           }
717           if (command.oldds == null)
718           {
719             command.oldds = new SequenceI[command.seqs.length];
720           }
721           command.oldds[i] = sequence.getDatasetSequence();
722           sameDatasetSequence = ds == sequence.getDatasetSequence();
723           ds.setSequenceFeatures(sequence.getSequenceFeatures());
724           if (!sameDatasetSequence && command.al.getDataset() != null)
725           {
726             // delete 'undone' sequence from alignment dataset
727             command.al.getDataset()
728                     .deleteSequence(sequence.getDatasetSequence());
729           }
730           sequence.setDatasetSequence(ds);
731         }
732         undoCutFeatures(command, command.seqs[i], start, length,
733                 sameDatasetSequence);
734       }
735     }
736     adjustAnnotations(command, true, seqWasDeleted, views);
737
738     command.string = null;
739   }
740
741   static void replace(Edit command)
742   {
743     StringBuilder tmp;
744     String oldstring;
745     int start = command.position;
746     int end = command.number;
747     // TODO TUTORIAL - Fix for replacement with different length of sequence (or
748     // whole sequence)
749     // TODO Jalview 2.4 bugfix change to an aggregate command - original
750     // sequence string is cut, new string is pasted in.
751     command.number = start + command.string[0].length;
752     for (int i = 0; i < command.seqs.length; i++)
753     {
754       boolean newDSWasNeeded = command.oldds != null
755               && command.oldds[i] != null;
756       boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
757
758       /**
759        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
760        * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
761        * viewport.alignment));
762        * 
763        */
764       /**
765        * then addHistoryItem(new EditCommand( "Add sequences",
766        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
767        * 
768        */
769       ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
770               start);
771       ContiguousI afterEditedPositions = command.seqs[i]
772               .findPositions(end + 1, command.seqs[i].getLength());
773       
774       oldstring = command.seqs[i].getSequenceAsString();
775       tmp = new StringBuilder(oldstring.substring(0, start));
776       tmp.append(command.string[i]);
777       String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
778               new String(command.string[i]));
779       if (end < oldstring.length())
780       {
781         tmp.append(oldstring.substring(end));
782       }
783       // stash end prior to updating the sequence object so we can save it if
784       // need be.
785       Range oldstartend = new Range(command.seqs[i].getStart(),
786               command.seqs[i].getEnd());
787       command.seqs[i].setSequence(tmp.toString());
788       command.string[i] = oldstring
789               .substring(start, Math.min(end, oldstring.length()))
790               .toCharArray();
791       String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
792               new String(command.string[i]));
793
794       if (!nogaprep.toLowerCase(Locale.ROOT).equals(nogapold.toLowerCase(Locale.ROOT)))
795       {
796         // we may already have dataset and limits stashed...
797         if (newDSWasNeeded || newStartEndWasNeeded)
798         {
799           if (newDSWasNeeded)
800           {
801           // then just switch the dataset sequence
802           SequenceI oldds = command.seqs[i].getDatasetSequence();
803           command.seqs[i].setDatasetSequence(command.oldds[i]);
804           command.oldds[i] = oldds;
805           }
806           if (newStartEndWasNeeded)
807           {
808             Range newStart = command.oldStartEnd[i];
809             command.oldStartEnd[i] = oldstartend;
810             command.seqs[i].setStart(newStart.getBegin());
811             command.seqs[i].setEnd(newStart.getEnd());
812           }
813         }
814         else         
815         {
816           // decide if we need a new dataset sequence or modify start/end
817           // first edit the original dataset sequence string
818           SequenceI oldds = command.seqs[i].getDatasetSequence();
819           String osp = oldds.getSequenceAsString();
820           int beforeStartOfEdit = -oldds.getStart() + 1
821                   + (beforeEditedPositions == null
822                           ? ((afterEditedPositions != null)
823                                   ? afterEditedPositions.getBegin() - 1
824                                   : oldstartend.getBegin()
825                                           + nogapold.length())
826                           : beforeEditedPositions.getEnd()
827                   );
828           int afterEndOfEdit = -oldds.getStart() + 1
829                   + ((afterEditedPositions == null)
830                   ? oldstartend.getEnd()
831                           : afterEditedPositions.getBegin() - 1);
832           String fullseq = osp.substring(0,
833                   beforeStartOfEdit)
834                   + nogaprep
835                   + osp.substring(afterEndOfEdit);
836
837           // and check if new sequence data is different..
838           if (!fullseq.equalsIgnoreCase(osp))
839           {
840             // old ds and edited ds are different, so
841             // create the new dataset sequence
842             SequenceI newds = new Sequence(oldds);
843             newds.setSequence(fullseq.toUpperCase(Locale.ROOT));
844
845             if (command.oldds == null)
846             {
847               command.oldds = new SequenceI[command.seqs.length];
848             }
849             command.oldds[i] = command.seqs[i].getDatasetSequence();
850
851             // And preserve start/end for good-measure
852
853             if (command.oldStartEnd == null)
854             {
855               command.oldStartEnd = new Range[command.seqs.length];
856             }
857             command.oldStartEnd[i] = oldstartend;
858             // TODO: JAL-1131 ensure newly created dataset sequence is added to
859             // the set of
860             // dataset sequences associated with the alignment.
861             // TODO: JAL-1131 fix up any annotation associated with new dataset
862             // sequence to ensure that original sequence/annotation
863             // relationships
864             // are preserved.
865             command.seqs[i].setDatasetSequence(newds);
866           }
867           else
868           {
869             if (command.oldStartEnd == null)
870             {
871               command.oldStartEnd = new Range[command.seqs.length];
872             }
873             command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
874                     command.seqs[i].getEnd());
875             if (beforeEditedPositions != null
876                     && afterEditedPositions == null)
877             {
878               // modification at end
879               command.seqs[i].setEnd(
880                       beforeEditedPositions.getEnd() + nogaprep.length()
881                               - nogapold.length());
882             }
883             else if (afterEditedPositions != null
884                     && beforeEditedPositions == null)
885             {
886               // modification at start
887               command.seqs[i].setStart(
888                       afterEditedPositions.getBegin() - nogaprep.length());
889             }
890             else
891             {
892               // edit covered both start and end. Here we can only guess the
893               // new
894               // start/end
895               String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
896                       command.seqs[i].getSequenceAsString().toUpperCase(Locale.ROOT));
897               int newStart = command.seqs[i].getDatasetSequence()
898                       .getSequenceAsString().indexOf(nogapalseq);
899               if (newStart == -1)
900               {
901                 throw new Error(
902                         "Implementation Error: could not locate start/end "
903                                 + "in dataset sequence after an edit of the sequence string");
904               }
905               int newEnd = newStart + nogapalseq.length() - 1;
906               command.seqs[i].setStart(newStart);
907               command.seqs[i].setEnd(newEnd);
908             }
909           }
910         }
911       }
912       tmp = null;
913       oldstring = null;
914     }
915   }
916
917   final static void adjustAnnotations(Edit command, boolean insert,
918           boolean modifyVisibility, AlignmentI[] views)
919   {
920     AlignmentAnnotation[] annotations = null;
921
922     if (modifyVisibility && !insert)
923     {
924       // only occurs if a sequence was added or deleted.
925       command.deletedAnnotationRows = new Hashtable<>();
926     }
927     if (command.fullAlignmentHeight)
928     {
929       annotations = command.al.getAlignmentAnnotation();
930     }
931     else
932     {
933       int aSize = 0;
934       AlignmentAnnotation[] tmp;
935       for (int s = 0; s < command.seqs.length; s++)
936       {
937         command.seqs[s].sequenceChanged();
938
939         if (modifyVisibility)
940         {
941           // Rows are only removed or added to sequence object.
942           if (!insert)
943           {
944             // remove rows
945             tmp = command.seqs[s].getAnnotation();
946             if (tmp != null)
947             {
948               int alen = tmp.length;
949               for (int aa = 0; aa < tmp.length; aa++)
950               {
951                 if (!command.al.deleteAnnotation(tmp[aa]))
952                 {
953                   // strip out annotation not in the current al (will be put
954                   // back on insert in all views)
955                   tmp[aa] = null;
956                   alen--;
957                 }
958               }
959               command.seqs[s].setAlignmentAnnotation(null);
960               if (alen != tmp.length)
961               {
962                 // save the non-null annotation references only
963                 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
964                 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
965                 {
966                   if (tmp[aa] != null)
967                   {
968                     saved[aapos++] = tmp[aa];
969                     tmp[aa] = null;
970                   }
971                 }
972                 tmp = saved;
973                 command.deletedAnnotationRows.put(command.seqs[s], saved);
974                 // and then remove any annotation in the other views
975                 for (int alview = 0; views != null
976                         && alview < views.length; alview++)
977                 {
978                   if (views[alview] != command.al)
979                   {
980                     AlignmentAnnotation[] toremove = views[alview]
981                             .getAlignmentAnnotation();
982                     if (toremove == null || toremove.length == 0)
983                     {
984                       continue;
985                     }
986                     // remove any alignment annotation on this sequence that's
987                     // on that alignment view.
988                     for (int aa = 0; aa < toremove.length; aa++)
989                     {
990                       if (toremove[aa].sequenceRef == command.seqs[s])
991                       {
992                         views[alview].deleteAnnotation(toremove[aa]);
993                       }
994                     }
995                   }
996                 }
997               }
998               else
999               {
1000                 // save all the annotation
1001                 command.deletedAnnotationRows.put(command.seqs[s], tmp);
1002               }
1003             }
1004           }
1005           else
1006           {
1007             // recover rows
1008             if (command.deletedAnnotationRows != null
1009                     && command.deletedAnnotationRows
1010                             .containsKey(command.seqs[s]))
1011             {
1012               AlignmentAnnotation[] revealed = command.deletedAnnotationRows
1013                       .get(command.seqs[s]);
1014               command.seqs[s].setAlignmentAnnotation(revealed);
1015               if (revealed != null)
1016               {
1017                 for (int aa = 0; aa < revealed.length; aa++)
1018                 {
1019                   // iterate through al adding original annotation
1020                   command.al.addAnnotation(revealed[aa]);
1021                 }
1022                 for (int aa = 0; aa < revealed.length; aa++)
1023                 {
1024                   command.al.setAnnotationIndex(revealed[aa], aa);
1025                 }
1026                 // and then duplicate added annotation on every other alignment
1027                 // view
1028                 for (int vnum = 0; views != null && 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, sf.getBegin()
1303                     + length, sf.getEnd() + length, sf.getFeatureGroup(),
1304                     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             System.err.println("Can't undo edit action " + action);
1438             // throw new IllegalStateException("Can't undo edit action " +
1439             // action);
1440           }
1441         }
1442       }
1443     }
1444     return result;
1445   }
1446
1447   public class Edit
1448   {
1449     SequenceI[] oldds;
1450
1451     /**
1452      * start and end of sequence prior to edit
1453      */
1454     Range[] oldStartEnd;
1455
1456     boolean fullAlignmentHeight = false;
1457
1458     Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1459
1460     Map<String, Annotation[]> deletedAnnotations;
1461
1462     /*
1463      * features deleted by the cut (re-add on Undo)
1464      * (including the original of any shortened features)
1465      */
1466     Map<SequenceI, List<SequenceFeature>> deletedFeatures;
1467
1468     /*
1469      * shortened features added by the cut (delete on Undo)
1470      */
1471     Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
1472
1473     AlignmentI al;
1474
1475     final Action command;
1476
1477     char[][] string;
1478
1479     SequenceI[] seqs;
1480
1481     int[] alIndex;
1482
1483     int position;
1484
1485     int number;
1486
1487     char gapChar;
1488
1489     /*
1490      * flag that identifies edits inserted to balance 
1491      * user edits in a 'locked editing' region
1492      */
1493     private boolean systemGenerated;
1494
1495     public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1496             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,
1506             AlignmentI align)
1507     {
1508       this(cmd, sqs, pos, count, align.getGapCharacter());
1509
1510       this.al = align;
1511
1512       alIndex = new int[sqs.length];
1513       for (int i = 0; i < sqs.length; i++)
1514       {
1515         alIndex[i] = align.findIndex(sqs[i]);
1516       }
1517
1518       fullAlignmentHeight = (align.getHeight() == sqs.length);
1519     }
1520
1521     /**
1522      * Constructor given a REPLACE command and the replacement string
1523      * 
1524      * @param cmd
1525      * @param sqs
1526      * @param pos
1527      * @param count
1528      * @param align
1529      * @param replace
1530      */
1531     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1532             AlignmentI align, String replace)
1533     {
1534       this(cmd, sqs, pos, count, align);
1535
1536       string = new char[sqs.length][];
1537       for (int i = 0; i < sqs.length; i++)
1538       {
1539         string[i] = replace.toCharArray();
1540       }
1541     }
1542
1543     public SequenceI[] getSequences()
1544     {
1545       return seqs;
1546     }
1547
1548     public int getPosition()
1549     {
1550       return position;
1551     }
1552
1553     public Action getAction()
1554     {
1555       return command;
1556     }
1557
1558     public int getNumber()
1559     {
1560       return number;
1561     }
1562
1563     public char getGapCharacter()
1564     {
1565       return gapChar;
1566     }
1567
1568     public void setSystemGenerated(boolean b)
1569     {
1570       systemGenerated = b;
1571     }
1572
1573     public boolean isSystemGenerated()
1574     {
1575       return systemGenerated;
1576     }
1577   }
1578
1579   /**
1580    * Returns an iterator over the list of edit commands which traverses the list
1581    * either forwards or backwards.
1582    * 
1583    * @param forwards
1584    * @return
1585    */
1586   public Iterator<Edit> getEditIterator(boolean forwards)
1587   {
1588     if (forwards)
1589     {
1590       return getEdits().iterator();
1591     }
1592     else
1593     {
1594       return new ReverseListIterator<>(getEdits());
1595     }
1596   }
1597
1598   /**
1599    * Adjusts features for Cut, and saves details of changes made to allow Undo
1600    * <ul>
1601    * <li>features left of the cut are unchanged</li>
1602    * <li>features right of the cut are shifted left</li>
1603    * <li>features internal to the cut region are deleted</li>
1604    * <li>features that overlap or span the cut are shortened</li>
1605    * <li>the originals of any deleted or shortened features are saved, to re-add
1606    * on Undo</li>
1607    * <li>any added (shortened) features are saved, to delete on Undo</li>
1608    * </ul>
1609    * 
1610    * @param command
1611    * @param seq
1612    * @param fromPosition
1613    * @param toPosition
1614    * @param cutIsInternal
1615    */
1616   protected static void cutFeatures(Edit command, SequenceI seq,
1617           int fromPosition, int toPosition, boolean cutIsInternal)
1618   {
1619     /* 
1620      * if the cut is at start or end of sequence
1621      * then we don't modify the sequence feature store
1622      */
1623     if (!cutIsInternal)
1624     {
1625       return;
1626     }
1627     List<SequenceFeature> added = new ArrayList<>();
1628     List<SequenceFeature> removed = new ArrayList<>();
1629   
1630     SequenceFeaturesI featureStore = seq.getFeatures();
1631     if (toPosition < fromPosition || featureStore == null)
1632     {
1633       return;
1634     }
1635   
1636     int cutStartPos = fromPosition;
1637     int cutEndPos = toPosition;
1638     int cutWidth = cutEndPos - cutStartPos + 1;
1639   
1640     synchronized (featureStore)
1641     {
1642       /*
1643        * get features that overlap the cut region
1644        */
1645       List<SequenceFeature> toAmend = featureStore.findFeatures(
1646               cutStartPos, cutEndPos);
1647   
1648       /*
1649        * add any contact features that span the cut region
1650        * (not returned by findFeatures)
1651        */
1652       for (SequenceFeature contact : featureStore.getContactFeatures())
1653       {
1654         if (contact.getBegin() < cutStartPos
1655                 && contact.getEnd() > cutEndPos)
1656         {
1657           toAmend.add(contact);
1658         }
1659       }
1660
1661       /*
1662        * adjust start-end of overlapping features;
1663        * delete features enclosed by the cut;
1664        * delete partially overlapping contact features
1665        */
1666       for (SequenceFeature sf : toAmend)
1667       {
1668         int sfBegin = sf.getBegin();
1669         int sfEnd = sf.getEnd();
1670         int newBegin = sfBegin;
1671         int newEnd = sfEnd;
1672         boolean toDelete = false;
1673         boolean follows = false;
1674         
1675         if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
1676         {
1677           /*
1678            * feature lies within cut region - delete it
1679            */
1680           toDelete = true;
1681         }
1682         else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
1683         {
1684           /*
1685            * feature spans cut region - left-shift the end
1686            */
1687           newEnd -= cutWidth;
1688         }
1689         else if (sfEnd <= cutEndPos)
1690         {
1691           /*
1692            * feature overlaps left of cut region - truncate right
1693            */
1694           newEnd = cutStartPos - 1;
1695           if (sf.isContactFeature())
1696           {
1697             toDelete = true;
1698           }
1699         }
1700         else if (sfBegin >= cutStartPos)
1701         {
1702           /*
1703            * remaining case - feature overlaps right
1704            * truncate left, adjust end of feature
1705            */
1706           newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
1707           newEnd = newBegin + sfEnd - cutEndPos - 1;
1708           if (sf.isContactFeature())
1709           {
1710             toDelete = true;
1711           }
1712         }
1713   
1714         seq.deleteFeature(sf);
1715         if (!follows)
1716         {
1717           removed.add(sf);
1718         }
1719         if (!toDelete)
1720         {
1721           SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1722                   sf.getFeatureGroup(), sf.getScore());
1723           seq.addSequenceFeature(copy);
1724           if (!follows)
1725           {
1726             added.add(copy);
1727           }
1728         }
1729       }
1730   
1731       /*
1732        * and left shift any features lying to the right of the cut region
1733        */
1734
1735       featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
1736     }
1737
1738     /*
1739      * save deleted and amended features, so that Undo can 
1740      * re-add or delete them respectively
1741      */
1742     if (command.deletedFeatures == null)
1743     {
1744       command.deletedFeatures = new HashMap<>();
1745     }
1746     if (command.truncatedFeatures == null)
1747     {
1748       command.truncatedFeatures = new HashMap<>();
1749     }
1750     command.deletedFeatures.put(seq, removed);
1751     command.truncatedFeatures.put(seq, added);
1752   }
1753 }