kopia lustrzana https://github.com/onthegomap/planetiler
Data type support for Expression / MultiExpression (#190)
rodzic
59a48e1620
commit
ffb157414e
|
@ -431,7 +431,7 @@ public class Generate {
|
|||
/** Imposm3 "mapping" to filter OSM elements that should appear in this "table". */
|
||||
public static final Expression MAPPING = %s;
|
||||
""".formatted(
|
||||
mappingExpression
|
||||
mappingExpression.generateJavaCode()
|
||||
);
|
||||
String tableName = "osm_" + key;
|
||||
String className = lowerUnderscoreToUpperCamel(tableName);
|
||||
|
@ -652,7 +652,7 @@ public class Generate {
|
|||
/** Returns java code that will recreate an {@link MultiExpression} identical to {@code mapping}. */
|
||||
private static String generateJavaCode(MultiExpression<String> mapping) {
|
||||
return "MultiExpression.of(List.of(" + mapping.expressions().stream()
|
||||
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression()))
|
||||
.map(s -> "MultiExpression.entry(%s, %s)".formatted(Format.quote(s.result()), s.expression().generateJavaCode()))
|
||||
.collect(joining(", ")) + "))";
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import static com.onthegomap.planetiler.TestUtils.newLineString;
|
|||
import static com.onthegomap.planetiler.TestUtils.newPoint;
|
||||
import static com.onthegomap.planetiler.TestUtils.rectangle;
|
||||
import static com.onthegomap.planetiler.basemap.BasemapProfile.OSM_SOURCE;
|
||||
import static com.onthegomap.planetiler.basemap.util.Utils.coalesce;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
@ -23,6 +24,7 @@ import com.onthegomap.planetiler.stats.Stats;
|
|||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.Wikidata;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -41,10 +43,15 @@ public abstract class AbstractLayerTest {
|
|||
final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats);
|
||||
|
||||
static void assertFeatures(int zoom, List<Map<String, Object>> expected, Iterable<FeatureCollector.Feature> actual) {
|
||||
List<FeatureCollector.Feature> actualList = StreamSupport.stream(actual.spliterator(), false).toList();
|
||||
assertEquals(expected.size(), actualList.size(), () -> "size: " + actualList);
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom));
|
||||
// ensure both are sorted by layer
|
||||
var expectedList =
|
||||
expected.stream().sorted(Comparator.comparing(d -> coalesce(d.get("_layer"), "").toString())).toList();
|
||||
var actualList = StreamSupport.stream(actual.spliterator(), false)
|
||||
.sorted(Comparator.comparing(FeatureCollector.Feature::getLayer))
|
||||
.toList();
|
||||
assertEquals(expectedList.size(), actualList.size(), () -> "size: " + actualList);
|
||||
for (int i = 0; i < expectedList.size(); i++) {
|
||||
assertSubmap(expectedList.get(i), TestUtils.toMap(actualList.get(i), zoom));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ class WaterTest extends AbstractLayerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testRiverk() {
|
||||
void testRiver() {
|
||||
assertFeatures(11, List.of(Map.of(
|
||||
"class", "river",
|
||||
"_layer", "water",
|
||||
|
|
|
@ -22,7 +22,7 @@ import org.locationtech.jts.geom.Geometry;
|
|||
*/
|
||||
public class BasemapMapping {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
public static void main(String[] args) {
|
||||
var profile = new BasemapProfile(Translations.nullProvider(List.of()), PlanetilerConfig.defaults(),
|
||||
Stats.inMemory());
|
||||
var random = new Random(0);
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.onthegomap.planetiler.expression;
|
||||
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -17,7 +19,7 @@ import java.util.stream.Stream;
|
|||
* <p>
|
||||
* Calling {@code toString()} on any expression will generate code that can be used to recreate an identical copy of the
|
||||
* original expression, assuming that the generated code includes:
|
||||
*
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
|
@ -32,27 +34,9 @@ public interface Expression {
|
|||
String RELATION_MEMBER_TYPE = "relation_member";
|
||||
|
||||
Set<String> supportedTypes = Set.of(LINESTRING_TYPE, POINT_TYPE, POLYGON_TYPE, RELATION_MEMBER_TYPE);
|
||||
Expression TRUE = new Expression() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TRUE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
Expression FALSE = new Expression() {
|
||||
public String toString() {
|
||||
return "FALSE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Expression TRUE = new Constant(true, "TRUE");
|
||||
Expression FALSE = new Constant(false, "FALSE");
|
||||
BiFunction<WithTags, String, Object> GET_TAG = WithTags::getTag;
|
||||
|
||||
static And and(Expression... children) {
|
||||
return and(List.of(children));
|
||||
|
@ -79,7 +63,7 @@ public interface Expression {
|
|||
* <p>
|
||||
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
|
||||
*/
|
||||
static MatchAny matchAny(String field, String... values) {
|
||||
static MatchAny matchAny(String field, Object... values) {
|
||||
return matchAny(field, List.of(values));
|
||||
}
|
||||
|
||||
|
@ -88,8 +72,29 @@ public interface Expression {
|
|||
* <p>
|
||||
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
|
||||
*/
|
||||
static MatchAny matchAny(String field, List<String> values) {
|
||||
return new MatchAny(field, values);
|
||||
static MatchAny matchAny(String field, List<?> values) {
|
||||
return new MatchAny(field, GET_TAG, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an expression that evaluates to true if the value for {@code field} tag is any of {@code values}, when
|
||||
* considering the tag as a specified data type and then converted to a string.
|
||||
* <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) {
|
||||
return matchAnyTyped(field, typeGetter, List.of(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an expression that evaluates to true if the value for {@code field} tag is any of {@code values}, when
|
||||
* considering the tag as a specified data type and then converted to a string.
|
||||
* <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,
|
||||
List<?> values) {
|
||||
return new MatchAny(field, typeGetter, values);
|
||||
}
|
||||
|
||||
/** Returns an expression that evaluates to true if the element has any value for tag {@code field}. */
|
||||
|
@ -115,8 +120,8 @@ public interface Expression {
|
|||
return new MatchType(type);
|
||||
}
|
||||
|
||||
private static String listToString(List<?> items) {
|
||||
return items.stream().map(Object::toString).collect(Collectors.joining(", "));
|
||||
private static String generateJavaCodeList(List<Expression> items) {
|
||||
return items.stream().map(Expression::generateJavaCode).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
private static Expression simplify(Expression initial) {
|
||||
|
@ -232,17 +237,33 @@ public interface Expression {
|
|||
* @param matchKeys list that this method call will add any key to that was responsible for triggering the match
|
||||
* @return true if this expression matches the input element
|
||||
*/
|
||||
boolean evaluate(SourceFeature input, List<String> matchKeys);
|
||||
boolean evaluate(WithTags input, List<String> matchKeys);
|
||||
|
||||
/** Returns Java code that can be used to reconstruct this expression. */
|
||||
String generateJavaCode();
|
||||
|
||||
/** A constant boolean value. */
|
||||
record Constant(boolean value, @Override String generateJavaCode) implements Expression {
|
||||
@Override
|
||||
public String toString() {
|
||||
return generateJavaCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
record And(List<Expression> children) implements Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "and(" + listToString(children) + ")";
|
||||
public String generateJavaCode() {
|
||||
return "and(" + generateJavaCodeList(children) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
for (Expression child : children) {
|
||||
if (!child.evaluate(input, matchKeys)) {
|
||||
matchKeys.clear();
|
||||
|
@ -256,12 +277,12 @@ public interface Expression {
|
|||
record Or(List<Expression> children) implements Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "or(" + listToString(children) + ")";
|
||||
public String generateJavaCode() {
|
||||
return "or(" + generateJavaCodeList(children) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
int size = children.size();
|
||||
// Optimization: this method consumes the most time when matching against input elements, and
|
||||
// iterating through this list by index is slightly faster than an enhanced for loop
|
||||
|
@ -297,12 +318,12 @@ public interface Expression {
|
|||
record Not(Expression child) implements Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "not(" + child + ")";
|
||||
public String generateJavaCode() {
|
||||
return "not(" + child.generateJavaCode() + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
return !child.evaluate(input, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
@ -317,28 +338,30 @@ public interface Expression {
|
|||
* @param matchWhenMissing if {@code values} contained ""
|
||||
*/
|
||||
record MatchAny(
|
||||
String field, List<String> values, Set<String> exactMatches, List<String> wildcards, boolean matchWhenMissing
|
||||
String field, List<?> values, Set<String> exactMatches, List<String> wildcards, boolean matchWhenMissing,
|
||||
BiFunction<WithTags, String, Object> valueGetter
|
||||
) implements Expression {
|
||||
|
||||
private static final Pattern containsPattern = Pattern.compile("^%(.*)%$");
|
||||
|
||||
MatchAny(String field, List<String> values) {
|
||||
MatchAny(String field, BiFunction<WithTags, String, Object> valueGetter, List<?> values) {
|
||||
this(field, values,
|
||||
values.stream().filter(v -> !v.contains("%")).collect(Collectors.toSet()),
|
||||
values.stream().filter(v -> v.contains("%")).map(val -> {
|
||||
values.stream().map(Object::toString).filter(v -> !v.contains("%")).collect(Collectors.toSet()),
|
||||
values.stream().map(Object::toString).filter(v -> v.contains("%")).map(val -> {
|
||||
var matcher = containsPattern.matcher(val);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("wildcards must start/end with %: " + val);
|
||||
}
|
||||
return matcher.group(1);
|
||||
}).toList(),
|
||||
values.contains("")
|
||||
values.contains(""),
|
||||
valueGetter
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
Object value = input.getTag(field);
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
Object value = valueGetter.apply(input, field);
|
||||
if (value == null) {
|
||||
return matchWhenMissing;
|
||||
} else {
|
||||
|
@ -358,9 +381,23 @@ public interface Expression {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "matchAny(" + Format.quote(field) + ", " + values.stream().map(Format::quote)
|
||||
.collect(Collectors.joining(", ")) + ")";
|
||||
public String generateJavaCode() {
|
||||
// java code generation only needed for the simple cases used by openmaptiles schema generation
|
||||
List<String> valueStrings = new ArrayList<>();
|
||||
|
||||
if (GET_TAG != valueGetter) {
|
||||
throw new UnsupportedOperationException("Code generation only supported for default getTag");
|
||||
}
|
||||
|
||||
for (var value : values) {
|
||||
if (value instanceof String string) {
|
||||
valueStrings.add(Format.quote(string));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Code generation only supported for string values, found: " +
|
||||
value.getClass().getCanonicalName() + " " + value);
|
||||
}
|
||||
}
|
||||
return "matchAny(" + Format.quote(field) + ", " + String.join(", ", valueStrings) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,12 +405,12 @@ public interface Expression {
|
|||
record MatchField(String field) implements Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public String generateJavaCode() {
|
||||
return "matchField(" + Format.quote(field) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
if (input.hasTag(field)) {
|
||||
matchKeys.add(field);
|
||||
return true;
|
||||
|
@ -388,19 +425,23 @@ public interface Expression {
|
|||
record MatchType(String type) implements Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public String generateJavaCode() {
|
||||
return "matchType(" + Format.quote(type) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(SourceFeature input, List<String> matchKeys) {
|
||||
return switch (type) {
|
||||
case LINESTRING_TYPE -> input.canBeLine();
|
||||
case POLYGON_TYPE -> input.canBePolygon();
|
||||
case POINT_TYPE -> input.isPoint();
|
||||
case RELATION_MEMBER_TYPE -> input.hasRelationInfo();
|
||||
default -> false;
|
||||
};
|
||||
public boolean evaluate(WithTags input, List<String> matchKeys) {
|
||||
if (input instanceof SourceFeature sourceFeature) {
|
||||
return switch (type) {
|
||||
case LINESTRING_TYPE -> sourceFeature.canBeLine();
|
||||
case POLYGON_TYPE -> sourceFeature.canBePolygon();
|
||||
case POINT_TYPE -> sourceFeature.isPoint();
|
||||
case RELATION_MEMBER_TYPE -> sourceFeature.hasRelationInfo();
|
||||
default -> false;
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,17 @@ import static com.onthegomap.planetiler.geo.GeoUtils.EMPTY_GEOMETRY;
|
|||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A list of {@link Expression Expressions} to evaluate on input elements.
|
||||
|
@ -31,6 +33,8 @@ import java.util.function.Predicate;
|
|||
*/
|
||||
public record MultiExpression<T> (List<Entry<T>> expressions) {
|
||||
|
||||
private static final Comparator<WithId> BY_ID = Comparator.comparingInt(WithId::id);
|
||||
|
||||
public static <T> MultiExpression<T> of(List<Entry<T>> expressions) {
|
||||
return new MultiExpression<>(expressions);
|
||||
}
|
||||
|
@ -51,7 +55,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
visited[expressionValue.id] = true;
|
||||
List<String> matchKeys = new ArrayList<>();
|
||||
if (expressionValue.expression().evaluate(input, matchKeys)) {
|
||||
result.add(new Match<>(expressionValue.result, matchKeys));
|
||||
result.add(new Match<>(expressionValue.result, matchKeys, expressionValue.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,9 +83,9 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
*/
|
||||
private static void getRelevantMissingKeys(Expression exp, Consumer<String> acceptKey) {
|
||||
if (exp instanceof Expression.And and) {
|
||||
and.children().forEach(child -> getRelevantKeys(child, acceptKey));
|
||||
and.children().forEach(child -> getRelevantMissingKeys(child, acceptKey));
|
||||
} else if (exp instanceof Expression.Or or) {
|
||||
or.children().forEach(child -> getRelevantKeys(child, acceptKey));
|
||||
or.children().forEach(child -> getRelevantMissingKeys(child, acceptKey));
|
||||
} else if (exp instanceof Expression.Not) {
|
||||
// ignore anything that's purely used as a filter
|
||||
} else if (exp instanceof Expression.MatchAny any && any.matchWhenMissing()) {
|
||||
|
@ -100,7 +104,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
}
|
||||
|
||||
/** Returns a copy of this multi-expression that replaces every expression using {@code mapper}. */
|
||||
public MultiExpression<T> map(Function<Expression, Expression> mapper) {
|
||||
public MultiExpression<T> map(UnaryOperator<Expression> mapper) {
|
||||
return new MultiExpression<>(
|
||||
expressions.stream()
|
||||
.map(entry -> entry(entry.result, mapper.apply(entry.expression).simplify()))
|
||||
|
@ -159,7 +163,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
/** Returns all data values associated with expressions that match an input element. */
|
||||
default List<T> getMatches(SourceFeature input) {
|
||||
List<Match<T>> matches = getMatchesWithTriggers(input);
|
||||
return matches.stream().map(d -> d.match).toList();
|
||||
return matches.stream().sorted(BY_ID).map(d -> d.match).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,6 +193,10 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
}
|
||||
}
|
||||
|
||||
private interface WithId {
|
||||
int id();
|
||||
}
|
||||
|
||||
private static class EmptyIndex<T> implements Index<T> {
|
||||
|
||||
@Override
|
||||
|
@ -214,26 +222,37 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
private final List<Map.Entry<String, List<EntryWithId<T>>>> keyToExpressionsList;
|
||||
// expressions that should match when certain tags are *not* present on an input element
|
||||
private final List<Map.Entry<String, List<EntryWithId<T>>>> missingKeyToExpressionList;
|
||||
// expressions that match a constant true input element
|
||||
private final List<EntryWithId<T>> constantTrueExpressionList;
|
||||
|
||||
private KeyIndex(MultiExpression<T> expressions) {
|
||||
AtomicInteger ids = new AtomicInteger();
|
||||
int id = 1;
|
||||
// build the indexes
|
||||
Map<String, Set<EntryWithId<T>>> keyToExpressions = new HashMap<>();
|
||||
Map<String, Set<EntryWithId<T>>> missingKeyToExpressions = new HashMap<>();
|
||||
List<EntryWithId<T>> constants = new ArrayList<>();
|
||||
|
||||
for (var entry : expressions.expressions) {
|
||||
Expression expression = entry.expression;
|
||||
EntryWithId<T> expressionValue = new EntryWithId<>(entry.result, expression, ids.incrementAndGet());
|
||||
EntryWithId<T> expressionValue = new EntryWithId<>(entry.result, expression, id++);
|
||||
getRelevantKeys(expression,
|
||||
key -> keyToExpressions.computeIfAbsent(key, k -> new HashSet<>()).add(expressionValue));
|
||||
getRelevantMissingKeys(expression,
|
||||
key -> missingKeyToExpressions.computeIfAbsent(key, k -> new HashSet<>()).add(expressionValue));
|
||||
if (expression.equals(TRUE)) {
|
||||
constants.add(expressionValue);
|
||||
}
|
||||
}
|
||||
keyToExpressionsMap = new HashMap<>();
|
||||
keyToExpressions.forEach((key, value) -> keyToExpressionsMap.put(key, value.stream().toList()));
|
||||
keyToExpressionsList = keyToExpressionsMap.entrySet().stream().toList();
|
||||
// create immutable copies for fast iteration at matching time
|
||||
constantTrueExpressionList = List.copyOf(constants);
|
||||
keyToExpressionsMap = keyToExpressions.entrySet().stream().collect(Collectors.toUnmodifiableMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().stream().toList()
|
||||
));
|
||||
keyToExpressionsList = List.copyOf(keyToExpressionsMap.entrySet());
|
||||
missingKeyToExpressionList = missingKeyToExpressions.entrySet().stream()
|
||||
.map(entry -> Map.entry(entry.getKey(), entry.getValue().stream().toList())).toList();
|
||||
numExpressions = ids.incrementAndGet();
|
||||
numExpressions = id;
|
||||
}
|
||||
|
||||
/** Lookup matches in this index for expressions that match a certain type. */
|
||||
|
@ -241,6 +260,9 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
public List<Match<T>> getMatchesWithTriggers(SourceFeature input) {
|
||||
List<Match<T>> result = new ArrayList<>();
|
||||
boolean[] visited = new boolean[numExpressions];
|
||||
for (var entry : constantTrueExpressionList) {
|
||||
result.add(new Match<>(entry.result, List.of(), entry.id));
|
||||
}
|
||||
for (var entry : missingKeyToExpressionList) {
|
||||
if (!input.hasTag(entry.getKey())) {
|
||||
visitExpressions(input, result, visited, entry.getValue());
|
||||
|
@ -308,7 +330,7 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
}
|
||||
|
||||
/** An expression/value pair with unique ID to store whether we evaluated it yet. */
|
||||
private record EntryWithId<T> (T result, Expression expression, int id) {}
|
||||
private record EntryWithId<T> (T result, Expression expression, @Override int id) implements WithId {}
|
||||
|
||||
/**
|
||||
* An {@code expression} to evaluate on input elements and {@code result} value to return when the element matches.
|
||||
|
@ -316,5 +338,5 @@ public record MultiExpression<T> (List<Entry<T>> expressions) {
|
|||
public record Entry<T> (T result, Expression expression) {}
|
||||
|
||||
/** The result when an expression matches, along with the input element tag {@code keys} that triggered the match. */
|
||||
public record Match<T> (T match, List<String> keys) {}
|
||||
public record Match<T> (T match, List<String> keys, @Override int id) implements WithId {}
|
||||
}
|
||||
|
|
|
@ -149,11 +149,11 @@ public class Format {
|
|||
}
|
||||
|
||||
/** Returns Java code that can re-create {@code string}: {@code null} if null, or {@code "contents"} if not empty. */
|
||||
public static String quote(String string) {
|
||||
public static String quote(Object string) {
|
||||
if (string == null) {
|
||||
return "null";
|
||||
}
|
||||
return '"' + StringEscapeUtils.escapeJava(string) + '"';
|
||||
return '"' + StringEscapeUtils.escapeJava(string.toString()) + '"';
|
||||
}
|
||||
|
||||
/** Returns an openstreetmap.org map link for a lat/lon */
|
||||
|
|
|
@ -3,8 +3,10 @@ package com.onthegomap.planetiler.expression;
|
|||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -101,4 +103,13 @@ class ExpressionTest {
|
|||
assertFalse(matchCD.contains(e -> e.equals(matchAB)));
|
||||
assertFalse(or(not(matchCD)).contains(e -> e.equals(matchAB)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyExpression() {
|
||||
//Ensure Expression.toString() returns valid Java code
|
||||
assertEquals("matchAny(\"key\", \"true\")", matchAny("key", "true").generateJavaCode());
|
||||
assertEquals("matchAny(\"key\", \"foo\")", matchAny("key", "foo").generateJavaCode());
|
||||
var expression = matchAnyTyped("key", WithTags::getDirection, 1);
|
||||
assertThrows(UnsupportedOperationException.class, expression::generateJavaCode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ import static com.onthegomap.planetiler.TestUtils.rectangle;
|
|||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static com.onthegomap.planetiler.expression.MultiExpression.entry;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
|
@ -47,6 +49,62 @@ class MultiExpressionTest {
|
|||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleElementBooleanTrue() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
entry("a", matchAnyTyped("key", WithTags::getBoolean, true))
|
||||
)).index();
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "true")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "yes")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "1")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "true", "otherkey", "othervalue")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "true", "key3", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "false")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleElementBooleanFalse() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
entry("a", matchAnyTyped("key", WithTags::getBoolean, false))
|
||||
)).index();
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "false")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "no")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "0")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "false", "otherkey", "othervalue")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "false", "key3", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "true")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleElementLong() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
entry("a", matchAnyTyped("key", WithTags::getLong, 42))
|
||||
)).index();
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "42")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "42", "otherkey", "othervalue")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "42", "key3", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "99")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleElementDirection() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
entry("a", matchAnyTyped("key", WithTags::getDirection, 1))
|
||||
)).index();
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "yes")));
|
||||
assertSameElements(List.of("a"), index.getMatches(featureWithTags("key", "1", "otherkey", "othervalue")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "1", "key3", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key2", "value")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags("key", "99")));
|
||||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBlankStringTreatedAsNotMatch() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
|
@ -75,6 +133,44 @@ class MultiExpressionTest {
|
|||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStaticBooleanMatch() {
|
||||
var index = MultiExpression.of(List.of(entry("t", TRUE))).index();
|
||||
assertTrue(index.matches(featureWithTags("key", "value")));
|
||||
|
||||
index = MultiExpression.of(List.of(entry("f", FALSE))).index();
|
||||
assertFalse(index.matches(featureWithTags("key", "value")));
|
||||
|
||||
index = MultiExpression.of(List.of(
|
||||
entry("a", matchField("key")),
|
||||
entry("t", TRUE),
|
||||
entry("f", FALSE)
|
||||
)).index();
|
||||
|
||||
assertSameElements(List.of("a", "t"), index.getMatches(featureWithTags("key", "value")));
|
||||
|
||||
index = MultiExpression.of(List.of(
|
||||
entry("a", matchField("key")),
|
||||
entry("t1", TRUE),
|
||||
entry("t2", TRUE),
|
||||
entry("t3", TRUE),
|
||||
entry("f1", FALSE),
|
||||
entry("f2", FALSE),
|
||||
entry("f3", FALSE)
|
||||
)).index();
|
||||
|
||||
assertSameElements(List.of("a", "t1", "t2", "t3"), index.getMatches(featureWithTags("key", "value")));
|
||||
|
||||
index = MultiExpression.of(List.of(
|
||||
entry("t3", TRUE),
|
||||
entry("t2", TRUE),
|
||||
entry("t1", TRUE),
|
||||
entry("a", matchField("key"))
|
||||
)).index();
|
||||
|
||||
assertSameElements(List.of("t3", "t2", "t1", "a"), index.getMatches(featureWithTags("key", "value")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWildcard() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
|
@ -91,6 +187,23 @@ class MultiExpressionTest {
|
|||
assertSameElements(List.of(), index.getMatches(featureWithTags()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleMatches() {
|
||||
var feature = featureWithTags("a", "b", "c", "d");
|
||||
var index = MultiExpression.of(List.of(
|
||||
entry("a", matchAny("a", "b")),
|
||||
entry("b", matchAny("c", "d"))
|
||||
)).index();
|
||||
var index2 = MultiExpression.of(List.of(
|
||||
entry("b", matchAny("c", "d")),
|
||||
entry("a", matchAny("a", "b"))
|
||||
)).index();
|
||||
assertSameElements(List.of("a", "b"), index.getMatches(feature));
|
||||
assertSameElements(List.of("b", "a"), index2.getMatches(feature));
|
||||
assertEquals("a", index.getOrElse(feature, "miss"));
|
||||
assertEquals("b", index2.getOrElse(feature, "miss"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleWildcardsMixedWithExacts() {
|
||||
var index = MultiExpression.of(List.of(
|
||||
|
@ -191,15 +304,15 @@ class MultiExpressionTest {
|
|||
))
|
||||
)).index();
|
||||
assertSameElements(List.of(new MultiExpression.Match<>(
|
||||
"a", List.of("key1")
|
||||
"a", List.of("key1"), 1
|
||||
)), index.getMatchesWithTriggers(featureWithTags("key1", "val1")));
|
||||
assertSameElements(List.of(new MultiExpression.Match<>(
|
||||
"a", List.of("key2")
|
||||
"a", List.of("key2"), 1
|
||||
), new MultiExpression.Match<>(
|
||||
"b", List.of("key2")
|
||||
"b", List.of("key2"), 2
|
||||
)), index.getMatchesWithTriggers(featureWithTags("key2", "val2")));
|
||||
assertSameElements(List.of(new MultiExpression.Match<>(
|
||||
"b", List.of("key3")
|
||||
"b", List.of("key3"), 2
|
||||
)), index.getMatchesWithTriggers(featureWithTags("key3", "val3")));
|
||||
}
|
||||
|
||||
|
@ -218,7 +331,7 @@ class MultiExpressionTest {
|
|||
))
|
||||
)).index();
|
||||
assertSameElements(List.of(new MultiExpression.Match<>(
|
||||
"a", List.of("key1", "key3")
|
||||
"a", List.of("key1", "key3"), 1
|
||||
)), index.getMatchesWithTriggers(featureWithTags("key1", "val1", "key3", "val3")));
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue