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.gui.mappaint.mapcss.CSSColors; 014import org.openstreetmap.josm.tools.ColorHelper; 015import org.openstreetmap.josm.tools.Logging; 016import org.openstreetmap.josm.tools.Utils; 017 018/** 019 * Simple map of properties with dynamic typing. 020 */ 021public final class Cascade { 022 023 private final Map<String, Object> prop; 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 /** 030 * Constructs a new {@code Cascade}. 031 */ 032 public Cascade() { 033 this.prop = new HashMap<>(); 034 } 035 036 /** 037 * Constructs a new {@code Cascade} from existing one. 038 * @param other other Cascade 039 */ 040 public Cascade(Cascade other) { 041 this.prop = new HashMap<>(other.prop); 042 } 043 044 /** 045 * Gets the value for a given key with the given type 046 * @param <T> the expected type 047 * @param key the key 048 * @param def default value, can be null 049 * @param klass the same as T 050 * @return if a value that can be converted to class klass has been mapped to key, returns this 051 * value, def otherwise 052 */ 053 public <T> T get(String key, T def, Class<T> klass) { 054 return get(key, def, klass, false); 055 } 056 057 /** 058 * Get value for the given key 059 * @param <T> the expected type 060 * @param key the key 061 * @param def default value, can be null 062 * @param klass the same as T 063 * @param suppressWarnings show or don't show a warning when some value is 064 * found, but cannot be converted to the requested type 065 * @return if a value that can be converted to class klass has been mapped to key, returns this 066 * value, def otherwise 067 */ 068 public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) { 069 if (def != null && !klass.isInstance(def)) 070 throw new IllegalArgumentException(def+" is not an instance of "+klass); 071 Object o = prop.get(key); 072 if (o == null) 073 return def; 074 T res = convertTo(o, klass); 075 if (res == null) { 076 if (!suppressWarnings) { 077 Logging.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass())); 078 } 079 return def; 080 } else 081 return res; 082 } 083 084 /** 085 * Gets a property for the given key (like stroke, ...) 086 * @param key The key of the property 087 * @return The value or <code>null</code> if it is not set. May be of any type 088 */ 089 public Object get(String key) { 090 return prop.get(key); 091 } 092 093 /** 094 * Sets the property for the given key 095 * @param key The key 096 * @param val The value 097 */ 098 public void put(String key, Object val) { 099 prop.put(key, val); 100 } 101 102 /** 103 * Sets the property for the given key, removes it if the value is <code>null</code> 104 * @param key The key 105 * @param val The value, may be <code>null</code> 106 */ 107 public void putOrClear(String key, Object val) { 108 if (val != null) { 109 prop.put(key, val); 110 } else { 111 prop.remove(key); 112 } 113 } 114 115 /** 116 * Removes the property with the given key 117 * @param key The key 118 */ 119 public void remove(String key) { 120 prop.remove(key); 121 } 122 123 /** 124 * Converts an object to a given other class. 125 * 126 * Only conversions that are useful for MapCSS are supported 127 * @param <T> The class type 128 * @param o The object to convert 129 * @param klass The class 130 * @return The converted object or <code>null</code> if the conversion failed 131 */ 132 @SuppressWarnings("unchecked") 133 public static <T> T convertTo(Object o, Class<T> klass) { 134 if (o == null) 135 return null; 136 if (klass.isInstance(o)) 137 return (T) o; 138 139 if (klass == float.class || klass == Float.class) 140 return (T) toFloat(o); 141 142 if (klass == double.class || klass == Double.class) { 143 o = toFloat(o); 144 if (o != null) { 145 o = Double.valueOf((Float) o); 146 } 147 return (T) o; 148 } 149 150 if (klass == boolean.class || klass == Boolean.class) 151 return (T) toBool(o); 152 153 if (klass == float[].class) 154 return (T) toFloatArray(o); 155 156 if (klass == Color.class) 157 return (T) toColor(o); 158 159 if (klass == String.class) { 160 if (o instanceof Keyword) 161 return (T) ((Keyword) o).val; 162 if (o instanceof Color) { 163 Color c = (Color) o; 164 int alpha = c.getAlpha(); 165 if (alpha != 255) 166 return (T) String.format("#%06x%02x", ((Color) o).getRGB() & 0x00ffffff, alpha); 167 return (T) String.format("#%06x", ((Color) o).getRGB() & 0x00ffffff); 168 } 169 170 return (T) o.toString(); 171 } 172 173 return null; 174 } 175 176 private static Float toFloat(Object o) { 177 if (o instanceof Number) 178 return ((Number) o).floatValue(); 179 if (o instanceof String && !((String) o).isEmpty()) { 180 try { 181 return Float.valueOf((String) o); 182 } catch (NumberFormatException e) { 183 Logging.debug("''{0}'' cannot be converted to float", o); 184 } 185 } 186 return null; 187 } 188 189 private static Boolean toBool(Object o) { 190 if (o instanceof Boolean) 191 return (Boolean) o; 192 String s = null; 193 if (o instanceof Keyword) { 194 s = ((Keyword) o).val; 195 } else if (o instanceof String) { 196 s = (String) o; 197 } 198 if (s != null) 199 return !(s.isEmpty() || "false".equals(s) || "no".equals(s) || "0".equals(s) || "0.0".equals(s)); 200 if (o instanceof Number) 201 return ((Number) o).floatValue() != 0; 202 if (o instanceof List) 203 return !((List<?>) o).isEmpty(); 204 if (o instanceof float[]) 205 return ((float[]) o).length != 0; 206 207 return null; 208 } 209 210 private static float[] toFloatArray(Object o) { 211 if (o instanceof float[]) 212 return (float[]) o; 213 if (o instanceof List) { 214 List<?> l = (List<?>) o; 215 float[] a = new float[l.size()]; 216 for (int i = 0; i < l.size(); ++i) { 217 Float f = toFloat(l.get(i)); 218 if (f == null) 219 return null; 220 else 221 a[i] = f; 222 } 223 return a; 224 } 225 Float f = toFloat(o); 226 if (f != null) 227 return new float[] {f}; 228 return null; 229 } 230 231 private static Color toColor(Object o) { 232 if (o instanceof Color) 233 return (Color) o; 234 if (o instanceof Keyword) 235 return CSSColors.get(((Keyword) o).val); 236 if (o instanceof String) { 237 Color c = CSSColors.get((String) o); 238 if (c != null) 239 return c; 240 if (HEX_COLOR_PATTERN.matcher((String) o).matches()) { 241 return ColorHelper.html2color((String) o); 242 } 243 } 244 return null; 245 } 246 247 @Override 248 public String toString() { 249 StringBuilder res = new StringBuilder("Cascade{ "); 250 // List properties in alphabetical order to be deterministic, without changing "prop" to a TreeMap 251 // (no reason too, not sure about the potential memory/performance impact of such a change) 252 TreeSet<String> props = new TreeSet<>(); 253 for (Entry<String, Object> entry : prop.entrySet()) { 254 StringBuilder sb = new StringBuilder(entry.getKey()).append(':'); 255 Object val = entry.getValue(); 256 if (val instanceof float[]) { 257 sb.append(Arrays.toString((float[]) val)); 258 } else if (val instanceof Color) { 259 sb.append(Utils.toString((Color) val)); 260 } else if (val != null) { 261 sb.append(val); 262 } 263 sb.append("; "); 264 props.add(sb.toString()); 265 } 266 for (String s : props) { 267 res.append(s); 268 } 269 return res.append('}').toString(); 270 } 271 272 /** 273 * Checks if this cascade has a value for given key 274 * @param key The key to check 275 * @return <code>true</code> if there is a value 276 */ 277 public boolean containsKey(String key) { 278 return prop.containsKey(key); 279 } 280 281 /** 282 * Get if the default selection drawing should be used for the object this cascade applies to 283 * @return <code>true</code> to use the default selection drawing 284 */ 285 public boolean isDefaultSelectedHandling() { 286 return defaultSelectedHandling; 287 } 288 289 /** 290 * Set that the default selection drawing should be used for the object this cascade applies to 291 * @param defaultSelectedHandling <code>true</code> to use the default selection drawing 292 */ 293 public void setDefaultSelectedHandling(boolean defaultSelectedHandling) { 294 this.defaultSelectedHandling = defaultSelectedHandling; 295 } 296}