JAL-2446 merged to spike branch
[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 desc)
126   {
127     this.description = desc;
128   }
129
130   public EditCommand(String desc, Action command, SequenceI[] seqs,
131           int position, int number, AlignmentI al)
132   {
133     this.description = desc;
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 desc, Action command, String replace,
143           SequenceI[] seqs, int position, int number, AlignmentI al)
144   {
145     this.description = desc;
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.getFeatures().hasFeatures())
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         command.seqs[s].sequenceChanged();
802
803         if (modifyVisibility)
804         {
805           // Rows are only removed or added to sequence object.
806           if (!insert)
807           {
808             // remove rows
809             tmp = command.seqs[s].getAnnotation();
810             if (tmp != null)
811             {
812               int alen = tmp.length;
813               for (int aa = 0; aa < tmp.length; aa++)
814               {
815                 if (!command.al.deleteAnnotation(tmp[aa]))
816                 {
817                   // strip out annotation not in the current al (will be put
818                   // back on insert in all views)
819                   tmp[aa] = null;
820                   alen--;
821                 }
822               }
823               command.seqs[s].setAlignmentAnnotation(null);
824               if (alen != tmp.length)
825               {
826                 // save the non-null annotation references only
827                 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
828                 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
829                 {
830                   if (tmp[aa] != null)
831                   {
832                     saved[aapos++] = tmp[aa];
833                     tmp[aa] = null;
834                   }
835                 }
836                 tmp = saved;
837                 command.deletedAnnotationRows.put(command.seqs[s], saved);
838                 // and then remove any annotation in the other views
839                 for (int alview = 0; views != null && alview < views.length; alview++)
840                 {
841                   if (views[alview] != command.al)
842                   {
843                     AlignmentAnnotation[] toremove = views[alview]
844                             .getAlignmentAnnotation();
845                     if (toremove == null || toremove.length == 0)
846                     {
847                       continue;
848                     }
849                     // remove any alignment annotation on this sequence that's
850                     // on that alignment view.
851                     for (int aa = 0; aa < toremove.length; aa++)
852                     {
853                       if (toremove[aa].sequenceRef == command.seqs[s])
854                       {
855                         views[alview].deleteAnnotation(toremove[aa]);
856                       }
857                     }
858                   }
859                 }
860               }
861               else
862               {
863                 // save all the annotation
864                 command.deletedAnnotationRows.put(command.seqs[s], tmp);
865               }
866             }
867           }
868           else
869           {
870             // recover rows
871             if (command.deletedAnnotationRows != null
872                     && command.deletedAnnotationRows
873                             .containsKey(command.seqs[s]))
874             {
875               AlignmentAnnotation[] revealed = command.deletedAnnotationRows
876                       .get(command.seqs[s]);
877               command.seqs[s].setAlignmentAnnotation(revealed);
878               if (revealed != null)
879               {
880                 for (int aa = 0; aa < revealed.length; aa++)
881                 {
882                   // iterate through al adding original annotation
883                   command.al.addAnnotation(revealed[aa]);
884                 }
885                 for (int aa = 0; aa < revealed.length; aa++)
886                 {
887                   command.al.setAnnotationIndex(revealed[aa], aa);
888                 }
889                 // and then duplicate added annotation on every other alignment
890                 // view
891                 for (int vnum = 0; views != null && vnum < views.length; vnum++)
892                 {
893                   if (views[vnum] != command.al)
894                   {
895                     int avwidth = views[vnum].getWidth() + 1;
896                     // duplicate in this view
897                     for (int a = 0; a < revealed.length; a++)
898                     {
899                       AlignmentAnnotation newann = new AlignmentAnnotation(
900                               revealed[a]);
901                       command.seqs[s].addAlignmentAnnotation(newann);
902                       newann.padAnnotation(avwidth);
903                       views[vnum].addAnnotation(newann);
904                       views[vnum].setAnnotationIndex(newann, a);
905                     }
906                   }
907                 }
908               }
909             }
910           }
911           continue;
912         }
913
914         if (command.seqs[s].getAnnotation() == null)
915         {
916           continue;
917         }
918
919         if (aSize == 0)
920         {
921           annotations = command.seqs[s].getAnnotation();
922         }
923         else
924         {
925           tmp = new AlignmentAnnotation[aSize
926                   + command.seqs[s].getAnnotation().length];
927
928           System.arraycopy(annotations, 0, tmp, 0, aSize);
929
930           System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
931                   command.seqs[s].getAnnotation().length);
932
933           annotations = tmp;
934         }
935         aSize = annotations.length;
936       }
937     }
938
939     if (annotations == null)
940     {
941       return;
942     }
943
944     if (!insert)
945     {
946       command.deletedAnnotations = new Hashtable<String, Annotation[]>();
947     }
948
949     int aSize;
950     Annotation[] temp;
951     for (int a = 0; a < annotations.length; a++)
952     {
953       if (annotations[a].autoCalculated
954               || annotations[a].annotations == null)
955       {
956         continue;
957       }
958
959       int tSize = 0;
960
961       aSize = annotations[a].annotations.length;
962       if (insert)
963       {
964         temp = new Annotation[aSize + command.number];
965         if (annotations[a].padGaps)
966         {
967           for (int aa = 0; aa < temp.length; aa++)
968           {
969             temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
970           }
971         }
972       }
973       else
974       {
975         if (command.position < aSize)
976         {
977           if (command.position + command.number >= aSize)
978           {
979             tSize = aSize;
980           }
981           else
982           {
983             tSize = aSize - command.number;
984           }
985         }
986         else
987         {
988           tSize = aSize;
989         }
990
991         if (tSize < 0)
992         {
993           tSize = aSize;
994         }
995         temp = new Annotation[tSize];
996       }
997
998       if (insert)
999       {
1000         if (command.position < annotations[a].annotations.length)
1001         {
1002           System.arraycopy(annotations[a].annotations, 0, temp, 0,
1003                   command.position);
1004
1005           if (command.deletedAnnotations != null
1006                   && command.deletedAnnotations
1007                           .containsKey(annotations[a].annotationId))
1008           {
1009             Annotation[] restore = command.deletedAnnotations
1010                     .get(annotations[a].annotationId);
1011
1012             System.arraycopy(restore, 0, temp, command.position,
1013                     command.number);
1014
1015           }
1016
1017           System.arraycopy(annotations[a].annotations, command.position,
1018                   temp, command.position + command.number, aSize
1019                           - command.position);
1020         }
1021         else
1022         {
1023           if (command.deletedAnnotations != null
1024                   && command.deletedAnnotations
1025                           .containsKey(annotations[a].annotationId))
1026           {
1027             Annotation[] restore = command.deletedAnnotations
1028                     .get(annotations[a].annotationId);
1029
1030             temp = new Annotation[annotations[a].annotations.length
1031                     + restore.length];
1032             System.arraycopy(annotations[a].annotations, 0, temp, 0,
1033                     annotations[a].annotations.length);
1034             System.arraycopy(restore, 0, temp,
1035                     annotations[a].annotations.length, restore.length);
1036           }
1037           else
1038           {
1039             temp = annotations[a].annotations;
1040           }
1041         }
1042       }
1043       else
1044       {
1045         if (tSize != aSize || command.position < 2)
1046         {
1047           int copylen = Math.min(command.position,
1048                   annotations[a].annotations.length);
1049           if (copylen > 0)
1050           {
1051             System.arraycopy(annotations[a].annotations, 0, temp, 0,
1052                     copylen); // command.position);
1053           }
1054
1055           Annotation[] deleted = new Annotation[command.number];
1056           if (copylen >= command.position)
1057           {
1058             copylen = Math.min(command.number,
1059                     annotations[a].annotations.length - command.position);
1060             if (copylen > 0)
1061             {
1062               System.arraycopy(annotations[a].annotations,
1063                       command.position, deleted, 0, copylen); // command.number);
1064             }
1065           }
1066
1067           command.deletedAnnotations.put(annotations[a].annotationId,
1068                   deleted);
1069           if (annotations[a].annotations.length > command.position
1070                   + command.number)
1071           {
1072             System.arraycopy(annotations[a].annotations, command.position
1073                     + command.number, temp, command.position,
1074                     annotations[a].annotations.length - command.position
1075                             - command.number); // aSize
1076           }
1077         }
1078         else
1079         {
1080           int dSize = aSize - command.position;
1081
1082           if (dSize > 0)
1083           {
1084             Annotation[] deleted = new Annotation[command.number];
1085             System.arraycopy(annotations[a].annotations, command.position,
1086                     deleted, 0, dSize);
1087
1088             command.deletedAnnotations.put(annotations[a].annotationId,
1089                     deleted);
1090
1091             tSize = Math.min(annotations[a].annotations.length,
1092                     command.position);
1093             temp = new Annotation[tSize];
1094             System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1095           }
1096           else
1097           {
1098             temp = annotations[a].annotations;
1099           }
1100         }
1101       }
1102
1103       annotations[a].annotations = temp;
1104     }
1105   }
1106
1107   final static void adjustFeatures(Edit command, int index, final int i,
1108           final int j, boolean insert)
1109   {
1110     SequenceI seq = command.seqs[index];
1111     SequenceI sequence = seq.getDatasetSequence();
1112     if (sequence == null)
1113     {
1114       sequence = seq;
1115     }
1116
1117     if (insert)
1118     {
1119       if (command.editedFeatures != null
1120               && command.editedFeatures.containsKey(seq))
1121       {
1122         sequence.setSequenceFeatures(command.editedFeatures.get(seq));
1123       }
1124
1125       return;
1126     }
1127
1128     List<SequenceFeature> sf = sequence.getFeatures()
1129             .getPositionalFeatures();
1130
1131     if (sf.isEmpty())
1132     {
1133       return;
1134     }
1135
1136     List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
1137
1138     int cSize = j - i;
1139
1140     for (SequenceFeature feature : sf)
1141     {
1142       SequenceFeature copy = new SequenceFeature(feature);
1143
1144       oldsf.add(copy);
1145
1146       if (feature.getEnd() < i)
1147       {
1148         continue;
1149       }
1150
1151       if (feature.getBegin() > j)
1152       {
1153         int newBegin = copy.getBegin() - cSize;
1154         int newEnd = copy.getEnd() - cSize;
1155         SequenceFeature newSf = new SequenceFeature(feature, newBegin,
1156                 newEnd, feature.getFeatureGroup(), feature.getScore());
1157         sequence.deleteFeature(feature);
1158         sequence.addSequenceFeature(newSf);
1159         // feature.setBegin(newBegin);
1160         // feature.setEnd(newEnd);
1161         continue;
1162       }
1163
1164       int newBegin = feature.getBegin();
1165       int newEnd = feature.getEnd();
1166       if (newBegin >= i)
1167       {
1168         newBegin = i;
1169         // feature.setBegin(i);
1170       }
1171
1172       if (newEnd < j)
1173       {
1174         newEnd = j - 1;
1175         // feature.setEnd(j - 1);
1176       }
1177       newEnd = newEnd - cSize;
1178       // feature.setEnd(feature.getEnd() - (cSize));
1179
1180       sequence.deleteFeature(feature);
1181       if (newEnd >= newBegin)
1182       {
1183         sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
1184                 newEnd, feature.getFeatureGroup(), feature.getScore()));
1185       }
1186       // if (feature.getBegin() > feature.getEnd())
1187       // {
1188       // sequence.deleteFeature(feature);
1189       // }
1190     }
1191
1192     if (command.editedFeatures == null)
1193     {
1194       command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
1195     }
1196
1197     command.editedFeatures.put(seq, oldsf);
1198
1199   }
1200
1201   /**
1202    * Returns the list of edit commands wrapped by this object.
1203    * 
1204    * @return
1205    */
1206   public List<Edit> getEdits()
1207   {
1208     return this.edits;
1209   }
1210
1211   /**
1212    * Returns a map whose keys are the dataset sequences, and values their
1213    * aligned sequences before the command edit list was applied. The aligned
1214    * sequences are copies, which may be updated without affecting the originals.
1215    * 
1216    * The command holds references to the aligned sequences (after editing). If
1217    * the command is an 'undo',then the prior state is simply the aligned state.
1218    * Otherwise, we have to derive the prior state by working backwards through
1219    * the edit list to infer the aligned sequences before editing.
1220    * 
1221    * Note: an alternative solution would be to cache the 'before' state of each
1222    * edit, but this would be expensive in space in the common case that the
1223    * original is never needed (edits are not mirrored).
1224    * 
1225    * @return
1226    * @throws IllegalStateException
1227    *           on detecting an edit command of a type that can't be unwound
1228    */
1229   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1230   {
1231     Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
1232     if (getEdits() == null)
1233     {
1234       return result;
1235     }
1236     if (forUndo)
1237     {
1238       for (Edit e : getEdits())
1239       {
1240         for (SequenceI seq : e.getSequences())
1241         {
1242           SequenceI ds = seq.getDatasetSequence();
1243           // SequenceI preEdit = result.get(ds);
1244           if (!result.containsKey(ds))
1245           {
1246             /*
1247              * copy sequence including start/end (but don't use copy constructor
1248              * as we don't need annotations)
1249              */
1250             SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1251                     seq.getStart(), seq.getEnd());
1252             preEdit.setDatasetSequence(ds);
1253             result.put(ds, preEdit);
1254           }
1255         }
1256       }
1257       return result;
1258     }
1259
1260     /*
1261      * Work backwards through the edit list, deriving the sequences before each
1262      * was applied. The final result is the sequence set before any edits.
1263      */
1264     Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
1265     while (editList.hasNext())
1266     {
1267       Edit oldEdit = editList.next();
1268       Action action = oldEdit.getAction();
1269       int position = oldEdit.getPosition();
1270       int number = oldEdit.getNumber();
1271       final char gap = oldEdit.getGapCharacter();
1272       for (SequenceI seq : oldEdit.getSequences())
1273       {
1274         SequenceI ds = seq.getDatasetSequence();
1275         SequenceI preEdit = result.get(ds);
1276         if (preEdit == null)
1277         {
1278           preEdit = new Sequence("", seq.getSequenceAsString(),
1279                   seq.getStart(), seq.getEnd());
1280           preEdit.setDatasetSequence(ds);
1281           result.put(ds, preEdit);
1282         }
1283         /*
1284          * 'Undo' this edit action on the sequence (updating the value in the
1285          * map).
1286          */
1287         if (ds != null)
1288         {
1289           if (action == Action.DELETE_GAP)
1290           {
1291             preEdit.setSequence(new String(StringUtils.insertCharAt(
1292                     preEdit.getSequence(), position, number, gap)));
1293           }
1294           else if (action == Action.INSERT_GAP)
1295           {
1296             preEdit.setSequence(new String(StringUtils.deleteChars(
1297                     preEdit.getSequence(), position, position + number)));
1298           }
1299           else
1300           {
1301             System.err.println("Can't undo edit action " + action);
1302             // throw new IllegalStateException("Can't undo edit action " +
1303             // action);
1304           }
1305         }
1306       }
1307     }
1308     return result;
1309   }
1310
1311   public class Edit
1312   {
1313     public SequenceI[] oldds;
1314
1315     boolean fullAlignmentHeight = false;
1316
1317     Hashtable<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1318
1319     Hashtable<String, Annotation[]> deletedAnnotations;
1320
1321     Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
1322
1323     AlignmentI al;
1324
1325     Action command;
1326
1327     char[][] string;
1328
1329     SequenceI[] seqs;
1330
1331     int[] alIndex;
1332
1333     int position, number;
1334
1335     char gapChar;
1336
1337     public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1338             char gap)
1339     {
1340       this.command = cmd;
1341       this.seqs = sqs;
1342       this.position = pos;
1343       this.number = count;
1344       this.gapChar = gap;
1345     }
1346
1347     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1348             AlignmentI align)
1349     {
1350       this.gapChar = align.getGapCharacter();
1351       this.command = cmd;
1352       this.seqs = sqs;
1353       this.position = pos;
1354       this.number = count;
1355       this.al = align;
1356
1357       alIndex = new int[sqs.length];
1358       for (int i = 0; i < sqs.length; i++)
1359       {
1360         alIndex[i] = align.findIndex(sqs[i]);
1361       }
1362
1363       fullAlignmentHeight = (align.getHeight() == sqs.length);
1364     }
1365
1366     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1367             AlignmentI align, String replace)
1368     {
1369       this.command = cmd;
1370       this.seqs = sqs;
1371       this.position = pos;
1372       this.number = count;
1373       this.al = align;
1374       this.gapChar = align.getGapCharacter();
1375       string = new char[sqs.length][];
1376       for (int i = 0; i < sqs.length; i++)
1377       {
1378         string[i] = replace.toCharArray();
1379       }
1380
1381       fullAlignmentHeight = (align.getHeight() == sqs.length);
1382     }
1383
1384     public SequenceI[] getSequences()
1385     {
1386       return seqs;
1387     }
1388
1389     public int getPosition()
1390     {
1391       return position;
1392     }
1393
1394     public Action getAction()
1395     {
1396       return command;
1397     }
1398
1399     public int getNumber()
1400     {
1401       return number;
1402     }
1403
1404     public char getGapCharacter()
1405     {
1406       return gapChar;
1407     }
1408   }
1409
1410   /**
1411    * Returns an iterator over the list of edit commands which traverses the list
1412    * either forwards or backwards.
1413    * 
1414    * @param forwards
1415    * @return
1416    */
1417   public Iterator<Edit> getEditIterator(boolean forwards)
1418   {
1419     if (forwards)
1420     {
1421       return getEdits().iterator();
1422     }
1423     else
1424     {
1425       return new ReverseListIterator<Edit>(getEdits());
1426     }
1427   }
1428 }