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