/*
* Subregions (base 1) described as { [start1, end1], [start2, end2], ...}
*/
- private List<int[]> fromShifts = new ArrayList<int[]>();
+ private List<int[]> fromShifts;
/*
* Same format as fromShifts, for the 'mapped to' sequence
*/
- private List<int[]> toShifts = new ArrayList<int[]>();
+ private List<int[]> toShifts;
/*
* number of steps in fromShifts to one toRatio unit
private int toHighest;
/**
+ * Constructor
+ */
+ public MapList()
+ {
+ fromShifts = new ArrayList<>();
+ toShifts = new ArrayList<>();
+ }
+
+ /**
* Two MapList objects are equal if they are the same object, or they both
* have populated shift ranges and all values are the same.
*/
@Override
public boolean equals(Object o)
{
- // TODO should also override hashCode to ensure equal objects have equal
- // hashcodes
if (o == null || !(o instanceof MapList))
{
return false;
{
return false;
}
- return Arrays
- .deepEquals(fromShifts.toArray(), obj.fromShifts.toArray())
- && Arrays
- .deepEquals(toShifts.toArray(), obj.toShifts.toArray());
+ return Arrays.deepEquals(fromShifts.toArray(), obj.fromShifts.toArray())
+ && Arrays.deepEquals(toShifts.toArray(),
+ obj.toShifts.toArray());
+ }
+
+ /**
+ * Returns a hashcode made from the fromRatio, toRatio, and from/to ranges
+ */
+ @Override
+ public int hashCode()
+ {
+ int hashCode = 31 * fromRatio;
+ hashCode = 31 * hashCode + toRatio;
+ hashCode = 31 * hashCode + fromShifts.toArray().hashCode();
+ hashCode = 31 * hashCode + toShifts.toArray().hashCode();
+ return hashCode;
}
/**
}
/**
- * Constructor.
+ * Constructor given from and to ranges as [start1, end1, start2, end2,...].
+ * If any end is equal to the next start, the ranges will be merged. There is
+ * no validation check that the ranges do not overlap each other.
*
* @param from
* contiguous regions as [start1, end1, start2, end2, ...]
*/
public MapList(int from[], int to[], int fromRatio, int toRatio)
{
+ this();
this.fromRatio = fromRatio;
this.toRatio = toRatio;
- fromLowest = from[0];
- fromHighest = from[1];
+ fromLowest = Integer.MAX_VALUE;
+ fromHighest = Integer.MIN_VALUE;
+ int added = 0;
+
for (int i = 0; i < from.length; i += 2)
{
- fromLowest = Math.min(fromLowest, from[i]);
- fromHighest = Math.max(fromHighest, from[i + 1]);
-
- fromShifts.add(new int[] { from[i], from[i + 1] });
+ /*
+ * note lowest and highest values - bearing in mind the
+ * direction may be reversed
+ */
+ fromLowest = Math.min(fromLowest, Math.min(from[i], from[i + 1]));
+ fromHighest = Math.max(fromHighest, Math.max(from[i], from[i + 1]));
+ if (added > 0 && from[i] == fromShifts.get(added - 1)[1])
+ {
+ /*
+ * this range starts where the last ended - just extend it
+ */
+ fromShifts.get(added - 1)[1] = from[i + 1];
+ }
+ else
+ {
+ fromShifts.add(new int[] { from[i], from[i + 1] });
+ added++;
+ }
}
- toLowest = to[0];
- toHighest = to[1];
+ toLowest = Integer.MAX_VALUE;
+ toHighest = Integer.MIN_VALUE;
+ added = 0;
for (int i = 0; i < to.length; i += 2)
{
- toLowest = Math.min(toLowest, to[i]);
- toHighest = Math.max(toHighest, to[i + 1]);
- toShifts.add(new int[] { to[i], to[i + 1] });
+ toLowest = Math.min(toLowest, Math.min(to[i], to[i + 1]));
+ toHighest = Math.max(toHighest, Math.max(to[i], to[i + 1]));
+ if (added > 0 && to[i] == toShifts.get(added - 1)[1])
+ {
+ toShifts.get(added - 1)[1] = to[i + 1];
+ }
+ else
+ {
+ toShifts.add(new int[] { to[i], to[i + 1] });
+ added++;
+ }
}
}
*/
public MapList(MapList map)
{
+ this();
// TODO not used - remove?
this.fromLowest = map.fromLowest;
this.fromHighest = map.fromHighest;
}
/**
- * Constructor given ranges as lists of [start, end] positions
+ * Constructor given ranges as lists of [start, end] positions. There is no
+ * validation check that the ranges do not overlap each other.
*
* @param fromRange
* @param toRange
public MapList(List<int[]> fromRange, List<int[]> toRange, int fromRatio,
int toRatio)
{
+ this();
+ fromRange = coalesceRanges(fromRange);
+ toRange = coalesceRanges(toRange);
this.fromShifts = fromRange;
this.toShifts = toRange;
this.fromRatio = fromRatio;
this.toRatio = toRatio;
fromLowest = Integer.MAX_VALUE;
- fromHighest = 0;
+ fromHighest = Integer.MIN_VALUE;
for (int[] range : fromRange)
{
- fromLowest = Math.min(fromLowest, range[0]);
- fromHighest = Math.max(fromHighest, range[1]);
+ fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
+ fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
}
toLowest = Integer.MAX_VALUE;
- toHighest = 0;
+ toHighest = Integer.MIN_VALUE;
for (int[] range : toRange)
{
- toLowest = Math.min(toLowest, range[0]);
- toHighest = Math.max(toHighest, range[1]);
+ toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
+ toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
}
}
/**
+ * Consolidates a list of ranges so that any contiguous ranges are merged.
+ * This assumes the ranges are already in start order (does not sort them).
+ *
+ * @param ranges
+ * @return the same list (if unchanged), else a new merged list, leaving the
+ * input list unchanged
+ */
+ public static List<int[]> coalesceRanges(final List<int[]> ranges)
+ {
+ if (ranges == null || ranges.size() < 2)
+ {
+ return ranges;
+ }
+
+ boolean changed = false;
+ List<int[]> merged = new ArrayList<>();
+ int[] lastRange = ranges.get(0);
+ int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1;
+ lastRange = new int[] { lastRange[0], lastRange[1] };
+ merged.add(lastRange);
+ boolean first = true;
+
+ for (final int[] range : ranges)
+ {
+ if (first)
+ {
+ first = false;
+ continue;
+ }
+ if (range[0] == lastRange[0] && range[1] == lastRange[1])
+ {
+ // drop duplicate range
+ changed = true;
+ continue;
+ }
+
+ /*
+ * drop this range if it lies within the last range
+ */
+ if ((lastDirection == 1 && range[0] >= lastRange[0]
+ && range[0] <= lastRange[1] && range[1] >= lastRange[0]
+ && range[1] <= lastRange[1])
+ || (lastDirection == -1 && range[0] <= lastRange[0]
+ && range[0] >= lastRange[1]
+ && range[1] <= lastRange[0]
+ && range[1] >= lastRange[1]))
+ {
+ changed = true;
+ continue;
+ }
+
+ int direction = range[1] >= range[0] ? 1 : -1;
+
+ /*
+ * if next range is in the same direction as last and contiguous,
+ * just update the end position of the last range
+ */
+ boolean sameDirection = range[1] == range[0]
+ || direction == lastDirection;
+ boolean extending = range[0] == lastRange[1] + lastDirection;
+ boolean overlapping = (lastDirection == 1 && range[0] >= lastRange[0]
+ && range[0] <= lastRange[1])
+ || (lastDirection == -1 && range[0] <= lastRange[0]
+ && range[0] >= lastRange[1]);
+ if (sameDirection && (overlapping || extending))
+ {
+ lastRange[1] = range[1];
+ changed = true;
+ }
+ else
+ {
+ lastRange = new int[] { range[0], range[1] };
+ merged.add(lastRange);
+ // careful: merging [5, 5] after [7, 6] should keep negative direction
+ lastDirection = (range[1] == range[0]) ? lastDirection : direction;
+ }
+ }
+
+ return changed ? merged : ranges;
+ }
+
+ /**
* get all mapped positions from 'from' to 'to'
*
* @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
{
return null;
}
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
if (fs <= fe)
{
intv = fs;
// TODO not used - remove?
if (local)
{
- return ((getFromLowest() >= map.getFromLowest() && getFromHighest() <= map
- .getFromHighest()) || (getFromLowest() <= map.getFromLowest() && getFromHighest() >= map
- .getFromHighest()));
+ return ((getFromLowest() >= map.getFromLowest()
+ && getFromHighest() <= map.getFromHighest())
+ || (getFromLowest() <= map.getFromLowest()
+ && getFromHighest() >= map.getFromHighest()));
}
else
{
- return ((getToLowest() >= map.getToLowest() && getToHighest() <= map
- .getToHighest()) || (getToLowest() <= map.getToLowest() && getToHighest() >= map
- .getToHighest()));
+ return ((getToLowest() >= map.getToLowest()
+ && getToHighest() <= map.getToHighest())
+ || (getToLowest() <= map.getToLowest()
+ && getToHighest() >= map.getToHighest()));
}
}
public String toString()
{
StringBuilder sb = new StringBuilder(64);
- sb.append("From (").append(fromRatio).append(":").append(toRatio)
- .append(") [");
+ sb.append("[");
for (int[] shift : fromShifts)
{
sb.append(" ").append(Arrays.toString(shift));
}
- sb.append(" ] To [");
+ sb.append(" ] ");
+ sb.append(fromRatio).append(":").append(toRatio);
+ sb.append(" to [");
for (int[] shift : toShifts)
{
sb.append(" ").append(Arrays.toString(shift));
sb.append(" ]");
return sb.toString();
}
+
+ /**
+ * Extend this map list by adding the given map's ranges. There is no
+ * validation check that the ranges do not overlap existing ranges (or each
+ * other), but contiguous ranges are merged.
+ *
+ * @param map
+ */
+ public void addMapList(MapList map)
+ {
+ if (this.equals(map))
+ {
+ return;
+ }
+ this.fromLowest = Math.min(fromLowest, map.fromLowest);
+ this.toLowest = Math.min(toLowest, map.toLowest);
+ this.fromHighest = Math.max(fromHighest, map.fromHighest);
+ this.toHighest = Math.max(toHighest, map.toHighest);
+
+ for (int[] range : map.getFromRanges())
+ {
+ addRange(range, fromShifts);
+ }
+ for (int[] range : map.getToRanges())
+ {
+ addRange(range, toShifts);
+ }
+ }
+
+ /**
+ * Adds the given range to a list of ranges. If the new range just extends
+ * existing ranges, the current endpoint is updated instead.
+ *
+ * @param range
+ * @param addTo
+ */
+ static void addRange(int[] range, List<int[]> addTo)
+ {
+ /*
+ * list is empty - add to it!
+ */
+ if (addTo.size() == 0)
+ {
+ addTo.add(range);
+ return;
+ }
+
+ int[] last = addTo.get(addTo.size() - 1);
+ boolean lastForward = last[1] >= last[0];
+ boolean newForward = range[1] >= range[0];
+
+ /*
+ * contiguous range in the same direction - just update endpoint
+ */
+ if (lastForward == newForward && last[1] == range[0])
+ {
+ last[1] = range[1];
+ return;
+ }
+
+ /*
+ * next range starts at +1 in forward sense - update endpoint
+ */
+ if (lastForward && newForward && range[0] == last[1] + 1)
+ {
+ last[1] = range[1];
+ return;
+ }
+
+ /*
+ * next range starts at -1 in reverse sense - update endpoint
+ */
+ if (!lastForward && !newForward && range[0] == last[1] - 1)
+ {
+ last[1] = range[1];
+ return;
+ }
+
+ /*
+ * just add the new range
+ */
+ addTo.add(range);
+ }
+
+ /**
+ * Returns true if mapping is from forward strand, false if from reverse
+ * strand. Result is just based on the first 'from' range that is not a single
+ * position. Default is true unless proven to be false. Behaviour is not well
+ * defined if the mapping has a mixture of forward and reverse ranges.
+ *
+ * @return
+ */
+ public boolean isFromForwardStrand()
+ {
+ return isForwardStrand(getFromRanges());
+ }
+
+ /**
+ * Returns true if mapping is to forward strand, false if to reverse strand.
+ * Result is just based on the first 'to' range that is not a single position.
+ * Default is true unless proven to be false. Behaviour is not well defined if
+ * the mapping has a mixture of forward and reverse ranges.
+ *
+ * @return
+ */
+ public boolean isToForwardStrand()
+ {
+ return isForwardStrand(getToRanges());
+ }
+
+ /**
+ * A helper method that returns true unless at least one range has start > end.
+ * Behaviour is undefined for a mixture of forward and reverse ranges.
+ *
+ * @param ranges
+ * @return
+ */
+ private boolean isForwardStrand(List<int[]> ranges)
+ {
+ boolean forwardStrand = true;
+ for (int[] range : ranges)
+ {
+ if (range[1] > range[0])
+ {
+ break; // forward strand confirmed
+ }
+ else if (range[1] < range[0])
+ {
+ forwardStrand = false;
+ break; // reverse strand confirmed
+ }
+ }
+ return forwardStrand;
+ }
+
+ /**
+ *
+ * @return true if from, or to is a three to 1 mapping
+ */
+ public boolean isTripletMap()
+ {
+ return (toRatio == 3 && fromRatio == 1)
+ || (fromRatio == 3 && toRatio == 1);
+ }
+
+ /**
+ * Returns a map which is the composite of this one and the input map. That
+ * is, the output map has the fromRanges of this map, and its toRanges are the
+ * toRanges of this map as transformed by the input map.
+ * <p>
+ * Returns null if the mappings cannot be traversed (not all toRanges of this
+ * map correspond to fromRanges of the input), or if this.toRatio does not
+ * match map.fromRatio.
+ *
+ * <pre>
+ * Example 1:
+ * this: from [1-100] to [501-600]
+ * input: from [10-40] to [60-90]
+ * output: from [10-40] to [560-590]
+ * Example 2 ('reverse strand exons'):
+ * this: from [1-100] to [2000-1951], [1000-951] // transcript to loci
+ * input: from [1-50] to [41-90] // CDS to transcript
+ * output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
+ * </pre>
+ *
+ * @param map
+ * @return
+ */
+ public MapList traverse(MapList map)
+ {
+ if (map == null)
+ {
+ return null;
+ }
+
+ /*
+ * compound the ratios by this rule:
+ * A:B with M:N gives A*M:B*N
+ * reduced by greatest common divisor
+ * so 1:3 with 3:3 is 3:9 or 1:3
+ * 1:3 with 3:1 is 3:3 or 1:1
+ * 1:3 with 1:3 is 1:9
+ * 2:5 with 3:7 is 6:35
+ */
+ int outFromRatio = getFromRatio() * map.getFromRatio();
+ int outToRatio = getToRatio() * map.getToRatio();
+ int gcd = MathUtils.gcd(outFromRatio, outToRatio);
+ outFromRatio /= gcd;
+ outToRatio /= gcd;
+
+ List<int[]> toRanges = new ArrayList<>();
+ for (int[] range : getToRanges())
+ {
+ int[] transferred = map.locateInTo(range[0], range[1]);
+ if (transferred == null)
+ {
+ return null;
+ }
+ toRanges.add(transferred);
+ }
+
+ return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
+ }
+
}