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