JAL-2069 spike updated with latest (FeatureTypeSettings)
[jalview.git] / src / jalview / schemes / FeatureColour.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 jalview.api.FeatureColourI;
24 import jalview.datamodel.SequenceFeature;
25 import jalview.util.ColorUtils;
26 import jalview.util.Format;
27
28 import java.awt.Color;
29 import java.util.StringTokenizer;
30
31 /**
32  * A class that represents a colour scheme for a feature type. Options supported
33  * are currently
34  * <ul>
35  * <li>a simple colour e.g. Red</li>
36  * <li>colour by label - a colour is generated from the feature description</li>
37  * <li>graduated colour by feature score</li>
38  * <ul>
39  * <li>minimum and maximum score range must be provided</li>
40  * <li>minimum and maximum value colours should be specified</li>
41  * <li>a colour for 'no value' may optionally be provided</li>
42  * <li>colours for intermediate scores are interpolated RGB values</li>
43  * <li>there is an optional threshold above/below which to colour values</li>
44  * <li>the range may be the full value range, or may be limited by the threshold
45  * value</li>
46  * </ul>
47  * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
48  * (numeric) value of a named attribute</li> </ul>
49  */
50 public class FeatureColour implements FeatureColourI
51 {
52   static final Color DEFAULT_NO_COLOUR = null;
53
54   private static final String BAR = "|";
55
56   final private Color colour;
57
58   final private Color minColour;
59
60   final private Color maxColour;
61
62   /*
63    * colour to use for colour by attribute when the 
64    * attribute value is absent
65    */
66   final private Color noColour;
67
68   /*
69    * if true, then colour has a gradient based on a numerical 
70    * range (either feature score, or an attribute value)
71    */
72   private boolean graduatedColour;
73
74   /*
75    * if true, colour values are generated from a text string,
76    * either feature description, or an attribute value
77    */
78   private boolean colourByLabel;
79
80   /*
81    * if not null, the value of [attribute, [sub-attribute] ...]
82    *  is used for colourByLabel or graduatedColour
83    */
84   private String[] attributeName;
85
86   private float threshold;
87
88   private float base;
89
90   private float range;
91
92   private boolean belowThreshold;
93
94   private boolean aboveThreshold;
95
96   private boolean isHighToLow;
97
98   private boolean autoScaled;
99
100   final private float minRed;
101
102   final private float minGreen;
103
104   final private float minBlue;
105
106   final private float deltaRed;
107
108   final private float deltaGreen;
109
110   final private float deltaBlue;
111
112   /**
113    * Parses a Jalview features file format colour descriptor
114    * [label|][mincolour|maxcolour
115    * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
116    * <ul>
117    * <li>red</li>
118    * <li>a28bbb</li>
119    * <li>25,125,213</li>
120    * <li>label</li>
121    * <li>label|||0.0|0.0|above|12.5</li>
122    * <li>label|||0.0|0.0|below|12.5</li>
123    * <li>red|green|12.0|26.0|none</li>
124    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
125    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
126    * </ul>
127    * 
128    * @param descriptor
129    * @return
130    * @throws IllegalArgumentException
131    *           if not parseable
132    */
133   public static FeatureColour parseJalviewFeatureColour(String descriptor)
134   {
135     StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
136     float min = Float.MIN_VALUE;
137     float max = Float.MAX_VALUE;
138     boolean labelColour = false;
139
140     String mincol = gcol.nextToken();
141     if (mincol == "|")
142     {
143       throw new IllegalArgumentException(
144               "Expected either 'label' or a colour specification in the line: "
145                       + descriptor);
146     }
147     String maxcol = null;
148     if (mincol.toLowerCase().indexOf("label") == 0)
149     {
150       labelColour = true;
151       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
152       // skip '|'
153       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
154     }
155
156     if (!labelColour && !gcol.hasMoreTokens())
157     {
158       /*
159        * only a simple colour specification - parse it
160        */
161       Color colour = ColorUtils.parseColourString(descriptor);
162       if (colour == null)
163       {
164         throw new IllegalArgumentException(
165                 "Invalid colour descriptor: " + descriptor);
166       }
167       return new FeatureColour(colour);
168     }
169
170     /*
171      * autoScaled == true: colours range over actual score range
172      * autoScaled == false ('abso'): colours range over min/max range
173      */
174     boolean autoScaled = true;
175     String tok = null, minval, maxval;
176     if (mincol != null)
177     {
178       // at least four more tokens
179       if (mincol.equals("|"))
180       {
181         mincol = "";
182       }
183       else
184       {
185         gcol.nextToken(); // skip next '|'
186       }
187       maxcol = gcol.nextToken();
188       if (maxcol.equals("|"))
189       {
190         maxcol = "";
191       }
192       else
193       {
194         gcol.nextToken(); // skip next '|'
195       }
196       tok = gcol.nextToken();
197       gcol.nextToken(); // skip next '|'
198       if (tok.toLowerCase().startsWith("abso"))
199       {
200         minval = gcol.nextToken();
201         gcol.nextToken(); // skip next '|'
202         autoScaled = false;
203       }
204       else
205       {
206         minval = tok;
207       }
208       maxval = gcol.nextToken();
209       if (gcol.hasMoreTokens())
210       {
211         gcol.nextToken(); // skip next '|'
212       }
213       try
214       {
215         if (minval.length() > 0)
216         {
217           min = new Float(minval).floatValue();
218         }
219       } catch (Exception e)
220       {
221         throw new IllegalArgumentException(
222                 "Couldn't parse the minimum value for graduated colour ("
223                         + descriptor + ")");
224       }
225       try
226       {
227         if (maxval.length() > 0)
228         {
229           max = new Float(maxval).floatValue();
230         }
231       } catch (Exception e)
232       {
233         throw new IllegalArgumentException(
234                 "Couldn't parse the maximum value for graduated colour ("
235                         + descriptor + ")");
236       }
237     }
238     else
239     {
240       // add in some dummy min/max colours for the label-only
241       // colourscheme.
242       mincol = "FFFFFF";
243       maxcol = "000000";
244     }
245
246     /*
247      * construct the FeatureColour
248      */
249     FeatureColour featureColour;
250     try
251     {
252       Color minColour = ColorUtils.parseColourString(mincol);
253       Color maxColour = ColorUtils.parseColourString(maxcol);
254       featureColour = new FeatureColour(minColour, maxColour, min, max);
255       featureColour.setColourByLabel(labelColour);
256       featureColour.setAutoScaled(autoScaled);
257       // add in any additional parameters
258       String ttype = null, tval = null;
259       if (gcol.hasMoreTokens())
260       {
261         // threshold type and possibly a threshold value
262         ttype = gcol.nextToken();
263         if (ttype.toLowerCase().startsWith("below"))
264         {
265           featureColour.setBelowThreshold(true);
266         }
267         else if (ttype.toLowerCase().startsWith("above"))
268         {
269           featureColour.setAboveThreshold(true);
270         }
271         else
272         {
273           if (!ttype.toLowerCase().startsWith("no"))
274           {
275             System.err.println(
276                     "Ignoring unrecognised threshold type : " + ttype);
277           }
278         }
279       }
280       if (featureColour.hasThreshold())
281       {
282         try
283         {
284           gcol.nextToken();
285           tval = gcol.nextToken();
286           featureColour.setThreshold(new Float(tval).floatValue());
287         } catch (Exception e)
288         {
289           System.err.println("Couldn't parse threshold value as a float: ("
290                   + tval + ")");
291         }
292       }
293       if (gcol.hasMoreTokens())
294       {
295         System.err.println(
296                 "Ignoring additional tokens in parameters in graduated colour specification\n");
297         while (gcol.hasMoreTokens())
298         {
299           System.err.println("|" + gcol.nextToken());
300         }
301         System.err.println("\n");
302       }
303       return featureColour;
304     } catch (Exception e)
305     {
306       throw new IllegalArgumentException(e.getMessage());
307     }
308   }
309
310   /**
311    * Default constructor
312    */
313   public FeatureColour()
314   {
315     this((Color) null);
316   }
317
318   /**
319    * Constructor given a simple colour
320    * 
321    * @param c
322    */
323   public FeatureColour(Color c)
324   {
325     minColour = Color.WHITE;
326     maxColour = Color.BLACK;
327     noColour = DEFAULT_NO_COLOUR;
328     minRed = 0f;
329     minGreen = 0f;
330     minBlue = 0f;
331     deltaRed = 0f;
332     deltaGreen = 0f;
333     deltaBlue = 0f;
334     colour = c;
335   }
336
337   /**
338    * Constructor given a colour range and a score range, defaulting 'no value
339    * colour' to be the same as minimum colour
340    * 
341    * @param low
342    * @param high
343    * @param min
344    * @param max
345    */
346   public FeatureColour(Color low, Color high, float min, float max)
347   {
348     this(low, high, low, min, max);
349   }
350
351   /**
352    * Copy constructor
353    * 
354    * @param fc
355    */
356   public FeatureColour(FeatureColour fc)
357   {
358     graduatedColour = fc.graduatedColour;
359     colour = fc.colour;
360     minColour = fc.minColour;
361     maxColour = fc.maxColour;
362     noColour = fc.noColour;
363     minRed = fc.minRed;
364     minGreen = fc.minGreen;
365     minBlue = fc.minBlue;
366     deltaRed = fc.deltaRed;
367     deltaGreen = fc.deltaGreen;
368     deltaBlue = fc.deltaBlue;
369     base = fc.base;
370     range = fc.range;
371     isHighToLow = fc.isHighToLow;
372     attributeName = fc.attributeName;
373     setAboveThreshold(fc.isAboveThreshold());
374     setBelowThreshold(fc.isBelowThreshold());
375     setThreshold(fc.getThreshold());
376     setAutoScaled(fc.isAutoScaled());
377     setColourByLabel(fc.isColourByLabel());
378   }
379
380   /**
381    * Copy constructor with new min/max ranges
382    * 
383    * @param fc
384    * @param min
385    * @param max
386    */
387   public FeatureColour(FeatureColour fc, float min, float max)
388   {
389     this(fc);
390     updateBounds(min, max);
391   }
392
393   /**
394    * Constructor for a graduated colour
395    * 
396    * @param low
397    * @param high
398    * @param noValueColour
399    * @param min
400    * @param max
401    */
402   public FeatureColour(Color low, Color high, Color noValueColour,
403           float min, float max)
404   {
405     if (low == null)
406     {
407       low = Color.white;
408     }
409     if (high == null)
410     {
411       high = Color.black;
412     }
413     graduatedColour = true;
414     colour = null;
415     minColour = low;
416     maxColour = high;
417     noColour = noValueColour;
418     threshold = Float.NaN;
419     isHighToLow = min >= max;
420     minRed = low.getRed() / 255f;
421     minGreen = low.getGreen() / 255f;
422     minBlue = low.getBlue() / 255f;
423     deltaRed = (high.getRed() / 255f) - minRed;
424     deltaGreen = (high.getGreen() / 255f) - minGreen;
425     deltaBlue = (high.getBlue() / 255f) - minBlue;
426     if (isHighToLow)
427     {
428       base = max;
429       range = min - max;
430     }
431     else
432     {
433       base = min;
434       range = max - min;
435     }
436   }
437
438   @Override
439   public boolean isGraduatedColour()
440   {
441     return graduatedColour;
442   }
443
444   /**
445    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
446    * false.
447    */
448   void setGraduatedColour(boolean b)
449   {
450     graduatedColour = b;
451     if (b)
452     {
453       setColourByLabel(false);
454     }
455   }
456
457   @Override
458   public Color getColour()
459   {
460     return colour;
461   }
462
463   @Override
464   public Color getMinColour()
465   {
466     return minColour;
467   }
468
469   @Override
470   public Color getMaxColour()
471   {
472     return maxColour;
473   }
474
475   @Override
476   public Color getNoColour()
477   {
478     return noColour;
479   }
480
481   @Override
482   public boolean isColourByLabel()
483   {
484     return colourByLabel;
485   }
486
487   /**
488    * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
489    * false.
490    */
491   @Override
492   public void setColourByLabel(boolean b)
493   {
494     colourByLabel = b;
495     if (b)
496     {
497       setGraduatedColour(false);
498     }
499   }
500
501   @Override
502   public boolean isBelowThreshold()
503   {
504     return belowThreshold;
505   }
506
507   @Override
508   public void setBelowThreshold(boolean b)
509   {
510     belowThreshold = b;
511     if (b)
512     {
513       setAboveThreshold(false);
514     }
515   }
516
517   @Override
518   public boolean isAboveThreshold()
519   {
520     return aboveThreshold;
521   }
522
523   @Override
524   public void setAboveThreshold(boolean b)
525   {
526     aboveThreshold = b;
527     if (b)
528     {
529       setBelowThreshold(false);
530     }
531   }
532
533   @Override
534   public float getThreshold()
535   {
536     return threshold;
537   }
538
539   @Override
540   public void setThreshold(float f)
541   {
542     threshold = f;
543   }
544
545   @Override
546   public boolean isAutoScaled()
547   {
548     return autoScaled;
549   }
550
551   @Override
552   public void setAutoScaled(boolean b)
553   {
554     this.autoScaled = b;
555   }
556
557   /**
558    * {@inheritDoc}
559    */
560   @Override
561   public void updateBounds(float min, float max)
562   {
563     if (max < min)
564     {
565       base = max;
566       range = min - max;
567       isHighToLow = true;
568     }
569     else
570     {
571       base = min;
572       range = max - min;
573       isHighToLow = false;
574     }
575   }
576
577   /**
578    * Returns the colour for the given instance of the feature. This may be a
579    * simple colour, a colour generated from the feature description (if
580    * isColourByLabel()), or a colour derived from the feature score (if
581    * isGraduatedColour()).
582    * 
583    * @param feature
584    * @return
585    */
586   @Override
587   public Color getColor(SequenceFeature feature)
588   {
589     if (isColourByLabel())
590     {
591       String label = attributeName == null ? feature.getDescription()
592               : feature.getValueAsString(attributeName);
593       return label == null ? noColour : ColorUtils
594               .createColourFromName(label);
595     }
596
597     if (!isGraduatedColour())
598     {
599       return getColour();
600     }
601
602     /*
603      * graduated colour case, optionally with threshold
604      * may be based on feature score on an attribute value
605      * Float.NaN, or no value, is assigned the 'no value' colour
606      */
607     float scr = feature.getScore();
608     if (attributeName != null)
609     {
610       try
611       {
612         String attVal = feature.getValueAsString(attributeName);
613         scr = Float.valueOf(attVal);
614       } catch (Throwable e)
615       {
616         scr = Float.NaN;
617       }
618     }
619     if (Float.isNaN(scr))
620     {
621       return noColour;
622     }
623
624     if (isAboveThreshold() && scr <= threshold)
625     {
626       return null;
627     }
628
629     if (isBelowThreshold() && scr >= threshold)
630     {
631       return null;
632     }
633     if (range == 0.0)
634     {
635       return getMaxColour();
636     }
637     float scl = (scr - base) / range;
638     if (isHighToLow)
639     {
640       scl = -scl;
641     }
642     if (scl < 0f)
643     {
644       scl = 0f;
645     }
646     if (scl > 1f)
647     {
648       scl = 1f;
649     }
650     return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
651             minBlue + scl * deltaBlue);
652   }
653
654   /**
655    * Returns the maximum score of the graduated colour range
656    * 
657    * @return
658    */
659   @Override
660   public float getMax()
661   {
662     // regenerate the original values passed in to the constructor
663     return (isHighToLow) ? base : (base + range);
664   }
665
666   /**
667    * Returns the minimum score of the graduated colour range
668    * 
669    * @return
670    */
671   @Override
672   public float getMin()
673   {
674     // regenerate the original value passed in to the constructor
675     return (isHighToLow) ? (base + range) : base;
676   }
677
678   @Override
679   public boolean isSimpleColour()
680   {
681     return (!isColourByLabel() && !isGraduatedColour());
682   }
683
684   @Override
685   public boolean hasThreshold()
686   {
687     return isAboveThreshold() || isBelowThreshold();
688   }
689
690   @Override
691   public String toJalviewFormat(String featureType)
692   {
693     String colourString = null;
694     if (isSimpleColour())
695     {
696       colourString = Format.getHexString(getColour());
697     }
698     else
699     {
700       StringBuilder sb = new StringBuilder(32);
701       if (isColourByLabel())
702       {
703         sb.append("label");
704         if (hasThreshold())
705         {
706           sb.append(BAR).append(BAR).append(BAR);
707         }
708       }
709       if (isGraduatedColour())
710       {
711         sb.append(Format.getHexString(getMinColour())).append(BAR);
712         sb.append(Format.getHexString(getMaxColour())).append(BAR);
713         if (!isAutoScaled())
714         {
715           sb.append("abso").append(BAR);
716         }
717       }
718       if (hasThreshold() || isGraduatedColour())
719       {
720         sb.append(getMin()).append(BAR);
721         sb.append(getMax()).append(BAR);
722         if (isBelowThreshold())
723         {
724           sb.append("below").append(BAR).append(getThreshold());
725         }
726         else if (isAboveThreshold())
727         {
728           sb.append("above").append(BAR).append(getThreshold());
729         }
730         else
731         {
732           sb.append("none");
733         }
734       }
735       colourString = sb.toString();
736     }
737     return String.format("%s\t%s", featureType, colourString);
738   }
739
740   @Override
741   public boolean isColourByAttribute()
742   {
743     return attributeName != null;
744   }
745
746   @Override
747   public String[] getAttributeName()
748   {
749     return attributeName;
750   }
751
752   @Override
753   public void setAttributeName(String... name)
754   {
755     attributeName = name;
756   }
757
758 }