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