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