JAL-4386 Some methods to create consistent colours from a string id, with applicable...
authorBen Soares <b.soares@dundee.ac.uk>
Wed, 6 Nov 2024 20:43:49 +0000 (20:43 +0000)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 7 Nov 2024 15:05:09 +0000 (15:05 +0000)
src/jalview/util/ColorUtils.java
src/jalview/util/StringUtils.java

index 24128ea..ee519a7 100644 (file)
@@ -30,6 +30,9 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Random;
 
+import jalview.bin.Cache;
+import jalview.bin.Console;
+
 public class ColorUtils
 {
   private static final int MAX_CACHE_SIZE = 1729;
@@ -37,7 +40,16 @@ public class ColorUtils
   /*
    * a cache for colours generated from text strings
    */
-  static Map<String, Color> myColours = new HashMap<>();
+  private static Map<String, Color> myColours = new HashMap<>();
+
+  private static Map<String, Color> myHSBSpacedColours = new HashMap<>();
+
+  public static final String ID_COLOUR_SCHEME;
+
+  static
+  {
+    ID_COLOUR_SCHEME = Cache.getDefault("ID_COLOUR_SCHEME", "NONE");
+  }
 
   /**
    * Generates a random color, will mix with input color. Code taken from
@@ -393,4 +405,113 @@ public class ColorUtils
 
     return col;
   }
-}
+
+  public static Color getDefaultColourFromName(String name)
+  {
+    return getColourFromNameAndScheme(name, ID_COLOUR_SCHEME);
+  }
+
+  public static Color getColourFromNameAndScheme(String name,
+          String colourSchemes)
+  {
+    String cacheKey = name + "::" + colourSchemes;
+    if (myHSBSpacedColours.containsKey(cacheKey))
+    {
+      return myHSBSpacedColours.get(cacheKey);
+    }
+    float Hmin = 0.0f;
+    float Hmax = 1.0f;
+    float Smin = 0.7f;
+    float Smax = 1.0f;
+    float Bmin = 0.7f;
+    float Bmax = 1.0f;
+    for (String scheme : colourSchemes.split(","))
+    {
+      switch (scheme)
+      {
+      case "AVOID_RED":
+        Hmin = 0.15f;
+        Hmax = 0.85f;
+        break;
+      case "AVOID_GREEN":
+        Hmin = 0.48f;
+        Hmax = 0.18f;
+        break;
+      case "AVOID_BLUE":
+        Hmin = 0.51f;
+        Hmax = 0.81f;
+        break;
+      case "SATURATED":
+        Smin = 1.0f;
+        Smax = 1.0f;
+        break;
+      case "PALE":
+        Smin = 0.2f;
+        Smax = 0.7f;
+        break;
+      case "GREYSCALE":
+        Smin = 0.0f;
+        Smax = 0.0f;
+      case "BRIGHT":
+        Bmin = 1.0f;
+        Bmax = 0.9f;
+        break;
+      case "DARK":
+        Bmin = 0.2f;
+        Bmax = 0.6f;
+        break;
+      }
+    }
+    Color col = getHSBColourspacedColourFromName(name, Hmin, Hmax, Smin,
+            Smax, Bmin, Bmax);
+    myHSBSpacedColours.put(cacheKey, col);
+    return col;
+  }
+
+  public static Color getHSBColourspacedColourFromName(String name,
+          float Hmin, float Hmax, float Smin, float Smax, float Bmin,
+          float Bmax)
+  {
+    if (name == null)
+    {
+      return Color.white;
+    }
+
+    // use first three bytes from MD5 rather than String.hashCode() as short
+    // strings all low number hashCodes
+    byte[] hash = StringUtils.getMD5Bytes(name);
+    int bl = hash.length > 0 ? hash[0] : 0;
+    int g = hash.length > 1 ? hash[1] : 0;
+    int r = hash.length > 2 ? hash[2] : 0;
+
+    float[] hsbf = Color.RGBtoHSB(r, g, bl, null);
+
+    if (hsbf.length < 3)
+    {
+      // This really shouldn't happen
+      Console.warn("Unexpected short length of HSB float array");
+    }
+    float h0 = hsbf.length > 0 ? hsbf[0] : 0f;
+    float s0 = hsbf.length > 1 ? hsbf[1] : 0f;
+    float b0 = hsbf.length > 2 ? hsbf[2] : 0f;
+
+    // Now map these HSB values into the given ranges
+
+    // hue wraps around 1.0->0.0 so deal with a Hmin-Hmax range that goes across
+    // this
+    float h1 = 0f;
+    if (Hmin > Hmax)
+    {
+      Hmax += 1f;
+    }
+    h1 = Hmin + (Hmax - Hmin) * h0;
+    if (h1 > 1f)
+    {
+      h1 -= 1f;
+    }
+    float s1 = Smin + (Smax - Smin) * s0;
+    float b1 = Bmin + (Bmax - Bmin) * b0;
+
+    return Color.getHSBColor(h1, s1, b1);
+  }
+}
\ No newline at end of file
index 42c52a7..3b3cb47 100644 (file)
 package jalview.util;
 
 import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
 import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import jalview.bin.Console;
+
 public class StringUtils
 {
   private static final Pattern DELIMITERS_PATTERN = Pattern
@@ -644,4 +649,39 @@ public class StringUtils
     }
     return max;
   }
+
+  /*
+   * return an MD5 hash of the given String
+   */
+  public static byte[] getMD5Bytes(String name)
+  {
+    final String alg = "MD5";
+    try
+    {
+      MessageDigest md5 = MessageDigest.getInstance(alg);
+      return md5.digest(name.getBytes());
+    } catch (NoSuchAlgorithmException e)
+    {
+      Console.warn("Could not find hashing algorithm '" + alg + "'");
+      return null;
+    }
+  }
+
+  public static String getMD5String(String name)
+  {
+    byte[] hashBytes = getMD5Bytes(name);
+    if (hashBytes == null)
+    {
+      return null;
+    }
+    BigInteger hashNum = new BigInteger(1, hashBytes);
+    String hashString = hashNum.toString(16);
+    StringBuilder sb = new StringBuilder(32);
+    for (int i = 0; i + hashString.length() < 32; i++)
+    {
+      sb.append('0');
+    }
+    sb.append(hashString);
+    return sb.toString();
+  }
 }