JAL-2360 refactoring for JalviewColourScheme enum,
[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("Invalid colour descriptor: "
129                 + 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       featureColour = new FeatureColour(
217               new UserColourScheme(mincol).findColour(),
218               new UserColourScheme(maxcol).findColour(), 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("Ignoring unrecognised threshold type : "
240                     + 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
260                 .println("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     graduatedColour = true;
311     colour = null;
312     minColour = low;
313     maxColour = high;
314     threshold = Float.NaN;
315     isHighToLow = min >= max;
316     minRed = low.getRed() / 255f;
317     minGreen = low.getGreen() / 255f;
318     minBlue = low.getBlue() / 255f;
319     deltaRed = (high.getRed() / 255f) - minRed;
320     deltaGreen = (high.getGreen() / 255f) - minGreen;
321     deltaBlue = (high.getBlue() / 255f) - minBlue;
322     if (isHighToLow)
323     {
324       base = max;
325       range = min - max;
326     }
327     else
328     {
329       base = min;
330       range = max - min;
331     }
332   }
333
334   /**
335    * Copy constructor
336    * 
337    * @param fc
338    */
339   public FeatureColour(FeatureColour fc)
340   {
341     graduatedColour = fc.graduatedColour;
342     colour = fc.colour;
343     minColour = fc.minColour;
344     maxColour = fc.maxColour;
345     minRed = fc.minRed;
346     minGreen = fc.minGreen;
347     minBlue = fc.minBlue;
348     deltaRed = fc.deltaRed;
349     deltaGreen = fc.deltaGreen;
350     deltaBlue = fc.deltaBlue;
351     base = fc.base;
352     range = fc.range;
353     isHighToLow = fc.isHighToLow;
354     setAboveThreshold(fc.isAboveThreshold());
355     setBelowThreshold(fc.isBelowThreshold());
356     setThreshold(fc.getThreshold());
357     setAutoScaled(fc.isAutoScaled());
358     setColourByLabel(fc.isColourByLabel());
359   }
360
361   /**
362    * Copy constructor with new min/max ranges
363    * 
364    * @param fc
365    * @param min
366    * @param max
367    */
368   public FeatureColour(FeatureColour fc, float min, float max)
369   {
370     this(fc);
371     graduatedColour = true;
372     updateBounds(min, max);
373   }
374
375   @Override
376   public boolean isGraduatedColour()
377   {
378     return graduatedColour;
379   }
380
381   /**
382    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
383    * false.
384    */
385   void setGraduatedColour(boolean b)
386   {
387     graduatedColour = b;
388     if (b)
389     {
390       setColourByLabel(false);
391     }
392   }
393
394   @Override
395   public Color getColour()
396   {
397     return colour;
398   }
399
400   @Override
401   public Color getMinColour()
402   {
403     return minColour;
404   }
405
406   @Override
407   public Color getMaxColour()
408   {
409     return maxColour;
410   }
411
412   @Override
413   public boolean isColourByLabel()
414   {
415     return colourByLabel;
416   }
417
418   /**
419    * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
420    * false.
421    */
422   @Override
423   public void setColourByLabel(boolean b)
424   {
425     colourByLabel = b;
426     if (b)
427     {
428       setGraduatedColour(false);
429     }
430   }
431
432   @Override
433   public boolean isBelowThreshold()
434   {
435     return belowThreshold;
436   }
437
438   @Override
439   public void setBelowThreshold(boolean b)
440   {
441     belowThreshold = b;
442     if (b)
443     {
444       setAboveThreshold(false);
445     }
446   }
447
448   @Override
449   public boolean isAboveThreshold()
450   {
451     return aboveThreshold;
452   }
453
454   @Override
455   public void setAboveThreshold(boolean b)
456   {
457     aboveThreshold = b;
458     if (b)
459     {
460       setBelowThreshold(false);
461     }
462   }
463
464   @Override
465   public boolean isThresholdMinMax()
466   {
467     return thresholdIsMinOrMax;
468   }
469
470   @Override
471   public void setThresholdMinMax(boolean b)
472   {
473     thresholdIsMinOrMax = b;
474   }
475
476   @Override
477   public float getThreshold()
478   {
479     return threshold;
480   }
481
482   @Override
483   public void setThreshold(float f)
484   {
485     threshold = f;
486   }
487
488   @Override
489   public boolean isAutoScaled()
490   {
491     return autoScaled;
492   }
493
494   @Override
495   public void setAutoScaled(boolean b)
496   {
497     this.autoScaled = b;
498   }
499
500   /**
501    * Updates the base and range appropriately for the given minmax range
502    * 
503    * @param min
504    * @param max
505    */
506   @Override
507   public void updateBounds(float min, float max)
508   {
509     if (max < min)
510     {
511       base = max;
512       range = min - max;
513       isHighToLow = true;
514     }
515     else
516     {
517       base = min;
518       range = max - min;
519       isHighToLow = false;
520     }
521   }
522
523   /**
524    * Returns the colour for the given instance of the feature. This may be a
525    * simple colour, a colour generated from the feature description (if
526    * isColourByLabel()), or a colour derived from the feature score (if
527    * isGraduatedColour()).
528    * 
529    * @param feature
530    * @return
531    */
532   @Override
533   public Color getColor(SequenceFeature feature)
534   {
535     if (isColourByLabel())
536     {
537       return ColorUtils
538               .createColourFromName(feature.getDescription());
539     }
540
541     if (!isGraduatedColour())
542     {
543       return getColour();
544     }
545
546     // todo should we check for above/below threshold here?
547     if (range == 0.0)
548     {
549       return getMaxColour();
550     }
551     float scr = feature.getScore();
552     if (Float.isNaN(scr))
553     {
554       return getMinColour();
555     }
556     float scl = (scr - base) / range;
557     if (isHighToLow)
558     {
559       scl = -scl;
560     }
561     if (scl < 0f)
562     {
563       scl = 0f;
564     }
565     if (scl > 1f)
566     {
567       scl = 1f;
568     }
569     return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
570             minBlue + scl * deltaBlue);
571   }
572
573   /**
574    * Returns the maximum score of the graduated colour range
575    * 
576    * @return
577    */
578   @Override
579   public float getMax()
580   {
581     // regenerate the original values passed in to the constructor
582     return (isHighToLow) ? base : (base + range);
583   }
584
585   /**
586    * Returns the minimum score of the graduated colour range
587    * 
588    * @return
589    */
590   @Override
591   public float getMin()
592   {
593     // regenerate the original value passed in to the constructor
594     return (isHighToLow) ? (base + range) : base;
595   }
596
597   /**
598    * Answers true if the feature has a simple colour, or is coloured by label,
599    * or has a graduated colour and the score of this feature instance is within
600    * the range to render (if any), i.e. does not lie below or above any
601    * threshold set.
602    * 
603    * @param feature
604    * @return
605    */
606   @Override
607   public boolean isColored(SequenceFeature feature)
608   {
609     if (isColourByLabel() || !isGraduatedColour())
610     {
611       return true;
612     }
613
614     float val = feature.getScore();
615     if (Float.isNaN(val))
616     {
617       return true;
618     }
619     if (Float.isNaN(this.threshold))
620     {
621       return true;
622     }
623
624     if (isAboveThreshold() && val <= threshold)
625     {
626       return false;
627     }
628     if (isBelowThreshold() && val >= threshold)
629     {
630       return false;
631     }
632     return true;
633   }
634
635   @Override
636   public boolean isSimpleColour()
637   {
638     return (!isColourByLabel() && !isGraduatedColour());
639   }
640
641   @Override
642   public boolean hasThreshold()
643   {
644     return isAboveThreshold() || isBelowThreshold();
645   }
646
647   @Override
648   public String toJalviewFormat(String featureType)
649   {
650     String colourString = null;
651     if (isSimpleColour())
652     {
653       colourString = Format.getHexString(getColour());
654     }
655     else
656     {
657       StringBuilder sb = new StringBuilder(32);
658       if (isColourByLabel())
659       {
660         sb.append("label");
661         if (hasThreshold())
662         {
663           sb.append(BAR).append(BAR).append(BAR);
664         }
665       }
666       if (isGraduatedColour())
667       {
668         sb.append(Format.getHexString(getMinColour())).append(BAR);
669         sb.append(Format.getHexString(getMaxColour())).append(BAR);
670         if (!isAutoScaled())
671         {
672           sb.append("abso").append(BAR);
673         }
674       }
675       if (hasThreshold() || isGraduatedColour())
676       {
677         sb.append(getMin()).append(BAR);
678         sb.append(getMax()).append(BAR);
679         if (isBelowThreshold())
680         {
681           sb.append("below").append(BAR).append(getThreshold());
682         }
683         else if (isAboveThreshold())
684         {
685           sb.append("above").append(BAR).append(getThreshold());
686         }
687         else
688         {
689           sb.append("none");
690         }
691       }
692       colourString = sb.toString();
693     }
694     return String.format("%s\t%s", featureType, colourString);
695   }
696
697 }