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