kopia lustrzana https://github.com/onthegomap/planetiler
lua profiles
rodzic
ed4c320e49
commit
a9467840f6
|
@ -34,6 +34,11 @@
|
|||
<option name="path" value="planetiler-custommap" />
|
||||
<option name="mappingKind" value="Directory" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="directory" value="true" />
|
||||
<option name="path" value="planetiler-experimental" />
|
||||
<option name="mappingKind" value="Directory" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
"java.sources.organizeImports.starThreshold": 999,
|
||||
"java.saveActions.organizeImports": true,
|
||||
"yaml.schemas": {
|
||||
"./planetiler-custommap/planetiler.schema.json": "planetiler-custommap/**/*.yml"
|
||||
"./planetiler-custommap/planetiler.schema.json": [
|
||||
"planetiler-custommap/**/*.y*ml",
|
||||
"planetiler-experimental/**/*.y*ml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ Planetiler licensed under the Apache license, Version 2.0
|
|||
|
||||
Copyright 2021 Michael Barry and Planetiler Contributors.
|
||||
|
||||
The `planetiler-core` module includes the following software:
|
||||
Planetiler includes the following software:
|
||||
|
||||
- Maven Dependencies:
|
||||
- Jackson for JSON/XML handling (Apache license)
|
||||
|
@ -29,6 +29,8 @@ The `planetiler-core` module includes the following software:
|
|||
- org.snakeyaml:snakeyaml-engine (Apache license)
|
||||
- org.commonmark:commonmark (BSD 2-clause license)
|
||||
- org.tukaani:xz (public domain)
|
||||
- org.luaj:luaj-jse (MIT license)
|
||||
- org.apache.bcel:bcel (Apache license)
|
||||
- Adapted code:
|
||||
- `DouglasPeuckerSimplifier` from [JTS](https://github.com/locationtech/jts) (EDL)
|
||||
- `OsmMultipolygon` from [imposm3](https://github.com/omniscale/imposm3) (Apache license)
|
||||
|
@ -48,6 +50,7 @@ The `planetiler-core` module includes the following software:
|
|||
- `SeekableInMemoryByteChannel`
|
||||
from [Apache Commons compress](https://commons.apache.org/proper/commons-compress/apidocs/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.html) (
|
||||
Apache License)
|
||||
- Several classes in `org.luaj.vm2.*` from [luaj](https://github.com/luaj/luaj) (MIT License)
|
||||
- [`planetiler-openmaptiles`](https://github.com/openmaptiles/planetiler-openmaptiles) submodule (BSD 3-Clause License)
|
||||
- Schema
|
||||
- The cartography and visual design features of the map tile schema are licensed
|
||||
|
@ -58,7 +61,7 @@ The `planetiler-core` module includes the following software:
|
|||
|
||||
## Data
|
||||
|
||||
| source | license | used as default | included in repo |
|
||||
| source | license | used as default | included in repo |
|
||||
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|-----------------|------------------|
|
||||
| OpenStreetMap (OSM) data | [ODBL](https://www.openstreetmap.org/copyright) | yes | yes |
|
||||
| Natural Earth | [public domain](https://www.naturalearthdata.com/about/terms-of-use/) | yes | yes |
|
||||
|
|
|
@ -154,6 +154,10 @@
|
|||
<artifactId>geopackage</artifactId>
|
||||
<version>${geopackage.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.snakeyaml</groupId>
|
||||
<artifactId>snakeyaml-engine</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -491,6 +491,10 @@ public class Planetiler {
|
|||
return this;
|
||||
}
|
||||
|
||||
public List<String> getDefaultLanguages() {
|
||||
return languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@link #translations()} to use name translations fetched from wikidata based on the
|
||||
* <a href="https://www.wikidata.org/wiki/Wikidata:OpenStreetMap">wikidata tag</a> on OSM elements.
|
||||
|
|
|
@ -426,13 +426,17 @@ public class Arguments {
|
|||
public Stats getStats() {
|
||||
String prometheus = getArg("pushgateway");
|
||||
if (prometheus != null && !prometheus.isBlank()) {
|
||||
LOGGER.info("argument: stats=use prometheus push gateway stats");
|
||||
if (!silent) {
|
||||
LOGGER.info("argument: stats=use prometheus push gateway stats");
|
||||
}
|
||||
String job = getString("pushgateway.job", "prometheus pushgateway job ID", "planetiler");
|
||||
Duration interval = getDuration("pushgateway.interval", "how often to send stats to prometheus push gateway",
|
||||
"15s");
|
||||
return Stats.prometheusPushGateway(prometheus, job, interval);
|
||||
} else {
|
||||
LOGGER.info("argument: stats=use in-memory stats");
|
||||
if (!silent) {
|
||||
LOGGER.info("argument: stats=use in-memory stats");
|
||||
}
|
||||
return Stats.inMemory();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
package com.onthegomap.planetiler.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
|
@ -0,0 +1,231 @@
|
|||
package com.onthegomap.planetiler.validator;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.AnsiColors;
|
||||
import com.onthegomap.planetiler.util.FileWatcher;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import com.onthegomap.planetiler.util.Try;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.locationtech.jts.io.WKTReader;
|
||||
|
||||
/**
|
||||
* Verifies that a profile maps input elements map to expected output vector tile features as defined by a
|
||||
* {@link SchemaSpecification} instance.
|
||||
*/
|
||||
public abstract class BaseSchemaValidator {
|
||||
|
||||
private static final String PASS_BADGE = AnsiColors.greenBackground(" PASS ");
|
||||
private static final String FAIL_BADGE = AnsiColors.redBackground(" FAIL ");
|
||||
protected final Arguments args;
|
||||
protected final PrintStream output;
|
||||
|
||||
protected BaseSchemaValidator(Arguments args, PrintStream output) {
|
||||
this.args = args;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
protected static boolean hasCause(Throwable t, Class<?> cause) {
|
||||
return t != null && (cause.isInstance(t) || hasCause(t.getCause(), cause));
|
||||
}
|
||||
|
||||
protected void runOrWatch() {
|
||||
var watch =
|
||||
args.getBoolean("watch", "Watch files for changes and re-run validation when schema or spec changes", false);
|
||||
|
||||
output.println("OK");
|
||||
var paths = validateFromCli();
|
||||
|
||||
if (watch) {
|
||||
output.println();
|
||||
output.println("Watching filesystem for changes...");
|
||||
var watcher = FileWatcher.newWatcher(paths.toArray(Path[]::new));
|
||||
watcher.pollForChanges(Duration.ofMillis(300), changed -> validateFromCli());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Path> validateFromCli() {
|
||||
Set<Path> pathsToWatch = ConcurrentHashMap.newKeySet();
|
||||
output.println();
|
||||
output.println("Validating...");
|
||||
output.println();
|
||||
BaseSchemaValidator.Result result;
|
||||
result = validate(pathsToWatch);
|
||||
if (result != null) {
|
||||
|
||||
int failed = 0, passed = 0;
|
||||
List<ExampleResult> failures = new ArrayList<>();
|
||||
for (var example : result.results) {
|
||||
if (example.ok()) {
|
||||
passed++;
|
||||
output.printf("%s %s%n", PASS_BADGE, example.example().name());
|
||||
} else {
|
||||
failed++;
|
||||
printFailure(example, output);
|
||||
failures.add(example);
|
||||
}
|
||||
}
|
||||
if (!failures.isEmpty()) {
|
||||
output.println();
|
||||
output.println("Summary of failures:");
|
||||
for (var failure : failures) {
|
||||
printFailure(failure, output);
|
||||
}
|
||||
}
|
||||
List<String> summary = new ArrayList<>();
|
||||
boolean none = (passed + failed) == 0;
|
||||
if (none || failed > 0) {
|
||||
summary.add(AnsiColors.redBold(failed + " failed"));
|
||||
}
|
||||
if (none || passed > 0) {
|
||||
summary.add(AnsiColors.greenBold(passed + " passed"));
|
||||
}
|
||||
if (none || passed > 0 && failed > 0) {
|
||||
summary.add((failed + passed) + " total");
|
||||
}
|
||||
output.println();
|
||||
output.println(String.join(", ", summary));
|
||||
}
|
||||
return pathsToWatch;
|
||||
}
|
||||
|
||||
protected abstract Result validate(Set<Path> pathsToWatch);
|
||||
|
||||
private static void printFailure(ExampleResult example, PrintStream output) {
|
||||
output.printf("%s %s%n", FAIL_BADGE, example.example().name());
|
||||
if (example.issues.isFailure()) {
|
||||
output.println(ExceptionUtils.getStackTrace(example.issues.exception()).indent(4).stripTrailing());
|
||||
} else {
|
||||
for (var issue : example.issues().get()) {
|
||||
output.println(" ● " + issue.indent(4).strip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Geometry parseGeometry(String geometry) {
|
||||
String wkt = switch (geometry.toLowerCase(Locale.ROOT).trim()) {
|
||||
case "point" -> "POINT (0 0)";
|
||||
case "line" -> "LINESTRING (0 0, 1 1)";
|
||||
case "polygon" -> "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))";
|
||||
default -> geometry;
|
||||
};
|
||||
try {
|
||||
return new WKTReader().read(wkt);
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException("""
|
||||
Bad geometry: "%s", must be "point" "line" "polygon" or a valid WKT string.
|
||||
""".formatted(geometry));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the result of validating {@code profile} against the examples in {@code specification}. */
|
||||
public static Result validate(Profile profile, SchemaSpecification specification, PlanetilerConfig config) {
|
||||
var featureCollectorFactory = new FeatureCollector.Factory(config, Stats.inMemory());
|
||||
return new Result(specification.examples().stream().map(example -> new ExampleResult(example, Try.apply(() -> {
|
||||
List<String> issues = new ArrayList<>();
|
||||
var input = example.input();
|
||||
var expectedFeatures = example.output();
|
||||
var geometry = parseGeometry(input.geometry());
|
||||
var feature = SimpleFeature.create(geometry, input.tags(), input.source(), null, 0);
|
||||
var collector = featureCollectorFactory.get(feature);
|
||||
profile.processFeature(feature, collector);
|
||||
List<FeatureCollector.Feature> result = new ArrayList<>();
|
||||
collector.forEach(result::add);
|
||||
if (result.size() != expectedFeatures.size()) {
|
||||
issues.add(
|
||||
"Different number of elements, expected=%s actual=%s".formatted(expectedFeatures.size(), result.size()));
|
||||
} else {
|
||||
// TODO print a diff of the input and output feature YAML representations
|
||||
for (int i = 0; i < expectedFeatures.size(); i++) {
|
||||
var expected = expectedFeatures.get(i);
|
||||
var actual = result.stream().max(proximityTo(expected)).orElseThrow();
|
||||
result.remove(actual);
|
||||
var actualTags = actual.getAttrsAtZoom(expected.atZoom());
|
||||
String prefix = "feature[%d]".formatted(i);
|
||||
validate(prefix + ".layer", issues, expected.layer(), actual.getLayer());
|
||||
validate(prefix + ".minzoom", issues, expected.minZoom(), actual.getMinZoom());
|
||||
validate(prefix + ".maxzoom", issues, expected.maxZoom(), actual.getMaxZoom());
|
||||
validate(prefix + ".minsize", issues, expected.minSize(), actual.getMinPixelSizeAtZoom(expected.atZoom()));
|
||||
validate(prefix + ".geometry", issues, expected.geometry(), GeometryType.typeOf(actual.getGeometry()));
|
||||
Set<String> tags = new TreeSet<>(actualTags.keySet());
|
||||
expected.tags().forEach((tag, value) -> {
|
||||
validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, value, actualTags.get(tag), false);
|
||||
tags.remove(tag);
|
||||
});
|
||||
if (Boolean.FALSE.equals(expected.allowExtraTags())) {
|
||||
for (var tag : tags) {
|
||||
validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, null, actualTags.get(tag), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}))).toList());
|
||||
}
|
||||
|
||||
private static Comparator<FeatureCollector.Feature> proximityTo(SchemaSpecification.OutputFeature expected) {
|
||||
return Comparator.comparingInt(item -> (Objects.equals(item.getLayer(), expected.layer()) ? 2 : 0) +
|
||||
(Objects.equals(GeometryType.typeOf(item.getGeometry()), expected.geometry()) ? 1 : 0));
|
||||
}
|
||||
|
||||
private static <T> void validate(String field, List<String> issues, T expected, T actual, boolean ignoreWhenNull) {
|
||||
if ((!ignoreWhenNull || expected != null) && !Objects.equals(expected, actual)) {
|
||||
// handle when expected and actual are int/long or long/int
|
||||
if (expected instanceof Number && actual instanceof Number && expected.toString().equals(actual.toString())) {
|
||||
return;
|
||||
}
|
||||
issues.add("%s: expected <%s> actual <%s>".formatted(field, format(expected), format(actual)));
|
||||
}
|
||||
}
|
||||
|
||||
private static String format(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
} else if (o instanceof String s) {
|
||||
return Format.quote(s);
|
||||
} else {
|
||||
return o.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void validate(String field, List<String> issues, T expected, T actual) {
|
||||
validate(field, issues, expected, actual, true);
|
||||
}
|
||||
|
||||
/** Result of comparing the output vector tile feature to what was expected. */
|
||||
public record ExampleResult(
|
||||
SchemaSpecification.Example example,
|
||||
// TODO include a symmetric diff so we can pretty-print the expected/actual output diff
|
||||
Try<List<String>> issues
|
||||
) {
|
||||
|
||||
public boolean ok() {
|
||||
return issues.isSuccess() && issues.get().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(List<ExampleResult> results) {
|
||||
|
||||
public boolean ok() {
|
||||
return results.stream().allMatch(ExampleResult::ok);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,21 @@
|
|||
package com.onthegomap.planetiler.custommap.validator;
|
||||
package com.onthegomap.planetiler.validator;
|
||||
|
||||
import static com.onthegomap.planetiler.config.PlanetilerConfig.MAX_MAXZOOM;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.onthegomap.planetiler.custommap.YAML;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** A model of example input source features and expected output vector tile features that a schema should produce. */
|
||||
/**
|
||||
* A model of example input source features and expected output vector tile features that a schema should produce.
|
||||
* <p>
|
||||
* Executed by a subclass of {@link BaseSchemaValidator}.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record SchemaSpecification(List<Example> examples) {
|
||||
|
|
@ -7,6 +7,7 @@ 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 static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
@ -26,6 +27,8 @@ import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
|||
import com.onthegomap.planetiler.mbtiles.Verify;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.validator.BaseSchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
|
@ -47,7 +50,9 @@ import java.util.TreeMap;
|
|||
import java.util.TreeSet;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.locationtech.jts.algorithm.Orientation;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
|
@ -340,6 +345,20 @@ public class TestUtils {
|
|||
return path;
|
||||
}
|
||||
|
||||
public static Stream<DynamicNode> validateProfile(Profile profile, String spec) {
|
||||
return validateProfile(profile, SchemaSpecification.load(spec));
|
||||
}
|
||||
|
||||
public static Stream<DynamicNode> validateProfile(Profile profile, SchemaSpecification spec) {
|
||||
var result = BaseSchemaValidator.validate(profile, spec, PlanetilerConfig.defaults());
|
||||
return result.results().stream().map(test -> dynamicTest(test.example().name(), () -> {
|
||||
var issues = test.issues().get();
|
||||
if (!issues.isEmpty()) {
|
||||
fail("Failed with " + issues.size() + " issues:\n" + String.join("\n", issues));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public interface GeometryComparision {
|
||||
|
||||
Geometry geom();
|
||||
|
@ -632,7 +651,7 @@ public class TestUtils {
|
|||
try {
|
||||
int num = Verify.getNumFeatures(db, layer, zoom, attrs, envelope, clazz);
|
||||
|
||||
assertTrue(expected < num,
|
||||
assertTrue(expected <= num,
|
||||
"z%d features in %s, expected at least %d got %d".formatted(zoom, layer, expected, num));
|
||||
} catch (GeometryException e) {
|
||||
fail(e);
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
package com.onthegomap.planetiler.validator;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.TestUtils;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
class BaseSchemaValidatorTest {
|
||||
|
||||
private final String goodSpecString = """
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
- layer: water
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
""";
|
||||
private final SchemaSpecification goodSpec = SchemaSpecification.load(goodSpecString);
|
||||
|
||||
private final Profile waterSchema = new Profile() {
|
||||
@Override
|
||||
public void processFeature(SourceFeature sourceFeature, FeatureCollector features) {
|
||||
if (sourceFeature.canBePolygon() && sourceFeature.hasTag("natural", "water")) {
|
||||
features.polygon("water")
|
||||
.setMinPixelSize(10)
|
||||
.inheritAttrFromSource("natural");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "test profile";
|
||||
}
|
||||
};
|
||||
|
||||
private Result validate(Profile profile, String spec) {
|
||||
var result = BaseSchemaValidator.validate(
|
||||
profile,
|
||||
SchemaSpecification.load(spec),
|
||||
PlanetilerConfig.defaults()
|
||||
);
|
||||
for (var example : result.results()) {
|
||||
if (example.issues().isFailure()) {
|
||||
assertNotNull(example.issues().get());
|
||||
}
|
||||
}
|
||||
// also exercise the cli writer and return what it would have printed to stdout
|
||||
var cliOutput = validateCli(profile, SchemaSpecification.load(spec));
|
||||
return new Result(result, cliOutput);
|
||||
}
|
||||
|
||||
private String validateCli(Profile profile, SchemaSpecification spec) {
|
||||
try (
|
||||
var baos = new ByteArrayOutputStream();
|
||||
var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8)
|
||||
) {
|
||||
new BaseSchemaValidator(Arguments.of(), printStream) {
|
||||
@Override
|
||||
protected Result validate(Set<Path> pathsToWatch) {
|
||||
return validate(profile, spec, PlanetilerConfig.from(args));
|
||||
}
|
||||
}.validateFromCli();
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException {
|
||||
return validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
layer: %s
|
||||
geometry: %s
|
||||
%s
|
||||
tags:
|
||||
%s
|
||||
""".formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
|
||||
tags == null ? "" : tags.indent(6).strip())
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"true,water,polygon,natural: water,",
|
||||
"true,water,polygon,,",
|
||||
"true,water,polygon,'natural: water\nother: null',",
|
||||
"false,water,polygon,natural: null,",
|
||||
"false,water2,polygon,natural: water,",
|
||||
"false,water,line,natural: water,",
|
||||
"false,water,line,natural: water,",
|
||||
"false,water,polygon,natural: water2,",
|
||||
"false,water,polygon,'natural: water\nother: value',",
|
||||
|
||||
"true,water,polygon,natural: water,allow_extra_tags: true",
|
||||
"true,water,polygon,natural: water,allow_extra_tags: false",
|
||||
"true,water,polygon,,allow_extra_tags: true",
|
||||
"false,water,polygon,,allow_extra_tags: false",
|
||||
|
||||
"true,water,polygon,,min_size: 10",
|
||||
"false,water,polygon,,min_size: 9",
|
||||
})
|
||||
void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags)
|
||||
throws IOException {
|
||||
var results = validateWater(layer, geometry, tags, allowExtraTags);
|
||||
assertEquals(1, results.output.results().size());
|
||||
assertEquals("test output", results.output.results().get(0).example().name());
|
||||
if (shouldBeOk) {
|
||||
assertTrue(results.output.ok(), results.toString());
|
||||
assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput);
|
||||
} else {
|
||||
assertFalse(results.output.ok(), "Expected an issue, but there were none");
|
||||
assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidationFailsWrongNumberOfFeatures() throws IOException {
|
||||
var results = validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
"""
|
||||
);
|
||||
assertFalse(results.output.ok(), results.toString());
|
||||
|
||||
results = validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
- layer: water
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
- layer: water2
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water2
|
||||
"""
|
||||
);
|
||||
assertFalse(results.output.ok(), results.toString());
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testJunitAdapterSpec() {
|
||||
return TestUtils.validateProfile(waterSchema, goodSpec);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testJunitAdapterString() {
|
||||
return TestUtils.validateProfile(waterSchema, goodSpecString);
|
||||
}
|
||||
|
||||
|
||||
record Result(BaseSchemaValidator.Result output, String cliOutput) {}
|
||||
}
|
|
@ -18,10 +18,6 @@
|
|||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.snakeyaml</groupId>
|
||||
<artifactId>snakeyaml-engine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.onthegomap.planetiler.config.Arguments;
|
|||
import com.onthegomap.planetiler.custommap.configschema.DataSourceType;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import com.onthegomap.planetiler.custommap.expression.ParseException;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.onthegomap.planetiler.custommap.YAML;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
|
|
@ -1,44 +1,31 @@
|
|||
package com.onthegomap.planetiler.custommap.validator;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.custommap.ConfiguredProfile;
|
||||
import com.onthegomap.planetiler.custommap.Contexts;
|
||||
import com.onthegomap.planetiler.custommap.YAML;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.AnsiColors;
|
||||
import com.onthegomap.planetiler.util.FileWatcher;
|
||||
import com.onthegomap.planetiler.util.Format;
|
||||
import com.onthegomap.planetiler.util.Try;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import com.onthegomap.planetiler.validator.BaseSchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.locationtech.jts.io.WKTReader;
|
||||
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
|
||||
|
||||
/** Verifies that a profile maps input elements map to expected output vector tile features. */
|
||||
public class SchemaValidator {
|
||||
public class SchemaValidator extends BaseSchemaValidator {
|
||||
|
||||
private static final String PASS_BADGE = AnsiColors.greenBackground(" PASS ");
|
||||
private static final String FAIL_BADGE = AnsiColors.redBackground(" FAIL ");
|
||||
private final Path schemaPath;
|
||||
|
||||
SchemaValidator(Arguments args, String schemaFile, PrintStream output) {
|
||||
super(args, output);
|
||||
schemaPath = schemaFile == null ? args.inputFile("schema", "Schema file") :
|
||||
args.inputFile("schema", "Schema file", Path.of(schemaFile));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// let users run `verify schema.yml` as a shortcut
|
||||
|
@ -48,36 +35,24 @@ public class SchemaValidator {
|
|||
args = Stream.of(args).skip(1).toArray(String[]::new);
|
||||
}
|
||||
var arguments = Arguments.fromEnvOrArgs(args);
|
||||
var schema = schemaFile == null ? arguments.inputFile("schema", "Schema file") :
|
||||
arguments.inputFile("schema", "Schema file", Path.of(schemaFile));
|
||||
var watch =
|
||||
arguments.getBoolean("watch", "Watch files for changes and re-run validation when schema or spec changes", false);
|
||||
|
||||
|
||||
PrintStream output = System.out;
|
||||
output.println("OK");
|
||||
var paths = validateFromCli(schema, output);
|
||||
|
||||
if (watch) {
|
||||
output.println();
|
||||
output.println("Watching filesystem for changes...");
|
||||
var watcher = FileWatcher.newWatcher(paths.toArray(Path[]::new));
|
||||
watcher.pollForChanges(Duration.ofMillis(300), changed -> validateFromCli(schema, output));
|
||||
}
|
||||
new SchemaValidator(arguments, schemaFile, System.out).runOrWatch();
|
||||
}
|
||||
|
||||
private static boolean hasCause(Throwable t, Class<?> cause) {
|
||||
return t != null && (cause.isInstance(t) || hasCause(t.getCause(), cause));
|
||||
|
||||
/**
|
||||
* Returns the result of validating the profile defined by {@code schema} against the examples in
|
||||
* {@code specification}.
|
||||
*/
|
||||
public static Result validate(SchemaConfig schema, SchemaSpecification specification) {
|
||||
var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
|
||||
return validate(new ConfiguredProfile(schema, context), specification, context.config());
|
||||
}
|
||||
|
||||
static Set<Path> validateFromCli(Path schemaPath, PrintStream output) {
|
||||
Set<Path> pathsToWatch = new HashSet<>();
|
||||
pathsToWatch.add(schemaPath);
|
||||
output.println();
|
||||
output.println("Validating...");
|
||||
output.println();
|
||||
SchemaValidator.Result result;
|
||||
@Override
|
||||
protected Result validate(Set<Path> pathsToWatch) {
|
||||
Result result = null;
|
||||
try {
|
||||
pathsToWatch.add(schemaPath);
|
||||
var schema = SchemaConfig.load(schemaPath);
|
||||
var examples = schema.examples();
|
||||
// examples can either be embedded in the yaml file, or referenced
|
||||
|
@ -108,169 +83,7 @@ public class SchemaValidator {
|
|||
String.join("\n", ExceptionUtils.getStackTrace(rootCause)))
|
||||
.indent(4));
|
||||
}
|
||||
return pathsToWatch;
|
||||
}
|
||||
int failed = 0, passed = 0;
|
||||
List<ExampleResult> failures = new ArrayList<>();
|
||||
for (var example : result.results) {
|
||||
if (example.ok()) {
|
||||
passed++;
|
||||
output.printf("%s %s%n", PASS_BADGE, example.example().name());
|
||||
} else {
|
||||
failed++;
|
||||
printFailure(example, output);
|
||||
failures.add(example);
|
||||
}
|
||||
}
|
||||
if (!failures.isEmpty()) {
|
||||
output.println();
|
||||
output.println("Summary of failures:");
|
||||
for (var failure : failures) {
|
||||
printFailure(failure, output);
|
||||
}
|
||||
}
|
||||
List<String> summary = new ArrayList<>();
|
||||
boolean none = (passed + failed) == 0;
|
||||
if (none || failed > 0) {
|
||||
summary.add(AnsiColors.redBold(failed + " failed"));
|
||||
}
|
||||
if (none || passed > 0) {
|
||||
summary.add(AnsiColors.greenBold(passed + " passed"));
|
||||
}
|
||||
if (none || passed > 0 && failed > 0) {
|
||||
summary.add((failed + passed) + " total");
|
||||
}
|
||||
output.println();
|
||||
output.println(String.join(", ", summary));
|
||||
return pathsToWatch;
|
||||
}
|
||||
|
||||
private static void printFailure(ExampleResult example, PrintStream output) {
|
||||
output.printf("%s %s%n", FAIL_BADGE, example.example().name());
|
||||
if (example.issues.isFailure()) {
|
||||
output.println(ExceptionUtils.getStackTrace(example.issues.exception()).indent(4).stripTrailing());
|
||||
} else {
|
||||
for (var issue : example.issues().get()) {
|
||||
output.println(" ● " + issue.indent(4).strip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Geometry parseGeometry(String geometry) {
|
||||
String wkt = switch (geometry.toLowerCase(Locale.ROOT).trim()) {
|
||||
case "point" -> "POINT (0 0)";
|
||||
case "line" -> "LINESTRING (0 0, 1 1)";
|
||||
case "polygon" -> "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))";
|
||||
default -> geometry;
|
||||
};
|
||||
try {
|
||||
return new WKTReader().read(wkt);
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException("""
|
||||
Bad geometry: "%s", must be "point" "line" "polygon" or a valid WKT string.
|
||||
""".formatted(geometry));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of validating the profile defined by {@code schema} against the examples in
|
||||
* {@code specification}.
|
||||
*/
|
||||
public static Result validate(SchemaConfig schema, SchemaSpecification specification) {
|
||||
var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
|
||||
return validate(new ConfiguredProfile(schema, context), specification, context.config());
|
||||
}
|
||||
|
||||
/** Returns the result of validating {@code profile} against the examples in {@code specification}. */
|
||||
public static Result validate(Profile profile, SchemaSpecification specification, PlanetilerConfig config) {
|
||||
var featureCollectorFactory = new FeatureCollector.Factory(config, Stats.inMemory());
|
||||
return new Result(specification.examples().stream().map(example -> new ExampleResult(example, Try.apply(() -> {
|
||||
List<String> issues = new ArrayList<>();
|
||||
var input = example.input();
|
||||
var expectedFeatures = example.output();
|
||||
var geometry = parseGeometry(input.geometry());
|
||||
var feature = SimpleFeature.create(geometry, input.tags(), input.source(), null, 0);
|
||||
var collector = featureCollectorFactory.get(feature);
|
||||
profile.processFeature(feature, collector);
|
||||
List<FeatureCollector.Feature> result = new ArrayList<>();
|
||||
collector.forEach(result::add);
|
||||
if (result.size() != expectedFeatures.size()) {
|
||||
issues.add(
|
||||
"Different number of elements, expected=%s actual=%s".formatted(expectedFeatures.size(), result.size()));
|
||||
} else {
|
||||
// TODO print a diff of the input and output feature YAML representations
|
||||
for (int i = 0; i < expectedFeatures.size(); i++) {
|
||||
var expected = expectedFeatures.get(i);
|
||||
var actual = result.stream().max(proximityTo(expected)).orElseThrow();
|
||||
result.remove(actual);
|
||||
var actualTags = actual.getAttrsAtZoom(expected.atZoom());
|
||||
String prefix = "feature[%d]".formatted(i);
|
||||
validate(prefix + ".layer", issues, expected.layer(), actual.getLayer());
|
||||
validate(prefix + ".minzoom", issues, expected.minZoom(), actual.getMinZoom());
|
||||
validate(prefix + ".maxzoom", issues, expected.maxZoom(), actual.getMaxZoom());
|
||||
validate(prefix + ".minsize", issues, expected.minSize(), actual.getMinPixelSizeAtZoom(expected.atZoom()));
|
||||
validate(prefix + ".geometry", issues, expected.geometry(), GeometryType.typeOf(actual.getGeometry()));
|
||||
Set<String> tags = new TreeSet<>(actualTags.keySet());
|
||||
expected.tags().forEach((tag, value) -> {
|
||||
validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, value, actualTags.get(tag), false);
|
||||
tags.remove(tag);
|
||||
});
|
||||
if (Boolean.FALSE.equals(expected.allowExtraTags())) {
|
||||
for (var tag : tags) {
|
||||
validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, null, actualTags.get(tag), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}))).toList());
|
||||
}
|
||||
|
||||
private static Comparator<FeatureCollector.Feature> proximityTo(SchemaSpecification.OutputFeature expected) {
|
||||
return Comparator.comparingInt(item -> (Objects.equals(item.getLayer(), expected.layer()) ? 2 : 0) +
|
||||
(Objects.equals(GeometryType.typeOf(item.getGeometry()), expected.geometry()) ? 1 : 0));
|
||||
}
|
||||
|
||||
private static <T> void validate(String field, List<String> issues, T expected, T actual, boolean ignoreWhenNull) {
|
||||
if ((!ignoreWhenNull || expected != null) && !Objects.equals(expected, actual)) {
|
||||
// handle when expected and actual are int/long or long/int
|
||||
if (expected instanceof Number && actual instanceof Number && expected.toString().equals(actual.toString())) {
|
||||
return;
|
||||
}
|
||||
issues.add("%s: expected <%s> actual <%s>".formatted(field, format(expected), format(actual)));
|
||||
}
|
||||
}
|
||||
|
||||
private static String format(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
} else if (o instanceof String s) {
|
||||
return Format.quote(s);
|
||||
} else {
|
||||
return o.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void validate(String field, List<String> issues, T expected, T actual) {
|
||||
validate(field, issues, expected, actual, true);
|
||||
}
|
||||
|
||||
/** Result of comparing the output vector tile feature to what was expected. */
|
||||
public record ExampleResult(
|
||||
SchemaSpecification.Example example,
|
||||
// TODO include a symmetric diff so we can pretty-print the expected/actual output diff
|
||||
Try<List<String>> issues
|
||||
) {
|
||||
|
||||
public boolean ok() {
|
||||
return issues.isSuccess() && issues.get().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(List<ExampleResult> results) {
|
||||
|
||||
public boolean ok() {
|
||||
return results.stream().allMatch(ExampleResult::ok);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import static com.onthegomap.planetiler.expression.Expression.*;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.onthegomap.planetiler.custommap.expression.ConfigExpression;
|
|||
import com.onthegomap.planetiler.expression.DataType;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.util.YAML;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||
|
||||
import com.onthegomap.planetiler.TestUtils;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import com.onthegomap.planetiler.custommap.validator.SchemaSpecification;
|
||||
import com.onthegomap.planetiler.custommap.validator.SchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
|
||||
class SchemaTests {
|
||||
@TestFactory
|
||||
List<DynamicTest> shortbread() {
|
||||
return testSchema("shortbread.yml", "shortbread.spec.yml");
|
||||
Stream<DynamicNode> shortbread() {
|
||||
return test("shortbread.yml", "shortbread.spec.yml");
|
||||
}
|
||||
|
||||
private List<DynamicTest> testSchema(String schema, String spec) {
|
||||
private static Stream<DynamicNode> test(String schemaFile, String specFile) {
|
||||
var base = Path.of("src", "main", "resources", "samples");
|
||||
var result = SchemaValidator.validate(
|
||||
SchemaConfig.load(base.resolve(schema)),
|
||||
SchemaSpecification.load(base.resolve(spec))
|
||||
);
|
||||
return result.results().stream()
|
||||
.map(test -> dynamicTest(test.example().name(), () -> {
|
||||
if (test.issues().isFailure()) {
|
||||
throw test.issues().exception();
|
||||
}
|
||||
if (!test.issues().get().isEmpty()) {
|
||||
throw new AssertionError("Validation failed:\n" + String.join("\n", test.issues().get()));
|
||||
}
|
||||
})).toList();
|
||||
SchemaConfig schema = SchemaConfig.load(base.resolve(schemaFile));
|
||||
SchemaSpecification specification = SchemaSpecification.load(base.resolve(specFile));
|
||||
var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
|
||||
var profile = new ConfiguredProfile(schema, context);
|
||||
return TestUtils.validateProfile(profile, specification);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import com.onthegomap.planetiler.validator.BaseSchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
|
@ -22,7 +25,7 @@ class SchemaValidatorTest {
|
|||
@TempDir
|
||||
Path tmpDir;
|
||||
|
||||
record Result(SchemaValidator.Result output, String cliOutput) {}
|
||||
record Result(BaseSchemaValidator.Result output, String cliOutput) {}
|
||||
|
||||
private Result validate(String schema, String spec) throws IOException {
|
||||
var result = SchemaValidator.validate(
|
||||
|
@ -57,55 +60,13 @@ class SchemaValidatorTest {
|
|||
var baos = new ByteArrayOutputStream();
|
||||
var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8)
|
||||
) {
|
||||
SchemaValidator.validateFromCli(
|
||||
path,
|
||||
printStream
|
||||
);
|
||||
new SchemaValidator(Arguments.of(), path.toString(), printStream).validateFromCli();
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
String waterSchema = """
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- id: water
|
||||
features:
|
||||
- source: osm
|
||||
geometry: polygon
|
||||
min_size: 10
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
||||
""";
|
||||
|
||||
private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException {
|
||||
return validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
layer: %s
|
||||
geometry: %s
|
||||
%s
|
||||
tags:
|
||||
%s
|
||||
""".formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
|
||||
tags == null ? "" : tags.indent(6).strip())
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"true,water,polygon,natural: water,",
|
||||
|
@ -128,7 +89,40 @@ class SchemaValidatorTest {
|
|||
})
|
||||
void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags)
|
||||
throws IOException {
|
||||
var results = validateWater(layer, geometry, tags, allowExtraTags);
|
||||
var results = validate(
|
||||
"""
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- id: water
|
||||
features:
|
||||
- source: osm
|
||||
geometry: polygon
|
||||
min_size: 10
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
||||
""",
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
layer: %s
|
||||
geometry: %s
|
||||
%s
|
||||
tags:
|
||||
%s
|
||||
""".formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
|
||||
tags == null ? "" : tags.indent(6).strip())
|
||||
);
|
||||
assertEquals(1, results.output.results().size());
|
||||
assertEquals("test output", results.output.results().get(0).example().name());
|
||||
if (shouldBeOk) {
|
||||
|
@ -140,47 +134,6 @@ class SchemaValidatorTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidationFailsWrongNumberOfFeatures() throws IOException {
|
||||
var results = validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
"""
|
||||
);
|
||||
assertFalse(results.output.ok(), results.toString());
|
||||
|
||||
results = validate(
|
||||
waterSchema,
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
- layer: water
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
- layer: water2
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water2
|
||||
"""
|
||||
);
|
||||
assertFalse(results.output.ok(), results.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidationWiresInArguments() throws IOException {
|
||||
var results = validate(
|
||||
|
|
|
@ -43,6 +43,11 @@
|
|||
<artifactId>planetiler-custommap</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-experimental</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-examples</artifactId>
|
||||
|
|
|
@ -10,6 +10,8 @@ import com.onthegomap.planetiler.examples.BikeRouteOverlay;
|
|||
import com.onthegomap.planetiler.examples.OsmQaTiles;
|
||||
import com.onthegomap.planetiler.examples.ToiletsOverlay;
|
||||
import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi;
|
||||
import com.onthegomap.planetiler.experimental.lua.LuaMain;
|
||||
import com.onthegomap.planetiler.experimental.lua.LuaValidator;
|
||||
import com.onthegomap.planetiler.mbtiles.Verify;
|
||||
import com.onthegomap.planetiler.util.TileSizeStats;
|
||||
import com.onthegomap.planetiler.util.TopOsmTiles;
|
||||
|
@ -43,12 +45,19 @@ public class Main {
|
|||
entry("generate-custom", ConfiguredMapMain::main),
|
||||
entry("custom", ConfiguredMapMain::main),
|
||||
|
||||
entry("lua", LuaMain::main),
|
||||
|
||||
entry("generate-shortbread", bundledSchema("shortbread.yml")),
|
||||
entry("shortbread", bundledSchema("shortbread.yml")),
|
||||
|
||||
entry("verify", SchemaValidator::main),
|
||||
entry("verify", validate()),
|
||||
entry("verify-custom", SchemaValidator::main),
|
||||
entry("verify-schema", SchemaValidator::main),
|
||||
entry("verify-lua", LuaValidator::main),
|
||||
entry("validate", validate()),
|
||||
entry("validate-custom", SchemaValidator::main),
|
||||
entry("validate-schema", SchemaValidator::main),
|
||||
entry("validate-lua", LuaValidator::main),
|
||||
|
||||
entry("example-bikeroutes", BikeRouteOverlay::main),
|
||||
entry("example-toilets", ToiletsOverlay::main),
|
||||
|
@ -73,6 +82,16 @@ public class Main {
|
|||
).toArray(String[]::new));
|
||||
}
|
||||
|
||||
private static EntryPoint validate() {
|
||||
return args -> {
|
||||
if (Arrays.stream(args).anyMatch(d -> d.endsWith(".lua"))) {
|
||||
LuaValidator.main(args);
|
||||
} else {
|
||||
SchemaValidator.main(args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
EntryPoint task = DEFAULT_TASK;
|
||||
|
||||
|
@ -81,6 +100,9 @@ public class Main {
|
|||
if (maybeTask.matches("^.*\\.ya?ml$")) {
|
||||
task = ConfiguredMapMain::main;
|
||||
args[0] = "--schema=" + args[0];
|
||||
} else if (maybeTask.matches("^.*\\.lua$")) {
|
||||
task = LuaMain::main;
|
||||
args[0] = "--script=" + args[0];
|
||||
} else {
|
||||
EntryPoint taskFromArg0 = ENTRY_POINTS.get(maybeTask);
|
||||
if (taskFromArg0 != null) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>planetiler-experimental</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.luaj</groupId>
|
||||
<artifactId>luaj-jse</artifactId>
|
||||
<version>3.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.bcel</groupId>
|
||||
<artifactId>bcel</artifactId>
|
||||
<version>6.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- To use test utilities: -->
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,193 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaTables;
|
||||
import org.luaj.vm2.LuaUserdata;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
|
||||
import org.luaj.vm2.lib.jse.CoerceLuaToJava;
|
||||
|
||||
/**
|
||||
* Helper methods to convert between lua and java types.
|
||||
*/
|
||||
public interface LuaConversions {
|
||||
Set<String> LUA_AND_NOT_JAVA_KEYWORDS = Set.of(
|
||||
"and",
|
||||
"elseif",
|
||||
"end",
|
||||
"function",
|
||||
"in",
|
||||
"local",
|
||||
"nil",
|
||||
"not",
|
||||
"or",
|
||||
"repeat",
|
||||
"then",
|
||||
"until"
|
||||
);
|
||||
|
||||
static LuaValue toLua(Object sourceFeature) {
|
||||
if (sourceFeature instanceof List<?> list) {
|
||||
return LuaValue.listOf(list.stream().map(LuaConversions::toLua).toArray(LuaValue[]::new));
|
||||
}
|
||||
return CoerceJavaToLua.coerce(sourceFeature);
|
||||
}
|
||||
|
||||
static LuaTable toLuaTable(Collection<?> list) {
|
||||
return LuaValue.listOf(list.stream().map(LuaConversions::toLua).toArray(LuaValue[]::new));
|
||||
}
|
||||
|
||||
static LuaTable toLuaTable(Map<?, ?> map) {
|
||||
return LuaValue.tableOf(map.entrySet().stream()
|
||||
.flatMap(entry -> Stream.of(toLua(entry.getKey()), toLua(entry.getValue())))
|
||||
.toArray(LuaValue[]::new));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T toJava(LuaValue value, Class<T> clazz) {
|
||||
return (T) CoerceLuaToJava.coerce(value, clazz);
|
||||
}
|
||||
|
||||
static List<Object> toJavaList(LuaValue list) {
|
||||
return toJavaList(list, Object.class);
|
||||
}
|
||||
|
||||
static <T> List<T> toJavaList(LuaValue list, Class<T> itemClass) {
|
||||
if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof List<?> fromLua) {
|
||||
@SuppressWarnings("unchecked") List<T> result = (List<T>) fromLua;
|
||||
return result;
|
||||
} else if (list.istable()) {
|
||||
int length = list.length();
|
||||
List<T> result = new ArrayList<>();
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.add(toJava(list.get(i + 1), itemClass));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
static Collection<Object> toJavaCollection(LuaValue list) {
|
||||
return toJavaCollection(list, Object.class);
|
||||
}
|
||||
|
||||
static <T> Collection<T> toJavaCollection(LuaValue list, Class<T> itemClass) {
|
||||
if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Collection<?> fromLua) {
|
||||
@SuppressWarnings("unchecked") Collection<T> result = (Collection<T>) fromLua;
|
||||
return result;
|
||||
} else {
|
||||
return toJavaList(list, itemClass);
|
||||
}
|
||||
}
|
||||
|
||||
static Iterable<Object> toJavaIterable(LuaValue list) {
|
||||
return toJavaIterable(list, Object.class);
|
||||
}
|
||||
|
||||
static <T> Iterable<T> toJavaIterable(LuaValue list, Class<T> itemClass) {
|
||||
if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Iterable<?> fromLua) {
|
||||
@SuppressWarnings("unchecked") Iterable<T> result = (Iterable<T>) fromLua;
|
||||
return result;
|
||||
} else {
|
||||
return toJavaList(list, itemClass);
|
||||
}
|
||||
}
|
||||
|
||||
static Set<Object> toJavaSet(LuaValue list) {
|
||||
return toJavaSet(list, Object.class);
|
||||
}
|
||||
|
||||
static <T> Set<T> toJavaSet(LuaValue list, Class<T> itemClass) {
|
||||
if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Set<?> fromLua) {
|
||||
@SuppressWarnings("unchecked") Set<T> result = (Set<T>) fromLua;
|
||||
return result;
|
||||
} else if (list instanceof LuaTable table) {
|
||||
Set<T> result = new LinkedHashSet<>();
|
||||
if (LuaTables.isArray(table)) {
|
||||
int length = list.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.add(toJava(list.get(i + 1), itemClass));
|
||||
}
|
||||
} else {
|
||||
for (var key : table.keys()) {
|
||||
var value = table.get(key);
|
||||
if (value.toboolean()) {
|
||||
result.add(toJava(key, itemClass));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
static Map<Object, Object> toJavaMap(LuaValue list) {
|
||||
return toJavaMap(list, Object.class, Object.class);
|
||||
}
|
||||
|
||||
static <K, V> Map<K, V> toJavaMap(LuaValue list, Class<K> keyClass, Class<V> valueClass) {
|
||||
if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Map<?, ?> fromLua) {
|
||||
@SuppressWarnings("unchecked") Map<K, V> result = (Map<K, V>) fromLua;
|
||||
return result;
|
||||
} else if (list instanceof LuaTable table) {
|
||||
Map<K, V> result = new LinkedHashMap<>();
|
||||
for (var key : table.keys()) {
|
||||
result.put(toJava(key, keyClass), toJava(table.get(key), valueClass));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
static <T> LuaValue consumerToLua(Consumer<T> consumer, Class<T> itemClass) {
|
||||
return new LuaConsumer<>(consumer, itemClass);
|
||||
}
|
||||
|
||||
class LuaConsumer<T> extends OneArgFunction {
|
||||
|
||||
private final Class<T> itemClass;
|
||||
private final Consumer<T> consumer;
|
||||
|
||||
public LuaConsumer(Consumer<T> consumer, Class<T> itemClass) {
|
||||
this.consumer = consumer;
|
||||
this.itemClass = itemClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
consumer.accept(toJava(arg, itemClass));
|
||||
return NIL;
|
||||
}
|
||||
}
|
||||
|
||||
static <I, O> LuaValue functionToLua(Function<I, O> fn, Class<I> inputClass) {
|
||||
return new FunctionWrapper<>(fn, inputClass);
|
||||
}
|
||||
|
||||
class FunctionWrapper<I, O> extends OneArgFunction {
|
||||
|
||||
private final Class<I> inputClass;
|
||||
private final Function<I, O> fn;
|
||||
|
||||
public FunctionWrapper(Function<I, O> fn, Class<I> inputClass) {
|
||||
this.fn = fn;
|
||||
this.inputClass = inputClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
return toLua(fn.apply(toJava(arg, inputClass)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJava;
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJavaMap;
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLua;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureMerge;
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.geo.GeoUtils;
|
||||
import com.onthegomap.planetiler.stats.Stats;
|
||||
import com.onthegomap.planetiler.util.BuildInfo;
|
||||
import com.onthegomap.planetiler.util.LanguageUtils;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import com.onthegomap.planetiler.util.SortKey;
|
||||
import com.onthegomap.planetiler.util.Translations;
|
||||
import com.onthegomap.planetiler.util.ZoomFunction;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.jse.ExtraPlanetilerCoercions;
|
||||
import org.luaj.vm2.lib.jse.JsePlatform;
|
||||
import org.luaj.vm2.lib.jse.LuaBindMethods;
|
||||
import org.luaj.vm2.lib.jse.LuaGetter;
|
||||
import org.luaj.vm2.lib.jse.LuaSetter;
|
||||
import org.luaj.vm2.luajc.LuaJC;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Global variables exposed to lua scripts.
|
||||
* <p>
|
||||
* All instance fields annotated with {@link ExposeToLua} will be exposed to lua as global variables.
|
||||
*/
|
||||
@SuppressWarnings({"java:S1104", "java:S116", "unused"})
|
||||
public class LuaEnvironment {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LuaEnvironment.class);
|
||||
private static final Set<Class<?>> CLASSES_TO_EXPOSE = Set.of(
|
||||
ZoomFunction.class,
|
||||
FeatureMerge.class,
|
||||
Parse.class,
|
||||
LanguageUtils.class,
|
||||
Expression.class,
|
||||
MultiExpression.class,
|
||||
GeoUtils.class,
|
||||
SortKey.class
|
||||
);
|
||||
@ExposeToLua
|
||||
public final PlanetilerNamespace planetiler;
|
||||
final Planetiler runner;
|
||||
public LuaProfile profile;
|
||||
public LuaValue main;
|
||||
|
||||
public LuaEnvironment(Planetiler runner) {
|
||||
this.runner = runner;
|
||||
this.planetiler = new PlanetilerNamespace();
|
||||
}
|
||||
|
||||
public static LuaEnvironment loadScript(Arguments arguments, Path script) throws IOException {
|
||||
return loadScript(arguments, Files.readString(script), script.getFileName().toString());
|
||||
}
|
||||
|
||||
public static LuaEnvironment loadScript(Arguments args, Path scriptPath, Set<Path> pathsToWatch) throws IOException {
|
||||
return loadScript(args, Files.readString(scriptPath), scriptPath.getFileName().toString(), Map.of(), pathsToWatch);
|
||||
}
|
||||
|
||||
public static LuaEnvironment loadScript(Arguments arguments, String script, String fileName) {
|
||||
return loadScript(arguments, script, fileName, Map.of(), ConcurrentHashMap.newKeySet());
|
||||
}
|
||||
|
||||
public static LuaEnvironment loadScript(Arguments arguments, String script, String fileName, Map<String, ?> extras,
|
||||
Set<Path> filesLoaded) {
|
||||
ExtraPlanetilerCoercions.install();
|
||||
boolean luajc = arguments.getBoolean("luajc", "compile lua to java bytecode", true);
|
||||
Globals globals = JsePlatform.standardGlobals();
|
||||
if (luajc) {
|
||||
LuaJC.install(globals);
|
||||
}
|
||||
Planetiler runner = Planetiler.create(arguments);
|
||||
LuaEnvironment env = new LuaEnvironment(runner);
|
||||
env.install(globals);
|
||||
extras.forEach((name, java) -> globals.set(name, toLua(java)));
|
||||
var oldFilder = globals.finder;
|
||||
globals.finder = filename -> {
|
||||
filesLoaded.add(Path.of(filename));
|
||||
return oldFilder.findResource(filename);
|
||||
};
|
||||
globals.load(script, fileName).call();
|
||||
LuaProfile profile = new LuaProfile(env);
|
||||
env.profile = new LuaProfile(env);
|
||||
env.main = globals.get("main");
|
||||
return env;
|
||||
}
|
||||
|
||||
public static LuaEnvironment loadScript(Arguments args, String script, String filename, Map<String, ?> map) {
|
||||
return loadScript(args, script, filename, map, ConcurrentHashMap.newKeySet());
|
||||
}
|
||||
|
||||
public void run() throws Exception {
|
||||
runner.setProfile(profile);
|
||||
if (main != null && main.isfunction()) {
|
||||
main.call(toLua(runner));
|
||||
} else {
|
||||
runner.overwriteOutput(planetiler.output.path).run();
|
||||
}
|
||||
}
|
||||
|
||||
public void install(Globals globals) {
|
||||
for (var field : getClass().getDeclaredFields()) {
|
||||
var annotation = field.getAnnotation(ExposeToLua.class);
|
||||
if (annotation != null) {
|
||||
String name = annotation.value().isBlank() ? field.getName() : annotation.value();
|
||||
try {
|
||||
globals.set(name, toLua(field.get(this)));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var clazz : CLASSES_TO_EXPOSE) {
|
||||
globals.set(clazz.getSimpleName(), toLua(clazz));
|
||||
}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface ExposeToLua {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
public static class PlanetilerOutput {
|
||||
public Path path = Path.of("data", "output.mbtiles");
|
||||
public String name;
|
||||
public String description;
|
||||
public String attribution;
|
||||
public String version;
|
||||
public boolean is_overlay;
|
||||
}
|
||||
|
||||
@LuaBindMethods
|
||||
public class PlanetilerNamespace {
|
||||
public final BuildInfo build = BuildInfo.get();
|
||||
public final PlanetilerConfig config = runner.config();
|
||||
public final Stats stats = runner.stats();
|
||||
public final Arguments args = runner.arguments();
|
||||
public final PlanetilerOutput output = new PlanetilerOutput();
|
||||
public LuaValue process_feature;
|
||||
public LuaValue cares_about_source;
|
||||
public LuaValue cares_about_wikidata_translation;
|
||||
public LuaValue estimate_ram_required;
|
||||
public LuaValue estimate_intermediate_disk_bytes;
|
||||
public LuaValue estimate_output_bytes;
|
||||
public LuaValue finish;
|
||||
public LuaValue preprocess_osm_node;
|
||||
public LuaValue preprocess_osm_way;
|
||||
public LuaValue preprocess_osm_relation;
|
||||
public LuaValue release;
|
||||
public LuaValue post_process;
|
||||
public String examples;
|
||||
|
||||
private static <T> T get(LuaValue map, String key, Class<T> clazz) {
|
||||
LuaValue value = map.get(key);
|
||||
return value.isnil() ? null : toJava(value, clazz);
|
||||
}
|
||||
|
||||
@LuaGetter
|
||||
public Translations translations() {
|
||||
return runner.translations();
|
||||
}
|
||||
|
||||
@LuaGetter
|
||||
public List<String> languages() {
|
||||
return runner.getDefaultLanguages();
|
||||
}
|
||||
|
||||
@LuaSetter
|
||||
public void languages(List<String> languages) {
|
||||
runner.setDefaultLanguages(languages);
|
||||
}
|
||||
|
||||
public void fetch_wikidata_translations(Path defaultPath) {
|
||||
runner.fetchWikidataNameTranslations(defaultPath);
|
||||
}
|
||||
|
||||
public void fetch_wikidata_translations() {
|
||||
runner.fetchWikidataNameTranslations(Path.of("data", "sources", "wikidata_names.json"));
|
||||
}
|
||||
|
||||
public void add_source(String name, LuaValue map) {
|
||||
String type = get(map, "type", String.class);
|
||||
Path path = get(map, "path", Path.class);
|
||||
if (name == null || type == null) {
|
||||
throw new IllegalArgumentException("Sources must have 'type', got: " + toJavaMap(map));
|
||||
}
|
||||
String url = get(map, "url", String.class);
|
||||
String projection = get(map, "projection", String.class);
|
||||
String glob = get(map, "glob", String.class);
|
||||
if (path == null) {
|
||||
if (url == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Sources must have either a 'url' or local 'path', got: " + toJavaMap(map));
|
||||
}
|
||||
String filename = url
|
||||
.replaceFirst("^https?://", "")
|
||||
.replaceAll("\\W&&[^.]+", "_");
|
||||
if (type.equals("osm") && !filename.endsWith(".pbf")) {
|
||||
filename = filename + ".osm.pbf";
|
||||
}
|
||||
path = Path.of("data", "sources", filename);
|
||||
}
|
||||
switch (type) {
|
||||
case "osm" -> runner.addOsmSource(name, path, url);
|
||||
case "shapefile" -> {
|
||||
if (glob != null) {
|
||||
runner.addShapefileGlobSource(projection, name, path, glob, url);
|
||||
} else {
|
||||
runner.addShapefileSource(projection, name, path, url);
|
||||
}
|
||||
}
|
||||
case "geopackage" -> runner.addGeoPackageSource(projection, name, path, url);
|
||||
case "natural_earth" -> runner.addNaturalEarthSource(name, path, url);
|
||||
default -> throw new IllegalArgumentException("Unrecognized source type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import java.nio.file.Path;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Main entrypoint for running a lua profile.
|
||||
*/
|
||||
public class LuaMain {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LuaMain.class);
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
LOGGER.warn(
|
||||
"Lua profiles are experimental and may change! Please provide feedback and report any bugs before depending on it in production.");
|
||||
var arguments = Arguments.fromEnvOrArgs(args);
|
||||
Path script = arguments.inputFile("script", "the lua script to run", Path.of("profile.lua"));
|
||||
LuaEnvironment.loadScript(arguments, script).run();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmElement;
|
||||
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import org.luaj.vm2.LuaInteger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Profile} that delegates to a lua script.
|
||||
*/
|
||||
public class LuaProfile implements Profile {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LuaProfile.class);
|
||||
private final LuaEnvironment.PlanetilerNamespace planetiler;
|
||||
|
||||
public LuaProfile(LuaEnvironment env) {
|
||||
this.planetiler = env.planetiler;
|
||||
if (planetiler.process_feature == null) {
|
||||
LOGGER.warn("Missing function planetiler.process_feature");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFeature(SourceFeature sourceFeature, FeatureCollector features) {
|
||||
if (planetiler.process_feature != null) {
|
||||
planetiler.process_feature.call(LuaConversions.toLua(sourceFeature), LuaConversions.toLua(features));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTile.Feature> postProcessLayerFeatures(String layer, int zoom, List<VectorTile.Feature> items) {
|
||||
if (planetiler.post_process != null) {
|
||||
return LuaConversions.toJavaList(
|
||||
planetiler.post_process.call(LuaConversions.toLua(layer), LuaConversions.toLua(zoom),
|
||||
LuaConversions.toLua(items)),
|
||||
VectorTile.Feature.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean caresAboutSource(String name) {
|
||||
return planetiler.cares_about_source == null || planetiler.cares_about_source.call(name).toboolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean caresAboutWikidataTranslation(OsmElement elem) {
|
||||
return planetiler.cares_about_wikidata_translation != null &&
|
||||
planetiler.cares_about_wikidata_translation.call(LuaConversions.toLua(elem)).toboolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return planetiler.output.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return planetiler.output.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String attribution() {
|
||||
return planetiler.output.attribution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return planetiler.output.version;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isOverlay() {
|
||||
return planetiler.output.is_overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
|
||||
Consumer<FeatureCollector.Feature> next) {
|
||||
if (planetiler.finish != null) {
|
||||
planetiler.finish.call(LuaConversions.toLua(sourceName), LuaConversions.toLua(featureCollectors),
|
||||
LuaConversions.consumerToLua(next, FeatureCollector.Feature.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateIntermediateDiskBytes(long osmFileSize) {
|
||||
return planetiler.estimate_intermediate_disk_bytes == null ? 0 :
|
||||
planetiler.estimate_intermediate_disk_bytes.call(LuaInteger.valueOf(osmFileSize)).tolong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateOutputBytes(long osmFileSize) {
|
||||
return planetiler.estimate_output_bytes == null ? 0 :
|
||||
planetiler.estimate_output_bytes.call(LuaInteger.valueOf(osmFileSize)).tolong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateRamRequired(long osmFileSize) {
|
||||
return planetiler.estimate_ram_required == null ? 0 :
|
||||
planetiler.estimate_ram_required.call(LuaInteger.valueOf(osmFileSize)).tolong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessOsmNode(OsmElement.Node node) {
|
||||
if (planetiler.preprocess_osm_node != null) {
|
||||
planetiler.preprocess_osm_node.call(LuaConversions.toLua(node));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessOsmWay(OsmElement.Way way) {
|
||||
if (planetiler.preprocess_osm_way != null) {
|
||||
planetiler.preprocess_osm_way.call(LuaConversions.toLua(way));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
|
||||
if (planetiler.preprocess_osm_relation == null) {
|
||||
return null;
|
||||
}
|
||||
return LuaConversions.toJavaList(planetiler.preprocess_osm_relation.call(LuaConversions.toLua(relation)),
|
||||
OsmRelationInfo.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (planetiler.release != null) {
|
||||
planetiler.release.call();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.util.AnsiColors;
|
||||
import com.onthegomap.planetiler.validator.BaseSchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
|
||||
|
||||
/**
|
||||
* Validates a lua profile against a yaml set of example source features and the vector tile features they should map to
|
||||
**/
|
||||
public class LuaValidator extends BaseSchemaValidator {
|
||||
|
||||
private final Path scriptPath;
|
||||
|
||||
LuaValidator(Arguments args, String schemaFile, PrintStream output) {
|
||||
super(args, output);
|
||||
scriptPath = schemaFile == null ? args.inputFile("script", "Schema file") :
|
||||
args.inputFile("script", "Script file", Path.of(schemaFile));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// let users run `verify schema.lua` as a shortcut
|
||||
String schemaFile = null;
|
||||
if (args.length > 0 && args[0].endsWith(".lua") && !args[0].startsWith("-")) {
|
||||
schemaFile = args[0];
|
||||
args = Stream.of(args).skip(1).toArray(String[]::new);
|
||||
}
|
||||
var arguments = Arguments.fromEnvOrArgs(args).silence();
|
||||
new LuaValidator(arguments, schemaFile, System.out).runOrWatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result validate(Set<Path> pathsToWatch) {
|
||||
Result result = null;
|
||||
try {
|
||||
pathsToWatch.add(scriptPath);
|
||||
var env = LuaEnvironment.loadScript(args, scriptPath, pathsToWatch);
|
||||
// examples can either be embedded in the lua file, or referenced
|
||||
Path specPath;
|
||||
if (env.planetiler.examples != null) {
|
||||
specPath = Path.of(env.planetiler.examples);
|
||||
if (!specPath.isAbsolute()) {
|
||||
specPath = scriptPath.resolveSibling(specPath);
|
||||
}
|
||||
} else {
|
||||
specPath = args.file("spec", "yaml spec", null);
|
||||
}
|
||||
SchemaSpecification spec;
|
||||
if (specPath != null) {
|
||||
pathsToWatch.add(specPath);
|
||||
spec = SchemaSpecification.load(specPath);
|
||||
} else {
|
||||
spec = new SchemaSpecification(List.of());
|
||||
}
|
||||
result = validate(env.profile, spec, PlanetilerConfig.from(args));
|
||||
} catch (Exception exception) {
|
||||
Throwable rootCause = ExceptionUtils.getRootCause(exception);
|
||||
if (hasCause(exception, YamlEngineException.class) || hasCause(exception, JacksonException.class)) {
|
||||
output.println(AnsiColors.red("Malformed yaml input:\n\n" + rootCause.toString().indent(4)));
|
||||
} else {
|
||||
output.println(AnsiColors.red(
|
||||
"Unexpected exception thrown:\n" + rootCause.toString().indent(4) + "\n" +
|
||||
String.join("\n", ExceptionUtils.getStackTrace(rootCause)))
|
||||
.indent(4));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,431 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2;
|
||||
|
||||
import org.luaj.vm2.lib.MathLib;
|
||||
|
||||
/**
|
||||
* Modified version of {@link LuaInteger} with race condition fixes.
|
||||
* <p>
|
||||
* Extension of {@link LuaNumber} which can hold a Java int as its value.
|
||||
* <p>
|
||||
* These instance are not instantiated directly by clients, but indirectly via the static functions
|
||||
* {@link LuaValue#valueOf(int)} or {@link LuaValue#valueOf(double)} functions. This ensures that policies regarding
|
||||
* pooling of instances are encapsulated.
|
||||
* <p>
|
||||
* There are no API's specific to LuaInteger that are useful beyond what is already exposed in {@link LuaValue}.
|
||||
*
|
||||
* @see LuaValue
|
||||
* @see LuaNumber
|
||||
* @see LuaDouble
|
||||
* @see LuaValue#valueOf(int)
|
||||
* @see LuaValue#valueOf(double)
|
||||
*/
|
||||
public class LuaInteger extends LuaNumber {
|
||||
// planetiler change: move int values into nested class to avoid race condition
|
||||
// since LuaInteger extends LuaValue, but LuaValue calls LuaInteger.valueOf
|
||||
private static class IntValues {
|
||||
|
||||
private static final LuaInteger[] intValues = new LuaInteger[512];
|
||||
static {
|
||||
for (int i = 0; i < 512; i++)
|
||||
intValues[i] = new LuaInteger(i - 256);
|
||||
}
|
||||
}
|
||||
|
||||
public static LuaInteger valueOf(int i) {
|
||||
return i <= 255 && i >= -256 ? IntValues.intValues[i + 256] : new LuaInteger(i);
|
||||
};
|
||||
|
||||
// TODO consider moving this to LuaValue
|
||||
/**
|
||||
* Return a LuaNumber that represents the value provided
|
||||
*
|
||||
* @param l long value to represent.
|
||||
* @return LuaNumber that is eithe LuaInteger or LuaDouble representing l
|
||||
* @see LuaValue#valueOf(int)
|
||||
* @see LuaValue#valueOf(double)
|
||||
*/
|
||||
public static LuaNumber valueOf(long l) {
|
||||
int i = (int) l;
|
||||
return l == i ? (i <= 255 && i >= -256 ? IntValues.intValues[i + 256] :
|
||||
(LuaNumber) new LuaInteger(i)) :
|
||||
(LuaNumber) LuaDouble.valueOf(l);
|
||||
}
|
||||
|
||||
/** The value being held by this instance. */
|
||||
public final int v;
|
||||
|
||||
/**
|
||||
* Package protected constructor.
|
||||
*
|
||||
* @see LuaValue#valueOf(int)
|
||||
**/
|
||||
LuaInteger(int i) {
|
||||
this.v = i;
|
||||
}
|
||||
|
||||
public boolean isint() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isinttype() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean islong() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public byte tobyte() {
|
||||
return (byte) v;
|
||||
}
|
||||
|
||||
public char tochar() {
|
||||
return (char) v;
|
||||
}
|
||||
|
||||
public double todouble() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public float tofloat() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public int toint() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public long tolong() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public short toshort() {
|
||||
return (short) v;
|
||||
}
|
||||
|
||||
public double optdouble(double defval) {
|
||||
return v;
|
||||
}
|
||||
|
||||
public int optint(int defval) {
|
||||
return v;
|
||||
}
|
||||
|
||||
public LuaInteger optinteger(LuaInteger defval) {
|
||||
return this;
|
||||
}
|
||||
|
||||
public long optlong(long defval) {
|
||||
return v;
|
||||
}
|
||||
|
||||
public String tojstring() {
|
||||
return Integer.toString(v);
|
||||
}
|
||||
|
||||
public LuaString strvalue() {
|
||||
return LuaString.valueOf(Integer.toString(v));
|
||||
}
|
||||
|
||||
public LuaString optstring(LuaString defval) {
|
||||
return LuaString.valueOf(Integer.toString(v));
|
||||
}
|
||||
|
||||
public LuaValue tostring() {
|
||||
return LuaString.valueOf(Integer.toString(v));
|
||||
}
|
||||
|
||||
public String optjstring(String defval) {
|
||||
return Integer.toString(v);
|
||||
}
|
||||
|
||||
public LuaInteger checkinteger() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isstring() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public static int hashCode(int x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// unary operators
|
||||
public LuaValue neg() {
|
||||
return valueOf(-(long) v);
|
||||
}
|
||||
|
||||
// object equality, used for key comparison
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof LuaInteger ? ((LuaInteger) o).v == v : false;
|
||||
}
|
||||
|
||||
// equality w/ metatable processing
|
||||
public LuaValue eq(LuaValue val) {
|
||||
return val.raweq(v) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public boolean eq_b(LuaValue val) {
|
||||
return val.raweq(v);
|
||||
}
|
||||
|
||||
// equality w/o metatable processing
|
||||
public boolean raweq(LuaValue val) {
|
||||
return val.raweq(v);
|
||||
}
|
||||
|
||||
public boolean raweq(double val) {
|
||||
return v == val;
|
||||
}
|
||||
|
||||
public boolean raweq(int val) {
|
||||
return v == val;
|
||||
}
|
||||
|
||||
// arithmetic operators
|
||||
public LuaValue add(LuaValue rhs) {
|
||||
return rhs.add(v);
|
||||
}
|
||||
|
||||
public LuaValue add(double lhs) {
|
||||
return LuaDouble.valueOf(lhs + v);
|
||||
}
|
||||
|
||||
public LuaValue add(int lhs) {
|
||||
return LuaInteger.valueOf(lhs + (long) v);
|
||||
}
|
||||
|
||||
public LuaValue sub(LuaValue rhs) {
|
||||
return rhs.subFrom(v);
|
||||
}
|
||||
|
||||
public LuaValue sub(double rhs) {
|
||||
return LuaDouble.valueOf(v - rhs);
|
||||
}
|
||||
|
||||
public LuaValue sub(int rhs) {
|
||||
return LuaDouble.valueOf(v - rhs);
|
||||
}
|
||||
|
||||
public LuaValue subFrom(double lhs) {
|
||||
return LuaDouble.valueOf(lhs - v);
|
||||
}
|
||||
|
||||
public LuaValue subFrom(int lhs) {
|
||||
return LuaInteger.valueOf(lhs - (long) v);
|
||||
}
|
||||
|
||||
public LuaValue mul(LuaValue rhs) {
|
||||
return rhs.mul(v);
|
||||
}
|
||||
|
||||
public LuaValue mul(double lhs) {
|
||||
return LuaDouble.valueOf(lhs * v);
|
||||
}
|
||||
|
||||
public LuaValue mul(int lhs) {
|
||||
return LuaInteger.valueOf(lhs * (long) v);
|
||||
}
|
||||
|
||||
public LuaValue pow(LuaValue rhs) {
|
||||
return rhs.powWith(v);
|
||||
}
|
||||
|
||||
public LuaValue pow(double rhs) {
|
||||
return MathLib.dpow(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue pow(int rhs) {
|
||||
return MathLib.dpow(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue powWith(double lhs) {
|
||||
return MathLib.dpow(lhs, v);
|
||||
}
|
||||
|
||||
public LuaValue powWith(int lhs) {
|
||||
return MathLib.dpow(lhs, v);
|
||||
}
|
||||
|
||||
public LuaValue div(LuaValue rhs) {
|
||||
return rhs.divInto(v);
|
||||
}
|
||||
|
||||
public LuaValue div(double rhs) {
|
||||
return LuaDouble.ddiv(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue div(int rhs) {
|
||||
return LuaDouble.ddiv(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue divInto(double lhs) {
|
||||
return LuaDouble.ddiv(lhs, v);
|
||||
}
|
||||
|
||||
public LuaValue mod(LuaValue rhs) {
|
||||
return rhs.modFrom(v);
|
||||
}
|
||||
|
||||
public LuaValue mod(double rhs) {
|
||||
return LuaDouble.dmod(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue mod(int rhs) {
|
||||
return LuaDouble.dmod(v, rhs);
|
||||
}
|
||||
|
||||
public LuaValue modFrom(double lhs) {
|
||||
return LuaDouble.dmod(lhs, v);
|
||||
}
|
||||
|
||||
// relational operators
|
||||
public LuaValue lt(LuaValue rhs) {
|
||||
return rhs.gt_b(v) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue lt(double rhs) {
|
||||
return v < rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue lt(int rhs) {
|
||||
return v < rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public boolean lt_b(LuaValue rhs) {
|
||||
return rhs.gt_b(v);
|
||||
}
|
||||
|
||||
public boolean lt_b(int rhs) {
|
||||
return v < rhs;
|
||||
}
|
||||
|
||||
public boolean lt_b(double rhs) {
|
||||
return v < rhs;
|
||||
}
|
||||
|
||||
public LuaValue lteq(LuaValue rhs) {
|
||||
return rhs.gteq_b(v) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue lteq(double rhs) {
|
||||
return v <= rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue lteq(int rhs) {
|
||||
return v <= rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public boolean lteq_b(LuaValue rhs) {
|
||||
return rhs.gteq_b(v);
|
||||
}
|
||||
|
||||
public boolean lteq_b(int rhs) {
|
||||
return v <= rhs;
|
||||
}
|
||||
|
||||
public boolean lteq_b(double rhs) {
|
||||
return v <= rhs;
|
||||
}
|
||||
|
||||
public LuaValue gt(LuaValue rhs) {
|
||||
return rhs.lt_b(v) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue gt(double rhs) {
|
||||
return v > rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue gt(int rhs) {
|
||||
return v > rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public boolean gt_b(LuaValue rhs) {
|
||||
return rhs.lt_b(v);
|
||||
}
|
||||
|
||||
public boolean gt_b(int rhs) {
|
||||
return v > rhs;
|
||||
}
|
||||
|
||||
public boolean gt_b(double rhs) {
|
||||
return v > rhs;
|
||||
}
|
||||
|
||||
public LuaValue gteq(LuaValue rhs) {
|
||||
return rhs.lteq_b(v) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue gteq(double rhs) {
|
||||
return v >= rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public LuaValue gteq(int rhs) {
|
||||
return v >= rhs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
public boolean gteq_b(LuaValue rhs) {
|
||||
return rhs.lteq_b(v);
|
||||
}
|
||||
|
||||
public boolean gteq_b(int rhs) {
|
||||
return v >= rhs;
|
||||
}
|
||||
|
||||
public boolean gteq_b(double rhs) {
|
||||
return v >= rhs;
|
||||
}
|
||||
|
||||
// string comparison
|
||||
public int strcmp(LuaString rhs) {
|
||||
typerror("attempt to compare number with string");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int checkint() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public long checklong() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public double checkdouble() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public String checkjstring() {
|
||||
return String.valueOf(v);
|
||||
}
|
||||
|
||||
public LuaString checkstring() {
|
||||
return valueOf(String.valueOf(v));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.luaj.vm2;
|
||||
|
||||
public class LuaTables {
|
||||
|
||||
/**
|
||||
* Returns true if the lua table is a list, and not a map.
|
||||
*/
|
||||
public static boolean isArray(LuaValue v) {
|
||||
return v instanceof LuaTable table && table.getArrayLength() > 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009-2011 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
|
||||
import java.util.Map;
|
||||
import org.luaj.vm2.LuaDouble;
|
||||
import org.luaj.vm2.LuaInteger;
|
||||
import org.luaj.vm2.LuaString;
|
||||
import org.luaj.vm2.LuaUserdata;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
/**
|
||||
* Modified version of {@link CoerceJavaToLua} that fixes a thread safety issue around concurrent map updates.
|
||||
* <p>
|
||||
* Helper class to coerce values from Java to lua within the luajava library.
|
||||
* <p>
|
||||
* This class is primarily used by the {@link org.luaj.vm2.lib.jse.LuajavaLib}, but can also be used directly when
|
||||
* working with Java/lua bindings.
|
||||
* <p>
|
||||
* To coerce scalar types, the various, generally the {@code valueOf(type)} methods on {@link LuaValue} may be used:
|
||||
* <ul>
|
||||
* <li>{@link LuaValue#valueOf(boolean)}</li>
|
||||
* <li>{@link LuaValue#valueOf(byte[])}</li>
|
||||
* <li>{@link LuaValue#valueOf(double)}</li>
|
||||
* <li>{@link LuaValue#valueOf(int)}</li>
|
||||
* <li>{@link LuaValue#valueOf(String)}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* To coerce arrays of objects and lists, the {@code listOf(..)} and {@code tableOf(...)} methods on {@link LuaValue}
|
||||
* may be used:
|
||||
* <ul>
|
||||
* <li>{@link LuaValue#listOf(LuaValue[])}</li>
|
||||
* <li>{@link LuaValue#listOf(LuaValue[], org.luaj.vm2.Varargs)}</li>
|
||||
* <li>{@link LuaValue#tableOf(LuaValue[])}</li>
|
||||
* <li>{@link LuaValue#tableOf(LuaValue[], LuaValue[], org.luaj.vm2.Varargs)}</li>
|
||||
* </ul>
|
||||
* The method {@link CoerceJavaToLua#coerce(Object)} looks as the type and dimesioning of the argument and tries to
|
||||
* guess the best fit for corrsponding lua scalar, table, or table of tables.
|
||||
*
|
||||
* @see CoerceJavaToLua#coerce(Object)
|
||||
* @see org.luaj.vm2.lib.jse.LuajavaLib
|
||||
*/
|
||||
public class CoerceJavaToLua {
|
||||
|
||||
interface Coercion {
|
||||
LuaValue coerce(Object javaValue);
|
||||
};
|
||||
|
||||
private static final class BoolCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
Boolean b = (Boolean) javaValue;
|
||||
return b ? LuaValue.TRUE : LuaValue.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IntCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
Number n = (Number) javaValue;
|
||||
return LuaInteger.valueOf(n.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CharCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
Character c = (Character) javaValue;
|
||||
return LuaInteger.valueOf(c.charValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DoubleCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
Number n = (Number) javaValue;
|
||||
return LuaDouble.valueOf(n.doubleValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StringCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
return LuaString.valueOf(javaValue.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BytesCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
return LuaValue.valueOf((byte[]) javaValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ClassCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
return JavaClass.forClass((Class<?>) javaValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InstanceCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
return new JavaInstance(javaValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ArrayCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
// should be userdata?
|
||||
return new JavaArray(javaValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LuaCoercion implements Coercion {
|
||||
public LuaValue coerce(Object javaValue) {
|
||||
return (LuaValue) javaValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// planetiler change: use immutable thread-safe map
|
||||
private static final Map<Class<?>, Coercion> COERCIONS;
|
||||
|
||||
static {
|
||||
Coercion boolCoercion = new BoolCoercion();
|
||||
Coercion intCoercion = new IntCoercion();
|
||||
Coercion charCoercion = new CharCoercion();
|
||||
Coercion doubleCoercion = new DoubleCoercion();
|
||||
Coercion stringCoercion = new StringCoercion();
|
||||
Coercion bytesCoercion = new BytesCoercion();
|
||||
Coercion classCoercion = new ClassCoercion();
|
||||
COERCIONS = Map.ofEntries(
|
||||
entry(Boolean.class, boolCoercion),
|
||||
entry(Byte.class, intCoercion),
|
||||
entry(Character.class, charCoercion),
|
||||
entry(Short.class, intCoercion),
|
||||
entry(Integer.class, intCoercion),
|
||||
entry(Long.class, doubleCoercion),
|
||||
entry(Float.class, doubleCoercion),
|
||||
entry(Double.class, doubleCoercion),
|
||||
entry(String.class, stringCoercion),
|
||||
entry(byte[].class, bytesCoercion),
|
||||
entry(Class.class, classCoercion)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerse a Java object to a corresponding lua value.
|
||||
* <p>
|
||||
* Integral types {@code boolean}, {@code byte}, {@code char}, and {@code int} will become {@link LuaInteger};
|
||||
* {@code long}, {@code float}, and {@code double} will become {@link LuaDouble}; {@code String} and {@code byte[]}
|
||||
* will become {@link LuaString}; types inheriting from {@link LuaValue} will be returned without coercion; other
|
||||
* types will become {@link LuaUserdata}.
|
||||
*
|
||||
* @param o Java object needing conversion
|
||||
* @return {@link LuaValue} corresponding to the supplied Java value.
|
||||
* @see LuaValue
|
||||
* @see LuaInteger
|
||||
* @see LuaDouble
|
||||
* @see LuaString
|
||||
* @see LuaUserdata
|
||||
*/
|
||||
public static LuaValue coerce(Object o) {
|
||||
if (o == null)
|
||||
return LuaValue.NIL;
|
||||
Class<?> clazz = o.getClass();
|
||||
// planetiler change: don't modify coercions
|
||||
Coercion c = COERCIONS.get(clazz);
|
||||
if (c == null) {
|
||||
c = o instanceof LuaValue ? luaCoercion : clazz.isArray() ? arrayCoercion : instanceCoercion;
|
||||
}
|
||||
return c.coerce(o);
|
||||
}
|
||||
|
||||
static final Coercion instanceCoercion = new InstanceCoercion();
|
||||
|
||||
static final Coercion arrayCoercion = new ArrayCoercion();
|
||||
|
||||
static final Coercion luaCoercion = new LuaCoercion();
|
||||
}
|
|
@ -0,0 +1,390 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009-2011 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.luaj.vm2.LuaString;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
/**
|
||||
* Modified version of {@link CoerceLuaToJava} that fixes a thread safety issue around concurrent map updates and also
|
||||
* removes usage of deprecated value class constructors.
|
||||
* <p>
|
||||
* Helper class to coerce values from lua to Java within the luajava library.
|
||||
* <p>
|
||||
* This class is primarily used by the {@link org.luaj.vm2.lib.jse.LuajavaLib}, but can also be used directly when
|
||||
* working with Java/lua bindings.
|
||||
* <p>
|
||||
* To coerce to specific Java values, generally the {@code toType()} methods on {@link LuaValue} may be used:
|
||||
* <ul>
|
||||
* <li>{@link LuaValue#toboolean()}</li>
|
||||
* <li>{@link LuaValue#tobyte()}</li>
|
||||
* <li>{@link LuaValue#tochar()}</li>
|
||||
* <li>{@link LuaValue#toshort()}</li>
|
||||
* <li>{@link LuaValue#toint()}</li>
|
||||
* <li>{@link LuaValue#tofloat()}</li>
|
||||
* <li>{@link LuaValue#todouble()}</li>
|
||||
* <li>{@link LuaValue#tojstring()}</li>
|
||||
* <li>{@link LuaValue#touserdata()}</li>
|
||||
* <li>{@link LuaValue#touserdata(Class)}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* For data in lua tables, the various methods on {@link LuaTable} can be used directly to convert data to something
|
||||
* more useful.
|
||||
*
|
||||
* @see org.luaj.vm2.lib.jse.LuajavaLib
|
||||
* @see CoerceJavaToLua
|
||||
*/
|
||||
public class CoerceLuaToJava {
|
||||
|
||||
static final int SCORE_NULL_VALUE = 0x10;
|
||||
static final int SCORE_WRONG_TYPE = 0x100;
|
||||
static final int SCORE_UNCOERCIBLE = 0x10000;
|
||||
|
||||
interface Coercion {
|
||||
int score(LuaValue value);
|
||||
|
||||
Object coerce(LuaValue value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce a LuaValue value to a specified java class
|
||||
*
|
||||
* @param value LuaValue to coerce
|
||||
* @param clazz Class to coerce into
|
||||
* @return Object of type clazz (or a subclass) with the corresponding value.
|
||||
*/
|
||||
public static Object coerce(LuaValue value, Class<?> clazz) {
|
||||
return getCoercion(clazz).coerce(value);
|
||||
}
|
||||
|
||||
static final Map<Class<?>, Coercion> COERCIONS = new ConcurrentHashMap<>();
|
||||
|
||||
static final class BoolCoercion implements Coercion {
|
||||
public String toString() {
|
||||
return "BoolCoercion()";
|
||||
}
|
||||
|
||||
public int score(LuaValue value) {
|
||||
return value.type() == LuaValue.TBOOLEAN ? 0 : 1;
|
||||
}
|
||||
|
||||
public Object coerce(LuaValue value) {
|
||||
return value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static final class NumericCoercion implements Coercion {
|
||||
static final int TARGET_TYPE_BYTE = 0;
|
||||
static final int TARGET_TYPE_CHAR = 1;
|
||||
static final int TARGET_TYPE_SHORT = 2;
|
||||
static final int TARGET_TYPE_INT = 3;
|
||||
static final int TARGET_TYPE_LONG = 4;
|
||||
static final int TARGET_TYPE_FLOAT = 5;
|
||||
static final int TARGET_TYPE_DOUBLE = 6;
|
||||
static final String[] TYPE_NAMES = {"byte", "char", "short", "int", "long", "float", "double"};
|
||||
final int targetType;
|
||||
|
||||
public String toString() {
|
||||
return "NumericCoercion(" + TYPE_NAMES[targetType] + ")";
|
||||
}
|
||||
|
||||
NumericCoercion(int targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
public int score(LuaValue value) {
|
||||
int fromStringPenalty = 0;
|
||||
if (value.type() == LuaValue.TSTRING) {
|
||||
value = value.tonumber();
|
||||
if (value.isnil()) {
|
||||
return SCORE_UNCOERCIBLE;
|
||||
}
|
||||
fromStringPenalty = 4;
|
||||
}
|
||||
if (value.isint()) {
|
||||
switch (targetType) {
|
||||
case TARGET_TYPE_BYTE: {
|
||||
int i = value.toint();
|
||||
return fromStringPenalty + ((i == (byte) i) ? 0 : SCORE_WRONG_TYPE);
|
||||
}
|
||||
case TARGET_TYPE_CHAR: {
|
||||
int i = value.toint();
|
||||
return fromStringPenalty + ((i == (byte) i) ? 1 : (i == (char) i) ? 0 : SCORE_WRONG_TYPE);
|
||||
}
|
||||
case TARGET_TYPE_SHORT: {
|
||||
int i = value.toint();
|
||||
return fromStringPenalty +
|
||||
((i == (byte) i) ? 1 : (i == (short) i) ? 0 : SCORE_WRONG_TYPE);
|
||||
}
|
||||
case TARGET_TYPE_INT: {
|
||||
int i = value.toint();
|
||||
return fromStringPenalty +
|
||||
((i == (byte) i) ? 2 : ((i == (char) i) || (i == (short) i)) ? 1 : 0);
|
||||
}
|
||||
case TARGET_TYPE_FLOAT:
|
||||
return fromStringPenalty + 1;
|
||||
case TARGET_TYPE_LONG:
|
||||
return fromStringPenalty + 1;
|
||||
case TARGET_TYPE_DOUBLE:
|
||||
return fromStringPenalty + 2;
|
||||
default:
|
||||
return SCORE_WRONG_TYPE;
|
||||
}
|
||||
} else if (value.isnumber()) {
|
||||
switch (targetType) {
|
||||
case TARGET_TYPE_BYTE:
|
||||
return SCORE_WRONG_TYPE;
|
||||
case TARGET_TYPE_CHAR:
|
||||
return SCORE_WRONG_TYPE;
|
||||
case TARGET_TYPE_SHORT:
|
||||
return SCORE_WRONG_TYPE;
|
||||
case TARGET_TYPE_INT:
|
||||
return SCORE_WRONG_TYPE;
|
||||
case TARGET_TYPE_LONG: {
|
||||
double d = value.todouble();
|
||||
return fromStringPenalty + ((d == (long) d) ? 0 : SCORE_WRONG_TYPE);
|
||||
}
|
||||
case TARGET_TYPE_FLOAT: {
|
||||
double d = value.todouble();
|
||||
return fromStringPenalty + ((d == (float) d) ? 0 : SCORE_WRONG_TYPE);
|
||||
}
|
||||
case TARGET_TYPE_DOUBLE: {
|
||||
double d = value.todouble();
|
||||
return fromStringPenalty + (((d == (long) d) || (d == (float) d)) ? 1 : 0);
|
||||
}
|
||||
default:
|
||||
return SCORE_WRONG_TYPE;
|
||||
}
|
||||
} else {
|
||||
return SCORE_UNCOERCIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
public Object coerce(LuaValue value) {
|
||||
// planetiler change: don't use deprecated value class constructors
|
||||
return switch (targetType) {
|
||||
case TARGET_TYPE_BYTE -> (byte) value.toint();
|
||||
case TARGET_TYPE_CHAR -> (char) value.toint();
|
||||
case TARGET_TYPE_SHORT -> (short) value.toint();
|
||||
case TARGET_TYPE_INT -> value.toint();
|
||||
case TARGET_TYPE_LONG -> (long) value.todouble();
|
||||
case TARGET_TYPE_FLOAT -> (float) value.todouble();
|
||||
case TARGET_TYPE_DOUBLE -> value.todouble();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static final class StringCoercion implements Coercion {
|
||||
public static final int TARGET_TYPE_STRING = 0;
|
||||
public static final int TARGET_TYPE_BYTES = 1;
|
||||
final int targetType;
|
||||
|
||||
public StringCoercion(int targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "StringCoercion(" + (targetType == TARGET_TYPE_STRING ? "String" : "byte[]") + ")";
|
||||
}
|
||||
|
||||
public int score(LuaValue value) {
|
||||
switch (value.type()) {
|
||||
case LuaValue.TSTRING:
|
||||
return value.checkstring().isValidUtf8() ?
|
||||
(targetType == TARGET_TYPE_STRING ? 0 : 1) :
|
||||
(targetType == TARGET_TYPE_BYTES ? 0 : SCORE_WRONG_TYPE);
|
||||
case LuaValue.TNIL:
|
||||
return SCORE_NULL_VALUE;
|
||||
default:
|
||||
return targetType == TARGET_TYPE_STRING ? SCORE_WRONG_TYPE : SCORE_UNCOERCIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
public Object coerce(LuaValue value) {
|
||||
if (value.isnil())
|
||||
return null;
|
||||
if (targetType == TARGET_TYPE_STRING)
|
||||
return value.tojstring();
|
||||
LuaString s = value.checkstring();
|
||||
byte[] b = new byte[s.m_length];
|
||||
s.copyInto(0, b, 0, b.length);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
static final class ArrayCoercion implements Coercion {
|
||||
final Class<?> componentType;
|
||||
final Coercion componentCoercion;
|
||||
|
||||
public ArrayCoercion(Class<?> componentType) {
|
||||
this.componentType = componentType;
|
||||
this.componentCoercion = getCoercion(componentType);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ArrayCoercion(" + componentType.getName() + ")";
|
||||
}
|
||||
|
||||
public int score(LuaValue value) {
|
||||
switch (value.type()) {
|
||||
case LuaValue.TTABLE:
|
||||
return value.length() == 0 ? 0 : componentCoercion.score(value.get(1));
|
||||
case LuaValue.TUSERDATA:
|
||||
return inheritanceLevels(componentType, value.touserdata().getClass().getComponentType());
|
||||
case LuaValue.TNIL:
|
||||
return SCORE_NULL_VALUE;
|
||||
default:
|
||||
return SCORE_UNCOERCIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
public Object coerce(LuaValue value) {
|
||||
switch (value.type()) {
|
||||
case LuaValue.TTABLE: {
|
||||
int n = value.length();
|
||||
Object a = Array.newInstance(componentType, n);
|
||||
for (int i = 0; i < n; i++)
|
||||
Array.set(a, i, componentCoercion.coerce(value.get(i + 1)));
|
||||
return a;
|
||||
}
|
||||
case LuaValue.TUSERDATA:
|
||||
return value.touserdata();
|
||||
case LuaValue.TNIL:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine levels of inheritance between a base class and a subclass
|
||||
*
|
||||
* @param baseclass base class to look for
|
||||
* @param subclass class from which to start looking
|
||||
* @return number of inheritance levels between subclass and baseclass, or SCORE_UNCOERCIBLE if not a subclass
|
||||
*/
|
||||
static int inheritanceLevels(Class<?> baseclass, Class<?> subclass) {
|
||||
if (subclass == null)
|
||||
return SCORE_UNCOERCIBLE;
|
||||
if (baseclass == subclass)
|
||||
return 0;
|
||||
int min = Math.min(SCORE_UNCOERCIBLE, inheritanceLevels(baseclass, subclass.getSuperclass()) + 1);
|
||||
Class<?>[] ifaces = subclass.getInterfaces();
|
||||
for (Class<?> iface : ifaces)
|
||||
min = Math.min(min, inheritanceLevels(baseclass, iface) + 1);
|
||||
return min;
|
||||
}
|
||||
|
||||
static final class ObjectCoercion implements Coercion {
|
||||
final Class<?> targetType;
|
||||
|
||||
ObjectCoercion(Class<?> targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ObjectCoercion(" + targetType.getName() + ")";
|
||||
}
|
||||
|
||||
public int score(LuaValue value) {
|
||||
switch (value.type()) {
|
||||
case LuaValue.TNUMBER:
|
||||
return inheritanceLevels(targetType, value.isint() ? Integer.class : Double.class);
|
||||
case LuaValue.TBOOLEAN:
|
||||
return inheritanceLevels(targetType, Boolean.class);
|
||||
case LuaValue.TSTRING:
|
||||
return inheritanceLevels(targetType, String.class);
|
||||
case LuaValue.TUSERDATA:
|
||||
return inheritanceLevels(targetType, value.touserdata().getClass());
|
||||
case LuaValue.TNIL:
|
||||
return SCORE_NULL_VALUE;
|
||||
default:
|
||||
return inheritanceLevels(targetType, value.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
public Object coerce(LuaValue value) {
|
||||
// planetiler change: don't use deprecated value class constructors
|
||||
return switch (value.type()) {
|
||||
case LuaValue.TNUMBER -> value.isint() ? (Object) value.toint() : (Object) value.todouble();
|
||||
case LuaValue.TBOOLEAN -> value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
|
||||
case LuaValue.TSTRING -> value.tojstring();
|
||||
case LuaValue.TUSERDATA -> value.optuserdata(targetType, null);
|
||||
case LuaValue.TNIL -> null;
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
Coercion boolCoercion = new BoolCoercion();
|
||||
Coercion byteCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_BYTE);
|
||||
Coercion charCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_CHAR);
|
||||
Coercion shortCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_SHORT);
|
||||
Coercion intCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_INT);
|
||||
Coercion longCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_LONG);
|
||||
Coercion floatCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_FLOAT);
|
||||
Coercion doubleCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_DOUBLE);
|
||||
Coercion stringCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_STRING);
|
||||
Coercion bytesCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_BYTES);
|
||||
|
||||
COERCIONS.put(Boolean.TYPE, boolCoercion);
|
||||
COERCIONS.put(Boolean.class, boolCoercion);
|
||||
COERCIONS.put(Byte.TYPE, byteCoercion);
|
||||
COERCIONS.put(Byte.class, byteCoercion);
|
||||
COERCIONS.put(Character.TYPE, charCoercion);
|
||||
COERCIONS.put(Character.class, charCoercion);
|
||||
COERCIONS.put(Short.TYPE, shortCoercion);
|
||||
COERCIONS.put(Short.class, shortCoercion);
|
||||
COERCIONS.put(Integer.TYPE, intCoercion);
|
||||
COERCIONS.put(Integer.class, intCoercion);
|
||||
COERCIONS.put(Long.TYPE, longCoercion);
|
||||
COERCIONS.put(Long.class, longCoercion);
|
||||
COERCIONS.put(Float.TYPE, floatCoercion);
|
||||
COERCIONS.put(Float.class, floatCoercion);
|
||||
COERCIONS.put(Double.TYPE, doubleCoercion);
|
||||
COERCIONS.put(Double.class, doubleCoercion);
|
||||
COERCIONS.put(String.class, stringCoercion);
|
||||
COERCIONS.put(byte[].class, bytesCoercion);
|
||||
}
|
||||
|
||||
static Coercion getCoercion(Class<?> c) {
|
||||
Coercion co = COERCIONS.get(c);
|
||||
if (co != null) {
|
||||
return co;
|
||||
}
|
||||
if (c.isArray()) {
|
||||
co = new ArrayCoercion(c.getComponentType());
|
||||
} else {
|
||||
co = new ObjectCoercion(c);
|
||||
}
|
||||
COERCIONS.putIfAbsent(c, co);
|
||||
return co;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import static org.luaj.vm2.lib.jse.CoerceLuaToJava.SCORE_WRONG_TYPE;
|
||||
|
||||
import com.onthegomap.planetiler.experimental.lua.LuaConversions;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntFunction;
|
||||
import org.luaj.vm2.LuaTables;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
/**
|
||||
* Call {@link #install()} to install planetiler's extra type conversions: {@link List}/table
|
||||
*/
|
||||
public class ExtraPlanetilerCoercions {
|
||||
public static void install() {}
|
||||
|
||||
static {
|
||||
coerceTable(List.class, LuaConversions::toJavaList);
|
||||
coerceTable(Collection.class, LuaConversions::toJavaCollection);
|
||||
coerceTable(Iterable.class, LuaConversions::toJavaIterable);
|
||||
coerceTable(Set.class, LuaConversions::toJavaSet);
|
||||
coerceTable(Map.class, LuaConversions::toJavaMap);
|
||||
CoerceLuaToJava.COERCIONS.put(Path.class, new PathCoercion());
|
||||
}
|
||||
|
||||
private static <T> void coerceTable(Class<T> clazz, Function<LuaValue, Object> coerce) {
|
||||
coerce(clazz, value -> value.type() == LuaValue.TTABLE ? 0 : SCORE_WRONG_TYPE, coerce);
|
||||
}
|
||||
|
||||
private static <T> void coerce(Class<T> clazz, ToIntFunction<LuaValue> score,
|
||||
Function<LuaValue, Object> coerce) {
|
||||
CoerceLuaToJava.COERCIONS.put(clazz, new ContainerCoercion(score, coerce));
|
||||
}
|
||||
|
||||
record ContainerCoercion(ToIntFunction<LuaValue> score, Function<LuaValue, Object> coerce)
|
||||
implements CoerceLuaToJava.Coercion {
|
||||
|
||||
@Override
|
||||
public int score(LuaValue value) {
|
||||
return score.applyAsInt(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object coerce(LuaValue value) {
|
||||
return coerce.apply(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PathCoercion implements CoerceLuaToJava.Coercion {
|
||||
@Override
|
||||
public int score(LuaValue value) {
|
||||
return value.isstring() || LuaTables.isArray(value) ? 0 : SCORE_WRONG_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object coerce(LuaValue value) {
|
||||
if (value.isstring()) {
|
||||
return Path.of(value.tojstring());
|
||||
}
|
||||
int len = value.length();
|
||||
String main = value.get(1).tojstring();
|
||||
String[] next = new String[len - 1];
|
||||
for (int i = 1; i < len; i++) {
|
||||
next[i - 1] = value.get(i + 1).tojstring();
|
||||
}
|
||||
return Path.of(main, next);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2011 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Converter;
|
||||
import com.onthegomap.planetiler.experimental.lua.LuaConversions;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.luaj.vm2.LuaString;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
/**
|
||||
* Modified version of {@link org.luaj.vm2.lib.jse.JavaClass} that uses {@link ConcurrentHashMap} instead of a
|
||||
* synchronized map to cache classes to improve performance, and also adds some utilities to improve java interop.
|
||||
* <p>
|
||||
* LuaValue that represents a Java class.
|
||||
* <p>
|
||||
* Will respond to get() and set() by returning field values, or java methods.
|
||||
* <p>
|
||||
* This class is not used directly. It is returned by calls to {@link CoerceJavaToLua#coerce(Object)} when a Class is
|
||||
* supplied.
|
||||
*
|
||||
* @see CoerceJavaToLua
|
||||
* @see CoerceLuaToJava
|
||||
*/
|
||||
class JavaClass extends JavaInstance {
|
||||
private static final Converter<String, String> CAMEL_TO_SNAKE_CASE =
|
||||
CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE);
|
||||
|
||||
static final Map<Class<?>, JavaClass> classes = new ConcurrentHashMap<>();
|
||||
|
||||
static final LuaValue NEW = valueOf("new");
|
||||
|
||||
private final Map<LuaValue, Field> fields;
|
||||
private final Map<LuaValue, LuaValue> methods;
|
||||
private final Map<LuaValue, Getter> getters;
|
||||
private final Map<LuaValue, Setter> setters;
|
||||
private final Map<LuaValue, Class<?>> innerclasses;
|
||||
public final boolean bindMethods;
|
||||
|
||||
static JavaClass forClass(Class<?> c) {
|
||||
// planetiler change: use ConcurrentHashMap instead of synchronized map to improve performance
|
||||
JavaClass j = classes.get(c);
|
||||
if (j == null) {
|
||||
j = classes.computeIfAbsent(c, JavaClass::new);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
JavaClass(Class<?> c) {
|
||||
super(c);
|
||||
this.jclass = this;
|
||||
this.bindMethods = c.isAnnotationPresent(LuaBindMethods.class);
|
||||
// planetiler change: compute these maps eagerly
|
||||
fields = computeFields();
|
||||
var result = computeMethods();
|
||||
methods = result.methods;
|
||||
getters = result.getters;
|
||||
setters = result.setters;
|
||||
innerclasses = computeInnerClasses();
|
||||
}
|
||||
|
||||
private Map<LuaValue, Field> computeFields() {
|
||||
Map<LuaValue, Field> tmpFields = new HashMap<>();
|
||||
Field[] f = ((Class<?>) m_instance).getFields();
|
||||
for (Field fi : f) {
|
||||
if (Modifier.isPublic(fi.getModifiers())) {
|
||||
tmpFields.put(LuaValue.valueOf(fi.getName()), fi);
|
||||
try {
|
||||
if (!fi.isAccessible()) {
|
||||
fi.setAccessible(true);
|
||||
}
|
||||
} catch (SecurityException | InaccessibleObjectException s) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// planetiler change: add snake_case aliases for camelCase methods
|
||||
putAliases(tmpFields);
|
||||
return Map.copyOf(tmpFields);
|
||||
}
|
||||
|
||||
private Methods computeMethods() {
|
||||
Map<LuaValue, LuaValue> tmpMethods = new HashMap<>();
|
||||
Map<LuaValue, Getter> tmpGettters = new HashMap<>();
|
||||
Map<LuaValue, Setter> tmpSetters = new HashMap<>();
|
||||
Map<String, List<JavaMethod>> namedlists = new HashMap<>();
|
||||
Class<?> clazz = (Class<?>) m_instance;
|
||||
Set<String> recordComponents =
|
||||
clazz.isRecord() ? Arrays.stream(clazz.getRecordComponents()).map(c -> c.getName()).collect(Collectors.toSet()) :
|
||||
Set.of();
|
||||
for (Method mi : clazz.getMethods()) {
|
||||
if (Modifier.isPublic(mi.getModifiers())) {
|
||||
String name = mi.getName();
|
||||
// planetiler change: allow methods annotated with @LuaGetter or @LuaSetter to simulate property access
|
||||
// also allow record components to be accessed as properties
|
||||
if ((recordComponents.contains(name) || mi.isAnnotationPresent(LuaGetter.class)) &&
|
||||
mi.getParameterCount() == 0) {
|
||||
tmpGettters.put(LuaString.valueOf(name), new Getter(mi));
|
||||
} else if (mi.isAnnotationPresent(LuaSetter.class)) {
|
||||
tmpSetters.put(LuaString.valueOf(name), new Setter(mi, mi.getParameterTypes()[0]));
|
||||
}
|
||||
namedlists.computeIfAbsent(name, k -> new ArrayList<>()).add(JavaMethod.forMethod(mi));
|
||||
}
|
||||
}
|
||||
Constructor<?>[] c = ((Class<?>) m_instance).getConstructors();
|
||||
List<JavaConstructor> list = new ArrayList<>();
|
||||
for (Constructor<?> constructor : c) {
|
||||
if (Modifier.isPublic(constructor.getModifiers())) {
|
||||
list.add(JavaConstructor.forConstructor(constructor));
|
||||
}
|
||||
}
|
||||
switch (list.size()) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
tmpMethods.put(NEW, list.get(0));
|
||||
break;
|
||||
default:
|
||||
tmpMethods.put(NEW,
|
||||
JavaConstructor.forConstructors(list.toArray(JavaConstructor[]::new)));
|
||||
break;
|
||||
}
|
||||
|
||||
for (Entry<String, List<JavaMethod>> e : namedlists.entrySet()) {
|
||||
String name = e.getKey();
|
||||
List<JavaMethod> classMethods = e.getValue();
|
||||
LuaValue luaMethod = classMethods.size() == 1 ?
|
||||
classMethods.get(0) :
|
||||
JavaMethod.forMethods(classMethods.toArray(JavaMethod[]::new));
|
||||
tmpMethods.put(LuaValue.valueOf(name), luaMethod);
|
||||
}
|
||||
|
||||
// planetiler change: add snake_case aliases for camelCase methods
|
||||
putAliases(tmpMethods);
|
||||
putAliases(tmpGettters);
|
||||
putAliases(tmpSetters);
|
||||
return new Methods(
|
||||
Map.copyOf(tmpMethods),
|
||||
Map.copyOf(tmpGettters),
|
||||
Map.copyOf(tmpSetters)
|
||||
);
|
||||
}
|
||||
|
||||
record Methods(Map<LuaValue, LuaValue> methods, Map<LuaValue, Getter> getters, Map<LuaValue, Setter> setters) {}
|
||||
|
||||
private Map<LuaValue, Class<?>> computeInnerClasses() {
|
||||
Map<LuaValue, Class<?>> result = new HashMap<>();
|
||||
Class<?>[] c = ((Class<?>) m_instance).getClasses();
|
||||
for (Class<?> ci : c) {
|
||||
String name = ci.getName();
|
||||
String stub = name.substring(Math.max(name.lastIndexOf('$'), name.lastIndexOf('.')) + 1);
|
||||
result.put(LuaValue.valueOf(stub), ci);
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private <T> void putAliases(Map<LuaValue, T> map) {
|
||||
for (var entry : List.copyOf(map.entrySet())) {
|
||||
String key = entry.getKey().tojstring();
|
||||
String key2;
|
||||
if (LuaConversions.LUA_AND_NOT_JAVA_KEYWORDS.contains(key)) {
|
||||
key2 = key.toUpperCase();
|
||||
} else {
|
||||
key2 = CAMEL_TO_SNAKE_CASE.convert(key);
|
||||
}
|
||||
map.putIfAbsent(LuaValue.valueOf(key2), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Field getField(LuaValue key) {
|
||||
return fields.get(key);
|
||||
}
|
||||
|
||||
LuaValue getMethod(LuaValue key) {
|
||||
return methods.get(key);
|
||||
}
|
||||
|
||||
Map<LuaValue, LuaValue> getMethods() {
|
||||
return methods;
|
||||
}
|
||||
|
||||
Class<?> getInnerClass(LuaValue key) {
|
||||
return innerclasses.get(key);
|
||||
}
|
||||
|
||||
public LuaValue getConstructor() {
|
||||
return getMethod(NEW);
|
||||
}
|
||||
|
||||
public Getter getGetter(LuaValue key) {
|
||||
return getters.get(key);
|
||||
}
|
||||
|
||||
public Setter getSetter(LuaValue key) {
|
||||
return setters.get(key);
|
||||
}
|
||||
|
||||
public record Getter(Method method) {
|
||||
|
||||
Object get(Object obj) throws InvocationTargetException, IllegalAccessException {
|
||||
return method.invoke(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public record Setter(Method method, Class<?> type) {
|
||||
|
||||
void set(Object obj, Object value) throws InvocationTargetException, IllegalAccessException {
|
||||
method.invoke(obj, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2011 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLuaTable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaFunction;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaUserdata;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
|
||||
/**
|
||||
* Modified version of {@link JavaInstance} with some tweaks to improve java interop.
|
||||
* <p>
|
||||
* LuaValue that represents a Java instance.
|
||||
* <p>
|
||||
* Will respond to get() and set() by returning field values or methods.
|
||||
* <p>
|
||||
* This class is not used directly. It is returned by calls to {@link CoerceJavaToLua#coerce(Object)} when a subclass of
|
||||
* Object is supplied.
|
||||
*
|
||||
* @see CoerceJavaToLua
|
||||
* @see CoerceLuaToJava
|
||||
*/
|
||||
class JavaInstance extends LuaUserdata {
|
||||
|
||||
volatile JavaClass jclass;
|
||||
private final Map<LuaValue, LuaValue> boundMethods;
|
||||
|
||||
JavaInstance(Object instance) {
|
||||
super(instance);
|
||||
// planetiler change: when class annotated with @LuaBindMethods, allow methods to be called with instance.method()
|
||||
if (m_instance.getClass().isAnnotationPresent(LuaBindMethods.class)) {
|
||||
boundMethods = jclass().getMethods().entrySet().stream().collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> new BoundMethod(entry.getValue())
|
||||
));
|
||||
} else {
|
||||
boundMethods = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaTable checktable() {
|
||||
// planetiler change: allow maps and lists to be accessed as tables
|
||||
return switch (m_instance) {
|
||||
case Collection<?> c -> toLuaTable(c);
|
||||
case Map<?, ?> m -> toLuaTable(m);
|
||||
default -> super.checktable();
|
||||
};
|
||||
}
|
||||
|
||||
// planetiler change: allow methods on classes annotated with @LuaBindMethods to be called with instance.method()
|
||||
private class BoundMethod extends LuaFunction {
|
||||
|
||||
private final LuaValue method;
|
||||
|
||||
BoundMethod(LuaValue method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
return method.call(JavaInstance.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
return method.call(JavaInstance.this, arg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2) {
|
||||
return method.call(JavaInstance.this, arg1, arg2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) {
|
||||
return method.invoke(LuaValue.varargsOf(new LuaValue[]{JavaInstance.this, arg1, arg2, arg3})).arg(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
return method.invoke(LuaValue.varargsOf(JavaInstance.this, args));
|
||||
}
|
||||
}
|
||||
|
||||
private JavaClass jclass() {
|
||||
if (jclass == null) {
|
||||
synchronized (this) {
|
||||
if (jclass == null) {
|
||||
jclass = JavaClass.forClass(m_instance.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
return jclass;
|
||||
}
|
||||
|
||||
public LuaValue get(LuaValue key) {
|
||||
// planetiler change: allow lists to be accessed as tables
|
||||
if (key.isnumber() && m_instance instanceof List<?> c) {
|
||||
int idx = key.toint();
|
||||
return idx <= 0 || idx > c.size() ? LuaValue.NIL : CoerceJavaToLua.coerce(c.get(idx - 1));
|
||||
}
|
||||
JavaClass clazz = jclass();
|
||||
Field f = clazz.getField(key);
|
||||
if (f != null)
|
||||
try {
|
||||
return CoerceJavaToLua.coerce(f.get(m_instance));
|
||||
} catch (Exception e) {
|
||||
throw new LuaError(e);
|
||||
}
|
||||
// planetiler change: allow getter methods
|
||||
var getter = clazz.getGetter(key);
|
||||
if (getter != null) {
|
||||
try {
|
||||
return CoerceJavaToLua.coerce(getter.get(m_instance));
|
||||
} catch (Exception e) {
|
||||
throw new LuaError(e);
|
||||
}
|
||||
}
|
||||
LuaValue m = boundMethods != null ? boundMethods.get(key) : clazz.getMethod(key);
|
||||
if (m != null)
|
||||
return m;
|
||||
Class<?> c = clazz.getInnerClass(key);
|
||||
if (c != null)
|
||||
return JavaClass.forClass(c);
|
||||
|
||||
// planetiler change: allow maps to be accessed as tables
|
||||
if (m_instance instanceof Map<?, ?> map) {
|
||||
Object key2 = CoerceLuaToJava.coerce(key, Object.class);
|
||||
if (key2 != null) {
|
||||
Object value = map.get(key2);
|
||||
if (value != null) {
|
||||
return CoerceJavaToLua.coerce(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
public void set(LuaValue key, LuaValue value) {
|
||||
// planetiler change: allow lists to be accessed as tables
|
||||
if (key.isnumber() && m_instance instanceof List c) {
|
||||
c.set(key.toint() - 1, CoerceLuaToJava.coerce(value, Object.class));
|
||||
return;
|
||||
}
|
||||
JavaClass clazz = jclass();
|
||||
Field f = clazz.getField(key);
|
||||
if (f != null)
|
||||
try {
|
||||
f.set(m_instance, CoerceLuaToJava.coerce(value, f.getType()));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
throw new LuaError(e);
|
||||
}
|
||||
// planetiler change: allow setter methods
|
||||
var setter = clazz.getSetter(key);
|
||||
if (setter != null) {
|
||||
try {
|
||||
setter.set(m_instance, CoerceLuaToJava.coerce(value, setter.type()));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
throw new LuaError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// planetiler change: allow maps to be accessed as tables
|
||||
if (m_instance instanceof Map map) {
|
||||
Object key2 = CoerceLuaToJava.coerce(key, Object.class);
|
||||
if (key2 != null) {
|
||||
Object value2 = CoerceLuaToJava.coerce(value, Object.class);
|
||||
if (value2 == null) {
|
||||
map.remove(key2);
|
||||
} else {
|
||||
map.put(key2, value2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.set(key, value);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2011 Luaj.org. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
******************************************************************************/
|
||||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaFunction;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
|
||||
/**
|
||||
* Modified version of {@link JavaMethod} with concurrency fixes and a fix to how varargs are handled.
|
||||
* <p>
|
||||
* LuaValue that represents a Java method.
|
||||
* <p>
|
||||
* Can be invoked via call(LuaValue...) and related methods.
|
||||
* <p>
|
||||
* This class is not used directly. It is returned by calls to calls to {@link JavaInstance#get(LuaValue key)} when a
|
||||
* method is named.
|
||||
*
|
||||
* @see CoerceJavaToLua
|
||||
* @see CoerceLuaToJava
|
||||
*/
|
||||
class JavaMethod extends JavaMember {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JavaMethod(" + method + ")";
|
||||
}
|
||||
|
||||
// planetiler change: use concurrent hash map instead of synchronized map
|
||||
static final Map<Method, JavaMethod> methods = new ConcurrentHashMap<>();
|
||||
|
||||
static JavaMethod forMethod(Method m) {
|
||||
JavaMethod j = methods.get(m);
|
||||
if (j == null)
|
||||
j = methods.computeIfAbsent(m, JavaMethod::new);
|
||||
return j;
|
||||
}
|
||||
|
||||
static LuaFunction forMethods(JavaMethod[] m) {
|
||||
return new Overload(m);
|
||||
}
|
||||
|
||||
final Method method;
|
||||
|
||||
JavaMethod(Method m) {
|
||||
super(m.getParameterTypes(), m.getModifiers());
|
||||
this.method = m;
|
||||
try {
|
||||
if (!m.isAccessible())
|
||||
m.setAccessible(true);
|
||||
} catch (SecurityException | InaccessibleObjectException s) {
|
||||
}
|
||||
}
|
||||
|
||||
public LuaValue call() {
|
||||
return error("method cannot be called without instance");
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg) {
|
||||
return invokeMethod(arg.checkuserdata(), LuaValue.NONE);
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2) {
|
||||
return invokeMethod(arg1.checkuserdata(), arg2);
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) {
|
||||
return invokeMethod(arg1.checkuserdata(), LuaValue.varargsOf(arg2, arg3));
|
||||
}
|
||||
|
||||
public Varargs invoke(Varargs args) {
|
||||
return invokeMethod(args.checkuserdata(1), args.subargs(2));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object[] convertArgs(Varargs args) {
|
||||
Object[] a;
|
||||
if (varargs == null) {
|
||||
a = new Object[fixedargs.length];
|
||||
for (int i = 0; i < a.length; i++)
|
||||
a[i] = fixedargs[i].coerce(args.arg(i + 1));
|
||||
} else {
|
||||
// planetiler fix: pass last arg through as vararg array parameter
|
||||
a = new Object[fixedargs.length + 1];
|
||||
for (int i = 0; i < fixedargs.length; i++)
|
||||
a[i] = fixedargs[i].coerce(args.arg(i + 1));
|
||||
a[a.length - 1] = varargs.coerce(LuaValue.listOf(null, args.subargs(fixedargs.length + 1)));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@Override
|
||||
int score(Varargs args) {
|
||||
int n = args.narg();
|
||||
int s = n > fixedargs.length && varargs == null ? CoerceLuaToJava.SCORE_WRONG_TYPE * (n - fixedargs.length) : 0;
|
||||
for (int j = 0; j < fixedargs.length; j++)
|
||||
s += fixedargs[j].score(args.arg(j + 1));
|
||||
// planetiler fix: use component coercion, not array coercion
|
||||
if (varargs instanceof CoerceLuaToJava.ArrayCoercion arrayCoercion)
|
||||
for (int k = fixedargs.length; k < n; k++)
|
||||
s += arrayCoercion.componentCoercion.score(args.arg(k + 1));
|
||||
return s;
|
||||
}
|
||||
|
||||
LuaValue invokeMethod(Object instance, Varargs args) {
|
||||
Object[] a = convertArgs(args);
|
||||
try {
|
||||
return CoerceJavaToLua.coerce(method.invoke(instance, a));
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new LuaError(e.getTargetException());
|
||||
} catch (Exception e) {
|
||||
return LuaValue.error("coercion error " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LuaValue that represents an overloaded Java method.
|
||||
* <p>
|
||||
* On invocation, will pick the best method from the list, and invoke it.
|
||||
* <p>
|
||||
* This class is not used directly. It is returned by calls to calls to {@link JavaInstance#get(LuaValue key)} when an
|
||||
* overloaded method is named.
|
||||
*/
|
||||
static class Overload extends LuaFunction {
|
||||
|
||||
final JavaMethod[] methods;
|
||||
|
||||
Overload(JavaMethod[] methods) {
|
||||
this.methods = methods;
|
||||
}
|
||||
|
||||
public LuaValue call() {
|
||||
return error("method cannot be called without instance");
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg) {
|
||||
return invokeBestMethod(arg.checkuserdata(), LuaValue.NONE);
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2) {
|
||||
return invokeBestMethod(arg1.checkuserdata(), arg2);
|
||||
}
|
||||
|
||||
public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) {
|
||||
return invokeBestMethod(arg1.checkuserdata(), LuaValue.varargsOf(arg2, arg3));
|
||||
}
|
||||
|
||||
public Varargs invoke(Varargs args) {
|
||||
return invokeBestMethod(args.checkuserdata(1), args.subargs(2));
|
||||
}
|
||||
|
||||
private LuaValue invokeBestMethod(Object instance, Varargs args) {
|
||||
JavaMethod best = null;
|
||||
int score = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < methods.length; i++) {
|
||||
int s = methods[i].score(args);
|
||||
if (s < score) {
|
||||
score = s;
|
||||
best = methods[i];
|
||||
if (score == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// any match?
|
||||
if (best == null)
|
||||
LuaValue.error("no coercible public method");
|
||||
|
||||
// invoke it
|
||||
return best.invokeMethod(instance, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that allows methods on a class to be called from lua with instance.method(), or for those methods to be
|
||||
* detached from the instance and called on their own.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface LuaBindMethods {
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that allows a method to intercept calls to get {@code instance.property} from lua.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface LuaGetter {
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.luaj.vm2.lib.jse;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that allows a method to intercept calls to set {@code instance.property = value} from lua.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface LuaSetter {
|
||||
}
|
|
@ -0,0 +1,742 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJava;
|
||||
import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLua;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.jse.LuaGetter;
|
||||
import org.luaj.vm2.lib.jse.LuaSetter;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class LuaEnvironmentTests {
|
||||
@Test
|
||||
void testCallMethod() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return 1
|
||||
end
|
||||
""");
|
||||
assertConvertsTo(1, env.main.call());
|
||||
assertConvertsTo("1", env.main.call());
|
||||
assertConvertsTo(1L, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindProfile() {
|
||||
var env = load("""
|
||||
planetiler.output.name = "name"
|
||||
planetiler.output.attribution = "attribution"
|
||||
planetiler.output.description = "description"
|
||||
planetiler.output.version = "version"
|
||||
|
||||
function planetiler.process_feature()
|
||||
return 1
|
||||
end
|
||||
function planetiler.finish()
|
||||
return 1
|
||||
end
|
||||
""");
|
||||
assertConvertsTo(1, env.planetiler.process_feature.call());
|
||||
assertConvertsTo(1, env.planetiler.finish.call());
|
||||
assertEquals("name", env.planetiler.output.name);
|
||||
assertEquals("attribution", env.planetiler.output.attribution);
|
||||
assertEquals("description", env.planetiler.output.description);
|
||||
assertEquals("version", env.planetiler.output.version);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOutputPath() {
|
||||
var env = load("""
|
||||
""");
|
||||
assertEquals(Path.of("data", "output.mbtiles"), env.planetiler.output.path);
|
||||
var env2 = load("""
|
||||
planetiler.output.path = "output.pmtiles"
|
||||
""");
|
||||
assertEquals(Path.of("output.pmtiles"), env2.planetiler.output.path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallExposedClassMethod() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return GeoUtils:meters_to_pixel_at_equator(14, 150)
|
||||
end
|
||||
""");
|
||||
assertEquals(15.699197, env.main.call().todouble(), 1e-5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallJavaMethod() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call(1) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(int arg) {
|
||||
return arg + 1;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(3, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallJavaMethodUsingLowerSnakeCase() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call_method(1) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int callMethod(int arg) {
|
||||
return arg + 1;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(3, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallJavaMethodWith4Args() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call(1, 2, 3, 4) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(int a, int b, int c, int d) {
|
||||
return a + b + c + d;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(11, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPassArrayToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3}) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(int[] args) {
|
||||
return IntStream.of(args).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPassBoxedArrayToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3}) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(Integer[] args) {
|
||||
return Stream.of(args).mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPassArrayToLua() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int[] call(int[] args) {
|
||||
return args;
|
||||
}
|
||||
}));
|
||||
assertArrayEquals(new int[]{1, 2, 3}, toJava(env.main.call(), int[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void passListToLua() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3}) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(List<Integer> args) {
|
||||
return args.stream().mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void passJavaListToLua() {
|
||||
var env = load("""
|
||||
function main(arg)
|
||||
return obj:call(arg) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(List<Integer> args) {
|
||||
return args.stream().mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call(toLua(List.of(1, 2, 3))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void passListToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public List<Integer> call(int[] args) {
|
||||
return IntStream.of(args).boxed().toList();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(List.of(1, 2, 3), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void passCollectionToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(Collection<Integer> args) {
|
||||
return args.stream().mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(6, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void passSetToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({1, 2, 3, 3})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(Set<Integer> args) {
|
||||
return args.stream().mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(6, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void passSetFromTableToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({[1] = true, [2] = true, [3] = true})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(Set<Integer> args) {
|
||||
return args.stream().mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(6, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void passMapToJava() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({[1] = "one", [2] = "two", [3] = "three"})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public String call(Map<Integer, String> args) {
|
||||
return args.get(1) + " " + args.get(3);
|
||||
}
|
||||
}));
|
||||
assertConvertsTo("one three", env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallPrimitiveJavaVarArgsMethod() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call(1, 2, 3) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(int... args) {
|
||||
return IntStream.of(args).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallBoxedJavaVarArgsMethod() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call(1, 2, 3) + 1
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(Integer... args) {
|
||||
return Stream.of(args).mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(7, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chooseVarArgMethodOverOthers() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return {
|
||||
obj:call(),
|
||||
obj:call(1),
|
||||
obj:call(1, 2),
|
||||
obj:call(1, 2, 3),
|
||||
obj:call(1, 2, 3, 4),
|
||||
obj:call(1, 2, 3, 4, 5),
|
||||
obj:call(1, 2, 3, 4, 5, 6),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
|
||||
}
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int call(int a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
public int call(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
public int call(int a, int b, int c) {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
public int call(int a, int b, int c, int d) {
|
||||
return a + b + c + d;
|
||||
}
|
||||
|
||||
public int call(int a, int b, int c, int d, int... rest) {
|
||||
return a + b + c + d + IntStream.of(rest).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(List.class, List.of(
|
||||
0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55
|
||||
), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chooseVarArgMethodOverOthersBoxed() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return {
|
||||
obj:call(),
|
||||
obj:call(1),
|
||||
obj:call(1, 2),
|
||||
obj:call(1, 2, 3),
|
||||
obj:call(1, 2, 3, 4),
|
||||
obj:call(1, 2, 3, 4, 5),
|
||||
obj:call(1, 2, 3, 4, 5, 6),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9),
|
||||
obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
|
||||
}
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public Integer call() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Integer call(Integer a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
public Integer call(Integer a, Integer b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
public Integer call(Integer a, Integer b, Integer c) {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
public Integer call(Integer a, Integer b, Integer c, Integer d) {
|
||||
return a + b + c + d;
|
||||
}
|
||||
|
||||
public Integer call(Integer a, Integer b, Integer c, Integer d, Integer... rest) {
|
||||
return a + b + c + d + Stream.of(rest).mapToInt(i -> i).sum();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(List.class, List.of(
|
||||
0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55
|
||||
), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoercesPathFromString() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call("test.java")
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public String call(Path path) {
|
||||
return path.toString();
|
||||
}
|
||||
}));
|
||||
assertConvertsTo("test.java", env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoercesPathFromList() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call({"a", "b", "c.java"})
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public Path call(Path path) {
|
||||
return path;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(Path.of("a", "b", "c.java"), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGettersAndSetters() {
|
||||
var obj = new Object() {
|
||||
public int value = 0;
|
||||
private int value2 = 0;
|
||||
|
||||
@LuaSetter
|
||||
public void value2(int v) {
|
||||
this.value2 = v;
|
||||
}
|
||||
|
||||
@LuaGetter
|
||||
public int value2() {
|
||||
return this.value2 + 1;
|
||||
}
|
||||
};
|
||||
var env = load("""
|
||||
function main()
|
||||
obj.value = 1
|
||||
obj.value2 = 2;
|
||||
return obj.value2;
|
||||
end
|
||||
""", Map.of("obj", obj));
|
||||
assertConvertsTo(3, env.main.call());
|
||||
assertEquals(1, obj.value);
|
||||
assertEquals(2, obj.value2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecord() {
|
||||
record Record(int a, String b) {
|
||||
public int c() {
|
||||
return a + 1;
|
||||
}
|
||||
}
|
||||
var env = load("""
|
||||
function main()
|
||||
return {obj.a + 1, obj.b .. 1, obj:c()};
|
||||
end
|
||||
""", Map.of("obj", new Record(1, "2")));
|
||||
assertConvertsTo(List.class, List.of(2, "21", 2), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetLanguages() {
|
||||
var env = load("""
|
||||
planetiler.languages = {"en", "es"}
|
||||
""");
|
||||
assertEquals(List.of("en", "es"), env.runner.getDefaultLanguages());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFetchWikidataTranslations() {
|
||||
var env = load("""
|
||||
planetiler.fetch_wikidata_translations()
|
||||
planetiler.fetch_wikidata_translations("data/sources/translations.json")
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddSource() {
|
||||
var env = load("""
|
||||
planetiler.add_source('osm', {
|
||||
type = 'osm',
|
||||
path = 'file.osm.pbf'
|
||||
})
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadArg() {
|
||||
var env = LuaEnvironment.loadScript(Arguments.of(
|
||||
"key", "value"
|
||||
), """
|
||||
function main()
|
||||
return planetiler.args:get_string("key")
|
||||
end
|
||||
""", "script.lua", Map.of());
|
||||
assertConvertsTo("value", env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadConfigRecord() {
|
||||
var env = LuaEnvironment.loadScript(Arguments.of(
|
||||
"threads", "99"
|
||||
), """
|
||||
function main()
|
||||
return planetiler.config.threads
|
||||
end
|
||||
""", "script.lua", Map.of());
|
||||
assertConvertsTo(99, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTranslations() {
|
||||
var env = load("""
|
||||
planetiler.languages = {"en", "es"}
|
||||
function main()
|
||||
return planetiler.translations:get_translations({
|
||||
['name:en'] = "english name",
|
||||
['name:es'] = "spanish name",
|
||||
['name:de'] = "german name",
|
||||
})
|
||||
end
|
||||
""");
|
||||
assertConvertsTo(Map.class, Map.of(
|
||||
"name:en", "english name",
|
||||
"name:es", "spanish name"
|
||||
), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transliterate() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return planetiler.translations:transliterate("日本")
|
||||
end
|
||||
""");
|
||||
assertConvertsTo("rì běn", env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStats() {
|
||||
var env = load("""
|
||||
planetiler.stats:data_error('lua_error')
|
||||
""");
|
||||
assertEquals(Map.of(
|
||||
"lua_error", 1L
|
||||
), env.planetiler.stats.dataErrors());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIterateOverList() {
|
||||
var env = load("""
|
||||
function main()
|
||||
local result = 0
|
||||
for i, match in ipairs(obj:call()) do
|
||||
result = result + match
|
||||
end
|
||||
return result
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public List<Integer> call() {
|
||||
return List.of(1, 2, 3);
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(6, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIterateOverMap() {
|
||||
var env = load("""
|
||||
function main()
|
||||
local result = 0
|
||||
for k, v in pairs(obj:call()) do
|
||||
result = result + k + v
|
||||
end
|
||||
return result
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public Map<Integer, Integer> call() {
|
||||
return Map.of(1, 2, 3, 4);
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(10, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvokeReservedKeyword() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:AND()
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int and() {
|
||||
return 1;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(1, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPickVarArgOverList() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return {obj:call(), obj:call('a'), obj:call({'a'})}
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(String... args) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int call(List<String> args) {
|
||||
return 2;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(List.class, List.of(1, 1, 2), env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPickVarArgOverListAfterFirstArg() {
|
||||
var env = load("""
|
||||
function main()
|
||||
return {obj:call('a'), obj:call('a', 'a'), obj:call('a', {'a'})}
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int call(String arg, String... args) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int call(String arg, List<String> args) {
|
||||
return 2;
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(List.class, List.of(1, 1, 2), env.main.call());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, 1, 2})
|
||||
void testGetWithIndexFromList(int idx) {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call()[%d]
|
||||
end
|
||||
""".formatted(idx), Map.of("obj", new Object() {
|
||||
public List<Integer> call() {
|
||||
return List.of(1);
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(Integer.class, idx == 1 ? 1 : 0, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetWithIndexFromList() {
|
||||
var env = load("""
|
||||
function main()
|
||||
local list = obj:call()
|
||||
list[1] = 2
|
||||
return list[1]
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public List<Integer> call() {
|
||||
return new ArrayList<>(List.of(1));
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(2, env.main.call());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"a", "b", "c"})
|
||||
void testGetFromMap(String value) {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call()["%s"]
|
||||
end
|
||||
""".formatted(value), Map.of("obj", new Object() {
|
||||
public Map<String, Integer> call() {
|
||||
return Map.of("b", 1);
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(Integer.class, value.equals("b") ? 1 : 0, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetMap() {
|
||||
var env = load("""
|
||||
function main()
|
||||
local list = obj:call()
|
||||
list["a"] = "c"
|
||||
return list["a"]
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public Map<String, String> call() {
|
||||
return new HashMap<>(Map.of("a", "a"));
|
||||
}
|
||||
}));
|
||||
assertConvertsTo("c", env.main.call());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, 1, 2})
|
||||
void testGetFromArray(int idx) {
|
||||
var env = load("""
|
||||
function main()
|
||||
return obj:call()[%d]
|
||||
end
|
||||
""".formatted(idx), Map.of("obj", new Object() {
|
||||
public int[] call() {
|
||||
return new int[]{1};
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(Integer.class, idx == 1 ? 1 : 0, env.main.call());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetArray() {
|
||||
var env = load("""
|
||||
function main()
|
||||
local list = obj:call()
|
||||
list[1] = 2
|
||||
return list[1]
|
||||
end
|
||||
""", Map.of("obj", new Object() {
|
||||
public int[] call() {
|
||||
return new int[]{1};
|
||||
}
|
||||
}));
|
||||
assertConvertsTo(2, env.main.call());
|
||||
}
|
||||
|
||||
private static <T> void assertConvertsTo(T java, LuaValue lua) {
|
||||
assertConvertsTo(java.getClass(), java, lua);
|
||||
}
|
||||
|
||||
private static <T> void assertConvertsTo(Class<?> clazz, T java, LuaValue lua) {
|
||||
assertEquals(java, toJava(lua, clazz));
|
||||
}
|
||||
|
||||
private static LuaEnvironment load(String script) {
|
||||
return load(script, Map.of());
|
||||
}
|
||||
|
||||
private static LuaEnvironment load(String script, Map<String, ?> extras) {
|
||||
return LuaEnvironment.loadScript(Arguments.of(), script, "script.lua", extras);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import com.onthegomap.planetiler.TestUtils;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
|
||||
class LuaProfilesTest {
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testPower() throws IOException {
|
||||
return validate("power.lua");
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testRoadsMain() throws IOException {
|
||||
return validate("roads_main.lua");
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testRoads() throws IOException {
|
||||
return validate("roads.lua");
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> testMultifile() throws IOException {
|
||||
return validate("multifile.lua");
|
||||
}
|
||||
|
||||
private static String readResource(String resource) throws IOException {
|
||||
try (var is = LuaProfilesTest.class.getResourceAsStream(resource)) {
|
||||
return new String(Objects.requireNonNull(is).readAllBytes());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<DynamicNode> validate(String name) throws IOException {
|
||||
return validate(name, null);
|
||||
}
|
||||
|
||||
private static Stream<DynamicNode> validate(String name, String spec) throws IOException {
|
||||
LuaEnvironment env = LuaEnvironment.loadScript(Arguments.of(), readResource("/" + name), name);
|
||||
return TestUtils.validateProfile(
|
||||
env.profile,
|
||||
SchemaSpecification.load(readResource("/" + (spec != null ? spec : env.planetiler.examples)))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
import com.onthegomap.planetiler.validator.BaseSchemaValidator;
|
||||
import com.onthegomap.planetiler.validator.SchemaSpecification;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
class LuaValidatorTest {
|
||||
private final Arguments args = Arguments.of();
|
||||
@TempDir
|
||||
Path tmpDir;
|
||||
|
||||
record Result(BaseSchemaValidator.Result output, String cliOutput) {}
|
||||
|
||||
private Result validate(String schema, String spec) throws IOException {
|
||||
var result = LuaValidator.validate(
|
||||
LuaEnvironment.loadScript(args, schema, "schema.lua").profile,
|
||||
SchemaSpecification.load(spec),
|
||||
PlanetilerConfig.defaults()
|
||||
);
|
||||
for (var example : result.results()) {
|
||||
if (example.issues().isFailure()) {
|
||||
assertNotNull(example.issues().get());
|
||||
}
|
||||
}
|
||||
// also exercise the cli writer and return what it would have printed to stdout
|
||||
var cliOutput = validateCli(Files.writeString(tmpDir.resolve("schema"),
|
||||
schema + "\nplanetiler.examples= '" + Files.writeString(tmpDir.resolve("spec.yml"), spec) + "'"));
|
||||
|
||||
return new Result(result, cliOutput);
|
||||
}
|
||||
|
||||
private String validateCli(Path path) {
|
||||
try (
|
||||
var baos = new ByteArrayOutputStream();
|
||||
var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8)
|
||||
) {
|
||||
new LuaValidator(args, path.toString(), printStream).validateFromCli();
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"true,water,polygon,natural: water,",
|
||||
"true,water,polygon,,",
|
||||
"true,water,polygon,'natural: water\nother: null',",
|
||||
"false,water,polygon,natural: null,",
|
||||
"false,water2,polygon,natural: water,",
|
||||
"false,water,line,natural: water,",
|
||||
"false,water,line,natural: water,",
|
||||
"false,water,polygon,natural: water2,",
|
||||
"false,water,polygon,'natural: water\nother: value',",
|
||||
|
||||
"true,water,polygon,natural: water,allow_extra_tags: true",
|
||||
"true,water,polygon,natural: water,allow_extra_tags: false",
|
||||
"true,water,polygon,,allow_extra_tags: true",
|
||||
"false,water,polygon,,allow_extra_tags: false",
|
||||
|
||||
"true,water,polygon,,min_size: 10",
|
||||
"false,water,polygon,,min_size: 9",
|
||||
})
|
||||
void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags)
|
||||
throws IOException {
|
||||
var results = validate(
|
||||
"""
|
||||
function planetiler.process_feature(source, features)
|
||||
if source:can_be_polygon() and source:has_tag("natural", "water") then
|
||||
features:polygon("water")
|
||||
:inherit_attr_from_source("natural")
|
||||
:set_min_pixel_size(10)
|
||||
end
|
||||
end
|
||||
function main() end
|
||||
""",
|
||||
"""
|
||||
examples:
|
||||
- name: test output
|
||||
input:
|
||||
source: osm
|
||||
geometry: polygon
|
||||
tags:
|
||||
natural: water
|
||||
output:
|
||||
layer: %s
|
||||
geometry: %s
|
||||
%s
|
||||
tags:
|
||||
%s
|
||||
""".formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
|
||||
tags == null ? "" : tags.indent(6).strip())
|
||||
);
|
||||
assertEquals(1, results.output.results().size());
|
||||
assertEquals("test output", results.output.results().get(0).example().name());
|
||||
if (shouldBeOk) {
|
||||
assertTrue(results.output.ok(), results.toString());
|
||||
assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput);
|
||||
} else {
|
||||
assertFalse(results.output.ok(), "Expected an issue, but there were none");
|
||||
assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package com.onthegomap.planetiler.experimental.lua;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.assertContains;
|
||||
import static com.onthegomap.planetiler.util.Gzip.gunzip;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.onthegomap.planetiler.TestUtils;
|
||||
import com.onthegomap.planetiler.VectorTile;
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
abstract class PlanetilerLuaTest {
|
||||
|
||||
private final String script;
|
||||
|
||||
PlanetilerLuaTest(String script) {
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
static class WithMainTest extends PlanetilerLuaTest {
|
||||
WithMainTest() {
|
||||
super("roads_main.lua");
|
||||
}
|
||||
}
|
||||
static class WithoutMainTest extends PlanetilerLuaTest {
|
||||
WithoutMainTest() {
|
||||
super("roads.lua");
|
||||
}
|
||||
}
|
||||
|
||||
public static final Envelope MONACO_BOUNDS = new Envelope(7.40921, 7.44864, 43.72335, 43.75169);
|
||||
|
||||
@TempDir
|
||||
static Path tmpDir;
|
||||
private Mbtiles mbtiles;
|
||||
|
||||
@BeforeAll
|
||||
void runPlanetiler() throws Exception {
|
||||
Path dbPath = tmpDir.resolve("output.mbtiles");
|
||||
LuaMain.main(
|
||||
// Use local data extracts instead of downloading
|
||||
"--script=" + pathToResource(script),
|
||||
"--osm_path=" + TestUtils.pathToResource("monaco-latest.osm.pbf"),
|
||||
|
||||
// Override temp dir location
|
||||
"--tmp=" + tmpDir,
|
||||
|
||||
// Override output location
|
||||
"--output=" + dbPath
|
||||
);
|
||||
mbtiles = Mbtiles.newReadOnlyDatabase(dbPath);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public void close() throws IOException {
|
||||
mbtiles.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMetadata() {
|
||||
Map<String, String> metadata = mbtiles.metadataTable().getAll();
|
||||
assertEquals("Road Schema", metadata.get("name"));
|
||||
assertEquals("0", metadata.get("minzoom"));
|
||||
assertEquals("14", metadata.get("maxzoom"));
|
||||
assertEquals("baselayer", metadata.get("type"));
|
||||
assertEquals("pbf", metadata.get("format"));
|
||||
assertEquals("7.40921,43.72335,7.44864,43.75169", metadata.get("bounds"));
|
||||
assertEquals("7.42892,43.73752,14", metadata.get("center"));
|
||||
assertContains("Simple", metadata.get("description"));
|
||||
assertContains("www.openstreetmap.org/copyright", metadata.get("attribution"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureValidGeometries() throws Exception {
|
||||
var parsedTiles = TestUtils.getTiles(mbtiles);
|
||||
for (var tileEntry : parsedTiles) {
|
||||
var decoded = VectorTile.decode(gunzip(tileEntry.bytes()));
|
||||
for (VectorTile.Feature feature : decoded) {
|
||||
TestUtils.validateGeometry(feature.geometry().decode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoads() {
|
||||
assertMinFeatures("road", Map.of(
|
||||
"highway", "primary"
|
||||
), 14, 317, LineString.class);
|
||||
assertMinFeatures("road", Map.of(
|
||||
"highway", "service"
|
||||
), 14, 310, LineString.class);
|
||||
}
|
||||
|
||||
private void assertMinFeatures(String layer, Map<String, Object> attrs, int zoom,
|
||||
int expected, Class<? extends Geometry> clazz) {
|
||||
TestUtils.assertMinFeatureCount(mbtiles, layer, zoom, attrs, MONACO_BOUNDS, expected, clazz);
|
||||
}
|
||||
|
||||
public static Path pathToResource(String resource) {
|
||||
return resolve(Path.of("planetiler-experimental", "src", "test", "resources", resource));
|
||||
}
|
||||
|
||||
|
||||
private static Path resolve(Path pathFromRoot) {
|
||||
Path cwd = Path.of("").toAbsolutePath();
|
||||
return cwd.resolveSibling(pathFromRoot);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
-- example profile that delegates handling for individual layers to separate files
|
||||
planetiler.examples = "multifile.spec.yaml"
|
||||
|
||||
planetiler.add_source('osm', {
|
||||
type = 'osm',
|
||||
url = 'geofabrik:monaco',
|
||||
})
|
||||
|
||||
local layers = {
|
||||
require("planetiler-experimental.src.test.resources.multifile_building"),
|
||||
require("planetiler-experimental.src.test.resources.multifile_housenumber"),
|
||||
}
|
||||
|
||||
-- TODO make a java utility that does this in a more complete, less verbose way
|
||||
-- (handle other profile methods, separate handler methods per source, expose layer name, etc.)
|
||||
local processors = {}
|
||||
for i, layer in ipairs(layers) do
|
||||
-- classes defined in LuaEnvironment.CLASSES_TO_EXPOSE are exposed as global variables to the profile
|
||||
table.insert(processors, MultiExpression:entry(layer.process_feature, layer.filter))
|
||||
end
|
||||
local feature_processors = MultiExpression:of(processors):index()
|
||||
|
||||
function planetiler.process_feature(source, features)
|
||||
for i, match in ipairs(feature_processors:get_matches_with_triggers(source)) do
|
||||
match.match(source, features, match.keys)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
# test cases for multifile.lua
|
||||
examples:
|
||||
- name: building
|
||||
input:
|
||||
geometry: polygon
|
||||
source: osm
|
||||
tags:
|
||||
building: yes
|
||||
output:
|
||||
min_zoom: 14
|
||||
layer: building
|
||||
tags:
|
||||
class: building
|
||||
- name: not building
|
||||
input:
|
||||
geometry: polygon
|
||||
source: osm
|
||||
tags:
|
||||
building: no
|
||||
aeroway: hangar
|
||||
output: [ ]
|
||||
- name: airport hangar
|
||||
input:
|
||||
geometry: polygon
|
||||
source: osm
|
||||
tags:
|
||||
aeroway: hangar
|
||||
output:
|
||||
min_zoom: 14
|
||||
layer: building
|
||||
tags:
|
||||
class: aeroway
|
||||
- name: housenumber
|
||||
input:
|
||||
geometry: point
|
||||
source: osm
|
||||
tags:
|
||||
addr:housenumber: 123
|
||||
output:
|
||||
min_zoom: 14
|
||||
layer: housenumber
|
||||
tags:
|
||||
housenumber: 123
|
|
@ -0,0 +1,22 @@
|
|||
-- handles building features from the multifile.lua example
|
||||
local mod = {}
|
||||
|
||||
-- multifile.lua builds an optimized MultiExpression matcher from each layer's filter
|
||||
-- TODO nicer way to build these?
|
||||
mod.filter = Expression:AND(
|
||||
Expression:OR(
|
||||
Expression:match_field('building'),
|
||||
Expression:match_any('aeroway', 'building', 'hangar')
|
||||
),
|
||||
Expression:NOT(Expression:OR(
|
||||
Expression:match_any('building', 'no', 'none')
|
||||
))
|
||||
)
|
||||
-- when filter matches, this function gets run
|
||||
function mod.process_feature(source, features, keys)
|
||||
features:polygon("building")
|
||||
:set_attr('class', keys[1])
|
||||
:set_min_zoom(14)
|
||||
end
|
||||
|
||||
return mod
|
|
@ -0,0 +1,12 @@
|
|||
-- handles addr:housenumber features from the multifile.lua example
|
||||
local mod = {}
|
||||
|
||||
mod.filter = Expression:match_field('addr:housenumber')
|
||||
|
||||
function mod.process_feature(source, features)
|
||||
features:point("housenumber")
|
||||
:set_attr("housenumber", source:get_tag("addr:housenumber"))
|
||||
:set_min_zoom(14)
|
||||
end
|
||||
|
||||
return mod
|
|
@ -0,0 +1,40 @@
|
|||
-- Example lua profile that emits power lines and poles from an openstreetmap source
|
||||
-- useful for hot air ballooning
|
||||
|
||||
-- The planetiler object defined in LuaEnvironment.PlanetilerNamespace is the interface for sharing
|
||||
-- data between lua scripts and Java
|
||||
planetiler.output.name = "Power"
|
||||
planetiler.output.description = "Simple"
|
||||
planetiler.output.attribution =
|
||||
'<a href="https://www.openstreetmap.org/copyright" target="_blank">©OpenStreetMap contributors</a>'
|
||||
planetiler.examples = "power.spec.yaml"
|
||||
planetiler.output.path = { "data", "buildings.pmtiles" }
|
||||
|
||||
local area = planetiler.args:get_string("area", "geofabrik area to download", "massachusetts")
|
||||
|
||||
planetiler.add_source('osm', {
|
||||
type = 'osm',
|
||||
url = 'geofabrik:' .. area,
|
||||
-- any java method or field that takes a Path can be called with a list of path parts from lua
|
||||
path = { 'data', 'sources', area .. '.osm.pbf' }
|
||||
})
|
||||
|
||||
function planetiler.process_feature(source, features)
|
||||
if source:can_be_line() and source:has_tag("power", "line") then
|
||||
features
|
||||
:line("power")
|
||||
:set_min_zoom(7)
|
||||
:inherit_attr_from_source("power")
|
||||
:inherit_attr_from_source("voltage")
|
||||
:inherit_attr_from_source("cables")
|
||||
:inherit_attr_from_source("operator")
|
||||
elseif source:isPoint() and source:has_tag("power", "pole") then
|
||||
features
|
||||
:point("power")
|
||||
:set_min_zoom(13)
|
||||
:inherit_attr_from_source("power")
|
||||
:inherit_attr_from_source("ref")
|
||||
:inherit_attr_from_source("height")
|
||||
:inherit_attr_from_source("operator")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
# test cases for power.lua
|
||||
examples:
|
||||
- name: power line
|
||||
input:
|
||||
geometry: line
|
||||
tags:
|
||||
power: line
|
||||
voltage: voltage
|
||||
cables: cables
|
||||
operator: operator
|
||||
output:
|
||||
layer: power
|
||||
geometry: line
|
||||
min_zoom: 7
|
||||
allow_extra_tags: false
|
||||
tags:
|
||||
power: line
|
||||
cables: cables
|
||||
operator: operator
|
||||
voltage: voltage
|
||||
|
||||
- name: pole
|
||||
input:
|
||||
geometry: point
|
||||
tags:
|
||||
power: pole
|
||||
ref: ref
|
||||
height: height
|
||||
operator: operator
|
||||
output:
|
||||
layer: power
|
||||
geometry: point
|
||||
min_zoom: 13
|
||||
allow_extra_tags: false
|
||||
tags:
|
||||
power: pole
|
||||
ref: ref
|
||||
height: height
|
||||
operator: operator
|
|
@ -0,0 +1,50 @@
|
|||
-- Simple lua profile example that emits road features
|
||||
|
||||
-- setup archive metadata
|
||||
planetiler.output.name = "Road Schema"
|
||||
planetiler.output.description = "Simple"
|
||||
planetiler.output.attribution =
|
||||
'<a href="https://www.openstreetmap.org/copyright" target="_blank">©OpenStreetMap contributors</a>'
|
||||
|
||||
-- tell planetiler where the tests are when you run `planetiler.jar validate roads_main.lua`
|
||||
planetiler.examples = "roads.spec.yaml"
|
||||
|
||||
-- planetiler.process_feature is called by many threads so it can read from shared data structures
|
||||
-- but not modify them
|
||||
local highway_minzooms = {
|
||||
trunk = 5,
|
||||
primary = 7,
|
||||
secondary = 8,
|
||||
tertiary = 9,
|
||||
motorway_link = 9,
|
||||
trunk_link = 9,
|
||||
primary_link = 9,
|
||||
secondary_link = 9,
|
||||
tertiary_link = 9,
|
||||
unclassified = 11,
|
||||
residential = 11,
|
||||
living_street = 11,
|
||||
track = 12,
|
||||
service = 13
|
||||
}
|
||||
|
||||
-- called by planetiler to map each input feature to output vector tile features
|
||||
function planetiler.process_feature(source, features)
|
||||
local highway = source:get_tag("highway")
|
||||
if source:can_be_line() and highway and highway_minzooms[highway] then
|
||||
features:line("road")
|
||||
:set_min_zoom(highway_minzooms[highway])
|
||||
:set_attr("highway", highway)
|
||||
end
|
||||
end
|
||||
|
||||
-- there are 2 ways to invoke planetiler: a main method (see roads_main.lua) and this method that
|
||||
-- sets up planetiler statically
|
||||
-- TODO not sure which is better?
|
||||
local area = planetiler.args:get_string("area", "geofabrik area to download", "germany")
|
||||
planetiler.add_source('osm', {
|
||||
type = 'osm',
|
||||
path = { 'data', 'sources', area .. '.osm.pbf' },
|
||||
url = 'geofabrik:' .. area
|
||||
})
|
||||
planetiler.output.path = 'roads.pmtiles'
|
|
@ -0,0 +1,41 @@
|
|||
# test cases for roads.lua and roads_main.lua
|
||||
examples:
|
||||
- name: trunk
|
||||
input:
|
||||
geometry: line
|
||||
tags:
|
||||
highway: trunk
|
||||
output:
|
||||
layer: road
|
||||
geometry: line
|
||||
min_zoom: 5
|
||||
allow_extra_tags: false
|
||||
tags:
|
||||
highway: trunk
|
||||
|
||||
- name: track
|
||||
input:
|
||||
geometry: line
|
||||
tags:
|
||||
highway: track
|
||||
output:
|
||||
layer: road
|
||||
geometry: line
|
||||
min_zoom: 12
|
||||
allow_extra_tags: false
|
||||
tags:
|
||||
highway: track
|
||||
|
||||
- name: service
|
||||
input:
|
||||
geometry: line
|
||||
tags:
|
||||
highway: service
|
||||
output:
|
||||
layer: road
|
||||
geometry: line
|
||||
min_zoom: 13
|
||||
allow_extra_tags: false
|
||||
tags:
|
||||
highway: service
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
-- Simple lua profile example that emits road features
|
||||
|
||||
-- setup archive metadata
|
||||
planetiler.output.name = "Road Schema"
|
||||
planetiler.output.description = "Simple"
|
||||
planetiler.output.attribution =
|
||||
'<a href="https://www.openstreetmap.org/copyright" target="_blank">©OpenStreetMap contributors</a>'
|
||||
|
||||
-- tell planetiler where the tests are when you run `planetiler.jar validate roads_main.lua`
|
||||
planetiler.examples = "roads.spec.yaml"
|
||||
|
||||
-- planetiler.process_feature is called by many threads so it can read from shared data structures
|
||||
-- but not modify them
|
||||
local highway_minzooms = {
|
||||
trunk = 5,
|
||||
primary = 7,
|
||||
secondary = 8,
|
||||
tertiary = 9,
|
||||
motorway_link = 9,
|
||||
trunk_link = 9,
|
||||
primary_link = 9,
|
||||
secondary_link = 9,
|
||||
tertiary_link = 9,
|
||||
unclassified = 11,
|
||||
residential = 11,
|
||||
living_street = 11,
|
||||
track = 12,
|
||||
service = 13
|
||||
}
|
||||
|
||||
-- called by planetiler to map each input feature to output vector tile features
|
||||
function planetiler.process_feature(source, features)
|
||||
local highway = source:get_tag("highway")
|
||||
if source:can_be_line() and highway and highway_minzooms[highway] then
|
||||
features:line("road")
|
||||
:set_min_zoom(highway_minzooms[highway])
|
||||
:set_attr("highway", highway)
|
||||
end
|
||||
end
|
||||
|
||||
-- there are 2 ways to invoke planetiler: a main method that takes a Planetiler instance configured
|
||||
-- with args and the profile defined above, or setup planetiler with calls to planetiler.add_source
|
||||
-- and planetiler.output.path.
|
||||
-- TODO not sure which is better?
|
||||
function main(runner)
|
||||
local area = planetiler.args:get_string("area", "geofabrik area to download", "massachusetts")
|
||||
runner
|
||||
:add_osm_source("osm", { "data", "sources", area .. ".osm.pbf" }, "geofabrik:" .. area)
|
||||
:overwrite_output({ "data", "roads.pmtiles" })
|
||||
:run()
|
||||
end
|
5
pom.xml
5
pom.xml
|
@ -26,7 +26,9 @@
|
|||
<sonar.organization>onthegomap</sonar.organization>
|
||||
<sonar.projectKey>onthegomap_planetiler</sonar.projectKey>
|
||||
<sonar.moduleKey>${project.artifactId}</sonar.moduleKey>
|
||||
<sonar.exclusions>planetiler-benchmarks/**/*, planetiler-openmaptiles/**/*</sonar.exclusions>
|
||||
<sonar.exclusions>planetiler-benchmarks/**/*, planetiler-openmaptiles/**/*,
|
||||
planetiler-experimental/src/main/java/org/luaj/**/*
|
||||
</sonar.exclusions>
|
||||
<revision>0.7-SNAPSHOT</revision>
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
</properties>
|
||||
|
@ -87,6 +89,7 @@
|
|||
<module>planetiler-core</module>
|
||||
<module>planetiler-openmaptiles/submodule.pom.xml</module>
|
||||
<module>planetiler-custommap</module>
|
||||
<module>planetiler-experimental</module>
|
||||
<module>planetiler-benchmarks</module>
|
||||
<module>planetiler-examples</module>
|
||||
<module>planetiler-dist</module>
|
||||
|
|
Ładowanie…
Reference in New Issue