Subversion Repositories mkgmap

Rev

Rev 4290 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2013
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 */

package uk.me.parabola.mkgmap.osmstyle.eval;

import java.util.Collection;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;

import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.osmstyle.function.FunctionFactory;
import uk.me.parabola.mkgmap.osmstyle.function.GetTagFunction;
import uk.me.parabola.mkgmap.osmstyle.function.StyleFunction;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.TokenScanner;
import uk.me.parabola.mkgmap.scan.WordInfo;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokType;

import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.*;


/**
 * Read an expression from a style file.
 */

public class ExpressionReader {
        private static final Logger log = Logger.getLogger(ExpressionReader.class);

        private final Stack<Op> stack = new Stack<>();
        private final Stack<Op> opStack = new Stack<>();
        private final TokenScanner scanner;
        private final FeatureKind kind;

        private final Set<String> usedTags = new HashSet<>();

        public ExpressionReader(TokenScanner scanner, FeatureKind kind) {
                this.scanner = scanner;
                this.kind = kind;
        }

        /**
         * Read the conditions.  They are terminated by a '[' or '{' character
         * or by end of file.
         */

        public Op readConditions() {
                return readConditions(Collections.emptyList());
        }
       
        /**
         * Read the conditions.  They are terminated by a '[' or '{' character
         * or by end of file.
         * @param ifStack expressions of enclosing if / else
         */

        public Op readConditions(Collection <Op[]> ifStack) {
                boolean consumedNonBlank = false;
                while (!scanner.isEndOfFile()) {
                        scanner.skipSpace();
                        if (scanner.checkToken("[") || scanner.checkToken("{") || scanner.checkToken("then"))
                                break;

                        consumedNonBlank = true;
                        WordInfo wordInfo = scanner.nextWordWithInfo();
                        if (isOperation(wordInfo)) {
                                saveOp(wordInfo.getText());
                        } else if (wordInfo.isQuoted()) {
                                pushValue(wordInfo.getText());
                        } else if (wordInfo.getText().charAt(0) == '$') {
                                String tagname = scanner.nextWord();
                                if ("{".equals(tagname)) {
                                        tagname = scanner.nextWord();
                                        scanner.validateNext("}");
                                }
                                stack.push(new GetTagFunction(tagname));
                        } else if (scanner.checkToken("(")) {
                                // it is a function
                                // this requires a () after the function name
                                scanner.validateNext("(");
                                List<String> funcParams = new ArrayList<>();
                                do {
                                        scanner.skipSpace();    
                                        Token tok = scanner.peekToken();
                                        if (tok.getType() != TokType.TEXT &&
                                            (tok.getType() != TokType.SYMBOL ||
                                             !("'".equals(tok.getValue()) || "\"".equals(tok.getValue()))))
                                                break;
                                        WordInfo funcParam = scanner.nextWordWithInfo();
                                        funcParams.add(funcParam.getText());
                                        if (scanner.checkToken(",")) {
                                                /*Token comma = */scanner.nextToken();
                                        } else {
                                                break;
                                        }
                                } while (true);
                                scanner.validateNext(")");
                                try {
                                        saveFunction(wordInfo.getText(), funcParams);
                                } catch (Exception e) {
                                        throw new SyntaxException(scanner, e.getMessage());
                                }
                        } else {
                                pushValue(wordInfo.getText());
                        }
                }

                // Complete building the tree
                while (!opStack.isEmpty())
                        runOp(scanner);

                if (consumedNonBlank && !ifStack.isEmpty() && stack.size() <= 1) {
                        // add expressions from enclosing if /else statements
                        Op op = null;
                        if (!stack.isEmpty())
                                op = stack.pop();
                        stack.push(appendIfExpr(op, ifStack));
                }
                       
                // The stack should contain one entry which is the complete tree
                if (stack.size() != 1) {
                        throw new SyntaxException(scanner, "Stack size is "+stack.size()+" (missing or incomplete expression)");
                }

                assert stack.size() == 1;
                Op op = stack.pop();
                if (op instanceof ValueOp)
                        throw new SyntaxException(scanner, "Incomplete expression, just a single symbol: " + op);
                return op;
        }

        /**
         * Append previously read if/else expressions.
         * @param expr
         * @param ifStack
         * @return
         */

        private static Op appendIfExpr(Op expr, Collection<Op[]> ifStack) {
                Op result = expr;
                for (Op[] ops : ifStack) {
                        if (result != null) {
                                AndOp and = new AndOp();
                                and.setFirst(result);
                                and.setSecond(ops[0].copy());
                                result = and;
                        } else  {
                                result = ops[0].copy();
                        }
                }
                return result;
        }

        /**
         * Is this a token representing an operation?
         * @param token The string to test.
         * @return True if this looks like an operator.
         */

        private static boolean isOperation(WordInfo token) {
                // A quoted word is not an operator eg: '=' is a string.
                if (token.isQuoted())
                        return false;

                // Quick check, operators are 1 or 2 characters long.
                String text = token.getText();
                if (text.length() > 2 || text.isEmpty())
                        return false;

                // If first character is an operation character then it is an operator
                // (or a syntax error)
                char first = text.charAt(0);
                String chars = "&|!=~()><";
                return chars.indexOf(first) >= 0;
        }

        /**
         * Tags used in all the expressions in this file.
         * @return A set of tag names.
         */

