/* * Copyright 2015 MiLaboratory.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.milaboratory.core; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.milaboratory.core.io.binary.RangeSerializer; import com.milaboratory.primitivio.annotations.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * This class represents a range of positions in a sequence (e.g. sub-sequence). Range can be <b>reversed</b> ({@code * from > to}), to represent reverse complement sub-sequence of a nucleotide sequence. * * <p><b>Main contract:</b> upper limit (with biggest value) is always exclusive, and lower is always inclusive.</p> */ @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE) @Serializable(by = RangeSerializer.class) public final class Range implements java.io.Serializable, Comparable<Range> { static final long serialVersionUID = 1L; private final int lower; private final int upper; private final boolean reversed; public Range(int lower, int upper, boolean reversed) { if (lower > upper) throw new IllegalArgumentException(); this.lower = lower; this.upper = upper; this.reversed = reversed; } @JsonCreator public Range(@JsonProperty("from") int from, @JsonProperty("to") int to) { if (this.reversed = (from > to)) { this.upper = from; this.lower = to; } else { this.upper = to; this.lower = from; } } public Range expand(int offset) { return expand(offset, offset); } public Range expand(int leftOffset, int rightOffset) { return new Range(lower - leftOffset, upper + rightOffset, reversed); } /** * Returns {@literal true} if {@code length() == 0}. * * @return {@literal true} if {@code length() == 0}. */ public boolean isEmpty() { return upper == lower; } /** * Returns the length of this range. * * @return length of this range */ public int length() { return upper - lower; } /** * Returns true if this range is reversed. * * @return true if this range is reversed */ public boolean isReverse() { return reversed; } /** * Returns true if two ranges has the same direction. Always return true if any of ranges are empty. * * @param other other range to compare with * @return true if two ranges has the same direction. Always return true if any of ranges are of zero length */ public boolean hasSameDirection(Range other) { return this.isEmpty() || other.isEmpty() || this.isReverse() == other.isReverse(); } /** * Returns from value. This bound may be exclusive of inclusive depending on the range orientation (see main * contract in the class description). * * @return from value (exclusive or inclusive) */ @JsonProperty("from") public int getFrom() { return reversed ? upper : lower; } /** * Returns to value. This bound may be exclusive of inclusive depending on the range orientation (see main contract * in the class description). * * @return to value (exclusive or inclusive) */ @JsonProperty("to") public int getTo() { return reversed ? lower : upper; } /** * Returns upper (with biggest value) bound of this range. This bound is always exclusive. * * @return upper limit of this range (exclusive) */ public int getUpper() { return upper; } /** * Returns lower (with least value) bound of this range. This bound is always inclusive. * * @return lower limit of this range (inclusive) */ public int getLower() { return lower; } /** * Returns reversed range. * * @return reversed range */ public Range inverse() { return new Range(lower, upper, !reversed); } /** * Returns {@code true} if range contains provided {@code position}. * * @param position position * @return {@code true} if range contains provided {@code position} */ public boolean contains(int position) { return position >= lower && position < upper; } public boolean containsBoundary(int position) { return position >= lower && position <= upper; } /** * Returns {@code true} if range contains {@code other} range. * * @param other other range * @return {@code true} if range contains {@code other} range */ public boolean contains(Range other) { return lower <= other.lower && upper >= other.upper; } /** * Returns {@code true} if range intersects with {@code other} range. * * @param other other range * @return {@code true} if range intersects with {@code other} range */ public boolean intersectsWith(Range other) { return !other.isEmpty() && !this.isEmpty() && (this.contains(other.lower) || other.contains(this.lower) || (other.upper > upper && other.lower < lower)); } /** * Returns {@code true} if range intersects with {@code other} range. * * @param other other range * @return {@code true} if range intersects with {@code other} range */ public boolean intersectsWithOrTouches(Range other) { return contains(other.lower) || contains(other.upper - 1) || (other.upper > upper && other.lower < lower) || other.lower == upper || other.upper == lower; } /** * Returns intersection range with {@code other} range. * * @param other other range * @return intersection range with {@code other} range or null if ranges not intersects */ public Range intersection(Range other) { if (!intersectsWith(other)) return null; return new Range(Math.max(lower, other.lower), Math.min(upper, other.upper), reversed && other.reversed); } /** * Returns union range with {@code other} range. * * @param other other range * @return intersection range with {@code other} range or null if ranges not intersects ot touches */ public Range tryMerge(Range other) { if (!intersectsWithOrTouches(other)) return null; return new Range(Math.min(lower, other.lower), Math.max(upper, other.upper), reversed && other.reversed); } /** * Returns range moved using provided offset (e.g. [lower + offset, upper + offset, reversed]) * * @param offset offset, can be negative * @return range moved using provided offset */ public Range move(int offset) { if (offset == 0) return this; return new Range(lower + offset, upper + offset, reversed); } /** * Returns relative point position inside this range. * * @param absolutePosition absolute point position (in the same coordinates as this range boundaries) * @return relative point position inside this range */ public int convertPointToRelativePosition(int absolutePosition) { if (absolutePosition < lower || absolutePosition >= upper) throw new IllegalArgumentException("Position outside this range (" + absolutePosition + ")."); if (reversed) return upper - 1 - absolutePosition; else return absolutePosition - lower; } /** * Returns relative boundary position inside this range. * * @param absolutePosition absolute boundary position (in the same coordinates as this range boundaries) * @return relative boundary position inside this range */ public int convertBoundaryToRelativePosition(int absolutePosition) { if (absolutePosition < lower || absolutePosition > upper) throw new IllegalArgumentException("Position outside this range (" + absolutePosition + ") this=" + this + "."); if (reversed) return upper - absolutePosition; else return absolutePosition - lower; } /** * Subtract provided range and return list of ranges contained in current range and not intersecting with other * range. * * @param range range to subtract * @return list of ranges contained in current range and not intersecting with other range */ @SuppressWarnings("unchecked") public List<Range> without(Range range) { if (!intersectsWith(range)) return Collections.singletonList(this); if (upper <= range.upper) return range.lower <= lower ? Collections.EMPTY_LIST : Collections.singletonList(new Range(lower, range.lower, reversed)); if (range.lower <= lower) return Collections.singletonList(new Range(range.upper, upper, reversed)); return Arrays.asList(new Range(lower, range.lower, reversed), new Range(range.upper, upper, reversed)); } public Range getRelativeRangeOf(Range range) { int from = convertBoundaryToRelativePosition(range.getFrom()), to = convertBoundaryToRelativePosition(range.getTo()); if (from == -1 || to == -1) return null; return new Range(from, to); } public int[] convertBoundariesToRelativePosition(int... absolutePositions) { int[] result = new int[absolutePositions.length]; for (int i = 0; i < absolutePositions.length; ++i) result[i] = convertBoundaryToRelativePosition(absolutePositions[i]); return result; } public int[] convertPointsToRelativePosition(int... absolutePositions) { int[] result = new int[absolutePositions.length]; for (int i = 0; i < absolutePositions.length; ++i) result[i] = convertPointToRelativePosition(absolutePositions[i]); return result; } /** * Converts relative point position to absolute position * * @param relativePosition relative point position * @return absolute point position */ public int convertPointToAbsolutePosition(int relativePosition) { if (relativePosition < 0 || relativePosition >= length()) throw new IllegalArgumentException("Relative position outside this range (" + relativePosition + ")."); if (reversed) return upper - 1 - relativePosition; else return relativePosition + lower; } /** * Converts relative boundary position to absolute position * * @param relativePosition relative boundary position * @return absolute point position */ public int convertBoundaryToAbsolutePosition(int relativePosition) { if (relativePosition < 0 || relativePosition > length()) throw new IllegalArgumentException("Relative position outside this range (" + relativePosition + ")."); if (reversed) return upper - relativePosition; else return relativePosition + lower; } @Override public int compareTo(Range o) { int cmp; if ((cmp = Integer.compare(getLower(), o.getLower())) != 0) return cmp; if ((cmp = Integer.compare(getUpper(), o.getUpper())) != 0) return cmp; return Boolean.compare(isReverse(), o.isReverse()); } @Override public String toString() { return "(" + lower + (reversed ? "<-" : "->") + upper + ")"; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Range range = (Range) o; return lower == range.lower && reversed == range.reversed && upper == range.upper; } @Override public int hashCode() { int result = lower; result = 31 * result + upper; result = 31 * result + (reversed ? 1 : 0); return result; } public static final Comparator<Range> COMPARATOR_BY_FROM = new Comparator<Range>() { @Override public int compare(Range o1, Range o2) { return Integer.compare(o1.getFrom(), o2.getTo()); } }; }