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