        public Set<String> getUsedTags() {
                return usedTags;
        }

        /**
         * An operation is saved on the operation stack.  The tree is built
         * as operations of different priorities arrive.
         */

        private void saveOp(String value) {
                log.debug("save op", value);
                if ("#".equals(value)) {
                        scanner.skipLine();
                        return;
                }

                Op op;
                try {
                        op = AbstractOp.createOp(value);
                        while (!opStack.isEmpty() && opStack.peek().hasHigherPriority(op))
                                runOp(scanner);
                } catch (SyntaxException e) {
                        throw new SyntaxException(scanner, e.getRawMessage());
                }

                if (op.getType() == CLOSE_PAREN) {
                        // Check that there was an opening parenthesis and remove it
                        if (opStack.isEmpty() || !opStack.peek().isType(OPEN_PAREN))
                                throw new SyntaxException(scanner, "No matching open parenthesis");
                        opStack.pop();
                } else {
                        opStack.push(op);
                }
        }

        /**
         * Combine the operation at the top of its stack with its values.
         * @param scanner The token scanner; used for line numbers.
         */

        private void runOp(TokenScanner scanner) {
                Op op = opStack.pop();
                log.debug("Running op...", op.getType());

                if (op instanceof BinaryOp) {
                        if (stack.size() < 2) {
                                throw new SyntaxException(scanner, String.format("Not enough arguments for '%s' operator",
                                                op.getType().toSymbol()));
                        }

                        Op arg2 = stack.pop();
                        Op arg1 = stack.pop();

                        if (arg1.isType(VALUE) /*&& arg2.isType(VALUE)*/)
                                arg1 = new GetTagFunction(arg1.getKeyValue());

                        // Deal with the case where you have: a & b=2.  The 'a' is a syntax error in this case.
                        if (op.isType(OR) || op.isType(AND)) {
                                if (arg1.isType(VALUE) || arg1.isType(FUNCTION))
                                        throw new SyntaxException(scanner, String.format("Value '%s' is not part of an expression", arg1));

                                if (arg2.isType(VALUE) || arg2.isType(FUNCTION))
                                        throw new SyntaxException(scanner, String.format("Value '%s' is not part of an expression", arg2));
                        } else {
                                // All binary ops other than OR and AND take two values. A function is a value
                                // type too.
                                if (!(arg1.isType(VALUE) || arg1.isType(FUNCTION))
                                                || !(arg2.isType(VALUE) || arg2.isType(FUNCTION)))
                                {
                                        String msg = String.format("Invalid arguments to %s: %s (%s) and %s (%s)",
                                                        op.getType(), arg1.getType(), arg1, arg2.getType(), arg2);
                                        throw new SyntaxException(scanner, msg);
                                }

                        }

                        BinaryOp binaryOp = (BinaryOp) op;
                        binaryOp.setFirst(arg1);
                        binaryOp.setSecond(arg2);

                        // The combination foo=* is converted to exists(foo).
                        if (op.isType(EQUALS) && arg2.isType(VALUE) && ((ValueOp) arg2).isValue("*")) {
                                log.debug("convert to EXISTS");
                                op = new ExistsOp();
                                op.setFirst(arg1);
                        } else if (op.isType(NOT_EQUALS) && arg2.isType(VALUE) && ((ValueOp) arg2).isValue("*")) {
                                log.debug("convert to NOT EXISTS");
                                op = new NotExistsOp();
                                op.setFirst(arg1);
                        }
                } else if (!op.isType(OPEN_PAREN)) {
                        if (stack.isEmpty())
                                throw new SyntaxException(scanner, String.format("Missing argument for %s operator",
                                                op.getType().toSymbol()));
                        op.setFirst(stack.pop());
                }

                Op first = op.getFirst();
                if (first == null)
                        throw new SyntaxException(scanner, "Invalid expression");

                if (first.isType(FUNCTION) && first.getKeyValue() != null) {
                        usedTags.add(first.getKeyValue());
                }
               

                stack.push(op);
        }

        /**
         * Lookup a function by its name and check that it is allowed for the kind of features that we
         * are reading.
         *
         * @param functionName A name to look up.
         */

    private void saveFunction(String functionName, List<String> functionParams) {
                StyleFunction function = FunctionFactory.createFunction(functionName);
                if (function == null)
                        throw new SyntaxException(String.format("No function with name '%s()'", functionName));
                function.setParams(functionParams, kind);

                // TODO: supportsWay split into supportsPoly{line,gon}, or one function supports(kind)
                boolean supported = false;
                switch (kind) {
                case POINT:
                        if (function.supportsNode()) supported = true;
                        break;
                case POLYLINE:
                        if (function.supportsWay()) supported = true;
                        break;
                case POLYGON:
                        if (function.supportsWay()) supported = true;
                        break;
                case RELATION:
                        if (function.supportsRelation()) supported = true;
                        break;
                case ALL:
                        if (function.supportsNode() || function.supportsWay() || function.supportsRelation()) supported = true;
                        break;
                }

                if (!supported)
                        throw new SyntaxException(String.format("Function '%s()' not supported for %s", functionName, kind));
                usedTags.addAll(function.getUsedTags());
                stack.push(function);
        }

        private void pushValue(String value) {
                stack.push(new ValueOp(value));
        }
}