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