JAL-2174 bug fixes + refactoring to allow + unit tests
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 19 Aug 2016 15:52:22 +0000 (16:52 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 19 Aug 2016 15:52:22 +0000 (16:52 +0100)
src/jalview/controller/AlignViewController.java
src/jalview/gui/FeatureSettings.java
test/jalview/controller/AlignViewControllerTest.java [new file with mode: 0644]

index 74a5235..f966072 100644 (file)
@@ -169,57 +169,192 @@ public class AlignViewController implements AlignViewControllerI
     // JBPNote this routine could also mark rows, not just columns.
     // need a decent query structure to allow all types of feature searches
     BitSet bs = new BitSet();
-    int alw, alStart;
-    SequenceCollectionI sqcol = (viewport.getSelectionGroup() == null ? viewport
-            .getAlignment() : viewport.getSelectionGroup());
-    alStart = sqcol.getStartRes();
-    alw = sqcol.getEndRes() + 1;
+    SequenceCollectionI sqcol = (viewport.getSelectionGroup() == null || extendCurrent) ? viewport
+            .getAlignment() : viewport.getSelectionGroup();
+
+    int nseq = findColumnsWithFeature(featureType, sqcol, bs);
+
+    ColumnSelection cs = viewport.getColumnSelection();
+    if (cs == null)
+    {
+      cs = new ColumnSelection();
+    }
+
+    if (bs.cardinality() > 0 || invert)
+    {
+      boolean changed = selectMarkedColumns(cs, invert, extendCurrent,
+              toggle, bs, sqcol.getStartRes(), sqcol.getEndRes());
+      if (changed)
+      {
+        viewport.setColumnSelection(cs);
+        alignPanel.paintAlignment(true);
+        int columnCount = invert ? (sqcol.getEndRes() - sqcol.getStartRes() + 1)
+                - bs.cardinality() : bs.cardinality();
+        avcg.setStatus(MessageManager.formatMessage(
+                "label.view_controller_toggled_marked",
+                new String[] {
+                    MessageManager.getString(toggle ? "label.toggled"
+                            : "label.marked"),
+                    String.valueOf(columnCount),
+                    MessageManager
+                            .getString(invert ? "label.not_containing"
+                                    : "label.containing"),
+                    featureType, Integer.valueOf(nseq).toString() }));
+        return true;
+      }
+    }
+    else
+    {
+      avcg.setStatus(MessageManager.formatMessage(
+              "label.no_feature_of_type_found",
+              new String[] { featureType }));
+      if (!extendCurrent && cs != null)
+      {
+        cs.clear();
+        alignPanel.paintAlignment(true);
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Updates the column selection depending on the parameters, and returns true
+   * if any change was made to the selection.
+   * 
+   * @param columnSelection
+   *          the current column selection
+   * @param invert
+   *          if true, deselect marked columns and select unmarked
+   * @param extendCurrent
+   *          if true, extend rather than replacing the current column selection
+   * @param toggle
+   *          if true, toggle the selection state of marked columns
+   * @param markedColumns
+   *          a set identify marked columns
+   * @param startCol
+   *          the first column of the range to operate over
+   * @param endCol
+   *          the last column of the range to operate over
+   * @return
+   */
+  static boolean selectMarkedColumns(ColumnSelection columnSelection,
+          boolean invert, boolean extendCurrent, boolean toggle,
+          BitSet markedColumns, int startCol, int endCol)
+  {
+    boolean changed = false;
+    if (!extendCurrent && !toggle)
+    {
+      changed = !columnSelection.isEmpty();
+      columnSelection.clear();
+    }
+    if (invert)
+    {
+      // invert only in the currently selected sequence region
+      int i = markedColumns.nextClearBit(startCol);
+      int ibs = markedColumns.nextSetBit(startCol);
+      while (i >= startCol && i <= endCol)
+      {
+        if (ibs < 0 || i < ibs)
+        {
+          changed = true;
+          if (toggle && columnSelection.contains(i))
+          {
+            columnSelection.removeElement(i++);
+          }
+          else
+          {
+            columnSelection.addElement(i++);
+          }
+        }
+        else
+        {
+          i = markedColumns.nextClearBit(ibs);
+          ibs = markedColumns.nextSetBit(i);
+        }
+      }
+    }
+    else
+    {
+      int i = markedColumns.nextSetBit(startCol);
+      while (i >= startCol && i <= endCol)
+      {
+        changed = true;
+        if (toggle && columnSelection.contains(i))
+        {
+          columnSelection.removeElement(i);
+        }
+        else
+        {
+          columnSelection.addElement(i);
+        }
+        i = markedColumns.nextSetBit(i + 1);
+      }
+    }
+    return changed;
+  }
+
+  /**
+   * Sets a bit in the BitSet for each column in the sequence collection which
+   * includes the specified feature type. Returns the number of sequences which
+   * have the feature in the selected range.
+   * 
+   * @param featureType
+   * @param sqcol
+   * @param bs
+   * @return
+   */
+  static int findColumnsWithFeature(String featureType,
+          SequenceCollectionI sqcol,
+          BitSet bs)
+  {
+    final int startPosition = sqcol.getStartRes() + 1; // converted to base 1
+    final int endPosition = sqcol.getEndRes() + 1;
     List<SequenceI> seqs = sqcol.getSequences();
     int nseq = 0;
     for (SequenceI sq : seqs)
     {
-      int tfeat = 0;
+      boolean sequenceHasFeature = false;
       if (sq != null)
       {
-        SequenceFeature[] sf = sq.getSequenceFeatures();
-        if (sf != null)
+        SequenceFeature[] sfs = sq.getSequenceFeatures();
+        if (sfs != null)
         {
           int ist = sq.findIndex(sq.getStart());
           int iend = sq.findIndex(sq.getEnd());
-          if (iend < alStart || ist > alw)
+          if (iend < startPosition || ist > endPosition)
           {
             // sequence not in region
             continue;
           }
-          for (SequenceFeature sfpos : sf)
+          for (SequenceFeature sf : sfs)
           {
-            // future functionalty - featureType == null means mark columns
+            // future functionality - featureType == null means mark columns
             // containing all displayed features
-            if (sfpos != null && (featureType.equals(sfpos.getType())))
+            if (sf != null && (featureType.equals(sf.getType())))
             {
-              tfeat++;
               // optimisation - could consider 'spos,apos' like cursor argument
               // - findIndex wastes time by starting from first character and
               // counting
 
-              int i = sq.findIndex(sfpos.getBegin());
-              int j = sq.findIndex(sfpos.getEnd());
-              if (j < alStart || i > alw)
+              int i = sq.findIndex(sf.getBegin());
+              int j = sq.findIndex(sf.getEnd());
+              if (j < startPosition || i > endPosition)
               {
                 // feature is outside selected region
                 continue;
               }
-              if (i < alStart)
+              sequenceHasFeature = true;
+              if (i < startPosition)
               {
-                i = alStart;
+                i = startPosition;
               }
               if (i < ist)
               {
                 i = ist;
               }
-              if (j > alw)
+              if (j > endPosition)
               {
-                j = alw;
+                j = endPosition;
               }
               for (; i <= j; i++)
               {
@@ -229,99 +364,13 @@ public class AlignViewController implements AlignViewControllerI
           }
         }
 
-        if (tfeat > 0)
+        if (sequenceHasFeature)
         {
           nseq++;
         }
       }
     }
-    ColumnSelection cs = viewport.getColumnSelection();
-    if (bs.cardinality() > 0 || invert)
-    {
-      boolean changed = false;
-      if (cs == null)
-      {
-        cs = new ColumnSelection();
-      }
-      else
-      {
-        if (!extendCurrent)
-        {
-          changed = !cs.isEmpty();
-          cs.clear();
-        }
-      }
-      if (invert)
-      {
-        // invert only in the currently selected sequence region
-        for (int i = bs.nextClearBit(alStart), ibs = bs.nextSetBit(alStart); i >= alStart
-                && i < (alw);)
-        {
-          if (ibs < 0 || i < ibs)
-          {
-            changed = true;
-            if (toggle && cs.contains(i))
-            {
-              cs.removeElement(i++);
-            }
-            else
-            {
-              cs.addElement(i++);
-            }
-          }
-          else
-          {
-            i = bs.nextClearBit(ibs);
-            ibs = bs.nextSetBit(i);
-          }
-        }
-      }
-      else
-      {
-        for (int i = bs.nextSetBit(alStart); i >= alStart; i = bs
-                .nextSetBit(i + 1))
-        {
-          changed = true;
-          if (toggle && cs.contains(i))
-          {
-            cs.removeElement(i);
-          }
-          else
-          {
-            cs.addElement(i);
-          }
-        }
-      }
-      if (changed)
-      {
-        viewport.setColumnSelection(cs);
-        alignPanel.paintAlignment(true);
-        avcg.setStatus(MessageManager.formatMessage(
-                "label.view_controller_toggled_marked",
-                new String[] {
-                    MessageManager.getString(toggle ? "label.toggled"
-                            : "label.marked"),
-                    String.valueOf(invert ? alw - alStart
-                            - bs.cardinality() : bs.cardinality()),
-                    MessageManager
-                            .getString(invert ? "label.not_containing"
-                                    : "label.containing"),
-                    featureType, Integer.valueOf(nseq).toString() }));
-        return true;
-      }
-    }
-    else
-    {
-      avcg.setStatus(MessageManager.formatMessage(
-              "label.no_feature_of_type_found",
-              new String[] { featureType }));
-      if (!extendCurrent && cs != null)
-      {
-        cs.clear();
-        alignPanel.paintAlignment(true);
-      }
-    }
-    return false;
+    return nseq;
   }
 
   @Override
index 742ff8d..3b8ce37 100644 (file)
@@ -174,7 +174,8 @@ public class FeatureSettings extends JPanel implements
       public void mousePressed(MouseEvent evt)
       {
         selectedRow = table.rowAtPoint(evt.getPoint());
-        if (SwingUtilities.isRightMouseButton(evt))
+        boolean ctrlDown = Platform.isControlDown(evt);
+        if (SwingUtilities.isRightMouseButton(evt) && !ctrlDown)
         {
           popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
                   table.getValueAt(selectedRow, 1), fr.getMinMax(),
@@ -183,9 +184,8 @@ public class FeatureSettings extends JPanel implements
         else if (evt.getClickCount() == 2)
         {
           boolean invertSelection = evt.isAltDown();
-          boolean ctrlDown = Platform.isControlDown(evt);
           boolean toggleSelection = ctrlDown;
-          boolean extendSelection = evt.isShiftDown() || ctrlDown;
+          boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
                   invertSelection, extendSelection, toggleSelection,
                   (String) table.getValueAt(selectedRow, 0));
diff --git a/test/jalview/controller/AlignViewControllerTest.java b/test/jalview/controller/AlignViewControllerTest.java
new file mode 100644 (file)
index 0000000..d85b68f
--- /dev/null
@@ -0,0 +1,198 @@
+package jalview.controller;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+
+import java.util.BitSet;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class AlignViewControllerTest
+{
+  @Test(groups = "Functional")
+  public void testFindColumnsWithFeature()
+  {
+    SequenceI seq1 = new Sequence("seq1", "aMMMaaaaaaaaaaaaaaaa");
+    SequenceI seq2 = new Sequence("seq2", "aaaMMMMMMMaaaaaaaaaa");
+    SequenceI seq3 = new Sequence("seq3", "aaaaaaaaaaMMMMMaaaaa");
+    SequenceI seq4 = new Sequence("seq3", "aaaaaaaaaaaaaaaaaaaa");
+
+    /*
+     * features start/end are base 1
+     */
+    seq1.addSequenceFeature(new SequenceFeature("Metal", "desc", 2, 4, 0f,
+            null));
+    seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
+            null));
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f,
+            null));
+    seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
+            0f, null));
+
+    /*
+     * select the first three columns --> seq1 2-3
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(0); // base 0
+    sg.setEndRes(2);
+    sg.addSequence(seq1, false);
+    sg.addSequence(seq2, false);
+    sg.addSequence(seq3, false);
+    sg.addSequence(seq4, false);
+
+    BitSet bs = new BitSet();
+    int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
+            bs);
+    assertEquals(1, seqCount);
+    assertEquals(2, bs.cardinality());
+    assertTrue(bs.get(1));
+    assertTrue(bs.get(2));
+    
+    /*
+     * select the first four columns: seq1 2:4, seq2 4:4
+     */
+    sg.setEndRes(3);
+    bs.clear();
+    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
+            bs);
+    assertEquals(2, seqCount);
+    assertEquals(3, bs.cardinality());
+    assertTrue(bs.get(1));
+    assertTrue(bs.get(2));
+    assertTrue(bs.get(3));
+
+    /*
+     * select column 11: seq3 only
+     */
+    sg.setStartRes(10);
+    sg.setEndRes(10);
+    bs.clear();
+    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    assertEquals(1, seqCount);
+    assertEquals(1, bs.cardinality());
+    assertTrue(bs.get(10));
+
+    /*
+     * select columns 16-20: no Metal feature
+     */
+    sg.setStartRes(15);
+    sg.setEndRes(19);
+    bs.clear();
+    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    assertEquals(0, seqCount);
+    assertEquals(0, bs.cardinality());
+
+    /*
+     * look for a feature that isn't there
+     */
+    sg.setStartRes(0);
+    sg.setEndRes(19);
+    bs.clear();
+    seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs);
+    assertEquals(0, seqCount);
+    assertEquals(0, bs.cardinality());
+  }
+
+  @Test(groups = "Functional")
+  public void testSelectMarkedColumns_invert()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(5); // this will be cleared
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+
+    /*
+     * inverted selection of {3, 6} should select {4, 5, 7, 8}
+     */
+    assertTrue(AlignViewController.selectMarkedColumns(cs, true, false,
+            false, toMark, 3, 8));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(4));
+    assertTrue(selected.contains(5));
+    assertTrue(selected.contains(7));
+    assertTrue(selected.contains(8));
+  }
+
+  @Test(groups = "Functional")
+  public void testSelectMarkedColumns()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(5); // this will be cleared
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+  
+    assertTrue(AlignViewController.selectMarkedColumns(cs, false, false,
+            false, toMark, 3, 8));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(2, selected.size());
+    assertTrue(selected.contains(3));
+    assertTrue(selected.contains(6));
+  }
+
+  @Test(groups = "Functional")
+  public void testSelectMarkedColumns_extend()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(1);
+    cs.addElement(5);
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+  
+    /*
+     * extending selection of {3, 6} should leave {1, 3, 5, 6} selected
+     */
+    assertTrue(AlignViewController.selectMarkedColumns(cs, false, true,
+            false, toMark, 3, 8));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(1));
+    assertTrue(selected.contains(3));
+    assertTrue(selected.contains(5));
+    assertTrue(selected.contains(6));
+  }
+
+  @Test(groups = "Functional")
+  public void testSelectMarkedColumns_toggle()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(1); // outside change range
+    cs.addElement(3);
+    cs.addElement(4);
+    cs.addElement(10); // outside change range
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+  
+    /*
+     * toggling state of {3, 6} should leave {1, 4, 6, 10} selected
+     */
+    assertTrue(AlignViewController.selectMarkedColumns(cs, false, false,
+            true, toMark, 3, 8));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(1));
+    assertTrue(selected.contains(4));
+    assertTrue(selected.contains(6));
+    assertTrue(selected.contains(10));
+  }
+  // TODO testSelectMarkedColumns with combinations of invert/extend/toggle set
+}