JAL-2835 spike updated to latest (use specific SO term for feature)
[jalview.git] / src / jalview / util / MapList.java
index ab811a9..c944345 100644 (file)
@@ -77,8 +77,8 @@ public class MapList
    */
   public MapList()
   {
-    fromShifts = new ArrayList<int[]>();
-    toShifts = new ArrayList<int[]>();
+    fromShifts = new ArrayList<>();
+    toShifts = new ArrayList<>();
   }
 
   /**
@@ -88,8 +88,6 @@ public class MapList
   @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;
@@ -105,10 +103,22 @@ public class MapList
     {
       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;
   }
 
   /**
@@ -331,18 +341,19 @@ public class MapList
    */
   public static List<int[]> coalesceRanges(final List<int[]> ranges)
   {
-    if (ranges == null || ranges.size() < 2) {
+    if (ranges == null || ranges.size() < 2)
+    {
       return ranges;
     }
 
     boolean changed = false;
-    List<int[]> merged = new ArrayList<int[]>();
+    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)
@@ -361,10 +372,12 @@ public class MapList
        * 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])
+              && 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]))
+                      && range[1] <= lastRange[0]
+                      && range[1] >= lastRange[1]))
       {
         changed = true;
         continue;
@@ -376,10 +389,13 @@ public class MapList
        * 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 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]);
+      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];
@@ -393,7 +409,7 @@ public class MapList
         lastDirection = (range[1] == range[0]) ? lastDirection : direction;
       }
     }
-    
+
     return changed ? merged : ranges;
   }
 
@@ -787,7 +803,7 @@ public class MapList
     {
       return null;
     }
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     if (fs <= fe)
     {
       intv = fs;
@@ -948,15 +964,17 @@ public class MapList
     // 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()));
     }
   }
 
@@ -992,6 +1010,10 @@ public class MapList
    */
   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);
@@ -1072,8 +1094,33 @@ public class MapList
    */
   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 : getFromRanges())
+    for (int[] range : ranges)
     {
       if (range[1] > range[0])
       {
@@ -1087,4 +1134,74 @@ public class MapList
     }
     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);
+  }
+
 }