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