Allow dynamic expressions on left hand side of match statements (#1063)

match-source-and-layer-optimization
Michael Barry 2024-10-12 05:37:09 -04:00 zatwierdzone przez GitHub
rodzic be7cec36db
commit e3d5645e1a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
17 zmienionych plików z 345 dodań i 51 usunięć

Wyświetl plik

@ -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;

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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

Wyświetl plik

@ -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",

Wyświetl plik

@ -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);
}
}
}
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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")

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -173,4 +173,9 @@ public class ConfigExpressionScript<I extends ScriptContext, O> implements Confi
}
return this;
}
@Override
public ScriptEnvironment<I> environment() {
return descriptor;
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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(