JAL-3099 use Alignment.getVisibleWidth() where wanted
[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>,
226      * or</li>
227      * <li>a new Delete <n> gaps which is <n> positions to the left of the last
228      * delete.</li>
229      * </ul>
230      */
231     boolean contiguous = (action == Action.INSERT_GAP
232             && e.position == lastEdit.position + lastEdit.number)
233             || (action == Action.DELETE_GAP
234                     && 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:
481       // "+command.number);
482     }
483
484     adjustAnnotations(command, true, false, null);
485   }
486
487   //
488   // final void insertNuc(Edit command)
489   // {
490   //
491   // for (int s = 0; s < command.seqs.length; s++)
492   // {
493   // System.out.println("pos: "+command.position+" number: "+command.number);
494   // command.seqs[s].insertCharAt(command.position, command.number,'A');
495   // }
496   //
497   // adjustAnnotations(command, true, false, null);
498   // }
499
500   /**
501    * Delete gap(s) in sequences as specified by the command, and adjust
502    * annotations.
503    * 
504    * @param command
505    */
506   final static private void deleteGap(Edit command)
507   {
508     for (int s = 0; s < command.seqs.length; s++)
509     {
510       command.seqs[s].deleteChars(command.position,
511               command.position + command.number);
512     }
513
514     adjustAnnotations(command, false, false, null);
515   }
516
517   /**
518    * Carry out a Cut action. The cut characters are saved in case Undo is
519    * requested.
520    * 
521    * @param command
522    * @param views
523    */
524   static void cut(Edit command, AlignmentI[] views)
525   {
526     boolean seqDeleted = false;
527     command.string = new char[command.seqs.length][];
528
529     for (int i = 0; i < command.seqs.length; i++)
530     {
531       final SequenceI sequence = command.seqs[i];
532       if (sequence.getLength() > command.position)
533       {
534         command.string[i] = sequence.getSequence(command.position,
535                 command.position + command.number);
536         SequenceI oldds = sequence.getDatasetSequence();
537         if (command.oldds != null && command.oldds[i] != null)
538         {
539           // we are redoing an undone cut.
540           sequence.setDatasetSequence(null);
541         }
542         sequence.deleteChars(command.position,
543                 command.position + command.number);
544         if (command.oldds != null && command.oldds[i] != null)
545         {
546           // oldds entry contains the cut dataset sequence.
547           sequence.setDatasetSequence(command.oldds[i]);
548           command.oldds[i] = oldds;
549         }
550         else
551         {
552           // modify the oldds if necessary
553           if (oldds != sequence.getDatasetSequence()
554                   || sequence.getFeatures().hasFeatures())
555           {
556             if (command.oldds == null)
557             {
558               command.oldds = new SequenceI[command.seqs.length];
559             }
560             command.oldds[i] = oldds;
561             // FIXME JAL-2541 JAL-2526 get correct positions if on a gap
562             adjustFeatures(
563                     command,
564                     i,
565                     sequence.findPosition(command.position),
566                     sequence.findPosition(command.position + command.number),
567                     false);
568           }
569         }
570       }
571
572       if (sequence.getLength() < 1)
573       {
574         command.al.deleteSequence(sequence);
575         seqDeleted = true;
576       }
577     }
578
579     adjustAnnotations(command, false, seqDeleted, views);
580   }
581
582   /**
583    * Perform the given Paste command. This may be to add cut or copied sequences
584    * to an alignment, or to undo a 'Cut' action on a region of the alignment.
585    * 
586    * @param command
587    * @param views
588    */
589   static void paste(Edit command, AlignmentI[] views)
590   {
591     StringBuffer tmp;
592     boolean newDSNeeded;
593     boolean newDSWasNeeded;
594     int newstart, newend;
595     boolean seqWasDeleted = false;
596     int start = 0, end = 0;
597
598     for (int i = 0; i < command.seqs.length; i++)
599     {
600       newDSNeeded = false;
601       newDSWasNeeded = command.oldds != null && command.oldds[i] != null;
602       if (command.seqs[i].getLength() < 1)
603       {
604         // ie this sequence was deleted, we need to
605         // readd it to the alignment
606         if (command.alIndex[i] < command.al.getHeight())
607         {
608           List<SequenceI> sequences;
609           synchronized (sequences = command.al.getSequences())
610           {
611             if (!(command.alIndex[i] < 0))
612             {
613               sequences.add(command.alIndex[i], command.seqs[i]);
614             }
615           }
616         }
617         else
618         {
619           command.al.addSequence(command.seqs[i]);
620         }
621         seqWasDeleted = true;
622       }
623       newstart = command.seqs[i].getStart();
624       newend = command.seqs[i].getEnd();
625
626       tmp = new StringBuffer();
627       tmp.append(command.seqs[i].getSequence());
628       // Undo of a delete does not replace original dataset sequence on to
629       // alignment sequence.
630
631       if (command.string != null && command.string[i] != null)
632       {
633         if (command.position >= tmp.length())
634         {
635           // This occurs if padding is on, and residues
636           // are removed from end of alignment
637           int length = command.position - tmp.length();
638           while (length > 0)
639           {
640             tmp.append(command.gapChar);
641             length--;
642           }
643         }
644         tmp.insert(command.position, command.string[i]);
645         for (int s = 0; s < command.string[i].length; s++)
646         {
647           if (jalview.schemes.ResidueProperties.aaIndex[command.string[i][s]] != 23)
648           {
649             if (!newDSNeeded)
650             {
651               newDSNeeded = true;
652               start = command.seqs[i].findPosition(command.position);
653               end = command.seqs[i]
654                       .findPosition(command.position + command.number);
655             }
656             if (command.seqs[i].getStart() == start)
657             {
658               newstart--;
659             }
660             else
661             {
662               newend++;
663             }
664           }
665         }
666         command.string[i] = null;
667       }
668
669       command.seqs[i].setSequence(tmp.toString());
670       command.seqs[i].setStart(newstart);
671       command.seqs[i].setEnd(newend);
672       if (newDSNeeded)
673       {
674         if (command.seqs[i].getDatasetSequence() != null)
675         {
676           SequenceI ds;
677           if (newDSWasNeeded)
678           {
679             ds = command.oldds[i];
680           }
681           else
682           {
683             // make a new DS sequence
684             // use new ds mechanism here
685             ds = new Sequence(command.seqs[i].getName(),
686                     jalview.analysis.AlignSeq.extractGaps(
687                             jalview.util.Comparison.GapChars,
688                             command.seqs[i].getSequenceAsString()),
689                     command.seqs[i].getStart(), command.seqs[i].getEnd());
690             ds.setDescription(command.seqs[i].getDescription());
691           }
692           if (command.oldds == null)
693           {
694             command.oldds = new SequenceI[command.seqs.length];
695           }
696           command.oldds[i] = command.seqs[i].getDatasetSequence();
697           command.seqs[i].setDatasetSequence(ds);
698         }
699         adjustFeatures(command, i, start, end, true);
700       }
701     }
702     adjustAnnotations(command, true, seqWasDeleted, views);
703
704     command.string = null;
705   }
706
707   static void replace(Edit command)
708   {
709     StringBuffer tmp;
710     String oldstring;
711     int start = command.position;
712     int end = command.number;
713     // TODO TUTORIAL - Fix for replacement with different length of sequence (or
714     // whole sequence)
715     // TODO Jalview 2.4 bugfix change to an aggregate command - original
716     // sequence string is cut, new string is pasted in.
717     command.number = start + command.string[0].length;
718     for (int i = 0; i < command.seqs.length; i++)
719     {
720       boolean newDSWasNeeded = command.oldds != null
721               && command.oldds[i] != null;
722
723       /**
724        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
725        * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
726        * viewport.alignment));
727        * 
728        */
729       /**
730        * then addHistoryItem(new EditCommand( "Add sequences",
731        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
732        * 
733        */
734       oldstring = command.seqs[i].getSequenceAsString();
735       tmp = new StringBuffer(oldstring.substring(0, start));
736       tmp.append(command.string[i]);
737       String nogaprep = jalview.analysis.AlignSeq.extractGaps(
738               jalview.util.Comparison.GapChars,
739               new String(command.string[i]));
740       int ipos = command.seqs[i].findPosition(start)
741               - command.seqs[i].getStart();
742       tmp.append(oldstring.substring(end));
743       command.seqs[i].setSequence(tmp.toString());
744       command.string[i] = oldstring.substring(start, end).toCharArray();
745       String nogapold = jalview.analysis.AlignSeq.extractGaps(
746               jalview.util.Comparison.GapChars,
747               new String(command.string[i]));
748       if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
749       {
750         if (newDSWasNeeded)
751         {
752           SequenceI oldds = command.seqs[i].getDatasetSequence();
753           command.seqs[i].setDatasetSequence(command.oldds[i]);
754           command.oldds[i] = oldds;
755         }
756         else
757         {
758           if (command.oldds == null)
759           {
760             command.oldds = new SequenceI[command.seqs.length];
761           }
762           command.oldds[i] = command.seqs[i].getDatasetSequence();
763           SequenceI newds = new Sequence(
764                   command.seqs[i].getDatasetSequence());
765           String fullseq, osp = newds.getSequenceAsString();
766           fullseq = osp.substring(0, ipos) + nogaprep
767                   + osp.substring(ipos + nogaprep.length());
768           newds.setSequence(fullseq.toUpperCase());
769           // TODO: JAL-1131 ensure newly created dataset sequence is added to
770           // the set of
771           // dataset sequences associated with the alignment.
772           // TODO: JAL-1131 fix up any annotation associated with new dataset
773           // sequence to ensure that original sequence/annotation relationships
774           // are preserved.
775           command.seqs[i].setDatasetSequence(newds);
776
777         }
778       }
779       tmp = null;
780       oldstring = null;
781     }
782   }
783
784   final static void adjustAnnotations(Edit command, boolean insert,
785           boolean modifyVisibility, AlignmentI[] views)
786   {
787     AlignmentAnnotation[] annotations = null;
788
789     if (modifyVisibility && !insert)
790     {
791       // only occurs if a sequence was added or deleted.
792       command.deletedAnnotationRows = new Hashtable<SequenceI, AlignmentAnnotation[]>();
793     }
794     if (command.fullAlignmentHeight)
795     {
796       annotations = command.al.getAlignmentAnnotation();
797     }
798     else
799     {
800       int aSize = 0;
801       AlignmentAnnotation[] tmp;
802       for (int s = 0; s < command.seqs.length; s++)
803       {
804         command.seqs[s].sequenceChanged();
805
806         if (modifyVisibility)
807         {
808           // Rows are only removed or added to sequence object.
809           if (!insert)
810           {
811             // remove rows
812             tmp = command.seqs[s].getAnnotation();
813             if (tmp != null)
814             {
815               int alen = tmp.length;
816               for (int aa = 0; aa < tmp.length; aa++)
817               {
818                 if (!command.al.deleteAnnotation(tmp[aa]))
819                 {
820                   // strip out annotation not in the current al (will be put
821                   // back on insert in all views)
822                   tmp[aa] = null;
823                   alen--;
824                 }
825               }
826               command.seqs[s].setAlignmentAnnotation(null);
827               if (alen != tmp.length)
828               {
829                 // save the non-null annotation references only
830                 AlignmentAnnotation[] saved = new AlignmentAnnotation[alen];
831                 for (int aa = 0, aapos = 0; aa < tmp.length; aa++)
832                 {
833                   if (tmp[aa] != null)
834                   {
835                     saved[aapos++] = tmp[aa];
836                     tmp[aa] = null;
837                   }
838                 }
839                 tmp = saved;
840                 command.deletedAnnotationRows.put(command.seqs[s], saved);
841                 // and then remove any annotation in the other views
842                 for (int alview = 0; views != null
843                         && alview < views.length; alview++)
844                 {
845                   if (views[alview] != command.al)
846                   {
847                     AlignmentAnnotation[] toremove = views[alview]
848                             .getAlignmentAnnotation();
849                     if (toremove == null || toremove.length == 0)
850                     {
851                       continue;
852                     }
853                     // remove any alignment annotation on this sequence that's
854                     // on that alignment view.
855                     for (int aa = 0; aa < toremove.length; aa++)
856                     {
857                       if (toremove[aa].sequenceRef == command.seqs[s])
858                       {
859                         views[alview].deleteAnnotation(toremove[aa]);
860                       }
861                     }
862                   }
863                 }
864               }
865               else
866               {
867                 // save all the annotation
868                 command.deletedAnnotationRows.put(command.seqs[s], tmp);
869               }
870             }
871           }
872           else
873           {
874             // recover rows
875             if (command.deletedAnnotationRows != null
876                     && command.deletedAnnotationRows
877                             .containsKey(command.seqs[s]))
878             {
879               AlignmentAnnotation[] revealed = command.deletedAnnotationRows
880                       .get(command.seqs[s]);
881               command.seqs[s].setAlignmentAnnotation(revealed);
882               if (revealed != null)
883               {
884                 for (int aa = 0; aa < revealed.length; aa++)
885                 {
886                   // iterate through al adding original annotation
887                   command.al.addAnnotation(revealed[aa]);
888                 }
889                 for (int aa = 0; aa < revealed.length; aa++)
890                 {
891                   command.al.setAnnotationIndex(revealed[aa], aa);
892                 }
893                 // and then duplicate added annotation on every other alignment
894                 // view
895                 for (int vnum = 0; views != null
896                         && vnum < views.length; vnum++)
897                 {
898                   if (views[vnum] != command.al)
899                   {
900                     int avwidth = views[vnum].getWidth() + 1;
901                     // duplicate in this view
902                     for (int a = 0; a < revealed.length; a++)
903                     {
904                       AlignmentAnnotation newann = new AlignmentAnnotation(
905                               revealed[a]);
906                       command.seqs[s].addAlignmentAnnotation(newann);
907                       newann.padAnnotation(avwidth);
908                       views[vnum].addAnnotation(newann);
909                       views[vnum].setAnnotationIndex(newann, a);
910                     }
911                   }
912                 }
913               }
914             }
915           }
916           continue;
917         }
918
919         if (command.seqs[s].getAnnotation() == null)
920         {
921           continue;
922         }
923
924         if (aSize == 0)
925         {
926           annotations = command.seqs[s].getAnnotation();
927         }
928         else
929         {
930           tmp = new AlignmentAnnotation[aSize
931                   + command.seqs[s].getAnnotation().length];
932
933           System.arraycopy(annotations, 0, tmp, 0, aSize);
934
935           System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize,
936                   command.seqs[s].getAnnotation().length);
937
938           annotations = tmp;
939         }
940         aSize = annotations.length;
941       }
942     }
943
944     if (annotations == null)
945     {
946       return;
947     }
948
949     if (!insert)
950     {
951       command.deletedAnnotations = new Hashtable<String, Annotation[]>();
952     }
953
954     int aSize;
955     Annotation[] temp;
956     for (int a = 0; a < annotations.length; a++)
957     {
958       if (annotations[a].autoCalculated
959               || annotations[a].annotations == null)
960       {
961         continue;
962       }
963
964       int tSize = 0;
965
966       aSize = annotations[a].annotations.length;
967       if (insert)
968       {
969         temp = new Annotation[aSize + command.number];
970         if (annotations[a].padGaps)
971         {
972           for (int aa = 0; aa < temp.length; aa++)
973           {
974             temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
975           }
976         }
977       }
978       else
979       {
980         if (command.position < aSize)
981         {
982           if (command.position + command.number >= aSize)
983           {
984             tSize = aSize;
985           }
986           else
987           {
988             tSize = aSize - command.number;
989           }
990         }
991         else
992         {
993           tSize = aSize;
994         }
995
996         if (tSize < 0)
997         {
998           tSize = aSize;
999         }
1000         temp = new Annotation[tSize];
1001       }
1002
1003       if (insert)
1004       {
1005         if (command.position < annotations[a].annotations.length)
1006         {
1007           System.arraycopy(annotations[a].annotations, 0, temp, 0,
1008                   command.position);
1009
1010           if (command.deletedAnnotations != null
1011                   && command.deletedAnnotations
1012                           .containsKey(annotations[a].annotationId))
1013           {
1014             Annotation[] restore = command.deletedAnnotations
1015                     .get(annotations[a].annotationId);
1016
1017             System.arraycopy(restore, 0, temp, command.position,
1018                     command.number);
1019
1020           }
1021
1022           System.arraycopy(annotations[a].annotations, command.position,
1023                   temp, command.position + command.number,
1024                   aSize - command.position);
1025         }
1026         else
1027         {
1028           if (command.deletedAnnotations != null
1029                   && command.deletedAnnotations
1030                           .containsKey(annotations[a].annotationId))
1031           {
1032             Annotation[] restore = command.deletedAnnotations
1033                     .get(annotations[a].annotationId);
1034
1035             temp = new Annotation[annotations[a].annotations.length
1036                     + restore.length];
1037             System.arraycopy(annotations[a].annotations, 0, temp, 0,
1038                     annotations[a].annotations.length);
1039             System.arraycopy(restore, 0, temp,
1040                     annotations[a].annotations.length, restore.length);
1041           }
1042           else
1043           {
1044             temp = annotations[a].annotations;
1045           }
1046         }
1047       }
1048       else
1049       {
1050         if (tSize != aSize || command.position < 2)
1051         {
1052           int copylen = Math.min(command.position,
1053                   annotations[a].annotations.length);
1054           if (copylen > 0)
1055           {
1056             System.arraycopy(annotations[a].annotations, 0, temp, 0,
1057                     copylen); // command.position);
1058           }
1059
1060           Annotation[] deleted = new Annotation[command.number];
1061           if (copylen >= command.position)
1062           {
1063             copylen = Math.min(command.number,
1064                     annotations[a].annotations.length - command.position);
1065             if (copylen > 0)
1066             {
1067               System.arraycopy(annotations[a].annotations, command.position,
1068                       deleted, 0, copylen); // command.number);
1069             }
1070           }
1071
1072           command.deletedAnnotations.put(annotations[a].annotationId,
1073                   deleted);
1074           if (annotations[a].annotations.length > command.position
1075                   + command.number)
1076           {
1077             System.arraycopy(annotations[a].annotations,
1078                     command.position + command.number, temp,
1079                     command.position, annotations[a].annotations.length
1080                             - command.position - command.number); // aSize
1081           }
1082         }
1083         else
1084         {
1085           int dSize = aSize - command.position;
1086
1087           if (dSize > 0)
1088           {
1089             Annotation[] deleted = new Annotation[command.number];
1090             System.arraycopy(annotations[a].annotations, command.position,
1091                     deleted, 0, dSize);
1092
1093             command.deletedAnnotations.put(annotations[a].annotationId,
1094                     deleted);
1095
1096             tSize = Math.min(annotations[a].annotations.length,
1097                     command.position);
1098             temp = new Annotation[tSize];
1099             System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize);
1100           }
1101           else
1102           {
1103             temp = annotations[a].annotations;
1104           }
1105         }
1106       }
1107
1108       annotations[a].annotations = temp;
1109     }
1110   }
1111
1112   final static void adjustFeatures(Edit command, int index, final int i,
1113           final int j, boolean insert)
1114   {
1115     SequenceI seq = command.seqs[index];
1116     SequenceI sequence = seq.getDatasetSequence();
1117     if (sequence == null)
1118     {
1119       sequence = seq;
1120     }
1121
1122     if (insert)
1123     {
1124       if (command.editedFeatures != null
1125               && command.editedFeatures.containsKey(seq))
1126       {
1127         sequence.setSequenceFeatures(command.editedFeatures.get(seq));
1128       }
1129
1130       return;
1131     }
1132
1133     List<SequenceFeature> sf = sequence.getFeatures()
1134             .getPositionalFeatures();
1135
1136     if (sf.isEmpty())
1137     {
1138       return;
1139     }
1140
1141     List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
1142
1143     int cSize = j - i;
1144
1145     for (SequenceFeature feature : sf)
1146     {
1147       SequenceFeature copy = new SequenceFeature(feature);
1148
1149       oldsf.add(copy);
1150
1151       if (feature.getEnd() < i)
1152       {
1153         continue;
1154       }
1155
1156       if (feature.getBegin() > j)
1157       {
1158         int newBegin = copy.getBegin() - cSize;
1159         int newEnd = copy.getEnd() - cSize;
1160         SequenceFeature newSf = new SequenceFeature(feature, newBegin,
1161                 newEnd, feature.getFeatureGroup(), feature.getScore());
1162         sequence.deleteFeature(feature);
1163         sequence.addSequenceFeature(newSf);
1164         // feature.setBegin(newBegin);
1165         // feature.setEnd(newEnd);
1166         continue;
1167       }
1168
1169       int newBegin = feature.getBegin();
1170       int newEnd = feature.getEnd();
1171       if (newBegin >= i)
1172       {
1173         newBegin = i;
1174         // feature.setBegin(i);
1175       }
1176
1177       if (newEnd < j)
1178       {
1179         newEnd = j - 1;
1180         // feature.setEnd(j - 1);
1181       }
1182       newEnd = newEnd - cSize;
1183       // feature.setEnd(feature.getEnd() - (cSize));
1184
1185       sequence.deleteFeature(feature);
1186       if (newEnd >= newBegin)
1187       {
1188         sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
1189                 newEnd, feature.getFeatureGroup(), feature.getScore()));
1190       }
1191       // if (feature.getBegin() > feature.getEnd())
1192       // {
1193       // sequence.deleteFeature(feature);
1194       // }
1195     }
1196
1197     if (command.editedFeatures == null)
1198     {
1199       command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
1200     }
1201
1202     command.editedFeatures.put(seq, oldsf);
1203
1204   }
1205
1206   /**
1207    * Returns the list of edit commands wrapped by this object.
1208    * 
1209    * @return
1210    */
1211   public List<Edit> getEdits()
1212   {
1213     return this.edits;
1214   }
1215
1216   /**
1217    * Returns a map whose keys are the dataset sequences, and values their
1218    * aligned sequences before the command edit list was applied. The aligned
1219    * sequences are copies, which may be updated without affecting the originals.
1220    * 
1221    * The command holds references to the aligned sequences (after editing). If
1222    * the command is an 'undo',then the prior state is simply the aligned state.
1223    * Otherwise, we have to derive the prior state by working backwards through
1224    * the edit list to infer the aligned sequences before editing.
1225    * 
1226    * Note: an alternative solution would be to cache the 'before' state of each
1227    * edit, but this would be expensive in space in the common case that the
1228    * original is never needed (edits are not mirrored).
1229    * 
1230    * @return
1231    * @throws IllegalStateException
1232    *           on detecting an edit command of a type that can't be unwound
1233    */
1234   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
1235   {
1236     Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
1237     if (getEdits() == null)
1238     {
1239       return result;
1240     }
1241     if (forUndo)
1242     {
1243       for (Edit e : getEdits())
1244       {
1245         for (SequenceI seq : e.getSequences())
1246         {
1247           SequenceI ds = seq.getDatasetSequence();
1248           // SequenceI preEdit = result.get(ds);
1249           if (!result.containsKey(ds))
1250           {
1251             /*
1252              * copy sequence including start/end (but don't use copy constructor
1253              * as we don't need annotations)
1254              */
1255             SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
1256                     seq.getStart(), seq.getEnd());
1257             preEdit.setDatasetSequence(ds);
1258             result.put(ds, preEdit);
1259           }
1260         }
1261       }
1262       return result;
1263     }
1264
1265     /*
1266      * Work backwards through the edit list, deriving the sequences before each
1267      * was applied. The final result is the sequence set before any edits.
1268      */
1269     Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
1270     while (editList.hasNext())
1271     {
1272       Edit oldEdit = editList.next();
1273       Action action = oldEdit.getAction();
1274       int position = oldEdit.getPosition();
1275       int number = oldEdit.getNumber();
1276       final char gap = oldEdit.getGapCharacter();
1277       for (SequenceI seq : oldEdit.getSequences())
1278       {
1279         SequenceI ds = seq.getDatasetSequence();
1280         SequenceI preEdit = result.get(ds);
1281         if (preEdit == null)
1282         {
1283           preEdit = new Sequence("", seq.getSequenceAsString(),
1284                   seq.getStart(), seq.getEnd());
1285           preEdit.setDatasetSequence(ds);
1286           result.put(ds, preEdit);
1287         }
1288         /*
1289          * 'Undo' this edit action on the sequence (updating the value in the
1290          * map).
1291          */
1292         if (ds != null)
1293         {
1294           if (action == Action.DELETE_GAP)
1295           {
1296             preEdit.setSequence(new String(StringUtils.insertCharAt(
1297                     preEdit.getSequence(), position, number, gap)));
1298           }
1299           else if (action == Action.INSERT_GAP)
1300           {
1301             preEdit.setSequence(new String(StringUtils.deleteChars(
1302                     preEdit.getSequence(), position, position + number)));
1303           }
1304           else
1305           {
1306             System.err.println("Can't undo edit action " + action);
1307             // throw new IllegalStateException("Can't undo edit action " +
1308             // action);
1309           }
1310         }
1311       }
1312     }
1313     return result;
1314   }
1315
1316   public class Edit
1317   {
1318     public SequenceI[] oldds;
1319
1320     boolean fullAlignmentHeight = false;
1321
1322     Hashtable<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
1323
1324     Hashtable<String, Annotation[]> deletedAnnotations;
1325
1326     Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
1327
1328     AlignmentI al;
1329
1330     Action command;
1331
1332     char[][] string;
1333
1334     SequenceI[] seqs;
1335
1336     int[] alIndex;
1337
1338     int position, number;
1339
1340     char gapChar;
1341
1342     public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1343             char gap)
1344     {
1345       this.command = cmd;
1346       this.seqs = sqs;
1347       this.position = pos;
1348       this.number = count;
1349       this.gapChar = gap;
1350     }
1351
1352     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1353             AlignmentI align)
1354     {
1355       this.gapChar = align.getGapCharacter();
1356       this.command = cmd;
1357       this.seqs = sqs;
1358       this.position = pos;
1359       this.number = count;
1360       this.al = align;
1361
1362       alIndex = new int[sqs.length];
1363       for (int i = 0; i < sqs.length; i++)
1364       {
1365         alIndex[i] = align.findIndex(sqs[i]);
1366       }
1367
1368       fullAlignmentHeight = (align.getHeight() == sqs.length);
1369     }
1370
1371     Edit(Action cmd, SequenceI[] sqs, int pos, int count,
1372             AlignmentI align, String replace)
1373     {
1374       this.command = cmd;
1375       this.seqs = sqs;
1376       this.position = pos;
1377       this.number = count;
1378       this.al = align;
1379       this.gapChar = align.getGapCharacter();
1380       string = new char[sqs.length][];
1381       for (int i = 0; i < sqs.length; i++)
1382       {
1383         string[i] = replace.toCharArray();
1384       }
1385
1386       fullAlignmentHeight = (align.getHeight() == sqs.length);
1387     }
1388
1389     public SequenceI[] getSequences()
1390     {
1391       return seqs;
1392     }
1393
1394     public int getPosition()
1395     {
1396       return position;
1397     }
1398
1399     public Action getAction()
1400     {
1401       return command;
1402     }
1403
1404     public int getNumber()
1405     {
1406       return number;
1407     }
1408
1409     public char getGapCharacter()
1410     {
1411       return gapChar;
1412     }
1413   }
1414
1415   /**
1416    * Returns an iterator over the list of edit commands which traverses the list
1417    * either forwards or backwards.
1418    * 
1419    * @param forwards
1420    * @return
1421    */
1422   public Iterator<Edit> getEditIterator(boolean forwards)
1423   {
1424     if (forwards)
1425     {
1426       return getEdits().iterator();
1427     }
1428     else
1429     {
1430       return new ReverseListIterator<Edit>(getEdits());
1431     }
1432   }
1433 }