kopia lustrzana https://github.com/onthegomap/planetiler
450 wiersze
17 KiB
Java
450 wiersze
17 KiB
Java
package com.onthegomap.planetiler.custommap;
|
|
|
|
import com.google.api.expr.v1alpha1.Constant;
|
|
import com.google.api.expr.v1alpha1.Decl;
|
|
import com.google.api.expr.v1alpha1.Type;
|
|
import com.google.protobuf.NullValue;
|
|
import com.onthegomap.planetiler.config.Arguments;
|
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|
import com.onthegomap.planetiler.custommap.expression.ParseException;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptContext;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptEnvironment;
|
|
import com.onthegomap.planetiler.expression.DataType;
|
|
import com.onthegomap.planetiler.reader.SourceFeature;
|
|
import com.onthegomap.planetiler.reader.WithGeometryType;
|
|
import com.onthegomap.planetiler.reader.WithTags;
|
|
import com.onthegomap.planetiler.util.Try;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.stream.Collectors;
|
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
import org.projectnessie.cel.checker.Decls;
|
|
import org.projectnessie.cel.common.types.NullT;
|
|
import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry;
|
|
import org.projectnessie.cel.common.types.ref.TypeAdapter;
|
|
import org.projectnessie.cel.common.types.ref.Val;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
/**
|
|
* Wrapper objects that provide all available inputs to different parts of planetiler schema configs at runtime.
|
|
* <p>
|
|
* Contexts provide inputs to java code, and also global variable definitions to CEL expressions. Contexts are nested so
|
|
* that all global variables from a parent context are also available to its child context.
|
|
*/
|
|
public class Contexts {
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(Contexts.class);
|
|
|
|
private static Object wrapNullable(Object nullable) {
|
|
return nullable == null ? NullT.NullValue : nullable;
|
|
}
|
|
|
|
public static Root emptyRoot() {
|
|
return new Root(Arguments.of().silence(), Map.of());
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link Root} context built from {@code schemaArgs} argument definitions and {@code origArguments}
|
|
* arguments provided from the command-line/environment.
|
|
* <p>
|
|
* Arguments may depend on the value of other arguments so this iteratively evaluates the arguments until their values
|
|
* settle.
|
|
*
|
|
* @throws ParseException if the argument definitions are malformed, or if there's an infinite loop
|
|
*/
|
|
public static Contexts.Root buildRootContext(Arguments origArguments, Map<String, Object> schemaArgs) {
|
|
boolean loggingEnabled = !origArguments.silenced();
|
|
origArguments.silence();
|
|
Map<String, String> argDescriptions = new LinkedHashMap<>();
|
|
Map<String, Object> unparsedSchemaArgs = new HashMap<>(schemaArgs);
|
|
Map<String, Object> parsedSchemaArgs = new HashMap<>(origArguments.toMap());
|
|
Contexts.Root result = new Root(origArguments, parsedSchemaArgs);
|
|
Arguments arguments = origArguments;
|
|
int iters = 0;
|
|
// arguments may reference the value of other arguments, so continue parsing until they all succeed...
|
|
while (!unparsedSchemaArgs.isEmpty()) {
|
|
final var root = result;
|
|
final var args = arguments;
|
|
Map<String, Exception> failures = new HashMap<>();
|
|
|
|
Map.copyOf(unparsedSchemaArgs).forEach((key, value) -> {
|
|
boolean builtin = root.builtInArgs.contains(key);
|
|
String description;
|
|
Object defaultValueObject;
|
|
DataType type = null;
|
|
if (value instanceof Map<?, ?> map) {
|
|
if (builtin) {
|
|
throw new ParseException("Cannot override built-in argument: " + key);
|
|
}
|
|
var typeObject = map.get("type");
|
|
if (typeObject != null) {
|
|
type = DataType.from(Objects.toString(typeObject));
|
|
}
|
|
var descriptionObject = map.get("description");
|
|
description = descriptionObject == null ? "no description provided" : descriptionObject.toString();
|
|
defaultValueObject = map.get("default");
|
|
if (type != null) {
|
|
var fromArgs = args.getString(key, description, null);
|
|
if (fromArgs != null) {
|
|
parsedSchemaArgs.put(key, type.convertFrom(fromArgs));
|
|
}
|
|
}
|
|
} else {
|
|
defaultValueObject = value;
|
|
description = "no description provided";
|
|
}
|
|
argDescriptions.put(key, description);
|
|
Try<Object> defaultValue = ConfigExpressionParser.tryStaticEvaluate(root, defaultValueObject, Object.class);
|
|
if (defaultValue.isSuccess()) {
|
|
Object raw = defaultValue.get();
|
|
String asString = Objects.toString(raw);
|
|
if (type == null) {
|
|
type = DataType.typeOf(raw);
|
|
}
|
|
var stringResult = args.getString(key, description, asString);
|
|
Object castedResult = type.convertFrom(stringResult);
|
|
if (stringResult == null) {
|
|
throw new ParseException("Missing required parameter: " + key + "(" + description + ")");
|
|
} else if (castedResult == null) {
|
|
throw new ParseException("Cannot convert value for " + key + " to " + type.id() + ": " + stringResult);
|
|
}
|
|
parsedSchemaArgs.put(key, castedResult);
|
|
unparsedSchemaArgs.remove(key);
|
|
} else {
|
|
failures.put(key, defaultValue.exception());
|
|
}
|
|
});
|
|
|
|
arguments = origArguments.orElse(Arguments.of(parsedSchemaArgs.entrySet().stream().collect(Collectors.toMap(
|
|
Map.Entry::getKey,
|
|
e -> Objects.toString(e.getValue()))
|
|
)));
|
|
result = new Root(arguments, parsedSchemaArgs);
|
|
if (iters++ > 100) {
|
|
failures
|
|
.forEach(
|
|
(key, failure) -> LOGGER.error("Error computing {}:\n{}", key,
|
|
ExceptionUtils.getRootCause(failure).toString().indent(4)));
|
|
throw new ParseException("Infinite loop while processing arguments: " + unparsedSchemaArgs.keySet());
|
|
}
|
|
}
|
|
var finalArguments = loggingEnabled ? arguments.withExactlyOnceLogging() : arguments.silence();
|
|
if (loggingEnabled) {
|
|
argDescriptions.forEach((key, description) -> finalArguments.getString(key, description, null));
|
|
}
|
|
return new Root(finalArguments, parsedSchemaArgs);
|
|
}
|
|
|
|
/**
|
|
* Root context available everywhere in a planetiler schema config.
|
|
* <p>
|
|
* Holds argument values parsed from the schema config and command-line args.
|
|
*/
|
|
public static final class Root implements ScriptContext {
|
|
private static final TypeAdapter TYPE_ADAPTER = ProtoTypeRegistry.newRegistry();
|
|
private final Arguments arguments;
|
|
private final PlanetilerConfig config;
|
|
private final ScriptEnvironment<Root> description;
|
|
private final Map<String, Val> bindings = new HashMap<>();
|
|
private final Map<String, Object> argumentValues = new HashMap<>();
|
|
public final Set<String> builtInArgs;
|
|
|
|
public Arguments arguments() {
|
|
return arguments;
|
|
}
|
|
|
|
public PlanetilerConfig config() {
|
|
return config;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
return this == o || (o instanceof Root root && argumentValues.equals(root.argumentValues));
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(argumentValues);
|
|
}
|
|
|
|
@Override
|
|
public Object argument(String key) {
|
|
return argumentValues.get(key);
|
|
}
|
|
|
|
private Root(Arguments arguments, Map<String, Object> schemaArgs) {
|
|
this.arguments = arguments;
|
|
this.config = PlanetilerConfig.from(arguments);
|
|
argumentValues.put("threads", config.threads());
|
|
argumentValues.put("write_threads", config.featureWriteThreads());
|
|
argumentValues.put("process_threads", config.featureProcessThreads());
|
|
argumentValues.put("feature_read_threads", config.featureReadThreads());
|
|
// args.put("loginterval", config.logInterval());
|
|
argumentValues.put("minzoom", config.minzoom());
|
|
argumentValues.put("maxzoom", config.maxzoom());
|
|
argumentValues.put("render_maxzoom", config.maxzoomForRendering());
|
|
argumentValues.put("force", config.force());
|
|
argumentValues.put("gzip_temp", config.gzipTempStorage());
|
|
argumentValues.put("mmap_temp", config.mmapTempStorage());
|
|
argumentValues.put("sort_max_readers", config.sortMaxReaders());
|
|
argumentValues.put("sort_max_writers", config.sortMaxWriters());
|
|
argumentValues.put("nodemap_type", config.nodeMapType());
|
|
argumentValues.put("nodemap_storage", config.nodeMapStorage());
|
|
argumentValues.put("nodemap_madvise", config.nodeMapMadvise());
|
|
argumentValues.put("multipolygon_geometry_storage", config.multipolygonGeometryStorage());
|
|
argumentValues.put("multipolygon_geometry_madvise", config.multipolygonGeometryMadvise());
|
|
argumentValues.put("http_user_agent", config.httpUserAgent());
|
|
// args.put("http_timeout", config.httpTimeout());
|
|
argumentValues.put("http_retries", config.httpRetries());
|
|
argumentValues.put("download_chunk_size_mb", config.downloadChunkSizeMB());
|
|
argumentValues.put("download_threads", config.downloadThreads());
|
|
argumentValues.put("download_max_bandwidth", config.downloadMaxBandwidth());
|
|
argumentValues.put("min_feature_size_at_max_zoom", config.minFeatureSizeAtMaxZoom());
|
|
argumentValues.put("min_feature_size", config.minFeatureSizeBelowMaxZoom());
|
|
argumentValues.put("simplify_tolerance_at_max_zoom", config.simplifyToleranceAtMaxZoom());
|
|
argumentValues.put("simplify_tolerance", config.simplifyToleranceBelowMaxZoom());
|
|
argumentValues.put("skip_filled_tiles", config.skipFilledTiles());
|
|
argumentValues.put("tile_warning_size_mb", config.tileWarningSizeBytes());
|
|
builtInArgs = Set.copyOf(argumentValues.keySet());
|
|
|
|
schemaArgs.forEach(argumentValues::putIfAbsent);
|
|
config.arguments().toMap().forEach(argumentValues::putIfAbsent);
|
|
|
|
argumentValues.forEach((k, v) -> bindings.put("args." + k, TYPE_ADAPTER.nativeToValue(v)));
|
|
bindings.put("args", TYPE_ADAPTER.nativeToValue(this.argumentValues));
|
|
|
|
description = ScriptEnvironment.root(this).forInput(Root.class)
|
|
.withDeclarations(
|
|
argumentValues.entrySet().stream()
|
|
.map(entry -> decl(entry.getKey(), entry.getValue()))
|
|
.toList()
|
|
).withDeclarations(
|
|
Decls.newVar("args", Decls.newMapType(Decls.String, Decls.Any))
|
|
);
|
|
}
|
|
|
|
private Decl decl(String name, Object value) {
|
|
Type type;
|
|
var builder = Constant.newBuilder();
|
|
if (value instanceof String s) {
|
|
builder.setStringValue(s);
|
|
type = Decls.String;
|
|
} else if (value instanceof Boolean b) {
|
|
builder.setBoolValue(b);
|
|
type = Decls.Bool;
|
|
} else if (value instanceof Long || value instanceof Integer) {
|
|
builder.setInt64Value(((Number) value).longValue());
|
|
type = Decls.Int;
|
|
} else if (value instanceof Double || value instanceof Float) {
|
|
builder.setDoubleValue(((Number) value).doubleValue());
|
|
type = Decls.Double;
|
|
} else if (value == null) {
|
|
builder.setNullValue(NullValue.NULL_VALUE);
|
|
type = Decls.Null;
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Unrecognized constant type: " + value + " (" + value.getClass().getName() + ")");
|
|
}
|
|
return Decls.newConst("args." + name, type, builder.build());
|
|
}
|
|
|
|
public ScriptEnvironment<Root> description() {
|
|
return description;
|
|
}
|
|
|
|
@Override
|
|
public Object apply(String input) {
|
|
return bindings.get(input);
|
|
}
|
|
|
|
public ProcessFeature createProcessFeatureContext(SourceFeature sourceFeature, TagValueProducer tagValueProducer) {
|
|
return new ProcessFeature(this, sourceFeature, tagValueProducer);
|
|
}
|
|
}
|
|
|
|
private interface NestedContext extends ScriptContext {
|
|
|
|
default Root root() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
default Object argument(String key) {
|
|
return root().argument(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 {
|
|
|
|
default FeatureContext parent() {
|
|
return null;
|
|
}
|
|
|
|
default SourceFeature feature() {
|
|
return parent().feature();
|
|
}
|
|
|
|
@Override
|
|
default Root root() {
|
|
return parent().root();
|
|
}
|
|
|
|
@Override
|
|
default Map<String, Object> tags() {
|
|
return feature().tags();
|
|
}
|
|
|
|
@Override
|
|
default TagValueProducer tagValueProducer() {
|
|
var parent = parent();
|
|
return parent == null ? TagValueProducer.EMPTY : parent.tagValueProducer();
|
|
}
|
|
|
|
@Override
|
|
default boolean isPoint() {
|
|
return feature().isPoint();
|
|
}
|
|
|
|
@Override
|
|
default boolean canBeLine() {
|
|
return feature().canBeLine();
|
|
}
|
|
|
|
@Override
|
|
default boolean canBePolygon() {
|
|
return feature().canBePolygon();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Context available when processing an input feature.
|
|
*
|
|
* @param feature The input feature being processed
|
|
* @param tagValueProducer Common parsing for input feature tags
|
|
*/
|
|
public record ProcessFeature(
|
|
@Override Root root, @Override SourceFeature feature,
|
|
@Override TagValueProducer tagValueProducer
|
|
)
|
|
implements FeatureContext {
|
|
|
|
private static final String FEATURE_TAGS = "feature.tags";
|
|
private static final String FEATURE_ID = "feature.id";
|
|
private static final String FEATURE_SOURCE = "feature.source";
|
|
private static final String FEATURE_SOURCE_LAYER = "feature.source_layer";
|
|
|
|
public static ScriptEnvironment<ProcessFeature> description(Root root) {
|
|
return root.description()
|
|
.forInput(ProcessFeature.class)
|
|
.withDeclarations(
|
|
Decls.newVar(FEATURE_TAGS, Decls.newMapType(Decls.String, Decls.Any)),
|
|
Decls.newVar(FEATURE_ID, Decls.Int),
|
|
Decls.newVar(FEATURE_SOURCE, Decls.String),
|
|
Decls.newVar(FEATURE_SOURCE_LAYER, Decls.String)
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public Object apply(String key) {
|
|
if (key != null) {
|
|
return switch (key) {
|
|
case FEATURE_TAGS -> tagValueProducer.mapTags(feature);
|
|
case FEATURE_ID -> feature.id();
|
|
case FEATURE_SOURCE -> feature.getSource();
|
|
case FEATURE_SOURCE_LAYER -> wrapNullable(feature.getSourceLayer());
|
|
default -> null;
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public FeaturePostMatch createPostMatchContext(List<String> matchKeys) {
|
|
return new FeaturePostMatch(this, matchKeys);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Context available after a feature has been matched.
|
|
* <p>
|
|
* Adds {@code match_key} and {@code match_value} variables that capture which tag key/value caused the feature to be
|
|
* included.
|
|
*
|
|
* @param parent The parent context
|
|
* @param matchKeys Keys that triggered the match
|
|
*/
|
|
public record FeaturePostMatch(@Override ProcessFeature parent, List<String> matchKeys) implements FeatureContext {
|
|
|
|
private static final String MATCH_KEY = "match_key";
|
|
private static final String MATCH_VALUE = "match_value";
|
|
|
|
public static ScriptEnvironment<FeaturePostMatch> description(Root root) {
|
|
return ProcessFeature.description(root)
|
|
.forInput(FeaturePostMatch.class)
|
|
.withDeclarations(
|
|
Decls.newVar(MATCH_KEY, Decls.String),
|
|
Decls.newVar(MATCH_VALUE, Decls.Any)
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public Object apply(String key) {
|
|
if (key != null) {
|
|
return switch (key) {
|
|
case MATCH_KEY -> wrapNullable(matchKey());
|
|
case MATCH_VALUE -> wrapNullable(matchValue());
|
|
default -> parent.apply(key);
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public String matchKey() {
|
|
return matchKeys().isEmpty() ? null : matchKeys().get(0);
|
|
}
|
|
|
|
public Object matchValue() {
|
|
String matchKey = matchKey();
|
|
return matchKey == null ? null : parent.tagValueProducer.valueForKey(parent().feature(), matchKey);
|
|
}
|
|
|
|
public FeatureAttribute createAttrZoomContext(Object value) {
|
|
return new FeatureAttribute(this, value);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Context available when configuring an attribute on an output feature after its value has been assigned (for example
|
|
* setting min/max zoom).
|
|
*
|
|
* @param parent The parent context
|
|
* @param value Value of the attribute
|
|
*/
|
|
public record FeatureAttribute(@Override FeaturePostMatch parent, Object value) implements FeatureContext {
|
|
|
|
private static final String VALUE = "value";
|
|
|
|
public static ScriptEnvironment<FeatureAttribute> description(Root root) {
|
|
return FeaturePostMatch.description(root)
|
|
.forInput(FeatureAttribute.class)
|
|
.withDeclarations(Decls.newVar(VALUE, Decls.Any));
|
|
}
|
|
|
|
@Override
|
|
public Object apply(String key) {
|
|
return VALUE.equals(key) ? wrapNullable(value) : parent.apply(key);
|
|
}
|
|
}
|
|
}
|