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;
012import java.util.stream.Collectors;
013
014import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors;
015import org.openstreetmap.josm.tools.ColorHelper;
016import org.openstreetmap.josm.tools.GenericParser;
017import org.openstreetmap.josm.tools.Logging;
018
019/**
020 * Simple map of properties with dynamic typing.
021 */
022public final class Cascade {
023
024    private final Map<String, Object> prop;
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    private static final GenericParser<Object> GENERIC_PARSER = new GenericParser<>()
031            .registerParser(float.class, Cascade::toFloat)
032            .registerParser(Float.class, Cascade::toFloat)
033            .registerParser(double.class, Cascade::toDouble)
034            .registerParser(Double.class, Cascade::toDouble)
035            .registerParser(boolean.class, Cascade::toBool)
036            .registerParser(Boolean.class, Cascade::toBool)
037            .registerParser(float[].class, Cascade::toFloatArray)
038            .registerParser(Color.class, Cascade::toColor)
039            .registerParser(String.class, Cascade::toString);
040
041    /**
042     * Constructs a new {@code Cascade}.
043     */
044    public Cascade() {
045        this.prop = new HashMap<>();
046    }
047
048    /**
049     * Constructs a new {@code Cascade} from existing one.
050     * @param other other Cascade
051     */
052    public Cascade(Cascade other) {
053        this.prop = new HashMap<>(other.prop);
054    }
055
056    /**
057     * Gets the value for a given key with the given type
058     * @param <T> the expected type
059     * @param key the key
060     * @param def default value, can be null
061     * @param klass the same as T
062     * @return if a value that can be converted to class klass has been mapped to key, returns this
063     *      value, def otherwise
064     */
065    public <T> T get(String key, T def, Class<T> klass) {
066        return get(key, def, klass, false);
067    }
068
069    /**
070     * Get value for the given key
071     * @param <T> the expected type
072     * @param key the key
073     * @param def default value, can be null
074     * @param klass the same as T
075     * @param suppressWarnings show or don't show a warning when some value is
076     *      found, but cannot be converted to the requested type
077     * @return if a value that can be converted to class klass has been mapped to key, returns this
078     *      value, def otherwise
079     */
080    public <T> T get(String key, T def, Class<T> klass, boolean suppressWarnings) {
081        if (def != null && !klass.isInstance(def))
082            throw new IllegalArgumentException(def+" is not an instance of "+klass);
083        Object o = prop.get(key);
084        if (o == null)
085            return def;
086        T res = convertTo(o, klass);
087        if (res == null) {
088            if (!suppressWarnings) {
089                Logging.warn(String.format("Unable to convert property %s to type %s: found %s of type %s!", key, klass, o, o.getClass()));
090            }
091            return def;
092        } else
093            return res;
094    }
095
096    /**
097     * Gets a property for the given key (like stroke, ...)
098     * @param key The key of the property
099     * @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    public static <T> T convertTo(Object o, Class<T> klass) {
145        if (o == null)
146            return null;
147        if (klass.isInstance(o))
148            return klass.cast(o);
149
150        return GENERIC_PARSER.supports(klass)
151                ? GENERIC_PARSER.parse(klass, o)
152                : null;
153    }
154
155    private static String toString(Object o) {
156        if (o instanceof Keyword)
157            return ((Keyword) o).val;
158        if (o instanceof Color) {
159            return ColorHelper.color2html((Color) o);
160        }
161        return o.toString();
162    }
163
164    private static Float toFloat(Object o) {
165        if (o instanceof Number)
166            return ((Number) o).floatValue();
167        if (o instanceof String && !((String) o).isEmpty()) {
168            try {
169                return Float.valueOf((String) o);
170            } catch (NumberFormatException e) {
171                Logging.debug("''{0}'' cannot be converted to float", o);
172            }
173        }
174        return null;
175    }
176
177    private static Double toDouble(Object o) {
178        final Float number = toFloat(o);
179        return number != null ? Double.valueOf(number) : null;
180    }
181
182    private static Boolean toBool(Object o) {
183        if (o instanceof Boolean)
184            return (Boolean) o;
185        String s = null;
186        if (o instanceof Keyword) {
187            s = ((Keyword) o).val;
188        } else if (o instanceof String) {
189            s = (String) o;
190        }
191        if (s != null)
192            return !(s.isEmpty() || "false".equals(s) || "no".equals(s) || "0".equals(s) || "0.0".equals(s));
193        if (o instanceof Number)
194            return ((Number) o).floatValue() != 0;
195        if (o instanceof List)
196            return !((List<?>) o).isEmpty();
197        if (o instanceof float[])
198            return ((float[]) o).length != 0;
199
200        return null;
201    }
202
203    private static float[] toFloatArray(Object o) {
204        if (o instanceof float[])
205            return (float[]) o;
206        if (o instanceof List) {
207            List<?> l = (List<?>) o;
208            float[] a = new float[l.size()];
209            for (int i = 0; i < l.size(); ++i) {
210                Float f = toFloat(l.get(i));
211                if (f == null)
212                    return null;
213                else
214                    a[i] = f;
215            }
216            return a;
217        }
218        Float f = toFloat(o);
219        if (f != null)
220            return new float[] {f};
221        return null;
222    }
223
224    private static Color toColor(Object o) {
225        if (o instanceof Color)
226            return (Color) o;
227        if (o instanceof Keyword)
228            return CSSColors.get(((Keyword) o).val);
229        if (o instanceof String) {
230            Color c = CSSColors.get((String) o);
231            if (c != null)
232                return c;
233            if (HEX_COLOR_PATTERN.matcher((String) o).matches()) {
234                return ColorHelper.html2color((String) o);
235            }
236        }
237        return null;
238    }
239
240    @Override
241    public String toString() {
242        // List properties in alphabetical order to be deterministic, without changing "prop" to a TreeMap
243        // (no reason too, not sure about the potential memory/performance impact of such a change)
244        TreeSet<String> props = new TreeSet<>();
245        for (Entry<String, Object> entry : prop.entrySet()) {
246            StringBuilder sb = new StringBuilder(entry.getKey()).append(':');
247            Object val = entry.getValue();
248            if (val instanceof float[]) {
249                sb.append(Arrays.toString((float[]) val));
250            } else if (val instanceof Color) {
251                sb.append(ColorHelper.color2html((Color) val));
252            } else if (val != null) {
253                sb.append(val);
254            }
255            sb.append("; ");
256            props.add(sb.toString());
257        }
258        return props.stream().collect(Collectors.joining("", "Cascade{ ", "}"));
259    }
260
261    /**
262     * Checks if this cascade has a value for given key
263     * @param key The key to check
264     * @return <code>true</code> if there is a value
265     */
266    public boolean containsKey(String key) {
267        return prop.containsKey(key);
268    }
269
270    /**
271     * Get if the default selection drawing should be used for the object this cascade applies to
272     * @return <code>true</code> to use the default selection drawing
273     */
274    public boolean isDefaultSelectedHandling() {
275        return defaultSelectedHandling;
276    }
277
278    /**
279     * Set that the default selection drawing should be used for the object this cascade applies to
280     * @param defaultSelectedHandling <code>true</code> to use the default selection drawing
281     */
282    public void setDefaultSelectedHandling(boolean defaultSelectedHandling) {
283        this.defaultSelectedHandling = defaultSelectedHandling;
284    }
285}