85a0b6cb26f840db17e4fbfcfd1c2f7f66af2894
[jalview.git] / src / jalview / util / ColorUtils.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 /**
22  * author: Lauren Michelle Lui
23  */
24
25 package jalview.util;
26
27 import java.util.Locale;
28
29 import java.awt.Color;
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Random;
33
34 public class ColorUtils
35 {
36   private static final int MAX_CACHE_SIZE = 1729;
37   /*
38    * a cache for colours generated from text strings
39    */
40   static Map<String, Color> myColours = new HashMap<>();
41
42   /**
43    * Generates a random color, will mix with input color. Code taken from
44    * http://stackoverflow
45    * .com/questions/43044/algorithm-to-randomly-generate-an-aesthetically
46    * -pleasing-color-palette
47    * 
48    * @param mix
49    * @return Random color in RGB
50    */
51   public static final Color generateRandomColor(Color mix)
52   {
53     Random random = new Random();
54     int red = random.nextInt(256);
55     int green = random.nextInt(256);
56     int blue = random.nextInt(256);
57
58     // mix the color
59     if (mix != null)
60     {
61       red = (red + mix.getRed()) / 2;
62       green = (green + mix.getGreen()) / 2;
63       blue = (blue + mix.getBlue()) / 2;
64     }
65
66     Color color = new Color(red, green, blue);
67     return color;
68
69   }
70
71   /**
72    * Convert to Tk colour code format
73    * 
74    * @param colour
75    * @return
76    * @see http
77    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/colortool.html#
78    *      tkcode
79    */
80   public static final String toTkCode(Color colour)
81   {
82     String colstring = "#" + ((colour.getRed() < 16) ? "0" : "")
83             + Integer.toHexString(colour.getRed())
84             + ((colour.getGreen() < 16) ? "0" : "")
85             + Integer.toHexString(colour.getGreen())
86             + ((colour.getBlue() < 16) ? "0" : "")
87             + Integer.toHexString(colour.getBlue());
88     return colstring;
89   }
90
91   /**
92    * Returns a colour three shades darker. Note you can't guarantee that
93    * brighterThan reverses this, as darkerThan may result in black.
94    * 
95    * @param col
96    * @return
97    */
98   public static Color darkerThan(Color col)
99   {
100     return col == null ? null : col.darker().darker().darker();
101   }
102
103   /**
104    * Returns a colour three shades brighter. Note you can't guarantee that
105    * darkerThan reverses this, as brighterThan may result in white.
106    * 
107    * @param col
108    * @return
109    */
110   public static Color brighterThan(Color col)
111   {
112     return col == null ? null : col.brighter().brighter().brighter();
113   }
114
115   /**
116    * Returns a color between minColour and maxColour; the RGB values are in
117    * proportion to where 'value' lies between minValue and maxValue
118    * 
119    * @param value
120    * @param minValue
121    * @param minColour
122    * @param maxValue
123    * @param maxColour
124    * @return
125    */
126   public static Color getGraduatedColour(float value, float minValue,
127           Color minColour, float maxValue, Color maxColour)
128   {
129     if (minValue == maxValue)
130     {
131       return minColour;
132     }
133     if (value < minValue)
134     {
135       value = minValue;
136     }
137     if (value > maxValue)
138     {
139       value = maxValue;
140     }
141
142     /*
143      * prop = proportion of the way value is from minValue to maxValue
144      */
145     float prop = (value - minValue) / (maxValue - minValue);
146     float r = minColour.getRed()
147             + prop * (maxColour.getRed() - minColour.getRed());
148     float g = minColour.getGreen()
149             + prop * (maxColour.getGreen() - minColour.getGreen());
150     float b = minColour.getBlue()
151             + prop * (maxColour.getBlue() - minColour.getBlue());
152     return new Color(r / 255, g / 255, b / 255);
153   }
154
155   /**
156    * 'Fades' the given colour towards white by the specified proportion. A
157    * factor of 1 or more results in White, a factor of 0 leaves the colour
158    * unchanged, and a factor between 0 and 1 results in a proportionate change
159    * of RGB values towards (255, 255, 255).
160    * <p>
161    * A negative bleachFactor can be specified to darken the colour towards Black
162    * (0, 0, 0).
163    * 
164    * @param colour
165    * @param bleachFactor
166    * @return
167    */
168   public static Color bleachColour(Color colour, float bleachFactor)
169   {
170     if (bleachFactor >= 1f)
171     {
172       return Color.WHITE;
173     }
174     if (bleachFactor <= -1f)
175     {
176       return Color.BLACK;
177     }
178     if (bleachFactor == 0f)
179     {
180       return colour;
181     }
182
183     int red = colour.getRed();
184     int green = colour.getGreen();
185     int blue = colour.getBlue();
186
187     if (bleachFactor > 0)
188     {
189       red += (255 - red) * bleachFactor;
190       green += (255 - green) * bleachFactor;
191       blue += (255 - blue) * bleachFactor;
192       return new Color(red, green, blue);
193     }
194     else
195     {
196       float factor = 1 + bleachFactor;
197       red *= factor;
198       green *= factor;
199       blue *= factor;
200       return new Color(red, green, blue);
201     }
202   }
203
204   /**
205    * Parses a string into a Color, where the accepted formats are
206    * <ul>
207    * <li>an AWT colour name e.g. white</li>
208    * <li>a hex colour value (without prefix) e.g. ff0000</li>
209    * <li>an rgb triple e.g. 100,50,150</li>
210    * </ul>
211    * 
212    * @param colour
213    * @return the parsed colour, or null if parsing fails
214    */
215   public static Color parseColourString(String colour)
216   {
217     if (colour == null)
218     {
219       return null;
220     }
221     colour = colour.trim();
222
223     Color col = null;
224     if (StringUtils.isHexString(colour))
225     {
226       try
227       {
228         int value = Integer.parseInt(colour, 16);
229         col = new Color(value);
230       } catch (NumberFormatException ex)
231       {
232       }
233     }
234
235     if (col == null)
236     {
237       col = ColorUtils.getAWTColorFromName(colour);
238     }
239
240     if (col == null)
241     {
242       try
243       {
244         String[] tokens = colour.split(",");
245         if (tokens.length == 3)
246         {
247           int r = Integer.parseInt(tokens[0].trim());
248           int g = Integer.parseInt(tokens[1].trim());
249           int b = Integer.parseInt(tokens[2].trim());
250           col = new Color(r, g, b);
251         }
252       } catch (Exception ex)
253       {
254         // non-numeric token or out of 0-255 range
255       }
256     }
257
258     return col;
259   }
260
261   /**
262    * Constructs a colour from a text string. The hashcode of the whole string is
263    * scaled to the range 0-135. This is added to RGB values made from the
264    * hashcode of each third of the string, and scaled to the range 20-229.
265    * 
266    * @param name
267    * @return
268    */
269   public static Color createColourFromName(String name)
270   {
271     if (name == null)
272     {
273       return Color.white;
274     }
275     if (myColours.containsKey(name))
276     {
277       return myColours.get(name);
278     }
279     int lsize = name.length();
280     int start = 0;
281     int end = lsize / 3;
282
283     int rgbOffset = Math.abs(name.hashCode() % 10) * 15; // 0-135
284
285     /*
286      * red: first third
287      */
288     int r = Math.abs(name.substring(start, end).hashCode() + rgbOffset)
289             % 210 + 20;
290     start = end;
291     end += lsize / 3;
292     if (end > lsize)
293     {
294       end = lsize;
295     }
296
297     /*
298      * green: second third
299      */
300     int g = Math.abs(name.substring(start, end).hashCode() + rgbOffset)
301             % 210 + 20;
302
303     /*
304      * blue: third third
305      */
306     int b = Math.abs(name.substring(end).hashCode() + rgbOffset) % 210 + 20;
307
308     Color color = new Color(r, g, b);
309
310     if (myColours.size() < MAX_CACHE_SIZE)
311     {
312       myColours.put(name, color);
313     }
314
315     return color;
316   }
317
318   /**
319    * Returns the Color constant for a given colour name e.g. "pink", or null if
320    * the name is not recognised
321    * 
322    * @param name
323    * @return
324    */
325   public static Color getAWTColorFromName(String name)
326   {
327     if (name == null)
328     {
329       return null;
330     }
331     Color col = null;
332     name = name.toLowerCase(Locale.ROOT);
333
334     // or make a static map; or use reflection on the field name
335     switch (name)
336     {
337     case "black":
338       col = Color.black;
339       break;
340     case "blue":
341       col = Color.blue;
342       break;
343     case "cyan":
344       col = Color.cyan;
345       break;
346     case "darkgray":
347       col = Color.darkGray;
348       break;
349     case "gray":
350       col = Color.gray;
351       break;
352     case "green":
353       col = Color.green;
354       break;
355     case "lightgray":
356       col = Color.lightGray;
357       break;
358     case "magenta":
359       col = Color.magenta;
360       break;
361     case "orange":
362       col = Color.orange;
363       break;
364     case "pink":
365       col = Color.pink;
366       break;
367     case "red":
368       col = Color.red;
369       break;
370     case "white":
371       col = Color.white;
372       break;
373     case "yellow":
374       col = Color.yellow;
375       break;
376     }
377
378     return col;
379   }
380 }