Clover icon

Coverage Report

  1. Project Clover database Thu Dec 4 2025 16:11:35 GMT
  2. Package jalview.util

File MappingUtils.java

 

Coverage histogram

../../img/srcFileCovDistChart9.png
13% of files have more coverage

Code metrics

160
306
23
1
1,168
669
130
0.42
13.3
23
5.65

Classes

Class Line # Actions
MappingUtils 58 306 130
0.824130982.4%
 

Contributing tests

This file is covered by 156 tests. .

Source view

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