Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

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),
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
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;
Expand Down Expand Up @@ -91,7 +90,7 @@ static MatchAny matchAny(String field, List<?> values) {
* <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));
}

Expand All @@ -101,7 +100,7 @@ static MatchAny matchAnyTyped(String field, BiFunction<WithTags, String, Object>
* <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);
}
Expand Down Expand Up @@ -405,10 +404,10 @@ record MatchAny(
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<>();

Expand Down Expand Up @@ -474,6 +473,10 @@ public boolean evaluate(WithTags input, List<String> matchKeys) {

@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));
}
Expand All @@ -496,7 +499,9 @@ private boolean evaluate(List<String> matchKeys, Object value) {
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;
}
Expand All @@ -510,7 +515,13 @@ private boolean evaluate(List<String> matchKeys, Object value) {

@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
Expand Down Expand Up @@ -557,6 +568,11 @@ private String patternString() {
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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static boolean mustAlwaysEvaluate(Expression expression) {
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);
Expand All @@ -79,7 +79,7 @@ private static void getRelevantKeys(Expression exp, Consumer<String> acceptKey)
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,19 @@ void testPartialEvaluateMatchAny() {
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);
Expand Down
1 change: 1 addition & 0 deletions planetiler-custommap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions planetiler-custommap/planetiler.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,27 @@ private Expression tagCriterionToExpression(String key, Object value) {
} 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
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;

Expand All @@ -17,7 +17,7 @@
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<>();

Expand Down Expand Up @@ -50,7 +50,7 @@ public TagValueProducer(Map<String, Object> map) {
/**
* 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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,
Expand Down Expand Up @@ -49,6 +50,9 @@ private static <I, O> Converter<I, O> converter(Class<I> in, Class<O> out, Funct
*/
@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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,6 +9,8 @@
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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Loading
Loading