diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/ForwardingProfile.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/ForwardingProfile.java
index 6ffde252..e919b41d 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/ForwardingProfile.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/ForwardingProfile.java
@@ -1,6 +1,7 @@
package com.onthegomap.planetiler;
import com.onthegomap.planetiler.geo.GeometryException;
+import com.onthegomap.planetiler.geo.TileCoord;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.osm.OsmElement;
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
@@ -20,7 +21,8 @@ import java.util.function.Consumer;
*
{@link FeatureProcessor} to handle features from a particular source (added through
* {@link #registerSourceHandler(String, FeatureProcessor)})
* {@link FinishHandler} to be notified whenever we finish processing each source
- * {@link FeaturePostProcessor} to post-process features in a layer before rendering the output tile
+ * {@link LayerPostProcesser} to post-process features in a layer before rendering the output tile
+ * {@link TilePostProcessor} to post-process features in a tile before rendering the output tile
*
* See {@code OpenMapTilesProfile} for a full implementation using this framework.
*/
@@ -35,8 +37,10 @@ public abstract class ForwardingProfile implements Profile {
private final List osmRelationPreprocessors = new ArrayList<>();
/** Handlers that get a callback when each source is finished reading. */
private final List finishHandlers = new ArrayList<>();
- /** Map from layer name to its handler if it implements {@link FeaturePostProcessor}. */
- private final Map> postProcessors = new HashMap<>();
+ /** Map from layer name to its handler if it implements {@link LayerPostProcesser}. */
+ private final Map> layerPostProcessors = new HashMap<>();
+ /** List of handlers that implement {@link TilePostProcessor}. */
+ private final List tilePostProcessors = new ArrayList<>();
/** Map from source ID to its handler if it implements {@link FeatureProcessor}. */
private final Map> sourceElementProcessors = new HashMap<>();
@@ -53,7 +57,7 @@ public abstract class ForwardingProfile implements Profile {
/**
* Call {@code handler} for different events based on which interfaces {@code handler} implements:
- * {@link OsmRelationPreprocessor}, {@link FinishHandler}, or {@link FeaturePostProcessor}.
+ * {@link OsmRelationPreprocessor}, {@link FinishHandler}, {@link TilePostProcessor} or {@link LayerPostProcesser}.
*/
public void registerHandler(Handler handler) {
this.handlers.add(handler);
@@ -69,10 +73,13 @@ public abstract class ForwardingProfile implements Profile {
if (handler instanceof FinishHandler finishHandler) {
finishHandlers.add(finishHandler);
}
- if (handler instanceof FeaturePostProcessor postProcessor) {
- postProcessors.computeIfAbsent(postProcessor.name(), name -> new ArrayList<>())
+ if (handler instanceof LayerPostProcesser postProcessor) {
+ layerPostProcessors.computeIfAbsent(postProcessor.name(), name -> new ArrayList<>())
.add(postProcessor);
}
+ if (handler instanceof TilePostProcessor postProcessor) {
+ tilePostProcessors.add(postProcessor);
+ }
}
@Override
@@ -129,10 +136,10 @@ public abstract class ForwardingProfile implements Profile {
public List postProcessLayerFeatures(String layer, int zoom, List items)
throws GeometryException {
// delegate feature post-processing to each layer, if it implements FeaturePostProcessor
- List handlers = postProcessors.get(layer);
+ List postProcessers = layerPostProcessors.get(layer);
List result = items;
- if (handlers != null) {
- for (FeaturePostProcessor handler : handlers) {
+ if (postProcessers != null) {
+ for (var handler : postProcessers) {
var thisResult = handler.postProcess(zoom, result);
if (thisResult != null) {
result = thisResult;
@@ -142,6 +149,20 @@ public abstract class ForwardingProfile implements Profile {
return result;
}
+ @Override
+ public Map> postProcessTileFeatures(TileCoord tileCoord,
+ Map> layers) throws GeometryException {
+ var result = layers;
+ for (TilePostProcessor postProcessor : tilePostProcessors) {
+ // TODO catch failures to isolate from other tile postprocessors?
+ var thisResult = postProcessor.postProcessTile(tileCoord, result);
+ if (thisResult != null) {
+ result = thisResult;
+ }
+ }
+ return result;
+ }
+
@Override
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
Consumer next) {
@@ -217,11 +238,11 @@ public abstract class ForwardingProfile implements Profile {
List preprocessOsmRelation(OsmElement.Relation relation);
}
- /** Handlers should implement this interface to post-process vector tile features before emitting an output tile. */
- public interface FeaturePostProcessor extends HandlerForLayer {
+ /** Handlers should implement this interface to post-process vector tile features before emitting an output layer. */
+ public interface LayerPostProcesser extends HandlerForLayer {
/**
- * Apply any post-processing to features in this output layer of a tile before writing it to the output file.
+ * Apply any post-processing to features in this output layer of a tile before writing it to the output archive.
*
* @throws GeometryException if the input elements cannot be deserialized, or output elements cannot be serialized
* @see Profile#postProcessLayerFeatures(String, int, List)
@@ -229,6 +250,26 @@ public abstract class ForwardingProfile implements Profile {
List postProcess(int zoom, List items) throws GeometryException;
}
+ /** @deprecated use {@link LayerPostProcesser} or {@link TilePostProcessor} instead */
+ @Deprecated(forRemoval = true)
+ public interface FeaturePostProcessor extends LayerPostProcesser {}
+
+ /**
+ * Handlers should implement this interface to post-process all features in a vector tile before writing to an
+ * archive.
+ */
+ public interface TilePostProcessor extends Handler {
+
+ /**
+ * Apply any post-processing to features in layers in this output tile before writing it to the output archive.
+ *
+ * @throws GeometryException if the input elements cannot be deserialized, or output elements cannot be serialized
+ * @see Profile#postProcessTileFeatures(TileCoord, Map)
+ */
+ Map> postProcessTile(TileCoord tileCoord,
+ Map> layers) throws GeometryException;
+ }
+
/** Handlers should implement this interface to process input features from a given source ID. */
public interface FeatureProcessor {
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/stats/DefaultStats.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/stats/DefaultStats.java
index 8ceb8a2d..f12823b3 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/stats/DefaultStats.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/stats/DefaultStats.java
@@ -6,7 +6,7 @@ package com.onthegomap.planetiler.stats;
public class DefaultStats {
private DefaultStats() {}
- private static Stats defaultValue = null;
+ private static Stats defaultValue = Stats.inMemory();
public static Stats get() {
return defaultValue;
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/ForwardingProfileTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/ForwardingProfileTests.java
index a0572134..51fc7370 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/ForwardingProfileTests.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/ForwardingProfileTests.java
@@ -6,11 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
+import com.onthegomap.planetiler.geo.TileCoord;
import com.onthegomap.planetiler.reader.SimpleFeature;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.osm.OsmElement;
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -130,7 +132,7 @@ class ForwardingProfileTests {
}
@Test
- void testFeaturePostProcessor() throws GeometryException {
+ void testLayerPostProcesser() throws GeometryException {
VectorTile.Feature feature = new VectorTile.Feature(
"layer",
1,
@@ -140,7 +142,7 @@ class ForwardingProfileTests {
assertEquals(List.of(feature), profile.postProcessLayerFeatures("layer", 0, List.of(feature)));
// ignore null response
- profile.registerHandler(new ForwardingProfile.FeaturePostProcessor() {
+ profile.registerHandler(new ForwardingProfile.LayerPostProcesser() {
@Override
public List postProcess(int zoom, List items) {
return null;
@@ -154,7 +156,7 @@ class ForwardingProfileTests {
assertEquals(List.of(feature), profile.postProcessLayerFeatures("a", 0, List.of(feature)));
// empty list removes
- profile.registerHandler(new ForwardingProfile.FeaturePostProcessor() {
+ profile.registerHandler(new ForwardingProfile.LayerPostProcesser() {
@Override
public List postProcess(int zoom, List items) {
return List.of();
@@ -170,7 +172,7 @@ class ForwardingProfileTests {
assertEquals(List.of(feature), profile.postProcessLayerFeatures("b", 0, List.of(feature)));
// 2 handlers for same layer run one after another
- var skip1 = new ForwardingProfile.FeaturePostProcessor() {
+ var skip1 = new ForwardingProfile.LayerPostProcesser() {
@Override
public List postProcess(int zoom, List items) {
return items.stream().skip(1).toList();
@@ -183,7 +185,7 @@ class ForwardingProfileTests {
};
profile.registerHandler(skip1);
profile.registerHandler(skip1);
- profile.registerHandler(new ForwardingProfile.FeaturePostProcessor() {
+ profile.registerHandler(new ForwardingProfile.LayerPostProcesser() {
@Override
public List postProcess(int zoom, List items) {
return null; // ensure that returning null after initial post-processors run keeps the postprocessed result
@@ -199,4 +201,63 @@ class ForwardingProfileTests {
assertEquals(List.of(feature, feature, feature, feature),
profile.postProcessLayerFeatures("c", 0, List.of(feature, feature, feature, feature)));
}
+
+ @Test
+ void testTilePostProcesser() throws GeometryException {
+ VectorTile.Feature feature = new VectorTile.Feature(
+ "layer",
+ 1,
+ VectorTile.encodeGeometry(GeoUtils.point(0, 0)),
+ Map.of()
+ );
+ assertEquals(Map.of("layer", List.of(feature)), profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0), Map.of(
+ "layer", List.of(feature)
+ )));
+
+ // ignore null response
+ profile.registerHandler((ForwardingProfile.TilePostProcessor) (tileCoord, layers) -> null);
+ assertEquals(Map.of("a", List.of(feature)),
+ profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0), Map.of("a", List.of(feature))));
+
+ // empty map removes
+ profile.registerHandler((ForwardingProfile.TilePostProcessor) (tileCoord, layers) -> Map.of());
+ assertEquals(Map.of(),
+ profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0), Map.of("a", List.of(feature))));
+ // also touches elements in another layer
+ assertEquals(Map.of(),
+ profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0), Map.of("b", List.of(feature))));
+ }
+
+ @Test
+ void testStackedTilePostProcessors() throws GeometryException {
+ VectorTile.Feature feature = new VectorTile.Feature(
+ "layer",
+ 1,
+ VectorTile.encodeGeometry(GeoUtils.point(0, 0)),
+ Map.of()
+ );
+ var skip1 = new ForwardingProfile.TilePostProcessor() {
+ @Override
+ public Map> postProcessTile(TileCoord tileCoord,
+ Map> layers) {
+ Map> result = new HashMap<>();
+ for (var key : layers.keySet()) {
+ result.put(key, layers.get(key).stream().skip(1).toList());
+ }
+ return result;
+ }
+ };
+ profile.registerHandler(skip1);
+ profile.registerHandler(skip1);
+ profile.registerHandler((ForwardingProfile.TilePostProcessor) (tileCoord, layers) -> {
+ // ensure that returning null after initial post-processors run keeps the postprocessed result
+ return null;
+ });
+ assertEquals(Map.of("b", List.of(feature, feature)),
+ profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0),
+ Map.of("b", List.of(feature, feature, feature, feature))));
+ assertEquals(Map.of("c", List.of(feature, feature)),
+ profile.postProcessTileFeatures(TileCoord.ofXYZ(0, 0, 0),
+ Map.of("c", List.of(feature, feature, feature, feature))));
+ }
}