lua-profiles
Mike Barry 2023-11-30 05:46:24 -05:00
rodzic ed4c320e49
commit a9467840f6
52 zmienionych plików z 4476 dodań i 337 usunięć

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"
]
}
}

Wyświetl plik

@ -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 |

Wyświetl plik

@ -154,6 +154,10 @@
<artifactId>geopackage</artifactId>
<version>${geopackage.version}</version>
</dependency>
<dependency>
<groupId>org.snakeyaml</groupId>
<artifactId>snakeyaml-engine</artifactId>
</dependency>
</dependencies>
<build>

Wyświetl plik

@ -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.

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -1,4 +1,4 @@
package com.onthegomap.planetiler.custommap;
package com.onthegomap.planetiler.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);

Wyświetl plik

@ -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) {}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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(

Wyświetl plik

@ -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>

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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>

Wyświetl plik

@ -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)));
}
}
}

Wyświetl plik

@ -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);
}
}
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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 {
}

Wyświetl plik

@ -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 {
}

Wyświetl plik

@ -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 {
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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)))
);
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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">&copy;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

Wyświetl plik

@ -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

Wyświetl plik

@ -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">&copy;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'

Wyświetl plik

@ -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

Wyświetl plik

@ -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">&copy;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

Wyświetl plik

@ -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>