JAL-845 code/refactoring/tests related to linking DNA and protein
[jalview.git] / src / jalview / util / MappingUtils.java
1 package jalview.util;
2
3 import jalview.analysis.AlignmentSorter;
4 import jalview.api.AlignViewportI;
5 import jalview.commands.CommandI;
6 import jalview.commands.EditCommand;
7 import jalview.commands.EditCommand.Action;
8 import jalview.commands.EditCommand.Edit;
9 import jalview.commands.OrderCommand;
10 import jalview.datamodel.AlignedCodonFrame;
11 import jalview.datamodel.AlignmentI;
12 import jalview.datamodel.AlignmentOrder;
13 import jalview.datamodel.ColumnSelection;
14 import jalview.datamodel.SearchResults;
15 import jalview.datamodel.SearchResults.Match;
16 import jalview.datamodel.Sequence;
17 import jalview.datamodel.SequenceGroup;
18 import jalview.datamodel.SequenceI;
19 import jalview.gui.AlignViewport;
20
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27
28 /**
29  * Helper methods for manipulations involving sequence mappings.
30  * 
31  * @author gmcarstairs
32  *
33  */
34 public final class MappingUtils
35 {
36
37   /**
38    * Helper method to map a CUT or PASTE command.
39    * 
40    * @param edit
41    *          the original command
42    * @param undo
43    *          if true, the command is to be undone
44    * @param targetSeqs
45    *          the mapped sequences to apply the mapped command to
46    * @param result
47    *          the mapped EditCommand to add to
48    * @param mappings
49    */
50   protected static void mapCutOrPaste(Edit edit, boolean undo,
51           List<SequenceI> targetSeqs, EditCommand result,
52           Set<AlignedCodonFrame> mappings)
53   {
54     Action action = edit.getAction();
55     if (undo)
56     {
57       action = action.getUndoAction();
58     }
59     // TODO write this
60     System.err.println("MappingUtils.mapCutOrPaste not yet implemented");
61   }
62
63   /**
64    * Returns a new EditCommand representing the given command as mapped to the
65    * given sequences. If there is no mapping, returns null.
66    * 
67    * @param command
68    * @param undo
69    * @param mapTo
70    * @param gapChar
71    * @param mappings
72    * @return
73    */
74   public static EditCommand mapEditCommand(EditCommand command,
75           boolean undo, final AlignmentI mapTo, char gapChar,
76           Set<AlignedCodonFrame> mappings)
77   {
78     /*
79      * For now, only support mapping from protein edits to cDna
80      */
81     if (!mapTo.isNucleotide())
82     {
83       return null;
84     }
85
86     /*
87      * Cache a copy of the target sequences so we can mimic successive edits on
88      * them. This lets us compute mappings for all edits in the set.
89      */
90     Map<SequenceI, SequenceI> targetCopies = new HashMap<SequenceI, SequenceI>();
91     for (SequenceI seq : mapTo.getSequences())
92     {
93       SequenceI ds = seq.getDatasetSequence();
94       if (ds != null)
95       {
96         final SequenceI copy = new Sequence("", new String(
97                 seq.getSequence()));
98         copy.setDatasetSequence(ds);
99         targetCopies.put(ds, copy);
100       }
101     }
102
103     /*
104      * Compute 'source' sequences as they were before applying edits:
105      */
106     Map<SequenceI, SequenceI> originalSequences = command.priorState(undo);
107
108     EditCommand result = new EditCommand();
109     Iterator<Edit> edits = command.getEditIterator(!undo);
110     while (edits.hasNext())
111     {
112       Edit edit = edits.next();
113       if (edit.getAction() == Action.CUT
114               || edit.getAction() == Action.PASTE)
115       {
116         mapCutOrPaste(edit, undo, mapTo.getSequences(), result, mappings);
117       }
118       else if (edit.getAction() == Action.INSERT_GAP
119               || edit.getAction() == Action.DELETE_GAP)
120       {
121         mapInsertOrDelete(edit, undo, originalSequences,
122                 mapTo.getSequences(), targetCopies, gapChar, result,
123                 mappings);
124       }
125     }
126     return result.getSize() > 0 ? result : null;
127   }
128
129   /**
130    * Helper method to map an edit command to insert or delete gaps.
131    * 
132    * @param edit
133    *          the original command
134    * @param undo
135    *          if true, the action is to undo the command
136    * @param originalSequences
137    *          the sequences the command acted on
138    * @param targetSeqs
139    * @param targetCopies
140    * @param gapChar
141    * @param result
142    *          the new EditCommand to add mapped commands to
143    * @param mappings
144    */
145   protected static void mapInsertOrDelete(Edit edit, boolean undo,
146           Map<SequenceI, SequenceI> originalSequences,
147           final List<SequenceI> targetSeqs,
148           Map<SequenceI, SequenceI> targetCopies, char gapChar,
149           EditCommand result, Set<AlignedCodonFrame> mappings)
150   {
151     Action action = edit.getAction();
152
153     /*
154      * Invert sense of action if an Undo.
155      */
156     if (undo)
157     {
158       action = action.getUndoAction();
159     }
160     final int count = edit.getNumber();
161     final int editPos = edit.getPosition();
162     for (SequenceI seq : edit.getSequences())
163     {
164       /*
165        * Get residue position at (or to right of) edit location. Note we use our
166        * 'copy' of the sequence before editing for this.
167        */
168       SequenceI ds = seq.getDatasetSequence();
169       if (ds == null)
170       {
171         continue;
172       }
173       final SequenceI actedOn = originalSequences.get(ds);
174       final int seqpos = actedOn.findPosition(editPos);
175
176       /*
177        * Determine all mappings from this position to mapped sequences.
178        */
179       SearchResults sr = buildSearchResults(seq, seqpos, mappings);
180
181       if (!sr.isEmpty())
182       {
183         for (SequenceI targetSeq : targetSeqs)
184         {
185           ds = targetSeq.getDatasetSequence();
186           if (ds == null)
187           {
188             continue;
189           }
190           SequenceI copyTarget = targetCopies.get(ds);
191           final int[] match = sr.getResults(copyTarget, 0,
192                   copyTarget.getLength());
193           if (match != null)
194           {
195             final int ratio = 3; // TODO: compute this - how?
196             final int mappedCount = count * ratio;
197
198             /*
199              * Shift Delete start position left, as it acts on positions to its
200              * right.
201              */
202             int mappedEditPos = action == Action.DELETE_GAP ? match[0]
203                     - mappedCount : match[0];
204             Edit e = result.new Edit(action, new SequenceI[]
205             { targetSeq }, mappedEditPos, mappedCount, gapChar);
206             result.addEdit(e);
207
208             /*
209              * and 'apply' the edit to our copy of its target sequence
210              */
211             if (action == Action.INSERT_GAP)
212             {
213               copyTarget.setSequence(new String(StringUtils.insertCharAt(
214                       copyTarget.getSequence(), mappedEditPos, mappedCount,
215                       gapChar)));
216             }
217             else if (action == Action.DELETE_GAP)
218             {
219               copyTarget.setSequence(new String(StringUtils.deleteChars(
220                       copyTarget.getSequence(), mappedEditPos,
221                       mappedEditPos + mappedCount)));
222             }
223           }
224         }
225       }
226       /*
227        * and 'apply' the edit to our copy of its source sequence
228        */
229       if (action == Action.INSERT_GAP)
230       {
231         actedOn.setSequence(new String(StringUtils.insertCharAt(
232                 actedOn.getSequence(), editPos, count, gapChar)));
233       }
234       else if (action == Action.DELETE_GAP)
235       {
236         actedOn.setSequence(new String(StringUtils.deleteChars(
237                 actedOn.getSequence(), editPos, editPos + count)));
238       }
239     }
240   }
241
242   /**
243    * Returns a SearchResults object describing the mapped region corresponding
244    * to the specified sequence position.
245    * 
246    * @param seq
247    * @param index
248    * @param seqmappings
249    * @return
250    */
251   public static SearchResults buildSearchResults(SequenceI seq, int index,
252           Set<AlignedCodonFrame> seqmappings)
253   {
254     SearchResults results;
255     results = new SearchResults();
256     if (index >= seq.getStart() && index <= seq.getEnd())
257     {
258       for (AlignedCodonFrame acf : seqmappings)
259       {
260         acf.markMappedRegion(seq, index, results);
261       }
262       results.addResult(seq, index, index);
263     }
264     return results;
265   }
266
267   /**
268    * Returns a (possibly empty) SequenceGroup containing any sequences in the
269    * mapped viewport corresponding to the given group in the source viewport.
270    * 
271    * @param sg
272    * @param mapFrom
273    * @param mapTo
274    * @return
275    */
276   public static SequenceGroup mapSequenceGroup(SequenceGroup sg,
277           AlignViewportI mapFrom, AlignViewportI mapTo)
278   {
279     /*
280      * Note the SequenceGroup holds aligned sequences, the mappings hold dataset
281      * sequences.
282      */
283     boolean targetIsNucleotide = mapTo.isNucleotide();
284     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
285     Set<AlignedCodonFrame> codonFrames = protein.getAlignment()
286             .getCodonFrames();
287
288     /*
289      * Copy group name, colours, but not sequences
290      */
291     SequenceGroup mappedGroup = new SequenceGroup(sg);
292     mappedGroup.clear();
293     // TODO set width of mapped group
294
295     for (SequenceI selected : sg.getSequences())
296     {
297       for (AlignedCodonFrame acf : codonFrames)
298       {
299         SequenceI mappedSequence = targetIsNucleotide ? acf
300                 .getDnaForAaSeq(selected) : acf.getAaForDnaSeq(selected);
301         if (mappedSequence != null)
302         {
303           for (SequenceI seq : mapTo.getAlignment().getSequences())
304           {
305             if (seq.getDatasetSequence() == mappedSequence)
306             {
307               mappedGroup.addSequence(seq, false);
308               break;
309             }
310           }
311         }
312       }
313     }
314     return mappedGroup;
315   }
316
317   /**
318    * Returns an OrderCommand equivalent to the given one, but acting on mapped
319    * sequences as described by the mappings, or null if no mapping can be made.
320    * 
321    * @param command
322    *          the original order command
323    * @param undo
324    *          if true, the action is to undo the sort
325    * @param mapTo
326    *          the alignment we are mapping to
327    * @param mappings
328    *          the mappings available
329    * @return
330    */
331   public static CommandI mapOrderCommand(OrderCommand command,
332           boolean undo, AlignmentI mapTo, Set<AlignedCodonFrame> mappings)
333   {
334     SequenceI[] sortOrder = command.getSequenceOrder(undo);
335     List<SequenceI> mappedOrder = new ArrayList<SequenceI>();
336     int j = 0;
337     for (SequenceI seq : sortOrder)
338     {
339       for (AlignedCodonFrame acf : mappings)
340       {
341         /*
342          * Try protein-to-Dna, failing that try dna-to-protein
343          */
344         SequenceI mappedSeq = acf.getDnaForAaSeq(seq);
345         if (mappedSeq == null)
346         {
347           mappedSeq = acf.getAaForDnaSeq(seq);
348         }
349         if (mappedSeq != null)
350         {
351           for (SequenceI seq2 : mapTo.getSequences())
352           {
353             if (seq2.getDatasetSequence() == mappedSeq)
354             {
355               mappedOrder.add(seq2);
356               j++;
357               break;
358             }
359           }
360         }
361       }
362     }
363
364     /*
365      * Return null if no mappings made.
366      */
367     if (j == 0)
368     {
369       return null;
370     }
371
372     /*
373      * Add any unmapped sequences on the end of the sort in their original
374      * ordering.
375      */
376     if (j < mapTo.getHeight())
377     {
378       for (SequenceI seq : mapTo.getSequences())
379       {
380         if (!mappedOrder.contains(seq))
381         {
382           mappedOrder.add(seq);
383         }
384       }
385     }
386
387     /*
388      * Have to align the sequences before constructing the OrderCommand - which
389      * then realigns them?!?
390      */
391     final SequenceI[] mappedOrderArray = mappedOrder
392             .toArray(new SequenceI[mappedOrder.size()]);
393     SequenceI[] oldOrder = mapTo.getSequencesArray();
394     AlignmentSorter.sortBy(mapTo, new AlignmentOrder(mappedOrderArray));
395     final OrderCommand result = new OrderCommand(command.getDescription(),
396             oldOrder, mapTo);
397     return result;
398   }
399
400   /**
401    * Returns a ColumnSelection in the 'mapTo' view which corresponds to the
402    * given selection in the 'mapFrom' view. We assume one is nucleotide, the
403    * other is protein (and holds the mappings from codons to protein residues).
404    * 
405    * @param colsel
406    * @param mapFrom
407    * @param mapTo
408    * @return
409    */
410   public static ColumnSelection mapColumnSelection(ColumnSelection colsel,
411           AlignViewportI mapFrom, AlignViewport mapTo)
412   {
413     boolean targetIsNucleotide = mapTo.isNucleotide();
414     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
415     Set<AlignedCodonFrame> codonFrames = protein.getAlignment()
416             .getCodonFrames();
417     ColumnSelection mappedColumns = new ColumnSelection();
418     char fromGapChar = mapFrom.getAlignment().getGapCharacter();
419
420     for (int col : colsel.getSelected())
421     {
422       int mappedToMin = Integer.MAX_VALUE;
423       int mappedToMax = Integer.MIN_VALUE;
424
425       /*
426        * For each sequence in the 'from' alignment
427        */
428       for (SequenceI fromSeq : mapFrom.getAlignment().getSequences())
429       {
430         /*
431          * Ignore gaps (unmapped anyway)
432          */
433         if (fromSeq.getCharAt(col) == fromGapChar)
434         {
435           continue;
436         }
437
438         /*
439          * Get the residue position and find the mapped position.
440          */
441         int residuePos = fromSeq.findPosition(col);
442         SearchResults sr = buildSearchResults(fromSeq, residuePos,
443                 codonFrames);
444         for (Match m : sr.getResults())
445         {
446           int mappedStartResidue = m.getStart();
447           int mappedEndResidue = m.getEnd();
448           SequenceI mappedSeq = m.getSequence();
449
450           /*
451            * Locate the aligned sequence whose dataset is mappedSeq.
452            */
453           for (SequenceI toSeq : mapTo.getAlignment().getSequences())
454           {
455             if (toSeq.getDatasetSequence() == mappedSeq)
456             {
457               int mappedStartCol = toSeq.findIndex(mappedStartResidue);
458               int mappedEndCol = toSeq.findIndex(mappedEndResidue);
459               mappedToMin = Math.min(mappedToMin, mappedStartCol);
460               mappedToMax = Math.max(mappedToMax, mappedEndCol);
461               // System.out.println(fromSeq.getName() + " mapped to cols "
462               // + mappedStartCol + ":" + mappedEndCol);
463             }
464           }
465         }
466       }
467       /*
468        * Add mapped columns to mapped selection (converting base 1 to base 0)
469        */
470       for (int i = mappedToMin; i <= mappedToMax; i++)
471       {
472         mappedColumns.addElement(i - 1);
473       }
474     }
475     return mappedColumns;
476   }
477
478 }