JAL-3120 preserve feature colour/mincolour/maxcolour while modifying,
[jalview.git] / test / jalview / schemes / FeatureColourTest.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.schemes;
22
23 import static org.testng.AssertJUnit.assertEquals;
24 import static org.testng.AssertJUnit.assertFalse;
25 import static org.testng.AssertJUnit.assertNull;
26 import static org.testng.AssertJUnit.assertTrue;
27 import static org.testng.AssertJUnit.fail;
28 import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
29
30 import jalview.api.FeatureColourI;
31 import jalview.datamodel.SequenceFeature;
32 import jalview.gui.JvOptionPane;
33 import jalview.util.ColorUtils;
34 import jalview.util.Format;
35
36 import java.awt.Color;
37
38 import org.testng.annotations.BeforeClass;
39 import org.testng.annotations.Test;
40
41 public class FeatureColourTest
42 {
43
44   @BeforeClass(alwaysRun = true)
45   public void setUpJvOptionPane()
46   {
47     JvOptionPane.setInteractiveMode(false);
48     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
49   }
50
51   @Test(groups = { "Functional" })
52   public void testConstructors()
53   {
54     FeatureColourI fc = new FeatureColour();
55     assertNull(fc.getColour());
56     assertTrue(fc.isSimpleColour());
57     assertFalse(fc.isColourByLabel());
58     assertFalse(fc.isGraduatedColour());
59     assertFalse(fc.isColourByAttribute());
60     assertEquals(Color.white, fc.getMinColour());
61     assertEquals(Color.black, fc.getMaxColour());
62
63     fc = new FeatureColour(Color.RED);
64     assertEquals(Color.red, fc.getColour());
65     assertTrue(fc.isSimpleColour());
66     assertFalse(fc.isColourByLabel());
67     assertFalse(fc.isGraduatedColour());
68     assertFalse(fc.isColourByAttribute());
69     assertEquals(ColorUtils.bleachColour(Color.RED, 0.9f),
70             fc.getMinColour());
71     assertEquals(Color.RED, fc.getMaxColour());
72
73   }
74
75   @Test(groups = { "Functional" })
76   public void testCopyConstructor()
77   {
78     /*
79      * plain colour
80      */
81     FeatureColour fc = new FeatureColour(Color.RED);
82     FeatureColour fc1 = new FeatureColour(fc);
83     assertTrue(fc1.getColour().equals(Color.RED));
84     assertFalse(fc1.isGraduatedColour());
85     assertFalse(fc1.isColourByLabel());
86     assertFalse(fc1.isColourByAttribute());
87     assertNull(fc1.getAttributeName());
88
89     /*
90      * min-max colour
91      */
92     fc = new FeatureColour(null, Color.gray, Color.black, Color.gray, 10f,
93             20f);
94     fc.setAboveThreshold(true);
95     fc.setThreshold(12f);
96     fc1 = new FeatureColour(fc);
97     assertTrue(fc1.isGraduatedColour());
98     assertFalse(fc1.isColourByLabel());
99     assertTrue(fc1.isAboveThreshold());
100     assertFalse(fc1.isColourByAttribute());
101     assertNull(fc1.getAttributeName());
102     assertEquals(12f, fc1.getThreshold());
103     assertEquals(Color.gray, fc1.getMinColour());
104     assertEquals(Color.black, fc1.getMaxColour());
105     assertEquals(Color.gray, fc1.getNoColour());
106     assertEquals(10f, fc1.getMin());
107     assertEquals(20f, fc1.getMax());
108
109     /*
110      * min-max-noValue colour
111      */
112     fc = new FeatureColour(Color.red, Color.gray, Color.black, Color.green,
113             10f, 20f);
114     fc.setAboveThreshold(true);
115     fc.setThreshold(12f);
116     fc1 = new FeatureColour(fc);
117     assertTrue(fc1.isGraduatedColour());
118     assertFalse(fc1.isColourByLabel());
119     assertFalse(fc1.isSimpleColour());
120     assertFalse(fc1.isColourByAttribute());
121     assertNull(fc1.getAttributeName());
122     assertTrue(fc1.isAboveThreshold());
123     assertEquals(12f, fc1.getThreshold());
124     assertEquals(Color.gray, fc1.getMinColour());
125     assertEquals(Color.black, fc1.getMaxColour());
126     assertEquals(Color.green, fc1.getNoColour());
127     assertEquals(Color.red, fc1.getColour());
128     assertEquals(10f, fc1.getMin());
129     assertEquals(20f, fc1.getMax());
130
131     /*
132      * colour by label
133      */
134     fc = new FeatureColour();
135     fc.setColourByLabel(true);
136     fc1 = new FeatureColour(fc);
137     assertTrue(fc1.isColourByLabel());
138     assertFalse(fc1.isGraduatedColour());
139     assertFalse(fc1.isColourByAttribute());
140     assertNull(fc1.getAttributeName());
141
142     /*
143      * colour by attribute (label)
144      */
145     fc = new FeatureColour();
146     fc.setColourByLabel(true);
147     fc.setAttributeName("AF");
148     fc1 = new FeatureColour(fc);
149     assertTrue(fc1.isColourByLabel());
150     assertFalse(fc1.isGraduatedColour());
151     assertTrue(fc1.isColourByAttribute());
152     assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
153
154     /*
155      * colour by attribute (value)
156      */
157     fc = new FeatureColour(Color.yellow, Color.gray, Color.black,
158             Color.green, 10f, 20f);
159     fc.setAboveThreshold(true);
160     fc.setThreshold(12f);
161     fc.setAttributeName("AF");
162     fc1 = new FeatureColour(fc);
163     assertTrue(fc1.isGraduatedColour());
164     assertFalse(fc1.isColourByLabel());
165     assertTrue(fc1.isColourByAttribute());
166     assertFalse(fc1.isSimpleColour());
167     assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
168     assertTrue(fc1.isAboveThreshold());
169     assertEquals(12f, fc1.getThreshold());
170     assertEquals(Color.gray, fc1.getMinColour());
171     assertEquals(Color.black, fc1.getMaxColour());
172     assertEquals(Color.green, fc1.getNoColour());
173     assertEquals(Color.yellow, fc1.getColour());
174     assertEquals(10f, fc1.getMin());
175     assertEquals(20f, fc1.getMax());
176   }
177
178   @Test(groups = { "Functional" })
179   public void testGetColor_simpleColour()
180   {
181     FeatureColour fc = new FeatureColour(Color.RED);
182     assertEquals(Color.RED,
183             fc.getColor(new SequenceFeature("Cath", "", 1, 2, 0f, null)));
184   }
185
186   @Test(groups = { "Functional" })
187   public void testGetColor_colourByLabel()
188   {
189     FeatureColour fc = new FeatureColour();
190     fc.setColourByLabel(true);
191     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
192             null);
193     Color expected = ColorUtils.createColourFromName("desc");
194     assertEquals(expected, fc.getColor(sf));
195   }
196
197   @Test(groups = { "Functional" })
198   public void testGetColor_Graduated()
199   {
200     /*
201      * graduated colour from 
202      * score 0 to 100
203      * gray(128, 128, 128) to red(255, 0, 0)
204      */
205     FeatureColour fc = new FeatureColour(null, Color.GRAY, Color.RED, null,
206             0f, 100f);
207     // feature score is 75 which is 3/4 of the way from GRAY to RED
208     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
209             null);
210     // the colour gradient is computed in float values from 0-1 (where 1 == 255)
211     float red = 128 / 255f + 3 / 4f * (255 - 128) / 255f;
212     float green = 128 / 255f + 3 / 4f * (0 - 128) / 255f;
213     float blue = 128 / 255f + 3 / 4f * (0 - 128) / 255f;
214     Color expected = new Color(red, green, blue);
215     assertEquals(expected, fc.getColor(sf));
216   }
217
218   @Test(groups = { "Functional" })
219   public void testGetColor_aboveBelowThreshold()
220   {
221     // gradient from [50, 150] from WHITE(255, 255, 255) to BLACK(0, 0, 0)
222     FeatureColour fc = new FeatureColour(null, Color.WHITE, Color.BLACK,
223             Color.white, 50f, 150f);
224     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 70f,
225             null);
226
227     /*
228      * feature with score of Float.NaN is always assigned minimum colour
229      */
230     SequenceFeature sf2 = new SequenceFeature("type", "desc", 0, 20,
231             Float.NaN, null);
232
233     fc.setThreshold(100f); // ignore for now
234     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
235     assertEquals(Color.white, fc.getColor(sf2));
236
237     fc.setAboveThreshold(true); // feature lies below threshold
238     assertNull(fc.getColor(sf));
239     assertEquals(Color.white, fc.getColor(sf2));
240
241     fc.setBelowThreshold(true);
242     fc.setThreshold(70f);
243     assertNull(fc.getColor(sf)); // feature score == threshold - hidden
244     assertEquals(Color.white, fc.getColor(sf2));
245     fc.setThreshold(69f);
246     assertNull(fc.getColor(sf)); // feature score > threshold - hidden
247     assertEquals(Color.white, fc.getColor(sf2));
248   }
249
250   /**
251    * Test output of feature colours to Jalview features file format
252    */
253   @Test(groups = { "Functional" })
254   public void testToJalviewFormat()
255   {
256     /*
257      * plain colour - to RGB hex code
258      */
259     FeatureColour fc = new FeatureColour(Color.RED);
260     String redHex = Format.getHexString(Color.RED);
261     String hexColour = redHex;
262     assertEquals("domain\t" + hexColour, fc.toJalviewFormat("domain"));
263
264     /*
265      * colour by label (no threshold)
266      */
267     fc = new FeatureColour();
268     fc.setColourByLabel(true);
269     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
270
271     /*
272      * colour by attribute text (no threshold)
273      */
274     fc = new FeatureColour();
275     fc.setColourByLabel(true);
276     fc.setAttributeName("CLIN_SIG");
277     assertEquals("domain\tattribute|CLIN_SIG", fc.toJalviewFormat("domain"));
278     
279     /*
280      * colour by label (autoscaled) (an odd state you can reach by selecting
281      * 'above threshold', then deselecting 'threshold is min/max' then 'colour
282      * by label')
283      */
284     fc.setAttributeName((String[]) null);
285     fc.setAutoScaled(true);
286     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
287
288     /*
289      * colour by label (above threshold) 
290      */
291     fc.setAutoScaled(false);
292     fc.setThreshold(12.5f);
293     fc.setAboveThreshold(true);
294     // min/max values are output though not used by this scheme
295     assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
296             fc.toJalviewFormat("domain"));
297
298     /*
299      * colour by label (below threshold)
300      */
301     fc.setBelowThreshold(true);
302     assertEquals("domain\tlabel|||0.0|0.0|below|12.5",
303             fc.toJalviewFormat("domain"));
304
305     /*
306      * colour by attributes text (below threshold)
307      */
308     fc.setBelowThreshold(true);
309     fc.setAttributeName("CSQ", "Consequence");
310     assertEquals("domain\tattribute|CSQ:Consequence|||0.0|0.0|below|12.5",
311             fc.toJalviewFormat("domain"));
312
313     /*
314      * graduated colour by score, no threshold
315      * - default constructor sets noValueColor = minColor
316      */
317     fc = new FeatureColour(null, Color.GREEN, Color.RED, Color.GREEN, 12f,
318             25f);
319     String greenHex = Format.getHexString(Color.GREEN);
320     String expected = String.format(
321             "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
322             redHex);
323     assertEquals(expected, fc.toJalviewFormat("domain"));
324
325     /*
326      * graduated colour by score, no threshold, no value gets min colour
327      */
328     fc = new FeatureColour(Color.RED, Color.GREEN, Color.RED, Color.GREEN,
329             12f, 25f);
330     expected = String.format(
331             "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
332             redHex);
333     assertEquals(expected, fc.toJalviewFormat("domain"));
334
335     /*
336      * graduated colour by score, no threshold, no value gets max colour
337      */
338     fc = new FeatureColour(Color.RED, Color.GREEN, Color.RED, Color.RED,
339             12f, 25f);
340     expected = String.format(
341             "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|none", greenHex,
342             redHex);
343     assertEquals(expected, fc.toJalviewFormat("domain"));
344     
345     /*
346      * colour ranges over the actual score ranges (not min/max)
347      */
348     fc.setAutoScaled(true);
349     expected = String.format(
350             "domain\tscore|%s|%s|noValueMax|12.0|25.0|none", greenHex,
351             redHex);
352     assertEquals(expected, fc.toJalviewFormat("domain"));
353
354     /*
355      * graduated colour by score, below threshold
356      */
357     fc.setThreshold(12.5f);
358     fc.setBelowThreshold(true);
359     expected = String.format(
360             "domain\tscore|%s|%s|noValueMax|12.0|25.0|below|12.5",
361             greenHex, redHex);
362     assertEquals(expected, fc.toJalviewFormat("domain"));
363
364     /*
365      * graduated colour by score, above threshold
366      */
367     fc.setThreshold(12.5f);
368     fc.setAboveThreshold(true);
369     fc.setAutoScaled(false);
370     expected = String.format(
371             "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
372             greenHex, redHex);
373     assertEquals(expected, fc.toJalviewFormat("domain"));
374
375     /*
376      * graduated colour by attribute, above threshold
377      */
378     fc.setAttributeName("CSQ", "AF");
379     fc.setAboveThreshold(true);
380     fc.setAutoScaled(false);
381     expected = String.format(
382             "domain\tattribute|CSQ:AF|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
383             greenHex, redHex);
384     assertEquals(expected, fc.toJalviewFormat("domain"));
385   }
386
387   /**
388    * Test parsing of feature colours from Jalview features file format
389    */
390   @Test(groups = { "Functional" })
391   public void testParseJalviewFeatureColour()
392   {
393     /*
394      * simple colour by name
395      */
396     FeatureColourI fc = FeatureColour.parseJalviewFeatureColour("red");
397     assertTrue(fc.isSimpleColour());
398     assertEquals(Color.RED, fc.getColour());
399
400     /*
401      * simple colour by hex code
402      */
403     fc = FeatureColour.parseJalviewFeatureColour(Format
404             .getHexString(Color.RED));
405     assertTrue(fc.isSimpleColour());
406     assertEquals(Color.RED, fc.getColour());
407
408     /*
409      * simple colour by rgb triplet
410      */
411     fc = FeatureColour.parseJalviewFeatureColour("255,0,0");
412     assertTrue(fc.isSimpleColour());
413     assertEquals(Color.RED, fc.getColour());
414
415     /*
416      * malformed colour
417      */
418     try
419     {
420       fc = FeatureColour.parseJalviewFeatureColour("oops");
421       fail("expected exception");
422     } catch (IllegalArgumentException e)
423     {
424       assertEquals("Invalid colour descriptor: oops", e.getMessage());
425     }
426
427     /*
428      * colour by label (no threshold)
429      */
430     fc = FeatureColour.parseJalviewFeatureColour("label");
431     assertTrue(fc.isColourByLabel());
432     assertFalse(fc.hasThreshold());
433
434     /*
435      * colour by label (with threshold)
436      */
437     fc = FeatureColour
438             .parseJalviewFeatureColour("label|||0.0|0.0|above|12.0");
439     assertTrue(fc.isColourByLabel());
440     assertTrue(fc.isAboveThreshold());
441     assertEquals(12.0f, fc.getThreshold());
442
443     /*
444      * colour by attribute text (no threshold)
445      */
446     fc = FeatureColour.parseJalviewFeatureColour("attribute|CLIN_SIG");
447     assertTrue(fc.isColourByAttribute());
448     assertTrue(fc.isColourByLabel());
449     assertFalse(fc.hasThreshold());
450     assertArrayEquals(new String[] { "CLIN_SIG" }, fc.getAttributeName());
451
452     /*
453      * colour by attributes text (with score threshold)
454      */
455     fc = FeatureColour.parseJalviewFeatureColour(
456             "attribute|CSQ:Consequence|||0.0|0.0|above|12.0");
457     assertTrue(fc.isColourByLabel());
458     assertTrue(fc.isColourByAttribute());
459     assertArrayEquals(new String[] { "CSQ", "Consequence" },
460             fc.getAttributeName());
461     assertTrue(fc.isAboveThreshold());
462     assertEquals(12.0f, fc.getThreshold());
463
464     /*
465      * graduated colour by score (with colour names) (no threshold)
466      */
467     fc = FeatureColour.parseJalviewFeatureColour("red|green|10.0|20.0");
468     assertTrue(fc.isGraduatedColour());
469     assertFalse(fc.hasThreshold());
470     assertEquals(Color.RED, fc.getMinColour());
471     assertEquals(Color.GREEN, fc.getMaxColour());
472     assertEquals(10f, fc.getMin());
473     assertEquals(20f, fc.getMax());
474     assertTrue(fc.isAutoScaled());
475
476     /*
477      * graduated colour (explicitly by 'score') (no threshold)
478      */
479     fc = FeatureColour
480             .parseJalviewFeatureColour("Score|red|green|10.0|20.0");
481     assertTrue(fc.isGraduatedColour());
482     assertFalse(fc.hasThreshold());
483     assertEquals(Color.RED, fc.getMinColour());
484     assertEquals(Color.GREEN, fc.getMaxColour());
485     assertEquals(10f, fc.getMin());
486     assertEquals(20f, fc.getMax());
487     assertTrue(fc.isAutoScaled());
488
489     /*
490      * graduated colour by attribute (no threshold)
491      */
492     fc = FeatureColour
493             .parseJalviewFeatureColour("attribute|AF|red|green|10.0|20.0");
494     assertTrue(fc.isGraduatedColour());
495     assertTrue(fc.isColourByAttribute());
496     assertArrayEquals(new String[] { "AF" }, fc.getAttributeName());
497     assertFalse(fc.hasThreshold());
498     assertEquals(Color.RED, fc.getMinColour());
499     assertEquals(Color.GREEN, fc.getMaxColour());
500     assertEquals(10f, fc.getMin());
501     assertEquals(20f, fc.getMax());
502     assertTrue(fc.isAutoScaled());
503
504     /*
505      * graduated colour by score (colours by hex code) (above threshold)
506      */
507     String descriptor = String.format("%s|%s|10.0|20.0|above|15",
508             Format.getHexString(Color.RED),
509             Format.getHexString(Color.GREEN));
510     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
511     assertTrue(fc.isGraduatedColour());
512     assertTrue(fc.hasThreshold());
513     assertTrue(fc.isAboveThreshold());
514     assertEquals(15f, fc.getThreshold());
515     assertEquals(Color.RED, fc.getMinColour());
516     assertEquals(Color.GREEN, fc.getMaxColour());
517     assertEquals(10f, fc.getMin());
518     assertEquals(20f, fc.getMax());
519     assertTrue(fc.isAutoScaled());
520
521     /*
522      * graduated colour by attributes (below threshold)
523      */
524     fc = FeatureColour.parseJalviewFeatureColour(
525             "attribute|CSQ:AF|red|green|10.0|20.0|below|13");
526     assertTrue(fc.isGraduatedColour());
527     assertTrue(fc.isColourByAttribute());
528     assertArrayEquals(new String[] { "CSQ", "AF" }, fc.getAttributeName());
529     assertTrue(fc.hasThreshold());
530     assertTrue(fc.isBelowThreshold());
531     assertEquals(13f, fc.getThreshold());
532     assertEquals(Color.RED, fc.getMinColour());
533     assertEquals(Color.GREEN, fc.getMaxColour());
534     assertEquals(10f, fc.getMin());
535     assertEquals(20f, fc.getMax());
536     assertTrue(fc.isAutoScaled());
537
538     /*
539      * graduated colour (by RGB triplet) (below threshold), absolute scale
540      */
541     descriptor = "255,0,0|0,255,0|abso|10.0|20.0|below|15";
542     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
543     assertTrue(fc.isGraduatedColour());
544     assertFalse(fc.isAutoScaled());
545     assertTrue(fc.hasThreshold());
546     assertTrue(fc.isBelowThreshold());
547     assertEquals(15f, fc.getThreshold());
548     assertEquals(Color.RED, fc.getMinColour());
549     assertEquals(Color.GREEN, fc.getMaxColour());
550     assertEquals(10f, fc.getMin());
551     assertEquals(20f, fc.getMax());
552
553     descriptor = "blue|255,0,255|absolute|20.0|95.0|below|66.0";
554     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
555     assertTrue(fc.isGraduatedColour());
556   }
557
558   @Test(groups = { "Functional" })
559   public void testGetColor_colourByAttributeText()
560   {
561     FeatureColour fc = new FeatureColour();
562     fc.setColourByLabel(true);
563     fc.setAttributeName("consequence");
564     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
565             null);
566
567     /*
568      * if feature has no such attribute, use 'no value' colour
569      */
570     assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf));
571
572     /*
573      * if feature has attribute, generate colour from value
574      */
575     sf.setValue("consequence", "benign");
576     Color expected = ColorUtils.createColourFromName("benign");
577     assertEquals(expected, fc.getColor(sf));
578   }
579
580   @Test(groups = { "Functional" })
581   public void testGetColor_GraduatedByAttributeValue()
582   {
583     /*
584      * graduated colour based on attribute value for AF
585      * given a min-max range of 0-100
586      */
587     FeatureColour fc = new FeatureColour(Color.white,
588             new Color(50, 100, 150), new Color(150, 200, 250), Color.yellow,
589             0f, 100f);
590     String attName = "AF";
591     fc.setAttributeName(attName);
592
593     /*
594      * first case: feature lacks the attribute - use 'no value' colour
595      */
596     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
597             null);
598     assertEquals(Color.yellow, fc.getColor(sf));
599
600     /*
601      * second case: attribute present but not numeric - treat as if absent
602      */
603     sf.setValue(attName, "twelve");
604     assertEquals(Color.yellow, fc.getColor(sf));
605
606     /*
607      * third case: valid attribute value
608      */
609     sf.setValue(attName, "20.0");
610     Color expected = new Color(70, 120, 170);
611     assertEquals(expected, fc.getColor(sf));
612   }
613 }