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