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