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