source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/Cascade.java@ 16184

Last change on this file since 16184 was 16184, checked in by simon04, 4 years ago

see #18954, see #18864 - Introduce GenericParser for Cascade.convertTo

  • Property svn:eol-style set to native
File size: 9.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import java.awt.Color;
5import java.util.Arrays;
6import java.util.HashMap;
7import java.util.List;
8import java.util.Map;
9import java.util.Map.Entry;
10import java.util.TreeSet;
11import java.util.regex.Pattern;
12
13import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors;
14import org.openstreetmap.josm.tools.ColorHelper;
15import org.openstreetmap.josm.tools.GenericParser;
16import org.openstreetmap.josm.tools.Logging;
17import org.openstreetmap.josm.tools.Utils;
18
19/**
20 * Simple map of properties with dynamic typing.
21 */
22public final class Cascade {
23
24 private final Map<String, Object> prop;
25
26 private boolean defaultSelectedHandling = true;
27
28 private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})");
29
30 private static final GenericParser<Object> GENERIC_PARSER = new GenericParser<>()
31 .registerParser(float.class, Cascade::toFloat)
32 .registerParser(Float.class, Cascade::toFloat)
33 .registerParser(double.class, Cascade::toDouble)
34 .registerParser(Double.class, Cascade::toDouble)
35 .registerParser(boolean.class, Cascade::toBool)
36 .registerParser(Boolean.class, Cascade::toBool)
37 .registerParser(float[].class, Cascade::toFloatArray)
38 .registerParser(Color.class, Cascade::toColor)
39 .registerParser(String.class, Cascade::toString);
40
41 /**
42 * Constructs a new {@code Cascade}.
43 */
44 public Cascade() {
45 this.prop = new HashMap<>();
46 }
47
48 /**
49 * Constructs a new {@code Cascade} from existing one.
50 * @param other other Cascade
51 */
52 public Cascade(Cascade other) {
53 this.prop = new HashMap<>(other.prop);
54 }
55
56 /**
57 * Gets the value for a given key with the given type
58 * @param <T> the expected type
59 * @param key the key
60 * @param def default value, can be null
61 * @param klass the same as T
62 * @return if a value that can be converted to class klass has been mapped to key, returns this
63 * value, def otherwise
64 */
65 public <T> T get(String key, T def, Class<T> klass) {
66 return get(key, def, klass, false);
67 }
68
69 /**
70 * Get value for the given key
71 * @param <T> the expected type
72 * @param key the key
73 * @param def default value, can be null
74 * @param klass the same as T
75 * @param suppressWarnings show or don't show a warning when some value is
76 * found, but cannot be converted to the requested type
77 * @return if a value that can be converted to class klass has been mapped to key, returns this
78 * value, def otherwise
79 */
80 public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) {
81 if (def != null && !klass.isInstance(def))
82 throw new IllegalArgumentException(def+" is not an instance of "+klass);
83 Object o = prop.get(key);
84 if (o == null)
85 return def;
86 T res = convertTo(o, klass);
87 if (res == null) {
88 if (!suppressWarnings) {
89 Logging.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass()));
90 }
91 return def;
92 } else
93 return res;
94 }
95
96 /**
97 * Gets a property for the given key (like stroke, ...)
98 * @param key The key of the property
99 * @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 @SuppressWarnings("unchecked")
145 public static <T> T convertTo(Object o, Class<T> klass) {
146 if (o == null)
147 return null;
148 if (klass.isInstance(o))
149 return (T) o;
150
151 return GENERIC_PARSER.supports(klass)
152 ? GENERIC_PARSER.parse(klass, o)
153 : null;
154 }
155
156 private static String toString(Object o) {
157 if (o instanceof Keyword)
158 return ((Keyword) o).val;
159 if (o instanceof Color) {
160 return ColorHelper.color2html((Color) o);
161 }
162 return o.toString();
163 }
164
165 private static Float toFloat(Object o) {
166 if (o instanceof Number)
167 return ((Number) o).floatValue();
168 if (o instanceof String && !((String) o).isEmpty()) {
169 try {
170 return Float.valueOf((String) o);
171 } catch (NumberFormatException e) {
172 Logging.debug("''{0}'' cannot be converted to float", o);
173 }
174 }
175 return null;
176 }
177
178 private static Double toDouble(Object o) {
179 final Float number = toFloat(o);
180 return number != null ? Double.valueOf(number) : null;
181 }
182
183 private static Boolean toBool(Object o) {
184 if (o instanceof Boolean)
185 return (Boolean) o;
186 String s = null;
187 if (o instanceof Keyword) {
188 s = ((Keyword) o).val;
189 } else if (o instanceof String) {
190 s = (String) o;
191 }
192 if (s != null)
193 return !(s.isEmpty() || "false".equals(s) || "no".equals(s) || "0".equals(s) || "0.0".equals(s));
194 if (o instanceof Number)
195 return ((Number) o).floatValue() != 0;
196 if (o instanceof List)
197 return !((List<?>) o).isEmpty();
198 if (o instanceof float[])
199 return ((float[]) o).length != 0;
200
201 return null;
202 }
203
204 private static float[] toFloatArray(Object o) {
205 if (o instanceof float[])
206 return (float[]) o;
207 if (o instanceof List) {
208 List<?> l = (List<?>) o;
209 float[] a = new float[l.size()];
210 for (int i = 0; i < l.size(); ++i) {
211 Float f = toFloat(l.get(i));
212 if (f == null)
213 return null;
214 else
215 a[i] = f;
216 }
217 return a;
218 }
219 Float f = toFloat(o);
220 if (f != null)
221 return new float[] {f};
222 return null;
223 }
224
225 private static Color toColor(Object o) {
226 if (o instanceof Color)
227 return (Color) o;
228 if (o instanceof Keyword)
229 return CSSColors.get(((Keyword) o).val);
230 if (o instanceof String) {
231 Color c = CSSColors.get((String) o);
232 if (c != null)
233 return c;
234 if (HEX_COLOR_PATTERN.matcher((String) o).matches()) {
235 return ColorHelper.html2color((String) o);
236 }
237 }
238 return null;
239 }
240
241 @Override
242 public String toString() {
243 StringBuilder res = new StringBuilder("Cascade{ ");
244 // List properties in alphabetical order to be deterministic, without changing "prop" to a TreeMap
245 // (no reason too, not sure about the potential memory/performance impact of such a change)
246 TreeSet<String> props = new TreeSet<>();
247 for (Entry<String, Object> entry : prop.entrySet()) {
248 StringBuilder sb = new StringBuilder(entry.getKey()).append(':');
249 Object val = entry.getValue();
250 if (val instanceof float[]) {
251 sb.append(Arrays.toString((float[]) val));
252 } else if (val instanceof Color) {
253 sb.append(Utils.toString((Color) val));
254 } else if (val != null) {
255 sb.append(val);
256 }
257 sb.append("; ");
258 props.add(sb.toString());
259 }
260 for (String s : props) {
261 res.append(s);
262 }
263 return res.append('}').toString();
264 }
265
266 /**
267 * Checks if this cascade has a value for given key
268 * @param key The key to check
269 * @return <code>true</code> if there is a value
270 */
271 public boolean containsKey(String key) {
272 return prop.containsKey(key);
273 }
274
275 /**
276 * Get if the default selection drawing should be used for the object this cascade applies to
277 * @return <code>true</code> to use the default selection drawing
278 */
279 public boolean isDefaultSelectedHandling() {
280 return defaultSelectedHandling;
281 }
282
283 /**
284 * Set that the default selection drawing should be used for the object this cascade applies to
285 * @param defaultSelectedHandling <code>true</code> to use the default selection drawing
286 */
287 public void setDefaultSelectedHandling(boolean defaultSelectedHandling) {
288 this.defaultSelectedHandling = defaultSelectedHandling;
289 }
290}
Note: See TracBrowser for help on using the repository browser.