JAL-2808 refactor KeyedMatcher as FeatureMatcher with byLabel, byScore, byAttribute...
[jalview.git] / test / jalview / renderer / seqfeatures / FeatureRendererTest.java
1 package jalview.renderer.seqfeatures;
2
3 import static org.testng.Assert.assertEquals;
4 import static org.testng.Assert.assertFalse;
5 import static org.testng.Assert.assertNull;
6 import static org.testng.Assert.assertTrue;
7
8 import jalview.api.AlignViewportI;
9 import jalview.api.FeatureColourI;
10 import jalview.datamodel.SequenceFeature;
11 import jalview.datamodel.SequenceI;
12 import jalview.datamodel.features.FeatureMatcher;
13 import jalview.datamodel.features.FeatureMatcherSet;
14 import jalview.datamodel.features.FeatureMatcherSetI;
15 import jalview.gui.AlignFrame;
16 import jalview.io.DataSourceType;
17 import jalview.io.FileLoader;
18 import jalview.schemes.FeatureColour;
19 import jalview.util.matcher.Condition;
20 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
21
22 import java.awt.Color;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28
29 import org.testng.annotations.Test;
30
31 public class FeatureRendererTest
32 {
33
34   @Test(groups = "Functional")
35   public void testFindAllFeatures()
36   {
37     String seqData = ">s1\nabcdef\n>s2\nabcdef\n>s3\nabcdef\n>s4\nabcdef\n";
38     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
39             DataSourceType.PASTE);
40     AlignViewportI av = af.getViewport();
41     FeatureRenderer fr = new FeatureRenderer(av);
42
43     /*
44      * with no features
45      */
46     fr.findAllFeatures(true);
47     assertTrue(fr.getRenderOrder().isEmpty());
48     assertTrue(fr.getFeatureGroups().isEmpty());
49
50     List<SequenceI> seqs = av.getAlignment().getSequences();
51
52     // add a non-positional feature - should be ignored by FeatureRenderer
53     SequenceFeature sf1 = new SequenceFeature("Type", "Desc", 0, 0, 1f,
54             "Group");
55     seqs.get(0).addSequenceFeature(sf1);
56     fr.findAllFeatures(true);
57     // ? bug - types and groups added for non-positional features
58     List<String> types = fr.getRenderOrder();
59     List<String> groups = fr.getFeatureGroups();
60     assertEquals(types.size(), 0);
61     assertFalse(types.contains("Type"));
62     assertEquals(groups.size(), 0);
63     assertFalse(groups.contains("Group"));
64
65     // add some positional features
66     seqs.get(1).addSequenceFeature(
67             new SequenceFeature("Pfam", "Desc", 5, 9, 1f, "PfamGroup"));
68     seqs.get(2).addSequenceFeature(
69             new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
70     // bug in findAllFeatures - group not checked for a known feature type
71     seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9,
72             Float.NaN, "RfamGroup"));
73     // existing feature type with null group
74     seqs.get(3).addSequenceFeature(
75             new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
76     // new feature type with null group
77     seqs.get(3).addSequenceFeature(
78             new SequenceFeature("Scop", "Desc", 5, 9, Float.NaN, null));
79     // null value for type produces NullPointerException
80     fr.findAllFeatures(true);
81     types = fr.getRenderOrder();
82     groups = fr.getFeatureGroups();
83     assertEquals(types.size(), 3);
84     assertFalse(types.contains("Type"));
85     assertTrue(types.contains("Pfam"));
86     assertTrue(types.contains("Rfam"));
87     assertTrue(types.contains("Scop"));
88     assertEquals(groups.size(), 2);
89     assertFalse(groups.contains("Group"));
90     assertTrue(groups.contains("PfamGroup"));
91     assertTrue(groups.contains("RfamGroup"));
92     assertFalse(groups.contains(null)); // null group is ignored
93
94     /*
95      * check min-max values
96      */
97     Map<String, float[][]> minMax = fr.getMinMax();
98     assertEquals(minMax.size(), 1); // non-positional and NaN not stored
99     assertEquals(minMax.get("Pfam")[0][0], 1f); // positional min
100     assertEquals(minMax.get("Pfam")[0][1], 2f); // positional max
101
102     // increase max for Pfam, add scores for Rfam
103     seqs.get(0).addSequenceFeature(
104             new SequenceFeature("Pfam", "Desc", 14, 22, 8f, "RfamGroup"));
105     seqs.get(1).addSequenceFeature(
106             new SequenceFeature("Rfam", "Desc", 5, 9, 6f, "RfamGroup"));
107     fr.findAllFeatures(true);
108     // note minMax is not a defensive copy, shouldn't expose this
109     assertEquals(minMax.size(), 2);
110     assertEquals(minMax.get("Pfam")[0][0], 1f);
111     assertEquals(minMax.get("Pfam")[0][1], 8f);
112     assertEquals(minMax.get("Rfam")[0][0], 6f);
113     assertEquals(minMax.get("Rfam")[0][1], 6f);
114
115     /*
116      * check render order (last is on top)
117      */
118     List<String> renderOrder = fr.getRenderOrder();
119     assertEquals(renderOrder, Arrays.asList("Scop", "Rfam", "Pfam"));
120
121     /*
122      * change render order (todo: an easier way)
123      * nb here last comes first in the data array
124      */
125     FeatureSettingsBean[] data = new FeatureSettingsBean[3];
126     FeatureColourI colour = new FeatureColour(Color.RED);
127     data[0] = new FeatureSettingsBean("Rfam", colour, null, true);
128     data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
129     data[2] = new FeatureSettingsBean("Scop", colour, null, false);
130     fr.setFeaturePriority(data);
131     assertEquals(fr.getRenderOrder(),
132             Arrays.asList("Scop", "Pfam", "Rfam"));
133     assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
134
135     /*
136      * add a new feature type: should go on top of render order as visible,
137      * other feature ordering and visibility should be unchanged
138      */
139     seqs.get(2).addSequenceFeature(
140             new SequenceFeature("Metal", "Desc", 14, 22, 8f, "MetalGroup"));
141     fr.findAllFeatures(true);
142     assertEquals(fr.getRenderOrder(),
143             Arrays.asList("Scop", "Pfam", "Rfam", "Metal"));
144     assertEquals(fr.getDisplayedFeatureTypes(),
145             Arrays.asList("Rfam", "Metal"));
146   }
147
148   @Test(groups = "Functional")
149   public void testFindFeaturesAtColumn()
150   {
151     String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n";
152     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
153             DataSourceType.PASTE);
154     AlignViewportI av = af.getViewport();
155     FeatureRenderer fr = new FeatureRenderer(av);
156     SequenceI seq = av.getAlignment().getSequenceAt(0);
157
158     /*
159      * with no features
160      */
161     List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, 3);
162     assertTrue(features.isEmpty());
163
164     /*
165      * add features
166      */
167     SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f,
168             "Group"); // non-positional
169     seq.addSequenceFeature(sf1);
170     SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f,
171             "Group1");
172     seq.addSequenceFeature(sf2);
173     SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
174             "Group2");
175     seq.addSequenceFeature(sf3);
176     SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
177             null); // null group is always treated as visible
178     seq.addSequenceFeature(sf4);
179
180     /*
181      * add contact features
182      */
183     SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7,
184             15, 1f, "Group1");
185     seq.addSequenceFeature(sf5);
186     SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7,
187             15, 1f, "Group2");
188     seq.addSequenceFeature(sf6);
189     SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 7,
190             15, 1f, null);
191     seq.addSequenceFeature(sf7);
192
193     // feature spanning B--C
194     SequenceFeature sf8 = new SequenceFeature("Type1", "Desc", 5, 6, 1f,
195             "Group");
196     seq.addSequenceFeature(sf8);
197     // contact feature B/C
198     SequenceFeature sf9 = new SequenceFeature("Disulphide Bond", "Desc", 5,
199             6, 1f, "Group");
200     seq.addSequenceFeature(sf9);
201
202     /*
203      * let feature renderer discover features (and make visible)
204      */
205     fr.findAllFeatures(true);
206     features = fr.findFeaturesAtColumn(seq, 15); // all positional
207     assertEquals(features.size(), 6);
208     assertTrue(features.contains(sf2));
209     assertTrue(features.contains(sf3));
210     assertTrue(features.contains(sf4));
211     assertTrue(features.contains(sf5));
212     assertTrue(features.contains(sf6));
213     assertTrue(features.contains(sf7));
214
215     /*
216      * at a non-contact position
217      */
218     features = fr.findFeaturesAtColumn(seq, 14);
219     assertEquals(features.size(), 3);
220     assertTrue(features.contains(sf2));
221     assertTrue(features.contains(sf3));
222     assertTrue(features.contains(sf4));
223
224     /*
225      * make "Type2" not displayed
226      */
227     FeatureColourI colour = new FeatureColour(Color.RED);
228     FeatureSettingsBean[] data = new FeatureSettingsBean[4];
229     data[0] = new FeatureSettingsBean("Type1", colour, null, true);
230     data[1] = new FeatureSettingsBean("Type2", colour, null, false);
231     data[2] = new FeatureSettingsBean("Type3", colour, null, true);
232     data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null,
233             true);
234     fr.setFeaturePriority(data);
235
236     features = fr.findFeaturesAtColumn(seq, 15);
237     assertEquals(features.size(), 5); // no sf2
238     assertTrue(features.contains(sf3));
239     assertTrue(features.contains(sf4));
240     assertTrue(features.contains(sf5));
241     assertTrue(features.contains(sf6));
242     assertTrue(features.contains(sf7));
243
244     /*
245      * make "Group2" not displayed
246      */
247     fr.setGroupVisibility("Group2", false);
248
249     features = fr.findFeaturesAtColumn(seq, 15);
250     assertEquals(features.size(), 3); // no sf2, sf3, sf6
251     assertTrue(features.contains(sf4));
252     assertTrue(features.contains(sf5));
253     assertTrue(features.contains(sf7));
254
255     // features 'at' a gap between b and c
256     // - returns enclosing feature BC but not contact feature B/C
257     features = fr.findFeaturesAtColumn(seq, 4);
258     assertEquals(features.size(), 1);
259     assertTrue(features.contains(sf8));
260     features = fr.findFeaturesAtColumn(seq, 5);
261     assertEquals(features.size(), 1);
262     assertTrue(features.contains(sf8));
263
264     /*
265      * give "Type3" features a graduated colour scheme
266      * - first with no threshold
267      */
268     FeatureColourI gc = new FeatureColour(Color.yellow, Color.red, null, 0f,
269             10f);
270     fr.getFeatureColours().put("Type3", gc);
271     features = fr.findFeaturesAtColumn(seq, 8);
272     assertTrue(features.contains(sf4));
273     // now with threshold > 2f - feature score of 1f is excluded
274     gc.setAboveThreshold(true);
275     gc.setThreshold(2f);
276     features = fr.findFeaturesAtColumn(seq, 8);
277     assertFalse(features.contains(sf4));
278
279     /*
280      * make "Type3" graduated colour by attribute "AF"
281      * - first with no attribute held - feature should be excluded
282      */
283     gc.setAttributeName("AF");
284     features = fr.findFeaturesAtColumn(seq, 8);
285     assertFalse(features.contains(sf4));
286     // now with the attribute above threshold - should be included
287     sf4.setValue("AF", "2.4");
288     features = fr.findFeaturesAtColumn(seq, 8);
289     assertTrue(features.contains(sf4));
290     // now with the attribute below threshold - should be excluded
291     sf4.setValue("AF", "1.4");
292     features = fr.findFeaturesAtColumn(seq, 8);
293     assertFalse(features.contains(sf4));
294   }
295
296   @Test(groups = "Functional")
297   public void testFilterFeaturesForDisplay()
298   {
299     String seqData = ">s1\nabcdef\n";
300     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
301             DataSourceType.PASTE);
302     AlignViewportI av = af.getViewport();
303     FeatureRenderer fr = new FeatureRenderer(av);
304
305     List<SequenceFeature> features = new ArrayList<>();
306     fr.filterFeaturesForDisplay(features); // empty list, does nothing
307
308     SequenceI seq = av.getAlignment().getSequenceAt(0);
309     SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
310             "group1");
311     seq.addSequenceFeature(sf1);
312     SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, 2f,
313             "group2");
314     seq.addSequenceFeature(sf2);
315     SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, 3f,
316             "group3");
317     seq.addSequenceFeature(sf3);
318     SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, 4f,
319             "group4");
320     seq.addSequenceFeature(sf4);
321     SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, 5f,
322             "group4");
323     seq.addSequenceFeature(sf5);
324
325     fr.findAllFeatures(true);
326
327     features = seq.getSequenceFeatures();
328     assertEquals(features.size(), 5);
329     assertTrue(features.contains(sf1));
330     assertTrue(features.contains(sf2));
331     assertTrue(features.contains(sf3));
332     assertTrue(features.contains(sf4));
333     assertTrue(features.contains(sf5));
334
335     /*
336      * filter out duplicate (co-located) features
337      * note: which gets removed is not guaranteed
338      */
339     fr.filterFeaturesForDisplay(features);
340     assertEquals(features.size(), 3);
341     assertTrue(features.contains(sf1) || features.contains(sf4));
342     assertFalse(features.contains(sf1) && features.contains(sf4));
343     assertTrue(features.contains(sf2) || features.contains(sf3));
344     assertFalse(features.contains(sf2) && features.contains(sf3));
345     assertTrue(features.contains(sf5));
346
347     /*
348      * hide groups 2 and 3 makes no difference to this method
349      */
350     fr.setGroupVisibility("group2", false);
351     fr.setGroupVisibility("group3", false);
352     features = seq.getSequenceFeatures();
353     fr.filterFeaturesForDisplay(features);
354     assertEquals(features.size(), 3);
355     assertTrue(features.contains(sf1) || features.contains(sf4));
356     assertFalse(features.contains(sf1) && features.contains(sf4));
357     assertTrue(features.contains(sf2) || features.contains(sf3));
358     assertFalse(features.contains(sf2) && features.contains(sf3));
359     assertTrue(features.contains(sf5));
360   }
361
362   @Test(groups = "Functional")
363   public void testGetColour()
364   {
365     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(">s1\nABCD\n",
366             DataSourceType.PASTE);
367     AlignViewportI av = af.getViewport();
368     FeatureRenderer fr = new FeatureRenderer(av);
369
370     /*
371      * simple colour, feature type and group displayed
372      */
373     FeatureColourI fc = new FeatureColour(Color.red);
374     fr.getFeatureColours().put("Cath", fc);
375     SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
376             "group1");
377     assertEquals(fr.getColour(sf1), Color.red);
378
379     /*
380      * hide feature type, then unhide
381      */
382     FeatureSettingsBean[] data = new FeatureSettingsBean[1];
383     data[0] = new FeatureSettingsBean("Cath", fc, null, false);
384     fr.setFeaturePriority(data);
385     assertNull(fr.getColour(sf1));
386     data[0] = new FeatureSettingsBean("Cath", fc, null, true);
387     fr.setFeaturePriority(data);
388     assertEquals(fr.getColour(sf1), Color.red);
389
390     /*
391      * hide feature group, then unhide
392      */
393     fr.setGroupVisibility("group1", false);
394     assertNull(fr.getColour(sf1));
395     fr.setGroupVisibility("group1", true);
396     assertEquals(fr.getColour(sf1), Color.red);
397
398     /*
399      * graduated colour by score, no threshold, no score
400      * 
401      */
402     FeatureColourI gc = new FeatureColour(Color.yellow, Color.red,
403             Color.green, 1f, 11f);
404     fr.getFeatureColours().put("Cath", gc);
405     assertEquals(fr.getColour(sf1), Color.green);
406
407     /*
408      * graduated colour by score, no threshold, with score value
409      */
410     SequenceFeature sf2 = new SequenceFeature("Cath", "", 6, 8, 6f,
411             "group1");
412     // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
413     Color expected = new Color(255, 128, 0);
414     assertEquals(fr.getColour(sf2), expected);
415
416     /*
417      * above threshold, score is above threshold - no change
418      */
419     gc.setAboveThreshold(true);
420     gc.setThreshold(5f);
421     assertEquals(fr.getColour(sf2), expected);
422
423     /*
424      * threshold is min-max; now score 6 is 1/6 of the way from 5 to 11
425      * or from yellow(255, 255, 0) to red(255, 0, 0)
426      */
427     gc = new FeatureColour(Color.yellow, Color.red, Color.green, 5f, 11f);
428     fr.getFeatureColours().put("Cath", gc);
429     gc.setAutoScaled(false); // this does little other than save a checkbox setting!
430     assertEquals(fr.getColour(sf2), new Color(255, 213, 0));
431
432     /*
433      * feature score is below threshold - no colour
434      */
435     gc.setAboveThreshold(true);
436     gc.setThreshold(7f);
437     assertNull(fr.getColour(sf2));
438
439     /*
440      * feature score is above threshold - no colour
441      */
442     gc.setBelowThreshold(true);
443     gc.setThreshold(3f);
444     assertNull(fr.getColour(sf2));
445
446     /*
447      * colour by feature attribute value
448      * first with no value held
449      */
450     gc = new FeatureColour(Color.yellow, Color.red, Color.green, 1f, 11f);
451     fr.getFeatureColours().put("Cath", gc);
452     gc.setAttributeName("AF");
453     assertEquals(fr.getColour(sf2), Color.green);
454
455     // with non-numeric attribute value
456     sf2.setValue("AF", "Five");
457     assertEquals(fr.getColour(sf2), Color.green);
458
459     // with numeric attribute value
460     sf2.setValue("AF", "6");
461     assertEquals(fr.getColour(sf2), expected);
462
463     // with numeric value outwith threshold
464     gc.setAboveThreshold(true);
465     gc.setThreshold(10f);
466     assertNull(fr.getColour(sf2));
467
468     // with filter on AF < 4
469     gc.setAboveThreshold(false);
470     assertEquals(fr.getColour(sf2), expected);
471     FeatureMatcherSetI filter = new FeatureMatcherSet();
472     filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
473     fr.setFeatureFilter("Cath", filter);
474     assertNull(fr.getColour(sf2));
475
476     // with filter on 'Consequence contains missense'
477     filter = new FeatureMatcherSet();
478     filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
479             "Consequence"));
480     fr.setFeatureFilter("Cath", filter);
481     // if feature has no Consequence attribute, no colour
482     assertNull(fr.getColour(sf2));
483     // if attribute does not match filter, no colour
484     sf2.setValue("Consequence", "Synonymous");
485     assertNull(fr.getColour(sf2));
486     // attribute matches filter
487     sf2.setValue("Consequence", "Missense variant");
488     assertEquals(fr.getColour(sf2), expected);
489
490     // with filter on CSQ:Feature contains "ENST01234"
491     filter = new FeatureMatcherSet();
492     filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
493             "CSQ", "Feature"));
494     fr.setFeatureFilter("Cath", filter);
495     // if feature has no CSQ data, no colour
496     assertNull(fr.getColour(sf2));
497     // if CSQ data does not include Feature, no colour
498     Map<String, String> csqData = new HashMap<>();
499     csqData.put("BIOTYPE", "Transcript");
500     sf2.setValue("CSQ", csqData);
501     assertNull(fr.getColour(sf2));
502     // if attribute does not match filter, no colour
503     csqData.put("Feature", "ENST9876");
504     assertNull(fr.getColour(sf2));
505     // attribute matches filter
506     csqData.put("Feature", "ENST01234");
507     assertEquals(fr.getColour(sf2), expected);
508   }
509 }