001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint; 003 004import java.awt.Color; 005import java.util.Arrays; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009import java.util.Map.Entry; 010import java.util.TreeSet; 011import java.util.regex.Pattern; 012 013import org.openstreetmap.josm.Main; 014import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors; 015import org.openstreetmap.josm.tools.ColorHelper; 016import org.openstreetmap.josm.tools.Utils; 017 018/** 019 * Simple map of properties with dynamic typing. 020 */ 021public final class Cascade implements Cloneable { 022 023 private Map<String, Object> prop = new HashMap<>(); 024 025 private boolean defaultSelectedHandling = true; 026 027 private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})"); 028 029 public <T> T get(String key, T def, Class<T> klass) { 030 return get(key, def, klass, false); 031 } 032 033 /** 034 * Get value for the given key 035 * @param <T> the expected type 036 * @param key the key 037 * @param def default value, can be null 038 * @param klass the same as T 039 * @param suppressWarnings show or don't show a warning when some value is 040 * found, but cannot be converted to the requested type 041 * @return if a value with class klass has been mapped to key, returns this 042 * value, def otherwise 043 */ 044 public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) { 045 if (def != null && !klass.isInstance(def)) 046 throw new IllegalArgumentException(def+" is not an instance of "+klass); 047 Object o = prop.get(key); 048 if (o == null) 049 return def; 050 T res = convertTo(o, klass); 051 if (res == null) { 052 if (!suppressWarnings) { 053 Main.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass())); 054 } 055 return def; 056 } else 057 return res; 058 } 059 060 public Object get(String key) { 061 return prop.get(key); 062 } 063 064 public void put(String key, Object val) { 065 prop.put(key, val); 066 } 067 068 public void putOrClear(String key, Object val) { 069 if (val != null) { 070 prop.put(key, val); 071 } else { 072 prop.remove(key); 073 } 074 } 075 076 public void remove(String key) { 077 prop.remove(key); 078 } 079 080 @SuppressWarnings("unchecked") 081 public static <T> T convertTo(Object o, Class<T> klass) { 082 if (o == null) 083 return null; 084 if (klass.isInstance(o)) 085 return (T) o; 086 087 if (klass == float.class || klass == Float.class) 088 return (T) toFloat(o); 089 090 if (klass == double.class || klass == Double.class) { 091 o = toFloat(o); 092 if (o != null) { 093 o = Double.valueOf((Float) o); 094 } 095 return (T) o; 096 } 097 098 if (klass == boolean.class || klass == Boolean.class) 099 return (T) toBool(o); 100 101 if (klass == float[].class) 102 return (T) toFloatArray(o); 103 104 if (klass == Color.class) 105 return (T) toColor(o); 106 107 if (klass == String.class) { 108 if (o instanceof Keyword) 109 return (T) ((Keyword) o).val; 110 if (o instanceof Color) { 111 Color c = (Color) o; 112 int alpha = c.getAlpha(); 113 if (alpha != 255) 114 return (T) String.format("#%06x%02x", ((Color) o).getRGB() & 0x00ffffff, alpha); 115 return (T) String.format("#%06x", ((Color) o).getRGB() & 0x00ffffff); 116 } 117 118 return (T) o.toString(); 119 } 120 121 return null; 122 } 123 124 private static Float toFloat(Object o) { 125 if (o instanceof Number) 126 return ((Number) o).floatValue(); 127 if (o instanceof String && !((String) o).isEmpty()) { 128 try { 129 return Float.valueOf((String) o); 130 } catch (NumberFormatException e) { 131 if (Main.isDebugEnabled()) { 132 Main.debug('\'' + (String) o + "' cannot be converted to float"); 133 } 134 } 135 } 136 return null; 137 } 138 139 private static Boolean toBool(Object o) { 140 if (o instanceof Boolean) 141 return (Boolean) o; 142 String s = null; 143 if (o instanceof Keyword) { 144 s = ((Keyword) o).val; 145 } else if (o instanceof String) { 146 s = (String) o; 147 } 148 if (s != null) 149 return !(s.isEmpty() || "false".equals(s) || "no".equals(s) || "0".equals(s) || "0.0".equals(s)); 150 if (o instanceof Number) 151 return ((Number) o).floatValue() != 0; 152 if (o instanceof List) 153 return !((List) o).isEmpty(); 154 if (o instanceof float[]) 155 return ((float[]) o).length != 0; 156 157 return null; 158 } 159 160 private static float[] toFloatArray(Object o) { 161 if (o instanceof float[]) 162 return (float[]) o; 163 if (o instanceof List) { 164 List<?> l = (List<?>) o; 165 float[] a = new float[l.size()]; 166 for (int i = 0; i < l.size(); ++i) { 167 Float f = toFloat(l.get(i)); 168 if (f == null) 169 return null; 170 else 171 a[i] = f; 172 } 173 return a; 174 } 175 Float f = toFloat(o); 176 if (f != null) 177 return new float[] {f}; 178 return null; 179 } 180 181 private static Color toColor(Object o) { 182 if (o instanceof Color) 183 return (Color) o; 184 if (o instanceof Keyword) 185 return CSSColors.get(((Keyword) o).val); 186 if (o instanceof String) { 187 Color c = CSSColors.get((String) o); 188 if (c != null) 189 return c; 190 if (HEX_COLOR_PATTERN.matcher((String) o).matches()) { 191 return ColorHelper.html2color((String) o); 192 } 193 } 194 return null; 195 } 196 197 @Override 198 public Cascade clone() { 199 try { 200 Cascade c = (Cascade) super.clone(); 201 @SuppressWarnings({ "unchecked", "rawtypes" }) 202 Map<String, Object> clonedProp = (Map<String, Object>) ((HashMap) this.prop).clone(); 203 c.prop = clonedProp; 204 return c; 205 } catch (CloneNotSupportedException e) { 206 throw new IllegalStateException(e); 207 } 208 } 209 210 @Override 211 public String toString() { 212 StringBuilder res = new StringBuilder("Cascade{ "); 213 // List properties in alphabetical order to be deterministic, without changing "prop" to a TreeMap 214 // (no reason too, not sure about the potential memory/performance impact of such a change) 215 TreeSet<String> props = new TreeSet<>(); 216 for (Entry<String, Object> entry : prop.entrySet()) { 217 StringBuilder sb = new StringBuilder(entry.getKey()).append(':'); 218 Object val = entry.getValue(); 219 if (val instanceof float[]) { 220 sb.append(Arrays.toString((float[]) val)); 221 } else if (val instanceof Color) { 222 sb.append(Utils.toString((Color) val)); 223 } else if (val != null) { 224 sb.append(val); 225 } 226 sb.append("; "); 227 props.add(sb.toString()); 228 } 229 for (String s : props) { 230 res.append(s); 231 } 232 return res.append('}').toString(); 233 } 234 235 public boolean containsKey(String key) { 236 return prop.containsKey(key); 237 } 238 239 public boolean isDefaultSelectedHandling() { 240 return defaultSelectedHandling; 241 } 242 243 public void setDefaultSelectedHandling(boolean defaultSelectedHandling) { 244 this.defaultSelectedHandling = defaultSelectedHandling; 245 } 246}