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