planetiler/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredFeature.java

284 wiersze
11 KiB
Java

package com.onthegomap.planetiler.custommap;
import static com.onthegomap.planetiler.custommap.expression.ConfigExpression.constOf;
import static com.onthegomap.planetiler.expression.Expression.not;
import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureCollector.Feature;
import com.onthegomap.planetiler.custommap.configschema.AttributeDefinition;
import com.onthegomap.planetiler.custommap.configschema.FeatureGeometry;
import com.onthegomap.planetiler.custommap.configschema.FeatureItem;
import com.onthegomap.planetiler.custommap.expression.ScriptEnvironment;
import com.onthegomap.planetiler.expression.Expression;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* A map feature, configured from a YML configuration file.
*
* {@link #matchExpression()} returns a filtering expression to limit input elements to ones this feature cares about,
* and {@link #processFeature(Contexts.FeaturePostMatch, FeatureCollector)} processes matching elements.
*/
public class ConfiguredFeature {
private static final double LOG4 = Math.log(4);
private final Expression geometryTest;
private final Function<FeatureCollector, Feature> geometryFactory;
private final Expression tagTest;
private final TagValueProducer tagValueProducer;
private final List<BiConsumer<Contexts.FeaturePostMatch, Feature>> featureProcessors;
private final Set<String> sources;
private final ScriptEnvironment<Contexts.ProcessFeature> processFeatureContext;
private final ScriptEnvironment<Contexts.FeatureAttribute> featureAttributeContext;
private ScriptEnvironment<Contexts.FeaturePostMatch> featurePostMatchContext;
public ConfiguredFeature(String layer, TagValueProducer tagValueProducer, FeatureItem feature,
Contexts.Root rootContext) {
sources = Set.copyOf(feature.source());
FeatureGeometry geometryType = feature.geometry();
//Test to determine whether this type of geometry is included
geometryTest = geometryType.featureTest();
//Factory to treat OSM tag values as specific data type values
this.tagValueProducer = tagValueProducer;
processFeatureContext = Contexts.ProcessFeature.description(rootContext);
featurePostMatchContext = Contexts.FeaturePostMatch.description(rootContext);
featureAttributeContext = Contexts.FeatureAttribute.description(rootContext);
//Test to determine whether this feature is included based on tagging
Expression filter;
if (feature.includeWhen() == null) {
filter = Expression.TRUE;
} else {
filter =
BooleanExpressionParser.parse(feature.includeWhen(), tagValueProducer,
processFeatureContext);
}
if (feature.excludeWhen() != null) {
filter = Expression.and(
filter,
Expression.not(
BooleanExpressionParser.parse(feature.excludeWhen(), tagValueProducer,
processFeatureContext))
);
}
tagTest = filter;
//Factory to generate the right feature type from FeatureCollector
geometryFactory = geometryType.newGeometryFactory(layer);
//Configure logic for each attribute in the output tile
List<BiConsumer<Contexts.FeaturePostMatch, Feature>> processors = new ArrayList<>();
for (var attribute : feature.attributes()) {
processors.add(attributeProcessor(attribute));
}
processors.add(makeFeatureProcessor(feature.minZoom(), Integer.class, Feature::setMinZoom));
processors.add(makeFeatureProcessor(feature.maxZoom(), Integer.class, Feature::setMaxZoom));
featureProcessors = processors.stream().filter(Objects::nonNull).toList();
}
private <T> BiConsumer<Contexts.FeaturePostMatch, Feature> makeFeatureProcessor(Object input, Class<T> clazz,
BiConsumer<Feature, T> consumer) {
if (input == null) {
return null;
}
var expression = ConfigExpressionParser.parse(
input,
tagValueProducer,
featurePostMatchContext,
clazz
);
if (expression.equals(constOf(null))) {
return null;
}
return (context, feature) -> {
var result = expression.apply(context);
if (result != null) {
consumer.accept(feature, result);
}
};
}
private static int minZoomFromTilePercent(SourceFeature sf, Double minTilePercent) {
if (minTilePercent == null) {
return 0;
}
try {
return (int) (Math.log(minTilePercent / sf.area()) / LOG4);
} catch (GeometryException e) {
return 14;
}
}
/**
* Produces logic that generates attribute values based on configuration and input data. If both a constantValue
* configuration and a tagValue configuration are set, this is likely a mistake, and the constantValue will take
* precedence.
*
* @param attribute - attribute definition configured from YML
* @return a function that generates an attribute value from a {@link SourceFeature} based on an attribute
* configuration.
*/
private Function<Contexts.FeaturePostMatch, Object> attributeValueProducer(AttributeDefinition attribute) {
Object type = attribute.type();
// some expression features are hoisted to the top-level for attribute values for brevity,
// so just map them to what the equivalent expression syntax would be and parse as an expression.
Map<String, Object> value = new HashMap<>();
if ("match_key".equals(type)) {
value.put("value", "${match_key}");
} else if ("match_value".equals(type)) {
value.put("value", "${match_value}");
} else {
if (type != null) {
value.put("type", type);
}
if (attribute.coalesce() != null) {
value.put("coalesce", attribute.coalesce());
} else if (attribute.value() != null) {
value.put("value", attribute.value());
} else if (attribute.tagValue() != null) {
value.put("tag_value", attribute.tagValue());
} else if (attribute.argValue() != null) {
value.put("arg_value", attribute.argValue());
} else {
value.put("tag_value", attribute.key());
}
}
return ConfigExpressionParser.parse(value, tagValueProducer, featurePostMatchContext, Object.class);
}
/**
* Generate logic which determines the minimum zoom level for a feature based on a configured pixel size limit.
*
* @param minTilePercent - minimum percentage of a tile that a feature must cover to be shown
* @param rawMinZoom - global minimum zoom for this feature, or an expression providing the min zoom dynamically
* @param minZoomByValue - map of tag values to zoom level
* @return minimum zoom function
*/
private Function<Contexts.FeatureAttribute, Integer> attributeZoomThreshold(
Double minTilePercent, Object rawMinZoom, Map<Object, Integer> minZoomByValue) {
var result = ConfigExpressionParser.parse(rawMinZoom, tagValueProducer,
featureAttributeContext, Integer.class);
if ((result.equals(constOf(0)) ||
result.equals(constOf(null))) && minZoomByValue.isEmpty()) {
return null;
}
if (minZoomByValue.isEmpty()) {
return context -> Math.max(result.apply(context), minZoomFromTilePercent(context.feature(), minTilePercent));
}
//Attribute value-specific zooms override static zooms
return context -> {
var value = minZoomByValue.get(context.value());
return value != null ? value :
Math.max(result.apply(context), minZoomFromTilePercent(context.feature(), minTilePercent));
};
}
/**
* Generates a function which produces a fully-configured attribute for a feature.
*
* @param attribute - configuration for this attribute
* @return processing logic
*/
private BiConsumer<Contexts.FeaturePostMatch, Feature> attributeProcessor(AttributeDefinition attribute) {
var tagKey = attribute.key();
Object attributeMinZoom = attribute.minZoom();
attributeMinZoom = attributeMinZoom == null ? "0" : attributeMinZoom;
var minZoomByValue = attribute.minZoomByValue();
minZoomByValue = minZoomByValue == null ? Map.of() : minZoomByValue;
//Workaround because numeric keys are mapped as String
minZoomByValue = tagValueProducer.remapKeysByType(tagKey, minZoomByValue);
var attributeValueProducer = attributeValueProducer(attribute);
var fallback = attribute.fallback();
var attrIncludeWhen = attribute.includeWhen();
var attrExcludeWhen = attribute.excludeWhen();
var attributeTest =
Expression.and(
attrIncludeWhen == null ? Expression.TRUE :
BooleanExpressionParser.parse(attrIncludeWhen, tagValueProducer,
featurePostMatchContext),
attrExcludeWhen == null ? Expression.TRUE :
not(BooleanExpressionParser.parse(attrExcludeWhen, tagValueProducer,
featurePostMatchContext))
).simplify();
var minTileCoverage = attrIncludeWhen == null ? null : attribute.minTileCoverSize();
Function<Contexts.FeatureAttribute, Integer> attributeZoomProducer =
attributeZoomThreshold(minTileCoverage, attributeMinZoom, minZoomByValue);
return (context, f) -> {
Object value = null;
if (attributeTest.evaluate(context)) {
value = attributeValueProducer.apply(context);
if ("".equals(value)) {
value = null;
}
}
if (value == null) {
value = fallback;
}
if (value != null) {
if (attributeZoomProducer != null) {
Integer minzoom = attributeZoomProducer.apply(context.createAttrZoomContext(value));
if (minzoom != null) {
f.setAttrWithMinzoom(tagKey, value, minzoom);
} else {
f.setAttr(tagKey, value);
}
} else {
f.setAttr(tagKey, value);
}
}
};
}
/**
* Returns an expression that evaluates to true if a source feature should be included in the output.
*/
public Expression matchExpression() {
return Expression.and(geometryTest, tagTest);
}
/**
* Generates a tile feature based on a source feature.
*
* @param context The evaluation context containing the source feature
* @param features output rendered feature collector
*/
public void processFeature(Contexts.FeaturePostMatch context, FeatureCollector features) {
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());
var f = geometryFactory.apply(features);
for (var processor : featureProcessors) {
processor.accept(context, f);
}
}
}