kopia lustrzana https://github.com/onthegomap/planetiler
270 wiersze
10 KiB
Java
270 wiersze
10 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.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;
|
|
|
|
|
|
public ConfiguredFeature(String layer, TagValueProducer tagValueProducer, FeatureItem feature) {
|
|
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;
|
|
|
|
//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, Contexts.ProcessFeature.DESCRIPTION);
|
|
}
|
|
if (feature.excludeWhen() != null) {
|
|
filter = Expression.and(
|
|
filter,
|
|
Expression.not(
|
|
BooleanExpressionParser.parse(feature.excludeWhen(), tagValueProducer, Contexts.ProcessFeature.DESCRIPTION))
|
|
);
|
|
}
|
|
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,
|
|
Contexts.FeaturePostMatch.DESCRIPTION,
|
|
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 {
|
|
value.put("tag_value", attribute.key());
|
|
}
|
|
}
|
|
|
|
return ConfigExpressionParser.parse(value, tagValueProducer, Contexts.FeaturePostMatch.DESCRIPTION, 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,
|
|
Contexts.FeatureAttribute.DESCRIPTION, 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, Contexts.FeaturePostMatch.DESCRIPTION),
|
|
attrExcludeWhen == null ? Expression.TRUE :
|
|
not(BooleanExpressionParser.parse(attrExcludeWhen, tagValueProducer, Contexts.FeaturePostMatch.DESCRIPTION))
|
|
).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);
|
|
}
|
|
}
|
|
}
|