planetiler/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfiguredFeatureTest.java

788 wiersze
23 KiB
Java

package com.onthegomap.planetiler.custommap;
import static com.onthegomap.planetiler.TestUtils.newLineString;
import static com.onthegomap.planetiler.TestUtils.newPoint;
import static com.onthegomap.planetiler.TestUtils.newPolygon;
import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Assertions.*;
import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureCollector.Feature;
import com.onthegomap.planetiler.Profile;
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
import com.onthegomap.planetiler.custommap.util.TestConfigurableUtils;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.stats.Stats;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
class ConfiguredFeatureTest {
private static final Function<String, Path> TEST_RESOURCE = TestConfigurableUtils::pathToTestResource;
private static final Function<String, Path> SAMPLE_RESOURCE = TestConfigurableUtils::pathToSample;
private static final Function<String, Path> TEST_INVALID_RESOURCE = TestConfigurableUtils::pathToTestInvalidResource;
private static final Map<String, Object> waterTags = Map.of(
"natural", "water",
"water", "pond",
"name", "Little Pond",
"test_zoom_tag", "test_zoom_value"
);
private static final Map<String, Object> motorwayTags = Map.of(
"highway", "motorway",
"layer", "1",
"bridge", "yes",
"tunnel", "yes"
);
private static final Map<String, Object> trunkTags = Map.of(
"highway", "trunk",
"toll", "yes"
);
private static final Map<String, Object> primaryTags = Map.of(
"highway", "primary",
"lanes", "2"
);
private static final Map<String, Object> highwayAreaTags = Map.of(
"area:highway", "motorway",
"layer", "1",
"bridge", "yes",
"surface", "asphalt"
);
private static final Map<String, Object> inputMappingTags = Map.of(
"s_type", "string_val",
"l_type", "1",
"i_type", "1",
"double_type", "1.5",
"b_type", "yes",
"d_type", "yes",
"intermittent", "yes",
"bridge", "yes"
);
private static Profile loadConfig(Function<String, Path> pathFunction, String filename) {
var staticAttributeConfig = pathFunction.apply(filename);
var schema = SchemaConfig.load(staticAttributeConfig);
return new ConfiguredProfile(schema);
}
private static Profile loadConfig(String config) {
var schema = SchemaConfig.load(config);
return new ConfiguredProfile(schema);
}
private static void testFeature(Function<String, Path> pathFunction, String schemaFilename, SourceFeature sf,
Consumer<Feature> test, int expectedMatchCount) {
var profile = loadConfig(pathFunction, schemaFilename);
testFeature(sf, test, expectedMatchCount, profile);
}
private static void testFeature(String config, SourceFeature sf, Consumer<Feature> test, int expectedMatchCount) {
var profile = loadConfig(config);
testFeature(sf, test, expectedMatchCount, profile);
}
private static void testFeature(SourceFeature sf, Consumer<Feature> test, int expectedMatchCount, Profile profile) {
var config = PlanetilerConfig.defaults();
var factory = new FeatureCollector.Factory(config, Stats.inMemory());
var fc = factory.get(sf);
profile.processFeature(sf, fc);
var length = new AtomicInteger(0);
fc.forEach(f -> {
test.accept(f);
length.incrementAndGet();
});
assertEquals(expectedMatchCount, length.get(), "Wrong number of features generated");
}
private static void testPolygon(String config, Map<String, Object> tags,
Consumer<Feature> test, int expectedMatchCount) {
var sf =
SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList());
testFeature(config, sf, test, expectedMatchCount);
}
private static void testPoint(String config, Map<String, Object> tags,
Consumer<Feature> test, int expectedMatchCount) {
var sf =
SimpleFeature.createFakeOsmFeature(newPoint(0, 0), tags, "osm", null, 1, emptyList());
testFeature(config, sf, test, expectedMatchCount);
}
private static void testLinestring(String config,
Map<String, Object> tags, Consumer<Feature> test, int expectedMatchCount) {
var sf =
SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList());
testFeature(config, sf, test, expectedMatchCount);
}
private static void testPolygon(Function<String, Path> pathFunction, String schemaFilename, Map<String, Object> tags,
Consumer<Feature> test, int expectedMatchCount) {
var sf =
SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList());
testFeature(pathFunction, schemaFilename, sf, test, expectedMatchCount);
}
private static void testLinestring(Function<String, Path> pathFunction, String schemaFilename,
Map<String, Object> tags, Consumer<Feature> test, int expectedMatchCount) {
var sf =
SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList());
testFeature(pathFunction, schemaFilename, sf, test, expectedMatchCount);
}
@Test
void testStaticAttributeTest() {
testPolygon(TEST_RESOURCE, "static_attribute.yml", waterTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals("aTestConstantValue", attr.get("natural"));
}, 1);
}
@Test
void testTagValueAttributeTest() {
testPolygon(TEST_RESOURCE, "tag_attribute.yml", waterTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals("water", attr.get("natural"));
}, 1);
}
@Test
void testTagIncludeAttributeTest() {
testPolygon(TEST_RESOURCE, "tag_include.yml", waterTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals("ok", attr.get("test_include"));
assertFalse(attr.containsKey("test_exclude"));
}, 1);
}
@Test
void testZoomAttributeTest() {
testPolygon(TEST_RESOURCE, "tag_include.yml", waterTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals("test_zoom_value", attr.get("test_zoom_tag"));
attr = f.getAttrsAtZoom(11);
assertNotEquals("test_zoom_value", attr.get("test_zoom_tag"));
attr = f.getAttrsAtZoom(9);
assertNotEquals("test_zoom_value", attr.get("test_zoom_tag"));
}, 1);
}
@Test
void testTagHighwayLinestringTest() {
testLinestring(TEST_RESOURCE, "road_motorway.yml", motorwayTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals("motorway", attr.get("highway"));
}, 1);
}
@Test
void testTagTypeConversionTest() {
testLinestring(TEST_RESOURCE, "road_motorway.yml", motorwayTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertTrue(attr.containsKey("layer"), "Produce attribute layer");
assertTrue(attr.containsKey("bridge"), "Produce attribute bridge");
assertTrue(attr.containsKey("tunnel"), "Produce attribute tunnel");
assertEquals(1L, attr.get("layer"), "Extract layer as LONG");
assertEquals(true, attr.get("bridge"), "Extract bridge as tagValue BOOLEAN");
assertEquals(true, attr.get("tunnel"), "Extract tunnel as constantValue BOOLEAN");
}, 1);
}
@Test
void testZoomFilterAttributeTest() {
testLinestring(TEST_RESOURCE, "road_motorway.yml", motorwayTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertTrue(attr.containsKey("bridge"), "Produce attribute bridge at z14");
attr = f.getAttrsAtZoom(10);
assertFalse(attr.containsKey("bridge"), "Don't produce attribute bridge at z10");
}, 1);
}
@Test
void testZoomFilterConditionalTest() {
testLinestring(TEST_RESOURCE, "zoom_filter.yml", motorwayTags, f -> {
var attr = f.getAttrsAtZoom(4);
assertEquals("motorway", attr.get("highway"), "Produce attribute highway at z4");
}, 1);
testLinestring(TEST_RESOURCE, "zoom_filter.yml", trunkTags, f -> {
assertEquals(5, f.getMinZoom());
var attr = f.getAttrsAtZoom(5);
assertEquals("trunk", attr.get("highway"), "Produce highway=trunk at z5");
assertNull(attr.get("toll"), "Skip toll at z5");
attr = f.getAttrsAtZoom(6);
assertEquals("trunk", attr.get("highway"), "Produce highway=trunk at z6");
attr = f.getAttrsAtZoom(8);
assertEquals("yes", attr.get("toll"), "render toll at z8");
}, 1);
testLinestring(TEST_RESOURCE, "zoom_filter.yml", primaryTags, f -> {
var attr = f.getAttrsAtZoom(6);
assertNull(attr.get("highway"), "Skip highway=primary at z6");
assertNull(attr.get("lanes"));
attr = f.getAttrsAtZoom(7);
assertEquals("primary", attr.get("highway"), "Produce highway=primary at z7");
assertNull(attr.get("lanes"));
attr = f.getAttrsAtZoom(12);
assertEquals("primary", attr.get("highway"), "Produce highway=primary at z12");
assertEquals(2L, attr.get("lanes"));
}, 1);
}
@Test
void testAllValuesInKey() {
//Show that a key in includeWhen with no values matches all values
testPolygon(SAMPLE_RESOURCE, "highway_areas.yml", highwayAreaTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals(true, attr.get("bridge"), "Produce bridge attribute");
assertEquals("motorway", attr.get("highway"), "Produce highway area attribute");
assertEquals("asphalt", attr.get("surface"), "Produce surface attribute");
assertEquals(1L, attr.get("layer"), "Produce layer attribute");
}, 1);
}
@Test
void testInputMapping() {
//Show that a key in includeWhen with no values matches all values
testLinestring(TEST_RESOURCE, "data_type_attributes.yml", inputMappingTags, f -> {
var attr = f.getAttrsAtZoom(14);
assertEquals(true, attr.get("b_type"), "Produce boolean");
assertEquals("string_val", attr.get("s_type"), "Produce string");
assertEquals(1, attr.get("d_type"), "Produce direction");
assertEquals(1L, attr.get("l_type"), "Produce long");
assertEquals(1, attr.get("i_type"), "Produce integer");
assertEquals(1.5, attr.get("double_type"), "Produce double");
assertEquals("yes", attr.get("intermittent"), "Produce raw attribute");
assertEquals(true, attr.get("is_intermittent"), "Produce and rename boolean");
assertEquals(true, attr.get("bridge"), "Produce boolean from full structure");
}, 1);
}
@ParameterizedTest
@ValueSource(strings = {"natural:", "natural: [__any__]", "natural: __any__"})
void testMatchAny(String filter) {
testPolygon("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
%s
""".formatted(filter), Map.of(
"natural", "water"
), feature -> {
}, 1);
}
@Test
void testExcludeValue() {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
natural: water
exclude_when:
name: excluded
""";
testPolygon(config, Map.of(
"natural", "water",
"name", "name"
), feature -> {
}, 1);
testPolygon(config, Map.of(
"natural", "water",
"name", "excluded"
), feature -> {
}, 0);
}
@ParameterizedTest
@ValueSource(strings = {"''", "['']", "[null]"})
void testRequireValue(String matchString) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
natural: water
exclude_when:
name: %s
""".formatted(matchString);
testPolygon(config, Map.of(
"natural", "water",
"name", "name"
), feature -> {
}, 1);
testPolygon(config, Map.of(
"natural", "water"
), feature -> {
}, 0);
testPolygon(config, Map.of(
"natural", "water",
"name", ""
), feature -> {
}, 0);
}
@Test
void testMappingKeyValue() {
testPolygon("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
natural: water
attributes:
- key: key
type: match_key
- key: value
type: match_value
""", Map.of(
"natural", "water"
), feature -> {
assertEquals(Map.of(
"key", "natural",
"value", "water"
), feature.getAttrsAtZoom(14));
}, 1);
}
@Test
void testCoerceAttributeValue() {
testPolygon("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
attributes:
- key: int
type: integer
- key: long
type: long
- key: double
type: double
""", Map.of(
"int", "1",
"long", "-1",
"double", "1.5"
), feature -> {
assertEquals(Map.of(
"int", 1,
"long", -1L,
"double", 1.5
), feature.getAttrsAtZoom(14));
}, 1);
}
@ParameterizedTest
@CsvSource(value = {
"1| 1",
"1+1| 1+1",
"${1+1}| 2",
"${match_key + '=' + match_value}| natural=water",
"${match_value.replace('ter', 'wa')}| wawa",
"${feature.tags.natural}| water",
"${feature.id}|1",
"\\${feature.id}|${feature.id}",
"\\\\${feature.id}|\\${feature.id}",
"${feature.source}|osm",
"${feature.source_layer}|null",
"${coalesce(feature.source_layer, 'missing')}|missing",
"{match: {test: {natural: water}}}|test",
"{match: {test: {natural: not_water}}}|null",
}, delimiter = '|')
void testExpressionValue(String expression, Object value) {
testPoint("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: point
include_when:
natural: water
attributes:
- key: key
value: %s
""".formatted(expression), Map.of(
"natural", "water"
), feature -> {
var result = feature.getAttrsAtZoom(14).get("key");
String resultString = result == null ? "null" : result.toString();
assertEquals(value, resultString);
}, 1);
}
@Test
void testGetTag() {
testPoint("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: point
include_when:
natural: water
attributes:
- key: key
value:
tag_value: natural
- key: key2
value:
tag_value: intval
type: integer
""", Map.of(
"natural", "water",
"intval", "1"
), feature -> {
assertEquals("water", feature.getAttrsAtZoom(14).get("key"));
assertEquals(1, feature.getAttrsAtZoom(14).get("key2"));
}, 1);
}
@ParameterizedTest
@ValueSource(strings = {
"",
"tag_value: depth",
"value: '${feature.tags[\"depth\"]}'",
"value: '${feature.tags.get(\"depth\")}'"
})
void testGetInExpressionUsesTagMapping(String getter) {
testPoint("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
tag_mappings:
depth: long
layers:
- id: testLayer
features:
- source: osm
geometry: point
attributes:
- key: depth
%s
""".formatted(getter), Map.of(
"depth", "35"
), feature -> {
assertEquals(35L, feature.getAttrsAtZoom(14).get("depth"));
}, 1);
}
@ParameterizedTest
@CsvSource(value = {
"12|12",
"${5+5}|10",
"${match_key.size()}|7",
"${value.size()}|5",
"{default_value: 4, overrides: {3: {natural: water}}}|3",
"{default_value: 4, overrides: {3: {natural: not_water}}}|4",
}, delimiter = '|')
void testAttributeMinZoomExpression(String expression, int minZoom) {
testPoint("""
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: point
include_when:
natural: water
attributes:
- key: key
value: value
min_zoom: %s
""".formatted(expression), Map.of(
"natural", "water"
), feature -> {
assertNull(feature.getAttrsAtZoom(minZoom - 1).get("key"));
assertEquals("value", feature.getAttrsAtZoom(minZoom).get("key"));
}, 1);
}
@Test
void testMinZoomExpression() {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: point
min_zoom:
default_value: 4
overrides:
- if: '${feature.tags.has("a", "b")}'
value: 5
include_when:
natural: water
""";
testPoint(config, Map.of(
"natural", "water"
), feature -> {
assertEquals(4, feature.getMinZoom());
}, 1);
testPoint(config, Map.of(
"natural", "water",
"a", "b"
), feature -> {
assertEquals(5, feature.getMinZoom());
}, 1);
}
@Test
void testFallbackValue() {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
natural: water
attributes:
- key: key
value: 1
include_when:
otherkey: value
else: 0
""";
testPolygon(config, Map.of(
"natural", "water"
), feature -> {
assertEquals(Map.of("key", 0), feature.getAttrsAtZoom(14));
}, 1);
testPolygon(config, Map.of(
"natural", "water",
"otherkey", "othervalue"
), feature -> {
assertEquals(Map.of("key", 0), feature.getAttrsAtZoom(14));
}, 1);
testPolygon(config, Map.of(
"natural", "water",
"otherkey", "value"
), feature -> {
assertEquals(Map.of("key", 1), feature.getAttrsAtZoom(14));
}, 1);
}
@ParameterizedTest
@CsvSource(value = {
"\"${feature.tags.has('natural', 'water')}\"",
"{__all__: [\"${feature.tags.has('natural', 'water')}\"]}",
}, delimiter = '|')
void testExpressionInMatch(String filter) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when: %s
""".formatted(filter);
testPolygon(config, Map.of(
"natural", "water"
), feature -> {
}, 1);
testPolygon(config, Map.of(
"natural", "other"
), feature -> {
}, 0);
testPolygon(config, Map.of(
), feature -> {
}, 0);
}
@Test
void testExpressionAttrFilter() {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when:
natural: water
highway: motorway
attributes:
- key: key
value: true
include_when: ${ match_value.startsWith("wa") }
else: false
""";
testPolygon(config, Map.of(
"natural", "water"
), feature -> {
assertEquals(true, feature.getAttrsAtZoom(14).get("key"));
}, 1);
testPolygon(config, Map.of(
"highway", "motorway"
), feature -> {
assertEquals(false, feature.getAttrsAtZoom(14).get("key"));
}, 1);
}
@Test
void testExpressionAttrFilterNoMatchingKey() {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
geometry: polygon
include_when: ${ feature.tags.has("natural", "water") }
attributes:
- key: key
value: true
include_when: ${ coalesce(match_value, '').startsWith("wa") }
else: false
""";
testPolygon(config, Map.of(
"natural", "water"
), feature -> {
assertEquals(false, feature.getAttrsAtZoom(14).get("key"));
}, 1);
}
@Test
void testGeometryTypeMismatch() {
//Validate that a schema that filters on lines does not match on a polygon feature
var sf =
SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), motorwayTags, "osm", null, 1,
emptyList());
testFeature(TEST_RESOURCE, "road_motorway.yml", sf, f -> {
}, 0);
}
@Test
void testSourceTypeMismatch() {
//Validate that a schema only matches on the specified data source
var sf =
SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1, 0, 0), highwayAreaTags, "not_osm", null, 1,
emptyList());
testFeature(SAMPLE_RESOURCE, "highway_areas.yml", sf, f -> {
}, 0);
}
@Test
void testInvalidSchemas() {
testInvalidSchema("bad_geometry_type.yml", "Profile defined with invalid geometry type");
testInvalidSchema("no_layers.yml", "Profile defined with no layers");
}
private void testInvalidSchema(String filename, String message) {
assertThrows(RuntimeException.class, () -> loadConfig(TEST_INVALID_RESOURCE, filename), message);
}
}