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