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/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)))); + } }