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