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