kopia lustrzana https://github.com/onthegomap/planetiler
Allow dynamic expressions on left hand side of match statements (#1063)
rodzic
be7cec36db
commit
e3d5645e1a
|
@ -2,13 +2,12 @@ package com.onthegomap.planetiler.expression;
|
|||
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Destination data types for an attribute that link the type to functions that can parse the value from an input object
|
||||
*/
|
||||
public enum DataType implements BiFunction<WithTags, String, Object> {
|
||||
public enum DataType implements TypedGetter {
|
||||
GET_STRING("string", WithTags::getString, Parse::parseStringOrNull),
|
||||
GET_BOOLEAN("boolean", WithTags::getBoolean, Parse::bool),
|
||||
GET_DIRECTION("direction", WithTags::getDirection, Parse::direction),
|
||||
|
@ -17,11 +16,11 @@ public enum DataType implements BiFunction<WithTags, String, Object> {
|
|||
GET_DOUBLE("double", Parse::parseDoubleOrNull),
|
||||
GET_TAG("get", WithTags::getTag, s -> s);
|
||||
|
||||
private final BiFunction<WithTags, String, Object> getter;
|
||||
private final TypedGetter getter;
|
||||
private final String id;
|
||||
private final UnaryOperator<Object> parser;
|
||||
|
||||
DataType(String id, BiFunction<WithTags, String, Object> getter, UnaryOperator<Object> parser) {
|
||||
DataType(String id, TypedGetter getter, UnaryOperator<Object> parser) {
|
||||
this.id = id;
|
||||
this.getter = getter;
|
||||
this.parser = parser;
|
||||
|
|
|
@ -13,7 +13,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -91,7 +90,7 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
* <p>
|
||||
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
|
||||
*/
|
||||
static MatchAny matchAnyTyped(String field, BiFunction<WithTags, String, Object> typeGetter, Object... values) {
|
||||
static MatchAny matchAnyTyped(String field, TypedGetter typeGetter, Object... values) {
|
||||
return matchAnyTyped(field, typeGetter, List.of(values));
|
||||
}
|
||||
|
||||
|
@ -101,7 +100,7 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
* <p>
|
||||
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
|
||||
*/
|
||||
static MatchAny matchAnyTyped(String field, BiFunction<WithTags, String, Object> typeGetter,
|
||||
static MatchAny matchAnyTyped(String field, TypedGetter typeGetter,
|
||||
List<?> values) {
|
||||
return MatchAny.from(field, typeGetter, values);
|
||||
}
|
||||
|
@ -405,10 +404,10 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
String field, List<?> values, Set<String> exactMatches,
|
||||
Pattern pattern,
|
||||
boolean matchWhenMissing,
|
||||
BiFunction<WithTags, String, Object> valueGetter
|
||||
TypedGetter valueGetter
|
||||
) implements Expression {
|
||||
|
||||
static MatchAny from(String field, BiFunction<WithTags, String, Object> valueGetter, List<?> values) {
|
||||
static MatchAny from(String field, TypedGetter valueGetter, List<?> values) {
|
||||
List<String> exactMatches = new ArrayList<>();
|
||||
List<String> patterns = new ArrayList<>();
|
||||
|
||||
|
@ -474,6 +473,10 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
|
||||
@Override
|
||||
public Expression partialEvaluate(PartialInput input) {
|
||||
if (field == null) {
|
||||
// dynamic getters always need to be evaluated
|
||||
return this;
|
||||
}
|
||||
Object value = input.getTag(field);
|
||||
return value == null ? this : constBool(evaluate(new ArrayList<>(), value));
|
||||
}
|
||||
|
@ -496,7 +499,9 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
return false;
|
||||
} else {
|
||||
String str = value.toString();
|
||||
if (exactMatches.contains(str)) {
|
||||
// when field is null, we rely on a dynamic getter function so when exactMatches is empty we match
|
||||
// on any value
|
||||
if (exactMatches.contains(str) || (field == null && exactMatches.isEmpty())) {
|
||||
matchKeys.add(field);
|
||||
return true;
|
||||
}
|
||||
|
@ -510,7 +515,13 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
|
||||
@Override
|
||||
public Expression simplifyOnce() {
|
||||
return isMatchAnything() ? matchField(field) : this;
|
||||
if (isMatchAnything()) {
|
||||
return matchField(field);
|
||||
} else if (valueGetter instanceof Simplifiable<?> simplifiable) {
|
||||
return new MatchAny(field, values, exactMatches, pattern, matchWhenMissing,
|
||||
(TypedGetter) simplifiable.simplifyOnce());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -557,6 +568,11 @@ public interface Expression extends Simplifiable<Expression> {
|
|||
public int hashCode() {
|
||||
return Objects.hash(field, values, exactMatches, patternString(), matchWhenMissing, valueGetter);
|
||||
}
|
||||
|
||||
public boolean mustAlwaysEvaluate() {
|
||||
// when field is null we rely on a dynamic getter function
|
||||
return field == null || matchWhenMissing;
|
||||
}
|
||||
}
|
||||
|
||||
/** Evaluates to true if an input element contains any value for {@code field} tag. */
|
||||
|
|
|
@ -60,7 +60,7 @@ public record MultiExpression<T>(List<Entry<T>> expressions) implements Simplifi
|
|||
case Expression.Or(var children) -> children.stream().anyMatch(MultiExpression::mustAlwaysEvaluate);
|
||||
case Expression.And(var children) -> children.stream().allMatch(MultiExpression::mustAlwaysEvaluate);
|
||||
case Expression.Not(var child) -> !mustAlwaysEvaluate(child);
|
||||
case Expression.MatchAny any when any.matchWhenMissing() -> true;
|
||||
case Expression.MatchAny any when any.mustAlwaysEvaluate() -> true;
|
||||
case null, default -> !(expression instanceof Expression.MatchAny) &&
|
||||
!(expression instanceof Expression.MatchField) &&
|
||||
!FALSE.equals(expression);
|
||||
|
@ -79,7 +79,7 @@ public record MultiExpression<T>(List<Entry<T>> expressions) implements Simplifi
|
|||
or.children().forEach(child -> getRelevantKeys(child, acceptKey));
|
||||
} else if (exp instanceof Expression.MatchField field) {
|
||||
acceptKey.accept(field.field());
|
||||
} else if (exp instanceof Expression.MatchAny any && !any.matchWhenMissing()) {
|
||||
} else if (exp instanceof Expression.MatchAny any && !any.mustAlwaysEvaluate()) {
|
||||
acceptKey.accept(any.field());
|
||||
}
|
||||
// ignore not case since not(matchAny("field", "")) should track "field" as a relevant key, but that gets
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.onthegomap.planetiler.expression;
|
||||
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TypedGetter {
|
||||
Object apply(WithTags withTags, String tag);
|
||||
}
|
|
@ -350,6 +350,19 @@ class ExpressionTest {
|
|||
new PartialInput(Set.of(), Set.of(), Map.of("field", "not a value"), Set.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialEvaluateMatchAnyDynamicGetter() {
|
||||
var expr = matchAnyTyped(null, ((withTags, tag) -> ""), 1, 2, 3);
|
||||
assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("other", "value"), Set.of())));
|
||||
assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), null, Set.of())));
|
||||
assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "value1"), Set.of())));
|
||||
assertEquals(expr, expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "other"), Set.of())));
|
||||
assertEquals(expr,
|
||||
expr.partialEvaluate(new PartialInput(Set.of(), Set.of(), Map.of("field", "other..."), Set.of())));
|
||||
assertEquals(expr, expr.partialEvaluate(
|
||||
new PartialInput(Set.of(), Set.of(), Map.of("field", "not a value"), Set.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialEvaluateMatchGeometryType() {
|
||||
var expr = matchGeometryType(GeometryType.POINT);
|
||||
|
|
|
@ -218,6 +218,7 @@ A feature is a defined set of objects that meet a specified filter criteria.
|
|||
- `geometry` - A string enum that indicates which geometry types to include, and how to transform them. Can be one
|
||||
of:
|
||||
- `point` `line` or `polygon` to pass the original feature through
|
||||
- `any` (default) to pass the original feature through regardless of geometry type
|
||||
- `polygon_centroid` to match on polygons, and emit a point at the center
|
||||
- `line_centroid` to match on lines, and emit a point at the center
|
||||
- `centroid` to match any geometry, and emit a point at the center
|
||||
|
|
|
@ -342,14 +342,12 @@
|
|||
},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"geometry"
|
||||
],
|
||||
"properties": {
|
||||
"geometry": {
|
||||
"description": "Include objects of a certain geometry type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"any",
|
||||
"point",
|
||||
"line",
|
||||
"polygon",
|
||||
|
|
|
@ -110,25 +110,27 @@ public class BooleanExpressionParser<T extends ScriptContext> {
|
|||
} else if (IS_NOT.test(key)) {
|
||||
// __not__ negates its children
|
||||
return not(parse(value));
|
||||
} else if (value == null || IS_ANY.test(value.toString()) ||
|
||||
(value instanceof Collection<?> values &&
|
||||
values.stream().anyMatch(d -> d != null && IS_ANY.test(d.toString().trim())))) {
|
||||
//If only a key is provided, with no value, match any object tagged with that key.
|
||||
return matchField(unescape(key));
|
||||
|
||||
} else if (value instanceof Collection<?> values) {
|
||||
//If a collection is provided, match any of these values.
|
||||
return matchAnyTyped(
|
||||
unescape(key),
|
||||
tagValueProducer.valueGetterForKey(key),
|
||||
values.stream().map(BooleanExpressionParser::unescape).toList());
|
||||
|
||||
} else {
|
||||
//Otherwise, a key and single value were passed, so match that exact tag
|
||||
return matchAnyTyped(
|
||||
unescape(key),
|
||||
tagValueProducer.valueGetterForKey(key),
|
||||
unescape(value));
|
||||
//If only a key is provided, with no value, match any object tagged with that key.
|
||||
boolean isAny = value == null || IS_ANY.test(value.toString()) ||
|
||||
(value instanceof Collection<?> values &&
|
||||
values.stream().anyMatch(d -> d != null && IS_ANY.test(d.toString().trim())));
|
||||
//If a collection or single item are provided, match any of these values.
|
||||
List<?> values = (value instanceof Collection<?> items ? items : value == null ? List.of() : List.of(value))
|
||||
.stream().map(BooleanExpressionParser::unescape).toList();
|
||||
if (ConfigExpressionScript.isScript(key)) {
|
||||
var expression = ConfigExpressionScript.parse(ConfigExpressionScript.extractScript(key), context);
|
||||
if (isAny) {
|
||||
values = List.of();
|
||||
}
|
||||
return matchAnyTyped(null, expression, values);
|
||||
}
|
||||
String field = unescape(key);
|
||||
if (isAny) {
|
||||
return matchField(field);
|
||||
} else {
|
||||
return matchAnyTyped(field, tagValueProducer.valueGetterForKey(key), values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ package com.onthegomap.planetiler.custommap;
|
|||
import static com.onthegomap.planetiler.expression.DataType.GET_TAG;
|
||||
|
||||
import com.onthegomap.planetiler.expression.DataType;
|
||||
import com.onthegomap.planetiler.expression.TypedGetter;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
|
@ -17,7 +17,7 @@ import java.util.function.UnaryOperator;
|
|||
public class TagValueProducer {
|
||||
public static final TagValueProducer EMPTY = new TagValueProducer(null);
|
||||
|
||||
private final Map<String, BiFunction<WithTags, String, Object>> valueRetriever = new HashMap<>();
|
||||
private final Map<String, TypedGetter> valueRetriever = new HashMap<>();
|
||||
|
||||
private final Map<String, String> keyType = new HashMap<>();
|
||||
|
||||
|
@ -50,7 +50,7 @@ public class TagValueProducer {
|
|||
/**
|
||||
* Returns a function that extracts the value for {@code key} from a {@link WithTags} instance.
|
||||
*/
|
||||
public BiFunction<WithTags, String, Object> valueGetterForKey(String key) {
|
||||
public TypedGetter valueGetterForKey(String key) {
|
||||
return valueRetriever.getOrDefault(key, GET_TAG);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.onthegomap.planetiler.custommap;
|
|||
import com.onthegomap.planetiler.util.Parse;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import org.projectnessie.cel.common.types.NullT;
|
||||
|
||||
/**
|
||||
* Utility for convert between types in a forgiving way (parse strings to get a number, call toString to get a string,
|
||||
|
@ -49,6 +50,9 @@ public class TypeConversion {
|
|||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <O> O convert(Object in, Class<O> out) {
|
||||
if (in == NullT.NullValue) {
|
||||
return null;
|
||||
}
|
||||
if (in == null || out.isInstance(in)) {
|
||||
return (O) in;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
|
@ -8,6 +9,8 @@ import java.util.function.BiFunction;
|
|||
import java.util.function.Function;
|
||||
|
||||
public enum FeatureGeometry {
|
||||
@JsonProperty("any") @JsonEnumDefaultValue
|
||||
ANY(GeometryType.UNKNOWN, FeatureCollector::anyGeometry),
|
||||
@JsonProperty("point")
|
||||
POINT(GeometryType.POINT, FeatureCollector::point),
|
||||
@JsonProperty("line")
|
||||
|
|
|
@ -10,7 +10,7 @@ public record FeatureItem(
|
|||
@JsonProperty("min_zoom") Object minZoom,
|
||||
@JsonProperty("max_zoom") Object maxZoom,
|
||||
@JsonProperty("min_size") Object minSize,
|
||||
@JsonProperty(required = true) FeatureGeometry geometry,
|
||||
@JsonProperty FeatureGeometry geometry,
|
||||
@JsonProperty("include_when") Object includeWhen,
|
||||
@JsonProperty("exclude_when") Object excludeWhen,
|
||||
Collection<AttributeDefinition> attributes
|
||||
|
@ -20,4 +20,9 @@ public record FeatureItem(
|
|||
public Collection<AttributeDefinition> attributes() {
|
||||
return attributes == null ? List.of() : attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeatureGeometry geometry() {
|
||||
return geometry == null ? FeatureGeometry.ANY : geometry;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ 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 com.onthegomap.planetiler.expression.TypedGetter;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
@ -21,7 +23,15 @@ import java.util.stream.Stream;
|
|||
* @param <O> Output type
|
||||
*/
|
||||
public interface ConfigExpression<I extends ScriptContext, O>
|
||||
extends Function<I, O>, Simplifiable<ConfigExpression<I, O>> {
|
||||
extends Function<I, O>, Simplifiable<ConfigExpression<I, O>>, TypedGetter {
|
||||
|
||||
ScriptEnvironment<I> environment();
|
||||
|
||||
@Override
|
||||
default Object apply(WithTags withTags, String tag) {
|
||||
Class<I> clazz = environment().clazz();
|
||||
return clazz.isInstance(withTags) ? apply(clazz.cast(withTags)) : null;
|
||||
}
|
||||
|
||||
static <I extends ScriptContext, O> ConfigExpression<I, O> script(Signature<I, O> signature, String script) {
|
||||
return ConfigExpressionScript.parse(script, signature.in(), signature.out());
|
||||
|
@ -32,12 +42,16 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
}
|
||||
|
||||
static <I extends ScriptContext, O> ConfigExpression<I, O> constOf(O value) {
|
||||
return new Const<>(value);
|
||||
return new Const<>(any(), value);
|
||||
}
|
||||
|
||||
static <I extends ScriptContext, O> ConfigExpression<I, O> coalesce(
|
||||
List<ConfigExpression<I, O>> values) {
|
||||
return new Coalesce<>(values);
|
||||
return new Coalesce<>(any(), values);
|
||||
}
|
||||
|
||||
static <I extends ScriptContext, O> Signature<I, O> any() {
|
||||
return new Signature<>(null, null);
|
||||
}
|
||||
|
||||
static <I extends ScriptContext, O> ConfigExpression<I, O> getTag(Signature<I, O> signature,
|
||||
|
@ -70,12 +84,18 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
}
|
||||
|
||||
/** An expression that always returns {@code value}. */
|
||||
record Const<I extends ScriptContext, O>(O value) implements ConfigExpression<I, O> {
|
||||
record Const<I extends ScriptContext, O>(Signature<I, O> signature, O value)
|
||||
implements ConfigExpression<I, O> {
|
||||
|
||||
@Override
|
||||
public O apply(I i) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that returns the value associated with the first matching boolean expression. */
|
||||
|
@ -143,10 +163,18 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
public Match<I, O> withDefaultValue(ConfigExpression<I, O> newFallback) {
|
||||
return new Match<>(signature, multiExpression, newFallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that returns the first non-null result of evaluating each child expression. */
|
||||
record Coalesce<I extends ScriptContext, O>(List<? extends ConfigExpression<I, O>> children)
|
||||
record Coalesce<I extends ScriptContext, O>(
|
||||
Signature<I, O> signature,
|
||||
List<? extends ConfigExpression<I, O>> children
|
||||
)
|
||||
implements ConfigExpression<I, O> {
|
||||
|
||||
@Override
|
||||
|
@ -181,6 +209,11 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that returns the value associated a given variable name at runtime. */
|
||||
|
@ -199,6 +232,11 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
public O apply(I i) {
|
||||
return TypeConversion.convert(i.apply(name), signature.out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that returns the value associated a given tag of the input feature at runtime. */
|
||||
|
@ -216,6 +254,11 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
public ConfigExpression<I, O> simplifyOnce() {
|
||||
return new GetTag<>(signature, tag.simplifyOnce());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that returns the value associated a given argument at runtime. */
|
||||
|
@ -239,6 +282,11 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
return new GetArg<>(signature, key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
/** An expression that converts the input to a desired output {@link DataType} at runtime. */
|
||||
|
@ -266,6 +314,11 @@ public interface ConfigExpression<I extends ScriptContext, O>
|
|||
return new Cast<>(signature, input.simplifyOnce(), output);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return signature.in;
|
||||
}
|
||||
}
|
||||
|
||||
record Signature<I extends ScriptContext, O>(ScriptEnvironment<I> in, Class<O> out) {
|
||||
|
|
|
@ -173,4 +173,9 @@ public class ConfigExpressionScript<I extends ScriptContext, O> implements Confi
|
|||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptEnvironment<I> environment() {
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
class ConfigExpressionParserTest {
|
||||
|
@ -42,11 +43,19 @@ class ConfigExpressionParserTest {
|
|||
assertParse(input, constOf(1d), Double.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVar() {
|
||||
assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Integer.class), "feature.id"), Integer.class);
|
||||
assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Long.class), "feature.id"), Long.class);
|
||||
assertParse("${feature.id}", variable(FEATURE_SIGNATURE.withOutput(Double.class), "feature.id"), Double.class);
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"feature.id",
|
||||
"feature.source",
|
||||
"feature.source_layer",
|
||||
"feature.osm_user_name"
|
||||
})
|
||||
void testVar(String var) {
|
||||
String script = "${" + var + "}";
|
||||
assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Integer.class), var), Integer.class);
|
||||
assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Long.class), var), Long.class);
|
||||
assertParse(script, variable(FEATURE_SIGNATURE.withOutput(Double.class), var), Double.class);
|
||||
assertParse(script, variable(FEATURE_SIGNATURE.withOutput(String.class), var), String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.onthegomap.planetiler.custommap;
|
|||
import static com.onthegomap.planetiler.TestUtils.newLineString;
|
||||
import static com.onthegomap.planetiler.TestUtils.newPoint;
|
||||
import static com.onthegomap.planetiler.TestUtils.newPolygon;
|
||||
import static com.onthegomap.planetiler.TestUtils.rectangle;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
@ -34,6 +35,8 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.locationtech.jts.geom.Lineal;
|
||||
import org.locationtech.jts.geom.Polygonal;
|
||||
import org.locationtech.jts.geom.Puntal;
|
||||
|
||||
class ConfiguredFeatureTest {
|
||||
|
@ -1288,6 +1291,36 @@ class ConfiguredFeatureTest {
|
|||
}, 1);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"geometry: any", ""})
|
||||
void testAnyGeometry(String expression) {
|
||||
var config = """
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
local_path: data/rhode-island.osm.pbf
|
||||
layers:
|
||||
- id: testLayer
|
||||
features:
|
||||
- source: osm
|
||||
%s
|
||||
""".formatted(expression).strip();
|
||||
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
|
||||
testLinestring(config, Map.of(
|
||||
), feature -> {
|
||||
assertInstanceOf(Lineal.class, feature.getGeometry());
|
||||
}, 1);
|
||||
testPoint(config, Map.of(
|
||||
), feature -> {
|
||||
assertInstanceOf(Puntal.class, feature.getGeometry());
|
||||
}, 1);
|
||||
testPolygon(config, Map.of(
|
||||
), feature -> {
|
||||
assertInstanceOf(Polygonal.class, feature.getGeometry());
|
||||
}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWikidataParse() {
|
||||
var config = """
|
||||
|
@ -1321,4 +1354,111 @@ class ConfiguredFeatureTest {
|
|||
assertEquals(Map.of("wikidata", 0L), feature.getAttrsAtZoom(14));
|
||||
}, 1);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"${feature.id}: 1",
|
||||
"${feature.id + 1}: 2",
|
||||
"${feature.id}: [1, 3]",
|
||||
"${feature.source_layer}: layer",
|
||||
"${ feature . source_layer }: [layer, layer2]",
|
||||
"${feature.osm_changeset}: 2",
|
||||
"${feature.osm_version}: 5",
|
||||
"${feature.osm_timestamp}: 3",
|
||||
"${feature.osm_user_id}: 4",
|
||||
"${feature.osm_user_name}: user",
|
||||
"${feature.osm_type}: way",
|
||||
}, delimiter = '\t')
|
||||
void testLeftHandSideExpression(String matchString) {
|
||||
var config = """
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
local_path: data/rhode-island.osm.pbf
|
||||
layers:
|
||||
- id: testLayer
|
||||
features:
|
||||
- source: osm
|
||||
include_when:
|
||||
%s
|
||||
""".formatted(matchString);
|
||||
var sfMatch =
|
||||
SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(),
|
||||
new OsmElement.Info(2, 3, 4, 5, "user"));
|
||||
var sfNoMatch =
|
||||
SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", "other layer", 2, emptyList(),
|
||||
new OsmElement.Info(6, 7, 8, 9, "other user"));
|
||||
testFeature(config, sfMatch, any -> {
|
||||
}, 1);
|
||||
testFeature(config, sfNoMatch, any -> {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"${feature.osm_user_name}: __any__",
|
||||
"${feature.osm_user_name}: null",
|
||||
"${feature.source_layer}: __any__",
|
||||
"${feature.source_layer}: null",
|
||||
}, delimiter = '\t')
|
||||
void testLeftHandSideExpressionMatchAny(String matchString) {
|
||||
var config = """
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
local_path: data/rhode-island.osm.pbf
|
||||
layers:
|
||||
- id: testLayer
|
||||
features:
|
||||
- source: osm
|
||||
include_when:
|
||||
%s
|
||||
""".formatted(matchString);
|
||||
var sfMatch =
|
||||
SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(),
|
||||
new OsmElement.Info(2, 3, 4, 5, "user"));
|
||||
var sfNoMatch =
|
||||
SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", null, 2, emptyList(),
|
||||
new OsmElement.Info(6, 7, 8, 9, ""));
|
||||
testFeature(config, sfMatch, any -> {
|
||||
}, 1);
|
||||
testFeature(config, sfNoMatch, any -> {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"${feature.osm_user_name}: ''",
|
||||
"${feature.osm_user_name}: ['']",
|
||||
"${feature.source_layer}: ''",
|
||||
"${feature.source_layer}: ['']",
|
||||
}, delimiter = '\t')
|
||||
void testLeftHandSideExpressionMatchNone(String matchString) {
|
||||
var config = """
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
local_path: data/rhode-island.osm.pbf
|
||||
layers:
|
||||
- id: testLayer
|
||||
features:
|
||||
- source: osm
|
||||
include_when:
|
||||
%s
|
||||
""".formatted(matchString);
|
||||
var sfMatch =
|
||||
SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(),
|
||||
new OsmElement.Info(2, 3, 4, 5, "user"));
|
||||
var sfNoMatch =
|
||||
SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(), "osm", null, 2, emptyList(),
|
||||
new OsmElement.Info(6, 7, 8, 9, ""));
|
||||
testFeature(config, sfMatch, any -> {
|
||||
}, 0);
|
||||
testFeature(config, sfNoMatch, any -> {
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,22 +6,25 @@ import static com.onthegomap.planetiler.custommap.TestContexts.PROCESS_FEATURE;
|
|||
import static com.onthegomap.planetiler.custommap.TestContexts.ROOT_CONTEXT;
|
||||
import static com.onthegomap.planetiler.custommap.expression.ConfigExpression.*;
|
||||
import static com.onthegomap.planetiler.expression.Expression.matchAny;
|
||||
import static com.onthegomap.planetiler.expression.Expression.matchAnyTyped;
|
||||
import static com.onthegomap.planetiler.expression.Expression.or;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.custommap.BooleanExpressionParser;
|
||||
import com.onthegomap.planetiler.custommap.Contexts;
|
||||
import com.onthegomap.planetiler.custommap.TagValueProducer;
|
||||
import com.onthegomap.planetiler.custommap.TestContexts;
|
||||
import com.onthegomap.planetiler.expression.DataType;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
class ConfigExpressionTest {
|
||||
private static final ConfigExpression.Signature<Contexts.Root, Integer> ROOT = signature(ROOT_CONTEXT, Integer.class);
|
||||
|
@ -104,8 +107,8 @@ class ConfigExpressionTest {
|
|||
FEATURE_SIGNATURE,
|
||||
MultiExpression.of(List.of(
|
||||
MultiExpression.entry(script(FEATURE_SIGNATURE, "1 + size(feature.tags.a)"),
|
||||
Expression.matchAny("a", "b")),
|
||||
MultiExpression.entry(constOf(1), Expression.matchAny("a", "c"))
|
||||
matchAny("a", "b")),
|
||||
MultiExpression.entry(constOf(1), matchAny("a", "c"))
|
||||
))
|
||||
).apply(context));
|
||||
}
|
||||
|
@ -133,6 +136,41 @@ class ConfigExpressionTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCantSimplifyDynamicLeftExpression() {
|
||||
assertEquals(
|
||||
match(FEATURE_SIGNATURE, MultiExpression.of(List.of(
|
||||
MultiExpression.entry(constOf(1),
|
||||
matchAnyTyped(null, script(FEATURE_SIGNATURE.withOutput(Object.class), "feature.id + 1"), List.of(1, 2)
|
||||
))))),
|
||||
|
||||
match(FEATURE_SIGNATURE, MultiExpression.of(List.of(
|
||||
MultiExpression.entry(constOf(1),
|
||||
BooleanExpressionParser.parse(Map.of("${feature.id + 1}", List.of(1, 2)), null, FEATURE_SIGNATURE.in())
|
||||
)))).simplify()
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"feature.id",
|
||||
"feature.osm_user_name",
|
||||
"feature.osm_type",
|
||||
})
|
||||
void testSimplifySimpleLeftExpression(String varname) {
|
||||
assertEquals(
|
||||
match(FEATURE_SIGNATURE, MultiExpression.of(List.of(
|
||||
MultiExpression.entry(constOf(1),
|
||||
matchAnyTyped(null, variable(FEATURE_SIGNATURE.withOutput(Object.class), varname), List.of(1, 2)
|
||||
))))),
|
||||
|
||||
match(FEATURE_SIGNATURE, MultiExpression.of(List.of(
|
||||
MultiExpression.entry(constOf(1),
|
||||
BooleanExpressionParser.parse(Map.of("${" + varname + "}", List.of(1, 2)), null, FEATURE_SIGNATURE.in())
|
||||
)))).simplify()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimplifyCoalesce() {
|
||||
assertEquals(
|
||||
|
|
Ładowanie…
Reference in New Issue