001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import static org.openstreetmap.josm.tools.Utils.equal;
005
006import java.awt.Color;
007import java.lang.reflect.Array;
008import java.lang.reflect.InvocationTargetException;
009import java.lang.reflect.Method;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.List;
013import java.util.regex.Matcher;
014import java.util.regex.Pattern;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.actions.search.SearchCompiler;
018import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
019import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.gui.mappaint.Cascade;
022import org.openstreetmap.josm.gui.mappaint.Environment;
023import org.openstreetmap.josm.tools.ColorHelper;
024import org.openstreetmap.josm.tools.Utils;
025
026/**
027 * Factory to generate Expressions.
028 *
029 * See {@link #createFunctionExpression}.
030 */
031public final class ExpressionFactory {
032
033    private static final List<Method> arrayFunctions;
034    private static final List<Method> parameterFunctions;
035    private static final Functions FUNCTIONS_INSTANCE = new Functions();
036
037    static {
038        arrayFunctions = new ArrayList<Method>();
039        parameterFunctions = new ArrayList<Method>();
040        for (Method m : Functions.class.getDeclaredMethods()) {
041            Class<?>[] paramTypes = m.getParameterTypes();
042            if (paramTypes.length == 1 && paramTypes[0].isArray()) {
043                arrayFunctions.add(m);
044            } else {
045                parameterFunctions.add(m);
046            }
047        }
048        try {
049            parameterFunctions.add(Math.class.getMethod("abs", float.class));
050            parameterFunctions.add(Math.class.getMethod("acos", double.class));
051            parameterFunctions.add(Math.class.getMethod("asin", double.class));
052            parameterFunctions.add(Math.class.getMethod("atan", double.class));
053            parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
054            parameterFunctions.add(Math.class.getMethod("ceil", double.class));
055            parameterFunctions.add(Math.class.getMethod("cos", double.class));
056            parameterFunctions.add(Math.class.getMethod("cosh", double.class));
057            parameterFunctions.add(Math.class.getMethod("exp", double.class));
058            parameterFunctions.add(Math.class.getMethod("floor", double.class));
059            parameterFunctions.add(Math.class.getMethod("log", double.class));
060            parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
061            parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
062            parameterFunctions.add(Math.class.getMethod("random"));
063            parameterFunctions.add(Math.class.getMethod("round", float.class));
064            parameterFunctions.add(Math.class.getMethod("signum", double.class));
065            parameterFunctions.add(Math.class.getMethod("sin", double.class));
066            parameterFunctions.add(Math.class.getMethod("sinh", double.class));
067            parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
068            parameterFunctions.add(Math.class.getMethod("tan", double.class));
069            parameterFunctions.add(Math.class.getMethod("tanh", double.class));
070        } catch (NoSuchMethodException ex) {
071            throw new RuntimeException(ex);
072        } catch (SecurityException ex) {
073            throw new RuntimeException(ex);
074        }
075    }
076    
077    private ExpressionFactory() {
078        // Hide default constructor for utils classes
079    }
080
081    public static class Functions {
082
083        Environment env;
084
085        public static Object eval(Object o) {
086            return o;
087        }
088
089        public static float plus(float... args) {
090            float res = 0;
091            for (float f : args) {
092                res += f;
093            }
094            return res;
095        }
096
097        public static Float minus(float... args) {
098            if (args.length == 0) {
099                return 0.0F;
100            }
101            if (args.length == 1) {
102                return -args[0];
103            }
104            float res = args[0];
105            for (int i = 1; i < args.length; ++i) {
106                res -= args[i];
107            }
108            return res;
109        }
110
111        public static float times(float... args) {
112            float res = 1;
113            for (float f : args) {
114                res *= f;
115            }
116            return res;
117        }
118
119        public static Float divided_by(float... args) {
120            if (args.length == 0) {
121                return 1.0F;
122            }
123            float res = args[0];
124            for (int i = 1; i < args.length; ++i) {
125                if (args[i] == 0.0F) {
126                    return null;
127                }
128                res /= args[i];
129            }
130            return res;
131        }
132
133        public static List list(Object... args) {
134            return Arrays.asList(args);
135        }
136
137        public static Object get(List<? extends Object> objects, float index) {
138            int idx = Math.round(index);
139            if (idx >= 0 && idx < objects.size()) {
140                return objects.get(idx);
141            }
142            return null;
143        }
144
145        public static List<String> split(String sep, String toSplit) {
146            return Arrays.asList(toSplit.split(Pattern.quote(sep), -1));
147        }
148
149        public static Color rgb(float r, float g, float b) {
150            Color c;
151            try {
152                c = new Color(r, g, b);
153            } catch (IllegalArgumentException e) {
154                return null;
155            }
156            return c;
157        }
158
159        public static Color html2color(String html) {
160            return ColorHelper.html2color(html);
161        }
162
163        public static String color2html(Color c) {
164            return ColorHelper.color2html(c);
165        }
166
167        public static float red(Color c) {
168            return Utils.color_int2float(c.getRed());
169        }
170
171        public static float green(Color c) {
172            return Utils.color_int2float(c.getGreen());
173        }
174
175        public static float blue(Color c) {
176            return Utils.color_int2float(c.getBlue());
177        }
178
179        public static String concat(Object... args) {
180            StringBuilder res = new StringBuilder();
181            for (Object f : args) {
182                res.append(f.toString());
183            }
184            return res.toString();
185        }
186
187        public Object prop(String key) {
188            return prop(key, null);
189        }
190
191        public Object prop(String key, String layer) {
192            Cascade c;
193            if (layer == null) {
194                c = env.mc.getCascade(env.layer);
195            } else {
196                c = env.mc.getCascade(layer);
197            }
198            return c.get(key);
199        }
200
201        public Boolean is_prop_set(String key) {
202            return is_prop_set(key, null);
203        }
204
205        public Boolean is_prop_set(String key, String layer) {
206            Cascade c;
207            if (layer == null) {
208                // env.layer is null if expression is evaluated
209                // in ExpressionCondition, but MultiCascade.getCascade
210                // handles this
211                c = env.mc.getCascade(env.layer);
212            } else {
213                c = env.mc.getCascade(layer);
214            }
215            return c.containsKey(key);
216        }
217
218        public String tag(String key) {
219            return env.osm.get(key);
220        }
221
222        public String parent_tag(String key) {
223            if (env.parent == null) {
224                // we don't have a matched parent, so just search all referrers
225                for (OsmPrimitive parent : env.osm.getReferrers()) {
226                    String value = parent.get(key);
227                    if (value != null) {
228                        return value;
229                    }
230                }
231                return null;
232            }
233            return env.parent.get(key);
234        }
235
236        public boolean has_tag_key(String key) {
237            return env.osm.hasKey(key);
238        }
239
240        public Float index() {
241            if (env.index == null) {
242                return null;
243            }
244            return new Float(env.index + 1);
245        }
246
247        public String role() {
248            return env.getRole();
249        }
250
251        public static boolean not(boolean b) {
252            return !b;
253        }
254
255        public static boolean greater_equal(float a, float b) {
256            return a >= b;
257        }
258
259        public static boolean less_equal(float a, float b) {
260            return a <= b;
261        }
262
263        public static boolean greater(float a, float b) {
264            return a > b;
265        }
266
267        public static boolean less(float a, float b) {
268            return a < b;
269        }
270
271        public static boolean equal(Object a, Object b) {
272            // make sure the casts are done in a meaningful way, so
273            // the 2 objects really can be considered equal
274            for (Class<?> klass : new Class[]{Float.class, Boolean.class, Color.class, float[].class, String.class}) {
275                Object a2 = Cascade.convertTo(a, klass);
276                Object b2 = Cascade.convertTo(b, klass);
277                if (a2 != null && b2 != null && a2.equals(b2)) {
278                    return true;
279                }
280            }
281            return false;
282        }
283
284        public Boolean JOSM_search(String s) {
285            Match m;
286            try {
287                m = SearchCompiler.compile(s, false, false);
288            } catch (ParseError ex) {
289                return null;
290            }
291            return m.match(env.osm);
292        }
293
294        public static String JOSM_pref(String s, String def) {
295            String res = Main.pref.get(s, null);
296            return res != null ? res : def;
297        }
298
299        public static Color JOSM_pref_color(String s, Color def) {
300            Color res = Main.pref.getColor(s, null);
301            return res != null ? res : def;
302        }
303
304        public static boolean regexp_test(String pattern, String target) {
305            return Pattern.matches(pattern, target);
306        }
307
308        public static boolean regexp_test(String pattern, String target, String flags) {
309            int f = 0;
310            if (flags.contains("i")) {
311                f |= Pattern.CASE_INSENSITIVE;
312            }
313            if (flags.contains("s")) {
314                f |= Pattern.DOTALL;
315            }
316            if (flags.contains("m")) {
317                f |= Pattern.MULTILINE;
318            }
319            return Pattern.compile(pattern, f).matcher(target).matches();
320        }
321
322        public static List<String> regexp_match(String pattern, String target, String flags) {
323            int f = 0;
324            if (flags.contains("i")) {
325                f |= Pattern.CASE_INSENSITIVE;
326            }
327            if (flags.contains("s")) {
328                f |= Pattern.DOTALL;
329            }
330            if (flags.contains("m")) {
331                f |= Pattern.MULTILINE;
332            }
333            Matcher m = Pattern.compile(pattern, f).matcher(target);
334            if (m.matches()) {
335                List<String> result = new ArrayList<String>(m.groupCount() + 1);
336                for (int i = 0; i <= m.groupCount(); i++) {
337                    result.add(m.group(i));
338                }
339                return result;
340            } else {
341                return null;
342            }
343        }
344
345        public static List<String> regexp_match(String pattern, String target) {
346            Matcher m = Pattern.compile(pattern).matcher(target);
347            if (m.matches()) {
348                List<String> result = new ArrayList<String>(m.groupCount() + 1);
349                for (int i = 0; i <= m.groupCount(); i++) {
350                    result.add(m.group(i));
351                }
352                return result;
353            } else {
354                return null;
355            }
356        }
357
358        public long osm_id() {
359            return env.osm.getUniqueId();
360        }
361    }
362
363    /**
364     * Main method to create an function-like expression.
365     *
366     * @param name the name of the function or operator
367     * @param args the list of arguments (as expressions)
368     * @return the generated Expression. If no suitable function can be found,
369     * returns {@link NullExpression#INSTANCE}.
370     */
371    public static Expression createFunctionExpression(String name, List<Expression> args) {
372        if (equal(name, "cond") && args.size() == 3)
373            return new CondOperator(args.get(0), args.get(1), args.get(2));
374        else if (equal(name, "and"))
375            return new AndOperator(args);
376        else if (equal(name, "or"))
377            return new OrOperator(args);
378        else if (equal(name, "length") && args.size() == 1)
379            return new LengthFunction(args.get(0));
380
381        for (Method m : arrayFunctions) {
382            if (m.getName().equals(name))
383                return new ArrayFunction(m, args);
384        }
385        for (Method m : parameterFunctions) {
386            if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
387                return new ParameterFunction(m, args);
388        }
389        return NullExpression.INSTANCE;
390    }
391
392    /**
393     * Expression that always evaluates to null.
394     */
395    public static class NullExpression implements Expression {
396
397        final public static NullExpression INSTANCE = new NullExpression();
398
399        @Override
400        public Object evaluate(Environment env) {
401            return null;
402        }
403    }
404
405    /**
406     * Conditional operator.
407     */
408    public static class CondOperator implements Expression {
409
410        private Expression condition, firstOption, secondOption;
411
412        public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
413            this.condition = condition;
414            this.firstOption = firstOption;
415            this.secondOption = secondOption;
416        }
417
418        @Override
419        public Object evaluate(Environment env) {
420            Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
421            if (b != null && b)
422                return firstOption.evaluate(env);
423            else
424                return secondOption.evaluate(env);
425        }
426    }
427
428    public static class AndOperator implements Expression {
429
430        private List<Expression> args;
431
432        public AndOperator(List<Expression> args) {
433            this.args = args;
434        }
435
436        @Override
437        public Object evaluate(Environment env) {
438            for (Expression arg : args) {
439                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
440                if (b == null || !b) {
441                    return false;
442                }
443            }
444            return true;
445        }
446    }
447
448    public static class OrOperator implements Expression {
449
450        private List<Expression> args;
451
452        public OrOperator(List<Expression> args) {
453            this.args = args;
454        }
455
456        @Override
457        public Object evaluate(Environment env) {
458            for (Expression arg : args) {
459                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
460                if (b != null && b) {
461                    return true;
462                }
463            }
464            return false;
465        }
466    }
467
468    /**
469     * Function to calculate the length of a string or list in a MapCSS eval
470     * expression.
471     *
472     * Separate implementation to support overloading for different
473     * argument types.
474     */
475    public static class LengthFunction implements Expression {
476
477        private Expression arg;
478
479        public LengthFunction(Expression args) {
480            this.arg = args;
481        }
482
483        @Override
484        public Object evaluate(Environment env) {
485            List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
486            if (l != null)
487                return l.size();
488            String s = Cascade.convertTo(arg.evaluate(env), String.class);
489            if (s != null)
490                return s.length();
491            return null;
492        }
493    }
494
495    /**
496     * Function that takes a certain number of argument with specific type.
497     *
498     * Implementation is based on a Method object.
499     * If any of the arguments evaluate to null, the result will also be null.
500     */
501    public static class ParameterFunction implements Expression {
502
503        private final Method m;
504        private final List<Expression> args;
505        private final Class<?>[] expectedParameterTypes;
506
507        public ParameterFunction(Method m, List<Expression> args) {
508            this.m = m;
509            this.args = args;
510            expectedParameterTypes = m.getParameterTypes();
511        }
512
513        @Override
514        public Object evaluate(Environment env) {
515            FUNCTIONS_INSTANCE.env = env;
516            Object[] convertedArgs = new Object[expectedParameterTypes.length];
517            for (int i = 0; i < args.size(); ++i) {
518                convertedArgs[i] = Cascade.convertTo(args.get(i).evaluate(env), expectedParameterTypes[i]);
519                if (convertedArgs[i] == null) {
520                    return null;
521                }
522            }
523            Object result = null;
524            try {
525                result = m.invoke(FUNCTIONS_INSTANCE, convertedArgs);
526            } catch (IllegalAccessException ex) {
527                throw new RuntimeException(ex);
528            } catch (IllegalArgumentException ex) {
529                throw new RuntimeException(ex);
530            } catch (InvocationTargetException ex) {
531                Main.error(ex);
532                return null;
533            }
534            return result;
535        }
536    }
537
538    /**
539     * Function that takes an arbitrary number of arguments.
540     *
541     * Currently, all array functions are static, so there is no need to
542     * provide the environment, like it is done in {@link ParameterFunction}.
543     * If any of the arguments evaluate to null, the result will also be null.
544     */
545    public static class ArrayFunction implements Expression {
546
547        private final Method m;
548        private final List<Expression> args;
549        private final Class<?> arrayComponentType;
550        private final Object[] convertedArgs;
551
552        public ArrayFunction(Method m, List<Expression> args) {
553            this.m = m;
554            this.args = args;
555            Class<?>[] expectedParameterTypes = m.getParameterTypes();
556            convertedArgs = new Object[expectedParameterTypes.length];
557            arrayComponentType = expectedParameterTypes[0].getComponentType();
558        }
559
560        @Override
561        public Object evaluate(Environment env) {
562            Object arrayArg = Array.newInstance(arrayComponentType, args.size());
563            for (int i = 0; i < args.size(); ++i) {
564                Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
565                if (o == null) {
566                    return null;
567                }
568                Array.set(arrayArg, i, o);
569            }
570            convertedArgs[0] = arrayArg;
571
572            Object result = null;
573            try {
574                result = m.invoke(null, convertedArgs);
575            } catch (IllegalAccessException ex) {
576                throw new RuntimeException(ex);
577            } catch (IllegalArgumentException ex) {
578                throw new RuntimeException(ex);
579            } catch (InvocationTargetException ex) {
580                Main.error(ex);
581                return null;
582            }
583            return result;
584        }
585    }
586
587}