+ /**
+ * Answers true if the mapping is from one contiguous range to another, else
+ * false
+ *
+ * @return
+ */
+ public boolean isContiguous()
+ {
+ return fromShifts.size() == 1 && toShifts.size() == 1;
+ }
+
+ /**
+ * Returns the [start, end, start, end, ...] ranges in the 'from' range that
+ * map to positions between {@code start} and {@code end} in the 'to' range.
+ * Returns null if no mapped positions are found in start-end.
+ *
+ * @param start
+ * @param end
+ * @return
+ */
+ public int[] locateInFrom2(int start, int end)
+ {
+ List<int[]> ranges = mapBetween(start, end, toShifts, fromShifts,
+ toRatio, fromRatio);
+
+ // TODO: or just return the List and adjust calling code to match
+ return ranges.isEmpty() ? null : MappingUtils.rangeListToArray(ranges);
+ }
+
+ /**
+ * Returns the [start, end, start, end, ...] ranges in the 'to' range that map
+ * to the given start-end in the 'from' range. Returns null if either
+ * {@code start} or {@code end} is not a mapped 'from' range position.
+ *
+ * @param start
+ * @param end
+ * @return
+ */
+ public int[] locateInTo2(int start, int end)
+ {
+ List<int[]> ranges = mapBetween(start, end, fromShifts, toShifts,
+ fromRatio, toRatio);
+
+ return ranges.isEmpty() ? null : MappingUtils.rangeListToArray(ranges);
+ }
+
+ /**
+ * A helper method for navigating the mapping. Returns a (possibly empty) list
+ * of [start-end] positions in {@code ranges2} that map to positions in
+ * {@code ranges1} between {@code start} and {@code end}.
+ *
+ * @param start
+ * @param end
+ * @param ranges1
+ * @param ranges2
+ * @param wordLength1
+ * @param wordLength2
+ * @return
+ */
+ final static List<int[]> mapBetween(int start, int end,
+ List<int[]> ranges1, List<int[]> ranges2, int wordLength1,
+ int wordLength2)
+ {
+ if (end < start)
+ {
+ int tmp = end;
+ end = start;
+ start = tmp;
+ }
+
+ /*
+ * first traverse ranges1 and record count of mapped positions
+ * to any that overlap start-end
+ */
+ List<int[]> overlaps = findOverlapPositions(ranges1, start, end);
+ if (overlaps.isEmpty())
+ {
+ return overlaps;
+ }
+
+ /*
+ * convert positions to equivalent 'word' positions in ranges2
+ */
+ mapWords(overlaps, wordLength1, wordLength2);
+
+ /*
+ * walk ranges2 and record the values found at
+ * the offsets in 'overlaps'
+ */
+ List<int[]> mapped = new ArrayList<>();
+ final int s1 = overlaps.size();
+ final int s2 = ranges2.size();
+ int ranges2Index = 0;
+
+ /*
+ * count of mapped positions preceding ranges2[ranges2Index]
+ */
+ int traversed = 0;
+
+ /*
+ * for each [from-to] range in overlaps:
+ * - walk (what remains of) ranges2
+ * - record the values at offsets [from-to]
+ * - stop when past 'to' positions (or at end of ranges2)
+ */
+ for (int i = 0; i < s1; i++)
+ {
+ int[] overlap = overlaps.get(i);
+ final int toAdd = overlap[1] - overlap[0] + 1;
+ int added = 0; // how much of overlap has been 'found'
+ for (; added < toAdd && ranges2Index < s2; ranges2Index++)
+ {
+ int[] range2 = ranges2.get(ranges2Index);
+ int rangeStart = range2[0];
+ int rangeEnd = range2[1];
+ boolean reverseStrand = range2[1] < range2[0];
+ int rangeLength = Math.abs(rangeEnd - rangeStart) + 1;
+ if (traversed + rangeLength <= overlap[0])
+ {
+ /*
+ * precedes overlap - keep looking
+ */
+ traversed += rangeLength;
+ continue;
+ }
+ int overlapStart = overlap[0] - traversed;
+ int overlapEnd = Math.min(overlapStart + toAdd - added - 1,
+ rangeLength - 1);
+ int mappedFrom = range2[0] + (reverseStrand ? - overlapStart : overlapStart);
+ int mappedTo = range2[0] + (reverseStrand ? - overlapEnd : overlapEnd);
+ mapped.add(new int[] { mappedFrom, mappedTo });
+ int found = overlapEnd - overlapStart + 1;
+ added += found;
+ overlap[0] += found;
+ traversed += rangeLength;
+ }
+ }
+
+ return mapped;
+ }
+
+ /**
+ * Converts the start-end positions (counted from zero) in the {@code ranges}
+ * list from one word length to another. Start-end positions are expanded if
+ * necessary to cover a whole word of length {@code wordLength1}. Positions
+ * are then divided by {@code wordLength1} and multiplied by
+ * {@code wordLength2} to give equivalent mapped words.
+ * <p>
+ * Put simply, this converts peptide residue positions to the corresponding
+ * codon ranges, and codons - including partial codons - to the corresponding
+ * peptide positions; for example
+ *
+ * <pre>
+ * [1, 10] with word lengths 3:1 converts (as if bases [0-11]) to [1, 4]
+ * </pre>
+ *
+ * @param ranges
+ * @param wordLength1
+ * @param wordLength2
+ * @return
+ */
+ final static void mapWords(List<int[]> ranges, int wordLength1,
+ int wordLength2)
+ {
+ if (wordLength1 == 1 && wordLength2 == 1)
+ {
+ return; // nothing to do here
+ }
+ int s = ranges.size();
+ for (int i = 0; i < s; i++)
+ {
+ int[] range = ranges.get(i);
+
+ /*
+ * expand range start to the start of a word,
+ * and convert to wordLength2
+ */
+ range[0] -= range[0] % wordLength1;
+ range[0] = range[0] / wordLength1 * wordLength2;
+
+ /*
+ * similar calculation for range end, adding
+ * (wordLength2 - 1) for end of mapped word
+ */
+ range[1] -= range[1] % wordLength1;
+ range[1] = range[1] / wordLength1 * wordLength2;
+ range[1] += wordLength2 - 1;
+ }
+ }
+
+ /**
+ * Helper method that returns a (possibly empty) list of offsets in
+ * {@code ranges} to subranges that overlap {@code start-end} (where start <=
+ * end}. The list returned holds counts of the number of positions traversed
+ * (exclusive) to reach the overlapping positions, not the overlapping values.
+ * Returns null if there are no overlaps.
+ *
+ * @param ranges
+ * @param start
+ * @param end
+ * @return
+ */
+ final static List<int[]> findOverlapPositions(List<int[]> ranges,
+ int start, int end)
+ {
+ List<int[]> positions = new ArrayList<>();
+ int pos = 0;
+ int s = ranges.size();
+ for (int i = 0; i < s; i++)
+ {
+ int[] range = ranges.get(i);
+ addOverlap(positions, pos, range, start, end);
+ pos += 1 + Math.abs(range[1] - range[0]);
+ }
+ return positions;
+ }
+
+ /**
+ * A helper method that checks whether {@code range} overlaps
+ * {@code start-end}, and if so adds the offset of the overlap in
+ * {@code range}, plus {@code pos}, to {@code positions}.
+ *
+ * @param positions
+ * a list of map offsets to add to
+ * @param pos
+ * the number of mapped positions already visited
+ * @param range
+ * a from-to range (may be forward or reverse)
+ * @param start
+ * position to test for overlap in range
+ * @param end
+ * position to test for overlap in range
+ * @return
+ */
+ final static void addOverlap(List<int[]> positions, int pos, int[] range,
+ int start, int end)
+ {
+ if (range[1] >= range[0])
+ {
+ /*
+ * forward direction range
+ */
+ if (start <= range[1] && end >= range[0])
+ {
+ /*
+ * overlap
+ */
+ int overlapStart = Math.max(start, range[0]);
+ int overlapStartOffset = pos + overlapStart - range[0];
+ int overlapEnd = Math.min(end, range[1]);
+ int overlapEndOffset = pos + overlapEnd - range[0];
+ int[] lastOverlap = positions.isEmpty() ? null
+ : positions.get(positions.size() - 1);
+ if (lastOverlap != null && overlapStartOffset == lastOverlap[1] + 1)
+ {
+ /*
+ * just extending the last overlap range
+ */
+ lastOverlap[1] = overlapEndOffset;
+ }
+ else
+ {
+ /*
+ * add a new (discontiguous) overlap range
+ */
+ positions.add(new int[] { overlapStartOffset, overlapEndOffset });
+ }
+ }
+ }
+ else
+ {
+ /*
+ * reverse direction range
+ */
+ if (start <= range[0] && end >= range[1])
+ {
+ /*
+ * overlap
+ */
+ int overlapStart = Math.max(start, range[1]);
+ int overlapEnd = Math.min(end, range[0]);
+ positions
+ .add(new int[]
+ { pos + range[0] - overlapEnd,
+ pos + range[0] - overlapStart });
+ }
+ }
+ }