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