diff --git a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java index 07338230..66562662 100644 --- a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java +++ b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java @@ -63,11 +63,15 @@ public class FeatureCollector implements Iterable { private double defaultBufferPixels = 4; private ZoomFunction bufferPixelOverrides; private double defaultMinPixelSize = 1; + private double minPixelSizeAtMaxZoom = 256d / 4096; private ZoomFunction minPixelSize = null; private ZoomFunction labelGridPixelSize = null; private ZoomFunction labelGridLimit = null; private boolean attrsChangeByZoom = false; private CacheByZoom> attrCache = null; + private double defaultPixelTolerance = 0.1d; + private double pixelToleranceAtMaxZoom = 256d / 4096; + private ZoomFunction pixelTolerance = null; private Feature(String layer, Geometry geom, boolean area) { this.layer = layer; @@ -130,7 +134,8 @@ public class FeatureCollector implements Iterable { } public double getMinPixelSize(int zoom) { - return ZoomFunction.applyAsDoubleOrElse(minPixelSize, zoom, defaultMinPixelSize); + return zoom == 14 ? minPixelSizeAtMaxZoom + : ZoomFunction.applyAsDoubleOrElse(minPixelSize, zoom, defaultMinPixelSize); } public Feature setMinPixelSize(double minPixelSize) { @@ -143,6 +148,41 @@ public class FeatureCollector implements Iterable { return this; } + public Feature setMinPixelSizeAtMaxZoom(double minPixelSize) { + this.minPixelSizeAtMaxZoom = minPixelSize; + return this; + } + + public Feature setMinPixelSizeAtAllZooms(int minPixelSize) { + return setMinPixelSizeAtMaxZoom(minPixelSize) + .setMinPixelSize(minPixelSize); + } + + + public Feature setPixelTolerance(double tolerance) { + this.defaultPixelTolerance = tolerance; + return this; + } + + public Feature setPixelToleranceAtMaxZoom(double tolerance) { + this.pixelToleranceAtMaxZoom = tolerance; + return this; + } + + public Feature setPixelToleranceAtAllZooms(double tolerance) { + return setPixelToleranceAtMaxZoom(tolerance).setPixelTolerance(tolerance); + } + + public Feature setPixelToleranceBelowZoom(int zoom, double tolerance) { + this.pixelTolerance = ZoomFunction.maxZoom(zoom, tolerance); + return this; + } + + public double getPixelTolerance(int zoom) { + return zoom == 14 ? pixelToleranceAtMaxZoom + : ZoomFunction.applyAsDoubleOrElse(pixelTolerance, zoom, defaultPixelTolerance); + } + public double getLabelGridPixelSizeAtZoom(int zoom) { return ZoomFunction.applyAsDoubleOrElse(labelGridPixelSize, zoom, DEFAULT_LABEL_GRID_SIZE); } diff --git a/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java index 6e516257..8b6ff97a 100644 --- a/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java +++ b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java @@ -7,11 +7,9 @@ import com.onthegomap.flatmap.VectorTileEncoder; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.geo.GeometryException; import com.onthegomap.flatmap.geo.TileCoord; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import org.locationtech.jts.geom.Coordinate; @@ -84,7 +82,6 @@ public class FeatureRenderer { long id = idGen.incrementAndGet(); boolean hasLabelGrid = feature.hasLabelGrid(); for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) { - Map> sliced = new HashMap<>(); Map attrs = feature.getAttrsAtZoom(zoom); double buffer = feature.getBufferPixelsAtZoom(zoom) / 256; int tilesAtZoom = 1 << zoom; @@ -138,18 +135,12 @@ public class FeatureRenderer { private void addLinearFeature(FeatureCollector.Feature feature, Geometry input) { long id = idGen.incrementAndGet(); - // TODO move to feature? - double minSizeAtMaxZoom = 1d / 4096; - double normalTolerance = 0.1 / 256; - double toleranceAtMaxZoom = 1d / 4096; - boolean area = input instanceof Polygonal; double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength(); for (int z = feature.getMaxZoom(); z >= feature.getMinZoom(); z--) { - boolean isMaxZoom = feature.getMaxZoom() == 14; double scale = 1 << z; - double tolerance = isMaxZoom ? toleranceAtMaxZoom : normalTolerance; - double minSize = isMaxZoom ? minSizeAtMaxZoom : (feature.getMinPixelSize(z) / 256); + double tolerance = feature.getPixelTolerance(z) / 256d; + double minSize = feature.getMinPixelSize(z) / 256d; if (area) { minSize *= minSize; } else if (worldLength > 0 && worldLength * scale < minSize) { diff --git a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java index 1a2cc447..ebcbd3b8 100644 --- a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java +++ b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java @@ -4,6 +4,7 @@ import static com.onthegomap.flatmap.TestUtils.assertSubmap; import static com.onthegomap.flatmap.TestUtils.newLineString; import static com.onthegomap.flatmap.TestUtils.newPoint; import static com.onthegomap.flatmap.TestUtils.newPolygon; +import static com.onthegomap.flatmap.TestUtils.rectangle; import static org.junit.jupiter.api.Assertions.assertEquals; import com.onthegomap.flatmap.read.ReaderFeature; @@ -103,7 +104,7 @@ public class FeatureCollectorTest { .setAttr("attr1", 2) .setMinPixelSize(1) .setMinPixelSizeBelowZoom(12, 10); - assertFeatures(14, List.of( + assertFeatures(13, List.of( Map.of( "_layer", "layername", "_minzoom", 12, @@ -134,7 +135,7 @@ public class FeatureCollectorTest { .inheritFromSource("key") .setMinPixelSize(1) .setMinPixelSizeBelowZoom(12, 10); - assertFeatures(14, List.of( + assertFeatures(13, List.of( Map.of( "_layer", "layername", "_minzoom", 12, @@ -152,5 +153,99 @@ public class FeatureCollectorTest { ), collector); } + @Test + public void testMinSizeAtMaxZoomDefaultsToTileResolution() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setMinPixelSize(1) + .setMinPixelSizeBelowZoom(12, 10); + assertEquals(10, poly.getMinPixelSize(12)); + assertEquals(1, poly.getMinPixelSize(13)); + assertEquals(256d / 4096, poly.getMinPixelSize(14)); + } + + @Test + public void testSetMinSizeAtMaxZoom() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setMinPixelSize(1) + .setMinPixelSizeAtMaxZoom(0.5) + .setMinPixelSizeBelowZoom(12, 10); + assertEquals(10, poly.getMinPixelSize(12)); + assertEquals(1, poly.getMinPixelSize(13)); + assertEquals(0.5, poly.getMinPixelSize(14)); + } + + @Test + public void testSetMinSizeAtAllZooms() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setMinPixelSizeAtAllZooms(2) + .setMinPixelSizeBelowZoom(12, 10); + assertEquals(10, poly.getMinPixelSize(12)); + assertEquals(2, poly.getMinPixelSize(13)); + assertEquals(2, poly.getMinPixelSize(14)); + } + + @Test + public void testDefaultMinPixelSize() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername"); + assertEquals(1, poly.getMinPixelSize(12)); + assertEquals(1, poly.getMinPixelSize(13)); + assertEquals(256d / 4096, poly.getMinPixelSize(14)); + } + + @Test + public void testToleranceDefault() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername"); + assertEquals(0.1, poly.getPixelTolerance(12)); + assertEquals(0.1, poly.getPixelTolerance(13)); + assertEquals(256d / 4096, poly.getPixelTolerance(14)); + } + + @Test + public void testSetTolerance() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setPixelTolerance(1); + assertEquals(1d, poly.getPixelTolerance(12)); + assertEquals(1d, poly.getPixelTolerance(13)); + assertEquals(256d / 4096, poly.getPixelTolerance(14)); + } + + @Test + public void testSetToleranceAtAllZooms() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setPixelToleranceAtAllZooms(1); + assertEquals(1d, poly.getPixelTolerance(12)); + assertEquals(1d, poly.getPixelTolerance(13)); + assertEquals(1d, poly.getPixelTolerance(14)); + } + + @Test + public void testSetMaxZoom() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setPixelToleranceAtMaxZoom(2); + assertEquals(0.1d, poly.getPixelTolerance(12)); + assertEquals(0.1d, poly.getPixelTolerance(13)); + assertEquals(2d, poly.getPixelTolerance(14)); + } + + @Test + public void testSetAllZoomMethods() { + var collector = factory.get(new ReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername") + .setPixelTolerance(1) + .setPixelToleranceAtMaxZoom(2) + .setPixelToleranceBelowZoom(12, 3); + assertEquals(3d, poly.getPixelTolerance(12)); + assertEquals(1d, poly.getPixelTolerance(13)); + assertEquals(2d, poly.getPixelTolerance(14)); + } + // TODO test shape coercion } diff --git a/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java index 250422dd..ecaaf7df 100644 --- a/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java +++ b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java @@ -433,6 +433,29 @@ public class FeatureRendererTest { ), renderGeometry(feature)); } + @Test + public void testDuplicatePointsRemovedAfterRounding() { + var eps = Z14_WIDTH / 4096; + var pixel = Z14_WIDTH / 256; + var feature = lineFeature(newLineString( + 0.5 + pixel * 10, 0.5 + pixel * 10, + 0.5 + pixel * 20, 0.5 + pixel * 10, + 0.5 + pixel * 20, 0.5 + pixel * 10 + eps / 3 + )) + .setMinPixelSize(1) + .setZoomRange(14, 14) + .setBufferPixels(0) + .setPixelToleranceAtAllZooms(0); + assertExactSameFeatures(Map.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + newLineString( + 10, 10, + 20, 10 + ) + ) + ), renderGeometry(feature)); + } + /* * POLYGON TESTS */