Merge branch 'bug/JAL-3120restoreFeatureColour' into merge/JAL-3120
[jalview.git] / test / jalview / renderer / seqfeatures / FeatureRendererTest.java
index ab5c137..723f3b8 100644 (file)
@@ -1,20 +1,48 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * 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;
 
@@ -60,9 +88,8 @@ public class FeatureRendererTest
     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"));
+    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));
@@ -115,13 +142,14 @@ public class FeatureRendererTest
      * change render order (todo: an easier way)
      * nb here last comes first in the data array
      */
-    Object[][] data = new Object[3][];
+    FeatureSettingsBean[] data = new FeatureSettingsBean[3];
     FeatureColourI colour = new FeatureColour(Color.RED);
-    data[0] = new Object[] { "Rfam", colour, true };
-    data[1] = new Object[] { "Pfam", colour, false };
-    data[2] = new Object[] { "Scop", colour, false };
+    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.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam"));
     assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
 
     /*
@@ -138,9 +166,9 @@ public class FeatureRendererTest
   }
 
   @Test(groups = "Functional")
-  public void testFindFeaturesAtRes()
+  public void testFindFeaturesAtColumn()
   {
-    String seqData = ">s1\nabcdefghijklmnopqrstuvwxyz\n";
+    String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n";
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
             DataSourceType.PASTE);
     AlignViewportI av = af.getViewport();
@@ -150,7 +178,7 @@ public class FeatureRendererTest
     /*
      * with no features
      */
-    List<SequenceFeature> features = fr.findFeaturesAtRes(seq, 3);
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, 3);
     assertTrue(features.isEmpty());
 
     /*
@@ -159,34 +187,43 @@ public class FeatureRendererTest
     SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f,
             "Group"); // non-positional
     seq.addSequenceFeature(sf1);
-    SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 5, 15, 1f,
+    SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f,
             "Group1");
     seq.addSequenceFeature(sf2);
-    SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 5, 15, 1f,
+    SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
             "Group2");
     seq.addSequenceFeature(sf3);
-    SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 5, 15, 1f,
+    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", 4,
-            12, 1f, "Group1");
+    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group1");
     seq.addSequenceFeature(sf5);
-    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 4,
-            12, 1f, "Group2");
+    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group2");
     seq.addSequenceFeature(sf6);
-    SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 4,
-            12, 1f, null);
+    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.findFeaturesAtRes(seq, 12); // all positional
+    features = fr.findFeaturesAtColumn(seq, 15); // all positional
     assertEquals(features.size(), 6);
     assertTrue(features.contains(sf2));
     assertTrue(features.contains(sf3));
@@ -198,7 +235,7 @@ public class FeatureRendererTest
     /*
      * at a non-contact position
      */
-    features = fr.findFeaturesAtRes(seq, 11);
+    features = fr.findFeaturesAtColumn(seq, 14);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf2));
     assertTrue(features.contains(sf3));
@@ -207,14 +244,16 @@ public class FeatureRendererTest
     /*
      * make "Type2" not displayed
      */
-    Object[][] data = new Object[4][];
     FeatureColourI colour = new FeatureColour(Color.RED);
-    data[0] = new Object[] { "Type1", colour, true };
-    data[1] = new Object[] { "Type2", colour, false };
-    data[2] = new Object[] { "Type3", colour, true };
-    data[3] = new Object[] { "Disulphide Bond", colour, true };
+    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.findFeaturesAtRes(seq, 12);
+
+    features = fr.findFeaturesAtColumn(seq, 15);
     assertEquals(features.size(), 5); // no sf2
     assertTrue(features.contains(sf3));
     assertTrue(features.contains(sf4));
@@ -226,10 +265,343 @@ public class FeatureRendererTest
      * make "Group2" not displayed
      */
     fr.setGroupVisibility("Group2", false);
-    features = fr.findFeaturesAtRes(seq, 12);
+
+    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<SequenceFeature> 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<String, String> 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
   }
 }