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