JAL-845 fixed start/end range of a mapped SequenceGroup
[jalview.git] / src / jalview / util / MappingUtils.java
1 package jalview.util;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.HashMap;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Set;
10
11 import jalview.analysis.AlignmentSorter;
12 import jalview.api.AlignViewportI;
13 import jalview.commands.CommandI;
14 import jalview.commands.EditCommand;
15 import jalview.commands.EditCommand.Action;
16 import jalview.commands.EditCommand.Edit;
17 import jalview.commands.OrderCommand;
18 import jalview.datamodel.AlignedCodonFrame;
19 import jalview.datamodel.AlignmentI;
20 import jalview.datamodel.AlignmentOrder;
21 import jalview.datamodel.ColumnSelection;
22 import jalview.datamodel.SearchResults;
23 import jalview.datamodel.SearchResults.Match;
24 import jalview.datamodel.Sequence;
25 import jalview.datamodel.SequenceGroup;
26 import jalview.datamodel.SequenceI;
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(seq);
97         copy.setDatasetSequence(ds);
98         targetCopies.put(ds, copy);
99       }
100     }
101
102     /*
103      * Compute 'source' sequences as they were before applying edits:
104      */
105     Map<SequenceI, SequenceI> originalSequences = command.priorState(undo);
106
107     EditCommand result = new EditCommand();
108     Iterator<Edit> edits = command.getEditIterator(!undo);
109     while (edits.hasNext())
110     {
111       Edit edit = edits.next();
112       if (edit.getAction() == Action.CUT
113               || edit.getAction() == Action.PASTE)
114       {
115         mapCutOrPaste(edit, undo, mapTo.getSequences(), result, mappings);
116       }
117       else if (edit.getAction() == Action.INSERT_GAP
118               || edit.getAction() == Action.DELETE_GAP)
119       {
120         mapInsertOrDelete(edit, undo, originalSequences,
121                 mapTo.getSequences(), targetCopies, gapChar, result,
122                 mappings);
123       }
124     }
125     return result.getSize() > 0 ? result : null;
126   }
127
128   /**
129    * Helper method to map an edit command to insert or delete gaps.
130    * 
131    * @param edit
132    *          the original command
133    * @param undo
134    *          if true, the action is to undo the command
135    * @param originalSequences
136    *          the sequences the command acted on
137    * @param targetSeqs
138    * @param targetCopies
139    * @param gapChar
140    * @param result
141    *          the new EditCommand to add mapped commands to
142    * @param mappings
143    */
144   protected static void mapInsertOrDelete(Edit edit, boolean undo,
145           Map<SequenceI, SequenceI> originalSequences,
146           final List<SequenceI> targetSeqs,
147           Map<SequenceI, SequenceI> targetCopies, char gapChar,
148           EditCommand result, Set<AlignedCodonFrame> mappings)
149   {
150     Action action = edit.getAction();
151
152     /*
153      * Invert sense of action if an Undo.
154      */
155     if (undo)
156     {
157       action = action.getUndoAction();
158     }
159     final int count = edit.getNumber();
160     final int editPos = edit.getPosition();
161     for (SequenceI seq : edit.getSequences())
162     {
163       /*
164        * Get residue position at (or to right of) edit location. Note we use our
165        * 'copy' of the sequence before editing for this.
166        */
167       SequenceI ds = seq.getDatasetSequence();
168       if (ds == null)
169       {
170         continue;
171       }
172       final SequenceI actedOn = originalSequences.get(ds);
173       final int seqpos = actedOn.findPosition(editPos);
174
175       /*
176        * Determine all mappings from this position to mapped sequences.
177        */
178       SearchResults sr = buildSearchResults(seq, seqpos, mappings);
179
180       if (!sr.isEmpty())
181       {
182         for (SequenceI targetSeq : targetSeqs)
183         {
184           ds = targetSeq.getDatasetSequence();
185           if (ds == null)
186           {
187             continue;
188           }
189           SequenceI copyTarget = targetCopies.get(ds);
190           final int[] match = sr.getResults(copyTarget, 0,
191                   copyTarget.getLength());
192           if (match != null)
193           {
194             final int ratio = 3; // TODO: compute this - how?
195             final int mappedCount = count * ratio;
196
197             /*
198              * Shift Delete start position left, as it acts on positions to its
199              * right.
200              */
201             int mappedEditPos = action == Action.DELETE_GAP ? match[0]
202                     - mappedCount : match[0];
203             Edit e = result.new Edit(action, new SequenceI[]
204             { targetSeq }, mappedEditPos, mappedCount, gapChar);
205             result.addEdit(e);
206
207             /*
208              * and 'apply' the edit to our copy of its target sequence
209              */
210             if (action == Action.INSERT_GAP)
211             {
212               copyTarget.setSequence(new String(StringUtils.insertCharAt(
213                       copyTarget.getSequence(), mappedEditPos, mappedCount,
214                       gapChar)));
215             }
216             else if (action == Action.DELETE_GAP)
217             {
218               copyTarget.setSequence(new String(StringUtils.deleteChars(
219                       copyTarget.getSequence(), mappedEditPos,
220                       mappedEditPos + mappedCount)));
221             }
222           }
223         }
224       }
225       /*
226        * and 'apply' the edit to our copy of its source sequence
227        */
228       if (action == Action.INSERT_GAP)
229       {
230         actedOn.setSequence(new String(StringUtils.insertCharAt(
231                 actedOn.getSequence(), editPos, count, gapChar)));
232       }
233       else if (action == Action.DELETE_GAP)
234       {
235         actedOn.setSequence(new String(StringUtils.deleteChars(
236                 actedOn.getSequence(), editPos, editPos + count)));
237       }
238     }
239   }
240
241   /**
242    * Returns a SearchResults object describing the mapped region corresponding
243    * to the specified sequence position.
244    * 
245    * @param seq
246    * @param index
247    * @param seqmappings
248    * @return
249    */
250   public static SearchResults buildSearchResults(SequenceI seq, int index,
251           Set<AlignedCodonFrame> seqmappings)
252   {
253     SearchResults results;
254     results = new SearchResults();
255     if (index >= seq.getStart() && index <= seq.getEnd())
256     {
257       for (AlignedCodonFrame acf : seqmappings)
258       {
259         acf.markMappedRegion(seq, index, results);
260       }
261     }
262     return results;
263   }
264
265   /**
266    * Returns a (possibly empty) SequenceGroup containing any sequences in the
267    * mapped viewport corresponding to the given group in the source viewport.
268    * 
269    * @param sg
270    * @param mapFrom
271    * @param mapTo
272    * @return
273    */
274   public static SequenceGroup mapSequenceGroup(final SequenceGroup sg,
275           final AlignViewportI mapFrom, final AlignViewportI mapTo)
276   {
277     /*
278      * Note the SequenceGroup holds aligned sequences, the mappings hold dataset
279      * sequences.
280      */
281     boolean targetIsNucleotide = mapTo.isNucleotide();
282     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
283     Set<AlignedCodonFrame> codonFrames = protein.getAlignment()
284             .getCodonFrames();
285     /*
286      * Copy group name, colours etc, but not sequences or sequence colour scheme
287      */
288     SequenceGroup mappedGroup = new SequenceGroup(sg);
289     mappedGroup.cs = mapTo.getGlobalColourScheme();
290     mappedGroup.clear();
291
292     int minStartCol = -1;
293     int maxEndCol = -1;
294     final int selectionStartRes = sg.getStartRes();
295     final int selectionEndRes = sg.getEndRes();
296     for (SequenceI selected : sg.getSequences())
297     {
298       /*
299        * Find the widest range of non-gapped positions in the selection range
300        */
301       int firstUngappedPos = selectionStartRes;
302       while (firstUngappedPos <= selectionEndRes
303               && Comparison.isGap(selected.getCharAt(firstUngappedPos)))
304       {
305         firstUngappedPos++;
306       }
307
308       /*
309        * If this sequence is only gaps in the selected range, skip it
310        */
311       if (firstUngappedPos > selectionEndRes)
312       {
313         continue;
314       }
315
316       int lastUngappedPos = selectionEndRes;
317       while (lastUngappedPos >= selectionStartRes
318               && Comparison.isGap(selected.getCharAt(lastUngappedPos)))
319       {
320         lastUngappedPos--;
321       }
322
323       /*
324        * Find the selected start/end residue positions in sequence
325        */
326       int startResiduePos = selected.findPosition(firstUngappedPos);
327       int endResiduePos = selected.findPosition(lastUngappedPos);
328       
329       for (AlignedCodonFrame acf : codonFrames)
330       {
331         SequenceI mappedSequence = targetIsNucleotide ? acf
332                 .getDnaForAaSeq(selected) : acf.getAaForDnaSeq(selected);
333         if (mappedSequence != null)
334         {
335           for (SequenceI seq : mapTo.getAlignment().getSequences())
336           {
337             int mappedStartResidue = 0;
338             int mappedEndResidue = 0;
339             if (seq.getDatasetSequence() == mappedSequence)
340             {
341               /*
342                * Found a sequence mapping. Locate the start/end mapped residues.
343                */
344               SearchResults sr = buildSearchResults(selected,
345                       startResiduePos, Collections.singleton(acf));
346               for (Match m : sr.getResults())
347               {
348                 mappedStartResidue = m.getStart();
349                 mappedEndResidue = m.getEnd();
350               }
351               sr = buildSearchResults(selected, endResiduePos,
352                       Collections.singleton(acf));
353               for (Match m : sr.getResults())
354               {
355                 mappedStartResidue = Math.min(mappedStartResidue,
356                         m.getStart());
357                 mappedEndResidue = Math.max(mappedEndResidue, m.getEnd());
358               }
359
360               /*
361                * Find the mapped aligned columns, save the range. Note findIndex
362                * returns a base 1 position, SequenceGroup uses base 0
363                */
364               int mappedStartCol = seq.findIndex(mappedStartResidue) - 1;
365               minStartCol = minStartCol == -1 ? mappedStartCol : Math.min(
366                       minStartCol, mappedStartCol);
367               int mappedEndCol = seq.findIndex(mappedEndResidue) - 1;
368               maxEndCol = maxEndCol == -1 ? mappedEndCol : Math.max(
369                       maxEndCol, mappedEndCol);
370               mappedGroup.addSequence(seq, false);
371               break;
372             }
373           }
374         }
375       }
376     }
377     mappedGroup.setStartRes(minStartCol < 0 ? 0 : minStartCol);
378     mappedGroup.setEndRes(maxEndCol < 0 ? 0 : maxEndCol);
379     return mappedGroup;
380   }
381
382   /**
383    * Returns an OrderCommand equivalent to the given one, but acting on mapped
384    * sequences as described by the mappings, or null if no mapping can be made.
385    * 
386    * @param command
387    *          the original order command
388    * @param undo
389    *          if true, the action is to undo the sort
390    * @param mapTo
391    *          the alignment we are mapping to
392    * @param mappings
393    *          the mappings available
394    * @return
395    */
396   public static CommandI mapOrderCommand(OrderCommand command,
397           boolean undo, AlignmentI mapTo, Set<AlignedCodonFrame> mappings)
398   {
399     SequenceI[] sortOrder = command.getSequenceOrder(undo);
400     List<SequenceI> mappedOrder = new ArrayList<SequenceI>();
401     int j = 0;
402     for (SequenceI seq : sortOrder)
403     {
404       for (AlignedCodonFrame acf : mappings)
405       {
406         /*
407          * Try protein-to-Dna, failing that try dna-to-protein
408          */
409         SequenceI mappedSeq = acf.getDnaForAaSeq(seq);
410         if (mappedSeq == null)
411         {
412           mappedSeq = acf.getAaForDnaSeq(seq);
413         }
414         if (mappedSeq != null)
415         {
416           for (SequenceI seq2 : mapTo.getSequences())
417           {
418             if (seq2.getDatasetSequence() == mappedSeq)
419             {
420               mappedOrder.add(seq2);
421               j++;
422               break;
423             }
424           }
425         }
426       }
427     }
428
429     /*
430      * Return null if no mappings made.
431      */
432     if (j == 0)
433     {
434       return null;
435     }
436
437     /*
438      * Add any unmapped sequences on the end of the sort in their original
439      * ordering.
440      */
441     if (j < mapTo.getHeight())
442     {
443       for (SequenceI seq : mapTo.getSequences())
444       {
445         if (!mappedOrder.contains(seq))
446         {
447           mappedOrder.add(seq);
448         }
449       }
450     }
451
452     /*
453      * Have to sort the sequences before constructing the OrderCommand - which
454      * then resorts them?!?
455      */
456     final SequenceI[] mappedOrderArray = mappedOrder
457             .toArray(new SequenceI[mappedOrder.size()]);
458     SequenceI[] oldOrder = mapTo.getSequencesArray();
459     AlignmentSorter.sortBy(mapTo, new AlignmentOrder(mappedOrderArray));
460     final OrderCommand result = new OrderCommand(command.getDescription(),
461             oldOrder, mapTo);
462     return result;
463   }
464
465   /**
466    * Returns a ColumnSelection in the 'mapTo' view which corresponds to the
467    * given selection in the 'mapFrom' view. We assume one is nucleotide, the
468    * other is protein (and holds the mappings from codons to protein residues).
469    * 
470    * @param colsel
471    * @param mapFrom
472    * @param mapTo
473    * @return
474    */
475   public static ColumnSelection mapColumnSelection(ColumnSelection colsel,
476           AlignViewportI mapFrom, AlignViewportI mapTo)
477   {
478     boolean targetIsNucleotide = mapTo.isNucleotide();
479     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
480     Set<AlignedCodonFrame> codonFrames = protein.getAlignment()
481             .getCodonFrames();
482     ColumnSelection mappedColumns = new ColumnSelection();
483     char fromGapChar = mapFrom.getAlignment().getGapCharacter();
484
485     // FIXME allow for hidden columns
486
487     /*
488      * For each mapped column, find the range of columns that residues in that
489      * column map to.
490      */
491     for (Object obj : colsel.getSelected())
492     {
493       int col = ((Integer) obj).intValue();
494       int mappedToMin = Integer.MAX_VALUE;
495       int mappedToMax = Integer.MIN_VALUE;
496
497       /*
498        * For each sequence in the 'from' alignment
499        */
500       for (SequenceI fromSeq : mapFrom.getAlignment().getSequences())
501       {
502         /*
503          * Ignore gaps (unmapped anyway)
504          */
505         if (fromSeq.getCharAt(col) == fromGapChar)
506         {
507           continue;
508         }
509
510         /*
511          * Get the residue position and find the mapped position.
512          */
513         int residuePos = fromSeq.findPosition(col);
514         SearchResults sr = buildSearchResults(fromSeq, residuePos,
515                 codonFrames);
516         for (Match m : sr.getResults())
517         {
518           int mappedStartResidue = m.getStart();
519           int mappedEndResidue = m.getEnd();
520           SequenceI mappedSeq = m.getSequence();
521
522           /*
523            * Locate the aligned sequence whose dataset is mappedSeq. TODO a
524            * datamodel that can do this efficiently.
525            */
526           for (SequenceI toSeq : mapTo.getAlignment().getSequences())
527           {
528             if (toSeq.getDatasetSequence() == mappedSeq)
529             {
530               int mappedStartCol = toSeq.findIndex(mappedStartResidue);
531               int mappedEndCol = toSeq.findIndex(mappedEndResidue);
532               mappedToMin = Math.min(mappedToMin, mappedStartCol);
533               mappedToMax = Math.max(mappedToMax, mappedEndCol);
534               // System.out.println(fromSeq.getName() + " mapped to cols "
535               // + mappedStartCol + ":" + mappedEndCol);
536               break;
537               // note: remove break if we ever want to map one to many sequences
538             }
539           }
540         }
541       }
542       /*
543        * Add the range of mapped columns to the mapped selection (converting
544        * base 1 to base 0). Note that this may include intron-only regions which
545        * lie between the start and end ranges of the selection.
546        */
547       for (int i = mappedToMin; i <= mappedToMax; i++)
548       {
549         mappedColumns.addElement(i - 1);
550       }
551     }
552     return mappedColumns;
553   }
554
555   /**
556    * Returns the mapped codon for a given aligned sequence column position (base
557    * 0).
558    * 
559    * @param seq
560    *          an aligned peptide sequence
561    * @param col
562    *          an aligned column position (base 0)
563    * @param mappings
564    *          a set of codon mappings
565    * @return the bases of the mapped codon in the cDNA dataset sequence, or null
566    *         if not found
567    */
568   public static char[] findCodonFor(SequenceI seq, int col,
569           Set<AlignedCodonFrame> mappings)
570   {
571     int dsPos = seq.findPosition(col);
572     for (AlignedCodonFrame mapping : mappings)
573     {
574       if (mapping.involvesSequence(seq))
575       {
576         return mapping.getMappedCodon(seq.getDatasetSequence(), dsPos);
577       }
578     }
579     return null;
580   }
581
582   /**
583    * Converts a series of [start, end] ranges into an array of individual
584    * positions.
585    * 
586    * @param ranges
587    * @return
588    */
589   public static int[] flattenRanges(int[] ranges)
590   {
591     /*
592      * Count how many positions altogether
593      */
594     int count = 0;
595     for (int i = 0; i < ranges.length - 1; i += 2)
596     {
597       count += ranges[i + 1] - ranges[i] + 1;
598     }
599
600     int[] result = new int[count];
601     int k = 0;
602     for (int i = 0; i < ranges.length - 1; i += 2)
603     {
604       for (int j = ranges[i]; j <= ranges[i + 1]; j++)
605       {
606         result[k++] = j;
607       }
608     }
609     return result;
610   }
611 }