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.regex.Pattern; 011 012import org.openstreetmap.josm.Main; 013import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors; 014import org.openstreetmap.josm.tools.ColorHelper; 015import org.openstreetmap.josm.tools.Utils; 016 017/** 018 * Simple map of properties with dynamic typing. 019 */ 020public final class Cascade implements Cloneable { 021 022 public static final Cascade EMPTY_CASCADE = new Cascade(); 023 024 private Map<String, Object> prop = new HashMap<>(); 025 026 private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})"); 027 028 public <T> T get(String key, T def, Class<T> klass) { 029 return get(key, def, klass, false); 030 } 031 032 /** 033 * Get value for the given key 034 * @param <T> the expected type 035 * @param key the key 036 * @param def default value, can be null 037 * @param klass the same as T 038 * @param suppressWarnings show or don't show a warning when some value is 039 * found, but cannot be converted to the requested type 040 * @return if a value with class klass has been mapped to key, returns this 041 * value, def otherwise 042 */ 043 public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) { 044 if (def != null && !klass.isInstance(def)) 045 throw new IllegalArgumentException(def+" is not an instance of "+klass); 046 Object o = prop.get(key); 047 if (o == null) 048 return def; 049 T res = convertTo(o, klass); 050 if (res == null) { 051 if (!suppressWarnings) { 052 Main.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass())); 053 } 054 return def; 055 } else 056 return res; 057 } 058 059 public Object get(String key) { 060 return prop.get(key); 061 } 062 063 public void put(String key, Object val) { 064 prop.put(key, val); 065 } 066 067 public void putOrClear(String key, Object val) { 068 if (val != null) { 069 prop.put(key, val); 070 } else { 071 prop.remove(key); 072 } 073 } 074 075 public void remove(String key) { 076 prop.remove(key); 077 } 078 079 @SuppressWarnings("unchecked") 080 public static <T> T convertTo(Object o, Class<T> klass) { 081 if (o == null) 082 return null; 083 if (klass.isInstance(o)) 084 return (T) o; 085 086 if (klass == float.class || klass == Float.class) 087 return (T) toFloat(o); 088 089 if (klass == double.class || klass == Double.class) { 090 o = toFloat(o); 091 if (o != null) { 092 o = new Double((Float) o); 093 } 094 return (T) o; 095 } 096 097 if (klass == boolean.class || klass == Boolean.class) 098 return (T) toBool(o); 099 100 if (klass == float[].class) 101 return (T) toFloatArray(o); 102 103 if (klass == Color.class) 104 return (T) toColor(o); 105 106 if (klass == String.class) { 107 if (o instanceof Keyword) 108 return (T) ((Keyword) o).val; 109 if (o instanceof Color) { 110 Color c = (Color) o; 111 int alpha = c.getAlpha(); 112 if (alpha != 255) 113 return (T) String.format("#%06x%02x", ((Color) o).getRGB() & 0x00ffffff, alpha); 114 return (T) String.format("#%06x", ((Color) o).getRGB() & 0x00ffffff); 115 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("'"+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 @SuppressWarnings("unchecked") 200 Map<String, Object> clonedProp = (Map<String, Object>) ((HashMap) this.prop).clone(); 201 Cascade c = new Cascade(); 202 c.prop = clonedProp; 203 return c; 204 } 205 206 @Override 207 public String toString() { 208 StringBuilder res = new StringBuilder("Cascade{ "); 209 for (Entry<String, Object> entry : prop.entrySet()) { 210 res.append(entry.getKey()+':'); 211 Object val = entry.getValue(); 212 if (val instanceof float[]) { 213 res.append(Arrays.toString((float[]) val)); 214 } else if (val instanceof Color) { 215 res.append(Utils.toString((Color) val)); 216 } else if (val != null) { 217 res.append(val); 218 } 219 res.append("; "); 220 } 221 return res.append('}').toString(); 222 } 223 224 public boolean containsKey(String key) { 225 return prop.containsKey(key); 226 } 227}