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