kopia lustrzana https://github.com/onthegomap/planetiler
Declarative schema from configuration file (#160)
rodzic
74b7474c46
commit
da12fef79f
|
@ -21,7 +21,6 @@
|
|||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
|
|
|
@ -200,7 +200,7 @@ public class Arguments {
|
|||
return value;
|
||||
}
|
||||
|
||||
/** Returns a {@link Path} parsed from {@code key} argument which may or may not exist. */
|
||||
/** Returns a {@link Path} parsed from {@code key} argument, or fall back to a default if the argument is not set. */
|
||||
public Path file(String key, String description, Path defaultValue) {
|
||||
String value = getArg(key);
|
||||
Path file = value == null ? defaultValue : Path.of(value);
|
||||
|
@ -208,6 +208,17 @@ public class Arguments {
|
|||
return file;
|
||||
}
|
||||
|
||||
/** Returns a {@link Path} parsed from {@code key} argument which may or may not exist. */
|
||||
public Path file(String key, String description) {
|
||||
String value = getArg(key);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Missing required parameter: " + key + " (" + description + ")");
|
||||
}
|
||||
Path file = Path.of(value);
|
||||
logArgValue(key, description, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Path} parsed from {@code key} argument which must exist for the program to function.
|
||||
*
|
||||
|
@ -221,6 +232,19 @@ public class Arguments {
|
|||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Path} parsed from a required {@code key} argument which must exist for the program to function.
|
||||
*
|
||||
* @throws IllegalArgumentException if the file does not exist or if the parameter is not provided.
|
||||
*/
|
||||
public Path inputFile(String key, String description) {
|
||||
Path path = file(key, description);
|
||||
if (!Files.exists(path)) {
|
||||
throw new IllegalArgumentException(path + " does not exist");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/** Returns a boolean parsed from {@code key} argument where {@code "true"} is true and anything else is false. */
|
||||
public boolean getBoolean(String key, String description, boolean defaultValue) {
|
||||
boolean value = "true".equalsIgnoreCase(getArg(key, Boolean.toString(defaultValue)));
|
||||
|
|
|
@ -41,6 +41,8 @@ public interface Expression {
|
|||
Expression FALSE = new Constant(false, "FALSE");
|
||||
BiFunction<WithTags, String, Object> GET_TAG = WithTags::getTag;
|
||||
|
||||
List<String> dummyList = new NoopList<>();
|
||||
|
||||
static And and(Expression... children) {
|
||||
return and(List.of(children));
|
||||
}
|
||||
|
@ -247,6 +249,26 @@ public interface Expression {
|
|||
*/
|
||||
boolean evaluate(WithTags input, List<String> matchKeys);
|
||||
|
||||
//A list that silently drops all additions
|
||||
class NoopList<T> extends ArrayList<T> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public boolean add(T t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this expression matches an input element.
|
||||
*
|
||||
* @param input the input element
|
||||
* @return true if this expression matches the input element
|
||||
*/
|
||||
default boolean evaluate(WithTags input) {
|
||||
return evaluate(input, dummyList);
|
||||
}
|
||||
|
||||
/** Returns Java code that can be used to reconstruct this expression. */
|
||||
String generateJavaCode();
|
||||
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package com.onthegomap.planetiler.geo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureCollector.Feature;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Lineal;
|
||||
import org.locationtech.jts.geom.Polygonal;
|
||||
|
@ -7,17 +13,27 @@ import org.locationtech.jts.geom.Puntal;
|
|||
import vector_tile.VectorTileProto;
|
||||
|
||||
public enum GeometryType {
|
||||
UNKNOWN(VectorTileProto.Tile.GeomType.UNKNOWN, 0),
|
||||
POINT(VectorTileProto.Tile.GeomType.POINT, 1),
|
||||
LINE(VectorTileProto.Tile.GeomType.LINESTRING, 2),
|
||||
POLYGON(VectorTileProto.Tile.GeomType.POLYGON, 4);
|
||||
UNKNOWN(VectorTileProto.Tile.GeomType.UNKNOWN, 0, (f, l) -> {
|
||||
throw new UnsupportedOperationException();
|
||||
}, "unknown"),
|
||||
@JsonProperty("point")
|
||||
POINT(VectorTileProto.Tile.GeomType.POINT, 1, FeatureCollector::point, "point"),
|
||||
@JsonProperty("line")
|
||||
LINE(VectorTileProto.Tile.GeomType.LINESTRING, 2, FeatureCollector::line, "linestring"),
|
||||
@JsonProperty("polygon")
|
||||
POLYGON(VectorTileProto.Tile.GeomType.POLYGON, 4, FeatureCollector::polygon, "polygon");
|
||||
|
||||
private final VectorTileProto.Tile.GeomType protobufType;
|
||||
private final int minPoints;
|
||||
private final BiFunction<FeatureCollector, String, Feature> geometryFactory;
|
||||
private final String matchTypeString;
|
||||
|
||||
GeometryType(VectorTileProto.Tile.GeomType protobufType, int minPoints) {
|
||||
GeometryType(VectorTileProto.Tile.GeomType protobufType, int minPoints,
|
||||
BiFunction<FeatureCollector, String, Feature> geometryFactory, String matchTypeString) {
|
||||
this.protobufType = protobufType;
|
||||
this.minPoints = minPoints;
|
||||
this.geometryFactory = geometryFactory;
|
||||
this.matchTypeString = matchTypeString;
|
||||
}
|
||||
|
||||
public static GeometryType valueOf(Geometry geom) {
|
||||
|
@ -49,4 +65,25 @@ public enum GeometryType {
|
|||
public int minPoints() {
|
||||
return minPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a factory method which creates a {@link Feature} from a {@link FeatureCollector} of the appropriate
|
||||
* geometry type.
|
||||
*
|
||||
* @param layerName - name of the layer
|
||||
* @return geometry factory method
|
||||
*/
|
||||
public Function<FeatureCollector, Feature> geometryFactory(String layerName) {
|
||||
return features -> geometryFactory.apply(features, layerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a test for whether a source feature is of the correct geometry to be included in the tile.
|
||||
*
|
||||
* @return geometry test method
|
||||
*/
|
||||
public Expression featureTest() {
|
||||
return Expression.matchType(matchTypeString);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -601,6 +601,18 @@ public class TestUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void assertMinFeatureCount(Mbtiles db, String layer, int zoom, Map<String, Object> attrs,
|
||||
Envelope envelope, int expected, Class<? extends Geometry> clazz) {
|
||||
try {
|
||||
int num = Verify.getNumFeatures(db, layer, zoom, attrs, envelope, clazz);
|
||||
|
||||
assertTrue(expected < num,
|
||||
"z%d features in %s, expected at least %d got %d".formatted(zoom, layer, expected, num));
|
||||
} catch (GeometryException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertFeatureNear(Mbtiles db, String layer, Map<String, Object> attrs, double lng, double lat,
|
||||
int minzoom, int maxzoom) {
|
||||
try {
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
package com.onthegomap.planetiler.expression;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.newPoint;
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static com.onthegomap.planetiler.expression.ExpressionTestUtil.featureWithTags;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -22,14 +18,6 @@ class ExpressionTest {
|
|||
public static final Expression.MatchAny matchCD = matchAny("c", "d");
|
||||
public static final Expression.MatchAny matchBC = matchAny("b", "c");
|
||||
|
||||
static SourceFeature featureWithTags(String... tags) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (int i = 0; i < tags.length; i += 2) {
|
||||
map.put(tags[i], tags[i + 1]);
|
||||
}
|
||||
return SimpleFeature.create(newPoint(0, 0), map);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimplify() {
|
||||
assertEquals(matchAB, matchAB.simplify());
|
||||
|
@ -159,4 +147,35 @@ class ExpressionTest {
|
|||
var expression = matchAnyTyped("key", WithTags::getDirection, 1);
|
||||
assertThrows(UnsupportedOperationException.class, expression::generateJavaCode);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEvaluate() {
|
||||
WithTags feature = featureWithTags("key1", "value1", "key2", "value2");
|
||||
|
||||
//And
|
||||
assertTrue(and(matchAny("key1", "value1"), matchAny("key2", "value2")).evaluate(feature));
|
||||
assertFalse(and(matchAny("key1", "value1"), matchAny("key2", "wrong")).evaluate(feature));
|
||||
assertFalse(and(matchAny("key1", "wrong"), matchAny("key2", "value2")).evaluate(feature));
|
||||
assertFalse(and(matchAny("key1", "wrong"), matchAny("key2", "wrong")).evaluate(feature));
|
||||
|
||||
//Or
|
||||
assertTrue(or(matchAny("key1", "value1"), matchAny("key2", "value2")).evaluate(feature));
|
||||
assertTrue(or(matchAny("key1", "value1"), matchAny("key2", "wrong")).evaluate(feature));
|
||||
assertTrue(or(matchAny("key1", "wrong"), matchAny("key2", "value2")).evaluate(feature));
|
||||
assertFalse(or(matchAny("key1", "wrong"), matchAny("key2", "wrong")).evaluate(feature));
|
||||
|
||||
//Not
|
||||
assertFalse(not(matchAny("key1", "value1")).evaluate(feature));
|
||||
assertTrue(not(matchAny("key1", "wrong")).evaluate(feature));
|
||||
|
||||
//MatchField
|
||||
assertTrue(matchField("key1").evaluate(feature));
|
||||
assertFalse(matchField("wrong").evaluate(feature));
|
||||
assertTrue(not(matchAny("key1", "")).evaluate(feature));
|
||||
assertTrue(matchAny("wrong", "").evaluate(feature));
|
||||
|
||||
//Constants
|
||||
assertTrue(TRUE.evaluate(feature));
|
||||
assertFalse(FALSE.evaluate(feature));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.onthegomap.planetiler.expression;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.newPoint;
|
||||
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExpressionTestUtil {
|
||||
static SourceFeature featureWithTags(String... tags) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (int i = 0; i < tags.length; i += 2) {
|
||||
map.put(tags[i], tags[i + 1]);
|
||||
}
|
||||
return SimpleFeature.create(newPoint(0, 0), map);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import static com.onthegomap.planetiler.TestUtils.newLineString;
|
|||
import static com.onthegomap.planetiler.TestUtils.newPoint;
|
||||
import static com.onthegomap.planetiler.TestUtils.rectangle;
|
||||
import static com.onthegomap.planetiler.expression.Expression.*;
|
||||
import static com.onthegomap.planetiler.expression.ExpressionTest.featureWithTags;
|
||||
import static com.onthegomap.planetiler.expression.ExpressionTestUtil.featureWithTags;
|
||||
import static com.onthegomap.planetiler.expression.MultiExpression.entry;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package com.onthegomap.planetiler.geo;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import com.onthegomap.planetiler.TestUtils;
|
||||
import com.onthegomap.planetiler.reader.SimpleFeature;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class GeometryTypeTest {
|
||||
|
||||
@Test
|
||||
void testGeometryFactory() throws Exception {
|
||||
Map<String, Object> tags = Map.of("key1", "value1");
|
||||
|
||||
var line =
|
||||
SimpleFeature.createFakeOsmFeature(TestUtils.newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList());
|
||||
var point =
|
||||
SimpleFeature.createFakeOsmFeature(TestUtils.newPoint(0, 0), tags, "osm", null, 1, emptyList());
|
||||
var poly =
|
||||
SimpleFeature.createFakeOsmFeature(TestUtils.newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1,
|
||||
emptyList());
|
||||
|
||||
Assertions.assertTrue(GeometryType.LINE.featureTest().evaluate(line));
|
||||
Assertions.assertFalse(GeometryType.LINE.featureTest().evaluate(point));
|
||||
Assertions.assertFalse(GeometryType.LINE.featureTest().evaluate(poly));
|
||||
|
||||
Assertions.assertFalse(GeometryType.POINT.featureTest().evaluate(line));
|
||||
Assertions.assertTrue(GeometryType.POINT.featureTest().evaluate(point));
|
||||
Assertions.assertFalse(GeometryType.POINT.featureTest().evaluate(poly));
|
||||
|
||||
Assertions.assertFalse(GeometryType.POLYGON.featureTest().evaluate(line));
|
||||
Assertions.assertFalse(GeometryType.POLYGON.featureTest().evaluate(point));
|
||||
Assertions.assertTrue(GeometryType.POLYGON.featureTest().evaluate(poly));
|
||||
|
||||
Assertions.assertThrows(Exception.class, () -> GeometryType.UNKNOWN.featureTest().evaluate(point));
|
||||
Assertions.assertThrows(Exception.class, () -> GeometryType.UNKNOWN.featureTest().evaluate(line));
|
||||
Assertions.assertThrows(Exception.class, () -> GeometryType.UNKNOWN.featureTest().evaluate(poly));
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*.mbtiles
|
|
@ -0,0 +1,113 @@
|
|||
# Configurable Planetiler Schema
|
||||
|
||||
It is possible to customize planetiler's output from configuration files. This is done using the parameter:
|
||||
`--schema=schema_file.yml`
|
||||
|
||||
The schema file provides information to planetiler about how to construct the tiles and which layers, features, and
|
||||
attributes will be posted to the file. Schema files are in [YAML](https://yaml.org) format.
|
||||
|
||||
NOTE: The configuration schema is under active development so the format may change between releases. Feedback is
|
||||
welcome to help shape the final product!
|
||||
|
||||
For examples, see [samples](src/main/resources/samples) or [test cases](src/test/resources/validSchema).
|
||||
|
||||
## Schema file definition
|
||||
|
||||
The root of the schema has the following attributes:
|
||||
|
||||
* `schema_name` - A descriptive name for the schema
|
||||
* `schema_description` - A longer description of the schema
|
||||
* `attribution` - An attribution statement, which may include HTML such as links
|
||||
* `sources` - A list of sources from which features should be extracted, specified as a list of names.
|
||||
See [Tag Mappings](#tag-mappings).
|
||||
* `dataTypes` - A map of tag keys that should be treated as a certain data type, with strings being the default.
|
||||
See [Tag Mappings](#tag-mappings).
|
||||
* `layers` - A list of vector tile layers and their definitions. See [Layers](#layers)
|
||||
|
||||
### Data Sources
|
||||
|
||||
A data source contains geospatial objects with tags that are consumed by planetiler. The configured data sources in the
|
||||
schema provide complete information on how to access those data sources.
|
||||
|
||||
* `type` - Either `shapefile` or `osm`
|
||||
* `url` - Location to download the shapefile from. For geofabrik named areas, use `geofabrik:` prefixes, for
|
||||
example `geofabrik:rhode-island`
|
||||
|
||||
### Layers
|
||||
|
||||
A layer contains a thematically-related set of features.
|
||||
|
||||
* `name` - Name of this layer
|
||||
* `features` - A list of features contained in this layer. See [Features](#features)
|
||||
|
||||
### Features
|
||||
|
||||
A feature is a defined set of objects that meet specified filter criteria.
|
||||
|
||||
* `geometry` - Include objects of a certain geometry type. Options are `polygon`, `line`, or `point`.
|
||||
* `min_tile_cover_size` - include objects of a certain geometry size, where 1.0 means "is the same size as a tile at
|
||||
this zoom".
|
||||
* `include_when` - A tag specification which determines which features to include. If unspecified, all features from the
|
||||
specified sources are included. See [Tag Filters](#tag-filters)
|
||||
* `exclude_when` - A tag specification which determines which features to exclude. This rule is applied
|
||||
after `includeWhen`. If unspecified, no exclusion filter is applied. See [Tag Filters](#tag-filters)
|
||||
* `min_zoom` - Minimum zoom to show the feature that matches the filter specifications.
|
||||
* `zoom_override` - List of rules that overrides the `min_zoom` for this feature if certain tags are present. If
|
||||
multiple rules match, the first matching rule will be applied. See [Feature Zoom Overrides](#feature-zoom-override)
|
||||
* `attributes` - Specifies the attributes that should be rendered into the tiles for this feature, and how they are
|
||||
constructed. See [Attributes](#attributes)
|
||||
|
||||
### Tag Mappings
|
||||
|
||||
Specifies that certain tag key should have their values treated as being a certain data type.
|
||||
|
||||
* `<key>: data_type` - A key, along with one of `boolean`, `string`, `direction`, or `long`
|
||||
* `<key>: mapping` - A mapping which produces a new attribute by retrieving from a different key.
|
||||
See [Tag Input and Output Mappings](#tag-input-and-output-mappings)
|
||||
|
||||
### Tag Input and Output Mappings
|
||||
|
||||
* `type`: One of `boolean`, `string`, `direction`, or `long`
|
||||
* `output`: The name of the typed key that will be presented to the attribute logic
|
||||
|
||||
### Feature Zoom Override
|
||||
|
||||
Specifies a zoom-based inclusion rules for this feature.
|
||||
|
||||
* `min` - Minimum zoom to render a feature matching this rule
|
||||
* `tag` - List of tags for which this rule applies. Tags are specified as a list of key/value pairs
|
||||
|
||||
### Attributes
|
||||
|
||||
* `key` - Name of this attribute in the tile.
|
||||
* `constant_value` - Value of the attribute in the tile, as a constant
|
||||
* `tag_value` - Value of the attribute in the tile, as copied from the value of the specified tag key. If neither
|
||||
constantValue nor tagValue are specified, the default behavior is to set the tag value equal to the input value (
|
||||
pass-through)
|
||||
* `include_when` - A filter specification which determines whether to include this attribute. If unspecified, the
|
||||
attribute will be included unless excluded by `excludeWhen`. See [Tag Filters](#tag-filters)
|
||||
* `exclude_when` - A filter specification which determines whether to exclude this attribute. This rule is applied
|
||||
after `includeWhen`. If unspecified, no exclusion filter is applied. See [Tag Filters](#tag-filters)
|
||||
* `min_zoom` - The minimum zoom at which to render this attribute.
|
||||
* `min_zoom_by_value` - Minimum zoom to render this attribute depending on the value. Contains a map of `value: zoom`
|
||||
entries that indicate the minimum zoom for each possible value.
|
||||
|
||||
### Tag Filters
|
||||
|
||||
A tag filter matches an object based on its tagging. Multiple key entries may be specified:
|
||||
|
||||
* `<key>:` - Match objects that contain this key.
|
||||
* ` <value>` - A single value or a list of values. Match objects in the specified key that contains one of these
|
||||
values. If no values are specified, this will match any value tagged with the specified key.
|
||||
|
||||
Example: match all `natural=water`:
|
||||
|
||||
natural: water
|
||||
|
||||
Example: match residential, commercial, and industrial land use:
|
||||
|
||||
landuse:
|
||||
- residential
|
||||
- commercial
|
||||
- industrial
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?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-custommap</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-parent</artifactId>
|
||||
<version>0.5-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.github.zlika</groupId>
|
||||
<artifactId>reproducible-build-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want to deploy this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,281 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static com.onthegomap.planetiler.custommap.TagCriteria.matcher;
|
||||
import static com.onthegomap.planetiler.expression.Expression.not;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.FeatureCollector.Feature;
|
||||
import com.onthegomap.planetiler.custommap.configschema.AttributeDefinition;
|
||||
import com.onthegomap.planetiler.custommap.configschema.FeatureItem;
|
||||
import com.onthegomap.planetiler.custommap.configschema.ZoomOverride;
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression.Entry;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression.Index;
|
||||
import com.onthegomap.planetiler.geo.GeometryException;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* A map feature, configured from a YML configuration file.
|
||||
*
|
||||
* {@link #matchExpression()} returns a filtering expression to limit input elements to ones this feature cares about,
|
||||
* and {@link #processFeature(SourceFeature, FeatureCollector)} processes matching elements.
|
||||
*/
|
||||
public class ConfiguredFeature {
|
||||
|
||||
private final Set<String> sources;
|
||||
private final Expression geometryTest;
|
||||
private final Function<FeatureCollector, Feature> geometryFactory;
|
||||
private final Expression tagTest;
|
||||
private final Index<Integer> zoomOverride;
|
||||
private final Integer featureMinZoom;
|
||||
private final Integer featureMaxZoom;
|
||||
private final TagValueProducer tagValueProducer;
|
||||
|
||||
private static final double LOG4 = Math.log(4);
|
||||
private static final Index<Integer> NO_ZOOM_OVERRIDE = MultiExpression.<Integer>of(List.of()).index();
|
||||
private static final Integer DEFAULT_MAX_ZOOM = 14;
|
||||
|
||||
private final List<BiConsumer<SourceFeature, Feature>> attributeProcessors;
|
||||
|
||||
public ConfiguredFeature(String layerName, TagValueProducer tagValueProducer, FeatureItem feature) {
|
||||
sources = new HashSet<>(feature.sources());
|
||||
|
||||
GeometryType geometryType = feature.geometry();
|
||||
|
||||
//Test to determine whether this type of geometry is included
|
||||
geometryTest = geometryType.featureTest();
|
||||
|
||||
//Factory to treat OSM tag values as specific data type values
|
||||
this.tagValueProducer = tagValueProducer;
|
||||
|
||||
//Test to determine whether this feature is included based on tagging
|
||||
if (feature.includeWhen() == null) {
|
||||
tagTest = Expression.TRUE;
|
||||
} else {
|
||||
tagTest = matcher(feature.includeWhen(), tagValueProducer);
|
||||
}
|
||||
|
||||
//Index of zoom ranges for a feature based on what tags are present.
|
||||
zoomOverride = zoomOverride(feature.zoom());
|
||||
|
||||
//Test to determine at which zooms to include this feature based on tagging
|
||||
featureMinZoom = feature.minZoom() == null ? 0 : feature.minZoom();
|
||||
featureMaxZoom = feature.maxZoom() == null ? DEFAULT_MAX_ZOOM : feature.maxZoom();
|
||||
|
||||
//Factory to generate the right feature type from FeatureCollector
|
||||
geometryFactory = geometryType.geometryFactory(layerName);
|
||||
|
||||
//Configure logic for each attribute in the output tile
|
||||
attributeProcessors = feature.attributes()
|
||||
.stream()
|
||||
.map(this::attributeProcessor)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an index that matches tags from configuration and returns a minimum zoom level
|
||||
*
|
||||
* @param zoom the configured zoom overrides
|
||||
* @return an index
|
||||
*/
|
||||
private Index<Integer> zoomOverride(Collection<ZoomOverride> zoom) {
|
||||
if (zoom == null || zoom.isEmpty()) {
|
||||
return NO_ZOOM_OVERRIDE;
|
||||
}
|
||||
|
||||
return MultiExpression.of(
|
||||
zoom.stream()
|
||||
.map(this::generateOverrideExpression)
|
||||
.toList())
|
||||
.index();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the zoom override configuration for a single zoom level and returns an expression that matches tags for that
|
||||
* level.
|
||||
*
|
||||
* @param config zoom override for a single level
|
||||
* @return matching expression
|
||||
*/
|
||||
private Entry<Integer> generateOverrideExpression(ZoomOverride config) {
|
||||
return MultiExpression.entry(config.min(),
|
||||
Expression.or(
|
||||
config.tag()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(this::generateKeyExpression)
|
||||
.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an expression that matches against single key with one or more values
|
||||
*
|
||||
* @param keyExpression a map containing a key and one or more values
|
||||
* @return a matching expression
|
||||
*/
|
||||
private Expression generateKeyExpression(Map.Entry<String, Object> keyExpression) {
|
||||
// Values are either a single value, or a collection
|
||||
String key = keyExpression.getKey();
|
||||
Object rawVal = keyExpression.getValue();
|
||||
|
||||
if (rawVal instanceof List<?> tagValues) {
|
||||
return Expression.matchAnyTyped(key, tagValueProducer.valueGetterForKey(key), tagValues);
|
||||
}
|
||||
|
||||
return Expression.matchAnyTyped(key, tagValueProducer.valueGetterForKey(key), rawVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces logic that generates attribute values based on configuration and input data. If both a constantValue
|
||||
* configuration and a tagValue configuration are set, this is likely a mistake, and the constantValue will take
|
||||
* precedence.
|
||||
*
|
||||
* @param attribute - attribute definition configured from YML
|
||||
* @return a function that generates an attribute value from a {@link SourceFeature} based on an attribute
|
||||
* configuration.
|
||||
*/
|
||||
private Function<WithTags, Object> attributeValueProducer(AttributeDefinition attribute) {
|
||||
|
||||
Object constVal = attribute.constantValue();
|
||||
if (constVal != null) {
|
||||
return sf -> constVal;
|
||||
}
|
||||
|
||||
String tagVal = attribute.tagValue();
|
||||
if (tagVal != null) {
|
||||
return tagValueProducer.valueProducerForKey(tagVal);
|
||||
}
|
||||
|
||||
//Default to producing a tag identical to the input
|
||||
return tagValueProducer.valueProducerForKey(attribute.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate logic which determines the minimum zoom level for a feature based on a configured pixel size limit.
|
||||
*
|
||||
* @param minTilePercent - minimum percentage of a tile that a feature must cover to be shown
|
||||
* @param minZoom - global minimum zoom for this feature
|
||||
* @param minZoomByValue - map of tag values to zoom level
|
||||
* @return minimum zoom function
|
||||
*/
|
||||
private static BiFunction<SourceFeature, Object, Integer> attributeZoomThreshold(Double minTilePercent, int minZoom,
|
||||
Map<Object, Integer> minZoomByValue) {
|
||||
|
||||
if (minZoom == 0 && minZoomByValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ToIntFunction<SourceFeature> staticZooms = sf -> Math.max(minZoom, minZoomFromTilePercent(sf, minTilePercent));
|
||||
|
||||
if (minZoomByValue.isEmpty()) {
|
||||
return (sf, key) -> staticZooms.applyAsInt(sf);
|
||||
}
|
||||
|
||||
//Attribute value-specific zooms override static zooms
|
||||
return (sourceFeature, key) -> minZoomByValue.getOrDefault(key, staticZooms.applyAsInt(sourceFeature));
|
||||
}
|
||||
|
||||
private static int minZoomFromTilePercent(SourceFeature sf, Double minTilePercent) {
|
||||
if (minTilePercent == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return (int) (Math.log(minTilePercent / sf.area()) / LOG4);
|
||||
} catch (GeometryException e) {
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a function which produces a fully-configured attribute for a feature.
|
||||
*
|
||||
* @param attribute - configuration for this attribute
|
||||
* @return processing logic
|
||||
*/
|
||||
private BiConsumer<SourceFeature, Feature> attributeProcessor(AttributeDefinition attribute) {
|
||||
var tagKey = attribute.key();
|
||||
|
||||
var attributeMinZoom = attribute.minZoom();
|
||||
attributeMinZoom = attributeMinZoom == null ? 0 : attributeMinZoom;
|
||||
|
||||
var minZoomByValue = attribute.minZoomByValue();
|
||||
minZoomByValue = minZoomByValue == null ? Map.of() : minZoomByValue;
|
||||
|
||||
//Workaround because numeric keys are mapped as String
|
||||
minZoomByValue = tagValueProducer.remapKeysByType(tagKey, minZoomByValue);
|
||||
|
||||
var attributeValueProducer = attributeValueProducer(attribute);
|
||||
|
||||
var attrIncludeWhen = attribute.includeWhen();
|
||||
var attrExcludeWhen = attribute.excludeWhen();
|
||||
|
||||
var attributeTest =
|
||||
Expression.and(
|
||||
attrIncludeWhen == null ? Expression.TRUE : matcher(attrIncludeWhen, tagValueProducer),
|
||||
attrExcludeWhen == null ? Expression.TRUE : not(matcher(attrExcludeWhen, tagValueProducer))
|
||||
).simplify();
|
||||
|
||||
var minTileCoverage = attrIncludeWhen == null ? null : attribute.minTileCoverSize();
|
||||
|
||||
BiFunction<SourceFeature, Object, Integer> attributeZoomProducer =
|
||||
attributeZoomThreshold(minTileCoverage, attributeMinZoom, minZoomByValue);
|
||||
|
||||
if (attributeZoomProducer != null) {
|
||||
return (sf, f) -> {
|
||||
if (attributeTest.evaluate(sf)) {
|
||||
Object value = attributeValueProducer.apply(sf);
|
||||
f.setAttrWithMinzoom(tagKey, value, attributeZoomProducer.apply(sf, value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (sf, f) -> {
|
||||
if (attributeTest.evaluate(sf)) {
|
||||
f.setAttr(tagKey, attributeValueProducer.apply(sf));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an expression that evaluates to true if a source feature should be included in the output.
|
||||
*/
|
||||
public Expression matchExpression() {
|
||||
return Expression.and(geometryTest, tagTest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tile feature based on a source feature.
|
||||
*
|
||||
* @param sourceFeature - input source feature
|
||||
* @param features - output rendered feature collector
|
||||
*/
|
||||
public void processFeature(SourceFeature sourceFeature, FeatureCollector features) {
|
||||
|
||||
//Ensure that this feature is from the correct source
|
||||
if (!sources.contains(sourceFeature.getSource())) {
|
||||
return;
|
||||
}
|
||||
|
||||
var minZoom = zoomOverride.getOrElse(sourceFeature, featureMinZoom);
|
||||
|
||||
var f = geometryFactory.apply(features)
|
||||
.setMinZoom(minZoom)
|
||||
.setMaxZoom(featureMaxZoom);
|
||||
|
||||
for (var processor : attributeProcessors) {
|
||||
processor.accept(sourceFeature, f);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.onthegomap.planetiler.Planetiler;
|
||||
import com.onthegomap.planetiler.config.Arguments;
|
||||
import com.onthegomap.planetiler.custommap.configschema.DataSource;
|
||||
import com.onthegomap.planetiler.custommap.configschema.DataSourceType;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Main driver to create maps configured by a YAML file.
|
||||
*
|
||||
* Parses the config file into a {@link ConfiguredProfile}, loads sources into {@link Planetiler} runner and kicks off
|
||||
* the map generation process.
|
||||
*/
|
||||
public class ConfiguredMapMain {
|
||||
|
||||
private static final Yaml yaml = new Yaml();
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/*
|
||||
* Main entrypoint
|
||||
*/
|
||||
public static void main(String... args) throws Exception {
|
||||
run(Arguments.fromArgsOrConfigFile(args));
|
||||
}
|
||||
|
||||
static void run(Arguments args) throws Exception {
|
||||
var dataDir = Path.of("data");
|
||||
var sourcesDir = dataDir.resolve("sources");
|
||||
|
||||
var schemaFile = args.inputFile(
|
||||
"schema",
|
||||
"Location of YML-format schema definition file");
|
||||
|
||||
var config = loadConfig(schemaFile);
|
||||
|
||||
var planetiler = Planetiler.create(args)
|
||||
.setProfile(new ConfiguredProfile(config));
|
||||
|
||||
var sources = config.sources();
|
||||
for (var source : sources.entrySet()) {
|
||||
configureSource(planetiler, sourcesDir, source.getKey(), source.getValue());
|
||||
}
|
||||
|
||||
planetiler.overwriteOutput("mbtiles", Path.of("data", "output.mbtiles"))
|
||||
.run();
|
||||
}
|
||||
|
||||
static SchemaConfig loadConfig(Path schemaFile) throws IOException {
|
||||
try (var schemaStream = Files.newInputStream(schemaFile)) {
|
||||
Map<String, Object> parsed = yaml.load(schemaStream);
|
||||
return mapper.convertValue(parsed, SchemaConfig.class);
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureSource(Planetiler planetiler, Path sourcesDir, String sourceName, DataSource source)
|
||||
throws URISyntaxException {
|
||||
|
||||
DataSourceType sourceType = source.type();
|
||||
Path localPath = source.localPath();
|
||||
|
||||
switch (sourceType) {
|
||||
case OSM -> {
|
||||
String url = source.url();
|
||||
String[] areaParts = url.split("[:/]");
|
||||
String areaFilename = areaParts[areaParts.length - 1];
|
||||
String areaName = areaFilename.replaceAll("\\..*$", "");
|
||||
if (localPath == null) {
|
||||
localPath = sourcesDir.resolve(areaName + ".osm.pbf");
|
||||
}
|
||||
planetiler.addOsmSource(sourceName, localPath, url);
|
||||
}
|
||||
case SHAPEFILE -> {
|
||||
String url = source.url();
|
||||
if (localPath == null) {
|
||||
localPath = sourcesDir.resolve(Paths.get(new URI(url).getPath()).getFileName().toString());
|
||||
}
|
||||
planetiler.addShapefileSource(sourceName, localPath, url);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unhandled source " + sourceType);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.MultiExpression.Entry;
|
||||
|
||||
import com.onthegomap.planetiler.FeatureCollector;
|
||||
import com.onthegomap.planetiler.Profile;
|
||||
import com.onthegomap.planetiler.custommap.configschema.FeatureLayer;
|
||||
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression;
|
||||
import com.onthegomap.planetiler.expression.MultiExpression.Index;
|
||||
import com.onthegomap.planetiler.reader.SourceFeature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A profile configured from a yml file.
|
||||
*/
|
||||
public class ConfiguredProfile implements Profile {
|
||||
|
||||
private final SchemaConfig schemaConfig;
|
||||
|
||||
private final Index<ConfiguredFeature> featureLayerMatcher;
|
||||
|
||||
public ConfiguredProfile(SchemaConfig schemaConfig) {
|
||||
this.schemaConfig = schemaConfig;
|
||||
|
||||
Collection<FeatureLayer> layers = schemaConfig.layers();
|
||||
if (layers == null || layers.isEmpty()) {
|
||||
throw new IllegalArgumentException("No layers defined");
|
||||
}
|
||||
|
||||
TagValueProducer tagValueProducer = new TagValueProducer(schemaConfig.inputMappings());
|
||||
|
||||
List<MultiExpression.Entry<ConfiguredFeature>> configuredFeatureEntries = new ArrayList<>();
|
||||
|
||||
for (var layer : layers) {
|
||||
String layerName = layer.name();
|
||||
for (var feature : layer.features()) {
|
||||
var configuredFeature = new ConfiguredFeature(layerName, tagValueProducer, feature);
|
||||
configuredFeatureEntries.add(
|
||||
new Entry<>(configuredFeature, configuredFeature.matchExpression()));
|
||||
}
|
||||
}
|
||||
|
||||
featureLayerMatcher = MultiExpression.of(configuredFeatureEntries).index();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return schemaConfig.schemaName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String attribution() {
|
||||
return schemaConfig.attribution();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFeature(SourceFeature sourceFeature, FeatureCollector featureCollector) {
|
||||
featureLayerMatcher.getMatches(sourceFeature)
|
||||
.forEach(configuredFeature -> configuredFeature.processFeature(sourceFeature, featureCollector));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return schemaConfig.schemaDescription();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static com.onthegomap.planetiler.expression.Expression.matchAnyTyped;
|
||||
import static com.onthegomap.planetiler.expression.Expression.matchField;
|
||||
|
||||
import com.onthegomap.planetiler.expression.Expression;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility that maps expressions in YAML format to {@link Expression Expressions}.
|
||||
*/
|
||||
public class TagCriteria {
|
||||
|
||||
private TagCriteria() {
|
||||
//Hide implicit public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that determines whether a source feature matches any of the entries in this specification
|
||||
*
|
||||
* @param map a map of tag criteria
|
||||
* @param tagValueProducer a TagValueProducer
|
||||
* @return a predicate which returns true if this criteria matches
|
||||
*/
|
||||
public static Expression matcher(Map<String, Object> map, TagValueProducer tagValueProducer) {
|
||||
return map.entrySet()
|
||||
.stream()
|
||||
.map(entry -> tagCriterionToExpression(tagValueProducer, entry.getKey(), entry.getValue()))
|
||||
.reduce(Expression::or)
|
||||
.orElse(Expression.TRUE);
|
||||
}
|
||||
|
||||
private static Expression tagCriterionToExpression(TagValueProducer tagValueProducer, String key, Object value) {
|
||||
|
||||
//If only a key is provided, with no value, match any object tagged with that key.
|
||||
if (value == null) {
|
||||
return matchField(key);
|
||||
|
||||
//If a collection is provided, match any of these values.
|
||||
} else if (value instanceof Collection<?> values) {
|
||||
return matchAnyTyped(
|
||||
key,
|
||||
tagValueProducer.valueGetterForKey(key),
|
||||
values.stream().toList());
|
||||
|
||||
//Otherwise, a key and single value were passed, so match that exact tag
|
||||
} else {
|
||||
return matchAnyTyped(
|
||||
key,
|
||||
tagValueProducer.valueGetterForKey(key),
|
||||
value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import com.onthegomap.planetiler.reader.WithTags;
|
||||
import com.onthegomap.planetiler.util.Parse;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Utility that parses attribute values from source features, based on YAML config.
|
||||
*/
|
||||
public class TagValueProducer {
|
||||
|
||||
private static final String STRING_DATATYPE = "string";
|
||||
private static final String BOOLEAN_DATATYPE = "boolean";
|
||||
private static final String DIRECTION_DATATYPE = "direction";
|
||||
private static final String LONG_DATATYPE = "long";
|
||||
|
||||
private static final BiFunction<WithTags, String, Object> DEFAULT_GETTER = WithTags::getTag;
|
||||
|
||||
private final Map<String, BiFunction<WithTags, String, Object>> valueRetriever = new HashMap<>();
|
||||
|
||||
private final Map<String, String> keyType = new HashMap<>();
|
||||
|
||||
private static final Map<String, BiFunction<WithTags, String, Object>> inputGetter =
|
||||
Map.of(
|
||||
STRING_DATATYPE, WithTags::getString,
|
||||
BOOLEAN_DATATYPE, WithTags::getBoolean,
|
||||
DIRECTION_DATATYPE, WithTags::getDirection,
|
||||
LONG_DATATYPE, WithTags::getLong
|
||||
);
|
||||
|
||||
private static final Map<String, UnaryOperator<Object>> inputParse =
|
||||
Map.of(
|
||||
STRING_DATATYPE, s -> s,
|
||||
BOOLEAN_DATATYPE, Parse::bool,
|
||||
DIRECTION_DATATYPE, Parse::direction,
|
||||
LONG_DATATYPE, Parse::parseLong
|
||||
);
|
||||
|
||||
public TagValueProducer(Map<String, Object> map) {
|
||||
if (map == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.forEach((key, value) -> {
|
||||
if (value instanceof String stringType) {
|
||||
valueRetriever.put(key, inputGetter.get(stringType));
|
||||
keyType.put(key, stringType);
|
||||
} else if (value instanceof Map<?, ?> renameMap) {
|
||||
String output = renameMap.containsKey("output") ? renameMap.get("output").toString() : key;
|
||||
BiFunction<WithTags, String, Object> getter =
|
||||
renameMap.containsKey("type") ? inputGetter.get(renameMap.get("type").toString()) : DEFAULT_GETTER;
|
||||
//When requesting the output value, actually retrieve the input key with the desired getter
|
||||
valueRetriever.put(output,
|
||||
(withTags, requestedKey) -> getter.apply(withTags, key));
|
||||
if (renameMap.containsKey("type")) {
|
||||
keyType.put(output, renameMap.get("type").toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that extracts the value for {@code key} from a {@link WithTags} instance.
|
||||
*/
|
||||
public BiFunction<WithTags, String, Object> valueGetterForKey(String key) {
|
||||
return valueRetriever.getOrDefault(key, DEFAULT_GETTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that extracts the value for {@code key} from a {@link WithTags} instance.
|
||||
*/
|
||||
public Function<WithTags, Object> valueProducerForKey(String key) {
|
||||
var getter = valueGetterForKey(key);
|
||||
return withTags -> getter.apply(withTags, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns copy of {@code keyedMap} where the keys have been transformed by the parser associated with {code key}.
|
||||
*/
|
||||
public <T> Map<Object, T> remapKeysByType(String key, Map<Object, T> keyedMap) {
|
||||
Map<Object, T> newMap = new LinkedHashMap<>();
|
||||
|
||||
String dataType = keyType.get(key);
|
||||
UnaryOperator<Object> parser;
|
||||
|
||||
if (dataType == null || (parser = inputParse.get(dataType)) == null) {
|
||||
newMap.putAll(keyedMap);
|
||||
} else {
|
||||
keyedMap.forEach((mapKey, value) -> newMap.put(parser.apply(mapKey), value));
|
||||
}
|
||||
|
||||
return newMap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
|
||||
public record AttributeDefinition(
|
||||
String key,
|
||||
@JsonProperty("constant_value") Object constantValue,
|
||||
@JsonProperty("tag_value") String tagValue,
|
||||
@JsonProperty("include_when") Map<String, Object> includeWhen,
|
||||
@JsonProperty("exclude_when") Map<String, Object> excludeWhen,
|
||||
@JsonProperty("min_zoom") Integer minZoom,
|
||||
@JsonProperty("min_zoom_by_value") Map<Object, Integer> minZoomByValue,
|
||||
@JsonProperty("min_tile_cover_size") Double minTileCoverSize
|
||||
) {}
|
|
@ -0,0 +1,10 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record DataSource(
|
||||
DataSourceType type,
|
||||
String url,
|
||||
@JsonProperty("local_path") Path localPath
|
||||
) {}
|
|
@ -0,0 +1,10 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public enum DataSourceType {
|
||||
@JsonProperty("osm")
|
||||
OSM,
|
||||
@JsonProperty("shapefile")
|
||||
SHAPEFILE
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.onthegomap.planetiler.geo.GeometryType;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public record FeatureItem(
|
||||
Collection<String> sources,
|
||||
@JsonProperty("min_zoom") Integer minZoom,
|
||||
@JsonProperty("max_zoom") Integer maxZoom,
|
||||
GeometryType geometry,
|
||||
@JsonProperty("zoom_override") Collection<ZoomOverride> zoom,
|
||||
@JsonProperty("include_when") Map<String, Object> includeWhen,
|
||||
@JsonProperty("exclude_when") Map<String, Object> excludeWhen,
|
||||
Collection<AttributeDefinition> attributes
|
||||
) {}
|
|
@ -0,0 +1,8 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public record FeatureLayer(
|
||||
String name,
|
||||
Collection<FeatureItem> features
|
||||
) {}
|
|
@ -0,0 +1,27 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An object representation of a vector tile server schema. This object is mapped to a schema YML file using SnakeYAML.
|
||||
*/
|
||||
public record SchemaConfig(
|
||||
@JsonProperty("schema_name") String schemaName,
|
||||
@JsonProperty("schema_description") String schemaDescription,
|
||||
String attribution,
|
||||
Map<String, DataSource> sources,
|
||||
@JsonProperty("tag_mappings") Map<String, Object> inputMappings,
|
||||
Collection<FeatureLayer> layers
|
||||
) {
|
||||
|
||||
private static final String DEFAULT_ATTRIBUTION = """
|
||||
<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>
|
||||
""".trim();
|
||||
|
||||
@Override
|
||||
public String attribution() {
|
||||
return attribution == null ? DEFAULT_ATTRIBUTION : attribution;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.onthegomap.planetiler.custommap.configschema;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration item that instructs the renderer to override the default zoom range for features which contain specific
|
||||
* tag combinations.
|
||||
*/
|
||||
public record ZoomOverride(
|
||||
Integer min,
|
||||
Integer max,
|
||||
Map<String, Object> tag) {}
|
|
@ -0,0 +1,37 @@
|
|||
schema_name: Highway areas
|
||||
schema_description: Features that represent the physical area of roads
|
||||
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">©
|
||||
OpenStreetMap contributors</a>
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:poland
|
||||
tag_mappings:
|
||||
bridge: boolean
|
||||
layer: long
|
||||
layers:
|
||||
- name: highway_area
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
min_zoom: 14
|
||||
include_when:
|
||||
area:highway:
|
||||
attributes:
|
||||
- key: highway
|
||||
tag_value: area:highway
|
||||
- key: layer
|
||||
- key: surface
|
||||
- key: bridge
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
min_zoom: 14
|
||||
include_when:
|
||||
man_made: bridge
|
||||
attributes:
|
||||
- key: man_made
|
||||
constant_value: bridge
|
||||
- key: layer
|
||||
- key: surface
|
|
@ -0,0 +1,22 @@
|
|||
schema_name: Manhole covers
|
||||
schema_description: Manhole covers
|
||||
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">©
|
||||
OpenStreetMap contributors</a>
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- name: manhole
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: point
|
||||
min_zoom: 14
|
||||
include_when:
|
||||
man_made: manhole
|
||||
attributes:
|
||||
- key: man_made
|
||||
- key: manhole
|
||||
- key: operator
|
||||
- key: ref
|
|
@ -0,0 +1,120 @@
|
|||
schema_name: OWG Simple Schema
|
||||
schema_description: Simple vector tile schema
|
||||
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">©
|
||||
OpenStreetMap contributors</a>
|
||||
sources:
|
||||
water_polygons:
|
||||
type: shapefile
|
||||
url: https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:monaco
|
||||
tag_mappings:
|
||||
bridge: boolean
|
||||
intermittent: boolean
|
||||
layer: long
|
||||
tunnel: boolean
|
||||
layers:
|
||||
- name: water
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
||||
- key: intermittent
|
||||
include_when:
|
||||
intermittent: true
|
||||
- key: name
|
||||
min_tile_cover_size: 0.01
|
||||
include_when:
|
||||
exclude_when:
|
||||
tag:
|
||||
key: water
|
||||
value:
|
||||
- river
|
||||
- canal
|
||||
- stream
|
||||
- sources:
|
||||
- water_polygons
|
||||
geometry: polygon
|
||||
include_when:
|
||||
attributes:
|
||||
- key: natural
|
||||
constant_value: water
|
||||
- sources:
|
||||
- osm
|
||||
min_zoom: 7
|
||||
geometry: line
|
||||
include_when:
|
||||
tag:
|
||||
key: waterway
|
||||
value:
|
||||
- river
|
||||
- stream
|
||||
- canal
|
||||
attributes:
|
||||
- key: waterway
|
||||
- key: intermittent
|
||||
include_when:
|
||||
intermittent: true
|
||||
- key: name
|
||||
min_zoom: 12
|
||||
- name: road
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: line
|
||||
include_when:
|
||||
highway:
|
||||
- motorway
|
||||
- trunk
|
||||
- primary
|
||||
- secondary
|
||||
- tertiary
|
||||
- motorway_link
|
||||
- trunk_link
|
||||
- primary_link
|
||||
- secondary_link
|
||||
- tertiary_link
|
||||
- unclassified
|
||||
- residential
|
||||
- living_street
|
||||
- service
|
||||
- track
|
||||
min_zoom: 4
|
||||
zoom_override:
|
||||
- min: 5
|
||||
tag:
|
||||
highway: trunk
|
||||
- min: 7
|
||||
tag:
|
||||
highway: primary
|
||||
- min: 8
|
||||
tag:
|
||||
highway: secondary
|
||||
- min: 9
|
||||
tag:
|
||||
highway:
|
||||
- tertiary
|
||||
- motorway_link
|
||||
- trunk_link
|
||||
- primary_link
|
||||
- secondary_link
|
||||
- tertiary_link
|
||||
- min: 11
|
||||
tag:
|
||||
highway:
|
||||
- unclassified
|
||||
- residential
|
||||
- living_street
|
||||
- min: 12
|
||||
tag:
|
||||
highway: track
|
||||
- min: 13
|
||||
tag:
|
||||
highway: service
|
||||
attributes:
|
||||
- key: highway
|
|
@ -0,0 +1,35 @@
|
|||
schema_name: Power
|
||||
schema_description: Features that represent electrical power grid
|
||||
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">©
|
||||
OpenStreetMap contributors</a>
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:new-jersey
|
||||
layers:
|
||||
- name: power
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: point
|
||||
min_zoom: 13
|
||||
include_when:
|
||||
power:
|
||||
- pole
|
||||
attributes:
|
||||
- key: power
|
||||
- key: ref
|
||||
- key: height
|
||||
- key: operator
|
||||
- sources:
|
||||
- osm
|
||||
geometry: line
|
||||
min_zoom: 12
|
||||
include_when:
|
||||
power:
|
||||
- line
|
||||
attributes:
|
||||
- key: power
|
||||
- key: voltage
|
||||
- key: cables
|
||||
- key: operator
|
|
@ -0,0 +1,299 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.newLineString;
|
||||
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.TestUtils;
|
||||
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
||||
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.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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 Map<String, Object> motorwayTags = Map.of(
|
||||
"highway", "motorway",
|
||||
"layer", "1",
|
||||
"bridge", "yes",
|
||||
"tunnel", "yes"
|
||||
);
|
||||
|
||||
private static Map<String, Object> trunkTags = Map.of(
|
||||
"highway", "trunk",
|
||||
"toll", "yes"
|
||||
);
|
||||
|
||||
private static Map<String, Object> primaryTags = Map.of(
|
||||
"highway", "primary",
|
||||
"lanes", "2"
|
||||
|
||||
);
|
||||
|
||||
private static Map<String, Object> highwayAreaTags = Map.of(
|
||||
"area:highway", "motorway",
|
||||
"layer", "1",
|
||||
"bridge", "yes",
|
||||
"surface", "asphalt"
|
||||
);
|
||||
|
||||
private static Map<String, Object> inputMappingTags = Map.of(
|
||||
"s_type", "string_val",
|
||||
"l_type", "1",
|
||||
"b_type", "yes",
|
||||
"d_type", "yes",
|
||||
"intermittent", "yes",
|
||||
"bridge", "yes"
|
||||
);
|
||||
|
||||
private static FeatureCollector polygonFeatureCollector() {
|
||||
var config = PlanetilerConfig.defaults();
|
||||
var factory = new FeatureCollector.Factory(config, Stats.inMemory());
|
||||
return factory.get(SimpleFeature.create(TestUtils.newPolygon(0, 0, 0.1, 0, 0.1, 0.1, 0, 0), new HashMap<>()));
|
||||
}
|
||||
|
||||
private static FeatureCollector linestringFeatureCollector() {
|
||||
var config = PlanetilerConfig.defaults();
|
||||
var factory = new FeatureCollector.Factory(config, Stats.inMemory());
|
||||
return factory.get(SimpleFeature.create(TestUtils.newLineString(0, 0, 0.1, 0, 0.1, 0.1, 0, 0), new HashMap<>()));
|
||||
}
|
||||
|
||||
private static Profile loadConfig(Function<String, Path> pathFunction, String filename) throws IOException {
|
||||
var staticAttributeConfig = pathFunction.apply(filename);
|
||||
var schema = ConfiguredMapMain.loadConfig(staticAttributeConfig);
|
||||
return new ConfiguredProfile(schema);
|
||||
}
|
||||
|
||||
private static void testFeature(Function<String, Path> pathFunction, String schemaFilename, SourceFeature sf,
|
||||
Supplier<FeatureCollector> fcFactory,
|
||||
Consumer<Feature> test, int expectedMatchCount)
|
||||
throws Exception {
|
||||
|
||||
var profile = loadConfig(pathFunction, schemaFilename);
|
||||
var fc = fcFactory.get();
|
||||
|
||||
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(Function<String, Path> pathFunction, String schemaFilename, Map<String, Object> tags,
|
||||
Consumer<Feature> test, int expectedMatchCount)
|
||||
throws Exception {
|
||||
var sf =
|
||||
SimpleFeature.createFakeOsmFeature(newPolygon(0, 0, 1, 0, 1, 1, 0, 0), tags, "osm", null, 1, emptyList());
|
||||
testFeature(pathFunction, schemaFilename, sf,
|
||||
ConfiguredFeatureTest::polygonFeatureCollector, test, expectedMatchCount);
|
||||
}
|
||||
|
||||
private static void testLinestring(Function<String, Path> pathFunction, String schemaFilename,
|
||||
Map<String, Object> tags, Consumer<Feature> test, int expectedMatchCount)
|
||||
throws Exception {
|
||||
var sf =
|
||||
SimpleFeature.createFakeOsmFeature(newLineString(0, 0, 1, 0, 1, 1), tags, "osm", null, 1, emptyList());
|
||||
testFeature(pathFunction, schemaFilename, sf,
|
||||
ConfiguredFeatureTest::linestringFeatureCollector, test, expectedMatchCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStaticAttributeTest() throws Exception {
|
||||
testPolygon(TEST_RESOURCE, "static_attribute.yml", waterTags, f -> {
|
||||
var attr = f.getAttrsAtZoom(14);
|
||||
assertEquals("aTestConstantValue", attr.get("natural"));
|
||||
}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTagValueAttributeTest() throws Exception {
|
||||
testPolygon(TEST_RESOURCE, "tag_attribute.yml", waterTags, f -> {
|
||||
var attr = f.getAttrsAtZoom(14);
|
||||
assertEquals("water", attr.get("natural"));
|
||||
}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTagIncludeAttributeTest() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
testLinestring(TEST_RESOURCE, "road_motorway.yml", motorwayTags, f -> {
|
||||
var attr = f.getAttrsAtZoom(14);
|
||||
assertEquals("motorway", attr.get("highway"));
|
||||
}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTagTypeConversionTest() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
//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() throws Exception {
|
||||
//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("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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGeometryTypeMismatch() throws Exception {
|
||||
//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,
|
||||
ConfiguredFeatureTest::linestringFeatureCollector, f -> {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSourceTypeMismatch() throws Exception {
|
||||
//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,
|
||||
ConfiguredFeatureTest::linestringFeatureCollector, f -> {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidSchemas() throws Exception {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static com.onthegomap.planetiler.TestUtils.assertContains;
|
||||
import static com.onthegomap.planetiler.custommap.util.VerifyMonaco.MONACO_BOUNDS;
|
||||
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.custommap.util.TestConfigurableUtils;
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
/**
|
||||
* End-to-end tests for custommap generation.
|
||||
* <p>
|
||||
* Generates an entire map for the smallest openstreetmap extract available (Monaco) and asserts that expected output
|
||||
* features exist
|
||||
*/
|
||||
class ConfiguredMapTest {
|
||||
|
||||
@TempDir
|
||||
static Path tmpDir;
|
||||
private static Mbtiles mbtiles;
|
||||
|
||||
@BeforeAll
|
||||
public static void runPlanetiler() throws Exception {
|
||||
Path dbPath = tmpDir.resolve("output.mbtiles");
|
||||
ConfiguredMapMain.main(
|
||||
"generate-custom",
|
||||
// Use local data extracts instead of downloading
|
||||
"--schema=" + TestConfigurableUtils.pathToSample("owg_simple.yml"),
|
||||
"--osm_path=" + TestUtils.pathToResource("monaco-latest.osm.pbf"),
|
||||
"--water_polygons_path=" + TestUtils.pathToResource("water-polygons-split-3857.zip"),
|
||||
|
||||
// Override temp dir location
|
||||
"--tmp=" + tmpDir,
|
||||
|
||||
// Override output location
|
||||
"--mbtiles=" + dbPath
|
||||
);
|
||||
mbtiles = Mbtiles.newReadOnlyDatabase(dbPath);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void close() throws IOException {
|
||||
mbtiles.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMetadata() {
|
||||
Map<String, String> metadata = mbtiles.metadata().getAll();
|
||||
assertEquals("OWG Simple 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 {
|
||||
Set<Mbtiles.TileEntry> parsedTiles = TestUtils.getAllTiles(mbtiles);
|
||||
for (var tileEntry : parsedTiles) {
|
||||
var decoded = VectorTile.decode(gunzip(tileEntry.bytes()));
|
||||
for (VectorTile.Feature feature : decoded) {
|
||||
TestUtils.validateGeometry(feature.geometry().decode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Test --TODO FIX after adding water layer
|
||||
void testContainsOceanPolyons() {
|
||||
assertMinFeatures("water", Map.of(
|
||||
"natural", "water"
|
||||
), 0, 1, Polygon.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoad() {
|
||||
assertMinFeatures("road", Map.of(
|
||||
"highway", "primary"
|
||||
), 14, 200, LineString.class);
|
||||
}
|
||||
|
||||
private static 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package com.onthegomap.planetiler.custommap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchemaYAMLLoadTest {
|
||||
|
||||
/**
|
||||
* Test to ensure that all bundled schemas load to POJOs.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
void testSchemaLoad() throws IOException {
|
||||
testSchemasInFolder(Paths.get("src", "main", "resources", "samples"));
|
||||
testSchemasInFolder(Paths.get("src", "test", "resources", "validSchema"));
|
||||
}
|
||||
|
||||
private void testSchemasInFolder(Path path) throws IOException {
|
||||
var schemaFiles = Files.walk(path)
|
||||
.filter(p -> p.getFileName().toString().endsWith(".yml"))
|
||||
.toList();
|
||||
|
||||
assertFalse(schemaFiles.isEmpty(), "No files found");
|
||||
|
||||
for (Path schemaFile : schemaFiles) {
|
||||
var schemaConfig = ConfiguredMapMain.loadConfig(schemaFile);
|
||||
assertNotNull(schemaConfig, () -> "Failed to unmarshall " + schemaFile.toString());
|
||||
assertNotNull(new ConfiguredProfile(schemaConfig), () -> "Failed to load profile from " + schemaFile.toString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.onthegomap.planetiler.custommap.util;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TestConfigurableUtils {
|
||||
public static Path pathToTestResource(String resource) {
|
||||
return resolve(Path.of("planetiler-custommap", "src", "test", "resources", "validSchema", resource));
|
||||
}
|
||||
|
||||
public static Path pathToTestInvalidResource(String resource) {
|
||||
return resolve(Path.of("planetiler-custommap", "src", "test", "resources", "invalidSchema", resource));
|
||||
}
|
||||
|
||||
public static Path pathToSample(String resource) {
|
||||
return resolve(Path.of("planetiler-custommap", "src", "main", "resources", "samples", resource));
|
||||
}
|
||||
|
||||
private static Path resolve(Path pathFromRoot) {
|
||||
Path cwd = Path.of("").toAbsolutePath();
|
||||
return cwd.resolveSibling(pathFromRoot);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.onthegomap.planetiler.custommap.util;
|
||||
|
||||
import com.onthegomap.planetiler.mbtiles.Mbtiles;
|
||||
import com.onthegomap.planetiler.mbtiles.Verify;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
/**
|
||||
* A utility to check the contents of an mbtiles file generated for Monaco.
|
||||
*/
|
||||
public class VerifyMonaco {
|
||||
|
||||
public static final Envelope MONACO_BOUNDS = new Envelope(7.40921, 7.44864, 43.72335, 43.75169);
|
||||
|
||||
/**
|
||||
* Returns a verification result with a basic set of checks against an openmaptiles map built from an extract for
|
||||
* Monaco.
|
||||
*/
|
||||
public static Verify verify(Mbtiles mbtiles) {
|
||||
Verify verify = Verify.verify(mbtiles);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "building", Map.of(), 13, 14, 100, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "transportation", Map.of(), 10, 14, 5, LineString.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "landcover", Map.of(
|
||||
"class", "grass",
|
||||
"subclass", "park"
|
||||
), 14, 10, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "water", Map.of("class", "ocean"), 0, 14, 1, Polygon.class);
|
||||
verify.checkMinFeatureCount(MONACO_BOUNDS, "place", Map.of("class", "country"), 2, 14, 1, Point.class);
|
||||
return verify;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
try (var mbtiles = Mbtiles.newReadOnlyDatabase(Path.of(args[0]))) {
|
||||
var result = verify(mbtiles);
|
||||
result.print();
|
||||
result.failIfErrors();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: smurf
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: water
|
||||
- constant_value: wet
|
|
@ -0,0 +1,7 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
|
@ -0,0 +1,31 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
tag_mappings:
|
||||
b_type: boolean
|
||||
l_type: long
|
||||
d_type: direction
|
||||
s_type: string
|
||||
intermittent:
|
||||
output: is_intermittent
|
||||
type: boolean
|
||||
bridge:
|
||||
type: boolean
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: line
|
||||
attributes:
|
||||
- key: b_type
|
||||
- key: l_type
|
||||
- key: d_type
|
||||
- key: s_type
|
||||
- key: intermittent
|
||||
- key: is_intermittent
|
||||
- key: bridge
|
|
@ -0,0 +1,18 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
local_path: data/rhode-island.osm.pbf
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
|
@ -0,0 +1,39 @@
|
|||
schema_name: OWG Simple Schema
|
||||
schema_description: Simple vector tile schema
|
||||
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">©
|
||||
OpenStreetMap contributors</a>
|
||||
sources:
|
||||
water_polygons:
|
||||
type: shapefile
|
||||
url: https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
tag_mappings:
|
||||
bridge: boolean # input=bridge, output=bridge, type=boolean
|
||||
layer: long
|
||||
tunnel: boolean
|
||||
layers:
|
||||
- name: road
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
min_zoom: 4
|
||||
geometry: line
|
||||
include_when:
|
||||
highway: motorway
|
||||
attributes:
|
||||
- key: highway
|
||||
- key: bridge
|
||||
include_when:
|
||||
bridge: true
|
||||
min_zoom: 11
|
||||
- key: tunnel
|
||||
constant_value: true
|
||||
include_when:
|
||||
tunnel: true
|
||||
min_zoom: 11
|
||||
- key: name
|
||||
min_zoom: 12
|
||||
- key: layer
|
||||
min_zoom: 13
|
|
@ -0,0 +1,18 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
||||
constant_value: aTestConstantValue
|
|
@ -0,0 +1,17 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: polygon
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: natural
|
|
@ -0,0 +1,27 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
min_zoom: 10
|
||||
geometry: polygon
|
||||
include_when:
|
||||
natural: water
|
||||
attributes:
|
||||
- key: test_include
|
||||
constant_value: ok
|
||||
include_when:
|
||||
natural: water
|
||||
- key: test_exclude
|
||||
constant_value: bad
|
||||
include_when:
|
||||
natural: mud
|
||||
- key: test_zoom_tag
|
||||
min_zoom: 12
|
|
@ -0,0 +1,37 @@
|
|||
schema_name: Test Case Schema
|
||||
schema_description: Test case tile schema
|
||||
attribution: Test attribution
|
||||
sources:
|
||||
osm:
|
||||
type: osm
|
||||
url: geofabrik:rhode-island
|
||||
tag_mappings:
|
||||
lanes: long
|
||||
layers:
|
||||
- name: testLayer
|
||||
features:
|
||||
- sources:
|
||||
- osm
|
||||
geometry: line
|
||||
min_zoom: 4
|
||||
zoom_override:
|
||||
- min: 5
|
||||
tag:
|
||||
highway: trunk
|
||||
- min: 7
|
||||
tag:
|
||||
highway: primary
|
||||
include_when:
|
||||
highway:
|
||||
attributes:
|
||||
- key: highway
|
||||
min_zoom_by_value:
|
||||
trunk: 5
|
||||
primary: 7
|
||||
- key: lanes
|
||||
min_zoom_by_value:
|
||||
4: 9
|
||||
3: 9
|
||||
2: 10
|
||||
- key: toll
|
||||
min_zoom: 8
|
|
@ -32,6 +32,11 @@
|
|||
<artifactId>planetiler-basemap</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-custommap</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.onthegomap.planetiler</groupId>
|
||||
<artifactId>planetiler-examples</artifactId>
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.onthegomap.planetiler.basemap.BasemapMain;
|
|||
import com.onthegomap.planetiler.basemap.util.VerifyMonaco;
|
||||
import com.onthegomap.planetiler.benchmarks.BasemapMapping;
|
||||
import com.onthegomap.planetiler.benchmarks.LongLongMapBench;
|
||||
import com.onthegomap.planetiler.custommap.ConfiguredMapMain;
|
||||
import com.onthegomap.planetiler.examples.BikeRouteOverlay;
|
||||
import com.onthegomap.planetiler.examples.ToiletsOverlay;
|
||||
import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi;
|
||||
|
@ -21,6 +22,7 @@ public class Main {
|
|||
private static final EntryPoint DEFAULT_TASK = BasemapMain::main;
|
||||
private static final Map<String, EntryPoint> ENTRY_POINTS = Map.of(
|
||||
"generate-basemap", BasemapMain::main,
|
||||
"generate-custom", ConfiguredMapMain::main,
|
||||
"basemap", BasemapMain::main,
|
||||
"example-bikeroutes", BikeRouteOverlay::main,
|
||||
"example-toilets", ToiletsOverlay::main,
|
||||
|
|
11
pom.xml
11
pom.xml
|
@ -84,6 +84,7 @@
|
|||
<modules>
|
||||
<module>planetiler-core</module>
|
||||
<module>planetiler-basemap</module>
|
||||
<module>planetiler-custommap</module>
|
||||
<module>planetiler-benchmarks</module>
|
||||
<module>planetiler-examples</module>
|
||||
<module>planetiler-dist</module>
|
||||
|
@ -91,6 +92,16 @@
|
|||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.18.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
|
|
Ładowanie…
Reference in New Issue