Skip to content

Commit

Permalink
Convert ${feature.source/source_layer}: [ ... ] match expression to…
Browse files Browse the repository at this point in the history
… `MatchSource`/`MatchSourceLayer` (#1065)
  • Loading branch information
msbarry authored Oct 12, 2024
1 parent e3d5645 commit bc4ba79
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ static And and(Expression... children) {
return and(List.of(children));
}

static And and(List<Expression> children) {
static And and(List<? extends Expression> children) {
return new And(children);
}

static Or or(Expression... children) {
return or(List.of(children));
}

static Or or(List<Expression> children) {
static Or or(List<? extends Expression> children) {
return new Or(children);
}

Expand Down Expand Up @@ -100,8 +100,7 @@ static MatchAny matchAnyTyped(String field, TypedGetter typeGetter, Object... va
* <p>
* {@code values} can contain exact matches, "%text%" to match any value containing "text", or "" to match any value.
*/
static MatchAny matchAnyTyped(String field, TypedGetter typeGetter,
List<?> values) {
static MatchAny matchAnyTyped(String field, TypedGetter typeGetter, List<?> values) {
return MatchAny.from(field, typeGetter, values);
}

Expand Down Expand Up @@ -153,7 +152,7 @@ static MatchSourceLayer matchSourceLayer(String layer) {
return new MatchSourceLayer(layer);
}

private static String generateJavaCodeList(List<Expression> items) {
private static String generateJavaCodeList(List<? extends Expression> items) {
return items.stream().map(Expression::generateJavaCode).collect(Collectors.joining(", "));
}

Expand Down Expand Up @@ -268,7 +267,7 @@ public boolean evaluate(WithTags input, List<String> matchKeys) {
}
}

record And(List<Expression> children) implements Expression {
record And(List<? extends Expression> children) implements Expression {

@Override
public String generateJavaCode() {
Expand Down Expand Up @@ -306,7 +305,7 @@ public Expression simplifyOnce() {
}
}

record Or(List<Expression> children) implements Expression {
record Or(List<? extends Expression> children) implements Expression {

@Override
public String generateJavaCode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import static com.onthegomap.planetiler.expression.Expression.TRUE;
import static com.onthegomap.planetiler.expression.Expression.matchType;

import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.WithGeometryType;
import com.onthegomap.planetiler.reader.WithSource;
import com.onthegomap.planetiler.reader.WithSourceLayer;
import com.onthegomap.planetiler.reader.WithTags;
import java.util.ArrayList;
import java.util.Comparator;
Expand Down Expand Up @@ -436,7 +437,7 @@ private SourceLayerIndex(MultiExpression<T> expressions, boolean warn) {

@Override
String extract(WithTags input) {
return input instanceof SourceFeature feature ? feature.getSourceLayer() : null;
return input instanceof WithSourceLayer feature ? feature.getSourceLayer() : null;
}
}

Expand All @@ -451,7 +452,7 @@ private SourceIndex(MultiExpression<T> expressions, boolean warn) {

@Override
String extract(WithTags input) {
return input instanceof SourceFeature feature ? feature.getSource() : null;
return input instanceof WithSource feature ? feature.getSource() : null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* All geometries except for {@link #latLonGeometry()} return elements in world web mercator coordinates where (0,0) is
* the northwest corner and (1,1) is the southeast corner of the planet.
*/
public abstract class SourceFeature implements WithTags, WithGeometryType {
public abstract class SourceFeature implements WithTags, WithGeometryType, WithSource, WithSourceLayer {

private final Map<String, Object> tags;
private final String source;
Expand Down Expand Up @@ -279,11 +279,13 @@ public double size() throws GeometryException {
}

/** Returns the ID of the source that this feature came from. */
@Override
public String getSource() {
return source;
}

/** Returns the layer ID within a source that this feature comes from. */
@Override
public String getSourceLayer() {
return sourceLayer;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.onthegomap.planetiler.reader;

public interface WithSource {
String getSource();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.onthegomap.planetiler.reader;

public interface WithSourceLayer {
String getSourceLayer();
}
3 changes: 2 additions & 1 deletion planetiler-custommap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ layers:

A feature is a defined set of objects that meet a specified filter criteria.

- `source` - A string [source](#source) ID, or list of source IDs from which features should be extracted
- `source` - A string [source](#source) ID, or list of source IDs from which features should be extracted. If missing,
features from all sources are included.
- `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
Expand Down
2 changes: 1 addition & 1 deletion planetiler-custommap/planetiler.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@
]
},
"source": {
"description": "A source ID or list of source IDs from which features should be extracted",
"description": "A source ID or list of source IDs from which features should be extracted. If unspecified, all sources are included",
"oneOf": [
{
"type": "string"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import static com.onthegomap.planetiler.expression.Expression.matchAnyTyped;
import static com.onthegomap.planetiler.expression.Expression.matchField;
import static com.onthegomap.planetiler.expression.Expression.not;
import static com.onthegomap.planetiler.expression.Expression.or;

import com.onthegomap.planetiler.custommap.expression.BooleanExpressionScript;
import com.onthegomap.planetiler.custommap.expression.ConfigExpression;
import com.onthegomap.planetiler.custommap.expression.ConfigExpressionScript;
import com.onthegomap.planetiler.custommap.expression.ParseException;
import com.onthegomap.planetiler.custommap.expression.ScriptContext;
Expand Down Expand Up @@ -119,11 +121,22 @@ private Expression tagCriterionToExpression(String key, Object value) {
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);
var expression = ConfigExpressionScript.parse(ConfigExpressionScript.extractScript(key), context).simplify();
if (isAny) {
values = List.of();
}
return matchAnyTyped(null, expression, values);
var result = matchAnyTyped(null, expression, values);
if (!values.isEmpty() && result.pattern() == null && !result.isMatchAnything() && !result.matchWhenMissing() &&
expression instanceof ConfigExpression.Variable<?, ?>(var ignored,var name)) {
if (name.equals("feature.source")) {
return or(values.stream().filter(String.class::isInstance).map(String.class::cast)
.map(Expression::matchSource).toList());
} else if (name.equals("feature.source_layer")) {
return or(values.stream().filter(String.class::isInstance).map(String.class::cast)
.map(Expression::matchSourceLayer).toList());
}
}
return result;
}
String field = unescape(key);
if (isAny) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public ConfiguredFeature(String layer, TagValueProducer tagValueProducer, Featur
BooleanExpressionParser.parse(feature.includeWhen(), tagValueProducer,
processFeatureContext);
}
if (!feature.source().isEmpty()) {
filter = Expression.and(
filter,
Expression.or(feature.source().stream().map(Expression::matchSource).toList())
);
}
if (feature.excludeWhen() != null) {
filter = Expression.and(
filter,
Expand Down Expand Up @@ -274,7 +280,7 @@ public void processFeature(Contexts.FeaturePostMatch context, FeatureCollector f
var sourceFeature = context.feature();

// Ensure that this feature is from the correct source (index should enforce this, so just check when assertions enabled)
assert sources.contains(sourceFeature.getSource());
assert sources.isEmpty() || sources.contains(sourceFeature.getSource());

var f = geometryFactory.apply(features);
for (var processor : featureProcessors) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.onthegomap.planetiler.custommap;

import static com.onthegomap.planetiler.expression.MultiExpression.Entry;
import static java.util.Map.entry;

import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureMerge;
Expand All @@ -19,50 +18,43 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* A profile configured from a yml file.
*/
public class ConfiguredProfile implements Profile {

private final SchemaConfig schema;

private final Collection<FeatureLayer> layers;
private final Map<String, FeatureLayer> layersById = new HashMap<>();
private final Map<String, Index<ConfiguredFeature>> featureLayerMatcher;
private final Index<ConfiguredFeature> featureLayerMatcher;
private final TagValueProducer tagValueProducer;
private final Contexts.Root rootContext;

public ConfiguredProfile(SchemaConfig schema, Contexts.Root rootContext) {
this.schema = schema;
this.rootContext = rootContext;

layers = schema.layers();
Collection<FeatureLayer> layers = schema.layers();
if (layers == null || layers.isEmpty()) {
throw new IllegalArgumentException("No layers defined");
}

tagValueProducer = new TagValueProducer(schema.inputMappings());

Map<String, List<MultiExpression.Entry<ConfiguredFeature>>> configuredFeatureEntries = new HashMap<>();
List<MultiExpression.Entry<ConfiguredFeature>> configuredFeatureEntries = new ArrayList<>();

for (var layer : layers) {
String layerId = layer.id();
layersById.put(layerId, layer);
for (var feature : layer.features()) {
var configuredFeature = new ConfiguredFeature(layerId, tagValueProducer, feature, rootContext);
var entry = new Entry<>(configuredFeature, configuredFeature.matchExpression());
for (var source : feature.source()) {
var list = configuredFeatureEntries.computeIfAbsent(source, s -> new ArrayList<>());
list.add(entry);
}
configuredFeatureEntries.add(entry);
}
}

featureLayerMatcher = configuredFeatureEntries.entrySet().stream()
.map(entry -> entry(entry.getKey(), MultiExpression.of(entry.getValue()).index()))
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
featureLayerMatcher = MultiExpression.of(configuredFeatureEntries).index();

}

@Override
Expand All @@ -78,15 +70,12 @@ public String attribution() {
@Override
public void processFeature(SourceFeature sourceFeature, FeatureCollector featureCollector) {
var context = rootContext.createProcessFeatureContext(sourceFeature, tagValueProducer);
var index = featureLayerMatcher.get(sourceFeature.getSource());
if (index != null) {
var matches = index.getMatchesWithTriggers(context);
for (var configuredFeature : matches) {
configuredFeature.match().processFeature(
context.createPostMatchContext(configuredFeature.keys()),
featureCollector
);
}
var matches = featureLayerMatcher.getMatchesWithTriggers(context);
for (var configuredFeature : matches) {
configuredFeature.match().processFeature(
context.createPostMatchContext(configuredFeature.keys()),
featureCollector
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.onthegomap.planetiler.expression.DataType;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.WithGeometryType;
import com.onthegomap.planetiler.reader.WithSource;
import com.onthegomap.planetiler.reader.WithSourceLayer;
import com.onthegomap.planetiler.reader.WithTags;
import com.onthegomap.planetiler.reader.osm.OsmElement;
import com.onthegomap.planetiler.reader.osm.OsmSourceFeature;
Expand Down Expand Up @@ -285,7 +287,8 @@ default Object argument(String key) {
* Makes nested contexts adhere to {@link WithTags} and {@link WithGeometryType} by recursively fetching source
* feature from the root context.
*/
private interface FeatureContext extends ScriptContext, WithTags, WithGeometryType, NestedContext {
private interface FeatureContext extends ScriptContext, WithTags, WithGeometryType, NestedContext, WithSourceLayer,
WithSource {

default FeatureContext parent() {
return null;
Expand Down Expand Up @@ -325,6 +328,16 @@ default boolean canBeLine() {
default boolean canBePolygon() {
return feature().canBePolygon();
}

@Override
default String getSource() {
return feature().getSource();
}

@Override
default String getSourceLayer() {
return feature().getSourceLayer();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public Collection<AttributeDefinition> attributes() {
public FeatureGeometry geometry() {
return geometry == null ? FeatureGeometry.ANY : geometry;
}

@Override
public List<String> source() {
return source == null ? List.of() : source;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,32 @@ void testAnyGeometry(String expression) {
}, 1);
}

@ParameterizedTest
@ValueSource(strings = {"source: []", ""})
void testAnySource(String expression) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- geometry: point
%s
""".formatted(expression).strip();
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
testFeature(config, SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(
), "osm", null, 1, emptyList(), OSM_INFO), feature -> {
assertInstanceOf(Puntal.class, feature.getGeometry());
}, 1);
testFeature(config, SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(
), "other", null, 1, emptyList(), OSM_INFO), feature -> {
assertInstanceOf(Puntal.class, feature.getGeometry());
}, 1);
}

@Test
void testWikidataParse() {
var config = """
Expand Down
Loading

0 comments on commit bc4ba79

Please sign in to comment.