Merge branch 'bug/JAL-2791exportFilteredFeature' into merge/JAL-2791
[jalview.git] / src / jalview / datamodel / HiddenColumns.java
index 6e594bd..2d43f02 100644 (file)
@@ -33,13 +33,13 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
  * use an iterator obtained from one of:
  * 
  * - getBoundedIterator: iterates over the hidden regions, within some bounds,
- * returning absolute positions
+ * returning *absolute* positions
  * 
  * - getBoundedStartIterator: iterates over the start positions of hidden
- * regions, within some bounds, returning visible positions
+ * regions, within some bounds, returning *visible* positions
  * 
  * - getVisContigsIterator: iterates over visible regions in a range, returning
- * absolute positions
+ * *absolute* positions
  * 
  * - getVisibleColsIterator: iterates over the visible *columns*
  * 
@@ -47,7 +47,20 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
  * numbering begins at 0 throughout this class.
  * 
  * @author kmourao
- *
+ */
+
+/* Implementation notes:
+ * 
+ * Methods which change the hiddenColumns collection should use a writeLock to
+ * prevent other threads accessing the hiddenColumns collection while changes
+ * are being made. They should also reset the hidden columns cursor, and either
+ * update the hidden columns count, or set it to 0 (so that it will later be
+ * updated when needed).
+ * 
+ * 
+ * Methods which only need read access to the hidden columns collection should
+ * use a readLock to prevent other threads changing the hidden columns
+ * collection while it is in use.
  */
 public class HiddenColumns
 {
@@ -62,7 +75,8 @@ public class HiddenColumns
   private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
 
   /*
-   * cache of the number of hidden columns
+   * cache of the number of hidden columns: must be kept up to date by methods 
+   * which add or remove hidden columns
    */
   private int numColumns = 0;
 
@@ -79,14 +93,6 @@ public class HiddenColumns
   {
   }
 
-  /* 
-   * Methods which change the hiddenColumns collection. These methods should 
-   * use a writeLock to prevent other threads accessing the hiddenColumns
-   * collection while changes are being made. They should also reset the hidden
-   * columns cursor, and either update the hidden columns count, or set it to 0 
-   * (so that it will later be updated when needed).
-   */
-
   /**
    * Copy constructor
    * 
@@ -135,7 +141,7 @@ public class HiddenColumns
             numColumns += region[1] - region[0] + 1;
           }
         }
-        cursor.resetCursor(hiddenColumns);
+        cursor = new HiddenColumnsCursor(hiddenColumns);
       }
     } finally
     {
@@ -163,7 +169,7 @@ public class HiddenColumns
       if (!hiddenColumns.isEmpty())
       {
         // set up cursor reset values
-        HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start);
+        HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start, false);
         regionindex = cursorPos.getRegionIndex();
 
         if (regionindex > 0)
@@ -176,13 +182,13 @@ public class HiddenColumns
         }
       }
 
-      /*
-       * new range follows everything else; check first to avoid looping over whole hiddenColumns collection
-       */
+      // new range follows everything else; check first to avoid looping over
+      // whole hiddenColumns collection
       if (hiddenColumns.isEmpty()
               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
       {
         hiddenColumns.add(new int[] { start, end });
+        numColumns += end - start + 1;
       }
       else
       {
@@ -203,11 +209,8 @@ public class HiddenColumns
 
       // reset the cursor to just before our insertion point: this saves
       // a lot of reprocessing in large alignments
-      cursor.resetCursor(hiddenColumns, previndex, prevHiddenCount);
-
-      // reset the number of columns so they will be recounted
-      numColumns = 0;
-
+      cursor = new HiddenColumnsCursor(hiddenColumns, previndex,
+              prevHiddenCount);
     } finally
     {
       LOCK.writeLock().unlock();
@@ -237,6 +240,7 @@ public class HiddenColumns
        * insert discontiguous preceding range
        */
       hiddenColumns.add(i, new int[] { start, end });
+      numColumns += end - start + 1;
       added = true;
     }
     else if (end <= region[1])
@@ -245,7 +249,10 @@ public class HiddenColumns
        * new range overlaps existing, or is contiguous preceding it - adjust
        * start column
        */
+      int oldstart = region[0];
       region[0] = Math.min(region[0], start);
+      numColumns += oldstart - region[0]; // new columns are between old and
+                                              // adjusted starts
       added = true;
     }
     else if (start <= region[1] + 1)
@@ -254,32 +261,55 @@ public class HiddenColumns
        * new range overlaps existing, or is contiguous following it - adjust
        * start and end columns
        */
-      region[0] = Math.min(region[0], start);
-      region[1] = Math.max(region[1], end);
+      insertRangeAtOverlap(i, start, end, region);
+      added = true;
+    }
+    return added;
+  }
 
-      /*
-       * also update or remove any subsequent ranges 
-       * that are overlapped
-       */
-      while (i < hiddenColumns.size() - 1)
+  /**
+   * Insert a range whose start position overlaps an existing region and/or is
+   * contiguous to the right of the region
+   * 
+   * @param i
+   *          index to insert at
+   * @param start
+   *          start of range to insert
+   * @param end
+   *          end of range to insert
+   * @param region
+   *          the overlapped/continued region
+   */
+  private void insertRangeAtOverlap(int i, int start, int end, int[] region)
+  {
+    int oldstart = region[0];
+    int oldend = region[1];
+    region[0] = Math.min(region[0], start);
+    region[1] = Math.max(region[1], end);
+
+    numColumns += oldstart - region[0];
+
+    /*
+     * also update or remove any subsequent ranges 
+     * that are overlapped
+     */
+    int endi = i;
+    while (endi < hiddenColumns.size() - 1)
+    {
+      int[] nextRegion = hiddenColumns.get(endi + 1);
+      if (nextRegion[0] > end + 1)
       {
-        int[] nextRegion = hiddenColumns.get(i + 1);
-        if (nextRegion[0] > end + 1)
-        {
-          /*
-           * gap to next hidden range - no more to update
-           */
-          break;
-        }
-        region[1] = Math.max(nextRegion[1], end);
-
-        // in theory this is faster than hiddenColumns.remove(i+1)
-        // benchmarking results a bit ambivalent
-        hiddenColumns.subList(i + 1, i + 2).clear();
+        /*
+         * gap to next hidden range - no more to update
+         */
+        break;
       }
-      added = true;
+      numColumns -= nextRegion[1] - nextRegion[0] + 1;
+      region[1] = Math.max(nextRegion[1], end);
+      endi++;
     }
-    return added;
+    numColumns += region[1] - oldend;
+    hiddenColumns.subList(i + 1, endi + 1).clear();
   }
 
   /**
@@ -296,8 +326,8 @@ public class HiddenColumns
       {
         hideColumns(r[0], r[1]);
       }
-      cursor.resetCursor(hiddenColumns);
-      numColumns = 0;
+      cursor = new HiddenColumnsCursor(hiddenColumns);
+
     } finally
     {
       LOCK.writeLock().unlock();
@@ -321,7 +351,7 @@ public class HiddenColumns
         }
       }
       hiddenColumns.clear();
-      cursor.resetCursor(hiddenColumns);
+      cursor = new HiddenColumnsCursor(hiddenColumns);
       numColumns = 0;
 
     } finally
@@ -347,7 +377,7 @@ public class HiddenColumns
 
       if (!hiddenColumns.isEmpty())
       {
-        int regionIndex = cursor.findRegionForColumn(start)
+        int regionIndex = cursor.findRegionForColumn(start, false)
                 .getRegionIndex();
 
         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
@@ -363,17 +393,7 @@ public class HiddenColumns
             }
             int colsToRemove = region[1] - region[0] + 1;
             hiddenColumns.remove(regionIndex);
-
-            if (hiddenColumns.isEmpty())
-            {
-              hiddenColumns.clear();
-              numColumns = 0;
-            }
-            else
-            {
-              numColumns -= colsToRemove;
-            }
-            cursor.updateForDeletedRegion(hiddenColumns);
+            numColumns -= colsToRemove;
           }
         }
       }
@@ -383,15 +403,6 @@ public class HiddenColumns
     }
   }
 
-
-
-
-  /*
-   * Methods which only need read access to the hidden columns collection. 
-   * These methods should use a readLock to prevent other threads changing
-   * the hidden columns collection while it is in use.
-   */
-
   /**
    * Output regions data as a string. String is in the format:
    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
@@ -438,28 +449,7 @@ public class HiddenColumns
    */
   public int getSize()
   {
-    try
-    {
-      LOCK.readLock().lock();
-
-      if (numColumns == 0)
-      {
-        // numColumns is out of date, so recalculate
-        int size = 0;
-
-        for (int[] range : hiddenColumns)
-        {
-          size += range[1] - range[0] + 1;
-        }
-
-        numColumns = size;
-      }
-
-      return numColumns;
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
+    return numColumns;
   }
 
   /**
@@ -479,6 +469,10 @@ public class HiddenColumns
     }
   }
 
+  /**
+   * Answers true if obj is an instance of HiddenColumns, and holds the same
+   * array of start-end column ranges as this, else answers false
+   */
   @Override
   public boolean equals(Object obj)
   {
@@ -534,7 +528,8 @@ public class HiddenColumns
 
       if (!hiddenColumns.isEmpty())
       {
-        result += cursor.findRegionForVisColumn(column).getHiddenSoFar();
+        result += cursor.findRegionForColumn(column, true)
+                .getHiddenSoFar();
       }
 
       return result;
@@ -564,7 +559,7 @@ public class HiddenColumns
       if (!hiddenColumns.isEmpty())
       {
         HiddenCursorPosition cursorPos = cursor
-                .findRegionForColumn(hiddenColumn);
+                .findRegionForColumn(hiddenColumn, false);
         int index = cursorPos.getRegionIndex();
         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
     
@@ -639,15 +634,22 @@ public class HiddenColumns
    * @return the index of the next hidden column, or alPos if there is no next
    *         hidden column
    */
-  public int getHiddenBoundaryRight(int alPos)
+  public int getNextHiddenBoundary(boolean left, int alPos)
   {
     try
     {
       LOCK.readLock().lock();
       if (!hiddenColumns.isEmpty())
       {
-        int index = cursor.findRegionForColumn(alPos).getRegionIndex();
-        if (index < hiddenColumns.size())
+        int index = cursor.findRegionForColumn(alPos, false)
+                .getRegionIndex();
+
+        if (left && index > 0)
+        {
+          int[] region = hiddenColumns.get(index - 1);
+          return region[1];
+        }
+        else if (!left && index < hiddenColumns.size())
         {
           int[] region = hiddenColumns.get(index);
           if (alPos < region[0])
@@ -672,38 +674,6 @@ public class HiddenColumns
   }
 
   /**
-   * This method returns the leftmost limit of a region of an alignment with
-   * hidden columns. In otherwords, the previous hidden column.
-   * 
-   * @param alPos
-   *          the absolute (visible) alignmentPosition to find the previous
-   *          hidden column for
-   */
-  public int getHiddenBoundaryLeft(int alPos)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-
-      if (!hiddenColumns.isEmpty())
-      {
-        int index = cursor.findRegionForColumn(alPos).getRegionIndex();
-
-        if (index > 0)
-        {
-          int[] region = hiddenColumns.get(index - 1);
-          return region[1];
-        }
-      }
-      return alPos;
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
-
-  /**
    * Answers if a column in the alignment is visible
    * 
    * @param column
@@ -716,155 +686,29 @@ public class HiddenColumns
     {
       LOCK.readLock().lock();
 
-      int regionindex = cursor.findRegionForColumn(column).getRegionIndex();
-      if (regionindex > -1 && regionindex < hiddenColumns.size())
-      {
-        int[] region = hiddenColumns.get(regionindex);
-        // already know that column <= region[1] as cursor returns containing
-        // region or region to right
-        if (column >= region[0])
-        {
-          return false;
-        }
-      }
-      return true;
-
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
-  /**
-   * Get the visible sections of a set of sequences
-   * 
-   * @param start
-   *          sequence position to start from
-   * @param end
-   *          sequence position to end at
-   * @param seqs
-   *          an array of sequences
-   * @return an array of strings encoding the visible parts of each sequence
-   */
-  public String[] getVisibleSequenceStrings(int start, int end,
-          SequenceI[] seqs)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      int iSize = seqs.length;
-      String[] selections = new String[iSize];
       if (!hiddenColumns.isEmpty())
       {
-        for (int i = 0; i < iSize; i++)
+        int regionindex = cursor.findRegionForColumn(column, false)
+                .getRegionIndex();
+        if (regionindex > -1 && regionindex < hiddenColumns.size())
         {
-          StringBuilder visibleSeq = new StringBuilder();
-
-          Iterator<int[]> blocks = new VisibleContigsIterator(start,
-                  end + 1, hiddenColumns);
-
-          while (blocks.hasNext())
+          int[] region = hiddenColumns.get(regionindex);
+          // already know that column <= region[1] as cursor returns containing
+          // region or region to right
+          if (column >= region[0])
           {
-            int[] block = blocks.next();
-            if (blocks.hasNext())
-            {
-              visibleSeq
-                      .append(seqs[i].getSequence(block[0], block[1] + 1));
-            }
-            else
-            {
-              visibleSeq
-                      .append(seqs[i].getSequence(block[0], block[1]));
-            }
+            return false;
           }
-
-          selections[i] = visibleSeq.toString();
-        }
-      }
-      else
-      {
-        for (int i = 0; i < iSize; i++)
-        {
-          selections[i] = seqs[i].getSequenceAsString(start, end);
-        }
-      }
-
-      return selections;
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
-  /**
-   * Locate the first position visible for this sequence. If seq isn't visible
-   * then return the position of the left side of the hidden boundary region.
-   * 
-   * @param seq
-   *          sequence to find position for
-   * @return visible start position
-   */
-  public int locateVisibleStartOfSequence(SequenceI seq)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      int start = 0;
-
-      if (hiddenColumns.isEmpty())
-      {
-        return seq.findIndex(seq.getStart()) - 1;
-      }
-
-      // Simply walk along the sequence whilst watching for hidden column
-      // boundaries
-      Iterator<int[]> regions = hiddenColumns.iterator();
-      int hideStart = seq.getLength();
-      int hideEnd = -1;
-      int visPrev = 0;
-      int visNext = 0;
-      boolean foundStart = false;
-
-      // step through the non-gapped positions of the sequence
-      for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
-      {
-        // get alignment position of this residue in the sequence
-        int p = seq.findIndex(i) - 1;
-
-        // update hidden region start/end
-        while (hideEnd < p && regions.hasNext())
-        {
-          int[] region = regions.next();
-          visPrev = visNext;
-          visNext += region[0] - visPrev;
-          hideStart = region[0];
-          hideEnd = region[1];
-        }
-        if (hideEnd < p)
-        {
-          hideStart = seq.getLength();
-        }
-        // update visible boundary for sequence
-        if (p < hideStart)
-        {
-          start = p;
-          foundStart = true;
         }
       }
+      return true;
 
-      if (foundStart)
-      {
-        return absoluteToVisibleColumn(start);
-      }
-      // otherwise, sequence was completely hidden
-      return visPrev;
     } finally
     {
       LOCK.readLock().unlock();
     }
   }
 
-
   /**
    * 
    * @return true if there are columns hidden
@@ -911,6 +755,7 @@ public class HiddenColumns
     {
       LOCK.readLock().lock();
       int hashCode = 1;
+
       for (int[] hidden : hiddenColumns)
       {
         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
@@ -931,18 +776,41 @@ public class HiddenColumns
    */
   public void hideColumns(BitSet inserts)
   {
+    hideColumns(inserts, 0, inserts.length() - 1);
+  }
+
+  /**
+   * Hide columns corresponding to the marked bits, within the range
+   * [start,end]. Entries in tohide which are outside [start,end] are ignored.
+   * 
+   * @param tohide
+   *          columns mapped to bits starting from zero
+   * @param start
+   *          start of range to hide columns within
+   * @param end
+   *          end of range to hide columns within
+   */
+  private void hideColumns(BitSet tohide, int start, int end)
+  {
     try
     {
       LOCK.writeLock().lock();
-      for (int firstSet = inserts
-              .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
+      for (int firstSet = tohide
+              .nextSetBit(start), lastSet = start; firstSet >= start
+                      && lastSet <= end; firstSet = tohide
                       .nextSetBit(lastSet))
       {
-        lastSet = inserts.nextClearBit(firstSet);
-        hideColumns(firstSet, lastSet - 1);
+        lastSet = tohide.nextClearBit(firstSet);
+        if (lastSet <= end)
+        {
+          hideColumns(firstSet, lastSet - 1);
+        }
+        else if (firstSet <= end)
+        {
+          hideColumns(firstSet, end);
+        }
       }
-      cursor.resetCursor(hiddenColumns);
-      numColumns = 0;
+      cursor = new HiddenColumnsCursor(hiddenColumns);
     } finally
     {
       LOCK.writeLock().unlock();
@@ -952,6 +820,7 @@ public class HiddenColumns
   /**
    * Hide columns corresponding to the marked bits, within the range
    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
+   * NB Existing entries in [start,end] are cleared.
    * 
    * @param tohide
    *          columns mapped to bits starting from zero
@@ -960,19 +829,10 @@ public class HiddenColumns
    * @param end
    *          end of range to hide columns within
    */
-  public void hideColumns(BitSet tohide, int start, int end)
+  public void clearAndHideColumns(BitSet tohide, int start, int end)
   {
     clearHiddenColumnsInRange(start, end);
-
-    // make sure only bits between start and end are set
-    if (!tohide.isEmpty())
-    {
-      tohide.clear(0, start);
-      tohide.clear(Math.min(end + 1, tohide.length() + 1),
-              tohide.length() + 1);
-    }
-
-    hideColumns(tohide);
+    hideColumns(tohide, start, end);
   }
 
   /**
@@ -991,9 +851,8 @@ public class HiddenColumns
       
       if (!hiddenColumns.isEmpty())
       {
-        HiddenCursorPosition pos = cursor.findRegionForColumn(start);
+        HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
         int index = pos.getRegionIndex();
-        int startindex = index; // first index in hiddenColumns to remove
 
         if (index != -1 && index != hiddenColumns.size())
         {
@@ -1003,30 +862,35 @@ public class HiddenColumns
           if (region[0] < start && region[1] >= start)
           {
             // region contains start, truncate so that it ends just before start
+            numColumns -= region[1] - start + 1;
             region[1] = start - 1;
-            startindex++;
+            index++;
           }
-        }
-
-        pos = cursor.findRegionForColumn(end);
-        index = pos.getRegionIndex();
-        int endindex = index - 1; // last index in hiddenColumns to remove
 
-        if (index != -1 && index != hiddenColumns.size())
-        {
-          // regionIndex is the region which either contains end
-          // or lies to the right of end
-          int[] region = hiddenColumns.get(index);
-          if (region[0] <= end && region[1] > end)
+          int endi = index;
+          while (endi < hiddenColumns.size())
           {
-            // region contains end, truncate so that it starts just after end
-            region[0] = end + 1;
+            region = hiddenColumns.get(endi);
+
+            if (region[1] > end)
+            {
+              if (region[0] <= end)
+              {
+                // region contains end, truncate so it starts just after end
+                numColumns -= end - region[0] + 1;
+                region[0] = end + 1;
+              }
+              break;
+            }
+
+            numColumns -= region[1] - region[0] + 1;
+            endi++;
           }
+          hiddenColumns.subList(index, endi).clear();
+
         }
 
-        hiddenColumns.subList(startindex, endindex + 1).clear();
-        cursor.resetCursor(hiddenColumns);
-        numColumns = 0;
+        cursor = new HiddenColumnsCursor(hiddenColumns);
       }
     } finally
     {
@@ -1118,7 +982,7 @@ public class HiddenColumns
       if (!hiddenColumns.isEmpty())
       {
         // look for a region ending just before adjres
-        int regionindex = cursor.findRegionForColumn(adjres - 1)
+        int regionindex = cursor.findRegionForColumn(adjres - 1, false)
                 .getRegionIndex();
         if (regionindex < hiddenColumns.size()
                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
@@ -1148,7 +1012,7 @@ public class HiddenColumns
     try
     {
       LOCK.readLock().lock();
-      return new HiddenColsIterator(hiddenColumns);
+      return new RangeIterator(hiddenColumns);
     } finally
     {
       LOCK.readLock().unlock();
@@ -1169,7 +1033,7 @@ public class HiddenColumns
     try
     {
       LOCK.readLock().lock();
-      return new HiddenColsIterator(start, end, hiddenColumns);
+      return new RangeIterator(start, end, hiddenColumns);
     } finally
     {
       LOCK.readLock().unlock();
@@ -1185,7 +1049,7 @@ public class HiddenColumns
    * @param end
    *          position to end at (inclusive, visible column position)
    */
-  public Iterator<Integer> getBoundedStartIterator(int start, int end)
+  public Iterator<Integer> getStartRegionIterator(int start, int end)
   {
     try
     {
@@ -1200,9 +1064,9 @@ public class HiddenColumns
       // region we'll get the cursor pointing to the region before, which is
       // what we want
       HiddenCursorPosition pos = cursor
-              .findRegionForColumn(absoluteStart - 1);
+              .findRegionForColumn(absoluteStart - 1, false);
 
-      return new BoundedStartRegionIterator(pos, start, end,
+      return new StartRegionIterator(pos, start, end,
               hiddenColumns);
     } finally
     {
@@ -1224,7 +1088,8 @@ public class HiddenColumns
     try
     {
       LOCK.readLock().lock();
-      return new VisibleColsIterator(start, end, hiddenColumns);
+      return new RangeElementsIterator(
+              new VisibleContigsIterator(start, end + 1, hiddenColumns));
     } finally
     {
       LOCK.readLock().unlock();