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