Merge branch 'develop' into features/JAL-1793VCF
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 26 Oct 2017 13:32:13 +0000 (14:32 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 26 Oct 2017 13:32:13 +0000 (14:32 +0100)
Conflicts:
src/jalview/util/MappingUtils.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/util/MappingUtilsTest.java

1  2 
src/jalview/analysis/AlignmentUtils.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AquaInternalFrameManager.java
src/jalview/gui/IdPanel.java
src/jalview/gui/SeqPanel.java
src/jalview/util/MappingUtils.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/util/MappingUtilsTest.java

@@@ -52,18 -51,6 +52,20 @@@ public class SequenceFeature implement
    // private key for ENA location designed not to conflict with real GFF data
    private static final String LOCATION = "!Location";
  
 +  private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td></tr>";
 +
 +  /*
 +   * map of otherDetails special keys, and their value fields' delimiter
 +   */
 +  private static final Map<String, String> INFO_KEYS = new HashMap<>();
 +
 +  static
 +  {
 +    INFO_KEYS.put("CSQ", ",");
++    // todo capture second level metadata (CSQ FORMAT)
++    // and delimiter "|" so as to report in a table within a table?
 +  }
 +
    /*
     * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
     * name1=value1;name2=value2,value3;...etc
    {
      return begin == 0 && end == 0;
    }
 +
 +  /**
 +   * Answers an html-formatted report of feature details
 +   * 
 +   * @return
 +   */
 +  public String getDetailsReport()
 +  {
 +    StringBuilder sb = new StringBuilder(128);
 +    sb.append("<br>");
 +    sb.append("<table>");
 +    sb.append(String.format(ROW_DATA, "Type", type));
 +    sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
 +            : begin + (isContactFeature() ? ":" : "-") + end));
 +    String desc = StringUtils.stripHtmlTags(description);
 +    sb.append(String.format(ROW_DATA, "Description", desc));
 +    if (!Float.isNaN(score) && score != 0f)
 +    {
 +      sb.append(String.format(ROW_DATA, "Score", score));
 +    }
 +    if (featureGroup != null)
 +    {
 +      sb.append(String.format(ROW_DATA, "Group", featureGroup));
 +    }
 +
 +    if (otherDetails != null)
 +    {
 +      TreeMap<String, Object> ordered = new TreeMap<>(
 +              String.CASE_INSENSITIVE_ORDER);
 +      ordered.putAll(otherDetails);
 +
 +      for (Entry<String, Object> entry : ordered.entrySet())
 +      {
 +        String key = entry.getKey();
 +        if (ATTRIBUTES.equals(key))
 +        {
 +          continue; // to avoid double reporting
 +        }
-         sb.append("<tr><td>").append(key).append("</td><td>");
 +        if (INFO_KEYS.containsKey(key))
 +        {
 +          /*
 +           * split selected INFO data by delimiter over multiple lines
 +           */
-           sb.append("</td></tr>");
 +          String delimiter = INFO_KEYS.get(key);
 +          String[] values = entry.getValue().toString().split(delimiter);
 +          for (String value : values)
 +          {
-             sb.append("<tr><td>&nbsp</td><td>").append(value)
++            sb.append("<tr><td>").append(key).append("</td><td>")
++                    .append(value)
 +                    .append("</td></tr>");
 +          }
 +        }
 +        else
-         {
++        { // tried <td title="key"> but it failed to provide a tooltip :-(
++          sb.append("<tr><td>").append(key).append("</td><td>");
 +          sb.append(entry.getValue().toString()).append("</td></tr>");
 +        }
 +      }
 +    }
 +    sb.append("</table>");
 +
 +    String text = sb.toString();
 +    return text;
 +  }
  }
