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