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