Simple merge
index 0000000,ea809eb..829135b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,257 +1,256 @@@
+ /*
+  * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
+  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+  *
+  * This code is free software; you can redistribute it and/or modify it
+  * under the terms of the GNU General Public License version 2 only, as
+  * published by the Free Software Foundation.  Oracle designates this
+  * particular file as subject to the "Classpath" exception as provided
+  * by Oracle in the LICENSE file that accompanied this code.
+  *
+  * This code is distributed in the hope that it will be useful, but WITHOUT
+  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+  * version 2 for more details (a copy is included in the LICENSE file that
+  * accompanied this code).
+  *
+  * You should have received a copy of the GNU General Public License version
+  * 2 along with this work; if not, write to the Free Software Foundation,
+  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+  *
+  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+  * or visit www.oracle.com if you need additional information or have any
+  * questions.
+  */
+ package jalview.gui;
+ import java.awt.Container;
+ import java.beans.PropertyVetoException;
+ import java.util.Vector;
+ import javax.swing.DefaultDesktopManager;
+ import javax.swing.DesktopManager;
+ import javax.swing.JInternalFrame;
+ /**
+  * Based on AquaInternalFrameManager
+  *
+  * DesktopManager implementation for Aqua
+  *
+  * Mac is more like Windows than it's like Motif/Basic
+  *
+  * From WindowsDesktopManager:
+  *
+  * This class implements a DesktopManager which more closely follows the MDI
+  * model than the DefaultDesktopManager. Unlike the DefaultDesktopManager
+  * policy, MDI requires that the selected and activated child frames are the
+  * same, and that that frame always be the top-most window.
+  * <p>
+  * The maximized state is managed by the DesktopManager with MDI, instead of
+  * just being a property of the individual child frame. This means that if the
+  * currently selected window is maximized and another window is selected, that
+  * new window will be maximized.
+  *
+  * Downloaded from
+  * https://raw.githubusercontent.com/frohoff/jdk8u-jdk/master/src/macosx/classes/com/apple/laf/AquaInternalFrameManager.java
+  * 
+  * Patch from Jim Procter - when the most recently opened frame is closed,
+  * correct behaviour is to go to the next most recent frame, rather than wrap
+  * around to the bottom of the window stack (as the original implementation
+  * does)
+  * 
 - * @see com.sun.java.swing.plaf.windows.WindowsDesktopManager
+  */
+ public class AquaInternalFrameManager extends DefaultDesktopManager
+ {
+   // Variables
+   /* The frame which is currently selected/activated.
+    * We store this value to enforce Mac's single-selection model.
+    */
+   JInternalFrame fCurrentFrame;
+   JInternalFrame fInitialFrame;
+   /* The list of frames, sorted by order of creation.
+    * This list is necessary because by default the order of
+    * child frames in the JDesktopPane changes during frame
+    * activation (the activated frame is moved to index 0).
+    * We preserve the creation order so that "next" and "previous"
+    * frame actions make sense.
+    */
+   Vector<JInternalFrame> fChildFrames = new Vector<>(1);
+   /**
+    * keep a reference to the original LAF manager so we can iconise/de-iconise
+    * correctly
+    */
+   private DesktopManager ourManager;
+   public AquaInternalFrameManager(DesktopManager desktopManager)
+   {
+     ourManager = desktopManager;
+   }
+   @Override
+   public void closeFrame(final JInternalFrame f)
+   {
+     if (f == fCurrentFrame)
+     {
+       boolean mostRecentFrame = fChildFrames
+               .indexOf(f) == fChildFrames.size() - 1;
+       if (!mostRecentFrame)
+       {
+         activateNextFrame();
+       }
+       else
+       {
+         activatePreviousFrame();
+       }
+     }
+     fChildFrames.removeElement(f);
+     super.closeFrame(f);
+   }
+   @Override
+   public void deiconifyFrame(final JInternalFrame f)
+   {
+     JInternalFrame.JDesktopIcon desktopIcon;
+     desktopIcon = f.getDesktopIcon();
+     // If the icon moved, move the frame to that spot before expanding it
+     // reshape does delta checks for us
+     f.reshape(desktopIcon.getX(), desktopIcon.getY(), f.getWidth(),
+             f.getHeight());
+     ourManager.deiconifyFrame(f);
+   }
+   void addIcon(final Container c,
+           final JInternalFrame.JDesktopIcon desktopIcon)
+   {
+     c.add(desktopIcon);
+   }
+   /**
+    * Removes the frame from its parent and adds its desktopIcon to the parent.
+    */
+   @Override
+   public void iconifyFrame(final JInternalFrame f)
+   {
+     ourManager.iconifyFrame(f);
+   }
+   // WindowsDesktopManager code
+   @Override
+   public void activateFrame(final JInternalFrame f)
+   {
+     try
+     {
+       if (f != null)
+       {
+         super.activateFrame(f);
+       }
+       // If this is the first activation, add to child list.
+       if (fChildFrames.indexOf(f) == -1)
+       {
+         fChildFrames.addElement(f);
+       }
+       if (fCurrentFrame != null && f != fCurrentFrame)
+       {
+         if (fCurrentFrame.isSelected())
+         {
+           fCurrentFrame.setSelected(false);
+         }
+       }
+       if (f != null && !f.isSelected())
+       {
+         f.setSelected(true);
+       }
+       fCurrentFrame = f;
+     } catch (final PropertyVetoException e)
+     {
+     }
+   }
+   private void switchFrame(final boolean next)
+   {
+     if (fCurrentFrame == null)
+     {
+       // initialize first frame we find
+       if (fInitialFrame != null)
+       {
+         activateFrame(fInitialFrame);
+       }
+       return;
+     }
+     final int count = fChildFrames.size();
+     if (count <= 1)
+     {
+       // No other child frames.
+       return;
+     }
+     final int currentIndex = fChildFrames.indexOf(fCurrentFrame);
+     if (currentIndex == -1)
+     {
+       // the "current frame" is no longer in the list
+       fCurrentFrame = null;
+       return;
+     }
+     int nextIndex;
+     if (next)
+     {
+       nextIndex = currentIndex + 1;
+       if (nextIndex == count)
+       {
+         nextIndex = 0;
+       }
+     }
+     else
+     {
+       nextIndex = currentIndex - 1;
+       if (nextIndex == -1)
+       {
+         nextIndex = count - 1;
+       }
+     }
+     final JInternalFrame f = fChildFrames.elementAt(nextIndex);
+     activateFrame(f);
+     fCurrentFrame = f;
+   }
+   /**
+    * Activate the next child JInternalFrame, as determined by the frames'
+    * Z-order. If there is only one child frame, it remains activated. If there
+    * are no child frames, nothing happens.
+    */
+   public void activateNextFrame()
+   {
+     switchFrame(true);
+   }
+   /**
+    * same as above but will activate a frame if none have been selected
+    */
+   public void activateNextFrame(final JInternalFrame f)
+   {
+     fInitialFrame = f;
+     switchFrame(true);
+   }
+   /**
+    * Activate the previous child JInternalFrame, as determined by the frames'
+    * Z-order. If there is only one child frame, it remains activated. If there
+    * are no child frames, nothing happens.
+    */
+   public void activatePreviousFrame()
+   {
+     switchFrame(false);
+   }
+ }
Simple merge
Simple merge
@@@ -941,30 -941,53 +941,81 @@@ public final class MappingUtil
    }
  
    /**
 +   * Answers true if range's start-end positions include those of queryRange,
 +   * where either range might be in reverse direction, else false
 +   * 
 +   * @param range
 +   *          a start-end range
 +   * @param queryRange
 +   *          a candidate subrange of range (start2-end2)
 +   * @return
 +   */
 +  public static boolean rangeContains(int[] range, int[] queryRange)
 +  {
 +    if (range == null || queryRange == null || range.length != 2
 +            || queryRange.length != 2)
 +    {
 +      /*
 +       * invalid arguments
 +       */
 +      return false;
 +    }
 +
 +    int min = Math.min(range[0], range[1]);
 +    int max = Math.max(range[0], range[1]);
 +  
 +    return (min <= queryRange[0] && max >= queryRange[0]
 +            && min <= queryRange[1] && max >= queryRange[1]);
 +  }
