package com.onthegomap.planetiler.custommap.expression; import com.onthegomap.planetiler.custommap.TypeConversion; import com.onthegomap.planetiler.expression.DataType; import com.onthegomap.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.expression.Simplifiable; import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.stream.Stream; /** * A function defined in part of a schema config that produces an output value (min zoom, attribute value, etc.) for a * feature at runtime. *

* This can be parsed from a structured object that lists combinations of tag key/values, an embedded script, or a * combination of the two. * * @param Type of the input context that expressions can pull values from at runtime. * @param Output type */ public interface ConfigExpression extends Function, Simplifiable> { static ConfigExpression script(Signature signature, String script) { return ConfigExpressionScript.parse(script, signature.in(), signature.out()); } static ConfigExpression variable(Signature signature, String text) { return new Variable<>(signature, text); } static ConfigExpression constOf(O value) { return new Const<>(value); } static ConfigExpression coalesce( List> values) { return new Coalesce<>(values); } static ConfigExpression getTag(Signature signature, ConfigExpression tag) { return new GetTag<>(signature, tag); } static ConfigExpression getArg(Signature signature, ConfigExpression tag) { return new GetArg<>(signature, tag); } static ConfigExpression cast(Signature signature, ConfigExpression input, DataType dataType) { return new Cast<>(signature, input, dataType); } static Match match(Signature description, MultiExpression> multiExpression) { return new Match<>(description, multiExpression, constOf(null)); } static Match match(Signature description, MultiExpression> multiExpression, ConfigExpression fallback) { return new Match<>(description, multiExpression, fallback); } static Signature signature(ScriptEnvironment in, Class out) { return new Signature<>(in, out); } /** An expression that always returns {@code value}. */ record Const (O value) implements ConfigExpression { @Override public O apply(I i) { return value; } } /** An expression that returns the value associated with the first matching boolean expression. */ record Match ( Signature signature, MultiExpression> multiExpression, ConfigExpression fallback, MultiExpression.Index> indexed ) implements ConfigExpression { public Match( Signature signature, MultiExpression> multiExpression, ConfigExpression fallback ) { this(signature, multiExpression, fallback, multiExpression.index()); } @Override public boolean equals(Object o) { // ignore the indexed expression return this == o || (o instanceof Match match && Objects.equals(signature, match.signature) && Objects.equals(multiExpression, match.multiExpression) && Objects.equals(fallback, match.fallback)); } @Override public int hashCode() { // ignore the indexed expression return Objects.hash(signature, multiExpression, fallback); } @Override public O apply(I i) { var resultFunction = indexed.getOrElse(i, fallback); return resultFunction == null ? null : resultFunction.apply(i); } @Override public ConfigExpression simplifyOnce() { var newMultiExpression = multiExpression .mapResults(Simplifiable::simplifyOnce) .simplify(); var newFallback = fallback.simplifyOnce(); if (newMultiExpression.expressions().isEmpty()) { return newFallback; } var expressions = newMultiExpression.expressions(); for (int i = 0; i < expressions.size(); i++) { var expression = expressions.get(i); // if one of the cases is always true, then ignore the cases after it and make this value the fallback if (Expression.TRUE.equals(expression.expression())) { return new Match<>( signature, MultiExpression.of(expressions.stream().limit(i).toList()), expression.result() ); } } return new Match<>(signature, newMultiExpression, newFallback); } public Match withDefaultValue(ConfigExpression newFallback) { return new Match<>(signature, multiExpression, newFallback); } } /** An expression that returns the first non-null result of evaluating each child expression. */ record Coalesce (List> children) implements ConfigExpression { @Override public O apply(I i) { for (var condition : children) { var result = condition.apply(i); if (result != null) { return result; } } return null; } @Override public ConfigExpression simplifyOnce() { return switch (children.size()) { case 0 -> constOf(null); case 1 -> children.get(0); default -> { var result = children.stream() .flatMap( child -> child instanceof Coalesce childCoalesce ? childCoalesce.children.stream() : Stream.of(child)) .filter(child -> !child.equals(constOf(null))) .distinct() .toList(); var indexOfFirstConst = result.stream().takeWhile(d -> !(d instanceof ConfigExpression.Const)).count(); yield coalesce(result.stream().map(d -> { @SuppressWarnings("unchecked") ConfigExpression casted = (ConfigExpression) d; return casted; }).limit(indexOfFirstConst + 1).toList()); } }; } } /** An expression that returns the value associated a given variable name at runtime. */ record Variable ( Signature signature, String name ) implements ConfigExpression { public Variable { if (!signature.in.containsVariable(name)) { throw new ParseException("Variable not available: " + name); } } @Override public O apply(I i) { return TypeConversion.convert(i.apply(name), signature.out); } } /** An expression that returns the value associated a given tag of the input feature at runtime. */ record GetTag ( Signature signature, ConfigExpression tag ) implements ConfigExpression { @Override public O apply(I i) { return TypeConversion.convert(i.tagValueProducer().valueForKey(i, tag.apply(i)), signature.out); } @Override public ConfigExpression simplifyOnce() { return new GetTag<>(signature, tag.simplifyOnce()); } } /** An expression that returns the value associated a given argument at runtime. */ record GetArg ( Signature signature, ConfigExpression arg ) implements ConfigExpression { @Override public O apply(I i) { return TypeConversion.convert(i.argument(arg.apply(i)), signature.out); } @Override public ConfigExpression simplifyOnce() { var key = arg.simplifyOnce(); if (key instanceof ConfigExpression.Const constKey) { var rawResult = signature.in.root().argument(constKey.value); return constOf(TypeConversion.convert(rawResult, signature.out)); } else { return new GetArg<>(signature, key); } } } /** An expression that converts the input to a desired output {@link DataType} at runtime. */ record Cast ( Signature signature, ConfigExpression input, DataType output ) implements ConfigExpression { @Override public O apply(I i) { return TypeConversion.convert(output.convertFrom(input.apply(i)), signature.out); } @Override public ConfigExpression simplifyOnce() { var in = input.simplifyOnce(); if (in instanceof ConfigExpression.Const inConst) { return constOf(TypeConversion.convert(output.convertFrom(inConst.value), signature.out)); } else if (in instanceof ConfigExpression.Cast cast && cast.output == output) { @SuppressWarnings("unchecked") ConfigExpression newIn = (ConfigExpression) cast.input; return cast(signature, newIn, output); } else { return new Cast<>(signature, input.simplifyOnce(), output); } } } record Signature (ScriptEnvironment in, Class out) { public Signature withOutput(Class newOut) { return new Signature<>(in, newOut); } } }