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