/*
* 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.renderer.seqfeatures;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import jalview.api.AlignViewportI;
import jalview.api.FeatureColourI;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.datamodel.features.FeatureMatcher;
import jalview.datamodel.features.FeatureMatcherSet;
import jalview.datamodel.features.FeatureMatcherSetI;
import jalview.gui.AlignFrame;
import jalview.io.DataSourceType;
import jalview.io.FileLoader;
import jalview.schemes.FeatureColour;
import jalview.util.matcher.Condition;
import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.testng.annotations.Test;
public class FeatureRendererTest
{
@Test(groups = "Functional")
public void testFindAllFeatures()
{
String seqData = ">s1\nabcdef\n>s2\nabcdef\n>s3\nabcdef\n>s4\nabcdef\n";
AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
DataSourceType.PASTE);
AlignViewportI av = af.getViewport();
FeatureRenderer fr = new FeatureRenderer(av);
/*
* with no features
*/
fr.findAllFeatures(true);
assertTrue(fr.getRenderOrder().isEmpty());
assertTrue(fr.getFeatureGroups().isEmpty());
List seqs = av.getAlignment().getSequences();
// add a non-positional feature - should be ignored by FeatureRenderer
SequenceFeature sf1 = new SequenceFeature("Type", "Desc", 0, 0, 1f,
"Group");
seqs.get(0).addSequenceFeature(sf1);
fr.findAllFeatures(true);
// ? bug - types and groups added for non-positional features
List types = fr.getRenderOrder();
List groups = fr.getFeatureGroups();
assertEquals(types.size(), 0);
assertFalse(types.contains("Type"));
assertEquals(groups.size(), 0);
assertFalse(groups.contains("Group"));
// add some positional features
seqs.get(1).addSequenceFeature(
new SequenceFeature("Pfam", "Desc", 5, 9, 1f, "PfamGroup"));
seqs.get(2).addSequenceFeature(
new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
// bug in findAllFeatures - group not checked for a known feature type
seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9,
Float.NaN, "RfamGroup"));
// existing feature type with null group
seqs.get(3).addSequenceFeature(
new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
// new feature type with null group
seqs.get(3).addSequenceFeature(
new SequenceFeature("Scop", "Desc", 5, 9, Float.NaN, null));
// null value for type produces NullPointerException
fr.findAllFeatures(true);
types = fr.getRenderOrder();
groups = fr.getFeatureGroups();
assertEquals(types.size(), 3);
assertFalse(types.contains("Type"));
assertTrue(types.contains("Pfam"));
assertTrue(types.contains("Rfam"));
assertTrue(types.contains("Scop"));
assertEquals(groups.size(), 2);
assertFalse(groups.contains("Group"));
assertTrue(groups.contains("PfamGroup"));
assertTrue(groups.contains("RfamGroup"));
assertFalse(groups.contains(null)); // null group is ignored
/*
* check min-max values
*/
Map minMax = fr.getMinMax();
assertEquals(minMax.size(), 1); // non-positional and NaN not stored
assertEquals(minMax.get("Pfam")[0][0], 1f); // positional min
assertEquals(minMax.get("Pfam")[0][1], 2f); // positional max
// increase max for Pfam, add scores for Rfam
seqs.get(0).addSequenceFeature(
new SequenceFeature("Pfam", "Desc", 14, 22, 8f, "RfamGroup"));
seqs.get(1).addSequenceFeature(
new SequenceFeature("Rfam", "Desc", 5, 9, 6f, "RfamGroup"));
fr.findAllFeatures(true);
// note minMax is not a defensive copy, shouldn't expose this
assertEquals(minMax.size(), 2);
assertEquals(minMax.get("Pfam")[0][0], 1f);
assertEquals(minMax.get("Pfam")[0][1], 8f);
assertEquals(minMax.get("Rfam")[0][0], 6f);
assertEquals(minMax.get("Rfam")[0][1], 6f);
/*
* check render order (last is on top)
*/
List renderOrder = fr.getRenderOrder();
assertEquals(renderOrder, Arrays.asList("Scop", "Rfam", "Pfam"));
/*
* change render order (todo: an easier way)
* nb here last comes first in the data array
*/
FeatureSettingsBean[] data = new FeatureSettingsBean[3];
FeatureColourI colour = new FeatureColour(Color.RED);
data[0] = new FeatureSettingsBean("Rfam", colour, null, true);
data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
data[2] = new FeatureSettingsBean("Scop", colour, null, false);
fr.setFeaturePriority(data);
assertEquals(fr.getRenderOrder(),
Arrays.asList("Scop", "Pfam", "Rfam"));
assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
/*
* add a new feature type: should go on top of render order as visible,
* other feature ordering and visibility should be unchanged
*/
seqs.get(2).addSequenceFeature(
new SequenceFeature("Metal", "Desc", 14, 22, 8f, "MetalGroup"));
fr.findAllFeatures(true);
assertEquals(fr.getRenderOrder(),
Arrays.asList("Scop", "Pfam", "Rfam", "Metal"));
assertEquals(fr.getDisplayedFeatureTypes(),
Arrays.asList("Rfam", "Metal"));
}
@Test(groups = "Functional")
public void testFindFeaturesAtColumn()
{
String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n";
AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
DataSourceType.PASTE);
AlignViewportI av = af.getViewport();
FeatureRenderer fr = new FeatureRenderer(av);
SequenceI seq = av.getAlignment().getSequenceAt(0);
/*
* with no features
*/
List features = fr.findFeaturesAtColumn(seq, 3);
assertTrue(features.isEmpty());
/*
* add features
*/
SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f,
"Group"); // non-positional
seq.addSequenceFeature(sf1);
SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f,
"Group1");
seq.addSequenceFeature(sf2);
SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
"Group2");
seq.addSequenceFeature(sf3);
SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
null); // null group is always treated as visible
seq.addSequenceFeature(sf4);
/*
* add contact features
*/
SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7,
15, 1f, "Group1");
seq.addSequenceFeature(sf5);
SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7,
15, 1f, "Group2");
seq.addSequenceFeature(sf6);
SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 7,
15, 1f, null);
seq.addSequenceFeature(sf7);
// feature spanning B--C
SequenceFeature sf8 = new SequenceFeature("Type1", "Desc", 5, 6, 1f,
"Group");
seq.addSequenceFeature(sf8);
// contact feature B/C
SequenceFeature sf9 = new SequenceFeature("Disulphide Bond", "Desc", 5,
6, 1f, "Group");
seq.addSequenceFeature(sf9);
/*
* let feature renderer discover features (and make visible)
*/
fr.findAllFeatures(true);
features = fr.findFeaturesAtColumn(seq, 15); // all positional
assertEquals(features.size(), 6);
assertTrue(features.contains(sf2));
assertTrue(features.contains(sf3));
assertTrue(features.contains(sf4));
assertTrue(features.contains(sf5));
assertTrue(features.contains(sf6));
assertTrue(features.contains(sf7));
/*
* at a non-contact position
*/
features = fr.findFeaturesAtColumn(seq, 14);
assertEquals(features.size(), 3);
assertTrue(features.contains(sf2));
assertTrue(features.contains(sf3));
assertTrue(features.contains(sf4));
/*
* make "Type2" not displayed
*/
FeatureColourI colour = new FeatureColour(Color.RED);
FeatureSettingsBean[] data = new FeatureSettingsBean[4];
data[0] = new FeatureSettingsBean("Type1", colour, null, true);
data[1] = new FeatureSettingsBean("Type2", colour, null, false);
data[2] = new FeatureSettingsBean("Type3", colour, null, true);
data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null,
true);
fr.setFeaturePriority(data);
features = fr.findFeaturesAtColumn(seq, 15);
assertEquals(features.size(), 5); // no sf2
assertTrue(features.contains(sf3));
assertTrue(features.contains(sf4));
assertTrue(features.contains(sf5));
assertTrue(features.contains(sf6));
assertTrue(features.contains(sf7));
/*
* make "Group2" not displayed
*/
fr.setGroupVisibility("Group2", false);
features = fr.findFeaturesAtColumn(seq, 15);
assertEquals(features.size(), 3); // no sf2, sf3, sf6
assertTrue(features.contains(sf4));
assertTrue(features.contains(sf5));
assertTrue(features.contains(sf7));
// features 'at' a gap between b and c
// - returns enclosing feature BC but not contact feature B/C
features = fr.findFeaturesAtColumn(seq, 4);
assertEquals(features.size(), 1);
assertTrue(features.contains(sf8));
features = fr.findFeaturesAtColumn(seq, 5);
assertEquals(features.size(), 1);
assertTrue(features.contains(sf8));
/*
* give "Type3" features a graduated colour scheme
* - first with no threshold
*/
FeatureColourI gc = new FeatureColour(Color.green, Color.yellow,
Color.red, null, 0f, 10f);
fr.getFeatureColours().put("Type3", gc);
features = fr.findFeaturesAtColumn(seq, 8);
assertTrue(features.contains(sf4));
// now with threshold > 2f - feature score of 1f is excluded
gc.setAboveThreshold(true);
gc.setThreshold(2f);
features = fr.findFeaturesAtColumn(seq, 8);
assertFalse(features.contains(sf4));
/*
* make "Type3" graduated colour by attribute "AF"
* - first with no attribute held - feature should be excluded
*/
gc.setAttributeName("AF");
features = fr.findFeaturesAtColumn(seq, 8);
assertFalse(features.contains(sf4));
// now with the attribute above threshold - should be included
sf4.setValue("AF", "2.4");
features = fr.findFeaturesAtColumn(seq, 8);
assertTrue(features.contains(sf4));
// now with the attribute below threshold - should be excluded
sf4.setValue("AF", "1.4");
features = fr.findFeaturesAtColumn(seq, 8);
assertFalse(features.contains(sf4));
}
@Test(groups = "Functional")
public void testFilterFeaturesForDisplay()
{
String seqData = ">s1\nabcdef\n";
AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
DataSourceType.PASTE);
AlignViewportI av = af.getViewport();
FeatureRenderer fr = new FeatureRenderer(av);
List features = new ArrayList<>();
fr.filterFeaturesForDisplay(features); // empty list, does nothing
SequenceI seq = av.getAlignment().getSequenceAt(0);
SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
"group1");
SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, 2f,
"group2");
SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, 3f,
"group3");
SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, 4f,
"group4");
SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, 5f,
"group4");
seq.addSequenceFeature(sf1);
seq.addSequenceFeature(sf2);
seq.addSequenceFeature(sf3);
seq.addSequenceFeature(sf4);
seq.addSequenceFeature(sf5);
fr.findAllFeatures(true);
features = seq.getSequenceFeatures();
assertEquals(features.size(), 5);
assertTrue(features.contains(sf1));
assertTrue(features.contains(sf2));
assertTrue(features.contains(sf3));
assertTrue(features.contains(sf4));
assertTrue(features.contains(sf5));
/*
* filter out duplicate (co-located) features
* note: which gets removed is not guaranteed
*/
fr.filterFeaturesForDisplay(features);
assertEquals(features.size(), 3);
assertTrue(features.contains(sf1) || features.contains(sf4));
assertFalse(features.contains(sf1) && features.contains(sf4));
assertTrue(features.contains(sf2) || features.contains(sf3));
assertFalse(features.contains(sf2) && features.contains(sf3));
assertTrue(features.contains(sf5));
/*
* features in hidden groups are removed
*/
fr.setGroupVisibility("group2", false);
fr.setGroupVisibility("group3", false);
features = seq.getSequenceFeatures();
fr.filterFeaturesForDisplay(features);
assertEquals(features.size(), 2);
assertTrue(features.contains(sf1) || features.contains(sf4));
assertFalse(features.contains(sf1) && features.contains(sf4));
assertFalse(features.contains(sf2));
assertFalse(features.contains(sf3));
assertTrue(features.contains(sf5));
/*
* no filtering if transparency is applied
*/
fr.setTransparency(0.5f);
features = seq.getSequenceFeatures();
fr.filterFeaturesForDisplay(features);
assertEquals(features.size(), 5);
}
@Test(groups = "Functional")
public void testGetColour()
{
AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(">s1\nABCD\n",
DataSourceType.PASTE);
AlignViewportI av = af.getViewport();
FeatureRenderer fr = new FeatureRenderer(av);
/*
* simple colour, feature type and group displayed
*/
FeatureColourI fc = new FeatureColour(Color.red);
fr.getFeatureColours().put("Cath", fc);
SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
"group1");
assertEquals(fr.getColour(sf1), Color.red);
/*
* hide feature type, then unhide
* - feature type visibility should not affect the result
*/
FeatureSettingsBean[] data = new FeatureSettingsBean[1];
data[0] = new FeatureSettingsBean("Cath", fc, null, false);
fr.setFeaturePriority(data);
assertEquals(fr.getColour(sf1), Color.red);
data[0] = new FeatureSettingsBean("Cath", fc, null, true);
fr.setFeaturePriority(data);
assertEquals(fr.getColour(sf1), Color.red);
/*
* hide feature group, then unhide
*/
fr.setGroupVisibility("group1", false);
assertNull(fr.getColour(sf1));
fr.setGroupVisibility("group1", true);
assertEquals(fr.getColour(sf1), Color.red);
/*
* graduated colour by score, no threshold, no score
*
*/
FeatureColourI gc = new FeatureColour(Color.red, Color.yellow,
Color.red, Color.green, 1f, 11f);
fr.getFeatureColours().put("Cath", gc);
assertEquals(fr.getColour(sf1), Color.green);
/*
* graduated colour by score, no threshold, with score value
*/
SequenceFeature sf2 = new SequenceFeature("Cath", "", 6, 8, 6f,
"group1");
// score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
Color expected = new Color(255, 128, 0);
assertEquals(fr.getColour(sf2), expected);
/*
* above threshold, score is above threshold - no change
*/
gc.setAboveThreshold(true);
gc.setThreshold(5f);
assertEquals(fr.getColour(sf2), expected);
/*
* threshold is min-max; now score 6 is 1/6 of the way from 5 to 11
* or from yellow(255, 255, 0) to red(255, 0, 0)
*/
gc = new FeatureColour(Color.red, Color.yellow, Color.red, Color.green,
5f, 11f);
fr.getFeatureColours().put("Cath", gc);
gc.setAutoScaled(false); // this does little other than save a checkbox setting!
assertEquals(fr.getColour(sf2), new Color(255, 213, 0));
/*
* feature score is below threshold - no colour
*/
gc.setAboveThreshold(true);
gc.setThreshold(7f);
assertNull(fr.getColour(sf2));
/*
* feature score is above threshold - no colour
*/
gc.setBelowThreshold(true);
gc.setThreshold(3f);
assertNull(fr.getColour(sf2));
/*
* colour by feature attribute value
* first with no value held
*/
gc = new FeatureColour(Color.red, Color.yellow, Color.red, Color.green,
1f, 11f);
fr.getFeatureColours().put("Cath", gc);
gc.setAttributeName("AF");
assertEquals(fr.getColour(sf2), Color.green);
// with non-numeric attribute value
sf2.setValue("AF", "Five");
assertEquals(fr.getColour(sf2), Color.green);
// with numeric attribute value
sf2.setValue("AF", "6");
assertEquals(fr.getColour(sf2), expected);
// with numeric value outwith threshold
gc.setAboveThreshold(true);
gc.setThreshold(10f);
assertNull(fr.getColour(sf2));
// with filter on AF < 4
gc.setAboveThreshold(false);
assertEquals(fr.getColour(sf2), expected);
FeatureMatcherSetI filter = new FeatureMatcherSet();
filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
fr.setFeatureFilter("Cath", filter);
assertNull(fr.getColour(sf2));
// with filter on 'Consequence contains missense'
filter = new FeatureMatcherSet();
filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
"Consequence"));
fr.setFeatureFilter("Cath", filter);
// if feature has no Consequence attribute, no colour
assertNull(fr.getColour(sf2));
// if attribute does not match filter, no colour
sf2.setValue("Consequence", "Synonymous");
assertNull(fr.getColour(sf2));
// attribute matches filter
sf2.setValue("Consequence", "Missense variant");
assertEquals(fr.getColour(sf2), expected);
// with filter on CSQ:Feature contains "ENST01234"
filter = new FeatureMatcherSet();
filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
"CSQ", "Feature"));
fr.setFeatureFilter("Cath", filter);
// if feature has no CSQ data, no colour
assertNull(fr.getColour(sf2));
// if CSQ data does not include Feature, no colour
Map csqData = new HashMap<>();
csqData.put("BIOTYPE", "Transcript");
sf2.setValue("CSQ", csqData);
assertNull(fr.getColour(sf2));
// if attribute does not match filter, no colour
csqData.put("Feature", "ENST9876");
assertNull(fr.getColour(sf2));
// attribute matches filter
csqData.put("Feature", "ENST01234");
assertEquals(fr.getColour(sf2), expected);
}
@Test(groups = "Functional")
public void testIsVisible()
{
String seqData = ">s1\nMLQGIFPRS\n";
AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
DataSourceType.PASTE);
AlignViewportI av = af.getViewport();
FeatureRenderer fr = new FeatureRenderer(av);
SequenceI seq = av.getAlignment().getSequenceAt(0);
SequenceFeature sf = new SequenceFeature("METAL", "Desc", 10, 10, 1f,
"Group");
sf.setValue("AC", "11");
sf.setValue("CLIN_SIG", "Likely Pathogenic");
seq.addSequenceFeature(sf);
assertFalse(fr.isVisible(null));
/*
* initial state FeatureRenderer hasn't 'found' feature
* and so its feature type has not yet been set visible
*/
assertFalse(fr.getDisplayedFeatureCols().containsKey("METAL"));
assertFalse(fr.isVisible(sf));
fr.findAllFeatures(true);
assertTrue(fr.isVisible(sf));
/*
* feature group not visible
*/
fr.setGroupVisibility("Group", false);
assertFalse(fr.isVisible(sf));
fr.setGroupVisibility("Group", true);
assertTrue(fr.isVisible(sf));
/*
* feature score outwith colour threshold (score > 2)
*/
FeatureColourI fc = new FeatureColour(null, Color.white, Color.black,
Color.white, 0, 10);
fc.setAboveThreshold(true);
fc.setThreshold(2f);
fr.setColour("METAL", fc);
assertFalse(fr.isVisible(sf)); // score 1 is not above threshold 2
fc.setBelowThreshold(true);
assertTrue(fr.isVisible(sf)); // score 1 is below threshold 2
/*
* colour with threshold on attribute AC (value is 11)
*/
fc.setAttributeName("AC");
assertFalse(fr.isVisible(sf)); // value 11 is not below threshold 2
fc.setAboveThreshold(true);
assertTrue(fr.isVisible(sf)); // value 11 is above threshold 2
fc.setAttributeName("AF"); // attribute AF is absent in sf
assertTrue(fr.isVisible(sf)); // feature is not excluded by threshold
FeatureMatcherSetI filter = new FeatureMatcherSet();
filter.and(FeatureMatcher.byAttribute(Condition.Contains, "pathogenic",
"CLIN_SIG"));
fr.setFeatureFilter("METAL", filter);
assertTrue(fr.isVisible(sf)); // feature matches filter
filter.and(FeatureMatcher.byScore(Condition.LE, "0.4"));
assertFalse(fr.isVisible(sf)); // feature doesn't match filter
}
}