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 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 public <T> T get(String key, T def, Class<T> klass) { 031 return get(key, def, klass, false); 032 } 033 034 /** 035 * Get value for the given key 036 * @param <T> the expected type 037 * @param key the key 038 * @param def default value, can be null 039 * @param klass the same as T 040 * @param suppressWarnings show or don't show a warning when some value is 041 * found, but cannot be converted to the requested type 042 * @return if a value with class klass has been mapped to key, returns this 043 * value, def otherwise 044 */ 045 public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) { 046 if (def != null && !klass.isInstance(def)) 047 throw new IllegalArgumentException(def+" is not an instance of "+klass); 048 Object o = prop.get(key); 049 if (o == null) 050 return def; 051 T res = convertTo(o, klass); 052 if (res == null) { 053 if (!suppressWarnings) { 054 Main.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass())); 055 } 056 return def; 057 } else 058 return res; 059 } 060 061 public Object get(String key) { 062 return prop.get(key); 063 } 064 065 public void put(String key, Object val) { 066 prop.put(key, val); 067 } 068 069 public void putOrClear(String key, Object val) { 070 if (val != null) { 071 prop.put(key, val); 072 } else { 073 prop.remove(key); 074 } 075 } 076 077 public void remove(String key) { 078 prop.remove(key); 079 } 080 081 @SuppressWarnings("unchecked") 082 public static <T> T convertTo(Object o, Class<T> klass) { 083 if (o == null) 084 return null; 085 if (klass.isInstance(o)) 086 return (T) o; 087 088 if (klass == float.class || klass == Float.class) 089 return (T) toFloat(o); 090 091 if (klass == double.class || klass == Double.class) { 092 o = toFloat(o); 093 if (o != null) { 094 o = new Double((Float) o); 095 } 096 return (T) o; 097 } 098 099 if (klass == boolean.class || klass == Boolean.class) 100 return (T) toBool(o); 101 102 if (klass == float[].class) 103 return (T) toFloatArray(o); 104 105 if (klass == Color.class) 106 return (T) toColor(o); 107 108 if (klass == String.class) { 109 if (o instanceof Keyword) 110 return (T) ((Keyword) o).val; 111 if (o instanceof Color) { 112 Color c = (Color) o; 113 int alpha = c.getAlpha(); 114 if (alpha != 255) 115 return (T) String.format("#%06x%02x", ((Color) o).getRGB() & 0x00ffffff, alpha); 116 return (T) String.format("#%06x", ((Color) o).getRGB() & 0x00ffffff); 117 118 } 119 120 return (T) o.toString(); 121 } 122 123 return null; 124 } 125 126 private static Float toFloat(Object o) { 127 if (o instanceof Number) 128 return ((Number) o).floatValue(); 129 if (o instanceof String && !((String) o).isEmpty()) { 130 try { 131 return Float.valueOf((String) o); 132 } catch (NumberFormatException e) { 133 if (Main.isDebugEnabled()) { 134 Main.debug("'"+o+"' cannot be converted to float"); 135 } 136 } 137 } 138 return null; 139 } 140 141 private static Boolean toBool(Object o) { 142 if (o instanceof Boolean) 143 return (Boolean) o; 144 String s = null; 145 if (o instanceof Keyword) { 146 s = ((Keyword) o).val; 147 } else if (o instanceof String) { 148 s = (String) o; 149 } 150 if (s != null) 151 return !(s.isEmpty() || "false".equals(s) || "no".equals(s) || "0".equals(s) || "0.0".equals(s)); 152 if (o instanceof Number) 153 return ((Number) o).floatValue() != 0; 154 if (o instanceof List) 155 return !((List) o).isEmpty(); 156 if (o instanceof float[]) 157 return ((float[]) o).length != 0; 158 159 return null; 160 } 161 162 private static float[] toFloatArray(Object o) { 163 if (o instanceof float[]) 164 return (float[]) o; 165 if (o instanceof List) { 166 List<?> l = (List<?>) o; 167 float[] a = new float[l.size()]; 168 for (int i = 0; i < l.size(); ++i) { 169 Float f = toFloat(l.get(i)); 170 if (f == null) 171 return null; 172 else 173 a[i] = f; 174 } 175 return a; 176 } 177 Float f = toFloat(o); 178 if (f != null) 179 return new float[] {f}; 180 return null; 181 } 182 183 private static Color toColor(Object o) { 184 if (o instanceof Color) 185 return (Color) o; 186 if (o instanceof Keyword) 187 return CSSColors.get(((Keyword) o).val); 188 if (o instanceof String) { 189 Color c = CSSColors.get((String) o); 190 if (c != null) 191 return c; 192 if (HEX_COLOR_PATTERN.matcher((String) o).matches()) { 193 return ColorHelper.html2color((String) o); 194 } 195 } 196 return null; 197 } 198 199 @Override 200 public Cascade clone() { 201 @SuppressWarnings("unchecked") 202 Map<String, Object> clonedProp = (Map<String, Object>) ((HashMap) this.prop).clone(); 203 Cascade c = new Cascade(); 204 c.prop = clonedProp; 205 return c; 206 } 207 208 @Override 209 public String toString() { 210 StringBuilder res = new StringBuilder("Cascade{ "); 211 for (Entry<String, Object> entry : prop.entrySet()) { 212 res.append(entry.getKey()+':'); 213 Object val = entry.getValue(); 214 if (val instanceof float[]) { 215 res.append(Arrays.toString((float[]) val)); 216 } else if (val instanceof Color) { 217 res.append(Utils.toString((Color) val)); 218 } else if (val != null) { 219 res.append(val); 220 } 221 res.append("; "); 222 } 223 return res.append('}').toString(); 224 } 225 226 public boolean containsKey(String key) { 227 return prop.containsKey(key); 228 } 229 230 public boolean isDefaultSelectedHandling() { 231 return defaultSelectedHandling; 232 } 233 234 public void setDefaultSelectedHandling(boolean defaultSelectedHandling) { 235 this.defaultSelectedHandling = defaultSelectedHandling; 236 } 237}