++
++  /**
+    * Removes the specified number of positions from the given ranges. Provided
+    * to allow a stop codon to be stripped from a CDS sequence so that it matches
+    * the peptide translation length.
+    * 
+    * @param positions
+    * @param ranges
+    *          a list of (single) [start, end] ranges
+    * @return
+    */
+   public static void removeEndPositions(int positions,
+           List<int[]> ranges)
+   {
+     int toRemove = positions;
+     Iterator<int[]> it = new ReverseListIterator<>(ranges);
+     while (toRemove > 0)
+     {
+       int[] endRange = it.next();
+       if (endRange.length != 2)
+       {
+         /*
+          * not coded for [start1, end1, start2, end2, ...]
+          */
+         System.err
+                 .println("MappingUtils.removeEndPositions doesn't handle multiple  ranges");
+         return;
+       }
+       int length = endRange[1] - endRange[0] + 1;
+       if (length <= 0)
+       {
+         /*
+          * not coded for a reverse strand range (end < start)
+          */
+         System.err
+                 .println("MappingUtils.removeEndPositions doesn't handle reverse strand");
+         return;
+       }
+       if (length > toRemove)
+       {
+         endRange[1] -= toRemove;
+         toRemove = 0;
+       }
+       else
+       {
+         toRemove -= length;
+         it.remove();
+       }
+     }
+   }
  }
