JAL-3806 todo - problem seems to be Protein:Many CDS are not selected
[jalview.git] / src / jalview / util / MappingUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.util;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Map;
29
30 import jalview.analysis.AlignmentSorter;
31 import jalview.api.AlignViewportI;
32 import jalview.bin.Cache;
33 import jalview.commands.CommandI;
34 import jalview.commands.EditCommand;
35 import jalview.commands.EditCommand.Action;
36 import jalview.commands.EditCommand.Edit;
37 import jalview.commands.OrderCommand;
38 import jalview.datamodel.AlignedCodonFrame;
39 import jalview.datamodel.AlignmentI;
40 import jalview.datamodel.AlignmentOrder;
41 import jalview.datamodel.ColumnSelection;
42 import jalview.datamodel.HiddenColumns;
43 import jalview.datamodel.SearchResultMatchI;
44 import jalview.datamodel.SearchResults;
45 import jalview.datamodel.SearchResultsI;
46 import jalview.datamodel.Sequence;
47 import jalview.datamodel.SequenceGroup;
48 import jalview.datamodel.SequenceI;
49
50 /**
51  * Helper methods for manipulations involving sequence mappings.
52  * 
53  * @author gmcarstairs
54  *
55  */
56 public final class MappingUtils
57 {
58
59   /**
60    * Helper method to map a CUT or PASTE command.
61    * 
62    * @param edit
63    *          the original command
64    * @param undo
65    *          if true, the command is to be undone
66    * @param targetSeqs
67    *          the mapped sequences to apply the mapped command to
68    * @param result
69    *          the mapped EditCommand to add to
70    * @param mappings
71    */
72   protected static void mapCutOrPaste(Edit edit, boolean undo,
73           List<SequenceI> targetSeqs, EditCommand result,
74           List<AlignedCodonFrame> mappings)
75   {
76     Action action = edit.getAction();
77     if (undo)
78     {
79       action = action.getUndoAction();
80     }
81     // TODO write this
82     Cache.log.error("MappingUtils.mapCutOrPaste not yet implemented");
83   }
84
85   /**
86    * Returns a new EditCommand representing the given command as mapped to the
87    * given sequences. If there is no mapping, returns null.
88    * 
89    * @param command
90    * @param undo
91    * @param mapTo
92    * @param gapChar
93    * @param mappings
94    * @return
95    */
96   public static EditCommand mapEditCommand(EditCommand command,
97           boolean undo, final AlignmentI mapTo, char gapChar,
98           List<AlignedCodonFrame> mappings)
99   {
100     /*
101      * For now, only support mapping from protein edits to cDna
102      */
103     if (!mapTo.isNucleotide())
104     {
105       return null;
106     }
107
108     /*
109      * Cache a copy of the target sequences so we can mimic successive edits on
110      * them. This lets us compute mappings for all edits in the set.
111      */
112     Map<SequenceI, SequenceI> targetCopies = new HashMap<>();
113     for (SequenceI seq : mapTo.getSequences())
114     {
115       SequenceI ds = seq.getDatasetSequence();
116       if (ds != null)
117       {
118         final SequenceI copy = new Sequence(seq);
119         copy.setDatasetSequence(ds);
120         targetCopies.put(ds, copy);
121       }
122     }
123
124     /*
125      * Compute 'source' sequences as they were before applying edits:
126      */
127     Map<SequenceI, SequenceI> originalSequences = command.priorState(undo);
128
129     EditCommand result = new EditCommand();
130     Iterator<Edit> edits = command.getEditIterator(!undo);
131     while (edits.hasNext())
132     {
133       Edit edit = edits.next();
134       if (edit.getAction() == Action.CUT
135               || edit.getAction() == Action.PASTE)
136       {
137         mapCutOrPaste(edit, undo, mapTo.getSequences(), result, mappings);
138       }
139       else if (edit.getAction() == Action.INSERT_GAP
140               || edit.getAction() == Action.DELETE_GAP)
141       {
142         mapInsertOrDelete(edit, undo, originalSequences,
143                 mapTo.getSequences(), targetCopies, gapChar, result,
144                 mappings);
145       }
146     }
147     return result.getSize() > 0 ? result : null;
148   }
149
150   /**
151    * Helper method to map an edit command to insert or delete gaps.
152    * 
153    * @param edit
154    *          the original command
155    * @param undo
156    *          if true, the action is to undo the command
157    * @param originalSequences
158    *          the sequences the command acted on
159    * @param targetSeqs
160    * @param targetCopies
161    * @param gapChar
162    * @param result
163    *          the new EditCommand to add mapped commands to
164    * @param mappings
165    */
166   protected static void mapInsertOrDelete(Edit edit, boolean undo,
167           Map<SequenceI, SequenceI> originalSequences,
168           final List<SequenceI> targetSeqs,
169           Map<SequenceI, SequenceI> targetCopies, char gapChar,
170           EditCommand result, List<AlignedCodonFrame> mappings)
171   {
172     Action action = edit.getAction();
173
174     /*
175      * Invert sense of action if an Undo.
176      */
177     if (undo)
178     {
179       action = action.getUndoAction();
180     }
181     final int count = edit.getNumber();
182     final int editPos = edit.getPosition();
183     for (SequenceI seq : edit.getSequences())
184     {
185       /*
186        * Get residue position at (or to right of) edit location. Note we use our
187        * 'copy' of the sequence before editing for this.
188        */
189       SequenceI ds = seq.getDatasetSequence();
190       if (ds == null)
191       {
192         continue;
193       }
194       final SequenceI actedOn = originalSequences.get(ds);
195       final int seqpos = actedOn.findPosition(editPos);
196
197       /*
198        * Determine all mappings from this position to mapped sequences.
199        */
200       SearchResultsI sr = buildSearchResults(seq, seqpos, mappings);
201
202       if (!sr.isEmpty())
203       {
204         for (SequenceI targetSeq : targetSeqs)
205         {
206           ds = targetSeq.getDatasetSequence();
207           if (ds == null)
208           {
209             continue;
210           }
211           SequenceI copyTarget = targetCopies.get(ds);
212           final int[] match = sr.getResults(copyTarget, 0,
213                   copyTarget.getLength());
214           if (match != null)
215           {
216             final int ratio = 3; // TODO: compute this - how?
217             final int mappedCount = count * ratio;
218
219             /*
220              * Shift Delete start position left, as it acts on positions to its
221              * right.
222              */
223             int mappedEditPos = action == Action.DELETE_GAP
224                     ? match[0] - mappedCount
225                     : match[0];
226             Edit e = result.new Edit(action, new SequenceI[] { targetSeq },
227                     mappedEditPos, mappedCount, gapChar);
228             result.addEdit(e);
229
230             /*
231              * and 'apply' the edit to our copy of its target sequence
232              */
233             if (action == Action.INSERT_GAP)
234             {
235               copyTarget.setSequence(new String(
236                       StringUtils.insertCharAt(copyTarget.getSequence(),
237                               mappedEditPos, mappedCount, gapChar)));
238             }
239             else if (action == Action.DELETE_GAP)
240             {
241               copyTarget.setSequence(new String(
242                       StringUtils.deleteChars(copyTarget.getSequence(),
243                               mappedEditPos, mappedEditPos + mappedCount)));
244             }
245           }
246         }
247       }
248       /*
249        * and 'apply' the edit to our copy of its source sequence
250        */
251       if (action == Action.INSERT_GAP)
252       {
253         actedOn.setSequence(new String(StringUtils.insertCharAt(
254                 actedOn.getSequence(), editPos, count, gapChar)));
255       }
256       else if (action == Action.DELETE_GAP)
257       {
258         actedOn.setSequence(new String(StringUtils.deleteChars(
259                 actedOn.getSequence(), editPos, editPos + count)));
260       }
261     }
262   }
263
264   /**
265    * Returns a SearchResults object describing the mapped region corresponding
266    * to the specified sequence position.
267    * 
268    * @param seq
269    * @param index
270    * @param seqmappings
271    * @return
272    */
273   public static SearchResultsI buildSearchResults(SequenceI seq, int index,
274           List<AlignedCodonFrame> seqmappings)
275   {
276     SearchResultsI results = new SearchResults();
277     addSearchResults(results, seq, index, seqmappings);
278     return results;
279   }
280
281   /**
282    * Adds entries to a SearchResults object describing the mapped region
283    * corresponding to the specified sequence position.
284    * 
285    * @param results
286    * @param seq
287    * @param index
288    * @param seqmappings
289    */
290   public static void addSearchResults(SearchResultsI results, SequenceI seq,
291           int index, List<AlignedCodonFrame> seqmappings)
292   {
293     if (index >= seq.getStart() && index <= seq.getEnd())
294     {
295       for (AlignedCodonFrame acf : seqmappings)
296       {
297         acf.markMappedRegion(seq, index, results);
298       }
299     }
300   }
301
302   /**
303    * Returns a (possibly empty) SequenceGroup containing any sequences in the
304    * mapped viewport corresponding to the given group in the source viewport.
305    * 
306    * @param sg
307    * @param mapFrom
308    * @param mapTo
309    * @return
310    */
311   public static SequenceGroup mapSequenceGroup(final SequenceGroup sg,
312           final AlignViewportI mapFrom, final AlignViewportI mapTo)
313   {
314     /*
315      * Note the SequenceGroup holds aligned sequences, the mappings hold dataset
316      * sequences.
317      */
318     boolean targetIsNucleotide = mapTo.isNucleotide();
319     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
320     List<AlignedCodonFrame> codonFrames = protein.getAlignment()
321             .getCodonFrames();
322     /*
323      * Copy group name, colours etc, but not sequences or sequence colour scheme
324      */
325     SequenceGroup mappedGroup = new SequenceGroup(sg);
326     mappedGroup.setColourScheme(mapTo.getGlobalColourScheme());
327     mappedGroup.clear();
328
329     int minStartCol = -1;
330     int maxEndCol = -1;
331     final int selectionStartRes = sg.getStartRes();
332     final int selectionEndRes = sg.getEndRes();
333     for (SequenceI selected : sg.getSequences())
334     {
335       /*
336        * Find the widest range of non-gapped positions in the selection range
337        */
338       int firstUngappedPos = selectionStartRes;
339       while (firstUngappedPos <= selectionEndRes
340               && Comparison.isGap(selected.getCharAt(firstUngappedPos)))
341       {
342         firstUngappedPos++;
343       }
344
345       /*
346        * If this sequence is only gaps in the selected range, skip it
347        */
348       if (firstUngappedPos > selectionEndRes)
349       {
350         continue;
351       }
352
353       int lastUngappedPos = selectionEndRes;
354       while (lastUngappedPos >= selectionStartRes
355               && Comparison.isGap(selected.getCharAt(lastUngappedPos)))
356       {
357         lastUngappedPos--;
358       }
359
360       /*
361        * Find the selected start/end residue positions in sequence
362        */
363       int startResiduePos = selected.findPosition(firstUngappedPos);
364       int endResiduePos = selected.findPosition(lastUngappedPos);
365
366       for (AlignedCodonFrame acf : codonFrames)
367       {
368         // TODO: handle multiple mappings
369         SequenceI mappedSequence = targetIsNucleotide
370                 ? acf.getDnaForAaSeq(selected)
371                 : acf.getAaForDnaSeq(selected);
372         if (mappedSequence != null)
373         {
374           for (SequenceI seq : mapTo.getAlignment().getSequences())
375           {
376             int mappedStartResidue = 0;
377             int mappedEndResidue = 0;
378             if (seq.getDatasetSequence() == mappedSequence)
379             {
380               /*
381                * Found a sequence mapping. Locate the start/end mapped residues.
382                */
383               List<AlignedCodonFrame> mapping = Arrays
384                       .asList(new AlignedCodonFrame[]
385                       { acf });
386               SearchResultsI sr = buildSearchResults(selected,
387                       startResiduePos, mapping);
388               for (SearchResultMatchI m : sr.getResults())
389               {
390                 mappedStartResidue = m.getStart();
391                 mappedEndResidue = m.getEnd();
392               }
393               sr = buildSearchResults(selected, endResiduePos, mapping);
394               for (SearchResultMatchI m : sr.getResults())
395               {
396                 mappedStartResidue = Math.min(mappedStartResidue,
397                         m.getStart());
398                 mappedEndResidue = Math.max(mappedEndResidue, m.getEnd());
399               }
400
401               /*
402                * Find the mapped aligned columns, save the range. Note findIndex
403                * returns a base 1 position, SequenceGroup uses base 0
404                */
405               int mappedStartCol = seq.findIndex(mappedStartResidue) - 1;
406               minStartCol = minStartCol == -1 ? mappedStartCol
407                       : Math.min(minStartCol, mappedStartCol);
408               int mappedEndCol = seq.findIndex(mappedEndResidue) - 1;
409               maxEndCol = maxEndCol == -1 ? mappedEndCol
410                       : Math.max(maxEndCol, mappedEndCol);
411               mappedGroup.addSequence(seq, false);
412               break;
413             }
414           }
415         }
416       }
417     }
418     mappedGroup.setStartRes(minStartCol < 0 ? 0 : minStartCol);
419     mappedGroup.setEndRes(maxEndCol < 0 ? 0 : maxEndCol);
420     return mappedGroup;
421   }
422
423   /**
424    * Returns an OrderCommand equivalent to the given one, but acting on mapped
425    * sequences as described by the mappings, or null if no mapping can be made.
426    * 
427    * @param command
428    *          the original order command
429    * @param undo
430    *          if true, the action is to undo the sort
431    * @param mapTo
432    *          the alignment we are mapping to
433    * @param mappings
434    *          the mappings available
435    * @return
436    */
437   public static CommandI mapOrderCommand(OrderCommand command, boolean undo,
438           AlignmentI mapTo, List<AlignedCodonFrame> mappings)
439   {
440     SequenceI[] sortOrder = command.getSequenceOrder(undo);
441     List<SequenceI> mappedOrder = new ArrayList<>();
442     int j = 0;
443
444     /*
445      * Assumption: we are only interested in a cDNA/protein mapping; refactor in
446      * future if we want to support sorting (c)dna as (c)dna or protein as
447      * protein
448      */
449     boolean mappingToNucleotide = mapTo.isNucleotide();
450     for (SequenceI seq : sortOrder)
451     {
452       for (AlignedCodonFrame acf : mappings)
453       {
454         SequenceI mappedSeq = mappingToNucleotide ? acf.getDnaForAaSeq(seq)
455                 : acf.getAaForDnaSeq(seq);
456         if (mappedSeq != null)
457         {
458           for (SequenceI seq2 : mapTo.getSequences())
459           {
460             if (seq2.getDatasetSequence() == mappedSeq)
461             {
462               mappedOrder.add(seq2);
463               j++;
464               break;
465             }
466           }
467         }
468       }
469     }
470
471     /*
472      * Return null if no mappings made.
473      */
474     if (j == 0)
475     {
476       return null;
477     }
478
479     /*
480      * Add any unmapped sequences on the end of the sort in their original
481      * ordering.
482      */
483     if (j < mapTo.getHeight())
484     {
485       for (SequenceI seq : mapTo.getSequences())
486       {
487         if (!mappedOrder.contains(seq))
488         {
489           mappedOrder.add(seq);
490         }
491       }
492     }
493
494     /*
495      * Have to sort the sequences before constructing the OrderCommand - which
496      * then resorts them?!?
497      */
498     final SequenceI[] mappedOrderArray = mappedOrder
499             .toArray(new SequenceI[mappedOrder.size()]);
500     SequenceI[] oldOrder = mapTo.getSequencesArray();
501     AlignmentSorter.sortBy(mapTo, new AlignmentOrder(mappedOrderArray));
502     final OrderCommand result = new OrderCommand(command.getDescription(),
503             oldOrder, mapTo);
504     return result;
505   }
506
507   /**
508    * Returns a ColumnSelection in the 'mapTo' view which corresponds to the
509    * given selection in the 'mapFrom' view. We assume one is nucleotide, the
510    * other is protein (and holds the mappings from codons to protein residues).
511    * 
512    * @param colsel
513    * @param mapFrom
514    * @param mapTo
515    * @return
516    */
517   public static void mapColumnSelection(ColumnSelection colsel,
518           HiddenColumns hiddencols, AlignViewportI mapFrom,
519           AlignViewportI mapTo, ColumnSelection newColSel,
520           HiddenColumns newHidden)
521   {
522     boolean targetIsNucleotide = mapTo.isNucleotide();
523     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
524     List<AlignedCodonFrame> codonFrames = protein.getAlignment()
525             .getCodonFrames();
526
527     if (colsel == null)
528     {
529       return; // mappedColumns;
530     }
531
532     char fromGapChar = mapFrom.getAlignment().getGapCharacter();
533
534     /*
535      * For each mapped column, find the range of columns that residues in that
536      * column map to.
537      */
538     List<SequenceI> fromSequences = mapFrom.getAlignment().getSequences();
539     List<SequenceI> toSequences = mapTo.getAlignment().getSequences();
540
541     for (Integer sel : colsel.getSelected())
542     {
543       mapColumn(sel.intValue(), codonFrames, newColSel, fromSequences,
544               toSequences, fromGapChar);
545     }
546
547     Iterator<int[]> regions = hiddencols.iterator();
548     while (regions.hasNext())
549     {
550       mapHiddenColumns(regions.next(), codonFrames, newHidden,
551               fromSequences, toSequences, fromGapChar);
552     }
553     return; // mappedColumns;
554   }
555
556   /**
557    * Helper method that maps a [start, end] hidden column range to its mapped
558    * equivalent
559    * 
560    * @param hidden
561    * @param mappings
562    * @param mappedColumns
563    * @param fromSequences
564    * @param toSequences
565    * @param fromGapChar
566    */
567   protected static void mapHiddenColumns(int[] hidden,
568           List<AlignedCodonFrame> mappings, HiddenColumns mappedColumns,
569           List<SequenceI> fromSequences, List<SequenceI> toSequences,
570           char fromGapChar)
571   {
572     for (int col = hidden[0]; col <= hidden[1]; col++)
573     {
574       int[] mappedTo = findMappedColumns(col, mappings, fromSequences,
575               toSequences, fromGapChar);
576
577       /*
578        * Add the range of hidden columns to the mapped selection (converting
579        * base 1 to base 0).
580        */
581       if (mappedTo != null)
582       {
583         mappedColumns.hideColumns(mappedTo[0] - 1, mappedTo[1] - 1);
584       }
585     }
586   }
587
588   /**
589    * Helper method to map one column selection
590    * 
591    * @param col
592    *          the column number (base 0)
593    * @param mappings
594    *          the sequence mappings
595    * @param mappedColumns
596    *          the mapped column selections to add to
597    * @param fromSequences
598    * @param toSequences
599    * @param fromGapChar
600    */
601   protected static void mapColumn(int col, List<AlignedCodonFrame> mappings,
602           ColumnSelection mappedColumns, List<SequenceI> fromSequences,
603           List<SequenceI> toSequences, char fromGapChar)
604   {
605     int[] mappedTo = findMappedColumns(col, mappings, fromSequences,
606             toSequences, fromGapChar);
607
608     /*
609      * Add the range of mapped columns to the mapped selection (converting
610      * base 1 to base 0). Note that this may include intron-only regions which
611      * lie between the start and end ranges of the selection.
612      */
613     if (mappedTo != null)
614     {
615       for (int i = mappedTo[0]; i <= mappedTo[1]; i++)
616       {
617         mappedColumns.addElement(i - 1);
618       }
619     }
620   }
621
622   /**
623    * Helper method to find the range of columns mapped to from one column.
624    * Returns the maximal range of columns mapped to from all sequences in the
625    * source column, or null if no mappings were found.
626    * 
627    * @param col
628    * @param mappings
629    * @param fromSequences
630    * @param toSequences
631    * @param fromGapChar
632    * @return
633    */
634   protected static int[] findMappedColumns(int col,
635           List<AlignedCodonFrame> mappings, List<SequenceI> fromSequences,
636           List<SequenceI> toSequences, char fromGapChar)
637   {
638     int[] mappedTo = new int[] { Integer.MAX_VALUE, Integer.MIN_VALUE };
639     boolean found = false;
640
641     /*
642      * For each sequence in the 'from' alignment
643      */
644     for (SequenceI fromSeq : fromSequences)
645     {
646       /*
647        * Ignore gaps (unmapped anyway)
648        */
649       if (fromSeq.getCharAt(col) == fromGapChar)
650       {
651         continue;
652       }
653
654       /*
655        * Get the residue position and find the mapped position.
656        */
657       int residuePos = fromSeq.findPosition(col);
658       SearchResultsI sr = buildSearchResults(fromSeq, residuePos, mappings);
659       for (SearchResultMatchI m : sr.getResults())
660       {
661         int mappedStartResidue = m.getStart();
662         int mappedEndResidue = m.getEnd();
663         SequenceI mappedSeq = m.getSequence();
664
665         /*
666          * Locate the aligned sequence whose dataset is mappedSeq. TODO a
667          * datamodel that can do this efficiently.
668          */
669         for (SequenceI toSeq : toSequences)
670         {
671           if (toSeq.getDatasetSequence() == mappedSeq)
672           {
673             int mappedStartCol = toSeq.findIndex(mappedStartResidue);
674             int mappedEndCol = toSeq.findIndex(mappedEndResidue);
675             mappedTo[0] = Math.min(mappedTo[0], mappedStartCol);
676             mappedTo[1] = Math.max(mappedTo[1], mappedEndCol);
677             found = true;
678             break;
679             // note: remove break if we ever want to map one to many sequences
680           }
681         }
682       }
683     }
684     return found ? mappedTo : null;
685   }
686
687   /**
688    * Returns the mapped codon or codons for a given aligned sequence column
689    * position (base 0).
690    * 
691    * @param seq
692    *          an aligned peptide sequence
693    * @param col
694    *          an aligned column position (base 0)
695    * @param mappings
696    *          a set of codon mappings
697    * @return the bases of the mapped codon(s) in the cDNA dataset sequence(s),
698    *         or an empty list if none found
699    */
700   public static List<char[]> findCodonsFor(SequenceI seq, int col,
701           List<AlignedCodonFrame> mappings)
702   {
703     List<char[]> result = new ArrayList<>();
704     int dsPos = seq.findPosition(col);
705     for (AlignedCodonFrame mapping : mappings)
706     {
707       if (mapping.involvesSequence(seq))
708       {
709         List<char[]> codons = mapping
710                 .getMappedCodons(seq.getDatasetSequence(), dsPos);
711         if (codons != null)
712         {
713           result.addAll(codons);
714         }
715       }
716     }
717     return result;
718   }
719
720   /**
721    * Converts a series of [start, end] range pairs into an array of individual
722    * positions. This also caters for 'reverse strand' (start > end) cases.
723    * 
724    * @param ranges
725    * @return
726    */
727   public static int[] flattenRanges(int[] ranges)
728   {
729     /*
730      * Count how many positions altogether
731      */
732     int count = 0;
733     for (int i = 0; i < ranges.length - 1; i += 2)
734     {
735       count += Math.abs(ranges[i + 1] - ranges[i]) + 1;
736     }
737
738     int[] result = new int[count];
739     int k = 0;
740     for (int i = 0; i < ranges.length - 1; i += 2)
741     {
742       int from = ranges[i];
743       final int to = ranges[i + 1];
744       int step = from <= to ? 1 : -1;
745       do
746       {
747         result[k++] = from;
748         from += step;
749       } while (from != to + step);
750     }
751     return result;
752   }
753
754   /**
755    * Returns a list of any mappings that are from or to the given (aligned or
756    * dataset) sequence.
757    * 
758    * @param sequence
759    * @param mappings
760    * @return
761    */
762   public static List<AlignedCodonFrame> findMappingsForSequence(
763           SequenceI sequence, List<AlignedCodonFrame> mappings)
764   {
765     return findMappingsForSequenceAndOthers(sequence, mappings, null);
766   }
767
768   /**
769    * Returns a list of any mappings that are from or to the given (aligned or
770    * dataset) sequence, optionally limited to mappings involving one of a given
771    * list of sequences.
772    * 
773    * @param sequence
774    * @param mappings
775    * @param filterList
776    * @return
777    */
778   public static List<AlignedCodonFrame> findMappingsForSequenceAndOthers(
779           SequenceI sequence, List<AlignedCodonFrame> mappings,
780           List<SequenceI> filterList)
781   {
782     List<AlignedCodonFrame> result = new ArrayList<>();
783     if (sequence == null || mappings == null)
784     {
785       return result;
786     }
787     for (AlignedCodonFrame mapping : mappings)
788     {
789       if (mapping.involvesSequence(sequence))
790       {
791         if (filterList != null)
792         {
793           for (SequenceI otherseq : filterList)
794           {
795             SequenceI otherDataset = otherseq.getDatasetSequence();
796             if (otherseq == sequence
797                     || otherseq == sequence.getDatasetSequence()
798                     || (otherDataset != null && (otherDataset == sequence
799                             || otherDataset == sequence
800                                     .getDatasetSequence())))
801             {
802               // skip sequences in subset which directly relate to sequence
803               continue;
804             }
805             if (mapping.involvesSequence(otherseq))
806             {
807               // selected a mapping contained in subselect alignment
808               result.add(mapping);
809               break;
810             }
811           }
812         }
813         else
814         {
815           result.add(mapping);
816         }
817       }
818     }
819     return result;
820   }
821
822   /**
823    * Returns the total length of the supplied ranges, which may be as single
824    * [start, end] or multiple [start, end, start, end ...]
825    * 
826    * @param ranges
827    * @return
828    */
829   public static int getLength(List<int[]> ranges)
830   {
831     if (ranges == null)
832     {
833       return 0;
834     }
835     int length = 0;
836     for (int[] range : ranges)
837     {
838       if (range.length % 2 != 0)
839       {
840         Cache.log.error(
841                 "Error unbalance start/end ranges: " + ranges.toString());
842         return 0;
843       }
844       for (int i = 0; i < range.length - 1; i += 2)
845       {
846         length += Math.abs(range[i + 1] - range[i]) + 1;
847       }
848     }
849     return length;
850   }
851
852   /**
853    * Answers true if any range includes the given value
854    * 
855    * @param ranges
856    * @param value
857    * @return
858    */
859   public static boolean contains(List<int[]> ranges, int value)
860   {
861     if (ranges == null)
862     {
863       return false;
864     }
865     for (int[] range : ranges)
866     {
867       if (range[1] >= range[0] && value >= range[0] && value <= range[1])
868       {
869         /*
870          * value within ascending range
871          */
872         return true;
873       }
874       if (range[1] < range[0] && value <= range[0] && value >= range[1])
875       {
876         /*
877          * value within descending range
878          */
879         return true;
880       }
881     }
882     return false;
883   }
884
885   /**
886    * Removes a specified number of positions from the start of a ranges list.
887    * For example, could be used to adjust cds ranges to allow for an incomplete
888    * start codon. Subranges are removed completely, or their start positions
889    * adjusted, until the required number of positions has been removed from the
890    * range. Reverse strand ranges are supported. The input array is not
891    * modified.
892    * 
893    * @param removeCount
894    * @param ranges
895    *          an array of [start, end, start, end...] positions
896    * @return a new array with the first removeCount positions removed
897    */
898   public static int[] removeStartPositions(int removeCount,
899           final int[] ranges)
900   {
901     if (removeCount <= 0)
902     {
903       return ranges;
904     }
905
906     int[] copy = Arrays.copyOf(ranges, ranges.length);
907     int sxpos = -1;
908     int cdspos = 0;
909     for (int x = 0; x < copy.length && sxpos == -1; x += 2)
910     {
911       cdspos += Math.abs(copy[x + 1] - copy[x]) + 1;
912       if (removeCount < cdspos)
913       {
914         /*
915          * we have removed enough, time to finish
916          */
917         sxpos = x;
918
919         /*
920          * increment start of first exon, or decrement if reverse strand
921          */
922         if (copy[x] <= copy[x + 1])
923         {
924           copy[x] = copy[x + 1] - cdspos + removeCount + 1;
925         }
926         else
927         {
928           copy[x] = copy[x + 1] + cdspos - removeCount - 1;
929         }
930         break;
931       }
932     }
933
934     if (sxpos > 0)
935     {
936       /*
937        * we dropped at least one entire sub-range - compact the array
938        */
939       int[] nxon = new int[copy.length - sxpos];
940       System.arraycopy(copy, sxpos, nxon, 0, copy.length - sxpos);
941       return nxon;
942     }
943     return copy;
944   }
945
946   /**
947    * Answers true if range's start-end positions include those of queryRange,
948    * where either range might be in reverse direction, else false
949    * 
950    * @param range
951    *          a start-end range
952    * @param queryRange
953    *          a candidate subrange of range (start2-end2)
954    * @return
955    */
956   public static boolean rangeContains(int[] range, int[] queryRange)
957   {
958     if (range == null || queryRange == null || range.length != 2
959             || queryRange.length != 2)
960     {
961       /*
962        * invalid arguments
963        */
964       return false;
965     }
966
967     int min = Math.min(range[0], range[1]);
968     int max = Math.max(range[0], range[1]);
969
970     return (min <= queryRange[0] && max >= queryRange[0]
971             && min <= queryRange[1] && max >= queryRange[1]);
972   }
973
974   /**
975    * Removes the specified number of positions from the given ranges. Provided
976    * to allow a stop codon to be stripped from a CDS sequence so that it matches
977    * the peptide translation length.
978    * 
979    * @param positions
980    * @param ranges
981    *          a list of (single) [start, end] ranges
982    * @return
983    */
984   public static void removeEndPositions(int positions, List<int[]> ranges)
985   {
986     int toRemove = positions;
987     Iterator<int[]> it = new ReverseListIterator<>(ranges);
988     while (toRemove > 0)
989     {
990       int[] endRange = it.next();
991       if (endRange.length != 2)
992       {
993         /*
994          * not coded for [start1, end1, start2, end2, ...]
995          */
996         Cache.log.error(
997                 "MappingUtils.removeEndPositions doesn't handle multiple  ranges");
998         return;
999       }
1000
1001       int length = endRange[1] - endRange[0] + 1;
1002       if (length <= 0)
1003       {
1004         /*
1005          * not coded for a reverse strand range (end < start)
1006          */
1007         Cache.log.error(
1008                 "MappingUtils.removeEndPositions doesn't handle reverse strand");
1009         return;
1010       }
1011       if (length > toRemove)
1012       {
1013         endRange[1] -= toRemove;
1014         toRemove = 0;
1015       }
1016       else
1017       {
1018         toRemove -= length;
1019         it.remove();
1020       }
1021     }
1022   }
1023
1024   /**
1025    * Converts a list of {@code start-end} ranges to a single array of
1026    * {@code start1, end1, start2, ... } ranges
1027    * 
1028    * @param ranges
1029    * @return
1030    */
1031   public static int[] rangeListToArray(List<int[]> ranges)
1032   {
1033     int rangeCount = ranges.size();
1034     int[] result = new int[rangeCount * 2];
1035     int j = 0;
1036     for (int i = 0; i < rangeCount; i++)
1037     {
1038       int[] range = ranges.get(i);
1039       result[j++] = range[0];
1040       result[j++] = range[1];
1041     }
1042     return result;
1043   }
1044
1045   /*
1046    * Returns the maximal start-end positions in the given (ordered) list of
1047    * ranges which is overlapped by the given begin-end range, or null if there
1048    * is no overlap.
1049    * 
1050    * <pre>
1051    * Examples:
1052    *   if ranges is {[4, 8], [10, 12], [16, 19]}
1053    * then
1054    *   findOverlap(ranges, 1, 20) == [4, 19]
1055    *   findOverlap(ranges, 6, 11) == [6, 11]
1056    *   findOverlap(ranges, 9, 15) == [10, 12]
1057    *   findOverlap(ranges, 13, 15) == null
1058    * </pre>
1059    * 
1060    * @param ranges
1061    * @param begin
1062    * @param end
1063    * @return
1064    */
1065   protected static int[] findOverlap(List<int[]> ranges, final int begin,
1066           final int end)
1067   {
1068     boolean foundStart = false;
1069     int from = 0;
1070     int to = 0;
1071
1072     /*
1073      * traverse the ranges to find the first position (if any) >= begin,
1074      * and the last position (if any) <= end
1075      */
1076     for (int[] range : ranges)
1077     {
1078       if (!foundStart)
1079       {
1080         if (range[0] >= begin)
1081         {
1082           /*
1083            * first range that starts with, or follows, begin
1084            */
1085           foundStart = true;
1086           from = Math.max(range[0], begin);
1087         }
1088         else if (range[1] >= begin)
1089         {
1090           /*
1091            * first range that contains begin
1092            */
1093           foundStart = true;
1094           from = begin;
1095         }
1096       }
1097
1098       if (range[0] <= end)
1099       {
1100         to = Math.min(end, range[1]);
1101       }
1102     }
1103
1104     return foundStart && to >= from ? new int[] { from, to } : null;
1105   }
1106 }