Clover icon

Coverage Report

  1. Project Clover database Wed Jan 7 2026 02:49:01 GMT
  2. Package jalview.util

File MappingUtils.java

 

Coverage histogram

../../img/srcFileCovDistChart8.png
20% of files have more coverage

Code metrics

164
321
24
1
1,217
700
135
0.42
13.38
24
5.62

Classes

Class Line # Actions
MappingUtils 58 321 135
0.791748579.2%
 

Contributing tests

This file is covered by 160 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  123 toggle public static void addSearchResults(SearchResultsI results, SequenceI seq,
293    int index, List<AlignedCodonFrame> seqmappings)
294    {
295  123 if (index >= seq.getStart() && index <= seq.getEnd())
296    {
297  123 for (AlignedCodonFrame acf : seqmappings)
298    {
299  123 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 public 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  0 toggle public static SearchResultsI allMappedRegionsForColumn(int col,List<AlignedCodonFrame> mappings, List<SequenceI> fromSequences,
696    List<SequenceI> toSequences, char fromGapChar)
697    {
698  0 int[] mappedTo = new int[] { Integer.MAX_VALUE, Integer.MIN_VALUE };
699   
700  0 SearchResultsI allsr=new SearchResults();
701    /*
702    * For each sequence in the 'from' alignment
703    */
704  0 for (SequenceI fromSeq : fromSequences)
705    {
706    /*
707    * Ignore gaps (unmapped anyway)
708    */
709  0 if (fromSeq.getCharAt(col) == fromGapChar)
710    {
711  0 continue;
712    }
713   
714    /*
715    * Get the residue position and find the mapped position.
716    */
717  0 int residuePos = fromSeq.findPosition(col);
718  0 SearchResultsI sr = buildSearchResults(fromSeq, residuePos, mappings);
719   
720  0 for (SearchResultMatchI m : sr.getResults())
721    {
722  0 int mappedStartResidue = m.getStart();
723  0 int mappedEndResidue = m.getEnd();
724  0 SequenceI mappedSeq = m.getSequence();
725   
726    /*
727    * Locate the aligned sequence whose dataset is mappedSeq. TODO a
728    * datamodel that can do this efficiently.
729    */
730  0 for (SequenceI toSeq : toSequences)
731    {
732  0 if (toSeq.getDatasetSequence() == mappedSeq
733    && mappedStartResidue >= toSeq.getStart()
734    && mappedEndResidue <= toSeq.getEnd())
735    {
736  0 allsr.addResult(toSeq, new int[] { mappedStartResidue,mappedEndResidue});
737    }
738    }
739    }
740    }
741  0 return allsr;
742    }
743   
744    /**
745    * Returns the mapped codon or codons for a given aligned sequence column
746    * position (base 0).
747    *
748    * @param seq
749    * an aligned peptide sequence
750    * @param col
751    * an aligned column position (base 0)
752    * @param mappings
753    * a set of codon mappings
754    * @return the bases of the mapped codon(s) in the cDNA dataset sequence(s),
755    * or an empty list if none found
756    */
 
757  5421 toggle public static List<char[]> findCodonsFor(SequenceI seq, int col,
758    List<AlignedCodonFrame> mappings)
759    {
760  5421 List<char[]> result = new ArrayList<>();
761  5421 int dsPos = seq.findPosition(col);
762  5421 for (AlignedCodonFrame mapping : mappings)
763    {
764  122615 if (mapping.involvesSequence(seq))
765    {
766  10748 List<char[]> codons = mapping
767    .getMappedCodons(seq.getDatasetSequence(), dsPos);
768  10748 if (codons != null)
769    {
770  10658 result.addAll(codons);
771    }
772    }
773    }
774  5421 return result;
775    }
776   
777    /**
778    * Converts a series of [start, end] range pairs into an array of individual
779    * positions. This also caters for 'reverse strand' (start > end) cases.
780    *
781    * @param ranges
782    * @return
783    */
 
784  10687 toggle public static int[] flattenRanges(int[] ranges)
785    {
786    /*
787    * Count how many positions altogether
788    */
789  10687 int count = 0;
790  21397 for (int i = 0; i < ranges.length - 1; i += 2)
791    {
792  10710 count += Math.abs(ranges[i + 1] - ranges[i]) + 1;
793    }
794   
795  10687 int[] result = new int[count];
796  10687 int k = 0;
797  21397 for (int i = 0; i < ranges.length - 1; i += 2)
798    {
799  10710 int from = ranges[i];
800  10710 final int to = ranges[i + 1];
801  10710 int step = from <= to ? 1 : -1;
802  10710 do
803    {
804  32092 result[k++] = from;
805  32092 from += step;
806  32092 } while (from != to + step);
807    }
808  10687 return result;
809    }
810   
811    /**
812    * Returns a list of any mappings that are from or to the given (aligned or
813    * dataset) sequence.
814    *
815    * @param sequence
816    * @param mappings
817    * @return
818    */
 
819  135 toggle public static List<AlignedCodonFrame> findMappingsForSequence(
820    SequenceI sequence, List<AlignedCodonFrame> mappings)
821    {
822  135 return findMappingsForSequenceAndOthers(sequence, mappings, null);
823    }
824   
825    /**
826    * Returns a list of any mappings that are from or to the given (aligned or
827    * dataset) sequence, optionally limited to mappings involving one of a given
828    * list of sequences.
829    *
830    * @param sequence
831    * @param mappings
832    * @param filterList
833    * @return
834    */
 
835  141 toggle public static List<AlignedCodonFrame> findMappingsForSequenceAndOthers(
836    SequenceI sequence, List<AlignedCodonFrame> mappings,
837    List<SequenceI> filterList)
838    {
839  141 List<AlignedCodonFrame> result = new ArrayList<>();
840  141 if (sequence == null || mappings == null)
841    {
842  5 return result;
843    }
844  136 for (AlignedCodonFrame mapping : mappings)
845    {
846  1402 if (mapping.involvesSequence(sequence))
847    {
848  158 if (filterList != null)
849    {
850  7 for (SequenceI otherseq : filterList)
851    {
852  51 SequenceI otherDataset = otherseq.getDatasetSequence();
853  51 if (otherseq == sequence
854    || otherseq == sequence.getDatasetSequence()
855    || (otherDataset != null && (otherDataset == sequence
856    || otherDataset == sequence
857    .getDatasetSequence())))
858    {
859    // skip sequences in subset which directly relate to sequence
860  4 continue;
861    }
862  47 if (mapping.involvesSequence(otherseq))
863    {
864    // selected a mapping contained in subselect alignment
865  4 result.add(mapping);
866  4 break;
867    }
868    }
869    }
870    else
871    {
872  151 result.add(mapping);
873    }
874    }
875    }
876  136 return result;
877    }
878   
879    /**
880    * Returns the total length of the supplied ranges, which may be as single
881    * [start, end] or multiple [start, end, start, end ...]
882    *
883    * @param ranges
884    * @return
885    */
 
886  154 toggle public static int getLength(List<int[]> ranges)
887    {
888  154 if (ranges == null)
889    {
890  1 return 0;
891    }
892  153 int length = 0;
893  153 for (int[] range : ranges)
894    {
895  175 if (range.length % 2 != 0)
896    {
897  0 Console.error(
898    "Error unbalance start/end ranges: " + ranges.toString());
899  0 return 0;
900    }
901  379 for (int i = 0; i < range.length - 1; i += 2)
902    {
903  204 length += Math.abs(range[i + 1] - range[i]) + 1;
904    }
905    }
906  153 return length;
907    }
908   
909    /**
910    * Answers true if any range includes the given value
911    *
912    * @param ranges
913    * @param value
914    * @return
915    */
 
916  22 toggle public static boolean contains(List<int[]> ranges, int value)
917    {
918  22 if (ranges == null)
919    {
920  1 return false;
921    }
922  21 for (int[] range : ranges)
923    {
924  72 if (range[1] >= range[0] && value >= range[0] && value <= range[1])
925    {
926    /*
927    * value within ascending range
928    */
929  8 return true;
930    }
931  64 if (range[1] < range[0] && value <= range[0] && value >= range[1])
932    {
933    /*
934    * value within descending range
935    */
936  5 return true;
937    }
938    }
939  8 return false;
940    }
941   
942    /**
943    * Removes a specified number of positions from the start of a ranges list.
944    * For example, could be used to adjust cds ranges to allow for an incomplete
945    * start codon. Subranges are removed completely, or their start positions
946    * adjusted, until the required number of positions has been removed from the
947    * range. Reverse strand ranges are supported. The input array is not
948    * modified.
949    *
950    * @param removeCount
951    * @param ranges
952    * an array of [start, end, start, end...] positions
953    * @return a new array with the first removeCount positions removed
954    */
 
955  21 toggle public static int[] removeStartPositions(int removeCount,
956    final int[] ranges)
957    {
958  21 if (removeCount <= 0)
959    {
960  5 return ranges;
961    }
962   
963  16 int[] copy = Arrays.copyOf(ranges, ranges.length);
964  16 int sxpos = -1;
965  16 int cdspos = 0;
966  28 for (int x = 0; x < copy.length && sxpos == -1; x += 2)
967    {
968  28 cdspos += Math.abs(copy[x + 1] - copy[x]) + 1;
969  28 if (removeCount < cdspos)
970    {
971    /*
972    * we have removed enough, time to finish
973    */
974  16 sxpos = x;
975   
976    /*
977    * increment start of first exon, or decrement if reverse strand
978    */
979  16 if (copy[x] <= copy[x + 1])
980    {
981  9 copy[x] = copy[x + 1] - cdspos + removeCount + 1;
982    }
983    else
984    {
985  7 copy[x] = copy[x + 1] + cdspos - removeCount - 1;
986    }
987  16 break;
988    }
989    }
990   
991  16 if (sxpos > 0)
992    {
993    /*
994    * we dropped at least one entire sub-range - compact the array
995    */
996  10 int[] nxon = new int[copy.length - sxpos];
997  10 System.arraycopy(copy, sxpos, nxon, 0, copy.length - sxpos);
998  10 return nxon;
999    }
1000  6 return copy;
1001    }
1002   
1003    /**
1004    * Answers true if range's start-end positions include those of queryRange,
1005    * where either range might be in reverse direction, else false
1006    *
1007    * @param range
1008    * a start-end range
1009    * @param queryRange
1010    * a candidate subrange of range (start2-end2)
1011    * @return
1012    */
 
1013  33 toggle public static boolean rangeContains(int[] range, int[] queryRange)
1014    {
1015  33 if (range == null || queryRange == null || range.length != 2
1016    || queryRange.length != 2)
1017    {
1018    /*
1019    * invalid arguments
1020    */
1021  4 return false;
1022    }
1023   
1024  29 int min = Math.min(range[0], range[1]);
1025  29 int max = Math.max(range[0], range[1]);
1026   
1027  29 return (min <= queryRange[0] && max >= queryRange[0]
1028    && min <= queryRange[1] && max >= queryRange[1]);
1029    }
1030   
1031    /**
1032    * Removes the specified number of positions from the given ranges. Provided
1033    * to allow a stop codon to be stripped from a CDS sequence so that it matches
1034    * the peptide translation length.
1035    *
1036    * @param positions
1037    * @param ranges
1038    * a list of (single) [start, end] ranges
1039    * @return
1040    */
 
1041  8 toggle public static void removeEndPositions(int positions, List<int[]> ranges)
1042    {
1043  8 int toRemove = positions;
1044  8 Iterator<int[]> it = new ReverseListIterator<>(ranges);
1045  19 while (toRemove > 0)
1046    {
1047  11 int[] endRange = it.next();
1048  11 if (endRange.length != 2)
1049    {
1050    /*
1051    * not coded for [start1, end1, start2, end2, ...]
1052    */
1053  0 Console.error(
1054    "MappingUtils.removeEndPositions doesn't handle multiple ranges");
1055  0 return;
1056    }
1057   
1058  11 int length = endRange[1] - endRange[0] + 1;
1059  11 if (length <= 0)
1060    {
1061    /*
1062    * not coded for a reverse strand range (end < start)
1063    */
1064  0 Console.error(
1065    "MappingUtils.removeEndPositions doesn't handle reverse strand");
1066  0 return;
1067    }
1068  11 if (length > toRemove)
1069    {
1070  7 endRange[1] -= toRemove;
1071  7 toRemove = 0;
1072    }
1073    else
1074    {
1075  4 toRemove -= length;
1076  4 it.remove();
1077    }
1078    }
1079    }
1080    /**
1081    * Adds the given range to a list of ranges. If the new range just extends
1082    * existing ranges, the current endpoint is updated instead.
1083    *
1084    * @param range
1085    * @param addTo
1086    */
 
1087  29 toggle public static void addRange(int[] range, List<int[]> addTo)
1088    {
1089    /*
1090    * list is empty - add to it!
1091    */
1092  29 if (addTo.size() == 0)
1093    {
1094  1 addTo.add(range);
1095  1 return;
1096    }
1097   
1098  28 int[] last = addTo.get(addTo.size() - 1);
1099  28 boolean lastForward = last[1] >= last[0];
1100  28 boolean newForward = range[1] >= range[0];
1101   
1102    /*
1103    * contiguous range in the same direction - just update endpoint
1104    */
1105  28 if (lastForward == newForward && last[1] == range[0])
1106    {
1107  4 last[1] = range[1];
1108  4 return;
1109    }
1110   
1111    /*
1112    * next range starts at +1 in forward sense - update endpoint
1113    */
1114  24 if (lastForward && newForward && range[0] == last[1] + 1)
1115    {
1116  3 last[1] = range[1];
1117  3 return;
1118    }
1119   
1120    /*
1121    * next range starts at -1 in reverse sense - update endpoint
1122    */
1123  21 if (!lastForward && !newForward && range[0] == last[1] - 1)
1124    {
1125  4 last[1] = range[1];
1126  4 return;
1127    }
1128   
1129    /*
1130    * just add the new range
1131    */
1132  17 addTo.add(range);
1133    }
1134   
1135    /**
1136    * Converts a list of {@code start-end} ranges to a single array of
1137    * {@code start1, end1, start2, ... } ranges
1138    *
1139    * @param ranges
1140    * @return
1141    */
 
1142  299260 toggle public static int[] rangeListToArray(List<int[]> ranges)
1143    {
1144  299260 int rangeCount = ranges.size();
1145  299260 int[] result = new int[rangeCount * 2];
1146  299260 int j = 0;
1147  598714 for (int i = 0; i < rangeCount; i++)
1148    {
1149  299457 int[] range = ranges.get(i);
1150  299457 result[j++] = range[0];
1151  299457 result[j++] = range[1];
1152    }
1153  299257 return result;
1154    }
1155   
1156    /*
1157    * Returns the maximal start-end positions in the given (ordered) list of
1158    * ranges which is overlapped by the given begin-end range, or null if there
1159    * is no overlap.
1160    *
1161    * <pre>
1162    * Examples:
1163    * if ranges is {[4, 8], [10, 12], [16, 19]}
1164    * then
1165    * findOverlap(ranges, 1, 20) == [4, 19]
1166    * findOverlap(ranges, 6, 11) == [6, 11]
1167    * findOverlap(ranges, 9, 15) == [10, 12]
1168    * findOverlap(ranges, 13, 15) == null
1169    * </pre>
1170    *
1171    * @param ranges
1172    * @param begin
1173    * @param end
1174    * @return
1175    */
 
1176  26 toggle protected static int[] findOverlap(List<int[]> ranges, final int begin,
1177    final int end)
1178    {
1179  26 boolean foundStart = false;
1180  26 int from = 0;
1181  26 int to = 0;
1182   
1183    /*
1184    * traverse the ranges to find the first position (if any) >= begin,
1185    * and the last position (if any) <= end
1186    */
1187  26 for (int[] range : ranges)
1188    {
1189  89 if (!foundStart)
1190    {
1191  51 if (range[0] >= begin)
1192    {
1193    /*
1194    * first range that starts with, or follows, begin
1195    */
1196  16 foundStart = true;
1197  16 from = Math.max(range[0], begin);
1198    }
1199  35 else if (range[1] >= begin)
1200    {
1201    /*
1202    * first range that contains begin
1203    */
1204  9 foundStart = true;
1205  9 from = begin;
1206    }
1207    }
1208   
1209  89 if (range[0] <= end)
1210    {
1211  54 to = Math.min(end, range[1]);
1212    }
1213    }
1214   
1215  26 return foundStart && to >= from ? new int[] { from, to } : null;
1216    }
1217    }