Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permit post-process merging in custommap schemas #626

Merged
merged 19 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import static java.util.Map.entry;

import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureMerge;
import com.onthegomap.planetiler.Profile;
import com.onthegomap.planetiler.VectorTile;
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.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -25,6 +28,8 @@ public class ConfiguredProfile implements Profile {

private final SchemaConfig schema;

private final Collection<FeatureLayer> layers;
private final Map<String, FeatureLayer> layersById = new HashMap<>();
private final Map<String, Index<ConfiguredFeature>> featureLayerMatcher;
private final TagValueProducer tagValueProducer;
private final Contexts.Root rootContext;
Expand All @@ -33,7 +38,7 @@ public ConfiguredProfile(SchemaConfig schema, Contexts.Root rootContext) {
this.schema = schema;
this.rootContext = rootContext;

Collection<FeatureLayer> layers = schema.layers();
layers = schema.layers();
if (layers == null || layers.isEmpty()) {
throw new IllegalArgumentException("No layers defined");
}
Expand All @@ -44,6 +49,7 @@ public ConfiguredProfile(SchemaConfig schema, Contexts.Root rootContext) {

for (var layer : layers) {
String layerId = layer.id();
layersById.put(layerId, layer);
for (var feature : layer.features()) {
var configuredFeature = new ConfiguredFeature(layerId, tagValueProducer, feature, rootContext);
var entry = new Entry<>(configuredFeature, configuredFeature.matchExpression());
Expand Down Expand Up @@ -84,6 +90,36 @@ public void processFeature(SourceFeature sourceFeature, FeatureCollector feature
}
}

@Override
public List<VectorTile.Feature> postProcessLayerFeatures(String layer, int zoom,
List<VectorTile.Feature> items) throws GeometryException {
FeatureLayer featureLayer = findFeatureLayer(layer);
msbarry marked this conversation as resolved.
Show resolved Hide resolved

if (featureLayer.postProcess() == null) {
return items;
}

if (featureLayer.postProcess().mergeLineStrings() != null) {
var merge = featureLayer.postProcess().mergeLineStrings();

return FeatureMerge.mergeLineStrings(items,
merge.minLength(), // after merging, remove lines that are still less than {minLength}px long
merge.tolerance(), // simplify output linestrings using a {tolerance}px tolerance
merge.buffer() // remove any detail more than {buffer}px outside the tile boundary
);
}

if (featureLayer.postProcess().mergePolygons() != null) {
var merge = featureLayer.postProcess().mergePolygons();

return FeatureMerge.mergeOverlappingPolygons(items,
merge.minArea() // after merging, remove polygons that are still less than {minArea} in square tile pixels
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a chance that there are lines and polygons in the same layer and we want to apply merge to both of them. mergeLineStrings will ignore polygons and mergeOverlappingPolygons will ignore linestrings, so let's change this so that if both are set it applies them sequentially to items and returns the result.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change pushed.


return items;
}

@Override
public String description() {
return schema.schemaDescription();
Expand All @@ -98,4 +134,8 @@ public List<Source> sources() {
});
return sources;
}

public FeatureLayer findFeatureLayer(String layerId) {
return layersById.get(layerId);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.onthegomap.planetiler.custommap.configschema;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collection;

public record FeatureLayer(
String id,
Collection<FeatureItem> features
Collection<FeatureItem> features,
@JsonProperty("tile_post_process") PostProcess postProcess
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good! Only thing left is to mention these features in planetiler.schema.json and planetiler-custommap/README.md

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change pushed.

) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.onthegomap.planetiler.custommap.configschema;

import com.fasterxml.jackson.annotation.JsonProperty;

public record MergeLineStrings(
@JsonProperty("min_length") double minLength,
@JsonProperty("tolerance") double tolerance,
@JsonProperty("buffer") double buffer
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.onthegomap.planetiler.custommap.configschema;

import com.fasterxml.jackson.annotation.JsonProperty;

public record MergePolygons(
@JsonProperty("min_area") double minArea
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.onthegomap.planetiler.custommap.configschema;

import com.fasterxml.jackson.annotation.JsonProperty;

public record PostProcess(
@JsonProperty("merge_line_strings") MergeLineStrings mergeLineStrings,
@JsonProperty("merge_polygons") MergePolygons mergePolygons
) {}
42 changes: 42 additions & 0 deletions planetiler-custommap/src/main/resources/samples/railways.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
schema_name: Railways
schema_description: Railways (layers outputting merged & un-merged lines)
attribution: <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>
args:
area:
description: Geofabrik area to download
default: greater-london
osm_url:
description: OSM URL to download
default: '${ args.area == "planet" ? "aws:latest" : ("geofabrik:" + args.area) }'
sources:
osm:
type: osm
url: '${ args.osm_url }'
layers:
- id: railways_merged
features:
- source: osm
geometry: line
min_zoom: 4
include_when:
__all__:
- railway: rail
- usage: main
exclude_when:
service: __any__
tile_post_process:
merge_line_strings:
min_length: 1
tolerance: 5
buffer: 10
- id: railways_unmerged
features:
- source: osm
geometry: line
min_zoom: 4
include_when:
__all__:
- railway: rail
- usage: main
exclude_when:
service: __any__
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureCollector.Feature;
import com.onthegomap.planetiler.Profile;
import com.onthegomap.planetiler.VectorTile;
import com.onthegomap.planetiler.config.Arguments;
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.custommap.configschema.DataSourceType;
import com.onthegomap.planetiler.custommap.configschema.MergeLineStrings;
import com.onthegomap.planetiler.custommap.configschema.MergePolygons;
import com.onthegomap.planetiler.custommap.configschema.PostProcess;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
import com.onthegomap.planetiler.custommap.util.TestConfigurableUtils;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.stats.Stats;
Expand Down Expand Up @@ -157,6 +163,89 @@ private void testLinestring(Function<String, Path> pathFunction, String schemaFi
testFeature(pathFunction, schemaFilename, sf, test, expectedMatchCount);
}

@Test
void testFeaturePostProcessorNoop() throws GeometryException {
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
""";
var profile = loadConfig(config);

VectorTile.Feature feature = new VectorTile.Feature(
"testLayer",
1,
VectorTile.encodeGeometry(GeoUtils.point(0, 0)),
Map.of()
);
assertEquals(List.of(feature), profile.postProcessLayerFeatures("testLayer", 0, List.of(feature)));
}

@Test
void testFeaturePostProcessorMergeLineStrings() throws GeometryException {
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
tile_post_process:
merge_line_strings:
min_length: 1
tolerance: 5
buffer: 10
""";
var profile = loadConfig(config);

VectorTile.Feature feature = new VectorTile.Feature(
"testLayer",
1,
VectorTile.encodeGeometry(GeoUtils.point(0, 0)),
Map.of()
);
assertEquals(List.of(feature), profile.postProcessLayerFeatures("testLayer", 0, List.of(feature)));
}

@Test
void testFeaturePostProcessorMergePolygons() throws GeometryException {
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
tile_post_process:
merge_polygons:
min_area: 3
""";
var profile = loadConfig(config);

VectorTile.Feature feature = new VectorTile.Feature(
"testLayer",
1,
VectorTile.encodeGeometry(GeoUtils.point(0, 0)),
Map.of()
);
assertEquals(List.of(feature), profile.postProcessLayerFeatures("testLayer", 0, List.of(feature)));
}

@Test
void testStaticAttributeTest() {
testPolygon(TEST_RESOURCE, "static_attribute.yml", waterTags, f -> {
Expand Down Expand Up @@ -786,6 +875,7 @@ void testSourceTypeMismatch() {
void testInvalidSchemas() {
testInvalidSchema("bad_geometry_type.yml", "Profile defined with invalid geometry type");
testInvalidSchema("no_layers.yml", "Profile defined with no layers");
testInvalidSchema("invalid_post_process.yml", "Profile defined with invalid post process element");
}

private void testInvalidSchema(String filename, String message) {
Expand Down Expand Up @@ -1047,4 +1137,78 @@ void setMinSize(String input, double output) {
assertEquals(output, feature.getMinPixelSizeAtZoom(11));
}, 1);
}

@Test
void testSchemaEmptyPostProcess() {
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
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertNull(loadConfig(config).findFeatureLayer("testLayer").postProcess());
}

@Test
void testSchemaPostProcessWithMergeLineStrings() {
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
tile_post_process:
merge_line_strings:
min_length: 1
tolerance: 5
buffer: 10
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
new MergeLineStrings(
1,
5,
10
),
null
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}

@Test
void testSchemaPostProcessWithMergePolygons() {
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
tile_post_process:
merge_polygons:
min_area: 3
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
null,
new MergePolygons(
3
)
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema_name: Test Case Schema
schema_description: Test case tile schema
attribution: Test attribution
sources:
osm:
type: osm
url: geofabrik:rhode-island
layers:
- id: testLayer
features:
- source:
- osm
geometry: line
include_when:
natural: water
attributes:
- key: water
- value: wet
tile_post_process:
merge_everything:
min_length: 1