diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/ConditionalExpressionVisitor.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/ConditionalExpressionVisitor.java index 7ad89132c6c..61fc0629103 100644 --- a/core/src/main/java/com/graphhopper/routing/weighting/custom/ConditionalExpressionVisitor.java +++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/ConditionalExpressionVisitor.java @@ -105,6 +105,10 @@ public Boolean visitRvalue(Java.Rvalue rv) throws Exception { return true; } } + } else if (n.identifiers.length == 1 && mi.arguments.length == 0 && variableValidator.isValid(n.identifiers[0])) { + // track_type.ordinal() + result.guessedVariables.add(n.identifiers[0]); + return true; } } invalidMessage = mi.methodName + " is an illegal method in a conditional expression"; diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java index c5390e3dbbc..972bbc677cf 100644 --- a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java +++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java @@ -17,42 +17,34 @@ */ package com.graphhopper.routing.weighting.custom; -import com.graphhopper.GraphHopper; -import com.graphhopper.json.MinMax; import com.graphhopper.json.Statement; -import com.graphhopper.routing.ev.*; +import com.graphhopper.routing.ev.BooleanEncodedValue; +import com.graphhopper.routing.ev.DecimalEncodedValue; +import com.graphhopper.routing.ev.EncodedValueLookup; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.weighting.TurnCostProvider; -import com.graphhopper.util.*; -import com.graphhopper.util.shapes.BBox; -import com.graphhopper.util.shapes.Polygon; -import org.codehaus.commons.compiler.CompileException; -import org.codehaus.commons.compiler.Location; -import org.codehaus.commons.compiler.io.Readers; -import org.codehaus.janino.Scanner; -import org.codehaus.janino.*; -import org.codehaus.janino.util.DeepCopier; -import org.locationtech.jts.geom.Polygonal; -import org.locationtech.jts.geom.prep.PreparedPolygon; +import com.graphhopper.util.CustomModel; +import com.graphhopper.util.Helper; +import com.graphhopper.util.JsonFeature; import org.slf4j.LoggerFactory; -import java.io.*; -import java.util.*; -import java.util.concurrent.atomic.AtomicLong; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.List; +import java.util.Set; public class CustomModelParser { - private static final AtomicLong longVal = new AtomicLong(1); static final String IN_AREA_PREFIX = "in_"; static final String BACKWARD_PREFIX = "backward_"; - private static final boolean JANINO_DEBUG = Boolean.getBoolean(Scanner.SYSTEM_PROPERTY_SOURCE_DEBUGGING_ENABLE); - private static final String SCRIPT_FILE_DIR = System.getProperty(Scanner.SYSTEM_PROPERTY_SOURCE_DEBUGGING_DIR, "./src/main/java/com/graphhopper/routing/weighting/custom"); // Without a cache the class creation takes 10-40ms which makes routingLM8 requests 20% slower on average. // CH requests and preparation is unaffected as cached weighting from preparation is used. // Use accessOrder==true to remove oldest accessed entry, not oldest inserted. private static final int CACHE_SIZE = Integer.getInteger("graphhopper.custom_weighting.cache_size", 1000); - private static final Map> CACHE = Collections.synchronizedMap( - new LinkedHashMap>(CACHE_SIZE, 0.75f, true) { + private static final Map CACHE = Collections.synchronizedMap( + new LinkedHashMap(CACHE_SIZE, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry eldest) { return size() > CACHE_SIZE; } @@ -62,7 +54,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { // of how frequent other Weightings are created and accessed. We only need to synchronize the get and put methods alone. // E.g. we do not care for the race condition where two identical classes are requested and one of them is overwritten. // TODO perf compare with ConcurrentHashMap, but I guess, if there is a difference at all, it is not big for small maps - private static final Map> INTERNAL_CACHE = Collections.synchronizedMap(new HashMap<>()); + private static final Map INTERNAL_CACHE = Collections.synchronizedMap(new HashMap<>()); private CustomModelParser() { // utility class @@ -100,293 +92,42 @@ public static CustomWeighting.Parameters createWeightingParameters(CustomModel c if (key.length() > 100_000) throw new IllegalArgumentException("Custom Model too big: " + key.length()); - Class clazz = customModel.isInternal() ? INTERNAL_CACHE.get(key) : null; - if (CACHE_SIZE > 0 && clazz == null) - clazz = CACHE.get(key); - if (clazz == null) { - if (GraphHopper.isAndroid()) { - clazz = AndroidWeightingHelperCreator.createClazz(customModel, lookup, globalMaxSpeed, globalMaxPriority); - } else { - clazz = createClazz(customModel, lookup, globalMaxSpeed, globalMaxPriority); - } - if (customModel.isInternal()) { - INTERNAL_CACHE.put(key, clazz); - if (INTERNAL_CACHE.size() > 100) { - CACHE.putAll(INTERNAL_CACHE); - INTERNAL_CACHE.clear(); - LoggerFactory.getLogger(CustomModelParser.class).warn("Internal cache must stay small but was " - + INTERNAL_CACHE.size() + ". Cleared it. Misuse of CustomModel::internal?"); - } - } else if (CACHE_SIZE > 0) { - CACHE.put(key, clazz); - } - } - - try { - // The class does not need to be thread-safe as we create an instance per request - CustomWeightingHelper prio = (CustomWeightingHelper) clazz.getDeclaredConstructor().newInstance(); - prio.init(lookup, avgSpeedEnc, priorityEnc, CustomModel.getAreasAsMap(customModel.getAreas())); - return new CustomWeighting.Parameters(prio::getSpeed, prio::getPriority, prio.getMaxSpeed(), prio.getMaxPriority(), - customModel.getDistanceInfluence() == null ? 0 : customModel.getDistanceInfluence(), - customModel.getHeadingPenalty() == null ? Parameters.Routing.DEFAULT_HEADING_PENALTY : customModel.getHeadingPenalty()); - } catch (ReflectiveOperationException ex) { - throw new IllegalArgumentException("Cannot compile expression " + ex.getMessage(), ex); - } - } - - /** - * This method does the following: - *
    - *
  • 0. optionally we already checked the right-hand side expressions before this method call in FindMinMax.checkLMConstraints - * (only the client-side custom model statements) - *
  • - *
  • 1. determine minimum and maximum values via parsing the right-hand side expression -> done in ValueExpressionVisitor. - * We need the maximum values for a simple negative check AND for the CustomWeighting.Parameters which is for - * Weighting.getMinWeight which is for A*. Note: we could make this step optional somehow for other algorithms, - * but parsing would be still required in the next step for security reasons. - *
  • - *
  • 2. parse condition value of priority and speed statements -> done in ConditionalExpressionVisitor (don't parse RHS expressions again) - *
  • - *
  • 3. create class template as String, inject the created statements and create the Class - *
  • - *
- */ - private static Class createClazz(CustomModel customModel, EncodedValueLookup lookup, - double globalMaxSpeed, double globalMaxPriority) { try { - HashSet priorityVariables = new LinkedHashSet<>(); - // initial value of minimum has to be >0 so that multiple_by with a negative value leads to a negative value and not 0 - MinMax minMaxPriority = new MinMax(1, globalMaxPriority); - FindMinMax.findMinMax(priorityVariables, minMaxPriority, customModel.getPriority(), lookup); - if (minMaxPriority.min < 0) - throw new IllegalArgumentException("priority has to be >=0 but can be negative (" + minMaxPriority.min + ")"); - if (minMaxPriority.max < 0) - throw new IllegalArgumentException("maximum priority has to be >=0 but was " + minMaxPriority.max); - List priorityStatements = createGetPriorityStatements(priorityVariables, customModel, lookup); - - HashSet speedVariables = new LinkedHashSet<>(); - MinMax minMaxSpeed = new MinMax(1, globalMaxSpeed); - FindMinMax.findMinMax(speedVariables, minMaxSpeed, customModel.getSpeed(), lookup); - if (minMaxSpeed.min < 0) - throw new IllegalArgumentException("speed has to be >=0 but can be negative (" + minMaxSpeed.min + ")"); - if (minMaxSpeed.max <= 0) - throw new IllegalArgumentException("maximum speed has to be >0 but was " + minMaxSpeed.max); - List speedStatements = createGetSpeedStatements(speedVariables, customModel, lookup); - // Create different class name, which is required only for debugging. - // TODO does it improve performance too? I.e. it could be that the JIT is confused if different classes - // have the same name and it mixes performance stats. See https://github.com/janino-compiler/janino/issues/137 - long counter = longVal.incrementAndGet(); - String classTemplate = createClassTemplate(counter, priorityVariables, minMaxPriority.max, speedVariables, minMaxSpeed.max, - lookup, CustomModel.getAreasAsMap(customModel.getAreas())); - Java.CompilationUnit cu = (Java.CompilationUnit) new Parser(new Scanner("source", new StringReader(classTemplate))). - parseAbstractCompilationUnit(); - cu = injectStatements(priorityStatements, speedStatements, cu); - SimpleCompiler sc = createCompiler(counter, cu); - return sc.getClassLoader().loadClass("com.graphhopper.routing.weighting.custom.JaninoCustomWeightingHelperSubclass" + counter); - } catch (Exception ex) { - String errString = "Cannot compile expression"; - throw new IllegalArgumentException(errString + ": " + ex.getMessage(), ex); - } - } - - /** - * Parse the expressions from CustomModel relevant for the method getSpeed - see createClassTemplate. - * - * @return the created statements (parsed expressions) - */ - private static List createGetSpeedStatements(Set speedVariables, - CustomModel customModel, EncodedValueLookup lookup) throws Exception { - List speedStatements = new ArrayList<>(verifyExpressions(new StringBuilder(), - "speed entry", speedVariables, customModel.getSpeed(), lookup)); - String speedMethodStartBlock = "double value = super.getRawSpeed(edge, reverse);\n"; - // a bit inefficient to possibly define variables twice, but for now we have two separate methods - for (String arg : speedVariables) { - speedMethodStartBlock += getVariableDeclaration(lookup, arg); - } - speedStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getSpeed", new StringReader(speedMethodStartBlock))). - parseBlockStatements()); - return speedStatements; - } - - /** - * Parse the expressions from CustomModel relevant for the method getPriority - see createClassTemplate. - * - * @return the created statements (parsed expressions) - */ - private static List createGetPriorityStatements(Set priorityVariables, - CustomModel customModel, EncodedValueLookup lookup) throws Exception { - List priorityStatements = new ArrayList<>(verifyExpressions(new StringBuilder(), - "priority entry", priorityVariables, customModel.getPriority(), lookup)); - String priorityMethodStartBlock = "double value = super.getRawPriority(edge, reverse);\n"; - for (String arg : priorityVariables) { - priorityMethodStartBlock += getVariableDeclaration(lookup, arg); - } - priorityStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getPriority", new StringReader(priorityMethodStartBlock))). - parseBlockStatements()); - return priorityStatements; - } - - /** - * For the methods getSpeed and getPriority we declare variables that contain the encoded value of the current edge - * or if an area contains the current edge. - */ - private static String getVariableDeclaration(EncodedValueLookup lookup, final String arg) { - if (lookup.hasEncodedValue(arg)) { - EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class); - return getReturnType(enc) + " " + arg + " = (" + getReturnType(enc) + ") (reverse ? " + - "edge.getReverse((" + getInterface(enc) + ") this." + arg + "_enc) : " + - "edge.get((" + getInterface(enc) + ") this." + arg + "_enc));\n"; - } else if (arg.startsWith(BACKWARD_PREFIX)) { - final String argSubstr = arg.substring(BACKWARD_PREFIX.length()); - if (lookup.hasEncodedValue(argSubstr)) { - EncodedValue enc = lookup.getEncodedValue(argSubstr, EncodedValue.class); - return getReturnType(enc) + " " + arg + " = (" + getReturnType(enc) + ") (reverse ? " + - "edge.get((" + getInterface(enc) + ") this." + argSubstr + "_enc) : " + - "edge.getReverse((" + getInterface(enc) + ") this." + argSubstr + "_enc));\n"; - } else { - throw new IllegalArgumentException("Not supported for backward: " + argSubstr); - } - } else if (arg.startsWith(IN_AREA_PREFIX)) { - return ""; - } else { - throw new IllegalArgumentException("Not supported " + arg); - } - } - - /** - * @return the interface as string of the provided EncodedValue, e.g. IntEncodedValue (only interface) or - * BooleanEncodedValue (first interface). For StringEncodedValue we return IntEncodedValue to return the index - * instead of the String for faster comparison. - */ - private static String getInterface(EncodedValue enc) { - if (enc instanceof StringEncodedValue) return IntEncodedValue.class.getSimpleName(); - if (enc.getClass().getInterfaces().length == 0) return enc.getClass().getSimpleName(); - return enc.getClass().getInterfaces()[0].getSimpleName(); - } - - private static String getReturnType(EncodedValue encodedValue) { - // order is important - if (encodedValue instanceof EnumEncodedValue) - return ((EnumEncodedValue) encodedValue).getEnumSimpleName(); - if (encodedValue instanceof StringEncodedValue) return "int"; // we use indexOf - if (encodedValue instanceof DecimalEncodedValue) return "double"; - if (encodedValue instanceof BooleanEncodedValue) return "boolean"; - if (encodedValue instanceof IntEncodedValue) return "int"; - throw new IllegalArgumentException("Unsupported EncodedValue: " + encodedValue.getClass()); - } - - /** - * Create the class source file from the detected variables (priorityVariables and speedVariables). We assume that - * these variables are safe although they are user input because we collected them from parsing via Janino. This - * means that the source file is free from user input and could be directly compiled. Before we do this we still - * have to inject that parsed and safe user expressions in a later step. - */ - private static String createClassTemplate(long counter, - Set priorityVariables, double maxPriority, - Set speedVariables, double maxSpeed, - EncodedValueLookup lookup, Map areas) { - final StringBuilder importSourceCode = new StringBuilder("import com.graphhopper.routing.ev.*;\n"); - importSourceCode.append("import java.util.Map;\n"); - final StringBuilder classSourceCode = new StringBuilder(100); - boolean includedAreaImports = false; - - final StringBuilder initSourceCode = new StringBuilder("this.avg_speed_enc = avgSpeedEnc;\n"); - initSourceCode.append("this.priority_enc = priorityEnc;\n"); - Set set = new HashSet<>(); - for (String prioVar : priorityVariables) - set.add(prioVar.startsWith(BACKWARD_PREFIX) ? prioVar.substring(BACKWARD_PREFIX.length()) : prioVar); - for (String speedVar : speedVariables) - set.add(speedVar.startsWith(BACKWARD_PREFIX) ? speedVar.substring(BACKWARD_PREFIX.length()) : speedVar); - - for (String arg : set) { - if (lookup.hasEncodedValue(arg)) { - EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class); - classSourceCode.append("protected " + getInterface(enc) + " " + arg + "_enc;\n"); - initSourceCode.append("this." + arg + "_enc = (" + getInterface(enc) - + ") lookup.getEncodedValue(\"" + arg + "\", EncodedValue.class);\n"); - } else if (arg.startsWith(IN_AREA_PREFIX)) { - if (!includedAreaImports) { - importSourceCode.append("import " + BBox.class.getName() + ";\n"); - importSourceCode.append("import " + GHUtility.class.getName() + ";\n"); - importSourceCode.append("import " + PreparedPolygon.class.getName() + ";\n"); - importSourceCode.append("import " + Polygonal.class.getName() + ";\n"); - importSourceCode.append("import " + JsonFeature.class.getName() + ";\n"); - importSourceCode.append("import " + Polygon.class.getName() + ";\n"); - includedAreaImports = true; + InterpretedCustomWeightingHelper.validateModel(customModel, lookup); + + InterpretedCustomWeightingHelper.CompiledModel compiled = customModel.isInternal() ? INTERNAL_CACHE.get(key) : null; + if (compiled == null && CACHE_SIZE > 0) + compiled = CACHE.get(key); + if (compiled == null) { + compiled = InterpretedCustomWeightingHelper.compile(customModel, lookup, globalMaxSpeed, globalMaxPriority); + + if (customModel.isInternal()) { + INTERNAL_CACHE.put(key, compiled); + if (INTERNAL_CACHE.size() > 100) { + CACHE.putAll(INTERNAL_CACHE); + INTERNAL_CACHE.clear(); + LoggerFactory.getLogger(CustomModelParser.class).warn("Internal cache must stay small but was " + + INTERNAL_CACHE.size() + ". Cleared it. Misuse of CustomModel::internal?"); + } + } else if (CACHE_SIZE > 0) { + CACHE.put(key, compiled); } - - if (!JsonFeature.isValidId(arg)) - throw new IllegalArgumentException("Area has invalid name: " + arg); - String id = arg.substring(IN_AREA_PREFIX.length()); - JsonFeature feature = areas.get(id); - if (feature == null) - throw new IllegalArgumentException("Area '" + id + "' wasn't found"); - if (feature.getGeometry() == null) - throw new IllegalArgumentException("Area '" + id + "' does not contain a geometry"); - if (!(feature.getGeometry() instanceof Polygonal)) - throw new IllegalArgumentException("Currently only type=Polygon is supported for areas but was " + feature.getGeometry().getGeometryType()); - if (feature.getBBox() != null) - throw new IllegalArgumentException("Bounding box of area " + id + " must be empty"); - classSourceCode.append("protected " + Polygon.class.getSimpleName() + " " + arg + ";\n"); - initSourceCode.append("JsonFeature feature_" + id + " = (JsonFeature) areas.get(\"" + id + "\");\n"); - initSourceCode.append("this." + arg + " = new Polygon(new PreparedPolygon((Polygonal) feature_" + id + ".getGeometry()));\n"); - } else { - if (!arg.startsWith(IN_AREA_PREFIX)) - throw new IllegalArgumentException("Variable not supported: " + arg); } - } - return "" - + "package com.graphhopper.routing.weighting.custom;\n" - + "import " + CustomWeightingHelper.class.getName() + ";\n" - + "import " + EncodedValueLookup.class.getName() + ";\n" - + "import " + EdgeIteratorState.class.getName() + ";\n" - + importSourceCode - + "\npublic class JaninoCustomWeightingHelperSubclass" + counter + " extends " + CustomWeightingHelper.class.getSimpleName() + " {\n" - + classSourceCode - + " @Override\n" - + " public void init(EncodedValueLookup lookup, " + DecimalEncodedValue.class.getName() + " avgSpeedEnc, " - + DecimalEncodedValue.class.getName() + " priorityEnc, Map areas) {\n" - + initSourceCode - + " }\n\n" - // we need these placeholder methods so that the hooks in DeepCopier are invoked - + " @Override\n" - + " public double getPriority(EdgeIteratorState edge, boolean reverse) {\n" - + " return 1; //will be overwritten by code injected in DeepCopier\n" - + " }\n" - + " @Override\n" - + " public double getSpeed(EdgeIteratorState edge, boolean reverse) {\n" - + " return getRawSpeed(edge, reverse); //will be overwritten by code injected in DeepCopier\n" - + " }\n" - + " @Override\n" - + " protected double getMaxSpeed() {\n" - + " return " + maxSpeed + ";" - + " }\n" - + " @Override\n" - + " protected double getMaxPriority() {\n" - + " return " + maxPriority + ";" - + " }\n" - + "}"; - } - - /** - * This method does: - * 1. check user expressions via Parser.parseConditionalExpression and only allow whitelisted variables and methods. - * 2. while this check it also guesses the variable names and stores it in createObjects - * 3. creates if-then-elseif expressions from the checks and returns them as BlockStatements - * - * @return the created if-then, else and elseif statements - */ - private static List verifyExpressions(StringBuilder expressions, String info, Set createObjects, - List list, EncodedValueLookup lookup) throws Exception { - // allow variables, all encoded values, constants and special variables like in_xyarea or backward_car_access - NameValidator nameInConditionValidator = name -> lookup.hasEncodedValue(name) - || name.toUpperCase(Locale.ROOT).equals(name) || name.startsWith(IN_AREA_PREFIX) - || name.startsWith(BACKWARD_PREFIX) && lookup.hasEncodedValue(name.substring(BACKWARD_PREFIX.length())); - - parseExpressions(expressions, nameInConditionValidator, info, createObjects, list); - return new Parser(new org.codehaus.janino.Scanner(info, new StringReader(expressions.toString()))). - parseBlockStatements(); + Map areaFeatures = CustomModel.getAreasAsMap(customModel.getAreas()); + return compiled.createParameters(lookup, avgSpeedEnc, priorityEnc, areaFeatures); + } catch (IllegalArgumentException ex) { + CACHE.remove(key); + INTERNAL_CACHE.remove(key); + String message = ex.getMessage(); + if (message != null && message.startsWith("Cannot compile expression")) + throw ex; + throw new IllegalArgumentException("Cannot compile expression: " + message, ex); + } catch (RuntimeException ex) { + CACHE.remove(key); + INTERNAL_CACHE.remove(key); + throw new IllegalArgumentException("Cannot compile expression: " + ex.getMessage(), ex); + } } static void parseExpressions(StringBuilder expressions, NameValidator nameInConditionValidator, @@ -414,80 +155,4 @@ static void parseExpressions(StringBuilder expressions, NameValidator nameInCond } expressions.append("return value;\n"); } - - /** - * Injects the already parsed expressions (converted to BlockStatement) via Janino's DeepCopier to the provided - * CompilationUnit cu (a class file). - */ - private static Java.CompilationUnit injectStatements(List priorityStatements, - List speedStatements, - Java.CompilationUnit cu) throws CompileException { - cu = new DeepCopier() { - boolean speedInjected = false; - boolean priorityInjected = false; - - @Override - public Java.MethodDeclarator copyMethodDeclarator(Java.MethodDeclarator subject) throws CompileException { - if (subject.name.equals("getSpeed") && !speedStatements.isEmpty() && !speedInjected) { - speedInjected = true; - return injectStatements(subject, this, speedStatements); - } else if (subject.name.equals("getPriority") && !priorityStatements.isEmpty() && !priorityInjected) { - priorityInjected = true; - return injectStatements(subject, this, priorityStatements); - } else { - return super.copyMethodDeclarator(subject); - } - } - }.copyCompilationUnit(cu); - return cu; - } - - private static Java.MethodDeclarator injectStatements(Java.MethodDeclarator subject, DeepCopier deepCopier, - List statements) { - try { - if (statements.isEmpty()) - throw new IllegalArgumentException("Statements cannot be empty when copying method"); - Java.MethodDeclarator methodDecl = new Java.MethodDeclarator( - new Location("m1", 1, 1), - subject.getDocComment(), - deepCopier.copyModifiers(subject.getModifiers()), - deepCopier.copyOptionalTypeParameters(subject.typeParameters), - deepCopier.copyType(subject.type), - subject.name, - deepCopier.copyFormalParameters(subject.formalParameters), - deepCopier.copyTypes(subject.thrownExceptions), - deepCopier.copyOptionalElementValue(subject.defaultValue), - deepCopier.copyOptionalStatements(statements) - ); - statements.forEach(st -> st.setEnclosingScope(methodDecl)); - return methodDecl; - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private static SimpleCompiler createCompiler(long counter, Java.AbstractCompilationUnit cu) throws CompileException { - if (JANINO_DEBUG) { - try { - StringWriter sw = new StringWriter(); - Unparser.unparse(cu, sw); - // System.out.println(sw.toString()); - File dir = new File(SCRIPT_FILE_DIR); - File temporaryFile = new File(dir, "JaninoCustomWeightingHelperSubclass" + counter + ".java"); - Reader reader = Readers.teeReader( - new StringReader(sw.toString()), // in - new FileWriter(temporaryFile), // out - true // closeWriterOnEoi - ); - return new SimpleCompiler(temporaryFile.getAbsolutePath(), reader); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } else { - SimpleCompiler compiler = new SimpleCompiler(); - // compiler.setWarningHandler((handle, message, location) -> System.out.println(handle + ", " + message + ", " + location)); - compiler.cook(cu); - return compiler; - } - } } diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/InterpretedCustomWeightingHelper.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/InterpretedCustomWeightingHelper.java new file mode 100644 index 00000000000..d2e7aaf2e93 --- /dev/null +++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/InterpretedCustomWeightingHelper.java @@ -0,0 +1,869 @@ +package com.graphhopper.routing.weighting.custom; + +import com.graphhopper.json.MinMax; +import com.graphhopper.json.Statement; +import com.graphhopper.routing.ev.BooleanEncodedValue; +import com.graphhopper.routing.ev.DecimalEncodedValue; +import com.graphhopper.routing.ev.EncodedValue; +import com.graphhopper.routing.ev.EncodedValueLookup; +import com.graphhopper.routing.ev.EnumEncodedValue; +import com.graphhopper.routing.ev.IntEncodedValue; +import com.graphhopper.routing.ev.StringEncodedValue; +import com.graphhopper.routing.weighting.custom.CustomWeighting.EdgeToDoubleMapping; +import com.graphhopper.util.CustomModel; +import com.graphhopper.util.EdgeIteratorState; +import com.graphhopper.util.Helper; +import com.graphhopper.util.JsonFeature; +import com.graphhopper.util.Parameters; +import com.graphhopper.util.shapes.Polygon; +import org.codehaus.commons.compiler.CompileException; +import org.codehaus.janino.Java; +import org.codehaus.janino.Parser; +import org.codehaus.janino.Scanner; +import org.codehaus.janino.TokenType; +import org.locationtech.jts.geom.Polygonal; +import org.locationtech.jts.geom.prep.PreparedPolygon; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static com.graphhopper.routing.weighting.custom.CustomModelParser.BACKWARD_PREFIX; +import static com.graphhopper.routing.weighting.custom.CustomModelParser.IN_AREA_PREFIX; + +final class InterpretedCustomWeightingHelper extends CustomWeightingHelper { + + static CompiledModel compile(CustomModel customModel, EncodedValueLookup lookup, + double globalMaxSpeed, double globalMaxPriority) { + validateValueExpressions(customModel.getPriority(), lookup); + compileRuleBlocks(customModel.getPriority(), "priority entry", lookup, new LinkedHashSet<>()); + + Set priorityVariables = new LinkedHashSet<>(); + MinMax minMaxPriority = new MinMax(1, globalMaxPriority); + FindMinMax.findMinMax(priorityVariables, minMaxPriority, customModel.getPriority(), lookup); + if (minMaxPriority.min < 0) + throw new IllegalArgumentException("priority has to be >=0 but can be negative (" + minMaxPriority.min + ")"); + if (minMaxPriority.max < 0) + throw new IllegalArgumentException("maximum priority has to be >=0 but was " + minMaxPriority.max); + List priorityBlocks = compileRuleBlocks(customModel.getPriority(), "priority entry", lookup, priorityVariables); + + validateValueExpressions(customModel.getSpeed(), lookup); + compileRuleBlocks(customModel.getSpeed(), "speed entry", lookup, new LinkedHashSet<>()); + + Set speedVariables = new LinkedHashSet<>(); + MinMax minMaxSpeed = new MinMax(1, globalMaxSpeed); + FindMinMax.findMinMax(speedVariables, minMaxSpeed, customModel.getSpeed(), lookup); + if (minMaxSpeed.min < 0) + throw new IllegalArgumentException("speed has to be >=0 but can be negative (" + minMaxSpeed.min + ")"); + if (minMaxSpeed.max <= 0) + throw new IllegalArgumentException("maximum speed has to be >0 but was " + minMaxSpeed.max); + List speedBlocks = compileRuleBlocks(customModel.getSpeed(), "speed entry", lookup, speedVariables); + + Set allVariables = new LinkedHashSet<>(); + allVariables.addAll(priorityVariables); + allVariables.addAll(speedVariables); + + double distanceInfluence = customModel.getDistanceInfluence() == null ? 0 : customModel.getDistanceInfluence(); + double headingPenalty = customModel.getHeadingPenalty() == null ? Parameters.Routing.DEFAULT_HEADING_PENALTY : customModel.getHeadingPenalty(); + + return new CompiledModel(speedBlocks, priorityBlocks, allVariables, minMaxSpeed.max, minMaxPriority.max, + distanceInfluence, headingPenalty); + } + + static final class CompiledModel { + private final List speedBlocks; + private final List priorityBlocks; + private final Set variables; + private final double maxSpeed; + private final double maxPriority; + private final double distanceInfluence; + private final double headingPenalty; + + CompiledModel(List speedBlocks, + List priorityBlocks, + Set variables, + double maxSpeed, + double maxPriority, + double distanceInfluence, + double headingPenalty) { + this.speedBlocks = Collections.unmodifiableList(new ArrayList<>(speedBlocks)); + this.priorityBlocks = Collections.unmodifiableList(new ArrayList<>(priorityBlocks)); + this.variables = Collections.unmodifiableSet(new LinkedHashSet<>(variables)); + this.maxSpeed = maxSpeed; + this.maxPriority = maxPriority; + this.distanceInfluence = distanceInfluence; + this.headingPenalty = headingPenalty; + } + + CustomWeighting.Parameters createParameters(EncodedValueLookup lookup, + DecimalEncodedValue avgSpeedEnc, + DecimalEncodedValue priorityEnc, + Map areas) { + BuildResult buildResult = buildVariableAccessors(variables, lookup, areas); + InterpretedCustomWeightingHelper helper = new InterpretedCustomWeightingHelper( + speedBlocks, + priorityBlocks, + buildResult.variableAccessors, + buildResult.enumLookups, + buildResult.areaPolygons, + maxSpeed, + maxPriority + ); + helper.init(lookup, avgSpeedEnc, priorityEnc, areas); + return new CustomWeighting.Parameters(helper::getSpeed, helper::getPriority, helper.getMaxSpeed(), helper.getMaxPriority(), + distanceInfluence, headingPenalty); + } + } + + private final List speedBlocks; + private final List priorityBlocks; + private final Map variableAccessors; + private final Map enumLookups; + private final Map areaPolygons; + private final double maxSpeed; + private final double maxPriority; + + private InterpretedCustomWeightingHelper(List speedBlocks, + List priorityBlocks, + Map variableAccessors, + Map enumLookups, + Map areaPolygons, + double maxSpeed, + double maxPriority) { + this.speedBlocks = speedBlocks; + this.priorityBlocks = priorityBlocks; + this.variableAccessors = variableAccessors; + this.enumLookups = enumLookups; + this.areaPolygons = areaPolygons; + this.maxSpeed = maxSpeed; + this.maxPriority = maxPriority; + } + + @Override + public void init(EncodedValueLookup lookup, DecimalEncodedValue avgSpeedEnc, DecimalEncodedValue priorityEnc, Map areas) { + super.init(lookup, avgSpeedEnc, priorityEnc, areas); + } + + @Override + public double getPriority(EdgeIteratorState edge, boolean reverse) { + double value = getRawPriority(edge, reverse); + if (priorityBlocks.isEmpty()) + return value; + EvaluationContext ctx = new EvaluationContext(this, edge, reverse); + for (RuleBlock block : priorityBlocks) { + value = block.apply(value, ctx); + } + return value; + } + + @Override + public double getSpeed(EdgeIteratorState edge, boolean reverse) { + double value = getRawSpeed(edge, reverse); + if (speedBlocks.isEmpty()) + return value; + EvaluationContext ctx = new EvaluationContext(this, edge, reverse); + for (RuleBlock block : speedBlocks) { + value = block.apply(value, ctx); + } + return value; + } + + @Override + protected double getMaxSpeed() { + return maxSpeed; + } + + @Override + protected double getMaxPriority() { + return maxPriority; + } + + Polygon resolveAreaByName(String identifier) { + Polygon polygon = areaPolygons.get(identifier); + if (polygon == null) + throw new IllegalArgumentException("Area '" + identifier + "' wasn't found"); + return polygon; + } + + VariableAccessor resolveVariableAccessor(String name) { + return variableAccessors.get(name); + } + + EnumLookup resolveEnumLookup(String type) { + return enumLookups.get(type); + } + + private static List compileRuleBlocks(List statements, String info, + EncodedValueLookup lookup, Set variables) { + if (statements.isEmpty()) + return Collections.emptyList(); + List blocks = new ArrayList<>(); + RuleBlock current = null; + NameValidator nameValidator = name -> lookup.hasEncodedValue(name) + || name.toUpperCase(Locale.ROOT).equals(name) + || name.startsWith(IN_AREA_PREFIX) + || name.startsWith(BACKWARD_PREFIX) && lookup.hasEncodedValue(name.substring(BACKWARD_PREFIX.length())); + for (Statement statement : statements) { + switch (statement.getKeyword()) { + case IF: + current = new RuleBlock(); + blocks.add(current); + current.addRule(compileRule(statement, info, lookup, variables, nameValidator, true)); + break; + case ELSEIF: + if (current == null) + throw new IllegalArgumentException("Every block must start with an if-statement"); + current.addRule(compileRule(statement, info, lookup, variables, nameValidator, true)); + break; + case ELSE: + if (current == null) + throw new IllegalArgumentException("Every block must start with an if-statement"); + current.addRule(compileRule(statement, info, lookup, variables, nameValidator, false)); + current = null; + break; + default: + throw new IllegalArgumentException("The statement must be either 'if', 'else_if' or 'else'"); + } + } + return blocks; + } + + private static CompiledRule compileRule(Statement statement, String info, EncodedValueLookup lookup, + Set variables, NameValidator nameValidator, boolean expectCondition) { + Java.Rvalue conditionAst = null; + String conditionSource = null; + if (expectCondition) { + ParseResult parseResult = ConditionalExpressionVisitor.parse(statement.getCondition(), nameValidator); + if (!parseResult.ok) + throw new IllegalArgumentException(info + " invalid condition \"" + statement.getCondition() + "\"" + + (parseResult.invalidMessage == null ? "" : ": " + parseResult.invalidMessage)); + variables.addAll(parseResult.guessedVariables); + conditionSource = parseResult.converted == null ? statement.getCondition() : parseResult.converted.toString(); + conditionAst = parseBooleanExpression(conditionSource); + } else if (!Helper.isEmpty(statement.getCondition())) { + throw new IllegalArgumentException("condition must be empty but was " + statement.getCondition()); + } + + Java.Rvalue valueAst = compileValueExpression(statement.getValue(), lookup, variables, info); + return new CompiledRule(conditionSource, conditionAst, statement.getOperation(), statement.getValue(), valueAst); + } + + private static Java.Rvalue compileValueExpression(String expression, EncodedValueLookup lookup, + Set variables, String info) { + ParseResult parseResult = ValueExpressionVisitor.parse(expression, lookup::hasEncodedValue); + if (!parseResult.ok) + throw new IllegalArgumentException("Cannot compile expression: " + + (parseResult.invalidMessage == null ? expression + " invalid" : parseResult.invalidMessage)); + String trimmed = expression.trim(); + if (isSimpleIdentifier(trimmed) && !lookup.hasEncodedValue(trimmed) && !trimmed.isEmpty()) { + throw new IllegalArgumentException("Cannot compile expression: '" + trimmed + "' not available"); + } + if (parseResult.guessedVariables != null) { + for (String variable : parseResult.guessedVariables) { + EncodedValue enc = lookup.getEncodedValue(variable, EncodedValue.class); + if (enc instanceof EnumEncodedValue) { + EnumEncodedValue enumEnc = (EnumEncodedValue) enc; + String typeName = enumEnc.getValues().getClass().getComponentType().getName(); + throw new IllegalArgumentException("Binary numeric promotion not possible on types \"double\" and \"" + typeName + "\""); + } + if (!(enc instanceof DecimalEncodedValue) && !(enc instanceof IntEncodedValue)) { + throw new IllegalArgumentException("Binary numeric promotion not possible on types \"double\" and \"" + enc.getClass().getName() + "\""); + } + } + variables.addAll(parseResult.guessedVariables); + } + return parseExpression(expression); + } + + private static Java.Rvalue parseBooleanExpression(String expression) { + return parseExpression(expression); + } + + private static Java.Rvalue parseExpression(String expression) { + try { + Parser parser = new Parser(new Scanner("custom-model", new StringReader(expression))); + Java.Rvalue result = parser.parseExpression(); + if (parser.peek().type != TokenType.END_OF_INPUT) + throw new IllegalArgumentException("Unexpected token in expression"); + return result; + } catch (Exception ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + + private static BuildResult buildVariableAccessors(Set variables, EncodedValueLookup lookup, + Map areaFeatures) { + Map accessors = new HashMap<>(); + Map enumLookups = new HashMap<>(); + Map polygons = new HashMap<>(); + Map encodedValuesByVariable = new HashMap<>(); + for (String variable : variables) { + if (variable.startsWith(IN_AREA_PREFIX)) { + if (!JsonFeature.isValidId(variable)) + throw new IllegalArgumentException("Area has invalid name: " + variable); + String id = variable.substring(IN_AREA_PREFIX.length()); + JsonFeature feature = areaFeatures.get(id); + if (feature == null) + throw new IllegalArgumentException("Area '" + id + "' wasn't found"); + if (feature.getGeometry() == null) + throw new IllegalArgumentException("Area '" + id + "' does not contain a geometry"); + if (!(feature.getGeometry() instanceof Polygonal)) + throw new IllegalArgumentException("Currently only type=Polygon is supported for areas but was " + + feature.getGeometry().getGeometryType()); + if (feature.getBBox() != null) + throw new IllegalArgumentException("Bounding box of area " + id + " must be empty"); + polygons.put(variable, new Polygon(new PreparedPolygon((Polygonal) feature.getGeometry()))); + continue; + } + + boolean inverted = false; + String baseName = variable; + if (variable.startsWith(BACKWARD_PREFIX)) { + inverted = true; + baseName = variable.substring(BACKWARD_PREFIX.length()); + } + if (!lookup.hasEncodedValue(baseName)) + throw new IllegalArgumentException("Variable not supported: " + variable); + EncodedValue enc = lookup.getEncodedValue(baseName, EncodedValue.class); + encodedValuesByVariable.put(variable, enc); + accessors.put(variable, createAccessor(enc, inverted, enumLookups)); + } + encodedValuesByVariable.forEach((name, enc) -> { + if (enc instanceof EnumEncodedValue) { + EnumEncodedValue enumEnc = (EnumEncodedValue) enc; + EnumLookup lookupEntry = enumLookups.computeIfAbsent(enumEnc.getEnumSimpleName(), key -> new EnumLookup(enumEnc.getValues())); + boolean inverted = name.startsWith(BACKWARD_PREFIX); + if (inverted) { + String base = name.substring(BACKWARD_PREFIX.length()); + VariableAccessor accessor = accessors.get(name); + if (!(accessor instanceof EnumAccessor)) + accessors.put(name, new EnumAccessor(enumEnc, true, lookupEntry)); + accessor = accessors.get(base); + if (accessor instanceof IntAccessor) + accessors.put(base, new EnumAccessor(enumEnc, false, lookupEntry)); + } else { + VariableAccessor accessor = accessors.get(name); + if (!(accessor instanceof EnumAccessor)) + accessors.put(name, new EnumAccessor(enumEnc, false, lookupEntry)); + String backwardName = BACKWARD_PREFIX + name; + VariableAccessor backwardAccessor = accessors.get(backwardName); + if (backwardAccessor instanceof IntAccessor) + accessors.put(backwardName, new EnumAccessor(enumEnc, true, lookupEntry)); + } + } + }); + return new BuildResult(accessors, enumLookups, polygons); + } + + static void validateModel(CustomModel customModel, EncodedValueLookup lookup) { + validateValueExpressions(customModel.getPriority(), lookup); + validateValueExpressions(customModel.getSpeed(), lookup); + } + + private static void validateValueExpressions(List statements, EncodedValueLookup lookup) { + for (Statement statement : statements) { + String value = statement.getValue(); + if (Helper.isEmpty(value)) + continue; + String trimmed = value.trim(); + if (trimmed.isEmpty()) + continue; + boolean simpleIdentifier = isSimpleIdentifier(trimmed); + boolean knownIdentifier = lookup.hasEncodedValue(trimmed); + ParseResult parsed = ValueExpressionVisitor.parse(trimmed, lookup::hasEncodedValue); + if (simpleIdentifier && !knownIdentifier) { + throw new IllegalArgumentException("Cannot compile expression: '" + trimmed + "' not available"); + } + if (!parsed.ok) + throw new IllegalArgumentException("Cannot compile expression: " + (parsed.invalidMessage == null ? trimmed + " invalid" : parsed.invalidMessage)); + } + } + + private static boolean isSimpleIdentifier(String expression) { + if (Helper.isEmpty(expression)) + return false; + char first = expression.charAt(0); + if (!Character.isJavaIdentifierStart(first)) + return false; + for (int i = 1; i < expression.length(); i++) { + if (!Character.isJavaIdentifierPart(expression.charAt(i))) + return false; + } + return true; + } + + private static VariableAccessor createAccessor(EncodedValue enc, boolean inverted, + Map enumLookups) { + if (enc instanceof DecimalEncodedValue) + return new DecimalAccessor((DecimalEncodedValue) enc, inverted); + if (enc instanceof BooleanEncodedValue) + return new BooleanAccessor((BooleanEncodedValue) enc, inverted); + if (enc instanceof IntEncodedValue) + return new IntAccessor((IntEncodedValue) enc, inverted); + if (enc instanceof EnumEncodedValue) { + EnumEncodedValue enumEnc = (EnumEncodedValue) enc; + EnumLookup lookup = enumLookups.computeIfAbsent(enumEnc.getEnumSimpleName(), key -> new EnumLookup(enumEnc.getValues())); + return new EnumAccessor(enumEnc, inverted, lookup); + } + if (enc instanceof StringEncodedValue) + return new StringAccessor((StringEncodedValue) enc, inverted); + throw new IllegalArgumentException("Unsupported EncodedValue: " + enc.getClass()); + } + + private static final class BuildResult { + final Map variableAccessors; + final Map enumLookups; + final Map areaPolygons; + + BuildResult(Map variableAccessors, Map enumLookups, + Map areaPolygons) { + this.variableAccessors = variableAccessors; + this.enumLookups = enumLookups; + this.areaPolygons = areaPolygons; + } + } + + private static final class EvaluationContext { + final InterpretedCustomWeightingHelper helper; + final EdgeIteratorState edge; + final boolean reverse; + + EvaluationContext(InterpretedCustomWeightingHelper helper, EdgeIteratorState edge, boolean reverse) { + this.helper = helper; + this.edge = edge; + this.reverse = reverse; + } + } + + private static final class RuleBlock { + private final List rules = new ArrayList<>(); + + void addRule(CompiledRule rule) { + rules.add(rule); + } + + double apply(double value, EvaluationContext ctx) { + for (CompiledRule rule : rules) { + if (rule.matches(ctx)) + return rule.apply(value, ctx); + } + return value; + } + } + + private static final class CompiledRule { + private final String conditionSource; + private final Java.Rvalue condition; + private final Statement.Op operation; + private final String valueSource; + private final Java.Rvalue valueExpression; + + CompiledRule(String conditionSource, Java.Rvalue condition, Statement.Op operation, + String valueSource, Java.Rvalue valueExpression) { + this.conditionSource = conditionSource; + this.condition = condition; + this.operation = operation; + this.valueSource = valueSource; + this.valueExpression = valueExpression; + } + + boolean matches(EvaluationContext ctx) { + if (condition == null) + return true; + return ExpressionEvaluator.evaluateBoolean(condition, ctx); + } + + double apply(double currentValue, EvaluationContext ctx) { + double operand = ExpressionEvaluator.evaluateDouble(valueExpression, ctx, valueSource); + if (operation == Statement.Op.MULTIPLY) + return currentValue * operand; + if (operation == Statement.Op.LIMIT) + return Math.min(currentValue, operand); + throw new IllegalArgumentException("Unsupported operation: " + operation); + } + + @Override + public String toString() { + return "Rule{" + + "condition='" + conditionSource + '\'' + + ", operation=" + operation + + ", value='" + valueSource + '\'' + + '}'; + } + } + + private abstract static class VariableAccessor { + private final boolean inverted; + + VariableAccessor(boolean inverted) { + this.inverted = inverted; + } + + final boolean useReverse(EvaluationContext ctx) { + return ctx.reverse ^ inverted; + } + + abstract Object get(EvaluationContext ctx); + } + + private static final class DecimalAccessor extends VariableAccessor { + private final DecimalEncodedValue enc; + + DecimalAccessor(DecimalEncodedValue enc, boolean inverted) { + super(inverted); + this.enc = enc; + } + + @Override + Object get(EvaluationContext ctx) { + return useReverse(ctx) ? ctx.edge.getReverse(enc) : ctx.edge.get(enc); + } + } + + private static final class BooleanAccessor extends VariableAccessor { + private final BooleanEncodedValue enc; + + BooleanAccessor(BooleanEncodedValue enc, boolean inverted) { + super(inverted); + this.enc = enc; + } + + @Override + Object get(EvaluationContext ctx) { + return useReverse(ctx) ? ctx.edge.getReverse(enc) : ctx.edge.get(enc); + } + } + + private static final class IntAccessor extends VariableAccessor { + private final IntEncodedValue enc; + + IntAccessor(IntEncodedValue enc, boolean inverted) { + super(inverted); + this.enc = enc; + } + + @Override + Object get(EvaluationContext ctx) { + return useReverse(ctx) ? ctx.edge.getReverse(enc) : ctx.edge.get(enc); + } + } + + private static final class EnumAccessor extends VariableAccessor { + private final EnumEncodedValue enc; + private final EnumLookup lookup; + + EnumAccessor(EnumEncodedValue enc, boolean inverted, EnumLookup lookup) { + super(inverted); + this.enc = enc; + this.lookup = lookup; + } + + @Override + Object get(EvaluationContext ctx) { + Object result = useReverse(ctx) ? ctx.edge.getReverse(enc) : ctx.edge.get(enc); + if (result instanceof Number) + return lookup.constantByOrdinal(((Number) result).intValue()); + return result; + } + } + + private static final class StringAccessor extends VariableAccessor { + private final StringEncodedValue enc; + + StringAccessor(StringEncodedValue enc, boolean inverted) { + super(inverted); + this.enc = enc; + } + + @Override + Object get(EvaluationContext ctx) { + return useReverse(ctx) ? ctx.edge.getReverse(enc) : ctx.edge.get(enc); + } + } + + private static final class EnumLookup { + private final Map> valuesByName; + private final Enum[] valuesByOrdinal; + + EnumLookup(Enum[] constants) { + valuesByName = new LinkedHashMap<>(constants.length); + valuesByOrdinal = constants; + for (Enum enumConstant : constants) { + valuesByName.put(enumConstant.name(), enumConstant); + } + } + + Enum constant(String name) { + Enum enumConstant = valuesByName.get(name); + if (enumConstant == null) + throw new IllegalArgumentException("Enum constant '" + name + "' not available"); + return enumConstant; + } + + Enum constantByOrdinal(int ordinal) { + if (ordinal < 0 || ordinal >= valuesByOrdinal.length) + throw new IllegalArgumentException("Enum ordinal " + ordinal + " out of range"); + return valuesByOrdinal[ordinal]; + } + } + + private static final class ExpressionEvaluator { + static boolean evaluateBoolean(Java.Rvalue expression, EvaluationContext ctx) { + Object value = evaluate(expression, ctx); + if (value instanceof Boolean) + return (Boolean) value; + if (value instanceof Number) + return ((Number) value).doubleValue() != 0d; + if (value instanceof String) { + String str = ((String) value).trim(); + if (str.equalsIgnoreCase("true")) return true; + if (str.equalsIgnoreCase("false")) return false; + try { + return Double.parseDouble(str) != 0d; + } catch (NumberFormatException ignored) { + } + } + throw new IllegalArgumentException("Expression did not evaluate to a boolean (" + + (value == null ? "null" : value.getClass().getSimpleName()) + ")"); + } + + static double evaluateDouble(Java.Rvalue expression, EvaluationContext ctx, String valueSource) { + Object value = evaluate(expression, ctx); + if (value instanceof Number) + return ((Number) value).doubleValue(); + if (value instanceof String) { + try { + return Double.parseDouble(((String) value).trim()); + } catch (NumberFormatException ignored) { + } + } + throw new IllegalArgumentException("Expression '" + valueSource + "' did not evaluate to a number (" + + (value == null ? "null" : value.getClass().getSimpleName()) + ")"); + } + + private static Object evaluate(Java.Atom expression, EvaluationContext ctx) { + if (expression instanceof Java.ParenthesizedExpression) + return evaluate(((Java.ParenthesizedExpression) expression).value, ctx); + if (expression instanceof Java.AmbiguousName) + return evaluateAmbiguousName((Java.AmbiguousName) expression, ctx); + if (expression instanceof Java.BooleanLiteral) { + Object boolVal = ((Java.BooleanLiteral) expression).value; + if (boolVal instanceof String) + return Boolean.parseBoolean(((String) boolVal).trim()); + return boolVal; + } + if (expression instanceof Java.Literal) { + Object literalValue = ((Java.Literal) expression).value; + if (literalValue instanceof String) { + String literalString = ((String) literalValue).trim(); + if ("true".equalsIgnoreCase(literalString)) + return Boolean.TRUE; + if ("false".equalsIgnoreCase(literalString)) + return Boolean.FALSE; + } + return literalValue; + } + if (expression instanceof Java.NullLiteral) + return null; + if (expression instanceof Java.UnaryOperation) + return evaluateUnary((Java.UnaryOperation) expression, ctx); + if (expression instanceof Java.BinaryOperation) + return evaluateBinary((Java.BinaryOperation) expression, ctx); + if (expression instanceof Java.MethodInvocation) + return evaluateMethodInvocation((Java.MethodInvocation) expression, ctx); + if (expression instanceof Java.FieldAccess) + return evaluateFieldAccess((Java.FieldAccess) expression, ctx); + if (expression instanceof Java.FieldAccessExpression) + return evaluateFieldAccessExpression((Java.FieldAccessExpression) expression, ctx); + if (expression instanceof Java.ThisReference) + return ctx.helper; + throw new IllegalArgumentException("Unsupported expression " + expression.getClass().getSimpleName()); + } + + private static Object evaluateAmbiguousName(Java.AmbiguousName name, EvaluationContext ctx) { + String[] identifiers = name.identifiers; + if (identifiers.length == 1) { + String id = identifiers[0]; + if ("this".equals(id)) + return ctx.helper; + if ("edge".equals(id)) + return ctx.edge; + if ("Math".equals(id)) + return Math.class; + if ("CustomWeightingHelper".equals(id)) + return CustomWeightingHelper.class; + VariableAccessor accessor = ctx.helper.resolveVariableAccessor(id); + if (accessor != null) + return accessor.get(ctx); + throw new IllegalArgumentException("Unknown identifier '" + id + "'"); + } + if (identifiers.length == 2) { + if ("CustomWeightingHelper".equals(identifiers[0])) + return CustomWeightingHelper.class; + if ("this".equals(identifiers[0])) + return ctx.helper.resolveAreaByName(identifiers[1]); + EnumLookup lookup = ctx.helper.resolveEnumLookup(identifiers[0]); + if (lookup != null) + return lookup.constant(identifiers[1]); + VariableAccessor nestedAccessor = ctx.helper.resolveVariableAccessor(identifiers[0]); + if (nestedAccessor != null && "ordinal".equals(identifiers[1])) + return nestedAccessor.get(ctx); + try { + Class enumClass = Class.forName("com.graphhopper.routing.ev." + identifiers[0]); + if (Enum.class.isAssignableFrom(enumClass)) { + @SuppressWarnings("unchecked") + Enum enumConstant = Enum.valueOf((Class) enumClass, identifiers[1]); + return enumConstant; + } + } catch (ClassNotFoundException ignored) { + } + if ("Math".equals(identifiers[0])) { + if ("PI".equals(identifiers[1])) + return Math.PI; + if ("E".equals(identifiers[1])) + return Math.E; + } + } + throw new IllegalArgumentException("Unknown identifier '" + name + "'"); + } + + private static Object evaluateUnary(Java.UnaryOperation unary, EvaluationContext ctx) { + Object value = evaluate(unary.operand, ctx); + switch (unary.operator) { + case "!": + return !toBoolean(value); + case "-": + return -toDouble(value); + case "+": + return toDouble(value); + default: + throw new IllegalArgumentException("Operator '" + unary.operator + "' not supported"); + } + } + + private static Object evaluateBinary(Java.BinaryOperation binary, EvaluationContext ctx) { + String op = binary.operator; + switch (op) { + case "&&": + return evaluateBoolean(binary.lhs, ctx) && evaluateBoolean(binary.rhs, ctx); + case "||": + return evaluateBoolean(binary.lhs, ctx) || evaluateBoolean(binary.rhs, ctx); + case "==": + Object leftEq = evaluate(binary.lhs, ctx); + Object rightEq = evaluate(binary.rhs, ctx); + return Objects.equals(leftEq, rightEq); + case "!=": + return !Objects.equals(evaluate(binary.lhs, ctx), evaluate(binary.rhs, ctx)); + case "<": + return toDouble(evaluate(binary.lhs, ctx)) < toDouble(evaluate(binary.rhs, ctx)); + case "<=": + return toDouble(evaluate(binary.lhs, ctx)) <= toDouble(evaluate(binary.rhs, ctx)); + case ">": + return toDouble(evaluate(binary.lhs, ctx)) > toDouble(evaluate(binary.rhs, ctx)); + case ">=": + return toDouble(evaluate(binary.lhs, ctx)) >= toDouble(evaluate(binary.rhs, ctx)); + case "+": { + Object left = evaluate(binary.lhs, ctx); + Object right = evaluate(binary.rhs, ctx); + if (left instanceof String || right instanceof String) + return String.valueOf(left) + right; + return toDouble(left) + toDouble(right); + } + case "-": + return toDouble(evaluate(binary.lhs, ctx)) - toDouble(evaluate(binary.rhs, ctx)); + case "*": + return toDouble(evaluate(binary.lhs, ctx)) * toDouble(evaluate(binary.rhs, ctx)); + default: + throw new IllegalArgumentException("Operator '" + op + "' not supported"); + } + } + + private static Object evaluateMethodInvocation(Java.MethodInvocation invocation, EvaluationContext ctx) { + if (invocation.target == null) + throw new IllegalArgumentException("Method invocation without target unsupported"); + Object target = evaluate(invocation.target, ctx); + List args = new ArrayList<>(invocation.arguments.length); + for (Java.Rvalue argument : invocation.arguments) { + args.add(evaluate(argument, ctx)); + } + String method = invocation.methodName; + if (target == Math.class) { + if ("sqrt".equals(method)) + return Math.sqrt(toDouble(args.get(0))); + if ("abs".equals(method)) + return Math.abs(toDouble(args.get(0))); + } else if (target == CustomWeightingHelper.class) { + if ("in".equals(method)) { + if (invocation.arguments.length != 2) + throw new IllegalArgumentException("CustomWeightingHelper.in expects two arguments"); + Polygon area = (Polygon) args.get(0); + return CustomWeightingHelper.in(area, ctx.edge); + } + } else if (target instanceof EdgeIteratorState) { + EdgeIteratorState edge = (EdgeIteratorState) target; + if ("getDistance".equals(method)) + return edge.getDistance(); + if ("getName".equals(method)) + return edge.getName(); + } else if (target instanceof Enum) { + if ("ordinal".equals(method)) + return ((Enum) target).ordinal(); + } else if (target instanceof CharSequence) { + if ("contains".equals(method)) + return target.toString().contains(String.valueOf(args.get(0))); + } + throw new IllegalArgumentException("Method " + method + " not supported in custom model expressions"); + } + + private static Object evaluateFieldAccess(Java.FieldAccess access, EvaluationContext ctx) { + String representation = access.toString(); + if (representation.startsWith("this.")) + return ctx.helper.resolveAreaByName(representation.substring("this.".length())); + throw new IllegalArgumentException("Unsupported field access '" + representation + "'"); + } + + private static Object evaluateFieldAccessExpression(Java.FieldAccessExpression access, EvaluationContext ctx) { + String representation = access.toString(); + if (representation.startsWith("this.")) + return ctx.helper.resolveAreaByName(representation.substring("this.".length())); + throw new IllegalArgumentException("Unsupported field access '" + representation + "'"); + } + + private static boolean toBoolean(Object value) { + if (value instanceof Boolean) + return (Boolean) value; + if (value instanceof Number) + return ((Number) value).doubleValue() != 0d; + if (value instanceof String) { + String str = ((String) value).trim(); + if (str.equalsIgnoreCase("true")) return true; + if (str.equalsIgnoreCase("false")) return false; + try { + return Double.parseDouble(str) != 0d; + } catch (NumberFormatException ignored) { + } + } + throw new IllegalArgumentException("Cannot convert " + value + " to boolean"); + } + + private static double toDouble(Object value) { + if (value instanceof Number) + return ((Number) value).doubleValue(); + if (value instanceof String) { + try { + return Double.parseDouble(((String) value).trim()); + } catch (NumberFormatException ignored) { + } + } + throw new IllegalArgumentException("Cannot convert " + value + " to number"); + } + } +}