Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
EditCommand | 69 | 477 | 205 | ||
EditCommand.Action | 71 | 6 | 6 | ||
EditCommand.Edit | 1448 | 22 | 12 |
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 java.util.Locale; | |
24 | ||
25 | import jalview.analysis.AlignSeq; | |
26 | import jalview.datamodel.AlignmentAnnotation; | |
27 | import jalview.datamodel.AlignmentI; | |
28 | import jalview.datamodel.Annotation; | |
29 | import jalview.datamodel.ContiguousI; | |
30 | import jalview.datamodel.Range; | |
31 | import jalview.datamodel.Sequence; | |
32 | import jalview.datamodel.SequenceFeature; | |
33 | import jalview.datamodel.SequenceI; | |
34 | import jalview.datamodel.features.SequenceFeaturesI; | |
35 | import jalview.util.Comparison; | |
36 | import jalview.util.ReverseListIterator; | |
37 | import jalview.util.StringUtils; | |
38 | ||
39 | import java.util.ArrayList; | |
40 | import java.util.HashMap; | |
41 | import java.util.Hashtable; | |
42 | import java.util.Iterator; | |
43 | import java.util.List; | |
44 | import java.util.ListIterator; | |
45 | import java.util.Map; | |
46 | ||
47 | /** | |
48 | * | |
49 | * <p> | |
50 | * Title: EditCommmand | |
51 | * </p> | |
52 | * | |
53 | * <p> | |
54 | * Description: Essential information for performing undo and redo for cut/paste | |
55 | * insert/delete gap which can be stored in the HistoryList | |
56 | * </p> | |
57 | * | |
58 | * <p> | |
59 | * Copyright: Copyright (c) 2006 | |
60 | * </p> | |
61 | * | |
62 | * <p> | |
63 | * Company: Dundee University | |
64 | * </p> | |
65 | * | |
66 | * @author not attributable | |
67 | * @version 1.0 | |
68 | */ | |
69 | public class EditCommand implements CommandI | |
70 | { | |
71 | public enum Action | |
72 | { | |
73 | INSERT_GAP | |
74 | { | |
75 | 0 | @Override |
76 | public Action getUndoAction() | |
77 | { | |
78 | 0 | return DELETE_GAP; |
79 | } | |
80 | }, | |
81 | DELETE_GAP | |
82 | { | |
83 | 0 | @Override |
84 | public Action getUndoAction() | |
85 | { | |
86 | 0 | return INSERT_GAP; |
87 | } | |
88 | }, | |
89 | CUT | |
90 | { | |
91 | 0 | @Override |
92 | public Action getUndoAction() | |
93 | { | |
94 | 0 | return PASTE; |
95 | } | |
96 | }, | |
97 | PASTE | |
98 | { | |
99 | 0 | @Override |
100 | public Action getUndoAction() | |
101 | { | |
102 | 0 | return CUT; |
103 | } | |
104 | }, | |
105 | REPLACE | |
106 | { | |
107 | 0 | @Override |
108 | public Action getUndoAction() | |
109 | { | |
110 | 0 | return REPLACE; |
111 | } | |
112 | }, | |
113 | INSERT_NUC | |
114 | { | |
115 | 0 | @Override |
116 | public Action getUndoAction() | |
117 | { | |
118 | 0 | return null; |
119 | } | |
120 | }; | |
121 | ||
122 | public abstract Action getUndoAction(); | |
123 | }; | |
124 | ||
125 | private List<Edit> edits = new ArrayList<>(); | |
126 | ||
127 | String description; | |
128 | ||
129 | 64 | public EditCommand() |
130 | { | |
131 | } | |
132 | ||
133 | 0 | public EditCommand(String desc) |
134 | { | |
135 | 0 | this.description = desc; |
136 | } | |
137 | ||
138 | 15 | public EditCommand(String desc, Action command, SequenceI[] seqs, |
139 | int position, int number, AlignmentI al) | |
140 | { | |
141 | 15 | this.description = desc; |
142 | 15 | if (command == Action.CUT || command == Action.PASTE) |
143 | { | |
144 | 15 | setEdit(new Edit(command, seqs, position, number, al)); |
145 | } | |
146 | ||
147 | 15 | performEdit(0, null); |
148 | } | |
149 | ||
150 | 3 | public EditCommand(String desc, Action command, String replace, |
151 | SequenceI[] seqs, int position, int number, AlignmentI al) | |
152 | { | |
153 | 3 | this.description = desc; |
154 | 3 | if (command == Action.REPLACE) |
155 | { | |
156 | 3 | setEdit(new Edit(command, seqs, position, number, al, replace)); |
157 | } | |
158 | ||
159 | 3 | performEdit(0, null); |
160 | } | |
161 | ||
162 | /** | |
163 | * Set the list of edits to the specified item (only). | |
164 | * | |
165 | * @param e | |
166 | */ | |
167 | 22 | protected void setEdit(Edit e) |
168 | { | |
169 | 22 | edits.clear(); |
170 | 22 | edits.add(e); |
171 | } | |
172 | ||
173 | /** | |
174 | * Add the given edit command to the stored list of commands. If simply | |
175 | * expanding the range of the last command added, then modify it instead of | |
176 | * adding a new command. | |
177 | * | |
178 | * @param e | |
179 | */ | |
180 | 74 | public void addEdit(Edit e) |
181 | { | |
182 | 74 | if (!expandEdit(edits, e)) |
183 | { | |
184 | 67 | edits.add(e); |
185 | } | |
186 | } | |
187 | ||
188 | /** | |
189 | * Returns true if the new edit is incorporated by updating (expanding the | |
190 | * range of) the last edit on the list, else false. We can 'expand' the last | |
191 | * edit if the new one is the same action, on the same sequences, and acts on | |
192 | * a contiguous range. This is the case where a mouse drag generates a series | |
193 | * of contiguous gap insertions or deletions. | |
194 | * | |
195 | * @param edits | |
196 | * @param e | |
197 | * @return | |
198 | */ | |
199 | 74 | protected static boolean expandEdit(List<Edit> edits, Edit e) |
200 | { | |
201 | 74 | if (edits == null || edits.isEmpty()) |
202 | { | |
203 | 28 | return false; |
204 | } | |
205 | 46 | Edit lastEdit = edits.get(edits.size() - 1); |
206 | 46 | Action action = e.command; |
207 | 46 | if (lastEdit.command != action) |
208 | { | |
209 | 15 | return false; |
210 | } | |
211 | ||
212 | /* | |
213 | * Both commands must act on the same sequences - compare the underlying | |
214 | * dataset sequences, rather than the aligned sequences, which change as | |
215 | * they are edited. | |
216 | */ | |
217 | 31 | if (lastEdit.seqs.length != e.seqs.length) |
218 | { | |
219 | 0 | return false; |
220 | } | |
221 | 68 | for (int i = 0; i < e.seqs.length; i++) |
222 | { | |
223 | 44 | if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i] |
224 | .getDatasetSequence()) | |
225 | { | |
226 | 7 | return false; |
227 | } | |
228 | } | |
229 | ||
230 | /** | |
231 | * Check a contiguous edit; either | |
232 | * <ul> | |
233 | * <li>a new Insert <n> positions to the right of the last <insert n>, | |
234 | * or</li> | |
235 | * <li>a new Delete <n> gaps which is <n> positions to the left of the last | |
236 | * delete.</li> | |
237 | * </ul> | |
238 | */ | |
239 | 24 | boolean contiguous = (action == Action.INSERT_GAP |
240 | && e.position == lastEdit.position + lastEdit.number) | |
241 | || (action == Action.DELETE_GAP | |
242 | && e.position + e.number == lastEdit.position); | |
243 | 24 | if (contiguous) |
244 | { | |
245 | /* | |
246 | * We are just expanding the range of the last edit. For delete gap, also | |
247 | * moving the start position left. | |
248 | */ | |
249 | 7 | lastEdit.number += e.number; |
250 | 7 | lastEdit.seqs = e.seqs; |
251 | 7 | if (action == Action.DELETE_GAP) |
252 | { | |
253 | 2 | lastEdit.position--; |
254 | } | |
255 | 7 | return true; |
256 | } | |
257 | 17 | return false; |
258 | } | |
259 | ||
260 | /** | |
261 | * Clear the list of stored edit commands. | |
262 | * | |
263 | */ | |
264 | 3 | protected void clearEdits() |
265 | { | |
266 | 3 | edits.clear(); |
267 | } | |
268 | ||
269 | /** | |
270 | * Returns the i'th stored Edit command. | |
271 | * | |
272 | * @param i | |
273 | * @return | |
274 | */ | |
275 | 24 | protected Edit getEdit(int i) |
276 | { | |
277 | 24 | if (i >= 0 && i < edits.size()) |
278 | { | |
279 | 24 | return edits.get(i); |
280 | } | |
281 | 0 | return null; |
282 | } | |
283 | ||
284 | 0 | @Override |
285 | final public String getDescription() | |
286 | { | |
287 | 0 | return description; |
288 | } | |
289 | ||
290 | 11 | @Override |
291 | public int getSize() | |
292 | { | |
293 | 11 | return edits.size(); |
294 | } | |
295 | ||
296 | /** | |
297 | * Return the alignment for the first edit (or null if no edit). | |
298 | * | |
299 | * @return | |
300 | */ | |
301 | 0 | final public AlignmentI getAlignment() |
302 | { | |
303 | 0 | return (edits.isEmpty() ? null : edits.get(0).al); |
304 | } | |
305 | ||
306 | /** | |
307 | * append a new editCommand Note. this shouldn't be called if the edit is an | |
308 | * operation affects more alignment objects than the one referenced in al (for | |
309 | * example, cut or pasting whole sequences). Use the form with an additional | |
310 | * AlignmentI[] views parameter. | |
311 | * | |
312 | * @param command | |
313 | * @param seqs | |
314 | * @param position | |
315 | * @param number | |
316 | * @param al | |
317 | * @param performEdit | |
318 | */ | |
319 | 12 | final public void appendEdit(Action command, SequenceI[] seqs, |
320 | int position, int number, AlignmentI al, boolean performEdit) | |
321 | { | |
322 | 12 | appendEdit(command, seqs, position, number, al, performEdit, null); |
323 | } | |
324 | ||
325 | /** | |
326 | * append a new edit command with a set of alignment views that may be | |
327 | * operated on | |
328 | * | |
329 | * @param command | |
330 | * @param seqs | |
331 | * @param position | |
332 | * @param number | |
333 | * @param al | |
334 | * @param performEdit | |
335 | * @param views | |
336 | */ | |
337 | 19 | final public void appendEdit(Action command, SequenceI[] seqs, |
338 | int position, int number, AlignmentI al, boolean performEdit, | |
339 | AlignmentI[] views) | |
340 | { | |
341 | 19 | Edit edit = new Edit(command, seqs, position, number, al); |
342 | 19 | appendEdit(edit, al, performEdit, views); |
343 | } | |
344 | ||
345 | /** | |
346 | * Overloaded method that accepts an Edit object with additional parameters. | |
347 | * | |
348 | * @param edit | |
349 | * @param al | |
350 | * @param performEdit | |
351 | * @param views | |
352 | */ | |
353 | 20 | final public void appendEdit(Edit edit, AlignmentI al, |
354 | boolean performEdit, AlignmentI[] views) | |
355 | { | |
356 | 20 | if (al.getHeight() == edit.seqs.length) |
357 | { | |
358 | 20 | edit.al = al; |
359 | 20 | edit.fullAlignmentHeight = true; |
360 | } | |
361 | ||
362 | 20 | addEdit(edit); |
363 | ||
364 | 20 | if (performEdit) |
365 | { | |
366 | 13 | performEdit(edit, views); |
367 | } | |
368 | } | |
369 | ||
370 | /** | |
371 | * Execute all the edit commands, starting at the given commandIndex | |
372 | * | |
373 | * @param commandIndex | |
374 | * @param views | |
375 | */ | |
376 | 45 | public final void performEdit(int commandIndex, AlignmentI[] views) |
377 | { | |
378 | 45 | ListIterator<Edit> iterator = edits.listIterator(commandIndex); |
379 | 98 | while (iterator.hasNext()) |
380 | { | |
381 | 53 | Edit edit = iterator.next(); |
382 | 53 | performEdit(edit, views); |
383 | } | |
384 | } | |
385 | ||
386 | /** | |
387 | * Execute one edit command in all the specified alignment views | |
388 | * | |
389 | * @param edit | |
390 | * @param views | |
391 | */ | |
392 | 66 | protected static void performEdit(Edit edit, AlignmentI[] views) |
393 | { | |
394 | 66 | switch (edit.command) |
395 | { | |
396 | 8 | case INSERT_GAP: |
397 | 8 | insertGap(edit); |
398 | 8 | break; |
399 | 11 | case DELETE_GAP: |
400 | 11 | deleteGap(edit); |
401 | 11 | break; |
402 | 37 | case CUT: |
403 | 37 | cut(edit, views); |
404 | 37 | break; |
405 | 0 | case PASTE: |
406 | 0 | paste(edit, views); |
407 | 0 | break; |
408 | 10 | case REPLACE: |
409 | 10 | replace(edit); |
410 | 10 | break; |
411 | 0 | case INSERT_NUC: |
412 | // TODO:add deleteNuc for UNDO | |
413 | // case INSERT_NUC: | |
414 | // insertNuc(edits[e]); | |
415 | 0 | break; |
416 | 0 | default: |
417 | 0 | break; |
418 | } | |
419 | } | |
420 | ||
421 | 18 | @Override |
422 | final public void doCommand(AlignmentI[] views) | |
423 | { | |
424 | 18 | performEdit(0, views); |
425 | } | |
426 | ||
427 | /** | |
428 | * Undo the stored list of commands, in reverse order. | |
429 | */ | |
430 | 42 | @Override |
431 | final public void undoCommand(AlignmentI[] views) | |
432 | { | |
433 | 42 | ListIterator<Edit> iterator = edits.listIterator(edits.size()); |
434 | 90 | while (iterator.hasPrevious()) |
435 | { | |
436 | 48 | Edit e = iterator.previous(); |
437 | 48 | switch (e.command) |
438 | { | |
439 | 3 | case INSERT_GAP: |
440 | 3 | deleteGap(e); |
441 | 3 | break; |
442 | 3 | case DELETE_GAP: |
443 | 3 | insertGap(e); |
444 | 3 | break; |
445 | 33 | case CUT: |
446 | 33 | paste(e, views); |
447 | 33 | break; |
448 | 0 | case PASTE: |
449 | 0 | cut(e, views); |
450 | 0 | break; |
451 | 9 | case REPLACE: |
452 | 9 | replace(e); |
453 | 9 | break; |
454 | 0 | case INSERT_NUC: |
455 | // not implemented | |
456 | 0 | break; |
457 | 0 | default: |
458 | 0 | break; |
459 | } | |
460 | } | |
461 | } | |
462 | ||
463 | /** | |
464 | * Insert gap(s) in sequences as specified by the command, and adjust | |
465 | * annotations. | |
466 | * | |
467 | * @param command | |
468 | */ | |
469 | 11 | final private static void insertGap(Edit command) |
470 | { | |
471 | ||
472 | 49 | for (int s = 0; s < command.seqs.length; s++) |
473 | { | |
474 | 38 | command.seqs[s].insertCharAt(command.position, command.number, |
475 | command.gapChar); | |
476 | // jalview.bin.Console.outPrintln("pos: "+command.position+" number: | |
477 | // "+command.number); | |
478 | } | |
479 | ||
480 | 11 | adjustAnnotations(command, true, false, null); |
481 | } | |
482 | ||
483 | // | |
484 | // final void insertNuc(Edit command) | |
485 | // { | |
486 | // | |
487 | // for (int s = 0; s < command.seqs.length; s++) | |
488 | // { | |
489 | // jalview.bin.Console.outPrintln("pos: "+command.position+" number: | |
490 | // "+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 | 14 | final static private void deleteGap(Edit command) |
504 | { | |
505 | 57 | for (int s = 0; s < command.seqs.length; s++) |
506 | { | |
507 | 43 | command.seqs[s].deleteChars(command.position, |
508 | command.position + command.number); | |
509 | } | |
510 | ||
511 | 14 | 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 | 40 | static void cut(Edit command, AlignmentI[] views) |
522 | { | |
523 | 40 | boolean seqDeleted = false; |
524 | 40 | command.string = new char[command.seqs.length][]; |
525 | ||
526 | 95 | for (int i = 0; i < command.seqs.length; i++) |
527 | { | |
528 | 55 | final SequenceI sequence = command.seqs[i]; |
529 | 55 | if (sequence.getLength() > command.position) |
530 | { | |
531 | 55 | command.string[i] = sequence.getSequence(command.position, |
532 | command.position + command.number); | |
533 | 55 | SequenceI oldds = sequence.getDatasetSequence(); |
534 | 55 | ContiguousI cutPositions = sequence.findPositions( |
535 | command.position + 1, command.position + command.number); | |
536 | 55 | boolean cutIsInternal = cutPositions != null |
537 | && sequence.getStart() != cutPositions.getBegin() | |
538 | && sequence.getEnd() != cutPositions.getEnd(); | |
539 | ||
540 | /* | |
541 | * perform the cut; if this results in a new dataset sequence, add | |
542 | * that to the alignment dataset | |
543 | */ | |
544 | 55 | SequenceI ds = sequence.getDatasetSequence(); |
545 | 55 | sequence.deleteChars(command.position, |
546 | command.position + command.number); | |
547 | ||
548 | 55 | if (command.oldds != null && command.oldds[i] != null) |
549 | { | |
550 | /* | |
551 | * we are Redoing a Cut, or Undoing a Paste - so | |
552 | * oldds entry contains the cut dataset sequence, | |
553 | * with sequence features in expected place | |
554 | */ | |
555 | 19 | sequence.setDatasetSequence(command.oldds[i]); |
556 | 19 | command.oldds[i] = oldds; |
557 | } | |
558 | else | |
559 | { | |
560 | /* | |
561 | * new cut operation: save the dataset sequence | |
562 | * so it can be restored in an Undo | |
563 | */ | |
564 | 36 | if (command.oldds == null) |
565 | { | |
566 | 23 | command.oldds = new SequenceI[command.seqs.length]; |
567 | } | |
568 | 36 | command.oldds[i] = oldds;// todo not if !cutIsInternal? |
569 | ||
570 | // do we need to edit sequence features for new sequence ? | |
571 | 36 | if (oldds != sequence.getDatasetSequence() || (cutIsInternal |
572 | && sequence.getFeatures().hasFeatures())) | |
573 | // todo or just test cutIsInternal && cutPositions != null ? | |
574 | { | |
575 | 18 | if (cutPositions != null) |
576 | { | |
577 | 18 | cutFeatures(command, sequence, cutPositions.getBegin(), |
578 | cutPositions.getEnd(), cutIsInternal); | |
579 | } | |
580 | } | |
581 | } | |
582 | 55 | SequenceI newDs = sequence.getDatasetSequence(); |
583 | 55 | if (newDs != ds && command.al != null |
584 | && command.al.getDataset() != null | |
585 | && !command.al.getDataset().getSequences().contains(newDs)) | |
586 | { | |
587 | 12 | command.al.getDataset().addSequence(newDs); |
588 | } | |
589 | } | |
590 | ||
591 | 55 | if (sequence.getLength() < 1) |
592 | { | |
593 | 2 | command.al.deleteSequence(sequence); |
594 | 2 | seqDeleted = true; |
595 | } | |
596 | } | |
597 | ||
598 | 40 | adjustAnnotations(command, false, seqDeleted, views); |
599 | } | |
600 | ||
601 | /** | |
602 | * Perform the given Paste command. This may be to add cut or copied sequences | |
603 | * to an alignment, or to undo a 'Cut' action on a region of the alignment. | |
604 | * | |
605 | * @param command | |
606 | * @param views | |
607 | */ | |
608 | 33 | static void paste(Edit command, AlignmentI[] views) |
609 | { | |
610 | 33 | boolean seqWasDeleted = false; |
611 | ||
612 | 71 | for (int i = 0; i < command.seqs.length; i++) |
613 | { | |
614 | 38 | boolean newDSNeeded = false; |
615 | 38 | boolean newDSWasNeeded = command.oldds != null |
616 | && command.oldds[i] != null; | |
617 | 38 | SequenceI sequence = command.seqs[i]; |
618 | 38 | if (sequence.getLength() < 1) |
619 | { | |
620 | /* | |
621 | * sequence was deleted; re-add it to the alignment | |
622 | */ | |
623 | 2 | if (command.alIndex[i] < command.al.getHeight()) |
624 | { | |
625 | 0 | List<SequenceI> sequences = command.al.getSequences(); |
626 | 0 | synchronized (sequences) |
627 | { | |
628 | 0 | if (!(command.alIndex[i] < 0)) |
629 | { | |
630 | 0 | sequences.add(command.alIndex[i], sequence); |
631 | } | |
632 | } | |
633 | } | |
634 | else | |
635 | { | |
636 | 2 | command.al.addSequence(sequence); |
637 | } | |
638 | 2 | seqWasDeleted = true; |
639 | } | |
640 | 38 | int newStart = sequence.getStart(); |
641 | 38 | int newEnd = sequence.getEnd(); |
642 | ||
643 | 38 | StringBuilder tmp = new StringBuilder(); |
644 | 38 | tmp.append(sequence.getSequence()); |
645 | // Undo of a delete does not replace original dataset sequence on to | |
646 | // alignment sequence. | |
647 | ||
648 | 38 | int start = 0; |
649 | 38 | int length = 0; |
650 | ||
651 | 38 | if (command.string != null && command.string[i] != null) |
652 | { | |
653 | 38 | if (command.position >= tmp.length()) |
654 | { | |
655 | // This occurs if padding is on, and residues | |
656 | // are removed from end of alignment | |
657 | 12 | int len = command.position - tmp.length(); |
658 | 12 | while (len > 0) |
659 | { | |
660 | 0 | tmp.append(command.gapChar); |
661 | 0 | len--; |
662 | } | |
663 | } | |
664 | 38 | tmp.insert(command.position, command.string[i]); |
665 | 130 | for (int s = 0; s < command.string[i].length; s++) |
666 | { | |
667 | 92 | if (!Comparison.isGap(command.string[i][s])) |
668 | { | |
669 | 90 | length++; |
670 | 90 | if (!newDSNeeded) |
671 | { | |
672 | 38 | newDSNeeded = true; |
673 | 38 | start = sequence.findPosition(command.position); |
674 | // end = sequence | |
675 | // .findPosition(command.position + command.number); | |
676 | } | |
677 | 90 | if (sequence.getStart() == start) |
678 | { | |
679 | 33 | newStart--; |
680 | } | |
681 | else | |
682 | { | |
683 | 57 | newEnd++; |
684 | } | |
685 | } | |
686 | } | |
687 | 38 | command.string[i] = null; |
688 | } | |
689 | ||
690 | 38 | sequence.setSequence(tmp.toString()); |
691 | 38 | sequence.setStart(newStart); |
692 | 38 | sequence.setEnd(newEnd); |
693 | ||
694 | /* | |
695 | * command and Undo share the same dataset sequence if cut was | |
696 | * at start or end of sequence | |
697 | */ | |
698 | 38 | boolean sameDatasetSequence = false; |
699 | 38 | if (newDSNeeded) |
700 | { | |
701 | 38 | if (sequence.getDatasetSequence() != null) |
702 | { | |
703 | 38 | SequenceI ds; |
704 | 38 | if (newDSWasNeeded) |
705 | { | |
706 | 38 | ds = command.oldds[i]; |
707 | } | |
708 | else | |
709 | { | |
710 | // make a new DS sequence | |
711 | // use new ds mechanism here | |
712 | 0 | String ungapped = AlignSeq.extractGaps(Comparison.GapChars, |
713 | sequence.getSequenceAsString()); | |
714 | 0 | ds = new Sequence(sequence.getName(), ungapped, |
715 | sequence.getStart(), sequence.getEnd()); | |
716 | 0 | ds.setDescription(sequence.getDescription()); |
717 | } | |
718 | 38 | if (command.oldds == null) |
719 | { | |
720 | 0 | command.oldds = new SequenceI[command.seqs.length]; |
721 | } | |
722 | 38 | command.oldds[i] = sequence.getDatasetSequence(); |
723 | 38 | sameDatasetSequence = ds == sequence.getDatasetSequence(); |
724 | 38 | ds.setSequenceFeatures(sequence.getSequenceFeatures()); |
725 | 38 | if (!sameDatasetSequence && command.al.getDataset() != null) |
726 | { | |
727 | // delete 'undone' sequence from alignment dataset | |
728 | 12 | command.al.getDataset() |
729 | .deleteSequence(sequence.getDatasetSequence()); | |
730 | } | |
731 | 38 | sequence.setDatasetSequence(ds); |
732 | } | |
733 | 38 | undoCutFeatures(command, command.seqs[i], start, length, |
734 | sameDatasetSequence); | |
735 | } | |
736 | } | |
737 | 33 | adjustAnnotations(command, true, seqWasDeleted, views); |
738 | ||
739 | 33 | command.string = null; |
740 | } | |
741 | ||
742 | 19 | static void replace(Edit command) |
743 | { | |
744 | 19 | StringBuilder tmp; |
745 | 19 | String oldstring; |
746 | 19 | int start = command.position; |
747 | 19 | int end = command.number; |
748 | // TODO TUTORIAL - Fix for replacement with different length of sequence (or | |
749 | // whole sequence) | |
750 | // TODO Jalview 2.4 bugfix change to an aggregate command - original | |
751 | // sequence string is cut, new string is pasted in. | |
752 | 19 | command.number = start + command.string[0].length; |
753 | 38 | for (int i = 0; i < command.seqs.length; i++) |
754 | { | |
755 | 19 | boolean newDSWasNeeded = command.oldds != null |
756 | && command.oldds[i] != null; | |
757 | 19 | boolean newStartEndWasNeeded = command.oldStartEnd != null |
758 | && command.oldStartEnd[i] != null; | |
759 | ||
760 | /** | |
761 | * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT, | |
762 | * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1, | |
763 | * viewport.alignment)); | |
764 | * | |
765 | */ | |
766 | /** | |
767 | * then addHistoryItem(new EditCommand( "Add sequences", | |
768 | * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) ); | |
769 | * | |
770 | */ | |
771 | 19 | ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1, |
772 | start); | |
773 | 19 | ContiguousI afterEditedPositions = command.seqs[i] |
774 | .findPositions(end + 1, command.seqs[i].getLength()); | |
775 | ||
776 | 19 | oldstring = command.seqs[i].getSequenceAsString(); |
777 | 19 | tmp = new StringBuilder(oldstring.substring(0, start)); |
778 | 19 | tmp.append(command.string[i]); |
779 | 19 | String nogaprep = AlignSeq.extractGaps(Comparison.GapChars, |
780 | new String(command.string[i])); | |
781 | 19 | if (end < oldstring.length()) |
782 | { | |
783 | 15 | tmp.append(oldstring.substring(end)); |
784 | } | |
785 | // stash end prior to updating the sequence object so we can save it if | |
786 | // need be. | |
787 | 19 | Range oldstartend = new Range(command.seqs[i].getStart(), |
788 | command.seqs[i].getEnd()); | |
789 | 19 | command.seqs[i].setSequence(tmp.toString()); |
790 | 19 | command.string[i] = oldstring |
791 | .substring(start, Math.min(end, oldstring.length())) | |
792 | .toCharArray(); | |
793 | 19 | String nogapold = AlignSeq.extractGaps(Comparison.GapChars, |
794 | new String(command.string[i])); | |
795 | ||
796 | 19 | if (!nogaprep.toLowerCase(Locale.ROOT) |
797 | .equals(nogapold.toLowerCase(Locale.ROOT))) | |
798 | { | |
799 | // we may already have dataset and limits stashed... | |
800 | 7 | if (newDSWasNeeded || newStartEndWasNeeded) |
801 | { | |
802 | 4 | if (newDSWasNeeded) |
803 | { | |
804 | // then just switch the dataset sequence | |
805 | 2 | SequenceI oldds = command.seqs[i].getDatasetSequence(); |
806 | 2 | command.seqs[i].setDatasetSequence(command.oldds[i]); |
807 | 2 | command.oldds[i] = oldds; |
808 | } | |
809 | 4 | if (newStartEndWasNeeded) |
810 | { | |
811 | 4 | Range newStart = command.oldStartEnd[i]; |
812 | 4 | command.oldStartEnd[i] = oldstartend; |
813 | 4 | command.seqs[i].setStart(newStart.getBegin()); |
814 | 4 | command.seqs[i].setEnd(newStart.getEnd()); |
815 | } | |
816 | } | |
817 | else | |
818 | { | |
819 | // decide if we need a new dataset sequence or modify start/end | |
820 | // first edit the original dataset sequence string | |
821 | 3 | SequenceI oldds = command.seqs[i].getDatasetSequence(); |
822 | 3 | String osp = oldds.getSequenceAsString(); |
823 | 3 | int beforeStartOfEdit = -oldds.getStart() + 1 |
824 | 3 | + (beforeEditedPositions == null |
825 | 1 | ? ((afterEditedPositions != null) |
826 | ? afterEditedPositions.getBegin() - 1 | |
827 | : oldstartend.getBegin() | |
828 | + nogapold.length()) | |
829 | : beforeEditedPositions.getEnd()); | |
830 | 3 | int afterEndOfEdit = -oldds.getStart() + 1 |
831 | 3 | + ((afterEditedPositions == null) ? oldstartend.getEnd() |
832 | : afterEditedPositions.getBegin() - 1); | |
833 | 3 | String fullseq = osp.substring(0, beforeStartOfEdit) + nogaprep |
834 | + osp.substring(afterEndOfEdit); | |
835 | ||
836 | // and check if new sequence data is different.. | |
837 | 3 | if (!fullseq.equalsIgnoreCase(osp)) |
838 | { | |
839 | // old ds and edited ds are different, so | |
840 | // create the new dataset sequence | |
841 | 2 | SequenceI newds = new Sequence(oldds); |
842 | 2 | newds.setSequence(fullseq.toUpperCase(Locale.ROOT)); |
843 | ||
844 | 2 | if (command.oldds == null) |
845 | { | |
846 | 2 | command.oldds = new SequenceI[command.seqs.length]; |
847 | } | |
848 | 2 | command.oldds[i] = command.seqs[i].getDatasetSequence(); |
849 | ||
850 | // And preserve start/end for good-measure | |
851 | ||
852 | 2 | if (command.oldStartEnd == null) |
853 | { | |
854 | 2 | command.oldStartEnd = new Range[command.seqs.length]; |
855 | } | |
856 | 2 | command.oldStartEnd[i] = oldstartend; |
857 | // TODO: JAL-1131 ensure newly created dataset sequence is added to | |
858 | // the set of | |
859 | // dataset sequences associated with the alignment. | |
860 | // TODO: JAL-1131 fix up any annotation associated with new dataset | |
861 | // sequence to ensure that original sequence/annotation | |
862 | // relationships | |
863 | // are preserved. | |
864 | 2 | command.seqs[i].setDatasetSequence(newds); |
865 | } | |
866 | else | |
867 | { | |
868 | 1 | if (command.oldStartEnd == null) |
869 | { | |
870 | 1 | command.oldStartEnd = new Range[command.seqs.length]; |
871 | } | |
872 | 1 | command.oldStartEnd[i] = new Range(command.seqs[i].getStart(), |
873 | command.seqs[i].getEnd()); | |
874 | 1 | if (beforeEditedPositions != null |
875 | && afterEditedPositions == null) | |
876 | { | |
877 | // modification at end | |
878 | 0 | command.seqs[i].setEnd(beforeEditedPositions.getEnd() |
879 | + nogaprep.length() - nogapold.length()); | |
880 | } | |
881 | 1 | else if (afterEditedPositions != null |
882 | && beforeEditedPositions == null) | |
883 | { | |
884 | // modification at start | |
885 | 1 | command.seqs[i].setStart( |
886 | afterEditedPositions.getBegin() - nogaprep.length()); | |
887 | } | |
888 | else | |
889 | { | |
890 | // edit covered both start and end. Here we can only guess the | |
891 | // new | |
892 | // start/end | |
893 | 0 | String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars, |
894 | command.seqs[i].getSequenceAsString() | |
895 | .toUpperCase(Locale.ROOT)); | |
896 | 0 | int newStart = command.seqs[i].getDatasetSequence() |
897 | .getSequenceAsString().indexOf(nogapalseq); | |
898 | 0 | if (newStart == -1) |
899 | { | |
900 | 0 | throw new Error( |
901 | "Implementation Error: could not locate start/end " | |
902 | + "in dataset sequence after an edit of the sequence string"); | |
903 | } | |
904 | 0 | int newEnd = newStart + nogapalseq.length() - 1; |
905 | 0 | command.seqs[i].setStart(newStart); |
906 | 0 | command.seqs[i].setEnd(newEnd); |
907 | } | |
908 | } | |
909 | } | |
910 | } | |
911 | 19 | tmp = null; |
912 | 19 | oldstring = null; |
913 | } | |
914 | } | |
915 | ||
916 | 98 | final static void adjustAnnotations(Edit command, boolean insert, |
917 | boolean modifyVisibility, AlignmentI[] views) | |
918 | { | |
919 | 98 | AlignmentAnnotation[] annotations = null; |
920 | ||
921 | 98 | if (modifyVisibility && !insert) |
922 | { | |
923 | // only occurs if a sequence was added or deleted. | |
924 | 2 | command.deletedAnnotationRows = new Hashtable<>(); |
925 | } | |
926 | 98 | if (command.fullAlignmentHeight) |
927 | { | |
928 | 98 | annotations = command.al.getAlignmentAnnotation(); |
929 | } | |
930 | else | |
931 | { | |
932 | 0 | int aSize = 0; |
933 | 0 | AlignmentAnnotation[] tmp; |
934 | 0 | for (int s = 0; s < command.seqs.length; s++) |
935 | { | |
936 | 0 | command.seqs[s].sequenceChanged(); |
937 | ||
938 | 0 | if (modifyVisibility) |
939 | { | |
940 | // Rows are only removed or added to sequence object. | |
941 | 0 | if (!insert) |
942 | { | |
943 | // remove rows | |
944 | 0 | tmp = command.seqs[s].getAnnotation(); |
945 | 0 | if (tmp != null) |
946 | { | |
947 | 0 | int alen = tmp.length; |
948 | 0 | for (int aa = 0; aa < tmp.length; aa++) |
949 | { | |
950 | 0 | if (!command.al.deleteAnnotation(tmp[aa])) |
951 | { | |
952 | // strip out annotation not in the current al (will be put | |
953 | // back on insert in all views) | |
954 | 0 | tmp[aa] = null; |
955 | 0 | alen--; |
956 | } | |
957 | } | |
958 | 0 | command.seqs[s].setAlignmentAnnotation(null); |
959 | 0 | if (alen != tmp.length) |
960 | { | |
961 | // save the non-null annotation references only | |
962 | 0 | AlignmentAnnotation[] saved = new AlignmentAnnotation[alen]; |
963 | 0 | for (int aa = 0, aapos = 0; aa < tmp.length; aa++) |
964 | { | |
965 | 0 | if (tmp[aa] != null) |
966 | { | |
967 | 0 | saved[aapos++] = tmp[aa]; |
968 | 0 | tmp[aa] = null; |
969 | } | |
970 | } | |
971 | 0 | tmp = saved; |
972 | 0 | command.deletedAnnotationRows.put(command.seqs[s], saved); |
973 | // and then remove any annotation in the other views | |
974 | 0 | for (int alview = 0; views != null |
975 | && alview < views.length; alview++) | |
976 | { | |
977 | 0 | if (views[alview] != command.al) |
978 | { | |
979 | 0 | AlignmentAnnotation[] toremove = views[alview] |
980 | .getAlignmentAnnotation(); | |
981 | 0 | if (toremove == null || toremove.length == 0) |
982 | { | |
983 | 0 | continue; |
984 | } | |
985 | // remove any alignment annotation on this sequence that's | |
986 | // on that alignment view. | |
987 | 0 | for (int aa = 0; aa < toremove.length; aa++) |
988 | { | |
989 | 0 | if (toremove[aa].sequenceRef == command.seqs[s]) |
990 | { | |
991 | 0 | views[alview].deleteAnnotation(toremove[aa]); |
992 | } | |
993 | } | |
994 | } | |
995 | } | |
996 | } | |
997 | else | |
998 | { | |
999 | // save all the annotation | |
1000 | 0 | command.deletedAnnotationRows.put(command.seqs[s], tmp); |
1001 | } | |
1002 | } | |
1003 | } | |
1004 | else | |
1005 | { | |
1006 | // recover rows | |
1007 | 0 | if (command.deletedAnnotationRows != null |
1008 | && command.deletedAnnotationRows | |
1009 | .containsKey(command.seqs[s])) | |
1010 | { | |
1011 | 0 | AlignmentAnnotation[] revealed = command.deletedAnnotationRows |
1012 | .get(command.seqs[s]); | |
1013 | 0 | command.seqs[s].setAlignmentAnnotation(revealed); |
1014 | 0 | if (revealed != null) |
1015 | { | |
1016 | 0 | for (int aa = 0; aa < revealed.length; aa++) |
1017 | { | |
1018 | // iterate through al adding original annotation | |
1019 | 0 | command.al.addAnnotation(revealed[aa]); |
1020 | } | |
1021 | 0 | for (int aa = 0; aa < revealed.length; aa++) |
1022 | { | |
1023 | 0 | command.al.setAnnotationIndex(revealed[aa], aa); |
1024 | } | |
1025 | // and then duplicate added annotation on every other alignment | |
1026 | // view | |
1027 | 0 | for (int vnum = 0; views != null |
1028 | && vnum < views.length; vnum++) | |
1029 | { | |
1030 | 0 | if (views[vnum] != command.al) |
1031 | { | |
1032 | 0 | int avwidth = views[vnum].getWidth() + 1; |
1033 | // duplicate in this view | |
1034 | 0 | for (int a = 0; a < revealed.length; a++) |
1035 | { | |
1036 | 0 | AlignmentAnnotation newann = new AlignmentAnnotation( |
1037 | revealed[a]); | |
1038 | 0 | command.seqs[s].addAlignmentAnnotation(newann); |
1039 | 0 | newann.padAnnotation(avwidth); |
1040 | 0 | views[vnum].addAnnotation(newann); |
1041 | 0 | views[vnum].setAnnotationIndex(newann, a); |
1042 | } | |
1043 | } | |
1044 | } | |
1045 | } | |
1046 | } | |
1047 | } | |
1048 | 0 | continue; |
1049 | } | |
1050 | ||
1051 | 0 | if (command.seqs[s].getAnnotation() == null) |
1052 | { | |
1053 | 0 | continue; |
1054 | } | |
1055 | ||
1056 | 0 | if (aSize == 0) |
1057 | { | |
1058 | 0 | annotations = command.seqs[s].getAnnotation(); |
1059 | } | |
1060 | else | |
1061 | { | |
1062 | 0 | tmp = new AlignmentAnnotation[aSize |
1063 | + command.seqs[s].getAnnotation().length]; | |
1064 | ||
1065 | 0 | System.arraycopy(annotations, 0, tmp, 0, aSize); |
1066 | ||
1067 | 0 | System.arraycopy(command.seqs[s].getAnnotation(), 0, tmp, aSize, |
1068 | command.seqs[s].getAnnotation().length); | |
1069 | ||
1070 | 0 | annotations = tmp; |
1071 | } | |
1072 | 0 | aSize = annotations.length; |
1073 | } | |
1074 | } | |
1075 | ||
1076 | 98 | if (annotations == null) |
1077 | { | |
1078 | 98 | return; |
1079 | } | |
1080 | ||
1081 | 0 | if (!insert) |
1082 | { | |
1083 | 0 | command.deletedAnnotations = new Hashtable<>(); |
1084 | } | |
1085 | ||
1086 | 0 | int aSize; |
1087 | 0 | Annotation[] temp; |
1088 | 0 | for (int a = 0; a < annotations.length; a++) |
1089 | { | |
1090 | 0 | if (annotations[a].autoCalculated |
1091 | || annotations[a].annotations == null) | |
1092 | { | |
1093 | 0 | continue; |
1094 | } | |
1095 | ||
1096 | 0 | int tSize = 0; |
1097 | ||
1098 | 0 | aSize = annotations[a].annotations.length; |
1099 | 0 | if (insert) |
1100 | { | |
1101 | 0 | temp = new Annotation[aSize + command.number]; |
1102 | 0 | if (annotations[a].padGaps) |
1103 | { | |
1104 | 0 | for (int aa = 0; aa < temp.length; aa++) |
1105 | { | |
1106 | 0 | temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0); |
1107 | } | |
1108 | } | |
1109 | } | |
1110 | else | |
1111 | { | |
1112 | 0 | if (command.position < aSize) |
1113 | { | |
1114 | 0 | if (command.position + command.number >= aSize) |
1115 | { | |
1116 | 0 | tSize = aSize; |
1117 | } | |
1118 | else | |
1119 | { | |
1120 | 0 | tSize = aSize - command.number; |
1121 | } | |
1122 | } | |
1123 | else | |
1124 | { | |
1125 | 0 | tSize = aSize; |
1126 | } | |
1127 | ||
1128 | 0 | if (tSize < 0) |
1129 | { | |
1130 | 0 | tSize = aSize; |
1131 | } | |
1132 | 0 | temp = new Annotation[tSize]; |
1133 | } | |
1134 | ||
1135 | 0 | if (insert) |
1136 | { | |
1137 | 0 | if (command.position < annotations[a].annotations.length) |
1138 | { | |
1139 | 0 | System.arraycopy(annotations[a].annotations, 0, temp, 0, |
1140 | command.position); | |
1141 | ||
1142 | 0 | if (command.deletedAnnotations != null |
1143 | && command.deletedAnnotations | |
1144 | .containsKey(annotations[a].annotationId)) | |
1145 | { | |
1146 | 0 | Annotation[] restore = command.deletedAnnotations |
1147 | .get(annotations[a].annotationId); | |
1148 | ||
1149 | 0 | System.arraycopy(restore, 0, temp, command.position, |
1150 | command.number); | |
1151 | ||
1152 | } | |
1153 | ||
1154 | 0 | System.arraycopy(annotations[a].annotations, command.position, |
1155 | temp, command.position + command.number, | |
1156 | aSize - command.position); | |
1157 | } | |
1158 | else | |
1159 | { | |
1160 | 0 | if (command.deletedAnnotations != null |
1161 | && command.deletedAnnotations | |
1162 | .containsKey(annotations[a].annotationId)) | |
1163 | { | |
1164 | 0 | Annotation[] restore = command.deletedAnnotations |
1165 | .get(annotations[a].annotationId); | |
1166 | ||
1167 | 0 | temp = new Annotation[annotations[a].annotations.length |
1168 | + restore.length]; | |
1169 | 0 | System.arraycopy(annotations[a].annotations, 0, temp, 0, |
1170 | annotations[a].annotations.length); | |
1171 | 0 | System.arraycopy(restore, 0, temp, |
1172 | annotations[a].annotations.length, restore.length); | |
1173 | } | |
1174 | else | |
1175 | { | |
1176 | 0 | temp = annotations[a].annotations; |
1177 | } | |
1178 | } | |
1179 | } | |
1180 | else | |
1181 | { | |
1182 | 0 | if (tSize != aSize || command.position < 2) |
1183 | { | |
1184 | 0 | int copylen = Math.min(command.position, |
1185 | annotations[a].annotations.length); | |
1186 | 0 | if (copylen > 0) |
1187 | { | |
1188 | 0 | System.arraycopy(annotations[a].annotations, 0, temp, 0, |
1189 | copylen); // command.position); | |
1190 | } | |
1191 | ||
1192 | 0 | Annotation[] deleted = new Annotation[command.number]; |
1193 | 0 | if (copylen >= command.position) |
1194 | { | |
1195 | 0 | copylen = Math.min(command.number, |
1196 | annotations[a].annotations.length - command.position); | |
1197 | 0 | if (copylen > 0) |
1198 | { | |
1199 | 0 | System.arraycopy(annotations[a].annotations, command.position, |
1200 | deleted, 0, copylen); // command.number); | |
1201 | } | |
1202 | } | |
1203 | ||
1204 | 0 | command.deletedAnnotations.put(annotations[a].annotationId, |
1205 | deleted); | |
1206 | 0 | if (annotations[a].annotations.length > command.position |
1207 | + command.number) | |
1208 | { | |
1209 | 0 | System.arraycopy(annotations[a].annotations, |
1210 | command.position + command.number, temp, | |
1211 | command.position, annotations[a].annotations.length | |
1212 | - command.position - command.number); // aSize | |
1213 | } | |
1214 | } | |
1215 | else | |
1216 | { | |
1217 | 0 | int dSize = aSize - command.position; |
1218 | ||
1219 | 0 | if (dSize > 0) |
1220 | { | |
1221 | 0 | Annotation[] deleted = new Annotation[command.number]; |
1222 | 0 | System.arraycopy(annotations[a].annotations, command.position, |
1223 | deleted, 0, dSize); | |
1224 | ||
1225 | 0 | command.deletedAnnotations.put(annotations[a].annotationId, |
1226 | deleted); | |
1227 | ||
1228 | 0 | tSize = Math.min(annotations[a].annotations.length, |
1229 | command.position); | |
1230 | 0 | temp = new Annotation[tSize]; |
1231 | 0 | System.arraycopy(annotations[a].annotations, 0, temp, 0, tSize); |
1232 | } | |
1233 | else | |
1234 | { | |
1235 | 0 | temp = annotations[a].annotations; |
1236 | } | |
1237 | } | |
1238 | } | |
1239 | ||
1240 | 0 | annotations[a].annotations = temp; |
1241 | } | |
1242 | } | |
1243 | ||
1244 | /** | |
1245 | * Restores features to the state before a Cut. | |
1246 | * <ul> | |
1247 | * <li>re-add any features deleted by the cut</li> | |
1248 | * <li>remove any truncated features created by the cut</li> | |
1249 | * <li>shift right any features to the right of the cut</li> | |
1250 | * </ul> | |
1251 | * | |
1252 | * @param command | |
1253 | * the Cut command | |
1254 | * @param seq | |
1255 | * the sequence the Cut applied to | |
1256 | * @param start | |
1257 | * the start residue position of the cut | |
1258 | * @param length | |
1259 | * the number of residues cut | |
1260 | * @param sameDatasetSequence | |
1261 | * true if dataset sequence and frame of reference were left | |
1262 | * unchanged by the Cut | |
1263 | */ | |
1264 | 38 | final static void undoCutFeatures(Edit command, SequenceI seq, |
1265 | final int start, final int length, boolean sameDatasetSequence) | |
1266 | { | |
1267 | 38 | SequenceI sequence = seq.getDatasetSequence(); |
1268 | 38 | if (sequence == null) |
1269 | { | |
1270 | 0 | sequence = seq; |
1271 | } | |
1272 | ||
1273 | /* | |
1274 | * shift right features that lie to the right of the restored cut (but not | |
1275 | * if dataset sequence unchanged - so coordinates were changed by Cut) | |
1276 | */ | |
1277 | 38 | if (!sameDatasetSequence) |
1278 | { | |
1279 | /* | |
1280 | * shift right all features right of and not | |
1281 | * contiguous with the cut position | |
1282 | */ | |
1283 | 16 | seq.getFeatures().shiftFeatures(start + 1, length); |
1284 | ||
1285 | /* | |
1286 | * shift right any features that start at the cut position, | |
1287 | * unless they were truncated | |
1288 | */ | |
1289 | 16 | List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start, |
1290 | start); | |
1291 | 16 | for (SequenceFeature sf : sfs) |
1292 | { | |
1293 | 80 | if (sf.getBegin() == start) |
1294 | { | |
1295 | 50 | if (!command.truncatedFeatures.containsKey(seq) |
1296 | || !command.truncatedFeatures.get(seq).contains(sf)) | |
1297 | { | |
1298 | /* | |
1299 | * feature was shifted left to cut position (not truncated), | |
1300 | * so shift it back right | |
1301 | */ | |
1302 | 20 | SequenceFeature shifted = new SequenceFeature(sf, |
1303 | sf.getBegin() + length, sf.getEnd() + length, | |
1304 | sf.getFeatureGroup(), sf.getScore()); | |
1305 | 20 | seq.addSequenceFeature(shifted); |
1306 | 20 | seq.deleteFeature(sf); |
1307 | } | |
1308 | } | |
1309 | } | |
1310 | } | |
1311 | ||
1312 | /* | |
1313 | * restore any features that were deleted or truncated | |
1314 | */ | |
1315 | 38 | if (command.deletedFeatures != null |
1316 | && command.deletedFeatures.containsKey(seq)) | |
1317 | { | |
1318 | 16 | for (SequenceFeature deleted : command.deletedFeatures.get(seq)) |
1319 | { | |
1320 | 120 | sequence.addSequenceFeature(deleted); |
1321 | } | |
1322 | } | |
1323 | ||
1324 | /* | |
1325 | * delete any truncated features | |
1326 | */ | |
1327 | 38 | if (command.truncatedFeatures != null |
1328 | && command.truncatedFeatures.containsKey(seq)) | |
1329 | { | |
1330 | 16 | for (SequenceFeature amended : command.truncatedFeatures.get(seq)) |
1331 | { | |
1332 | 90 | sequence.deleteFeature(amended); |
1333 | } | |
1334 | } | |
1335 | } | |
1336 | ||
1337 | /** | |
1338 | * Returns the list of edit commands wrapped by this object. | |
1339 | * | |
1340 | * @return | |
1341 | */ | |
1342 | 32 | public List<Edit> getEdits() |
1343 | { | |
1344 | 32 | return this.edits; |
1345 | } | |
1346 | ||
1347 | /** | |
1348 | * Returns a map whose keys are the dataset sequences, and values their | |
1349 | * aligned sequences before the command edit list was applied. The aligned | |
1350 | * sequences are copies, which may be updated without affecting the originals. | |
1351 | * | |
1352 | * The command holds references to the aligned sequences (after editing). If | |
1353 | * the command is an 'undo',then the prior state is simply the aligned state. | |
1354 | * Otherwise, we have to derive the prior state by working backwards through | |
1355 | * the edit list to infer the aligned sequences before editing. | |
1356 | * | |
1357 | * Note: an alternative solution would be to cache the 'before' state of each | |
1358 | * edit, but this would be expensive in space in the common case that the | |
1359 | * original is never needed (edits are not mirrored). | |
1360 | * | |
1361 | * @return | |
1362 | * @throws IllegalStateException | |
1363 | * on detecting an edit command of a type that can't be unwound | |
1364 | */ | |
1365 | 8 | public Map<SequenceI, SequenceI> priorState(boolean forUndo) |
1366 | { | |
1367 | 8 | Map<SequenceI, SequenceI> result = new HashMap<>(); |
1368 | 8 | if (getEdits() == null) |
1369 | { | |
1370 | 0 | return result; |
1371 | } | |
1372 | 8 | if (forUndo) |
1373 | { | |
1374 | 0 | for (Edit e : getEdits()) |
1375 | { | |
1376 | 0 | for (SequenceI seq : e.getSequences()) |
1377 | { | |
1378 | 0 | SequenceI ds = seq.getDatasetSequence(); |
1379 | // SequenceI preEdit = result.get(ds); | |
1380 | 0 | if (!result.containsKey(ds)) |
1381 | { | |
1382 | /* | |
1383 | * copy sequence including start/end (but don't use copy constructor | |
1384 | * as we don't need annotations) | |
1385 | */ | |
1386 | 0 | SequenceI preEdit = new Sequence("", seq.getSequenceAsString(), |
1387 | seq.getStart(), seq.getEnd()); | |
1388 | 0 | preEdit.setDatasetSequence(ds); |
1389 | 0 | result.put(ds, preEdit); |
1390 | } | |
1391 | } | |
1392 | } | |
1393 | 0 | return result; |
1394 | } | |
1395 | ||
1396 | /* | |
1397 | * Work backwards through the edit list, deriving the sequences before each | |
1398 | * was applied. The final result is the sequence set before any edits. | |
1399 | */ | |
1400 | 8 | Iterator<Edit> editList = new ReverseListIterator<>(getEdits()); |
1401 | 24 | while (editList.hasNext()) |
1402 | { | |
1403 | 16 | Edit oldEdit = editList.next(); |
1404 | 16 | Action action = oldEdit.getAction(); |
1405 | 16 | int position = oldEdit.getPosition(); |
1406 | 16 | int number = oldEdit.getNumber(); |
1407 | 16 | final char gap = oldEdit.getGapCharacter(); |
1408 | 16 | for (SequenceI seq : oldEdit.getSequences()) |
1409 | { | |
1410 | 20 | SequenceI ds = seq.getDatasetSequence(); |
1411 | 20 | SequenceI preEdit = result.get(ds); |
1412 | 20 | if (preEdit == null) |
1413 | { | |
1414 | 12 | preEdit = new Sequence("", seq.getSequenceAsString(), |
1415 | seq.getStart(), seq.getEnd()); | |
1416 | 12 | preEdit.setDatasetSequence(ds); |
1417 | 12 | result.put(ds, preEdit); |
1418 | } | |
1419 | /* | |
1420 | * 'Undo' this edit action on the sequence (updating the value in the | |
1421 | * map). | |
1422 | */ | |
1423 | 20 | if (ds != null) |
1424 | { | |
1425 | 20 | if (action == Action.DELETE_GAP) |
1426 | { | |
1427 | 14 | preEdit.setSequence(new String(StringUtils.insertCharAt( |
1428 | preEdit.getSequence(), position, number, gap))); | |
1429 | } | |
1430 | 6 | else if (action == Action.INSERT_GAP) |
1431 | { | |
1432 | 6 | preEdit.setSequence(new String(StringUtils.deleteChars( |
1433 | preEdit.getSequence(), position, position + number))); | |
1434 | } | |
1435 | else | |
1436 | { | |
1437 | 0 | jalview.bin.Console |
1438 | .errPrintln("Can't undo edit action " + action); | |
1439 | // throw new IllegalStateException("Can't undo edit action " + | |
1440 | // action); | |
1441 | } | |
1442 | } | |
1443 | } | |
1444 | } | |
1445 | 8 | return result; |
1446 | } | |
1447 | ||
1448 | public class Edit | |
1449 | { | |
1450 | SequenceI[] oldds; | |
1451 | ||
1452 | /** | |
1453 | * start and end of sequence prior to edit | |
1454 | */ | |
1455 | Range[] oldStartEnd; | |
1456 | ||
1457 | boolean fullAlignmentHeight = false; | |
1458 | ||
1459 | Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows; | |
1460 | ||
1461 | Map<String, Annotation[]> deletedAnnotations; | |
1462 | ||
1463 | /* | |
1464 | * features deleted by the cut (re-add on Undo) | |
1465 | * (including the original of any shortened features) | |
1466 | */ | |
1467 | Map<SequenceI, List<SequenceFeature>> deletedFeatures; | |
1468 | ||
1469 | /* | |
1470 | * shortened features added by the cut (delete on Undo) | |
1471 | */ | |
1472 | Map<SequenceI, List<SequenceFeature>> truncatedFeatures; | |
1473 | ||
1474 | AlignmentI al; | |
1475 | ||
1476 | final Action command; | |
1477 | ||
1478 | char[][] string; | |
1479 | ||
1480 | SequenceI[] seqs; | |
1481 | ||
1482 | int[] alIndex; | |
1483 | ||
1484 | int position; | |
1485 | ||
1486 | int number; | |
1487 | ||
1488 | char gapChar; | |
1489 | ||
1490 | /* | |
1491 | * flag that identifies edits inserted to balance | |
1492 | * user edits in a 'locked editing' region | |
1493 | */ | |
1494 | private boolean systemGenerated; | |
1495 | ||
1496 | 99 | public Edit(Action cmd, SequenceI[] sqs, int pos, int count, char gap) |
1497 | { | |
1498 | 99 | this.command = cmd; |
1499 | 99 | this.seqs = sqs; |
1500 | 99 | this.position = pos; |
1501 | 99 | this.number = count; |
1502 | 99 | this.gapChar = gap; |
1503 | } | |
1504 | ||
1505 | 64 | Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align) |
1506 | { | |
1507 | 64 | this(cmd, sqs, pos, count, align.getGapCharacter()); |
1508 | ||
1509 | 64 | this.al = align; |
1510 | ||
1511 | 64 | alIndex = new int[sqs.length]; |
1512 | 181 | for (int i = 0; i < sqs.length; i++) |
1513 | { | |
1514 | 117 | alIndex[i] = align.findIndex(sqs[i]); |
1515 | } | |
1516 | ||
1517 | 64 | fullAlignmentHeight = (align.getHeight() == sqs.length); |
1518 | } | |
1519 | ||
1520 | /** | |
1521 | * Constructor given a REPLACE command and the replacement string | |
1522 | * | |
1523 | * @param cmd | |
1524 | * @param sqs | |
1525 | * @param pos | |
1526 | * @param count | |
1527 | * @param align | |
1528 | * @param replace | |
1529 | */ | |
1530 | 9 | Edit(Action cmd, SequenceI[] sqs, int pos, int count, AlignmentI align, |
1531 | String replace) | |
1532 | { | |
1533 | 9 | this(cmd, sqs, pos, count, align); |
1534 | ||
1535 | 9 | string = new char[sqs.length][]; |
1536 | 18 | for (int i = 0; i < sqs.length; i++) |
1537 | { | |
1538 | 9 | string[i] = replace.toCharArray(); |
1539 | } | |
1540 | } | |
1541 | ||
1542 | 21 | public SequenceI[] getSequences() |
1543 | { | |
1544 | 21 | return seqs; |
1545 | } | |
1546 | ||
1547 | 25 | public int getPosition() |
1548 | { | |
1549 | 25 | return position; |
1550 | } | |
1551 | ||
1552 | 68 | public Action getAction() |
1553 | { | |
1554 | 68 | return command; |
1555 | } | |
1556 | ||
1557 | 67 | public int getNumber() |
1558 | { | |
1559 | 67 | return number; |
1560 | } | |
1561 | ||
1562 | 16 | public char getGapCharacter() |
1563 | { | |
1564 | 16 | return gapChar; |
1565 | } | |
1566 | ||
1567 | 6 | public void setSystemGenerated(boolean b) |
1568 | { | |
1569 | 6 | systemGenerated = b; |
1570 | } | |
1571 | ||
1572 | 63 | public boolean isSystemGenerated() |
1573 | { | |
1574 | 63 | return systemGenerated; |
1575 | } | |
1576 | } | |
1577 | ||
1578 | /** | |
1579 | * Returns an iterator over the list of edit commands which traverses the list | |
1580 | * either forwards or backwards. | |
1581 | * | |
1582 | * @param forwards | |
1583 | * @return | |
1584 | */ | |
1585 | 1 | public Iterator<Edit> getEditIterator(boolean forwards) |
1586 | { | |
1587 | 1 | if (forwards) |
1588 | { | |
1589 | 1 | return getEdits().iterator(); |
1590 | } | |
1591 | else | |
1592 | { | |
1593 | 0 | return new ReverseListIterator<>(getEdits()); |
1594 | } | |
1595 | } | |
1596 | ||
1597 | /** | |
1598 | * Adjusts features for Cut, and saves details of changes made to allow Undo | |
1599 | * <ul> | |
1600 | * <li>features left of the cut are unchanged</li> | |
1601 | * <li>features right of the cut are shifted left</li> | |
1602 | * <li>features internal to the cut region are deleted</li> | |
1603 | * <li>features that overlap or span the cut are shortened</li> | |
1604 | * <li>the originals of any deleted or shortened features are saved, to re-add | |
1605 | * on Undo</li> | |
1606 | * <li>any added (shortened) features are saved, to delete on Undo</li> | |
1607 | * </ul> | |
1608 | * | |
1609 | * @param command | |
1610 | * @param seq | |
1611 | * @param fromPosition | |
1612 | * @param toPosition | |
1613 | * @param cutIsInternal | |
1614 | */ | |
1615 | 18 | protected static void cutFeatures(Edit command, SequenceI seq, |
1616 | int fromPosition, int toPosition, boolean cutIsInternal) | |
1617 | { | |
1618 | /* | |
1619 | * if the cut is at start or end of sequence | |
1620 | * then we don't modify the sequence feature store | |
1621 | */ | |
1622 | 18 | if (!cutIsInternal) |
1623 | { | |
1624 | 0 | return; |
1625 | } | |
1626 | 18 | List<SequenceFeature> added = new ArrayList<>(); |
1627 | 18 | List<SequenceFeature> removed = new ArrayList<>(); |
1628 | ||
1629 | 18 | SequenceFeaturesI featureStore = seq.getFeatures(); |
1630 | 18 | if (toPosition < fromPosition || featureStore == null) |
1631 | { | |
1632 | 0 | return; |
1633 | } | |
1634 | ||
1635 | 18 | int cutStartPos = fromPosition; |
1636 | 18 | int cutEndPos = toPosition; |
1637 | 18 | int cutWidth = cutEndPos - cutStartPos + 1; |
1638 | ||
1639 | 18 | synchronized (featureStore) |
1640 | { | |
1641 | /* | |
1642 | * get features that overlap the cut region | |
1643 | */ | |
1644 | 18 | List<SequenceFeature> toAmend = featureStore.findFeatures(cutStartPos, |
1645 | cutEndPos); | |
1646 | ||
1647 | /* | |
1648 | * add any contact features that span the cut region | |
1649 | * (not returned by findFeatures) | |
1650 | */ | |
1651 | 18 | for (SequenceFeature contact : featureStore.getContactFeatures()) |
1652 | { | |
1653 | 4 | if (contact.getBegin() < cutStartPos |
1654 | && contact.getEnd() > cutEndPos) | |
1655 | { | |
1656 | 1 | toAmend.add(contact); |
1657 | } | |
1658 | } | |
1659 | ||
1660 | /* | |
1661 | * adjust start-end of overlapping features; | |
1662 | * delete features enclosed by the cut; | |
1663 | * delete partially overlapping contact features | |
1664 | */ | |
1665 | 18 | for (SequenceFeature sf : toAmend) |
1666 | { | |
1667 | 67 | int sfBegin = sf.getBegin(); |
1668 | 67 | int sfEnd = sf.getEnd(); |
1669 | 67 | int newBegin = sfBegin; |
1670 | 67 | int newEnd = sfEnd; |
1671 | 67 | boolean toDelete = false; |
1672 | 67 | boolean follows = false; |
1673 | ||
1674 | 67 | if (sfBegin >= cutStartPos && sfEnd <= cutEndPos) |
1675 | { | |
1676 | /* | |
1677 | * feature lies within cut region - delete it | |
1678 | */ | |
1679 | 17 | toDelete = true; |
1680 | } | |
1681 | 50 | else if (sfBegin < cutStartPos && sfEnd > cutEndPos) |
1682 | { | |
1683 | /* | |
1684 | * feature spans cut region - left-shift the end | |
1685 | */ | |
1686 | 16 | newEnd -= cutWidth; |
1687 | } | |
1688 | 34 | else if (sfEnd <= cutEndPos) |
1689 | { | |
1690 | /* | |
1691 | * feature overlaps left of cut region - truncate right | |
1692 | */ | |
1693 | 17 | newEnd = cutStartPos - 1; |
1694 | 17 | if (sf.isContactFeature()) |
1695 | { | |
1696 | 1 | toDelete = true; |
1697 | } | |
1698 | } | |
1699 | 17 | else if (sfBegin >= cutStartPos) |
1700 | { | |
1701 | /* | |
1702 | * remaining case - feature overlaps right | |
1703 | * truncate left, adjust end of feature | |
1704 | */ | |
1705 | 17 | newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1; |
1706 | 17 | newEnd = newBegin + sfEnd - cutEndPos - 1; |
1707 | 17 | if (sf.isContactFeature()) |
1708 | { | |
1709 | 1 | toDelete = true; |
1710 | } | |
1711 | } | |
1712 | ||
1713 | 67 | seq.deleteFeature(sf); |
1714 | 67 | if (!follows) |
1715 | { | |
1716 | 67 | removed.add(sf); |
1717 | } | |
1718 | 67 | if (!toDelete) |
1719 | { | |
1720 | 48 | SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd, |
1721 | sf.getFeatureGroup(), sf.getScore()); | |
1722 | 48 | seq.addSequenceFeature(copy); |
1723 | 48 | if (!follows) |
1724 | { | |
1725 | 48 | added.add(copy); |
1726 | } | |
1727 | } | |
1728 | } | |
1729 | ||
1730 | /* | |
1731 | * and left shift any features lying to the right of the cut region | |
1732 | */ | |
1733 | ||
1734 | 18 | featureStore.shiftFeatures(cutEndPos + 1, -cutWidth); |
1735 | } | |
1736 | ||
1737 | /* | |
1738 | * save deleted and amended features, so that Undo can | |
1739 | * re-add or delete them respectively | |
1740 | */ | |
1741 | 18 | if (command.deletedFeatures == null) |
1742 | { | |
1743 | 9 | command.deletedFeatures = new HashMap<>(); |
1744 | } | |
1745 | 18 | if (command.truncatedFeatures == null) |
1746 | { | |
1747 | 9 | command.truncatedFeatures = new HashMap<>(); |
1748 | } | |
1749 | 18 | command.deletedFeatures.put(seq, removed); |
1750 | 18 | command.truncatedFeatures.put(seq, added); |
1751 | } | |
1752 | } |