Add post-processing settings for `*at_max_zoom` (#1307)

pull/1331/head
zstadler 2025-09-06 04:14:31 +03:00 zatwierdzone przez GitHub
rodzic 10c3f6194e
commit 17f747cc8d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
9 zmienionych plików z 269 dodań i 49 usunięć

Wyświetl plik

@ -542,6 +542,10 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
return geometryType == GeometryType.POLYGON;
}
public boolean isLine() {
return geometryType == GeometryType.LINE;
}
/**
* Returns the value by which features are sorted within a layer in the output vector tile.
*/

Wyświetl plik

@ -240,7 +240,17 @@ A feature is a defined set of objects that meet a specified filter criteria.
expression should be skipped. If unspecified, no exclusion filter is applied.
- `min_zoom` - An [Expression](#expression) that returns the minimum zoom to render this feature at.
- `min_size` - An [Expression](#expression) that returns the minimum length of line features or square root of the
minimum area of polygon features to emit below the maximum zoom-level of the map.
minimum area of polygon features to emit below the maximum zoom-level of the map. This value is ignored if the layer
[Tile Post Process](#tile-post-process) is defined.
- `min_size_at_max_zoom` - An [Expression](#expression) that returns the minimum length of line features or square root of the
minimum area of polygon features to emit at the maximum zoom-level of the map. This value is ignored if the layer
[Tile Post Process](#tile-post-process) is defined.
- `tolerance` - An [Expression](#expression) that returns the value for the tile pixel tolerance to use when
simplifying features below the maximum zoom level of the map. This value is ignored for lines or polygons if the layer
[Tile Post Process](#tile-post-process) `tolerance` is defined for `merge_line_strings` or `merge_polygons`, respectively.
- `tolerance_at_max_zoom` - An [Expression](#expression) that returns the value for the tile pixel tolerance to use when
simplifying features at the maximum zoom level of the map. This value is ignored for lines or polygons if the layer
[Tile Post Process](#tile-post-process) `tolerance_at_max_zoom` is defined for `merge_line_strings` or `merge_polygons`, respectively.
- `attributes` - An array of [Feature Attribute](#feature-attribute) objects that specify the attributes to be included
on this output feature.
@ -310,23 +320,41 @@ Specific tile post processing operations for merging features may be defined:
The follow attributes for `merge_line_strings` may be set:
- `min_length` - Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings.
- `tolerance` - After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step.
- `min_length` - Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings,
below the maximum zoom-level of the map.
- `min_length_at_max_zoom` - Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings,
at the maximum zoom-level of the map.
- `tolerance` - After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step,
below the maximum zoom-level of the map.
- `tolerance_at_max_zoom` - After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step,
at the maximum zoom-level of the map.
- `buffer` - Number of pixels outside the visible tile area to include detail for, or -1 to skip clipping step.
The follow attribute for `merge_polygons` may be set:
- `min_area` - Minimum area in square tile pixels of polygons to emit.
- `min_area` - Minimum area in square tile pixels of polygons to emit,
below the maximum zoom-level of the map.
- `min_area_at_max_zoom` - Minimum area in square tile pixels of polygons to emit,
below the maximum zoom-level of the map.
- `tolerance` - Before merging, simplify polygons using this pixel tolerance, or 0 to avoid simplification,
below the maximum zoom-level of the map.
- `tolerance_at_max_zoom` - Before merging, simplify polygons using this pixel tolerance, or 0 to avoid simplification,
at the maximum zoom-level of the map.
For example:
```yaml
merge_line_strings:
min_length: 1
min_length: 3
min_length_at_max_zoom: 0.125
tolerance: 1
tolerance_at_max_zoom: 0.0625
buffer: 5
merge_polygons:
min_area: 1
min_area_at_max_zoom: 0.25
tolerance: 0.5
tolerance_at_max_zoom: 0.125
```
## Data Type

Wyświetl plik

@ -402,16 +402,28 @@
"description": "An expression that returns the minimum zoom to render this feature at.",
"$ref": "#/$defs/expression"
},
"min_size": {
"description": "Minimum length of line features or square root of the minimum area of polygon features to emit below the maximum zoom-level of the map. This value is ignored if the layer Tile Post Process is defined.",
"$ref": "#/$defs/expression"
},
"min_size_at_max_zoom": {
"description": "Minimum length of line features or square root of the minimum area of polygon features to emit at the maximum zoom-level of the map. This value is ignored if the layer Tile Post Process is defined.",
"$ref": "#/$defs/expression"
},
"tolerance": {
"description": "Tile pixel tolerance to use when simplifying features below the maximum zoom level of the map. This value is ignored for lines or polygons if the layerTile Post Process 'tolerance' is defined for 'merge_line_strings' or 'merge_polygons', respectively.",
"$ref": "#/$defs/expression"
},
"tolerance_at_max_zoom": {
"description": "Tile pixel tolerance to use when simplifying features at the maximum zoom level of the map. This value is ignored for lines or polygons if the layerTile Post Process 'tolerance' is defined for 'merge_line_strings' or 'merge_polygons', respectively.",
"$ref": "#/$defs/expression"
},
"attributes": {
"description": "Specifies the attributes that should be rendered into the tiles for this feature, and how they are constructed",
"description": "Specifies the attributes that should be rendered into the tiles for this feature, and how they are constructed.",
"type": "array",
"items": {
"$ref": "#/$defs/attribute"
}
},
"min_size": {
"description": "Minimum length of line features or square root of the minimum area of polygon features to emit below the maximum zoom-level of the map",
"$ref": "#/$defs/expression"
}
}
},
@ -423,11 +435,19 @@
"type": "object",
"properties": {
"min_length": {
"description": "Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings",
"description": "Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings, below the maximum zoom-level of the map.",
"type": "number"
},
"min_length_at_max_zoom": {
"description": "Minimum tile pixel length of features to emit, or 0 to emit all merged linestrings, at the maximum zoom-level of the map.",
"type": "number"
},
"tolerance": {
"description": "After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step",
"description": "After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step, below the maximum zoom-level of the map.",
"type": "number"
},
"tolerance_at_max_zoom": {
"description": "After merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step, at the maximum zoom-level of the map.",
"type": "number"
},
"buffer": {
@ -441,7 +461,19 @@
"type": "object",
"properties": {
"min_area": {
"description": "Minimum area in square tile pixels of polygons to emit",
"description": "Minimum area in square tile pixels of polygons to emit, below the maximum zoom-level of the map.",
"type": "number"
},
"min_area_at_max_zoom": {
"description": "Minimum area in square tile pixels of polygons to emit, at the maximum zoom-level of the map.",
"type": "number"
},
"tolerance": {
"description": "Before merging, simplify polygons using this pixel tolerance, or 0 to avoid simplification, below the maximum zoom-level of the map.",
"type": "number"
},
"tolerance_at_max_zoom": {
"description": "Before merging, simplify polygons using this pixel tolerance, or 0 to avoid simplification, at the maximum zoom-level of the map.",
"type": "number"
}
}

Wyświetl plik

@ -21,8 +21,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.ObjDoubleConsumer;
/**
* A map feature, configured from a YML configuration file.
@ -31,8 +30,6 @@ import org.slf4j.LoggerFactory;
* and {@link #processFeature(Contexts.FeaturePostMatch, FeatureCollector)} processes matching elements.
*/
public class ConfiguredFeature {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfiguredFeature.class);
private static final double LOG4 = Math.log(4);
private final Expression geometryTest;
private final Function<FeatureCollector, Feature> geometryFactory;
@ -95,26 +92,59 @@ public class ConfiguredFeature {
}
processors.add(makeFeatureProcessor(feature.minZoom(), Integer.class, Feature::setMinZoom));
processors.add(makeFeatureProcessor(feature.maxZoom(), Integer.class, Feature::setMaxZoom));
if (layer.postProcess() == null) {
processors.add(makeFeatureProcessor(feature.minSize(), Double.class, Feature::setMinPixelSize));
} else {
processors.add(makeFeatureProcessor(0, Double.class, Feature::setMinPixelSize));
if (feature.minSize() != null ) {
LOGGER.info("Ignored min_size settings in layer {} in favour of its tile_post_process settings",
layer.id());
}
var merge = layer.postProcess().mergeLineStrings();
if (merge != null) {
var minLength = merge.minLength();
if (minLength > 4) {
processors.add(makeFeatureProcessor(minLength, Double.class, Feature::setBufferPixels));
}
}
}
addPostProcessingImplications(layer, feature, processors, rootContext);
// per-feature tolerance settings should take precedence over defaults from post-processing config
processors.add(makeFeatureProcessor(feature.tolerance(), Double.class, Feature::setPixelTolerance));
processors.add(makeFeatureProcessor(feature.toleranceAtMaxZoom(), Double.class, Feature::setPixelToleranceAtMaxZoom));
featureProcessors = processors.stream().filter(Objects::nonNull).toList();
}
/** Consider implications of Post Processing on the feature's processors **/
private void addPostProcessingImplications(FeatureLayer layer, FeatureItem feature,
List<BiConsumer<Contexts.FeaturePostMatch, Feature>> processors,
Contexts.Root rootContext) {
var postProcess = layer.postProcess();
// Consider min_size and min_size_at_max_zoom
if (postProcess == null) {
processors.add(makeFeatureProcessor(feature.minSize(), Double.class, Feature::setMinPixelSize));
processors.add(makeFeatureProcessor(feature.minSizeAtMaxZoom(), Double.class, Feature::setMinPixelSizeAtMaxZoom));
return;
}
// In order for Post-processing to receive all features, the default MinPixelSize* are zero when features are collected
processors.add(makeFeatureProcessor(Objects.requireNonNullElse(feature.minSize(),0), Double.class, Feature::setMinPixelSize));
processors.add(makeFeatureProcessor(Objects.requireNonNullElse(feature.minSizeAtMaxZoom(),0), Double.class, Feature::setMinPixelSizeAtMaxZoom));
// Implications of tile_post_process.merge_line_strings
var mergeLineStrings = postProcess.mergeLineStrings();
if (mergeLineStrings != null) {
processors.add(makeLineFeatureProcessor(mergeLineStrings.tolerance(),Feature::setPixelTolerance));
processors.add(makeLineFeatureProcessor(mergeLineStrings.toleranceAtMaxZoom(),Feature::setPixelToleranceAtMaxZoom));
// postProcess.mergeLineStrings.minLength* and postProcess.mergeLineStrings.buffer
var bufferPixels = maxIgnoringNulls(mergeLineStrings.minLength(), mergeLineStrings.buffer());
var bufferPixelsAtMaxZoom = maxIgnoringNulls(mergeLineStrings.minLengthAtMaxZoom(), mergeLineStrings.buffer());
int maxZoom = rootContext.config().maxzoomForRendering();
if (bufferPixels != null || bufferPixelsAtMaxZoom != null) {
processors.add((context, f) -> {
if (f.isLine()) {
f.setBufferPixelOverrides(z -> z == maxZoom ? bufferPixelsAtMaxZoom : bufferPixels);
}
});
}
}
// Implications of tile_post_process.merge_polygons
var mergePolygons = postProcess.mergePolygons();
if (mergePolygons != null) {
// postProcess.mergePolygons.tolerance*
processors.add(makePolygonFeatureProcessor(mergePolygons.tolerance(),Feature::setPixelTolerance));
processors.add(makePolygonFeatureProcessor(mergePolygons.toleranceAtMaxZoom(),Feature::setPixelToleranceAtMaxZoom));
// TODO: postProcess.mergeLineStrings.minArea*
}
}
private <T> BiConsumer<Contexts.FeaturePostMatch, Feature> makeFeatureProcessor(Object input, Class<T> clazz,
BiConsumer<Feature, T> consumer) {
if (input == null) {
@ -137,6 +167,30 @@ public class ConfiguredFeature {
};
}
private BiConsumer<Contexts.FeaturePostMatch, Feature> makeLineFeatureProcessor(Double input,
ObjDoubleConsumer<Feature> consumer) {
if (input == null) {
return null;
}
return (context, feature) -> {
if (feature.isLine()) {
consumer.accept(feature, input);
}
};
}
private BiConsumer<Contexts.FeaturePostMatch, Feature> makePolygonFeatureProcessor(Double input,
ObjDoubleConsumer<Feature> consumer) {
if (input == null) {
return null;
}
return (context, feature) -> {
if (feature.isPolygon()) {
consumer.accept(feature, input);
}
};
}
private static int minZoomFromTilePercent(SourceFeature sf, Double minTilePercent) {
if (minTilePercent == null) {
return 0;
@ -307,4 +361,10 @@ public class ConfiguredFeature {
processor.accept(context, f);
}
}
private Double maxIgnoringNulls(Double a, Double b) {
if (a == null) return b;
if (b == null) return a;
return Double.max(a, b);
}
}

Wyświetl plik

@ -18,6 +18,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A profile configured from a yml file.
@ -88,21 +89,38 @@ public class ConfiguredProfile implements Profile {
return items;
}
var config = rootContext.config();
if (featureLayer.postProcess().mergeLineStrings() != null) {
var merge = featureLayer.postProcess().mergeLineStrings();
var minLength = Objects.requireNonNullElse(
(zoom == config.maxzoomForRendering()) ?
merge.minLengthAtMaxZoom() :
merge.minLength(),
config.minFeatureSize(zoom));
var tolerance = Objects.requireNonNullElse(
(zoom == config.maxzoomForRendering()) ?
merge.toleranceAtMaxZoom() :
merge.tolerance(),
config.tolerance(zoom));
var buffer = Objects.requireNonNullElse(merge.buffer(), 4.0);
items = 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
minLength, // after merging, remove lines that are still less than {minLength}px long
tolerance, // simplify output linestrings using a {tolerance}px tolerance
buffer // remove any detail more than {buffer}px outside the tile boundary
);
}
if (featureLayer.postProcess().mergePolygons() != null) {
var merge = featureLayer.postProcess().mergePolygons();
var merge = featureLayer.postProcess().mergePolygons();
if (merge != null) {
var minArea = Objects.requireNonNullElse(
(zoom == config.maxzoomForRendering()) ?
merge.minAreaAtMaxZoom() :
merge.minArea(),
config.minFeatureSize(zoom)*config.minFeatureSize(zoom));
items = FeatureMerge.mergeOverlappingPolygons(items,
merge.minArea() // after merging, remove polygons that are still less than {minArea} in square tile pixels
minArea // after merging, remove polygons that are still less than {minArea} in square tile pixels
);
}

Wyświetl plik

@ -10,6 +10,9 @@ public record FeatureItem(
@JsonProperty("min_zoom") Object minZoom,
@JsonProperty("max_zoom") Object maxZoom,
@JsonProperty("min_size") Object minSize,
@JsonProperty("min_size_at_max_zoom") Object minSizeAtMaxZoom,
@JsonProperty("tolerance") Object tolerance,
@JsonProperty("tolerance_at_max_zoom") Object toleranceAtMaxZoom,
@JsonProperty FeatureGeometry geometry,
@JsonProperty("include_when") Object includeWhen,
@JsonProperty("exclude_when") Object excludeWhen,

Wyświetl plik

@ -3,7 +3,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
@JsonProperty("min_length") Double minLength,
@JsonProperty("min_length_at_max_zoom") Double minLengthAtMaxZoom,
@JsonProperty("tolerance") Double tolerance,
@JsonProperty("tolerance_at_max_zoom") Double toleranceAtMaxZoom,
@JsonProperty("buffer") Double buffer
) {}

Wyświetl plik

@ -3,5 +3,8 @@ package com.onthegomap.planetiler.custommap.configschema;
import com.fasterxml.jackson.annotation.JsonProperty;
public record MergePolygons(
@JsonProperty("min_area") double minArea
@JsonProperty("min_area") Double minArea,
@JsonProperty("min_area_at_max_zoom") Double minAreaAtMaxZoom,
@JsonProperty("tolerance") Double tolerance,
@JsonProperty("tolerance_at_max_zoom") Double toleranceAtMaxZoom
) {}

Wyświetl plik

@ -242,10 +242,12 @@ class ConfiguredFeatureTest {
tile_post_process:
merge_line_strings:
min_length: 10
min_length_at_max_zoom: 2
tolerance: 5
buffer: 4
buffer: 6
""", Map.of(), f -> {
assertEquals(10, f.getBufferPixelsAtZoom(14));
assertEquals(10, f.getBufferPixelsAtZoom(12));
assertEquals(6, f.getBufferPixelsAtZoom(14));
}, 1);
}
@ -1250,20 +1252,84 @@ class ConfiguredFeatureTest {
tile_post_process:
merge_line_strings:
min_length: 1
min_length_at_max_zoom: 0.125
tolerance: 5
tolerance_at_max_zoom: 0.0625
buffer: 10
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
new MergeLineStrings(
1,
5,
10
1.0,
0.125,
5.0,
0.0625,
10.0
),
null
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}
@Test
void testSchemaPostProcessWithMergeLineStringsDefaults() {
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:
buffer: 10
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
new MergeLineStrings(
null,
null,
null,
null,
10.0
),
null
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}
@Test
void testSchemaPostProcessMergePolygonsTolerance() {
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:
tolerance: 1.23
tolerance_at_max_zoom: 0.123
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
null,
new MergePolygons(
null,
null,
1.23,
0.123
)
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}
@Test
void testSchemaPostProcessWithMergePolygons() {
var config = """
@ -1280,12 +1346,16 @@ class ConfiguredFeatureTest {
tile_post_process:
merge_polygons:
min_area: 3
min_area_at_max_zoom: 1
""";
this.planetilerConfig = PlanetilerConfig.from(Arguments.of(Map.of()));
assertEquals(new PostProcess(
null,
new MergePolygons(
3
3.0,
1.0,
null,
null
)
), loadConfig(config).findFeatureLayer("testLayer").postProcess());
}