@@@ -64,6 -63,6 +64,8 @@@ import org.testng.annotations.Test
  
  public class AlignmentUtilsTests
  {
++  private static Sequence ts = new Sequence("short",
++          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
@@@ -72,9 -71,9 +74,6 @@@
      JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
    }
  
-   private static Sequence ts = new Sequence("short",
-           "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
 -  public static Sequence ts = new Sequence("short",
 -          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
 -
    @Test(groups = { "Functional" })
    public void testExpandContext()
    {
      assertEquals(s_as3, uas3.getSequenceAsString());
    }
  
 +  @Test(groups = { "Functional" })
 +  public void testTransferGeneLoci()
 +  {
 +    SequenceI from = new Sequence("transcript",
 +            "aaacccgggTTTAAACCCGGGtttaaacccgggttt");
 +    SequenceI to = new Sequence("CDS", "TTTAAACCCGGG");
 +    MapList map = new MapList(new int[] { 1, 12 }, new int[] { 10, 21 }, 1,
 +            1);
 +
 +    /*
 +     * first with nothing to transfer
 +     */
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +    assertNull(to.getGeneLoci());
 +
 +    /*
 +     * next with gene loci set on 'from' sequence
 +     */
 +    int[] exons = new int[] { 100, 105, 155, 164, 210, 229 };
 +    MapList geneMap = new MapList(new int[] { 1, 36 }, exons, 1, 1);
 +    from.setGeneLoci("human", "GRCh38", "7", geneMap);
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +
 +    GeneLociI toLoci = to.getGeneLoci();
 +    assertNotNull(toLoci);
 +    // DBRefEntry constructor upper-cases 'source'
 +    assertEquals("HUMAN", toLoci.getSpeciesId());
 +    assertEquals("GRCh38", toLoci.getAssemblyId());
 +    assertEquals("7", toLoci.getChromosomeId());
 +
 +    /*
 +     * transcript 'exons' are 1-6, 7-16, 17-36
 +     * CDS 1:12 is transcript 10-21
 +     * transcript 'CDS' is 10-16, 17-21
 +     * which is 'gene' 158-164, 210-214
 +     */
 +    MapList toMap = toLoci.getMap();
 +    assertEquals(1, toMap.getFromRanges().size());
 +    assertEquals(2, toMap.getFromRanges().get(0).length);
 +    assertEquals(1, toMap.getFromRanges().get(0)[0]);
 +    assertEquals(12, toMap.getFromRanges().get(0)[1]);
 +    assertEquals(1, toMap.getToRanges().size());
 +    assertEquals(4, toMap.getToRanges().get(0).length);
 +    assertEquals(158, toMap.getToRanges().get(0)[0]);
 +    assertEquals(164, toMap.getToRanges().get(0)[1]);
 +    assertEquals(210, toMap.getToRanges().get(0)[2]);
 +    assertEquals(214, toMap.getToRanges().get(0)[3]);
 +    // or summarised as (but toString might change in future):
 +    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
 +            toMap.toString());
 +
 +    /*
 +     * an existing value is not overridden 
 +     */
 +    geneMap = new MapList(new int[] { 1, 36 }, new int[] { 36, 1 }, 1, 1);
 +    from.setGeneLoci("inhuman", "GRCh37", "6", geneMap);
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +    assertEquals("GRCh38", toLoci.getAssemblyId());
 +    assertEquals("7", toLoci.getChromosomeId());
 +    toMap = toLoci.getMap();
 +    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
 +            toMap.toString());
 +  }
++
+   /**
+    * Tests for the method that maps nucleotide to protein based on CDS features
+    */
+   @Test(groups = "Functional")
+   public void testMapCdsToProtein()
+   {
+     SequenceI peptide = new Sequence("pep", "KLQ");
+     /*
+      * Case 1: CDS 3 times length of peptide
+      * NB method only checks lengths match, not translation
+      */
+     SequenceI dna = new Sequence("dna", "AACGacgtCTCCT");
+     dna.createDatasetSequence();
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 13, null));
+     MapList ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertEquals(3, ml.getFromRatio());
+     assertEquals(1, ml.getToRatio());
+     assertEquals("[[1, 3]]",
+             Arrays.deepToString(ml.getToRanges().toArray()));
+     assertEquals("[[1, 4], [9, 13]]",
+             Arrays.deepToString(ml.getFromRanges().toArray()));
+     /*
+      * Case 2: CDS 3 times length of peptide + stop codon
+      * (note code does not currently check trailing codon is a stop codon)
+      */
+     dna = new Sequence("dna", "AACGacgtCTCCTTGA");
+     dna.createDatasetSequence();
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 16, null));
+     ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertEquals(3, ml.getFromRatio());
+     assertEquals(1, ml.getToRatio());
+     assertEquals("[[1, 3]]",
+             Arrays.deepToString(ml.getToRanges().toArray()));
+     assertEquals("[[1, 4], [9, 13]]",
+             Arrays.deepToString(ml.getFromRanges().toArray()));
+     /*
+      * Case 3: CDS not 3 times length of peptide - no mapping is made
+      */
+     dna = new Sequence("dna", "AACGacgtCTCCTTG");
+     dna.createDatasetSequence();
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 15, null));
+     ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertNull(ml);
+     /*
+      * Case 4: incomplete start codon corresponding to X in peptide
+      */
+     dna = new Sequence("dna", "ACGacgtCTCCTTGG");
+     dna.createDatasetSequence();
+     SequenceFeature sf = new SequenceFeature("CDS", "", 1, 3, null);
+     sf.setPhase("2"); // skip 2 positions (AC) to start of next codon (GCT)
+     dna.addSequenceFeature(sf);
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 8, 15, null));
+     peptide = new Sequence("pep", "XLQ");
+     ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertEquals("[[2, 3]]",
+             Arrays.deepToString(ml.getToRanges().toArray()));
+     assertEquals("[[3, 3], [8, 12]]",
+             Arrays.deepToString(ml.getFromRanges().toArray()));
+   }
 -
  }
