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