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