/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
package jalview.datamodel;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import jalview.analysis.AlignmentGenerator;
import jalview.gui.JvOptionPane;
import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.ThresholdType;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class ColumnSelectionTest
{
@BeforeClass(alwaysRun = true)
public void setUpJvOptionPane()
{
JvOptionPane.setInteractiveMode(false);
JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
}
@Test(groups = { "Functional" })
public void testAddElement()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(2);
cs.addElement(5);
cs.addElement(3);
cs.addElement(5); // ignored
List sel = cs.getSelected();
assertEquals("[2, 5, 3]", sel.toString());
}
@Test(groups = { "Functional" })
public void testSetElementsFrom()
{
ColumnSelection fromcs = new ColumnSelection();
ColumnSelection tocs = new ColumnSelection();
HiddenColumns hidden = new HiddenColumns();
fromcs.addElement(2);
fromcs.addElement(3);
fromcs.addElement(5);
tocs.setElementsFrom(fromcs, hidden);
assertTrue(tocs.equals(fromcs));
hidden.hideColumns(4, 6);
tocs.setElementsFrom(fromcs, hidden);
// expect cols 2 and 3 to be selected but not 5
ColumnSelection expectcs = new ColumnSelection();
expectcs.addElement(2);
expectcs.addElement(3);
assertTrue(tocs.equals(expectcs));
}
/**
* Test the remove method - in particular to verify that remove(int i) removes
* the element whose value is i, _NOT_ the i'th element.
*/
@Test(groups = { "Functional" })
public void testRemoveElement()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(2);
cs.addElement(5);
// removing elements not in the list has no effect
cs.removeElement(0);
cs.removeElement(1);
List sel = cs.getSelected();
assertEquals(2, sel.size());
assertEquals(Integer.valueOf(2), sel.get(0));
assertEquals(Integer.valueOf(5), sel.get(1));
// removing an element in the list removes it
cs.removeElement(2);
// ...and also from the read-only view
assertEquals(1, sel.size());
sel = cs.getSelected();
assertEquals(1, sel.size());
assertEquals(Integer.valueOf(5), sel.get(0));
}
/**
* Test the method that hides a specified column including any adjacent
* selected columns. This is a convenience method for the case where multiple
* column regions are selected and then hidden using menu option View | Hide |
* Selected Columns.
*/
@Test(groups = { "Functional" })
public void testHideColumns_withSelection()
{
// create random alignment
AlignmentGenerator gen = new AlignmentGenerator(false);
AlignmentI al = gen.generate(50, 20, 123, 5, 5);
ColumnSelection cs = new ColumnSelection();
// select columns 4-6
cs.addElement(4);
cs.addElement(5);
cs.addElement(6);
// hide column 5 (and adjacent):
cs.hideSelectedColumns(5, al.getHiddenColumns());
// 4,5,6 now hidden:
Iterator regions = al.getHiddenColumns().iterator();
assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
assertEquals("[4, 6]", Arrays.toString(regions.next()));
// none now selected:
assertTrue(cs.getSelected().isEmpty());
// repeat, hiding column 4 (5 and 6)
al = gen.generate(50, 20, 123, 5, 5);
cs = new ColumnSelection();
cs.addElement(4);
cs.addElement(5);
cs.addElement(6);
cs.hideSelectedColumns(4, al.getHiddenColumns());
regions = al.getHiddenColumns().iterator();
assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
assertEquals("[4, 6]", Arrays.toString(regions.next()));
assertTrue(cs.getSelected().isEmpty());
// repeat, hiding column (4, 5 and) 6
al = gen.generate(50, 20, 123, 5, 5);
cs = new ColumnSelection();
cs.addElement(4);
cs.addElement(5);
cs.addElement(6);
cs.hideSelectedColumns(6, al.getHiddenColumns());
regions = al.getHiddenColumns().iterator();
assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
assertEquals("[4, 6]", Arrays.toString(regions.next()));
assertTrue(cs.getSelected().isEmpty());
// repeat, with _only_ adjacent columns selected
al = gen.generate(50, 20, 123, 5, 5);
cs = new ColumnSelection();
cs.addElement(4);
cs.addElement(6);
cs.hideSelectedColumns(5, al.getHiddenColumns());
regions = al.getHiddenColumns().iterator();
assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
assertEquals("[4, 6]", Arrays.toString(regions.next()));
assertTrue(cs.getSelected().isEmpty());
}
/**
* Test the method that hides all (possibly disjoint) selected column ranges
*/
@Test(groups = { "Functional" })
public void testHideSelectedColumns()
{
// create random alignment
AlignmentGenerator gen = new AlignmentGenerator(false);
AlignmentI al = gen.generate(50, 20, 123, 5, 5);
ColumnSelection cs = new ColumnSelection();
int[] sel = { 2, 3, 4, 7, 8, 9, 20, 21, 22 };
for (int col : sel)
{
cs.addElement(col);
}
HiddenColumns cols = al.getHiddenColumns();
cols.hideColumns(15, 18);
cs.hideSelectedColumns(al);
assertTrue(cs.getSelected().isEmpty());
Iterator regions = cols.iterator();
assertEquals(4, cols.getNumberOfRegions());
assertEquals("[2, 4]", Arrays.toString(regions.next()));
assertEquals("[7, 9]", Arrays.toString(regions.next()));
assertEquals("[15, 18]", Arrays.toString(regions.next()));
assertEquals("[20, 22]", Arrays.toString(regions.next()));
}
/**
* Test the method that gets runs of selected columns ordered by column. If
* this fails, HideSelectedColumns may also fail
*/
@Test(groups = { "Functional" })
public void testGetSelectedRanges()
{
/*
* getSelectedRanges returns ordered columns regardless
* of the order in which they are added
*/
ColumnSelection cs = new ColumnSelection();
int[] sel = { 4, 3, 7, 21, 9, 20, 8, 22, 2 };
for (int col : sel)
{
cs.addElement(col);
}
List range;
range = cs.getSelectedRanges();
assertEquals(3, range.size());
assertEquals("[2, 4]", Arrays.toString(range.get(0)));
assertEquals("[7, 9]", Arrays.toString(range.get(1)));
assertEquals("[20, 22]", Arrays.toString(range.get(2)));
cs.addElement(0);
cs.addElement(1);
range = cs.getSelectedRanges();
assertEquals(3, range.size());
assertEquals("[0, 4]", Arrays.toString(range.get(0)));
}
@Test(groups = { "Functional" })
public void testInvertColumnSelection()
{
// create random alignment
AlignmentGenerator gen = new AlignmentGenerator(false);
AlignmentI al = gen.generate(50, 20, 123, 5, 5);
ColumnSelection cs = new ColumnSelection();
cs.addElement(4);
cs.addElement(6);
cs.addElement(8);
HiddenColumns cols = al.getHiddenColumns();
cols.hideColumns(3, 3);
cols.hideColumns(6, 6);
// invert selection from start (inclusive) to end (exclusive)
cs.invertColumnSelection(2, 9, al);
assertEquals("[2, 5, 7]", cs.getSelected().toString());
cs.invertColumnSelection(1, 9, al);
assertEquals("[1, 4, 8]", cs.getSelected().toString());
}
@Test(groups = { "Functional" })
public void testMaxColumnSelection()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(0);
cs.addElement(513);
cs.addElement(1);
assertEquals(513, cs.getMax());
cs.removeElement(513);
assertEquals(1, cs.getMax());
cs.removeElement(1);
assertEquals(0, cs.getMax());
cs.addElement(512);
cs.addElement(513);
assertEquals(513, cs.getMax());
}
@Test(groups = { "Functional" })
public void testMinColumnSelection()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(0);
cs.addElement(513);
cs.addElement(1);
assertEquals(0, cs.getMin());
cs.removeElement(0);
assertEquals(1, cs.getMin());
cs.addElement(0);
assertEquals(0, cs.getMin());
}
@Test(groups = { "Functional" })
public void testEquals()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(0);
cs.addElement(513);
cs.addElement(1);
// same selections added in a different order
ColumnSelection cs2 = new ColumnSelection();
cs2.addElement(1);
cs2.addElement(513);
cs2.addElement(0);
assertTrue(cs.equals(cs2));
assertTrue(cs.equals(cs));
assertTrue(cs2.equals(cs));
assertTrue(cs2.equals(cs2));
cs2.addElement(12);
assertFalse(cs.equals(cs2));
assertFalse(cs2.equals(cs));
cs2.removeElement(12);
assertTrue(cs.equals(cs2));
}
/*
cs2.hideSelectedColumns(88);
assertFalse(cs.equals(cs2));
/*
* unhiding a column adds it to selection!
*/
/* cs2.revealHiddenColumns(88);
assertFalse(cs.equals(cs2));
cs.addElement(88);
assertTrue(cs.equals(cs2));
*/
/**
* Test the method that returns selected columns, in the order in which they
* were added
*/
@Test(groups = { "Functional" })
public void testGetSelected()
{
ColumnSelection cs = new ColumnSelection();
int[] sel = { 4, 3, 7, 21 };
for (int col : sel)
{
cs.addElement(col);
}
List selected = cs.getSelected();
assertEquals(4, selected.size());
assertEquals("[4, 3, 7, 21]", selected.toString());
/*
* getSelected returns a read-only view of the list
* verify the view follows any changes in it
*/
cs.removeElement(7);
cs.addElement(1);
cs.removeElement(4);
assertEquals("[3, 21, 1]", selected.toString());
}
/**
* Test to verify that the list returned by getSelection cannot be modified
*/
@Test(groups = { "Functional" })
public void testGetSelected_isReadOnly()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(3);
List selected = cs.getSelected();
try
{
selected.clear();
fail("expected exception");
} catch (UnsupportedOperationException e)
{
// expected
}
try
{
selected.add(1);
fail("expected exception");
} catch (UnsupportedOperationException e)
{
// expected
}
try
{
selected.remove(3);
fail("expected exception");
} catch (UnsupportedOperationException e)
{
// expected
}
try
{
Collections.sort(selected);
fail("expected exception");
} catch (UnsupportedOperationException e)
{
// expected
}
}
/**
* Test that demonstrates a ConcurrentModificationException is thrown if you
* change the selection while iterating over it
*/
@Test(
groups = "Functional",
expectedExceptions = { ConcurrentModificationException.class })
public void testGetSelected_concurrentModification()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(0);
cs.addElement(1);
cs.addElement(2);
/*
* simulate changing the list under us (e.g. in a separate
* thread) while iterating over it -> ConcurrentModificationException
*/
List selected = cs.getSelected();
for (Integer col : selected)
{
if (col.intValue() == 0)
{
cs.removeElement(1);
}
}
}
@Test(groups = "Functional")
public void testMarkColumns()
{
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(cs.markColumns(toMark, 3, 8, false, false, false));
List selected = cs.getSelected();
assertEquals(2, selected.size());
assertTrue(selected.contains(3));
assertTrue(selected.contains(6));
}
@Test(groups = "Functional")
public void testMarkColumns_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(cs.markColumns(toMark, 3, 8, false, true, false));
List 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 testMarkColumns_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(cs.markColumns(toMark, 3, 8, true, false, false));
List 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 testMarkColumns_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(cs.markColumns(toMark, 3, 8, false, false, true));
List selected = cs.getSelected();
assertEquals(4, selected.size());
assertTrue(selected.contains(1));
assertTrue(selected.contains(4));
assertTrue(selected.contains(6));
assertTrue(selected.contains(10));
}
@Test(groups = "Functional")
public void testCopyConstructor()
{
ColumnSelection cs = new ColumnSelection();
cs.addElement(3);
cs.addElement(1);
ColumnSelection cs2 = new ColumnSelection(cs);
assertTrue(cs2.hasSelectedColumns());
// order of column selection is preserved
assertEquals("[3, 1]", cs2.getSelected().toString());
}
@Test(groups = { "Functional" })
public void testStretchGroup_expand()
{
/*
* test that emulates clicking column 4 (selected)
* and dragging right to column 5 (all base 0)
*/
ColumnSelection cs = new ColumnSelection();
cs.addElement(4);
SequenceGroup sg = new SequenceGroup();
sg.setStartRes(4);
sg.setEndRes(4);
cs.stretchGroup(5, sg, 4, 4);
assertEquals(cs.getSelected().size(), 2);
assertTrue(cs.contains(4));
assertTrue(cs.contains(5));
assertEquals(sg.getStartRes(), 4);
assertEquals(sg.getEndRes(), 5);
/*
* emulate drag right with columns 10-20 already selected
*/
cs.clear();
for (int i = 10; i <= 20; i++)
{
cs.addElement(i);
}
assertEquals(cs.getSelected().size(), 11);
sg = new SequenceGroup();
sg.setStartRes(10);
sg.setEndRes(20);
cs.stretchGroup(21, sg, 10, 20);
assertEquals(cs.getSelected().size(), 12);
assertTrue(cs.contains(10));
assertTrue(cs.contains(21));
assertEquals(sg.getStartRes(), 10);
assertEquals(sg.getEndRes(), 21);
}
@Test(groups = { "Functional" })
public void testStretchGroup_shrink()
{
/*
* emulate drag left to 19 with columns 10-20 already selected
*/
ColumnSelection cs = new ColumnSelection();
for (int i = 10; i <= 20; i++)
{
cs.addElement(i);
}
assertEquals(cs.getSelected().size(), 11);
SequenceGroup sg = new SequenceGroup();
sg.setStartRes(10);
sg.setEndRes(20);
cs.stretchGroup(19, sg, 10, 20);
assertEquals(cs.getSelected().size(), 10);
assertTrue(cs.contains(10));
assertTrue(cs.contains(19));
assertFalse(cs.contains(20));
assertEquals(sg.getStartRes(), 10);
assertEquals(sg.getEndRes(), 19);
}
@Test(groups = { "Functional" })
public void testFilterAnnotations()
{
ColumnSelection cs = new ColumnSelection();
/*
* filter with no conditions clears the selection
*/
Annotation[] anns = new Annotation[] { null };
AnnotationFilterParameter filter = new AnnotationFilterParameter();
cs.addElement(3);
int added = cs.filterAnnotations(anns, filter);
assertEquals(0, added);
assertTrue(cs.isEmpty());
/*
* select on description (regex)
*/
filter.setRegexString("w.rld");
filter.addRegexSearchField(SearchableAnnotationField.DESCRIPTION);
Annotation helix = new Annotation("(", "hello", '<', 2f);
Annotation sheet = new Annotation("(", "world", '<', 2f);
added = cs.filterAnnotations(new Annotation[] { null, helix, sheet },
filter);
assertEquals(1, added);
assertTrue(cs.contains(2));
/*
* select on label (invalid regex, exact match)
*/
filter = new AnnotationFilterParameter();
filter.setRegexString("(");
filter.addRegexSearchField(SearchableAnnotationField.DISPLAY_STRING);
added = cs.filterAnnotations(new Annotation[] { null, helix, sheet },
filter);
assertEquals(2, added);
assertTrue(cs.contains(1));
assertTrue(cs.contains(2));
/*
* select Helix (secondary structure symbol H)
*/
filter = new AnnotationFilterParameter();
filter.setFilterAlphaHelix(true);
helix = new Annotation("x", "desc", 'H', 0f);
sheet = new Annotation("x", "desc", 'E', 1f);
Annotation turn = new Annotation("x", "desc", 'S', 2f);
Annotation ann4 = new Annotation("x", "desc", 'Y', 3f);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 },
filter);
assertEquals(1, added);
assertTrue(cs.contains(1));
/*
* select Helix and Sheet (E)
*/
filter.setFilterBetaSheet(true);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(2, added);
assertTrue(cs.contains(1));
assertTrue(cs.contains(2));
/*
* select Sheet and Turn (S)
*/
filter.setFilterAlphaHelix(false);
filter.setFilterTurn(true);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(2, added);
assertTrue(cs.contains(2));
assertTrue(cs.contains(3));
/*
* select value < 2f (ann1, ann2)
*/
filter = new AnnotationFilterParameter();
filter.setThresholdType(ThresholdType.BELOW_THRESHOLD);
filter.setThresholdValue(2f);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(2, added);
assertTrue(cs.contains(1));
assertTrue(cs.contains(2));
/*
* select value > 2f (ann4 only)
*/
filter.setThresholdType(ThresholdType.ABOVE_THRESHOLD);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(1, added);
assertTrue(cs.contains(4));
/*
* select >2f or Helix
*/
filter.setFilterAlphaHelix(true);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(2, added);
assertTrue(cs.contains(1));
assertTrue(cs.contains(4));
/*
* select < 1f or Helix; one annotation matches both
* return value should only count it once
*/
filter.setThresholdType(ThresholdType.BELOW_THRESHOLD);
filter.setThresholdValue(1f);
added = cs
.filterAnnotations(new Annotation[]
{ null, helix, sheet, turn, ann4 }, filter);
assertEquals(1, added);
assertTrue(cs.contains(1));
}
}