@@@ -1149,93 -1149,49 +1149,138 @@@ public class MappingUtilsTes
      assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
    }
  
 +  @Test(groups = { "Functional" })
 +  public void testRangeContains()
 +  {
 +    /*
 +     * both forward ranges
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        2, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 9 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        4, 5 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        0, 9 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        -10, -9 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        11, 12 }));
 +
 +    /*
 +     * forward range, reverse query
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        9, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 2 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        5, 5 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        11, 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 0 }));
 +
 +    /*
 +     * reverse range, forward query
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        1, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        1, 9 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        2, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        6, 6 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        6, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        11, 20 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        -3, -2 }));
 +
 +    /*
 +     * both reverse
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        9, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 2 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        3, 3 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        11, 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 0 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        12, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        -5, -8 }));
 +
 +    /*
 +     * bad arguments
 +     */
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10, 12 },
 +            new int[] {
 +        1, 10 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 },
 +            new int[] { 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, null));
 +    assertFalse(MappingUtils.rangeContains(null, new int[] { 1, 10 }));
 +  }
 +
+   @Test(groups = "Functional")
+   public void testRemoveEndPositions()
+   {
+     List<int[]> ranges = new ArrayList<>();
+     /*
+      * case 1: truncate last range
+      */
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 30 });
+     MappingUtils.removeEndPositions(5, ranges);
+     assertEquals(2, ranges.size());
+     assertEquals(25, ranges.get(1)[1]);
+     /*
+      * case 2: remove last range
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 22 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(10, ranges.get(0)[1]);
+     /*
+      * case 3: truncate penultimate range
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 21 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(9, ranges.get(0)[1]);
+     /*
+      * case 4: remove last two ranges
+      */
+     ranges.clear();
+     ranges.add(new int[] { 1, 10 });
+     ranges.add(new int[] { 20, 20 });
+     ranges.add(new int[] { 30, 30 });
+     MappingUtils.removeEndPositions(3, ranges);
+     assertEquals(1, ranges.size());
+     assertEquals(9, ranges.get(0)[1]);
+   }
  }