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